From eacc18e461307150cc68150db00dc1dfa2e26a2c Mon Sep 17 00:00:00 2001 From: William Brawner Date: Fri, 25 Jun 2021 19:01:54 -0600 Subject: [PATCH] Finish implementing /api/budget routes Signed-off-by: William Brawner --- src/client/app/app.component.ts | 2 +- .../add-edit-budget.component.ts | 4 +- src/client/app/shared/twigs.http.service.ts | 4 + src/server/budget/controller.ts | 164 +++++++++++++++--- src/server/budget/model.ts | 6 + src/server/categories/controller.ts | 10 +- src/server/index.ts | 4 +- src/server/permissions/permission.ts | 25 +++ src/server/transactions/controller.ts | 10 +- src/server/types/express/index.d.ts | 2 + src/server/utils.ts | 59 +++++++ 11 files changed, 256 insertions(+), 34 deletions(-) create mode 100644 src/server/permissions/permission.ts diff --git a/src/client/app/app.component.ts b/src/client/app/app.component.ts index cb29f19..720cb56 100644 --- a/src/client/app/app.component.ts +++ b/src/client/app/app.component.ts @@ -5,7 +5,7 @@ import { TWIGS_SERVICE, TwigsService } from './shared/twigs.service'; import { SwUpdate } from '@angular/service-worker'; import { first, filter, map } from 'rxjs/operators'; import { interval, concat, BehaviorSubject } from 'rxjs'; -import { Router, ActivationEnd, ActivatedRoute } from '@angular/router'; +import { Router } from '@angular/router'; import { Actionable, isActionable } from './shared/actionable'; @Component({ diff --git a/src/client/app/budgets/add-edit-budget/add-edit-budget.component.ts b/src/client/app/budgets/add-edit-budget/add-edit-budget.component.ts index a83566d..24039f4 100644 --- a/src/client/app/budgets/add-edit-budget/add-edit-budget.component.ts +++ b/src/client/app/budgets/add-edit-budget/add-edit-budget.component.ts @@ -3,6 +3,7 @@ import { Budget } from '../budget'; import { AppComponent } from 'src/client/app/app.component'; import { User, UserPermission, Permission } from 'src/client/app/users/user'; import { TWIGS_SERVICE, TwigsService } from 'src/client/app/shared/twigs.service'; +import { Router } from '@angular/router'; @Component({ selector: 'app-add-edit-budget', @@ -20,6 +21,7 @@ export class AddEditBudgetComponent { constructor( private app: AppComponent, @Inject(TWIGS_SERVICE) private twigsService: TwigsService, + private router: Router ) { this.app.setTitle(this.title) this.app.setBackEnabled(true); @@ -51,7 +53,7 @@ export class AddEditBudgetComponent { this.isLoading = true; this.twigsService.deleteBudget(this.budget.id) .subscribe(() => { - this.app.goBack(); + this.router.navigateByUrl('/budgets'); }); } diff --git a/src/client/app/shared/twigs.http.service.ts b/src/client/app/shared/twigs.http.service.ts index c2d0c65..09aa25b 100644 --- a/src/client/app/shared/twigs.http.service.ts +++ b/src/client/app/shared/twigs.http.service.ts @@ -128,12 +128,16 @@ export class TwigsHttpService implements TwigsService { 'id': id, 'name': name, 'description': description, +<<<<<<< HEAD 'users': users.map(userPermission => { return { user: userPermission.user, permission: Permission[userPermission.permission] }; }) +======= + 'users': users +>>>>>>> 4488aff (Finish implementing /api/budget routes) }; return this.http.post(this.apiUrl + '/budgets', params, this.options) .pipe(map(budget => { diff --git a/src/server/budget/controller.ts b/src/server/budget/controller.ts index a906d83..db6e95b 100644 --- a/src/server/budget/controller.ts +++ b/src/server/budget/controller.ts @@ -1,29 +1,153 @@ import express from 'express'; +import { Permission } from '../permissions/permission'; +import sqlite3 from 'sqlite3'; +import { authMiddleware, budgetPermissionMiddleware, firstOfMonth, lastOfMonth, randomId } from '../utils'; +import { Budget } from './model'; -const router = express.Router() +export default function budgetRouter(db: sqlite3.Database): express.Router { + const router = express.Router() -router.get('/', (req, res) => { - -}); + router.get('/', authMiddleware(db), (req, res) => { + db.prepare('SELECT * FROM budget WHERE id IN (SELECT budget_id FROM user_permission WHERE user_id = ?)') + .all(req.user.id, (err?: Error, rows?: any) => { + if (err) { + console.error(err) + res.status(500).send("Failed to get budgets") + return; + } + console.log(rows); + res.send(rows || []); + }) + .finalize(); + }); -router.post('/', (req, res) => { + router.post('/', authMiddleware(db), (req, res) => { + const id = (req.body.id && req.body.id.length == 32) ? req.body.id : randomId(); + let budget = new Budget(id, req.body.name, req.body.description); + const users = req.body.users; + db.prepare('INSERT INTO budget (id, name, description) VALUES (?, ?, ?);') + .run([budget.id, budget.name, budget.description], function (err?: Error) { + if (err) { + res.status(400).send(err); + return; + } + db.serialize(function () { + let budgetUsers: any[] = []; + const query = db.prepare('INSERT INTO user_permission (budget_id, user_id, permission) values (?, ?, ?);'); + for (let i = 0; i < users.length; i++) { + const user = users[i]; + query.run([id, user['user'], user['permission']], function (err?: Error) { + if (!err) { + budgetUsers.push(user); + console.log("Added budget user") + } else { + console.error("Unable to add user to budget", err); + } + if (i == users.length - 1) { + res.send({ ...budget, users: budgetUsers }); + console.log("Sent budget response") + } + }); + } + query.finalize(); + }); + }) + .finalize(); + }); -}); + router.get( + '/:id', + authMiddleware(db), + (req, res, next) => budgetPermissionMiddleware(db, req.params['id'], Permission.READ)(req, res, next), + (req, res) => { + db.prepare('SELECT id, username, user_permission.permission FROM user INNER JOIN user_permission ON id = user_permission.user_id WHERE id IN (SELECT user_id FROM user_permission WHERE budget_id = ?) AND user_permission.budget_id = ?') + .all([req.budget.id, req.budget.id], function (err?: Error, rows?: any[]) { + if (err) { + res.status(500).send(`Failed to fetch users for budget ${req.budget.id}`) + return; + } + res.send({ ...req.budget, users: rows }); + }); + } + ); -router.get('/:id', (req, res) => { + router.get( + '/:id/balance', + authMiddleware(db), + (req, res, next) => budgetPermissionMiddleware(db, req.params['id'], Permission.READ)(req, res, next), + (req, res) => { + const from = req.query['from'] || firstOfMonth(); + const to = req.query['to'] || lastOfMonth(); + db.prepare(`SELECT ( + COALESCE( + ( + SELECT SUM(amount) from \`transaction\` WHERE budget_id = ? AND expense = 0 AND date >= ? AND date <= ? + ), + 0 + ) + ) - ( + COALESCE( + ( + SELECT SUM(amount) from \`transaction\` WHERE budget_id = ? AND expense = 1 AND date >= ? AND date <= ? + ), + 0 + ) + );`).get([req.budget.id, from, to, req.budget.id, from, to], function (err?: Error, row?: any) { + if (err) { + console.error('Failed to load budget balance', err); + res.status(500).send(`Failed to load budget balance for budget ${req.budget.id}`) + return; + } + res.send(`${Object.values(row)[0] || 0}`); + }).finalize(); + } + ); -}); + router.put( + '/:id', + authMiddleware(db), + (req, res, next) => budgetPermissionMiddleware(db, req.params['id'], Permission.MANAGE)(req, res, next), + (req, res) => { + console.log('Reached update') + const name = req.body.name || req.budget.name; + const description = req.body.description || req.budget.description; + // TODO: Allow changing user permissions + // let users = req.body.users; + db.prepare('UPDATE budget SET name = ?, description = ? WHERE id = ?') + .run([name, description, req.budget.id], function (err?: Error) { + if (err) { + res.status(500).send("Failed to update budget"); + return; + } + db.prepare('SELECT id, username FROM user WHERE id IN (SELECT user_id FROM user_permission WHERE budget_id = ?)') + .all(req.budget.id, function (err?: Error, rows?: any[]) { + if (err) { + res.status(500).send(`Failed to fetch users for budget ${req.budget.id}`) + return; + } + res.send({ ...req.budget, name: name, description: description, users: rows }); + }); + }) + .finalize(); + } + ); -router.get('/:id/balance', (req, res) => { + router.delete( + '/:id', + authMiddleware(db), + (req, res, next) => budgetPermissionMiddleware(db, req.params['id'], Permission.OWNER)(req, res, next), + (req, res) => { + db.prepare('DELETE FROM budget WHERE id = ?') + .run(req.budget.id, function (err?: Error) { + if (err) { + console.error(err); + res.status(500).send('Failed to delete budget') + } else { + res.status(204).send(); + } + }) + } + ); -}); - -router.put('/:id', (req, res) => { - -}); - -router.delete('/:id', (req, res) => { - -}); - -export { router }; \ No newline at end of file + return router; +} diff --git a/src/server/budget/model.ts b/src/server/budget/model.ts index 9c524a3..8fe6e77 100644 --- a/src/server/budget/model.ts +++ b/src/server/budget/model.ts @@ -5,4 +5,10 @@ export class Budget { name: string; description: string; currencyCode: string; + + constructor(id: string, name: string, description: string) { + this.id = id; + this.name = name; + this.description = description; + } } diff --git a/src/server/categories/controller.ts b/src/server/categories/controller.ts index e297e82..be0520f 100644 --- a/src/server/categories/controller.ts +++ b/src/server/categories/controller.ts @@ -3,23 +3,23 @@ import express from 'express'; const router = express.Router() router.get('/', (req, res) => { - + res.status(500).send("GET /"); }); router.post('/', (req, res) => { - + res.status(500).send("POST /"); }); router.get('/:id', (req, res) => { - + res.status(500).send("GET /:id"); }); router.put('/:id', (req, res) => { - + res.status(500).send("PUT /:id"); }); router.delete('/:id', (req, res) => { - + res.status(500).send("DELETE /:id"); }); export { router }; \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 77b2d61..267016e 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,5 +1,5 @@ import express from 'express'; -import { router as budgetRouter } from './budget/controller' +import budgetRouter from './budget/controller' import { router as categoryRouter } from './categories/controller' import { router as permissionsRouter } from './permissions/controller' import { router as transactionRouter } from './transactions/controller' @@ -16,7 +16,7 @@ app.use(express.json()); app.use(express.static(__dirname + '/public')); -app.use('/api/budgets', budgetRouter); +app.use('/api/budgets', budgetRouter(db)); app.use('/api/categories', categoryRouter); app.use('/api/permissions', permissionsRouter); app.use('/api/transactions', transactionRouter); diff --git a/src/server/permissions/permission.ts b/src/server/permissions/permission.ts new file mode 100644 index 0000000..4179c64 --- /dev/null +++ b/src/server/permissions/permission.ts @@ -0,0 +1,25 @@ + +export enum Permission { + READ = "READ", + WRITE = "WRITE", + MANAGE = "MANAGE", + OWNER = "OWNER" +}; + +export function isAtLeast(permission: Permission, min: Permission): boolean { + switch (permission) { + case Permission.OWNER: + return true; + case Permission.MANAGE: + return min !== Permission.OWNER; + case Permission.WRITE: + return min === Permission.READ || min === Permission.WRITE; + case Permission.READ: + return min === Permission.READ; + } +} + +export class UserPermission { + user: string; + permission: Permission; +} \ No newline at end of file diff --git a/src/server/transactions/controller.ts b/src/server/transactions/controller.ts index e297e82..be0520f 100644 --- a/src/server/transactions/controller.ts +++ b/src/server/transactions/controller.ts @@ -3,23 +3,23 @@ import express from 'express'; const router = express.Router() router.get('/', (req, res) => { - + res.status(500).send("GET /"); }); router.post('/', (req, res) => { - + res.status(500).send("POST /"); }); router.get('/:id', (req, res) => { - + res.status(500).send("GET /:id"); }); router.put('/:id', (req, res) => { - + res.status(500).send("PUT /:id"); }); router.delete('/:id', (req, res) => { - + res.status(500).send("DELETE /:id"); }); export { router }; \ No newline at end of file diff --git a/src/server/types/express/index.d.ts b/src/server/types/express/index.d.ts index b1b9762..dd5f2e8 100644 --- a/src/server/types/express/index.d.ts +++ b/src/server/types/express/index.d.ts @@ -1,9 +1,11 @@ import { User } from "../../users/user"; import { Request } from 'express'; +import { Budget } from "../../budget/model"; declare module "express-serve-static-core" { interface Request { user?: User; + budget?: Budget; } } \ No newline at end of file diff --git a/src/server/utils.ts b/src/server/utils.ts index a15fe50..60a0a24 100644 --- a/src/server/utils.ts +++ b/src/server/utils.ts @@ -1,6 +1,7 @@ import { randomInt } from 'crypto'; import { Request, Response, NextFunction } from 'express'; import { ParamsDictionary } from 'express-serve-static-core'; +import { isAtLeast, Permission } from './permissions/permission'; import QueryString from 'qs'; import sqlite3 from 'sqlite3'; import { User } from './users/user'; @@ -17,6 +18,27 @@ export function twoWeeksFromNow(): Date { return date; } +export function firstOfMonth(): Date { + const date = new Date(); + date.setMilliseconds(0); + date.setSeconds(0); + date.setMinutes(0); + date.setHours(0); + date.setDate(1); + return date; +} + +export function lastOfMonth(): Date { + const date = new Date(); + date.setMilliseconds(999); + date.setSeconds(59); + date.setMinutes(59); + date.setHours(23); + date.setMonth(date.getMonth() + 1); + date.setDate(0); + return date; +} + export function authMiddleware(db: sqlite3.Database): ( req: Request>, res: Response>, @@ -57,4 +79,41 @@ export function authMiddleware(db: sqlite3.Database): ( } }); } +} + +export function budgetPermissionMiddleware(db: sqlite3.Database, budgetId: string, minPermission: Permission): ( + req: Request>, + res: Response>, + next: NextFunction +) => void { + return (req, res, next) => { + db.prepare('SELECT * FROM user_permission WHERE budget_id = ? AND user_id = ?;') + .get([budgetId, req.user.id], (err?: Error, row?: any) => { + if (err) { + console.error(err) + res.status(500).send("Failed to get budget permissions") + return; + } + if (!row) { + res.status(404).send(`No budget found for ID ${budgetId}`); + return; + } + if (!isAtLeast(row.permission, minPermission)) { + res.status(403).send(`Insufficient permissions for budget ${budgetId}`) + return; + } + db.prepare('SELECT * FROM budget WHERE id = ?') + .get(budgetId, (err?: Error, row?: any) => { + if (err) { + console.error(err) + res.status(500).send("Failed to get budget") + return; + } + req.budget = row; + next(); + }) + .finalize(); + }) + .finalize(); + } } \ No newline at end of file