Moved to chinook + added .env

This commit is contained in:
Daniel
2020-12-16 10:58:53 +01:00
committed by Daniel Hutzel
parent 5cec82fa00
commit 69e510a407
136 changed files with 6 additions and 5 deletions

28
chinook/srv/auth.js Normal file
View File

@@ -0,0 +1,28 @@
const cds = require("@sap/cds");
const jwt = require("jsonwebtoken");
const { ACCESS_TOKEN_SECRET } = process.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];
try {
const decodedUser = jwt.verify(token, ACCESS_TOKEN_SECRET);
req.user = new MyUser(
{ ID: decodedUser.ID },
[decodedUser.roles, "authenticated-user"],
decodedUser.email
);
} catch (error) {
req.user = new cds.User();
} finally {
next();
}
};

View File

@@ -0,0 +1,40 @@
using {sap.capire.media.store as my} from '../db/schema';
using {BrowseTracks.Tracks} from './browse-tracks-service';
service BrowseInvoices @(requires : 'customer') {
/**
* Invoices entity also restricted programmatically Only owned
* invoices youser can access
*/
@readonly
entity Invoices as projection on my.Invoices;
action invoice(tracks : array of {
ID : Integer;
});
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
};
@readonly
entity Genres as projection on my.Genres {
* , tracks : redirected to Tracks
};
@readonly
entity Albums as projection on my.Albums {
* , tracks : redirected to Tracks
};
@readonly
entity Artists as projection on my.Artists;
}

View File

@@ -0,0 +1,124 @@
const cds = require("@sap/cds");
const moment = require("moment");
const LEVERAGE_DURATION = 1; // in hours. should be the same in the frontend
const CANCEL_STATUS = -1;
const SHIPPED_STATUS = 1;
const UTC_DATE_TIME_FORMAT = "YYYY-MM-DDThh:mm:ss";
function roundNumber(num) {
return Math.round((num + Number.EPSILON) * 100) / 100;
}
// the same function there is in the frontend
const isLeverageTimeExpired = (utcNowTimestamp, invoiceDate) => {
const duration = moment.duration(
moment(utcNowTimestamp).diff(moment(invoiceDate).valueOf())
);
return duration.asHours() > LEVERAGE_DURATION;
};
module.exports = async function () {
const db = await cds.connect.to("db"); // connect to database service
const { Invoices, InvoiceItems, Tracks } = db.entities;
this.on("READ", "Invoices", async (req) => {
return await db.run(req.query.where({ customer_ID: req.user.attr.ID }));
});
this.on("invoice", async (req) => {
const { tracks } = req.data;
const newInvoicedTrackIds = tracks.map(({ ID }) => ID);
const customerId = req.user.attr.ID;
const utcNowDateTime = moment().utc().format(UTC_DATE_TIME_FORMAT);
const transaction = await db.tx(req);
// check if already exists
const invoicedTracks = await transaction.run(
SELECT.from(InvoiceItems)
.columns("track_ID")
.where(
"invoice_ID in",
SELECT("ID").from(Invoices).where({
customer_ID: req.user.attr.ID,
status: SHIPPED_STATUS,
})
)
);
const isInValidInvoice = invoicedTracks.some(({ track_ID: curID }) => {
return newInvoicedTrackIds.includes(curID);
});
if (isInValidInvoice) {
req.reject(400, "Invoice contains already owned values");
}
const newInvoicedTracks = await transaction.run(
SELECT("ID", "unitPrice").from(Tracks).where({ ID: newInvoicedTrackIds })
);
const total = newInvoicedTracks.reduce(
(acc, { unitPrice }) => acc + roundNumber(Number(unitPrice)),
0
);
// getting last ids for new records
let { ID: lastInvoiceId } = await transaction.run(
SELECT.one(Invoices).columns("ID").orderBy({ ID: "desc" })
);
let { ID: lastInvoiceItemId } = await transaction.run(
SELECT.one(InvoiceItems).columns("ID").orderBy({ ID: "desc" })
);
// creating invoice
const newInvoiceId = ++lastInvoiceId;
await transaction.run(
INSERT.into(Invoices)
.columns("ID", "customer_ID", "total", "invoiceDate")
.values(newInvoiceId, customerId, total, utcNowDateTime)
);
// creating invoice items
await transaction.run(
INSERT.into(InvoiceItems)
.columns("ID", "invoice_ID", "track_ID", "unitPrice")
.rows(
newInvoicedTracks.map(({ ID: trackID, unitPrice }, index) => [
lastInvoiceItemId + index + 1,
newInvoiceId,
trackID,
unitPrice,
])
)
);
await transaction.commit();
});
this.on("cancelInvoice", async (req) => {
const { ID } = req.data;
const currentInvoice = await db.run(
SELECT.one(Invoices)
.where({
ID,
customer_ID: req.user.attr.ID,
})
.columns("ID", "invoiceDate", "customer_ID")
);
if (!currentInvoice) {
req.reject(
404,
"Seems like you are not owning this invoice or it is not exists"
);
}
const utcNowTimestamp = moment(
moment().utc().format(UTC_DATE_TIME_FORMAT)
).valueOf();
if (isLeverageTimeExpired(utcNowTimestamp, currentInvoice.invoiceDate)) {
req.reject(400, "Leverage time was expired");
}
return await db.run(
UPDATE(Invoices).set({ status: CANCEL_STATUS }).where({ ID })
);
});
};

View File

@@ -0,0 +1,31 @@
using {sap.capire.media.store as my} from '../db/schema';
service BrowseTracks {
@readonly
entity Tracks as projection on my.Tracks excluding {
alreadyOrdered
};
@readonly
entity MarkedTracks @(restrict : [{
grant : ['*'],
to : 'customer'
}]) 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 {
* , tracks : redirected to Tracks
};
@readonly
entity Albums as projection on my.Albums {
* , tracks : redirected to Tracks
};
@readonly
entity Artists as projection on my.Artists;
}

View File

@@ -0,0 +1,33 @@
const cds = require("@sap/cds");
const SHIPPED_STATUS = 1;
module.exports = async function () {
const db = await cds.connect.to("db"); // connect to database service
const { Invoices, InvoiceItems } = db.entities;
this.on("READ", "MarkedTracks", async (req) => {
const invoiceItemEntries = await db.run(
SELECT.from(InvoiceItems)
.columns("track_ID")
.where(
"invoice_ID in",
SELECT("ID").from(Invoices).where({
customer_ID: req.user.attr.ID,
status: SHIPPED_STATUS,
})
)
);
const trackIds = invoiceItemEntries.map(({ track_ID }) => track_ID);
const result = [];
await db.foreach(req.query, (track) => {
result.push({
...track,
alreadyOrdered: trackIds.includes(track.ID),
});
});
return result;
});
};

View File

@@ -0,0 +1,12 @@
using {sap.capire.media.store as my} from '../db/schema';
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 Genres as projection on my.Genres;
}

View File

@@ -0,0 +1,24 @@
const cds = require("@sap/cds");
module.exports = async function () {
const db = await cds.connect.to("db"); // connect to database service
this.on("CREATE", "*", async (req) => {
const selectLastQuery = SELECT.one(req.entity).orderBy({ ID: "desc" });
const transaction = await db.tx(req);
let { ID: lastEntityID } = await transaction.run(selectLastQuery);
const columns = ["ID", ...Object.keys(req.data)];
const values = [++lastEntityID, ...Object.values(req.data)];
const insertQuery = INSERT.into(req.entity).columns(columns).values(values);
await transaction.run(insertQuery);
const result = await transaction.run(selectLastQuery);
await transaction.commit();
return result;
});
};

View File

@@ -0,0 +1,47 @@
using {sap.capire.media.store as my} from '../db/schema';
service Users {
/**
* Below entities also restricted programmatically. Only User
* can only access to yours record
*/
entity Customers as projection on my.Customers excluding {
password,
supportRep
};
entity Employees as projection on my.Employees excluding {
password,
reportsTo,
title,
birthDate,
hireDate
};
type AuthData {
accessToken : String(500);
refreshToken : String(500);
ID : Integer;
email : String(500);
roles : array of String(111);
};
action login(email : String(111), password : String(200)) returns AuthData;
action refreshTokens(refreshToken : String(500)) returns AuthData;
}
annotate Users.Customers with @(restrict : [{
grant : [
'READ',
'UPDATE'
],
to : 'customer'
}]);
annotate Users.Employees with @(restrict : [{
grant : [
'READ',
'UPDATE'
],
to : 'employee'
}]);

113
chinook/srv/user-service.js Normal file
View File

@@ -0,0 +1,113 @@
const cds = require("@sap/cds");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");
const { ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET } = process.env;
const ACCESS_TOKEN_EXP_IN = "10m";
const REFRESH_TOKEN_EXPIRES_IN = "20m";
const comparePasswords = async (password, hashedPassword) => {
return new Promise((resolve, reject) =>
bcrypt.compare(password, hashedPassword, (err, res) => {
if (err || res === false) {
reject(err);
} else {
resolve(res);
}
})
);
};
const createTokens = (email, ID, roles) => {
const accessToken = jwt.sign({ email, ID, roles }, ACCESS_TOKEN_SECRET, {
expiresIn: ACCESS_TOKEN_EXP_IN,
});
const refreshToken = jwt.sign({ email, ID, roles }, REFRESH_TOKEN_SECRET, {
expiresIn: REFRESH_TOKEN_EXPIRES_IN,
});
return [accessToken, refreshToken];
};
module.exports = async function () {
const db = await cds.connect.to("db");
const { Employees, Customers } = db.entities;
async function getUser(email) {
let userFromDb = await db.run(SELECT.one(Employees).where({ email }));
let roles = ["employee"];
if (!userFromDb) {
userFromDb = await db.run(SELECT.one(Customers).where({ email }));
roles = ["customer"];
}
return Object.assign({}, userFromDb, { roles });
}
/**
* User can only update and read his data
*/
this.before("UPDATE", "*", async (req) => {
req.query = req.query.where({ ID: req.user.attr.ID });
});
this.before("READ", "*", async (req) => {
req.query = req.query.where({ ID: req.user.attr.ID });
});
this.on("login", async (req) => {
const { email, password } = req.data;
const userFromDb = await getUser(email);
if (!userFromDb) {
req.reject(401);
}
try {
await comparePasswords(password, userFromDb.password);
} catch (error) {
req.reject(401);
}
const [accessToken, refreshToken] = createTokens(
userFromDb.email,
userFromDb.ID,
userFromDb.roles
);
return {
accessToken,
refreshToken,
ID: userFromDb.ID,
email: userFromDb.email,
roles: userFromDb.roles,
};
});
this.on("refreshTokens", async (req) => {
let decodedUser;
try {
const { refreshToken } = req.data;
decodedUser = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET);
} catch (error) {
req.reject(401);
}
const userFromDb = await getUser(decodedUser.email);
if (!userFromDb) {
req.reject(401);
}
const [accessToken, refreshToken] = createTokens(
userFromDb.email,
userFromDb.ID,
userFromDb.roles
);
return {
accessToken,
refreshToken,
ID: userFromDb.ID,
email: userFromDb.email,
roles: userFromDb.roles,
};
});
};