Implement login and registration

This commit is contained in:
William Brawner 2021-02-12 21:50:10 -07:00
parent 5a8613c701
commit f5f4dbe7e1
12 changed files with 286 additions and 31 deletions

View file

@ -13,7 +13,7 @@
"build": { "build": {
"builder": "@angular-devkit/build-angular:browser", "builder": "@angular-devkit/build-angular:browser",
"options": { "options": {
"outputPath": "dist/twigs", "outputPath": "dist/public",
"index": "src/client/index.html", "index": "src/client/index.html",
"main": "src/client/main.ts", "main": "src/client/main.ts",
"polyfills": "src/client/polyfills.ts", "polyfills": "src/client/polyfills.ts",

86
package-lock.json generated
View file

@ -18817,6 +18817,12 @@
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
"dev": true "dev": true
}, },
"@types/bcrypt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-3.0.1.tgz",
"integrity": "sha512-SwBrq5wb6jXP0o3O3jStdPWbKpimTImfdFD/OZE3uW+jhGpds/l5wMX9lfYOTDOa5Bod2QmOgo9ln+tMp2XP/w==",
"dev": true
},
"@types/body-parser": { "@types/body-parser": {
"version": "1.19.2", "version": "1.19.2",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
@ -19669,6 +19675,80 @@
"integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==",
"dev": true "dev": true
}, },
"bcrypt": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz",
"integrity": "sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw==",
"dev": true,
"requires": {
"@mapbox/node-pre-gyp": "^1.0.0",
"node-addon-api": "^3.1.0"
},
"dependencies": {
"@mapbox/node-pre-gyp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz",
"integrity": "sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==",
"dev": true,
"requires": {
"detect-libc": "^1.0.3",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
"node-fetch": "^2.6.1",
"nopt": "^5.0.0",
"npmlog": "^4.1.2",
"rimraf": "^3.0.2",
"semver": "^7.3.4",
"tar": "^6.1.0"
}
},
"agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dev": true,
"requires": {
"debug": "4"
}
},
"https-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
"dev": true,
"requires": {
"agent-base": "6",
"debug": "4"
}
},
"make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"dev": true,
"requires": {
"semver": "^6.0.0"
},
"dependencies": {
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
}
},
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
}
}
}
},
"bcrypt-pbkdf": { "bcrypt-pbkdf": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
@ -24426,6 +24506,12 @@
"whatwg-url": "^5.0.0" "whatwg-url": "^5.0.0"
} }
}, },
"node-fetch": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
"dev": true
},
"node-forge": { "node-forge": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",

View file

@ -43,11 +43,14 @@
"@angular/cli": "^14.0.4", "@angular/cli": "^14.0.4",
"@angular/compiler-cli": "^14.0.4", "@angular/compiler-cli": "^14.0.4",
"@angular/language-service": "^14.0.4", "@angular/language-service": "^14.0.4",
"@types/bcrypt": "^3.0.0",
"@types/express": "^4.17.11", "@types/express": "^4.17.11",
"@types/express-serve-static-core": "^4.17.18",
"@types/jasmine": "~3.10.3", "@types/jasmine": "~3.10.3",
"@types/jasminewd2": "^2.0.10", "@types/jasminewd2": "^2.0.10",
"@types/node": "^17.0.10", "@types/node": "^17.0.10",
"@types/sqlite3": "^3.1.7", "@types/sqlite3": "^3.1.7",
"bcrypt": "^5.0.0",
"concurrently": "^5.3.0", "concurrently": "^5.3.0",
"eslint": "^8.7.0", "eslint": "^8.7.0",
"express": "^4.17.1", "express": "^4.17.1",

View file

@ -4,7 +4,7 @@
export const environment = { export const environment = {
production: false, production: false,
apiUrl: 'http://localhost:8080/api' apiUrl: 'http://localhost:3000/api'
}; };
/* /*

View file

@ -5,7 +5,8 @@ import * as migrations from './migrations';
export function db(dataDir: string): sqlite3.Database { export function db(dataDir: string): sqlite3.Database {
const dbPath = path.join(dataDir, "twigs.db"); const dbPath = path.join(dataDir, "twigs.db");
console.log(`Initializing database at ${dbPath}`) console.log(`Initializing database at ${dbPath}`)
const db = new sqlite3.Database(dbPath, (err?: Error) => { const sqlite3verbose = sqlite3.verbose();
const db = new sqlite3verbose.Database(dbPath, (err?: Error) => {
if (err != null) { if (err != null) {
console.error("Failed to open db"); console.error("Failed to open db");
console.error(err); console.error(err);

View file

@ -3,7 +3,7 @@ import { router as 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'
import { router as userRouter } from './users/controller' import { userRouter } from './users'
import { db as _db } from './db'; import { db as _db } from './db';
const port = process.env.PORT || 3000; const port = process.env.PORT || 3000;
@ -12,6 +12,8 @@ const app = express();
const dataDir = process.env.TWIGS_DATA || __dirname; const dataDir = process.env.TWIGS_DATA || __dirname;
const db = _db(dataDir); const db = _db(dataDir);
app.use(express.json());
app.use(express.static(__dirname + '/public')); app.use(express.static(__dirname + '/public'));
// app.get('/', (req, res) => { // app.get('/', (req, res) => {
@ -23,7 +25,7 @@ app.use('/api/budgets', budgetRouter);
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);
app.use('/api/users', userRouter); app.use('/api/users', userRouter(db));
app.get('/*', (req, res) => { app.get('/*', (req, res) => {
res.sendFile(__dirname + '/public/index.html'); res.sendFile(__dirname + '/public/index.html');

9
src/server/types/express/index.d.ts vendored Normal file
View file

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

View file

@ -1,25 +0,0 @@
import express from 'express';
const router = express.Router()
router.get('/', (req, res) => {
});
router.post('/', (req, res) => {
});
router.get('/:id', (req, res) => {
});
router.put('/:id', (req, res) => {
});
router.delete('/:id', (req, res) => {
});
export { router };

View file

@ -0,0 +1,3 @@
import userRouter from './user_controller';
export { userRouter };

37
src/server/users/user.ts Normal file
View file

@ -0,0 +1,37 @@
import { randomId } from '../utils';
export class User {
id: string = randomId();
username: string;
email: string;
password: string;
constructor(
id: string,
username: string,
email: string,
password?: string
) {
this.id = id;
this.username = username;
this.email = email;
this.password = password;
}
}
function twoWeeksFromNow(): Date {
const date = new Date();
date.setDate(date.getDate() + 14);
return date;
}
export class Session {
id: string = randomId();
userId: string;
token: string = randomId(256);
expiration: Date = twoWeeksFromNow();
constructor(userId: string) {
this.userId = userId;
}
}

View file

@ -0,0 +1,101 @@
import express from 'express';
import sqlite3 from 'sqlite3';
import { authMiddleware, randomId } from '../utils';
import { hashSync, compareSync } from 'bcrypt';
import { Session } from './user';
const SALT_ROUNDS = 10;
export default function userRouter(db: sqlite3.Database): express.Router {
const router = express.Router()
router.post('/login', (req, res) => {
const username = req.body.username;
const password = req.body.password;
db.prepare('SELECT * FROM user WHERE username = ?')
.get(username, (err?: Error, row?: any) => {
if (err) {
console.error(err)
res.status(500).send("Login failed")
return;
}
if (!row) {
res.status(401).send({ 'message': 'Invalid credentials' })
return;
}
if (!compareSync(password, row.password)) {
res.status(401).send({ 'message': 'Invalid credentials' })
return;
}
const session = new Session(row.id);
db.prepare('INSERT INTO session (id, user_id, token, expiration) VALUES (?, ?, ?, ?)')
.run([session.id, session.userId, session.token, session.expiration], (sessionErr?: Error) => {
if (sessionErr) {
console.error(err)
res.status(500).send("Session creation failed")
return;
}
res.send({
token: session.token,
expiration: session.expiration.toISOString()
});
})
.finalize();
})
.finalize();
});
router.get('/', (req, res) => {
res.status(500).send("GET /");
});
router.post('/', (req, res) => {
if (!req.body.username || !req.body.password) {
res.status(400).send("Username and password are required fields");
return;
}
let id = req.body.id;
if (!id || id.length < 32) {
id = randomId();
}
const email = req.body.email;
const username = req.body.username;
const password = hashSync(req.body.password, SALT_ROUNDS);
db.prepare('INSERT INTO user (id, username, email, password) VALUES (?, ?, ?, ?)')
.run([id, username, email, password], (err?: Error) => {
if (err) {
console.error(err)
res.status(500).send("Registration failed")
return;
}
res.send({
id: id,
username: username,
email: email
});
})
.finalize();
});
router.get('/me', authMiddleware(db), (req, res) => {
res.send({
id: req.user.id,
username: req.user.username,
email: req.user.email
})
});
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");
});
return router;
}

View file

@ -1,7 +1,45 @@
import { randomInt } from 'crypto'; import { randomInt } from 'crypto';
import { Request, Response, NextFunction } from 'express';
import { ParamsDictionary } from 'express-serve-static-core';
import QueryString from 'qs';
import sqlite3 from 'sqlite3';
import { User } from './users/user';
const CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' const CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
export function randomId(length = 32): string { export function randomId(length = 32): string {
return Array.from(new Array(length), () => { CHARACTERS[randomInt(CHARACTERS.length)] }).join(''); return Array.from(new Array(length), () => CHARACTERS[randomInt(CHARACTERS.length)]).join('');
}
export function authMiddleware(db: sqlite3.Database): (
req: Request<ParamsDictionary, any, any, QueryString.ParsedQs, Record<string, any>>,
res: Response<any, Record<string, any>>,
next: NextFunction
) => void {
return (req, res, next) => {
const auth = req.get('Authorization');
if (!auth) {
res.status(401).send();
return;
}
const token = auth.substring(7);
db.prepare('SELECT U.id, U.username, U.email FROM user U INNER JOIN session S ON S.user_id = U.id WHERE S.token = ?')
.get(token, (err, row) => {
if (err) {
console.error(`Auth error: ${err}`)
res.status(401).send();
} else if (!row) {
console.log("Invalid session token")
res.status(401).send("Invalid session token")
} else {
console.log(`Found user for token: ${row}`);
req.user = new User(
row.id,
row.username,
row.email
);
next();
}
});
}
} }