diff --git a/src/@shared/src/auth/index.ts b/src/@shared/src/auth/index.ts index c41aa26..b814771 100644 --- a/src/@shared/src/auth/index.ts +++ b/src/@shared/src/auth/index.ts @@ -1,23 +1,23 @@ -import OTP from "otp"; -import cookie from "@fastify/cookie"; -import fastifyJwt from "@fastify/jwt"; -import fp from "fastify-plugin"; -import { FastifyPluginAsync, preValidationAsyncHookHandler } from "fastify"; -import { Static, Type } from "@sinclair/typebox"; -import { UserId } from "@shared/database/mixin/user"; -import { useDatabase } from "@shared/database"; -import { isNullish, makeResponse } from "@shared/utils"; +import OTP from 'otp'; +import cookie from '@fastify/cookie'; +import fastifyJwt from '@fastify/jwt'; +import fp from 'fastify-plugin'; +import { FastifyPluginAsync, preValidationAsyncHookHandler } from 'fastify'; +import { Static, Type } from '@sinclair/typebox'; +import { UserId } from '@shared/database/mixin/user'; +import { useDatabase } from '@shared/database'; +import { isNullish, makeResponse } from '@shared/utils'; -const kRouteAuthDone = Symbol("shared-route-auth-done"); +const kRouteAuthDone = Symbol('shared-route-auth-done'); type AuthedUser = { id: UserId; name: string; }; -declare module "fastify" { +declare module 'fastify' { export interface FastifyInstance { - signJwt: (kind: "auth" | "otp", who: string) => string; + signJwt: (kind: 'auth' | 'otp', who: string) => string; [s: symbol]: boolean; } export interface FastifyRequest { @@ -33,10 +33,10 @@ let jwtAdded = false; export const jwtPlugin = fp(async (fastify, _opts) => { if (jwtAdded) return; jwtAdded = true; - let env = process.env.JWT_SECRET; - if (isNullish(env)) throw "JWT_SECRET is not defined"; - if (!fastify.hasDecorator("signJwt")) { - void fastify.decorate("signJwt", (kind, who) => + const env = process.env.JWT_SECRET; + if (isNullish(env)) throw 'JWT_SECRET is not defined'; + if (!fastify.hasDecorator('signJwt')) { + void fastify.decorate('signJwt', (kind, who) => fastify.jwt.sign({ kind, who, createdAt: Date.now() }), ); void fastify.register(fastifyJwt, { @@ -48,16 +48,16 @@ export const jwtPlugin = fp(async (fastify, _opts) => { export const JwtType = Type.Object({ kind: Type.Union([ - Type.Const("otp", { - description: "the token is only valid for otp call", + Type.Const('otp', { + description: 'the token is only valid for otp call', }), - Type.Const("auth", { - description: "the token is valid for authentication", + Type.Const('auth', { + description: 'the token is valid for authentication', }), ]), - who: Type.String({ description: "the login of the user" }), + who: Type.String({ description: 'the login of the user' }), createdAt: Type.Integer({ - description: "Unix timestamp of when the token as been created at", + description: 'Unix timestamp of when the token as been created at', }), }); @@ -65,52 +65,57 @@ export type JwtType = Static; let authAdded = false; export const authPlugin = fp(async (fastify, _opts) => { - if (authAdded) return void console.log("skipping"); + if (authAdded) return void console.log('skipping'); authAdded = true; await fastify.register(useDatabase as any, {}); await fastify.register(jwtPlugin as any, {}); await fastify.register(cookie); - if (!fastify.hasRequestDecorator("authUser")) - fastify.decorateRequest("authUser", undefined); - fastify.addHook("onRoute", (routeOpts) => { + if (!fastify.hasRequestDecorator('authUser')) {fastify.decorateRequest('authUser', undefined);} + fastify.addHook('onRoute', (routeOpts) => { if ( routeOpts.config?.requireAuth && !(routeOpts as any)[kRouteAuthDone] ) { - let f: preValidationAsyncHookHandler = async function(req, res) { + const f: preValidationAsyncHookHandler = async function(req, res) { try { - if (isNullish(req.cookies.token)) + if (isNullish(req.cookies.token)) { return res - .clearCookie("token") + .clearCookie('token') .send( - JSON.stringify(makeResponse("notLoggedIn", "auth.noCookie")), + JSON.stringify(makeResponse('notLoggedIn', 'auth.noCookie')), ); - let tok = this.jwt.verify(req.cookies.token); - if (tok.kind != "auth") + } + const tok = this.jwt.verify(req.cookies.token); + if (tok.kind != 'auth') { return res - .clearCookie("token") + .clearCookie('token') .send( - JSON.stringify(makeResponse("notLoggedIn", "auth.invalidKind")), + JSON.stringify(makeResponse('notLoggedIn', 'auth.invalidKind')), ); - let user = this.db.getUserFromName(tok.who); - if (isNullish(user)) + } + const user = this.db.getUserFromName(tok.who); + if (isNullish(user)) { return res - .clearCookie("token") + .clearCookie('token') .send( - JSON.stringify(makeResponse("notLoggedIn", "auth.noUser")), + JSON.stringify(makeResponse('notLoggedIn', 'auth.noUser')), ); + } req.authUser = { id: user.id, name: tok.who }; - } catch { + } + catch { return res - .clearCookie("token") - .send(JSON.stringify(makeResponse("notLoggedIn", "auth.invalid"))); + .clearCookie('token') + .send(JSON.stringify(makeResponse('notLoggedIn', 'auth.invalid'))); } }; if (!routeOpts.preValidation) { routeOpts.preValidation = [f]; - } else if (Array.isArray(routeOpts.preValidation)) { + } + else if (Array.isArray(routeOpts.preValidation)) { routeOpts.preValidation.push(f); - } else { + } + else { routeOpts.preValidation = [routeOpts.preValidation, f]; } diff --git a/src/@shared/src/database/index.ts b/src/@shared/src/database/index.ts index d77b176..d3bd9cf 100644 --- a/src/@shared/src/database/index.ts +++ b/src/@shared/src/database/index.ts @@ -1,8 +1,8 @@ -import fp from 'fastify-plugin' -import { FastifyInstance, FastifyPluginAsync } from 'fastify' +import fp from 'fastify-plugin'; +import { FastifyInstance, FastifyPluginAsync } from 'fastify'; -import { Database as DbImpl } from "./mixin/_base"; -import { UserImpl, IUserDb } from "./mixin/user"; +import { Database as DbImpl } from './mixin/_base'; +import { UserImpl, IUserDb } from './mixin/user'; import { isNullish } from '@shared/utils'; @@ -22,16 +22,13 @@ let dbAdded = false; export const useDatabase = fp(async function( f: FastifyInstance, _options: {}) { - if (dbAdded) - return; + if (dbAdded) {return;} dbAdded = true; - let path = process.env.DATABASE_DIR; - if (isNullish(path)) - throw "env `DATABASE_DIR` not defined"; - f.log.info(`Opening database with path: ${path}/database.db`) - let db: Database = new DbImpl(`${path}/database.db`) as Database; - if (!f.hasDecorator("db")) - f.decorate('db', db); + const path = process.env.DATABASE_DIR; + if (isNullish(path)) {throw 'env `DATABASE_DIR` not defined';} + f.log.info(`Opening database with path: ${path}/database.db`); + const db: Database = new DbImpl(`${path}/database.db`) as Database; + if (!f.hasDecorator('db')) {f.decorate('db', db);} }); export default useDatabase; diff --git a/src/@shared/src/database/mixin/_base.ts b/src/@shared/src/database/mixin/_base.ts index 1d601f1..e039883 100644 --- a/src/@shared/src/database/mixin/_base.ts +++ b/src/@shared/src/database/mixin/_base.ts @@ -1,57 +1,57 @@ -import sqlite from "better-sqlite3"; +import sqlite from 'better-sqlite3'; // @ts-ignore: this file is included using vite, typescript doesn't know how to include this... -import initSql from "../init.sql?raw" +import initSql from '../init.sql?raw'; export type SqliteReturn = object | undefined; // Only way to use the database. Everything must be done through this. // Prefer to use prepared statement `using this.db.prepare` // -// this is the base, meaning that no actual query are made from this file. +// this is the base, meaning that no actual query are made from this file. // To create a new query function, go open another file, create a class that inherit from this class // in the `index.ts` file, import the new class, and make the `Database` class inherit it export class Database { - private db: sqlite.Database; - private st: Map = new Map(); + private db: sqlite.Database; + private st: Map = new Map(); - /** + /** * Create a new instance of the database, and init it to a known state * the file ./init.sql will be ran onto the database, creating any table that might be missing */ - constructor(db_path: string) { - this.db = sqlite(db_path, {}); - this.db.pragma('journal_mode = WAL'); - this.db.transaction(() => this.db.exec(initSql))(); - } + constructor(db_path: string) { + this.db = sqlite(db_path, {}); + this.db.pragma('journal_mode = WAL'); + this.db.transaction(() => this.db.exec(initSql))(); + } - /** + /** * close the database */ - public destroy(): void { - // remove any statement from the cache - this.st.clear(); - // close the database - this.db?.close(); - } + public destroy(): void { + // remove any statement from the cache + this.st.clear(); + // close the database + this.db?.close(); + } - /** + /** * use this to create queries. This will create statements (kinda expensive) and cache them * since they will be cached, this means that they are only created once, * otherwise they'll be just spat out from the cache - * the statements are cached by the {query} argument, + * the statements are cached by the {query} argument, * meaning that if you try to make two identiqual statement, but with different {query} they won't be cached * * @example this.prepare('SELECT * FROM users WHERE id = ?') * @example this.prepare('SELECT * FROM users LIMIT 100 OFFSET ?') */ - protected prepare(query: string): sqlite.Statement { - let st = this.st.get(query); - if (st !== undefined) return st; + protected prepare(query: string): sqlite.Statement { + let st = this.st.get(query); + if (st !== undefined) return st; - st = this.db.prepare(query); - this.st.set(query, st); - return st; - } + st = this.db.prepare(query); + this.st.set(query, st); + return st; + } } diff --git a/src/@shared/src/database/mixin/_template.ts b/src/@shared/src/database/mixin/_template.ts index 05ced1e..c7fbee5 100644 --- a/src/@shared/src/database/mixin/_template.ts +++ b/src/@shared/src/database/mixin/_template.ts @@ -1,4 +1,4 @@ -import type { Database } from "./_base"; +import type { Database } from './_base'; // never use this directly @@ -9,28 +9,28 @@ export interface ITemplateDb extends Database { }; export const UserImpl: Omit = { - /** + /** * whole function description * * @param id the argument description * * @returns what does the function return ? */ - normalFunction(this: ITemplateDb, id: TemplateId): TemplateData | undefined { - void id; - return undefined; - }, - /** + normalFunction(this: ITemplateDb, id: TemplateId): TemplateData | undefined { + void id; + return undefined; + }, + /** * whole function description * * @param id the argument description * * @returns what does the function return ? */ - async asyncFunction(this: ITemplateDb, id: TemplateId): Promise { - void id; - return undefined; - }, + async asyncFunction(this: ITemplateDb, id: TemplateId): Promise { + void id; + return undefined; + }, }; export type TemplateId = number & { readonly __brand: unique symbol }; @@ -43,13 +43,13 @@ export type TemplateData = { // this function will be able to be called from everywhere export async function freeFloatingExportedFunction(): Promise { - return false; + return false; } // this function will never be able to be called outside of this module async function privateFunction(): Promise { - return undefined + return undefined; } -//silence warnings +// silence warnings void privateFunction; diff --git a/src/@shared/src/database/mixin/user.ts b/src/@shared/src/database/mixin/user.ts index 1fc54fc..6885100 100644 --- a/src/@shared/src/database/mixin/user.ts +++ b/src/@shared/src/database/mixin/user.ts @@ -1,7 +1,7 @@ -import type { Database, SqliteReturn } from "./_base"; -import { Otp } from "@shared/auth"; -import { isNullish } from "@shared/utils"; -import * as bcrypt from "bcrypt"; +import type { Database, SqliteReturn } from './_base'; +import { Otp } from '@shared/auth'; +import { isNullish } from '@shared/utils'; +import * as bcrypt from 'bcrypt'; // never use this directly @@ -38,7 +38,7 @@ export const UserImpl: Omit = { getUserFromName(this: IUserDb, name: string): User | undefined { return userFromRow( this.prepare( - "SELECT * FROM user WHERE name = @name LIMIT 1", + 'SELECT * FROM user WHERE name = @name LIMIT 1', ).get({ name }), ); }, @@ -52,7 +52,7 @@ export const UserImpl: Omit = { */ getUserFromRawId(this: IUserDb, id: number): User | undefined { return userFromRow( - this.prepare("SELECT * FROM user WHERE id = @id LIMIT 1").get({ + this.prepare('SELECT * FROM user WHERE id = @id LIMIT 1').get({ id, }) as SqliteReturn, ); @@ -70,7 +70,7 @@ export const UserImpl: Omit = { password = await hashPassword(password); return userFromRow( this.prepare( - "INSERT OR FAIL INTO user (name, password) VALUES (@name, @password) RETURNING *", + 'INSERT OR FAIL INTO user (name, password) VALUES (@name, @password) RETURNING *', ).get({ name, password }), ); }, @@ -88,30 +88,29 @@ export const UserImpl: Omit = { password = await hashPassword(password); return userFromRow( this.prepare( - "UPDATE OR FAIL user SET password = @password WHERE id = @id RETURNING *", + 'UPDATE OR FAIL user SET password = @password WHERE id = @id RETURNING *', ).get({ password, id }) as SqliteReturn, ); }, getUserOtpSecret(this: IUserDb, id: UserId): string | undefined { - let otp: any = this.prepare("SELECT otp FROM user WHERE id = @id LIMIT 1").get({ id }) as SqliteReturn; + const otp: any = this.prepare('SELECT otp FROM user WHERE id = @id LIMIT 1').get({ id }) as SqliteReturn; if (isNullish(otp?.otp)) return undefined; return otp.otp; }, ensureUserOtpSecret(this: IUserDb, id: UserId): string | undefined { - let otp = this.getUserOtpSecret(id); - if (!isNullish(otp)) - return otp; - let otpGen = new Otp(); - const res: any = this.prepare("UPDATE OR IGNORE user SET otp = @otp WHERE id = @id RETURNING otp") + const otp = this.getUserOtpSecret(id); + if (!isNullish(otp)) {return otp;} + const otpGen = new Otp(); + const res: any = this.prepare('UPDATE OR IGNORE user SET otp = @otp WHERE id = @id RETURNING otp') .get({ id, otp: otpGen.secret }); return res?.otp; }, deleteUserOtpSecret(this: IUserDb, id: UserId): void { - this.prepare("UPDATE OR IGNORE user SET otp = NULL WHERE id = @id").run({ id }); - } + this.prepare('UPDATE OR IGNORE user SET otp = NULL WHERE id = @id').run({ id }); + }, }; export type UserId = number & { readonly __brand: unique symbol }; @@ -163,4 +162,4 @@ function userFromRow(row: any): User | undefined { password: row.password || undefined, otp: row.otp || undefined, }; -} +} diff --git a/src/@shared/src/utils/index.ts b/src/@shared/src/utils/index.ts index 350b93e..3918b27 100644 --- a/src/@shared/src/utils/index.ts +++ b/src/@shared/src/utils/index.ts @@ -1,4 +1,4 @@ -import { Type } from "@sinclair/typebox"; +import { Type } from '@sinclair/typebox'; /** * @description Represent a message key @@ -20,21 +20,21 @@ export type ResponseBase = { /** * @description Builds a response from a `kind`, `key` and an arbitrary payload - * + * * * USE THIS FUNCTION TO ALLOW GREPING :) * * * @example makeResponse("failure", "login.failure.invalid") * @example makeResponse("success", "login.success", { token: "supersecrettoken" }) */ export function makeResponse(kind: string, key: MessageKey, payload?: T): ResponseBase { - console.log(`making response {kind: ${JSON.stringify(kind)}; key: ${JSON.stringify(key)}}`) - return { kind, msg: key, payload } + console.log(`making response {kind: ${JSON.stringify(kind)}; key: ${JSON.stringify(key)}}`); + return { kind, msg: key, payload }; } -/** +/** * @description Create a typebox Type for a response. - * + * * @example typeResponse("failure", ["login.failure.invalid", "login.failure.generic", "login.failure.missingPassword"]) * @example typeResponse("otpRequired", "login.otpRequired", { token: Type.String() }) * @example typeResponse("success", "login.success", { token: Type.String() }) @@ -43,7 +43,8 @@ export function typeResponse(kind: string, key: MessageKey | MessageKey[], paylo let tKey; if (key instanceof Array) { tKey = Type.Union(key.map(l => Type.Const(l))); - } else { + } + else { tKey = Type.Const(key); } @@ -51,10 +52,9 @@ export function typeResponse(kind: string, key: MessageKey | MessageKey[], paylo kind: Type.Const(kind), msg: tKey, }; - if (payload !== undefined) - Object.assign(Ty, { payload: Type.Object(payload) }) + if (payload !== undefined) {Object.assign(Ty, { payload: Type.Object(payload) });} - return Type.Object(Ty) + return Type.Object(Ty); } /** @@ -69,5 +69,5 @@ export function typeResponse(kind: string, key: MessageKey | MessageKey[], paylo * @example assert_equal(isNullish(false), false); */ export function isNullish(v: T | undefined | null): v is (null | undefined) { - return v === null || v === undefined + return v === null || v === undefined; }