diff --git a/media-store/.cdsrc.json b/media-store/.cdsrc.json deleted file mode 100644 index 0967ef42..00000000 --- a/media-store/.cdsrc.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/media-store/db/schema.cds b/media-store/db/schema.cds index aa86bf4d..3b4cfb08 100644 --- a/media-store/db/schema.cds +++ b/media-store/db/schema.cds @@ -1,3 +1,5 @@ +using {managed} from '@sap/cds/common'; + namespace sap.capire.media.store; aspect Named { @@ -17,7 +19,7 @@ aspect Person { phone : String(24); fax : String(24); email : String(60); - password : String(111); + password : String(500); } entity MediaTypes : Named {} @@ -88,7 +90,7 @@ entity InvoiceItems { quantity : Integer default 1; } -entity Tracks { +entity Tracks : managed { key ID : Integer; name : String(200); album : Association to Albums; diff --git a/media-store/package.json b/media-store/package.json index e71a80ab..ea2ef4f1 100644 --- a/media-store/package.json +++ b/media-store/package.json @@ -7,7 +7,9 @@ "private": true, "dependencies": { "@sap/cds": "^4.2.8", + "bcrypt": "^5.0.0", "express": "^4", + "jsonwebtoken": "^8.5.1", "moment": "^2.29.1", "passport": "^0.4.1" }, @@ -17,10 +19,10 @@ "scripts": { "start": "npx cds run", "deploy": "cds deploy --to sqlite:mychinook.db", - "rebuild": "npm run deploy && npm run start", "test": "mocha test/media-service.test.js --verbose --timeout 10000" }, "cds": { + "ACCESS_TOKEN_SECRET": "secret", "requires": { "db": { "kind": "sqlite", @@ -30,30 +32,7 @@ } }, "auth": { - "strategy": "mock", - "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 - } + "impl": "srv/auth.js" } } } diff --git a/media-store/server.js b/media-store/server.js index ac26c75f..88235cba 100644 --- a/media-store/server.js +++ b/media-store/server.js @@ -25,9 +25,16 @@ cds.on("bootstrap", (app) => { }); // 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); - // 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; diff --git a/media-store/srv/auth.js b/media-store/srv/auth.js new file mode 100644 index 00000000..3ab95e32 --- /dev/null +++ b/media-store/srv/auth.js @@ -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(); + } +}; diff --git a/media-store/srv/browse-invoices-service.cds b/media-store/srv/browse-invoices-service.cds index c8d1532c..56bf1f94 100644 --- a/media-store/srv/browse-invoices-service.cds +++ b/media-store/srv/browse-invoices-service.cds @@ -1,8 +1,8 @@ using {sap.capire.media.store as my} from '../db/schema'; using {BrowseTracks.Tracks} from './browse-tracks-service'; -@(requires : 'customer') -service BrowseInvoices { + +service BrowseInvoices @(requires : 'customer') { @readonly entity Invoices as projection on my.Invoices; @@ -13,6 +13,10 @@ service BrowseInvoices { action cancelInvoice(ID : Integer); + /* + Below entities exposed + due to 'navigation property errors' when expanding with odata + */ @readonly entity Tracks as projection on my.Tracks excluding { alreadyOrdered diff --git a/media-store/srv/browse-invoices-service.js b/media-store/srv/browse-invoices-service.js index 5f825c16..f9478ac5 100644 --- a/media-store/srv/browse-invoices-service.js +++ b/media-store/srv/browse-invoices-service.js @@ -16,16 +16,11 @@ module.exports = async function () { const db = await cds.connect.to("db"); // connect to database service const { Invoices, InvoiceItems } = db.entities; - this.before("*", (req) => { - console.log( - "[USER]:", - req.user.id, - " [LEVEL]: ", - req.user.attr.level, - "[ROLE]", - req.user.is("user") ? "user" : "other" - ); - }); + // this.before("*", (req) => { + // if (!req.user.is("customer")) { + // req.reject(403); + // } + // }); this.on("READ", "Invoices", async (req) => { return await db.run(req.query.where({ customer_ID: req.user.attr.ID })); diff --git a/media-store/srv/browse-tracks-service.cds b/media-store/srv/browse-tracks-service.cds index a13dc6e6..e51c55f4 100644 --- a/media-store/srv/browse-tracks-service.cds +++ b/media-store/srv/browse-tracks-service.cds @@ -2,24 +2,36 @@ using {sap.capire.media.store as my} from '../db/schema'; service BrowseTracks { @readonly - entity Tracks as projection on my.Tracks excluding { + entity Tracks as projection on my.Tracks excluding { alreadyOrdered }; - @(requires : 'authenticated-user') @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 - entity Genres as projection on my.Genres { + entity Genres as projection on my.Genres { * , tracks : redirected to Tracks }; @readonly - entity Albums as projection on my.Albums { + entity Albums as projection on my.Albums { * , tracks : redirected to Tracks }; @readonly - entity Artists as projection on my.Artists; + entity Artists as projection on my.Artists; } diff --git a/media-store/srv/browse-tracks-service.js b/media-store/srv/browse-tracks-service.js index 7ed17075..b864519c 100644 --- a/media-store/srv/browse-tracks-service.js +++ b/media-store/srv/browse-tracks-service.js @@ -6,26 +6,21 @@ const selectTracksByEmail = (email) => ` join sap_capire_media_store_Invoices invoices on tracks.ID = invoiceItems.track_ID join sap_capire_media_store_InvoiceItems invoiceItems - on (invoices.ID = invoiceItems.invoice_ID and invoices.status='2') or - (invoices.ID = invoiceItems.invoice_ID and invoices.status='1') + on invoices.ID = invoiceItems.invoice_ID join sap_capire_media_store_Customers customers 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 () { const db = await cds.connect.to("db"); // connect to database service - this.before("*", (req) => { - console.log( - "[USER]:", - req.user.id, - " [LEVEL]: ", - req.user.attr.level, - "[ROLE]", - req.user.is("user") ? "user" : "other" - ); - }); + // this.before("READ", "MarkedTracks", (req) => { + // if (!req.user.is("customer")) { + // req.reject(403); + // } + // }); this.on("READ", "MarkedTracks", async (req) => { const myTrackIds = (await db.run(selectTracksByEmail(req.user.id))).map( diff --git a/media-store/srv/manage-store-service.cds b/media-store/srv/manage-store-service.cds index b467b017..7528693f 100644 --- a/media-store/srv/manage-store-service.cds +++ b/media-store/srv/manage-store-service.cds @@ -1,9 +1,13 @@ using {sap.capire.media.store as my} from '../db/schema'; -@(requires : 'employee') -service ManageStore { - entity Tracks as projection on my.Tracks; - action addTrack(name : String(25), albumTitle : String(255), genreName : String(255), composer : String(255)); - entity Albums as projection on my.Albums; - entity Genres as projection on my.Genres; +service ManageStore @(requires : 'employee') { + entity Tracks as projection on my.Tracks; + entity Albums as projection on my.Albums; + entity Artists as projection on my.Artists; + /* + 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; } diff --git a/media-store/srv/manage-store-service.js b/media-store/srv/manage-store-service.js index 303ed726..d6675f93 100644 --- a/media-store/srv/manage-store-service.js +++ b/media-store/srv/manage-store-service.js @@ -3,24 +3,24 @@ const cds = require("@sap/cds"); module.exports = async function () { 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) => { - console.log( - "[USER]:", - req.user.id, - " [LEVEL]: ", - req.user.attr.level, - "[ROLE]", - req.user.is("user") ? "user" : "other" + this.before("CREATE", "Tracks", async (req) => { + let { ID: lastTrackId } = await db.run( + SELECT.one(Tracks).columns("ID").orderBy({ ID: "desc" }) ); + req.data = { ...req.data, ID: ++lastTrackId }; }); - - this.on("addTrack", async (req) => { - const { albumTitle, genreName, name: trackName, composer } = req.data; - - const genre = await db.run(SELECT.one(Genres).where({ name: genreName })); - const album = await db.run(SELECT.one(Albums).where({ title: albumTitle })); - // todo impl + this.before("CREATE", "Artists", async (req) => { + let { ID: lastArtistId } = await db.run( + SELECT.one(Artists).columns("ID").orderBy({ ID: "desc" }) + ); + req.data = { ...req.data, ID: ++lastArtistId }; + }); + 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 }; }); }; diff --git a/media-store/srv/user-service.cds b/media-store/srv/user-service.cds index ba56c24b..3f1f70c0 100644 --- a/media-store/srv/user-service.cds +++ b/media-store/srv/user-service.cds @@ -22,17 +22,34 @@ service Users { email : String(60); } - @(requires : 'authenticated-user') + @(restrict : [ + { + grant : '*', + to : 'customer' + }, + { + grant : '*', + to : 'employee' + }, + ]) action updatePerson(person : Person); - @(requires : 'authenticated-user') + @(restrict : [ + { + grant : '*', + to : 'customer' + }, + { + grant : '*', + to : 'employee' + }, + ]) function getPerson() returns Person; - function mockLogin(email : String(111), password : String(200)) returns { - roles : array of String(111); - level : Integer; - mockedToken : String(500); - email : my.Person.email; - ID : my.Person.ID + action login(email : String(111), password : String(200)) returns { + roles : array of String(111); + token : String(500); + email : String(500); + ID : Integer; }; } diff --git a/media-store/srv/user-service.js b/media-store/srv/user-service.js index d488ace9..9993b305 100644 --- a/media-store/srv/user-service.js +++ b/media-store/srv/user-service.js @@ -1,6 +1,9 @@ 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 () { const db = await cds.connect.to("db"); @@ -8,17 +11,6 @@ module.exports = async function () { 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) => { await UPDATE( 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; let userFromDb = await db.run(SELECT.one(Employees).where({ email })); - let role = "employee"; + let roles = ["employee"]; if (!userFromDb) { 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); } + const token = jwt.sign( + { email, ID: userFromDb.ID, roles }, + ACCESS_TOKEN_SECRET, + { + expiresIn: ACCESS_TOKEN_EXP_IN, + } + ); + return { - mockedToken: Buffer.from(`${email}:${password}`).toString("base64"), - level: USER_LEVELS[role], + token, + roles, email: userFromDb.email, ID: userFromDb.ID, - roles: [role], }; }); }; diff --git a/media-store/util/importData.js b/media-store/util/importData.js index 62159a45..1725fbed 100644 --- a/media-store/util/importData.js +++ b/media-store/util/importData.js @@ -1,4 +1,7 @@ +const { resolve } = require("@sap/cds"); const cds = require("@sap/cds"); +const bcrypt = require("bcrypt"); +const saltRounds = 10; const FIRST_INDEX = 0; const ZERO_VALUE = 0; @@ -74,6 +77,16 @@ async function importData(targetDb) { 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) { const targetEntityName = targetCSNEntitiesNames[index]; console.log(`[LOG]: Processing ${targetEntityName}`); @@ -110,7 +123,7 @@ async function importData(targetDb) { columns.push("password"); srcResultRows = srcResultRows.map((row) => ({ ...row, - password: "some", + password: hashedPassword, })); }