Finish implementing /api/budget routes

Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
William Brawner 2021-06-25 19:01:54 -06:00
parent 9bc86d620a
commit eacc18e461
11 changed files with 256 additions and 34 deletions

View file

@ -5,7 +5,7 @@ import { TWIGS_SERVICE, TwigsService } from './shared/twigs.service';
import { SwUpdate } from '@angular/service-worker'; import { SwUpdate } from '@angular/service-worker';
import { first, filter, map } from 'rxjs/operators'; import { first, filter, map } from 'rxjs/operators';
import { interval, concat, BehaviorSubject } from 'rxjs'; import { interval, concat, BehaviorSubject } from 'rxjs';
import { Router, ActivationEnd, ActivatedRoute } from '@angular/router'; import { Router } from '@angular/router';
import { Actionable, isActionable } from './shared/actionable'; import { Actionable, isActionable } from './shared/actionable';
@Component({ @Component({

View file

@ -3,6 +3,7 @@ import { Budget } from '../budget';
import { AppComponent } from 'src/client/app/app.component'; import { AppComponent } from 'src/client/app/app.component';
import { User, UserPermission, Permission } from 'src/client/app/users/user'; import { User, UserPermission, Permission } from 'src/client/app/users/user';
import { TWIGS_SERVICE, TwigsService } from 'src/client/app/shared/twigs.service'; import { TWIGS_SERVICE, TwigsService } from 'src/client/app/shared/twigs.service';
import { Router } from '@angular/router';
@Component({ @Component({
selector: 'app-add-edit-budget', selector: 'app-add-edit-budget',
@ -20,6 +21,7 @@ export class AddEditBudgetComponent {
constructor( constructor(
private app: AppComponent, private app: AppComponent,
@Inject(TWIGS_SERVICE) private twigsService: TwigsService, @Inject(TWIGS_SERVICE) private twigsService: TwigsService,
private router: Router
) { ) {
this.app.setTitle(this.title) this.app.setTitle(this.title)
this.app.setBackEnabled(true); this.app.setBackEnabled(true);
@ -51,7 +53,7 @@ export class AddEditBudgetComponent {
this.isLoading = true; this.isLoading = true;
this.twigsService.deleteBudget(this.budget.id) this.twigsService.deleteBudget(this.budget.id)
.subscribe(() => { .subscribe(() => {
this.app.goBack(); this.router.navigateByUrl('/budgets');
}); });
} }

View file

@ -128,12 +128,16 @@ export class TwigsHttpService implements TwigsService {
'id': id, 'id': id,
'name': name, 'name': name,
'description': description, 'description': description,
<<<<<<< HEAD
'users': users.map(userPermission => { 'users': users.map(userPermission => {
return { return {
user: userPermission.user, user: userPermission.user,
permission: Permission[userPermission.permission] permission: Permission[userPermission.permission]
}; };
}) })
=======
'users': users
>>>>>>> 4488aff (Finish implementing /api/budget routes)
}; };
return this.http.post<Budget>(this.apiUrl + '/budgets', params, this.options) return this.http.post<Budget>(this.apiUrl + '/budgets', params, this.options)
.pipe(map(budget => { .pipe(map(budget => {

View file

@ -1,29 +1,153 @@
import express from 'express'; 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();
}
})
}
);
}); return router;
}
router.put('/:id', (req, res) => {
});
router.delete('/:id', (req, res) => {
});
export { router };

View file

@ -5,4 +5,10 @@ export class Budget {
name: string; name: string;
description: string; description: string;
currencyCode: string; currencyCode: string;
constructor(id: string, name: string, description: string) {
this.id = id;
this.name = name;
this.description = description;
}
} }

View file

@ -3,23 +3,23 @@ import express from 'express';
const router = express.Router() const router = express.Router()
router.get('/', (req, res) => { router.get('/', (req, res) => {
res.status(500).send("GET /");
}); });
router.post('/', (req, res) => { router.post('/', (req, res) => {
res.status(500).send("POST /");
}); });
router.get('/:id', (req, res) => { router.get('/:id', (req, res) => {
res.status(500).send("GET /:id");
}); });
router.put('/:id', (req, res) => { router.put('/:id', (req, res) => {
res.status(500).send("PUT /:id");
}); });
router.delete('/:id', (req, res) => { router.delete('/:id', (req, res) => {
res.status(500).send("DELETE /:id");
}); });
export { router }; export { router };

View file

@ -1,5 +1,5 @@
import express from 'express'; 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 categoryRouter } from './categories/controller'
import { router as permissionsRouter } from './permissions/controller' import { router as permissionsRouter } from './permissions/controller'
import { router as transactionRouter } from './transactions/controller' import { router as transactionRouter } from './transactions/controller'
@ -16,7 +16,7 @@ app.use(express.json());
app.use(express.static(__dirname + '/public')); 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/categories', categoryRouter);
app.use('/api/permissions', permissionsRouter); app.use('/api/permissions', permissionsRouter);
app.use('/api/transactions', transactionRouter); app.use('/api/transactions', transactionRouter);

View file

@ -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;
}

View file

@ -3,23 +3,23 @@ import express from 'express';
const router = express.Router() const router = express.Router()
router.get('/', (req, res) => { router.get('/', (req, res) => {
res.status(500).send("GET /");
}); });
router.post('/', (req, res) => { router.post('/', (req, res) => {
res.status(500).send("POST /");
}); });
router.get('/:id', (req, res) => { router.get('/:id', (req, res) => {
res.status(500).send("GET /:id");
}); });
router.put('/:id', (req, res) => { router.put('/:id', (req, res) => {
res.status(500).send("PUT /:id");
}); });
router.delete('/:id', (req, res) => { router.delete('/:id', (req, res) => {
res.status(500).send("DELETE /:id");
}); });
export { router }; export { router };

View file

@ -1,9 +1,11 @@
import { User } from "../../users/user"; import { User } from "../../users/user";
import { Request } from 'express'; import { Request } from 'express';
import { Budget } from "../../budget/model";
declare module "express-serve-static-core" { declare module "express-serve-static-core" {
interface Request { interface Request {
user?: User; user?: User;
budget?: Budget;
} }
} }

View file

@ -1,6 +1,7 @@
import { randomInt } from 'crypto'; import { randomInt } from 'crypto';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { ParamsDictionary } from 'express-serve-static-core'; import { ParamsDictionary } from 'express-serve-static-core';
import { isAtLeast, Permission } from './permissions/permission';
import QueryString from 'qs'; import QueryString from 'qs';
import sqlite3 from 'sqlite3'; import sqlite3 from 'sqlite3';
import { User } from './users/user'; import { User } from './users/user';
@ -17,6 +18,27 @@ export function twoWeeksFromNow(): Date {
return 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): ( export function authMiddleware(db: sqlite3.Database): (
req: Request<ParamsDictionary, any, any, QueryString.ParsedQs, Record<string, any>>, req: Request<ParamsDictionary, any, any, QueryString.ParsedQs, Record<string, any>>,
res: Response<any, Record<string, any>>, res: Response<any, Record<string, any>>,
@ -57,4 +79,41 @@ export function authMiddleware(db: sqlite3.Database): (
} }
}); });
} }
}
export function budgetPermissionMiddleware(db: sqlite3.Database, budgetId: string, minPermission: Permission): (
req: Request<ParamsDictionary, any, any, QueryString.ParsedQs, Record<string, any>>,
res: Response<any, Record<string, any>>,
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();
}
} }