Finish implementing /api/budget routes
Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
parent
9bc86d620a
commit
eacc18e461
11 changed files with 256 additions and 34 deletions
|
@ -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({
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Budget>(this.apiUrl + '/budgets', params, this.options)
|
||||
.pipe(map(budget => {
|
||||
|
|
|
@ -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 };
|
||||
return router;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 };
|
|
@ -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);
|
||||
|
|
25
src/server/permissions/permission.ts
Normal file
25
src/server/permissions/permission.ts
Normal 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;
|
||||
}
|
|
@ -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 };
|
2
src/server/types/express/index.d.ts
vendored
2
src/server/types/express/index.d.ts
vendored
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ParamsDictionary, any, any, QueryString.ParsedQs, 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();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue