Skip to content
Snippets Groups Projects
auth.ts 6.5 KiB
Newer Older

import express, { NextFunction, Request, Response } from 'express';
import { v4 as uuid } from 'uuid';
import { hash, compare } from 'bcrypt'
import { sign, verify } from 'jsonwebtoken';

import database from '../database';
import { isOfType, asyncify } from '../util';
import { getPublicKey, getPrivateKey } from '../keys';

const auth = express();

export interface Token {
    id: string;
}

export async function tokenVerification(req: Request, _res: Response, next: NextFunction) {
    const header = req.headers?.authorization;
    let token: string | null = null;
    if (header) {
        const bearer = header.split(' ');
        token = bearer[1];
    } else if (!req.body) {
        req.body = {};
    } else if (req.body.token) {
        token = req.body.token;
    }
    if (token) {
        try {
            const decoded = await asyncify(verify, token, await getPublicKey(), { algorithms: ["ES384"] });
            req.body.token = decoded;
        } catch (err) {
            delete req.body.token;
        }
        next();
    } else {
        next();
    }
}

export function requireVerification(req: Request, res: Response, next: NextFunction) {
        next();
    } else {
        res.status(403).json({
            status: 'error',
            message: 'authentication failed',
        });
    }
}

async function generateToken(token: Token) {
    return asyncify(sign, token, await getPrivateKey(), { algorithm: "ES384", expiresIn: 60 * 60 });
}

interface RegisterBody {
    username: string;
    password: string;
    email?: string;
    realname?: string;
}

auth.post('/register', async (req, res) => {
    if (isOfType<RegisterBody>(req.body, [['username', 'string'], ['password', 'string']])) {
        const body: RegisterBody = req.body;
        const id = uuid();
        const passwdHash = await hash(body.password, 10);
        try {
            const token = await generateToken({ id: id });
            await database('users').insert({
                id: id,
                user_name: body.username.trim(),
                passwd_hash: passwdHash,
                email: body.email ?? null,
                real_name: body.realname ?? null,
            });
            res.status(200).json({
                status: 'success',
            // Fails if unique constraint for username is not met
            res.status(400).json({
                status: 'error',
                message: 'failed to create user',
            });
        }
    } else {
        res.status(400).json({
            status: 'error',
            message: 'missing request fields',
        });
    }
});

interface TokenBody {
    username: string;
    password: string;
}

auth.post('/token', async (req, res) => {
    if (isOfType<TokenBody>(req.body, [['username', 'string'], ['password', 'string']])) {
        const body: TokenBody = req.body;
        try {
            const user = await database('users').where({ user_name: body.username.trim() });
            if (user.length === 1) {
                if (await compare(body.password, user[0].passwd_hash)) {
                    const token = await generateToken({ id: user[0].id });
                    res.status(200).json({
                        status: 'success',
                        token: token,
                    });
                } else {
                        status: 'error',
                        message: 'wrong username or password',
                    });
                }
            } else {
                    status: 'error',
                    message: 'username not found',
                });
            }
        } catch (e) {
            res.status(400).json({
                status: 'error',
                message: 'failed to acquire token',
            });
        }
    } else {
        res.status(400).json({
            status: 'error',
            message: 'missing request fields',
        });
    }
});

auth.use(requireVerification);

auth.get("/extend", async function (req, res) {
    const token = await asyncify(sign, {
        id: req.body.token.id,
    }, await getPrivateKey(), { algorithm: "ES384", expiresIn: 60 * 60 });
    res.status(200).json({
        status: 'success',
        token: token,
    });
interface UsernameBody {
    token: Token;
    username: string;
}

auth.put("/username", async function (req, res) {
    if (isOfType<UsernameBody>(req.body, [['username', 'string']])) {
        const body: UsernameBody = req.body;
            const count = await database('users').update({
                user_name: body.username.trim(),
            }).where({
                id: body.token.id,
            });
            if (count == 1) {
                res.status(200).json({
                    status: 'success',
                });
            } else {
                res.status(404).json({
                    status: 'error',
                    message: 'user not found',
                });
            }
        } catch (e) {
            // Fails if unique constraint for username is not met
            res.status(400).json({
                status: 'error',
                message: 'failed to update username',
            });
        res.status(400).json({
            status: 'error',
            message: 'missing request fields',
        });
});

interface PasswordBody {
    token: Token;
    password: string;
auth.put("/password", async function (req, res) {
    if (isOfType<PasswordBody>(req.body, [['password', 'string']])) {
        const body: PasswordBody = req.body;
        try {
            const passwdHash = await hash(body.password, 10);
            const count = await database('users').update({
                passwd_hash: passwdHash
            }).where({
                id: body.token.id,
            });
            if (count == 1) {
                res.status(200).json({
                    status: 'success',
                });
            } else {
                res.status(404).json({
                    status: 'error',
                    message: 'user not found',
                });
            }
        } catch (e) {
            res.status(400).json({
                status: 'error',
                message: 'failed to update password',
            });
        }
        res.status(400).json({
            message: 'missing request fields',