add custom authentication checks
This commit is contained in:
committed by
Daniel Hutzel
parent
3cf02cb567
commit
70b0c85346
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using {managed} from '@sap/cds/common';
|
||||||
|
|
||||||
namespace sap.capire.media.store;
|
namespace sap.capire.media.store;
|
||||||
|
|
||||||
aspect Named {
|
aspect Named {
|
||||||
@@ -17,7 +19,7 @@ aspect Person {
|
|||||||
phone : String(24);
|
phone : String(24);
|
||||||
fax : String(24);
|
fax : String(24);
|
||||||
email : String(60);
|
email : String(60);
|
||||||
password : String(111);
|
password : String(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
entity MediaTypes : Named {}
|
entity MediaTypes : Named {}
|
||||||
@@ -88,7 +90,7 @@ entity InvoiceItems {
|
|||||||
quantity : Integer default 1;
|
quantity : Integer default 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
entity Tracks {
|
entity Tracks : managed {
|
||||||
key ID : Integer;
|
key ID : Integer;
|
||||||
name : String(200);
|
name : String(200);
|
||||||
album : Association to Albums;
|
album : Association to Albums;
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sap/cds": "^4.2.8",
|
"@sap/cds": "^4.2.8",
|
||||||
|
"bcrypt": "^5.0.0",
|
||||||
"express": "^4",
|
"express": "^4",
|
||||||
|
"jsonwebtoken": "^8.5.1",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"passport": "^0.4.1"
|
"passport": "^0.4.1"
|
||||||
},
|
},
|
||||||
@@ -17,10 +19,10 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npx cds run",
|
"start": "npx cds run",
|
||||||
"deploy": "cds deploy --to sqlite:mychinook.db",
|
"deploy": "cds deploy --to sqlite:mychinook.db",
|
||||||
"rebuild": "npm run deploy && npm run start",
|
|
||||||
"test": "mocha test/media-service.test.js --verbose --timeout 10000"
|
"test": "mocha test/media-service.test.js --verbose --timeout 10000"
|
||||||
},
|
},
|
||||||
"cds": {
|
"cds": {
|
||||||
|
"ACCESS_TOKEN_SECRET": "secret",
|
||||||
"requires": {
|
"requires": {
|
||||||
"db": {
|
"db": {
|
||||||
"kind": "sqlite",
|
"kind": "sqlite",
|
||||||
@@ -30,30 +32,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"strategy": "mock",
|
"impl": "srv/auth.js"
|
||||||
"users": {
|
|
||||||
"andrew@chinookcorp.com": {
|
|
||||||
"password": "some",
|
|
||||||
"roles": [
|
|
||||||
"employee"
|
|
||||||
],
|
|
||||||
"userAttributes": {
|
|
||||||
"level": 1,
|
|
||||||
"ID": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"luisg@embraer.com.br": {
|
|
||||||
"password": "some",
|
|
||||||
"roles": [
|
|
||||||
"customer"
|
|
||||||
],
|
|
||||||
"userAttributes": {
|
|
||||||
"level": 0,
|
|
||||||
"ID": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"*": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,16 @@ cds.on("bootstrap", (app) => {
|
|||||||
});
|
});
|
||||||
// add your own middleware before any by cds are added
|
// add your own middleware before any by cds are added
|
||||||
});
|
});
|
||||||
cds.on("served", async ({ db }) => {
|
cds.on("served", async ({ db, messaging, ...servedServices }) => {
|
||||||
|
// import data from chinook db if needed
|
||||||
await importData(db);
|
await importData(db);
|
||||||
// add more middleware after all CDS servies
|
|
||||||
|
// add logging current user before any request
|
||||||
|
for (let i in servedServices) {
|
||||||
|
servedServices[i].prepend((srv) =>
|
||||||
|
srv.before("*", (req) => console.log("[USER]:", req.user))
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// delegate to default server.js:
|
|
||||||
module.exports = cds.server;
|
module.exports = cds.server;
|
||||||
|
|||||||
29
media-store/srv/auth.js
Normal file
29
media-store/srv/auth.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const cds = require("@sap/cds");
|
||||||
|
const jwt = require("jsonwebtoken");
|
||||||
|
|
||||||
|
const { ACCESS_TOKEN_SECRET } = cds.env;
|
||||||
|
class MyUser extends cds.User {
|
||||||
|
constructor(attr, roles, id) {
|
||||||
|
super({ attr, _roles: roles, id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = (req, res, next) => {
|
||||||
|
const { authorization: authHeader } = req.headers;
|
||||||
|
const token = authHeader && authHeader.split(" ")[1];
|
||||||
|
if (token === null) {
|
||||||
|
return res.sendStatus(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decodedUser = jwt.verify(token, ACCESS_TOKEN_SECRET);
|
||||||
|
req.user = new MyUser(
|
||||||
|
{ ID: decodedUser.ID },
|
||||||
|
decodedUser.roles,
|
||||||
|
decodedUser.email
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
} finally {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using {sap.capire.media.store as my} from '../db/schema';
|
using {sap.capire.media.store as my} from '../db/schema';
|
||||||
using {BrowseTracks.Tracks} from './browse-tracks-service';
|
using {BrowseTracks.Tracks} from './browse-tracks-service';
|
||||||
|
|
||||||
@(requires : 'customer')
|
|
||||||
service BrowseInvoices {
|
service BrowseInvoices @(requires : 'customer') {
|
||||||
@readonly
|
@readonly
|
||||||
entity Invoices as projection on my.Invoices;
|
entity Invoices as projection on my.Invoices;
|
||||||
|
|
||||||
@@ -13,6 +13,10 @@ service BrowseInvoices {
|
|||||||
|
|
||||||
action cancelInvoice(ID : Integer);
|
action cancelInvoice(ID : Integer);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Below entities exposed
|
||||||
|
due to 'navigation property errors' when expanding with odata
|
||||||
|
*/
|
||||||
@readonly
|
@readonly
|
||||||
entity Tracks as projection on my.Tracks excluding {
|
entity Tracks as projection on my.Tracks excluding {
|
||||||
alreadyOrdered
|
alreadyOrdered
|
||||||
|
|||||||
@@ -16,16 +16,11 @@ module.exports = async function () {
|
|||||||
const db = await cds.connect.to("db"); // connect to database service
|
const db = await cds.connect.to("db"); // connect to database service
|
||||||
const { Invoices, InvoiceItems } = db.entities;
|
const { Invoices, InvoiceItems } = db.entities;
|
||||||
|
|
||||||
this.before("*", (req) => {
|
// this.before("*", (req) => {
|
||||||
console.log(
|
// if (!req.user.is("customer")) {
|
||||||
"[USER]:",
|
// req.reject(403);
|
||||||
req.user.id,
|
// }
|
||||||
" [LEVEL]: ",
|
// });
|
||||||
req.user.attr.level,
|
|
||||||
"[ROLE]",
|
|
||||||
req.user.is("user") ? "user" : "other"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.on("READ", "Invoices", async (req) => {
|
this.on("READ", "Invoices", async (req) => {
|
||||||
return await db.run(req.query.where({ customer_ID: req.user.attr.ID }));
|
return await db.run(req.query.where({ customer_ID: req.user.attr.ID }));
|
||||||
|
|||||||
@@ -2,24 +2,36 @@ using {sap.capire.media.store as my} from '../db/schema';
|
|||||||
|
|
||||||
service BrowseTracks {
|
service BrowseTracks {
|
||||||
@readonly
|
@readonly
|
||||||
entity Tracks as projection on my.Tracks excluding {
|
entity Tracks as projection on my.Tracks excluding {
|
||||||
alreadyOrdered
|
alreadyOrdered
|
||||||
};
|
};
|
||||||
|
|
||||||
@(requires : 'authenticated-user')
|
|
||||||
@readonly
|
@readonly
|
||||||
entity MarkedTracks as projection on my.Tracks;
|
entity MarkedTracks @(restrict : [
|
||||||
|
{
|
||||||
|
grant : ['*', ],
|
||||||
|
to : 'customer'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
grant : '*',
|
||||||
|
to : 'employee'
|
||||||
|
},
|
||||||
|
]) as projection on my.Tracks;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Below entities exposed
|
||||||
|
due to 'navigation property errors' when expanding with odata
|
||||||
|
*/
|
||||||
@readonly
|
@readonly
|
||||||
entity Genres as projection on my.Genres {
|
entity Genres as projection on my.Genres {
|
||||||
* , tracks : redirected to Tracks
|
* , tracks : redirected to Tracks
|
||||||
};
|
};
|
||||||
|
|
||||||
@readonly
|
@readonly
|
||||||
entity Albums as projection on my.Albums {
|
entity Albums as projection on my.Albums {
|
||||||
* , tracks : redirected to Tracks
|
* , tracks : redirected to Tracks
|
||||||
};
|
};
|
||||||
|
|
||||||
@readonly
|
@readonly
|
||||||
entity Artists as projection on my.Artists;
|
entity Artists as projection on my.Artists;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,26 +6,21 @@ const selectTracksByEmail = (email) => `
|
|||||||
join sap_capire_media_store_Invoices invoices
|
join sap_capire_media_store_Invoices invoices
|
||||||
on tracks.ID = invoiceItems.track_ID
|
on tracks.ID = invoiceItems.track_ID
|
||||||
join sap_capire_media_store_InvoiceItems invoiceItems
|
join sap_capire_media_store_InvoiceItems invoiceItems
|
||||||
on (invoices.ID = invoiceItems.invoice_ID and invoices.status='2') or
|
on invoices.ID = invoiceItems.invoice_ID
|
||||||
(invoices.ID = invoiceItems.invoice_ID and invoices.status='1')
|
|
||||||
join sap_capire_media_store_Customers customers
|
join sap_capire_media_store_Customers customers
|
||||||
on customers.ID = invoices.customer_ID
|
on customers.ID = invoices.customer_ID
|
||||||
where customers.email='${email}'
|
where (customers.email='${email}' and invoices.status='2')
|
||||||
|
or (customers.email='${email}' and invoices.status='1')
|
||||||
`;
|
`;
|
||||||
|
|
||||||
module.exports = async function () {
|
module.exports = async function () {
|
||||||
const db = await cds.connect.to("db"); // connect to database service
|
const db = await cds.connect.to("db"); // connect to database service
|
||||||
|
|
||||||
this.before("*", (req) => {
|
// this.before("READ", "MarkedTracks", (req) => {
|
||||||
console.log(
|
// if (!req.user.is("customer")) {
|
||||||
"[USER]:",
|
// req.reject(403);
|
||||||
req.user.id,
|
// }
|
||||||
" [LEVEL]: ",
|
// });
|
||||||
req.user.attr.level,
|
|
||||||
"[ROLE]",
|
|
||||||
req.user.is("user") ? "user" : "other"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.on("READ", "MarkedTracks", async (req) => {
|
this.on("READ", "MarkedTracks", async (req) => {
|
||||||
const myTrackIds = (await db.run(selectTracksByEmail(req.user.id))).map(
|
const myTrackIds = (await db.run(selectTracksByEmail(req.user.id))).map(
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
using {sap.capire.media.store as my} from '../db/schema';
|
using {sap.capire.media.store as my} from '../db/schema';
|
||||||
|
|
||||||
@(requires : 'employee')
|
service ManageStore @(requires : 'employee') {
|
||||||
service ManageStore {
|
entity Tracks as projection on my.Tracks;
|
||||||
entity Tracks as projection on my.Tracks;
|
entity Albums as projection on my.Albums;
|
||||||
action addTrack(name : String(25), albumTitle : String(255), genreName : String(255), composer : String(255));
|
entity Artists as projection on my.Artists;
|
||||||
entity Albums as projection on my.Albums;
|
/*
|
||||||
entity Genres as projection on my.Genres;
|
Below entities exposed
|
||||||
|
due to errors when creating Tracks/Albums/Artists
|
||||||
|
*/
|
||||||
|
entity MediaTypes as projection on my.MediaTypes;
|
||||||
|
entity Genres as projection on my.Genres;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,24 +3,24 @@ const cds = require("@sap/cds");
|
|||||||
module.exports = async function () {
|
module.exports = async function () {
|
||||||
const db = await cds.connect.to("db"); // connect to database service
|
const db = await cds.connect.to("db"); // connect to database service
|
||||||
|
|
||||||
const { Genres, Albums } = db.entities;
|
const { Albums, Tracks, Artists } = db.entities;
|
||||||
|
|
||||||
this.before("*", (req) => {
|
this.before("CREATE", "Tracks", async (req) => {
|
||||||
console.log(
|
let { ID: lastTrackId } = await db.run(
|
||||||
"[USER]:",
|
SELECT.one(Tracks).columns("ID").orderBy({ ID: "desc" })
|
||||||
req.user.id,
|
|
||||||
" [LEVEL]: ",
|
|
||||||
req.user.attr.level,
|
|
||||||
"[ROLE]",
|
|
||||||
req.user.is("user") ? "user" : "other"
|
|
||||||
);
|
);
|
||||||
|
req.data = { ...req.data, ID: ++lastTrackId };
|
||||||
});
|
});
|
||||||
|
this.before("CREATE", "Artists", async (req) => {
|
||||||
this.on("addTrack", async (req) => {
|
let { ID: lastArtistId } = await db.run(
|
||||||
const { albumTitle, genreName, name: trackName, composer } = req.data;
|
SELECT.one(Artists).columns("ID").orderBy({ ID: "desc" })
|
||||||
|
);
|
||||||
const genre = await db.run(SELECT.one(Genres).where({ name: genreName }));
|
req.data = { ...req.data, ID: ++lastArtistId };
|
||||||
const album = await db.run(SELECT.one(Albums).where({ title: albumTitle }));
|
});
|
||||||
// todo impl
|
this.before("CREATE", "Albums", async (req) => {
|
||||||
|
let { ID: lastAlbumId } = await db.run(
|
||||||
|
SELECT.one(Albums).columns("ID").orderBy({ ID: "desc" })
|
||||||
|
);
|
||||||
|
req.data = { ...req.data, ID: ++lastAlbumId };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,17 +22,34 @@ service Users {
|
|||||||
email : String(60);
|
email : String(60);
|
||||||
}
|
}
|
||||||
|
|
||||||
@(requires : 'authenticated-user')
|
@(restrict : [
|
||||||
|
{
|
||||||
|
grant : '*',
|
||||||
|
to : 'customer'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
grant : '*',
|
||||||
|
to : 'employee'
|
||||||
|
},
|
||||||
|
])
|
||||||
action updatePerson(person : Person);
|
action updatePerson(person : Person);
|
||||||
|
|
||||||
@(requires : 'authenticated-user')
|
@(restrict : [
|
||||||
|
{
|
||||||
|
grant : '*',
|
||||||
|
to : 'customer'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
grant : '*',
|
||||||
|
to : 'employee'
|
||||||
|
},
|
||||||
|
])
|
||||||
function getPerson() returns Person;
|
function getPerson() returns Person;
|
||||||
|
|
||||||
function mockLogin(email : String(111), password : String(200)) returns {
|
action login(email : String(111), password : String(200)) returns {
|
||||||
roles : array of String(111);
|
roles : array of String(111);
|
||||||
level : Integer;
|
token : String(500);
|
||||||
mockedToken : String(500);
|
email : String(500);
|
||||||
email : my.Person.email;
|
ID : Integer;
|
||||||
ID : my.Person.ID
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
const cds = require("@sap/cds");
|
const cds = require("@sap/cds");
|
||||||
|
const jwt = require("jsonwebtoken");
|
||||||
|
const bcrypt = require("bcrypt");
|
||||||
|
|
||||||
const USER_LEVELS = { customer: 1, employee: 2 };
|
const { ACCESS_TOKEN_SECRET } = cds.env;
|
||||||
|
const ACCESS_TOKEN_EXP_IN = "10m";
|
||||||
|
|
||||||
module.exports = async function () {
|
module.exports = async function () {
|
||||||
const db = await cds.connect.to("db");
|
const db = await cds.connect.to("db");
|
||||||
@@ -8,17 +11,6 @@ module.exports = async function () {
|
|||||||
|
|
||||||
const getUserEntity = (isCustomer) => (isCustomer ? Customers : Employees);
|
const getUserEntity = (isCustomer) => (isCustomer ? Customers : Employees);
|
||||||
|
|
||||||
this.before("*", (req) => {
|
|
||||||
console.log(
|
|
||||||
"[USER]:",
|
|
||||||
req.user.id,
|
|
||||||
" [LEVEL]: ",
|
|
||||||
req.user.attr.level,
|
|
||||||
"[ROLE]",
|
|
||||||
req.user.is("user") ? "user" : "other"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.on("updatePerson", async (req) => {
|
this.on("updatePerson", async (req) => {
|
||||||
await UPDATE(
|
await UPDATE(
|
||||||
getUserEntity(req.user && req.user._roles && req.user.is("customer"))
|
getUserEntity(req.user && req.user._roles && req.user.is("customer"))
|
||||||
@@ -48,25 +40,37 @@ module.exports = async function () {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.on("mockLogin", async (req) => {
|
this.on("login", async (req) => {
|
||||||
const { email, password } = req.data;
|
const { email, password } = req.data;
|
||||||
|
|
||||||
let userFromDb = await db.run(SELECT.one(Employees).where({ email }));
|
let userFromDb = await db.run(SELECT.one(Employees).where({ email }));
|
||||||
let role = "employee";
|
let roles = ["employee"];
|
||||||
if (!userFromDb) {
|
if (!userFromDb) {
|
||||||
userFromDb = await db.run(SELECT.one(Customers).where({ email }));
|
userFromDb = await db.run(SELECT.one(Customers).where({ email }));
|
||||||
role = "customer";
|
roles = ["customer"];
|
||||||
}
|
}
|
||||||
if (!userFromDb || password !== userFromDb.password) {
|
|
||||||
|
const userEqualPassword = await bcrypt.compare(
|
||||||
|
password,
|
||||||
|
userFromDb.password
|
||||||
|
);
|
||||||
|
if (!userEqualPassword) {
|
||||||
req.reject(401);
|
req.reject(401);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ email, ID: userFromDb.ID, roles },
|
||||||
|
ACCESS_TOKEN_SECRET,
|
||||||
|
{
|
||||||
|
expiresIn: ACCESS_TOKEN_EXP_IN,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mockedToken: Buffer.from(`${email}:${password}`).toString("base64"),
|
token,
|
||||||
level: USER_LEVELS[role],
|
roles,
|
||||||
email: userFromDb.email,
|
email: userFromDb.email,
|
||||||
ID: userFromDb.ID,
|
ID: userFromDb.ID,
|
||||||
roles: [role],
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
const { resolve } = require("@sap/cds");
|
||||||
const cds = require("@sap/cds");
|
const cds = require("@sap/cds");
|
||||||
|
const bcrypt = require("bcrypt");
|
||||||
|
const saltRounds = 10;
|
||||||
|
|
||||||
const FIRST_INDEX = 0;
|
const FIRST_INDEX = 0;
|
||||||
const ZERO_VALUE = 0;
|
const ZERO_VALUE = 0;
|
||||||
@@ -74,6 +77,16 @@ async function importData(targetDb) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await new Promise((resolve, reject) =>
|
||||||
|
bcrypt.hash("some", saltRounds, (error, hash) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
resolve(hash);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
console.log("hashedPassword", hashedPassword);
|
||||||
|
|
||||||
for (index in targetCSNEntities) {
|
for (index in targetCSNEntities) {
|
||||||
const targetEntityName = targetCSNEntitiesNames[index];
|
const targetEntityName = targetCSNEntitiesNames[index];
|
||||||
console.log(`[LOG]: Processing ${targetEntityName}`);
|
console.log(`[LOG]: Processing ${targetEntityName}`);
|
||||||
@@ -110,7 +123,7 @@ async function importData(targetDb) {
|
|||||||
columns.push("password");
|
columns.push("password");
|
||||||
srcResultRows = srcResultRows.map((row) => ({
|
srcResultRows = srcResultRows.map((row) => ({
|
||||||
...row,
|
...row,
|
||||||
password: "some",
|
password: hashedPassword,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user