Implement login and registration
This commit is contained in:
parent
5a8613c701
commit
f5f4dbe7e1
12 changed files with 286 additions and 31 deletions
|
@ -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
86
package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
apiUrl: 'http://localhost:8080/api'
|
apiUrl: 'http://localhost:3000/api'
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
9
src/server/types/express/index.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { User } from "../../users/user";
|
||||||
|
import { Request } from 'express';
|
||||||
|
|
||||||
|
declare module "express-serve-static-core" {
|
||||||
|
interface Request {
|
||||||
|
user?: User;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 };
|
|
3
src/server/users/index.ts
Normal file
3
src/server/users/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import userRouter from './user_controller';
|
||||||
|
|
||||||
|
export { userRouter };
|
37
src/server/users/user.ts
Normal file
37
src/server/users/user.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
101
src/server/users/user_controller.ts
Normal file
101
src/server/users/user_controller.ts
Normal 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;
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue