From cc33de056cc34c5e17157db1c8ee0778849bf079 Mon Sep 17 00:00:00 2001 From: splatter Date: Sun, 27 Oct 2024 14:39:16 +0000 Subject: [PATCH] add auth checking Signed-off-by: splatter --- src/common/auth.ts | 43 ++++++++++++++++++++++++++++++++++++++++++- src/common/error.ts | 2 ++ src/common/types.ts | 6 ++++++ src/index.ts | 5 +++++ src/rest/auth.ts | 12 ++++++------ src/rest/user.ts | 16 ++++++++++++++++ 6 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 src/rest/user.ts diff --git a/src/common/auth.ts b/src/common/auth.ts index 909a368..8b3a8cb 100644 --- a/src/common/auth.ts +++ b/src/common/auth.ts @@ -1,5 +1,6 @@ import { compare, hash } from "bcrypt"; import { randomBytes } from "crypto"; +import { FastifyReply, FastifyRequest } from "fastify"; import { SALT } from "./constants"; import { psqlClient } from "./database"; import { ErrorCode, RESTError } from "./error"; @@ -26,8 +27,9 @@ export async function generateToken(userID: bigint, addToDatabase: boolean): Pro const token = randomBytes(60).toString("base64").replace("+", ""); const hashedToken = await hash(token, SALT); const seed = Math.floor(Math.random() * (999999 - 100000 + 1)) + 100000; + if (addToDatabase) - await psqlClient.query("INSERT INTO tokens VALUES ($1, $2, $3)", [ + await psqlClient.query("INSERT INTO tokens (id, seed, token) VALUES ($1, $2, $3)", [ userID, seed, hashedToken @@ -36,6 +38,45 @@ export async function generateToken(userID: bigint, addToDatabase: boolean): Pro userID, seed, token, + // FIXME: why is this a string if the whole thing is provided? -- splatter full: `${userID}.${seed}.${token}` }; } + +export const authHook = async (req: FastifyRequest, reply: FastifyReply) => { + const auth = req.headers.authorization; + + if (!auth) return; + + const [userID, seed, token] = auth.split("."); + + const tokenQuery = await psqlClient.query("SELECT * FROM tokens WHERE id=$1 AND seed=$2", [ + userID, + seed + ]); + if (!tokenQuery.rowCount) { + throw new RESTError(ErrorCode.AuthError, "Invalid token"); + } + + const potentialToken = tokenQuery.rows[0]; + + const authed = await compare(token, potentialToken.token); + + if (!authed) { + throw new RESTError(ErrorCode.AuthError, "Invalid token"); + } + + const user = await psqlClient.query("SELECT * FROM users WHERE id=$1", [userID]); + + if (!user.rowCount) { + throw new RESTError(ErrorCode.AuthError, "Invalid token"); + } + + req.user = user.rows[0]; +}; + +declare module "fastify" { + interface FastifyRequest { + user: User; + } +} diff --git a/src/common/error.ts b/src/common/error.ts index a6a4381..2843918 100644 --- a/src/common/error.ts +++ b/src/common/error.ts @@ -62,6 +62,8 @@ export class RESTError extends APIError { return 500; } } + + static Authed = new RESTError(ErrorCode.AuthError, "Must be authenticated"); } export class SocketError extends APIError { diff --git a/src/common/types.ts b/src/common/types.ts index 0b31114..83d7378 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -8,6 +8,12 @@ export type User = { role: number; }; +export const sanitiseUser = (user: User): User => { + const { password, ...sanitised } = user; + + return sanitised; +}; + export type Token = { userID: bigint; seed: number; diff --git a/src/index.ts b/src/index.ts index 2d177f4..919564e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import fastify from "fastify"; import { bootstrap } from "fastify-decorators"; import { resolve } from "path"; +import { authHook } from "./common/auth.js"; import { __DEV__ } from "./common/constants.js"; import { ERROR_HANDLER } from "./common/error.js"; import { Yapper } from "./common/Yapper.js"; @@ -18,6 +19,10 @@ server.register(bootstrap, { server.setErrorHandler(ERROR_HANDLER); +server.decorateRequest("user", null); +server.addHook("preHandler", authHook); + +// what is this for? - splatter if (__DEV__) { server.log.trace("Trace message"); server.log.debug("Debug message"); diff --git a/src/rest/auth.ts b/src/rest/auth.ts index 23fc267..2db033f 100644 --- a/src/rest/auth.ts +++ b/src/rest/auth.ts @@ -68,13 +68,13 @@ export default class AuthController { if (!(await validateCaptcha(body.turnstileKey))) throw new RESTError(ErrorCode.ValidationError, "Invalid captcha"); + const existing = await psqlClient.query( + "SELECT id FROM users WHERE username=$1 OR email=$2", + [body.username, body.email] + ); + // Check for existing info - if ( - await psqlClient.query("SELECT id FROM users WHERE username=$1 OR email=$2", [ - body.username, - body.email - ]) - ) + if (existing.rowCount) throw new RESTError(ErrorCode.ConflictError, "Username or email already exists"); // Moderate username diff --git a/src/rest/user.ts b/src/rest/user.ts new file mode 100644 index 0000000..bd233fd --- /dev/null +++ b/src/rest/user.ts @@ -0,0 +1,16 @@ +import { Controller, GET } from "fastify-decorators"; +import { RESTError } from "../common/error.js"; +import { sanitiseUser } from "../common/types.js"; + +@Controller({ route: "/users" }) +export default class UserController { + @GET({ + url: "/me" + }) + async helloHandler(req, res) { + // TODO: add a decorator? + if (!req.user) throw RESTError.Authed; + + return sanitiseUser(req.user); + } +}