From a16852c1b91a96163fcfba1d712acc0afc3b877f Mon Sep 17 00:00:00 2001 From: Maieul BOYER Date: Mon, 25 Aug 2025 18:42:35 +0200 Subject: [PATCH] feat(death): I want to die... --- .gitignore | 5 + docker-compose.yml | 19 ++ flake.nix | 8 +- nginx/conf/locations/auth.conf | 5 + src/@shared/package.json | 2 + src/@shared/src/auth/_inner.ts | 43 +++++ src/@shared/src/auth/index.ts | 15 +- src/@shared/src/database/index.ts | 18 +- src/@shared/src/database/init.dbml | 12 +- src/@shared/src/database/init.sql | 13 +- src/@shared/src/database/mixin/_base.ts | 84 ++++---- src/@shared/src/database/mixin/_template.ts | 64 ++++-- src/@shared/src/database/mixin/session.ts | 27 --- src/@shared/src/database/mixin/user.ts | 203 +++++++++++++++----- src/@shared/src/uuid/index.ts | 48 ----- src/auth/entrypoint.sh | 10 + src/auth/extra/.gitkeep | 0 src/auth/extra/providers-schema.json | 35 ---- src/auth/extra/providers.toml | 7 - src/auth/package.json | 1 + src/auth/src/app.ts | 12 +- src/auth/src/routes/login.ts | 81 ++++++++ src/auth/src/routes/set.ts | 49 ----- src/auth/src/routes/signin.ts | 99 ++++++++++ src/auth/src/routes/whoami.ts | 20 ++ src/auth/src/run.ts | 2 +- src/auth/vite.config.js | 26 ++- src/icons/src/app.ts | 3 +- src/icons/src/run.ts | 2 +- src/icons/vite.config.js | 4 +- src/package-lock.json | 159 ++++++++++++++- src/package.json | 3 +- src/pnpm-workspace.yaml | 1 + src/tsconfig.base.json | 2 +- 34 files changed, 761 insertions(+), 321 deletions(-) create mode 100644 nginx/conf/locations/auth.conf create mode 100644 src/@shared/src/auth/_inner.ts delete mode 100644 src/@shared/src/database/mixin/session.ts delete mode 100644 src/@shared/src/uuid/index.ts create mode 100644 src/auth/entrypoint.sh create mode 100644 src/auth/extra/.gitkeep delete mode 100644 src/auth/extra/providers-schema.json delete mode 100644 src/auth/extra/providers.toml create mode 100644 src/auth/src/routes/login.ts delete mode 100644 src/auth/src/routes/set.ts create mode 100644 src/auth/src/routes/signin.ts create mode 100644 src/auth/src/routes/whoami.ts diff --git a/.gitignore b/.gitignore index 4890b79..5f68721 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,8 @@ node_modules/ **/dist/ pnpm-lock.yaml +# sqlite stuff +*.db +*.db-shm +*.db-wal +/db/ diff --git a/docker-compose.yml b/docker-compose.yml index dd1c958..ba7e23c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,6 +45,25 @@ services: - DATABASE_DIR=/database + ############### + # AUTH # + ############### + auth: + build: + context: ./src/ + args: + - SERVICE=auth + #- EXTRA_FILES=icons/extra + container_name: auth + restart: always + networks: + - transcendance-network + volumes: + - sqlite-volume:/database + environment: + - DATABASE_DIR=/database + + volumes: images-volume: sqlite-volume: diff --git a/flake.nix b/flake.nix index 1e5be0a..0aa5758 100644 --- a/flake.nix +++ b/flake.nix @@ -25,16 +25,18 @@ podman podman-compose gnumake - nodejs_24 + nodejs_22 pnpm typescript dbmlSQLite.packages.${system}.default - - # allow building better-sqlite3 + sqlite-interactive clang ]; shellHook = '' export PODMAN_COMPOSE_WARNING_LOGS="false"; + export DATABASE_DIR="$(realpath .)/db" + mkdir -p $DATABASE_DIR + export JWT_SECRET="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ''; }; } diff --git a/nginx/conf/locations/auth.conf b/nginx/conf/locations/auth.conf new file mode 100644 index 0000000..9ff9048 --- /dev/null +++ b/nginx/conf/locations/auth.conf @@ -0,0 +1,5 @@ +#forward the post request to the microservice +location /api/auth/ { + rewrite ^/api/auth/(.*) $1 break; + proxy_pass http://auth/$uri; +} diff --git a/src/@shared/package.json b/src/@shared/package.json index 6a31bd4..3585902 100644 --- a/src/@shared/package.json +++ b/src/@shared/package.json @@ -10,6 +10,8 @@ "license": "ISC", "dependencies": { "@fastify/jwt": "^9.1.0", + "@types/bcrypt": "^6.0.0", + "bcrypt": "^6.0.0", "better-sqlite3": "^11.10.0", "fastify": "^5.0.0", "fastify-plugin": "^5.0.1", diff --git a/src/@shared/src/auth/_inner.ts b/src/@shared/src/auth/_inner.ts new file mode 100644 index 0000000..58f5329 --- /dev/null +++ b/src/@shared/src/auth/_inner.ts @@ -0,0 +1,43 @@ +//! Anything in this file shouldn't be used... +//! +//! This file is here because it is easier to share code in here. + +import { FastifyInstance } from "fastify"; +import type { Database } from "@shared/database" +import { UserId } from "../database/mixin/user"; + +export default {}; + + +class OTP { + private db: Database; + private static EPOCH: number = 0; + private static KEY_SIZE: number = 64; + private static TIME_STEP: number = 30; + + constructor(db: Database) { + this.db = db; + } + + private static Now(): number { + return Math.floor(Date.now() / 1000) + } + + private static getT(): number { + return Math.floor((OTP.Now() - this.EPOCH) / this.TIME_STEP) + } + + public verify(userid: UserId, code: string): boolean { + return true; + } + + public newUser(userid: UserId): string { + return "super topt secret"; + } + + public generate(userid: UserId): string | null { + let secret = this.db.getUserOTPSecret(userid); + + return null + } +} diff --git a/src/@shared/src/auth/index.ts b/src/@shared/src/auth/index.ts index 4b085f4..b720ba8 100644 --- a/src/@shared/src/auth/index.ts +++ b/src/@shared/src/auth/index.ts @@ -1,11 +1,24 @@ import fastifyJwt from "@fastify/jwt"; import { FastifyPluginAsync } from "fastify"; import fp from 'fastify-plugin' +import { user } from "@shared/database" export const jwtPlugin = fp(async (fastify, _opts) => { let env = process.env.JWT_SECRET; if (env === undefined || env === null) throw "JWT_SECRET is not defined" - void fastify.register(fastifyJwt, { secret: env }); + void fastify.register(fastifyJwt, { + secret: env, + decode: { complete: false }, + }); }); +export type JwtClaims = { + id: user.UserId, +}; + +export * as _inner from "./_inner.js"; + +export const otpPlugin = fp(async (fastify, _opts) => { + fastify.decorate('otp', {}, ["db"]); +}) diff --git a/src/@shared/src/database/index.ts b/src/@shared/src/database/index.ts index 84ff4b2..2382a70 100644 --- a/src/@shared/src/database/index.ts +++ b/src/@shared/src/database/index.ts @@ -1,15 +1,13 @@ import fp from 'fastify-plugin' import { FastifyInstance, FastifyPluginAsync } from 'fastify' -import { Base } from "./mixin/_base"; -import { UserDb } from "./mixin/user"; -import { SessionDb } from "./mixin/session"; +import { Database as DbImpl } from "./mixin/_base"; +import { UserImpl, IUserDb } from "./mixin/user"; -class Database extends UserDb(SessionDb(Base as any)) { - constructor(path: string) { - super(path); - } -} + +Object.assign(DbImpl.prototype, UserImpl); + +export interface Database extends DbImpl, IUserDb { } // When using .decorate you have to specify added properties for Typescript declare module 'fastify' { @@ -26,8 +24,10 @@ export const useDatabase = fp(async function( if (path === null || path === undefined) throw "env `DATABASE_DIR` not defined"; f.log.info(`Opening database with path: ${path}/database.db`) - f.decorate('db', new Database(`${path}/database.db`)); + let db: Database = new DbImpl(`${path}/database.db`) as Database; + f.decorate('db', db); }); +export * as user from "./mixin/user" export default useDatabase; diff --git a/src/@shared/src/database/init.dbml b/src/@shared/src/database/init.dbml index 490b50e..1082695 100644 --- a/src/@shared/src/database/init.dbml +++ b/src/@shared/src/database/init.dbml @@ -19,6 +19,7 @@ Table user { id integer [PK, not null, increment] name text [unique, not null] password text [null, Note: "If password is NULL, this means that the user is created through OAUTH2"] + otp text [null, Note: "If otp is NULL, then the user didn't configure 2FA"] Note: "Represent a user" } @@ -32,14 +33,3 @@ Table auth { Aka can't have two account bound to the same account '''] } - -Table session { - id integer [PK, not null, increment] - cookie text [unique, not null] - userid integer [ref: > user.id, not null] - createAt text [not null] - userAgent text [not null] - reason integer [null] - - Note: "Every session for users" -} diff --git a/src/@shared/src/database/init.sql b/src/@shared/src/database/init.sql index eeabdf2..9afe616 100644 --- a/src/@shared/src/database/init.sql +++ b/src/@shared/src/database/init.sql @@ -1,7 +1,8 @@ CREATE TABLE IF NOT EXISTS user ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL UNIQUE, - password TEXT + password TEXT, + otp TEXT ); CREATE TABLE IF NOT EXISTS auth ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, @@ -10,13 +11,3 @@ CREATE TABLE IF NOT EXISTS auth ( oauth2_user TEXT NOT NULL UNIQUE, FOREIGN KEY(user) REFERENCES user(id) ); -CREATE TABLE IF NOT EXISTS session ( - id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - cookie TEXT NOT NULL UNIQUE, - userid INTEGER NOT NULL, - createAt TEXT NOT NULL, - userAgent TEXT NOT NULL, - reason INTEGER, - FOREIGN KEY(userid) REFERENCES user(id) -); - diff --git a/src/@shared/src/database/mixin/_base.ts b/src/@shared/src/database/mixin/_base.ts index a82056d..1d601f1 100644 --- a/src/@shared/src/database/mixin/_base.ts +++ b/src/@shared/src/database/mixin/_base.ts @@ -3,13 +3,6 @@ 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" -export type MixinBase = new (...args: any[]) => { - constructor(db_path: string): void, - destroy(): void, - prepare(s: string): sqlite.Statement, - db: sqlite.Database, -} & T; - export type SqliteReturn = object | undefined; // Only way to use the database. Everything must be done through this. @@ -18,48 +11,47 @@ export type SqliteReturn = object | undefined; // 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 Base { - private db: sqlite.Database; - private st: Map = new Map(); +export class Database { + 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) { - console.log("NEW DB :)"); - this.db = sqlite(db_path, {}); - this.db.pragma('journal_mode = WAL'); - this.db.transaction(() => this.db.exec(initSql))(); - } + /** + * 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))(); + } - /** - * close the database - */ - public destroy(): void { - // remove any statement from the cache - this.st.clear(); - // close the database - this.db?.close(); - } + /** + * close the database + */ + 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, - * 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; + /** + * 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, + * 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; - 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 115e506..bf459ed 100644 --- a/src/@shared/src/database/mixin/_template.ts +++ b/src/@shared/src/database/mixin/_template.ts @@ -1,21 +1,55 @@ -//import sqlite from "better-sqlite3" -import { MixinBase } from "./_base" +import type { Database } from "./_base"; // never use this directly -export const TemplateDb = function (Base: TBase) { - return class extends Base { - constructor(...args: any[]) { - if (args.length != 1 && !(args[0] instanceof String)) - throw "Invalid arguments to mixing class" - super(args[0]); - } - } -} + +// describe every function in the object +export interface ITemplateDb extends Database { + normalFunction(id: TemplateId): TemplateData | null, + asyncFunction(id: TemplateId): Promise, +}; + +export const UserImpl: Omit = { + /** + * whole function description + * + * @param id the argument description + * + * @returns what does the function return ? + */ + normalFunction(this: ITemplateDb, id: TemplateId): TemplateData | null { + void id; + return null; + }, + /** + * whole function description + * + * @param id the argument description + * + * @returns what does the function return ? + */ + async asyncFunction(this: ITemplateDb, id: TemplateId): Promise { + void id; + return null; + }, +}; export type TemplateId = number & { readonly __brand: unique symbol }; -export type TemplateType = { - readonly id: TemplateId, - readonly field: string, - readonly field2: number, +export type TemplateData = { + readonly id: TemplateId; + readonly name: string; + readonly password?: string; }; + +// this function will be able to be called from everywhere +export async function freeFloatingExportedFunction(): Promise { + return false; +} + +// this function will never be able to be called outside of this module +async function privateFunction(): Promise { + return null +} + +//silence warnings +void privateFunction; diff --git a/src/@shared/src/database/mixin/session.ts b/src/@shared/src/database/mixin/session.ts deleted file mode 100644 index cb9f724..0000000 --- a/src/@shared/src/database/mixin/session.ts +++ /dev/null @@ -1,27 +0,0 @@ -//import sqlite from "better-sqlite3" -import { MixinBase } from "./_base" - -// never use this directly - -export const SessionDb = function (Base: TBase) { - return class extends Base { - constructor(...args: any[]) { - if (args.length != 1 && !(args[0] instanceof String)) - throw "Invalid arguments to mixing class" - super(args[0]); - } - - public getSessionFromId(id: SessionId): Session | null { - return null - } - } -} - -export type SessionId = number & { readonly __brand: unique symbol }; - -export type Session = { - readonly id: SessionId, - readonly name: string, - readonly salt: string, - readonly password: string, -}; diff --git a/src/@shared/src/database/mixin/user.ts b/src/@shared/src/database/mixin/user.ts index 4738a35..189fdc3 100644 --- a/src/@shared/src/database/mixin/user.ts +++ b/src/@shared/src/database/mixin/user.ts @@ -1,57 +1,174 @@ -//import sqlite from "better-sqlite3" -import Joi from "joi" -import { MixinBase, SqliteReturn } from "./_base" +import type { Database, SqliteReturn } from "./_base"; +import * as bcrypt from "bcrypt"; // never use this directly -const schema = Joi.object({ - id: Joi.number(), - name: Joi.string(), - password: Joi.string().optional().allow(null), - salt: Joi.string().optional().allow(null), -}) +export interface IUserDb extends Database { + getUser(id: UserId): User | null, + getUserFromName(name: string): User | null, + getUserFromRawId(id: number): User | null, + getUserOtpSecret(id: UserId): string | null, + createUser(name: string, password: string | null): Promise, + setUserPassword(id: UserId, password: string | null): Promise, +}; +export const UserImpl: Omit = { + /** + * Get a user from an [UserId] + * + * @param id the userid to fetch + * + * @returns The user if it exists, null otherwise + */ + getUser(this: IUserDb, id: UserId): User | null { + return this.getUserFromRawId(id); + }, -export const UserDb = function (Base: TBase) { - return class extends Base { - constructor(...args: any[]) { - if (args.length != 1 && !(args[0] instanceof String)) - throw "Invalid arguments to mixing class" - super(args[0]); - } + /** + * Get a user from a username [string] + * + * @param name the username to fetch + * + * @returns The user if it exists, null otherwise + */ + getUserFromName(this: IUserDb, name: string): User | null { + return userFromRow( + this.prepare( + "SELECT * FROM user WHERE name = @name LIMIT 1", + ).get({ name }), + ); + }, - private userFromRow(row: any): User { - const v = Joi.attempt(row, schema); - return { - id: v.id as UserId, - name: v.name || null, - password: v.password || null, - salt: v.salt, - } - } + /** + * Get a user from a raw [UserId] + * + * @param id the userid to modify + * + * @returns The user if it exists, null otherwise + */ + getUserFromRawId(this: IUserDb, id: number): User | null { + return userFromRow( + this.prepare("SELECT * FROM user WHERE id = @id LIMIT 1").get({ + id, + }) as SqliteReturn, + ); + }, - public getUser(id: UserId): User | null { - return this.getUserFromRawId(id); - } + /** + * Create a new user using password hash + * + * @param name the username for the new user (must be unique and sanitized) + * @param password the plaintext password of the new user (if any) + * + * @returns The user struct + */ + async createUser(this: IUserDb, name: string, password: string | null): Promise { + password = await hashPassword(password); + return userFromRow( + this.prepare( + "INSERT OR FAIL INTO user (name, password) VALUES (@name, @password) RETURNING *", + ).get({ name, password }), + ); + }, - public getUserFromRawId(id: number): User | null { - let res = this.prepare('SELECT * FROM user WHERE id = ?').get(id) as SqliteReturn; - if (res === null || res === undefined) return null; - return this.userFromRow(res); - } + /** + * Set the hash of a password in the database for a specific user. + * You are required to hash the password before storing it in the database + * + * @param id the userid to modify + * @param password the plaintext password to store (can be null to remove password login) + * + * @returns The modified user if it exists, null otherwise + */ + async setUserPassword(this: IUserDb, id: UserId, password: string | null): Promise { + password = await hashPassword(password); + return userFromRow( + this.prepare( + "UPDATE OR FAIL user SET password = @password WHERE id = @id RETURNING *", + ).get({ password, id }) as SqliteReturn, + ); + }, - public setUser(id: UserId, partialUser: Partial>): User | null { - return null - } - - } -} + getUserOtpSecret(this: IUserDb, id: UserId): string | null { + let otp: any = this.prepare("SELECT otp FROM user WHERE id = @id LIMIT 1").get({ id }) as SqliteReturn; + console.log(otp); + if (otp?.otp === undefined || otp?.otp === null) return null; + return otp.otp; + }, +}; export type UserId = number & { readonly __brand: unique symbol }; export type User = { - readonly id: UserId, - readonly name: string, - readonly salt: string, - readonly password: string, + readonly id: UserId; + readonly name: string; + readonly password?: string; + readonly otp?: string; }; + +/** + * Represent different state a "username" might be + * + * @enum V_valid The username is valid + * @enum E_tooShort The username is too short + * @enum E_tooLong The username is too long + * @enum E_invalChar the username contains invalid characters (must be alphanumeric) + * + */ +export const enum ValidUserNameRet { + V_valid = "username.valid", + E_tooShort = "username.tooShort", + E_tooLong = "username.toLong", + E_invalChar = "username.invalChar" +} + +export function validUserName(username: string): ValidUserNameRet { + if (username.length < 4) + return ValidUserNameRet.E_tooShort; + if (username.length > 16) + return ValidUserNameRet.E_tooLong; + if (!(RegExp("^[0-9a-zA-Z]$").test(username))) + return ValidUserNameRet.E_invalChar; + return ValidUserNameRet.V_valid; +} + +export async function verifyUserPassword( + user: User, + password: string, +): Promise { + // The user doesn't have a password, so it can't match. + // This is somewhat bad thing to do, since it is a time-attack vector, but I don't care ? + if (user.password == null) return false; + return await bcrypt.compare(password, user.password); +} + +/** + * Hash a password using the correct options + * + * @param password the plaintext password to hash (if any)\ + * @returns the bcrypt hashed password + * + * @note: This function will do nothing if [`null`] is passed (it'll return null directly) + */ +async function hashPassword( + password: string | null, +): Promise { + if (password === null) return null; + return await bcrypt.hash(password, 12); +} + +/** + * Get a user from a row + * + * @param row The data from sqlite + * + * @returns The user if it exists, null otherwise + */ +function userFromRow(row: any): User | null { + if (row == null || row == undefined) return null; + return { + id: row.id as UserId, + name: row.name || null, + password: row.password || null, + }; +} diff --git a/src/@shared/src/uuid/index.ts b/src/@shared/src/uuid/index.ts deleted file mode 100644 index 89f6cf6..0000000 --- a/src/@shared/src/uuid/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -// ************************************************************************** // -// // -// ::: :::::::: // -// index.ts :+: :+: :+: // -// +:+ +:+ +:+ // -// By: maiboyer +#+ +:+ +#+ // -// +#+#+#+#+#+ +#+ // -// Created: 2025/06/20 17:41:01 by maiboyer #+# #+# // -// Updated: 2025/07/30 16:08:19 by maiboyer ### ########.fr // -// // -// ************************************************************************** // - -import { uuidv7 } from "uuidv7"; - -export class InvalidUUID extends Error { - public readonly type = 'invalid-uuid'; -}; - -// A UUID is a all lowercase string that looks like this: -// `xxxxxxxx-xxxx-7xxx-xxx-xxxxxxxxxxxx` -// where x is any hex number -// -// it is a unique identifier, where when created can be assumed to be unique -// (aka no checks are needed) -// -// this uses the v7 of UUID, which means that every uuid is part random, -// part based on the timestamp it was Created -// -// This allows better ergonomics as you can "see" which uuid are older -// and which one are newer. -// This also makes sure that random UUID don't collide (unless you go back in time...). -export type UUIDv7 = string & { readonly __brand: unique symbol }; - -const uuidv7Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - -export function isUUIDv7(value: string): value is UUIDv7 { - return uuidv7Regex.test(value); -} - -//export function toUUIDv7(value: string): Result { -// if (!isUUIDv7(value)) return Result.error(new InvalidUUID()); -// -// return Result.ok(value.toLowerCase() as UUIDv7); -//} - -export function newUUIDv7(): UUIDv7 { - return uuidv7() as UUIDv7; -} diff --git a/src/auth/entrypoint.sh b/src/auth/entrypoint.sh new file mode 100644 index 0000000..4a3dac4 --- /dev/null +++ b/src/auth/entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +set -e +set -x +# do anything here + +cp -r /extra /files + +# run the CMD [ ... ] from the dockerfile +exec "$@" diff --git a/src/auth/extra/.gitkeep b/src/auth/extra/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/auth/extra/providers-schema.json b/src/auth/extra/providers-schema.json deleted file mode 100644 index a312144..0000000 --- a/src/auth/extra/providers-schema.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "type": "object", - "properties": { - "providers": { - "required": [], - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "redirect_url": { - "type": "string" - }, - "token_url": { - "type": "string" - }, - "client_id": { - "type": "string" - }, - "secret_env": { - "type": "string" - } - }, - "required": [ - "redirect_url", - "token_url", - "client_id", - "secret_env" - ] - } - } - }, - "required": [ - "providers" - ] -} diff --git a/src/auth/extra/providers.toml b/src/auth/extra/providers.toml deleted file mode 100644 index ab99f42..0000000 --- a/src/auth/extra/providers.toml +++ /dev/null @@ -1,7 +0,0 @@ -#:schema ./providers-schema.json - -[providers.42] -token_url = "" # which url to use -redirect_url = "" # redirect_url -client_id = "" # the client_id for the provider -secret_env = "" # env containing the secret for the provider diff --git a/src/auth/package.json b/src/auth/package.json index 36fb649..a93831b 100644 --- a/src/auth/package.json +++ b/src/auth/package.json @@ -22,6 +22,7 @@ "@fastify/multipart": "^9.0.3", "@fastify/sensible": "^6.0.0", "@fastify/static": "^8.2.0", + "@sinclair/typebox": "^0.34.40", "fastify": "^5.0.0", "fastify-cli": "^7.4.0", "fastify-plugin": "^5.0.0", diff --git a/src/auth/src/app.ts b/src/auth/src/app.ts index f385cde..bda8745 100644 --- a/src/auth/src/app.ts +++ b/src/auth/src/app.ts @@ -3,8 +3,12 @@ import fastifyFormBody from '@fastify/formbody' import fastifyMultipart from '@fastify/multipart' import { mkdir } from 'node:fs/promises' import fp from 'fastify-plugin' +import * as db from '@shared/database' +import * as auth from '@shared/auth' +// @ts-ignore: import.meta.glob is a vite thing. Typescript doesn't know this... const plugins = import.meta.glob('./plugins/**/*.ts', { eager: true }); +// @ts-ignore: import.meta.glob is a vite thing. Typescript doesn't know this... const routes = import.meta.glob('./routes/**/*.ts', { eager: true }); @@ -21,15 +25,17 @@ const app: FastifyPluginAsync = async ( ): Promise => { // Place here your custom code! for (const plugin of Object.values(plugins)) { - void fastify.register(plugin, {}); + void fastify.register(plugin as any, {}); } for (const route of Object.values(routes)) { - void fastify.register(route, {}); + void fastify.register(route as any, {}); } - //void fastify.register(MyPlugin, {}) + await fastify.register(db.useDatabase as any, {}) + await fastify.register(auth.jwtPlugin as any, {}) void fastify.register(fastifyFormBody, {}) void fastify.register(fastifyMultipart, {}) + console.log(fastify.db.getUser(0 as any)); // The use of fastify-plugin is required to be able // to export the decorators to the outer scope diff --git a/src/auth/src/routes/login.ts b/src/auth/src/routes/login.ts new file mode 100644 index 0000000..f33913c --- /dev/null +++ b/src/auth/src/routes/login.ts @@ -0,0 +1,81 @@ +import { FastifyPluginAsync } from "fastify"; + +import { Static, Type } from "@sinclair/typebox"; +import { user as userDb } from "@shared/database"; +import type { } from "@shared/auth"; + +export const LoginReq = Type.Object({ + name: Type.String(), + password: Type.String({ minLength: 8, maxLength: 32 }), +}); + +export type LoginReq = Static; + + +export const LoginRes = Type.Union([ + Type.Object({ + kind: Type.Const("failed"), + msg_key: Type.Union([ + Type.Const("login.failed.generic"), + Type.Const("login.failed.invalid"), + ]), + }), + Type.Object({ + kind: Type.Const("otpRequired"), + msg_key: Type.Const("login.otpRequired"), + token: Type.String({ + description: "Code to send with the OTP to finish login", + }), + }), + Type.Object({ + kind: Type.Const("success"), + msg_key: Type.Const("login.success"), + token: Type.String({ description: "The JWT token" }), + }), +]); + +export type LoginRes = Static; + +const route: FastifyPluginAsync = async (fastify, opts): Promise => { + fastify.post<{ Body: LoginReq; Response: LoginRes }>( + "/login", + { + schema: { + body: LoginReq, + response: { "2xx": LoginRes }, + }, + }, + async function(req, res) { + try { + let { name, password } = req.body; + let user = this.db.getUserFromName(name); + + // does the user exist + // does it have a password setup ? + if (user === null || user.password === null) + return { kind: "failed", msg_key: "login.failed.invalid" }; + + // does the password he provided match the one we have + if (!(await userDb.verifyUserPassword(user, password))) + return { kind: "failed", msg_key: "login.failed.invalid" }; + + // does the user has 2FA up ? + if (user.otp !== null) { + // yes -> we ask them to fill it, + // send them somehting to verify that they indeed passed throught the user+password phase + let otpToken = this.jwt.sign({ kind: "otp", user: user.name, createAt: Date.now() / 1000 }); + return { kind: "otpRequired", msg_key: "login.otpRequired", token: otpToken }; + } + + // every check has been passed, they are now logged in, using this token to say who they are... + let userToken = this.jwt.sign({ kind: "auth", user: user.name, createAt: Date.now() / 1000 }); + return { kind: "success", msg_key: "login.success", token: userToken } + } + catch { + return { kind: "failed", msg_key: "login.failed.generic" }; + } + }, + ); +}; + +export default route; diff --git a/src/auth/src/routes/set.ts b/src/auth/src/routes/set.ts deleted file mode 100644 index 1d315a6..0000000 --- a/src/auth/src/routes/set.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { FastifyPluginAsync } from 'fastify' -import { join } from 'node:path' -import { open } from 'node:fs/promises' -import sharp from 'sharp' -import { newUUIDv7 } from '@shared/uuid' -import rawBody from 'raw-body' - -const route: FastifyPluginAsync = async (fastify, opts): Promise => { - // await fastify.register(authMethod, {}); - // here we register plugins that will be active for the current fastify instance (aka everything in this function) - - // we register a route handler for: `/` - // it sets some configuration options, and set the actual function that will handle the request - - fastify.addContentTypeParser('*', function(request, payload, done) { - done() - }); - - fastify.post('/:userid', async function(request, reply) { - let buffer = await rawBody(request.raw); - // this is how we get the `:userid` part of things - const userid: string | undefined = (request.params as any)['userid']; - if (userid === undefined) { - return await reply.code(403); - } - const image_store: string = fastify.getDecorator('image_store') - const image_path = join(image_store, userid) - - try { - let img = sharp(buffer); - img.resize({ - height: 128, - width: 128, - fit: 'fill', - }) - const data = await img.png({ compressionLevel: 6 }).toBuffer() - let image_file = await open(image_path, "w", 0o666) - await image_file.write(data); - await image_file.close() - } catch (e: any) { - fastify.log.error(`Error: ${e}`); - reply.code(400); - return { status: "error", message: e.toString() }; - } - }) -} - -export default route - diff --git a/src/auth/src/routes/signin.ts b/src/auth/src/routes/signin.ts new file mode 100644 index 0000000..ea7c41e --- /dev/null +++ b/src/auth/src/routes/signin.ts @@ -0,0 +1,99 @@ +import { FastifyPluginAsync } from "fastify"; + +import { Static, Type } from "@sinclair/typebox"; + +const USERNAME_CHECK: RegExp = /^[a-zA-Z\_0-9]+$/; + +export const SignInReq = Type.Object({ + name: Type.String(), + password: Type.String({ minLength: 8, maxLength: 32 }), +}); + +export type SignInReq = Static; + +export const SignInRes = Type.Union([ + Type.Object({ + kind: Type.Const("failed"), + msg_key: Type.Union([ + Type.Const("signin.failed.generic"), + Type.Const("signin.failed.username.existing"), + Type.Const("signin.failed.username.toolong"), + Type.Const("signin.failed.username.tooshort"), + Type.Const("signin.failed.username.invalid"), + Type.Const("signin.failed.password.toolong"), + Type.Const("signin.failed.password.tooshort"), + Type.Const("signin.failed.password.invalid"), + ]), + }), + Type.Object({ + kind: Type.Const("sucess"), + msg_key: Type.Const("signin.sucess"), + token: Type.String({ description: "The JWT token" }), + }), +]); + +export type SignInRes = Static; + +const route: FastifyPluginAsync = async (fastify, opts): Promise => { + fastify.post<{ Body: SignInReq; Response: SignInRes }>( + "/signin", + { schema: { body: SignInReq, response: { "2xx": SignInRes } } }, + async function(req, res) { + const { name, password } = req.body; + + if (name.length < 4) + return { + kind: "failed", + msg_key: "signin.failed.username.tooshort", + }; + if (name.length > 32) + return { + kind: "failed", + msg_key: "signin.failed.username.toolong", + }; + if (!USERNAME_CHECK.test(name)) + return { + kind: "failed", + msg_key: "signin.failed.username.invalid", + }; + // username if good now :) + + if (password.length < 8) + return { + kind: "failed", + msg_key: "signin.failed.password.tooshort", + }; + if (password.length > 64) + return { + kind: "failed", + msg_key: "signin.failed.password.toolong", + }; + // password is good too ! + + if (this.db.getUserFromName(name) !== null) + return { + kind: "failed", + msg_key: "signin.failed.username.existing", + }; + let u = await this.db.createUser(name, password); + if (u === null) + return { kind: "failed", msg_key: "signin.failed.generic" }; + + // every check has been passed, they are now logged in, using this token to say who they are... + let userToken = this.jwt.sign({ + kind: "auth", + user: u.name, + createAt: Date.now() / 1000, + }); + let out = { + kind: "success", + msg_key: "login.success", + token: userToken, + }; + console.log(out) + return out; + }, + ); +}; + +export default route; diff --git a/src/auth/src/routes/whoami.ts b/src/auth/src/routes/whoami.ts new file mode 100644 index 0000000..54d19a7 --- /dev/null +++ b/src/auth/src/routes/whoami.ts @@ -0,0 +1,20 @@ +import { FastifyPluginAsync } from "fastify"; + +import { Static, Type } from "@sinclair/typebox"; + + +export const WhoAmIRes = Type.String({ description: "username" }); + +export type WhoAmIRes = Static; + +const route: FastifyPluginAsync = async (fastify, opts): Promise => { + fastify.get( + "/whoami", + { schema: { response: { "2xx": WhoAmIRes } } }, + async function(req, res) { + return "yes"; + }, + ); +}; + +export default route; diff --git a/src/auth/src/run.ts b/src/auth/src/run.ts index 47c6f48..efaa386 100644 --- a/src/auth/src/run.ts +++ b/src/auth/src/run.ts @@ -1,7 +1,7 @@ // this sould only be used by the docker file ! import fastify, { FastifyInstance } from "fastify"; -import app from './app.js' +import app from "./app" const start = async () => { const envToLogger = { diff --git a/src/auth/vite.config.js b/src/auth/vite.config.js index 2b9d86b..89a197c 100644 --- a/src/auth/vite.config.js +++ b/src/auth/vite.config.js @@ -2,12 +2,32 @@ import { defineConfig } from 'vite' import tsconfigPaths from 'vite-tsconfig-paths' import nodeExternals from 'rollup-plugin-node-externals' import path from 'node:path' +import fs from 'node:fs' + +function collectDeps(...pkgJsonPaths) { + const allDeps = new Set(); + for (const pkgPath of pkgJsonPaths) { + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); + for (const dep of Object.keys(pkg.dependencies || {})) { + allDeps.add(dep); + } + for (const peer of Object.keys(pkg.peerDependencies || {})) { + allDeps.add(peer); + } + } + return Array.from(allDeps); +} + +const externals = collectDeps( + './package.json', + '../@shared/package.json' +); + export default defineConfig({ root: __dirname, // service root plugins: [tsconfigPaths(), nodeExternals()], build: { - ssr: true, outDir: 'dist', emptyOutDir: true, @@ -17,9 +37,9 @@ export default defineConfig({ fileName: () => 'index.js', }, rollupOptions: { - external: [], + external: externals, }, - target: 'node24', // or whatever Node version you use + target: 'node22', // or whatever Node version you use sourcemap: true, minify: false, // for easier debugging } diff --git a/src/icons/src/app.ts b/src/icons/src/app.ts index ffb6ffa..d2b29df 100644 --- a/src/icons/src/app.ts +++ b/src/icons/src/app.ts @@ -30,9 +30,10 @@ const app: FastifyPluginAsync = async ( void fastify.register(route as any, {}); } - void fastify.register(db.uDatabase as any, {}) + await fastify.register(db.useDatabase as any, {}) void fastify.register(fastifyFormBody, {}) void fastify.register(fastifyMultipart, {}) + console.log(fastify.db.getUser(1)); // The use of fastify-plugin is required to be able // to export the decorators to the outer scope diff --git a/src/icons/src/run.ts b/src/icons/src/run.ts index 47c6f48..efaa386 100644 --- a/src/icons/src/run.ts +++ b/src/icons/src/run.ts @@ -1,7 +1,7 @@ // this sould only be used by the docker file ! import fastify, { FastifyInstance } from "fastify"; -import app from './app.js' +import app from "./app" const start = async () => { const envToLogger = { diff --git a/src/icons/vite.config.js b/src/icons/vite.config.js index 89a197c..8de6f14 100644 --- a/src/icons/vite.config.js +++ b/src/icons/vite.config.js @@ -40,7 +40,7 @@ export default defineConfig({ external: externals, }, target: 'node22', // or whatever Node version you use - sourcemap: true, - minify: false, // for easier debugging + sourcemap: false, + minify: true, // for easier debugging } }) diff --git a/src/package-lock.json b/src/package-lock.json index 57bea85..84d73a8 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -9,8 +9,12 @@ "version": "0.0.0", "workspaces": [ "./@shared", - "./icons" + "./icons", + "./auth" ], + "dependencies": { + "bindings": "^1.5.0" + }, "devDependencies": { "rimraf": "^5.0.1" } @@ -21,9 +25,12 @@ "license": "ISC", "dependencies": { "@fastify/jwt": "^9.1.0", + "@types/bcrypt": "^6.0.0", + "bcrypt": "^6.0.0", "better-sqlite3": "^11.10.0", "fastify": "^5.0.0", "fastify-plugin": "^5.0.1", + "joi": "^18.0.0", "uuidv7": "^1.0.2" }, "devDependencies": { @@ -31,6 +38,29 @@ "@types/node": "^22.1.0" } }, + "auth": { + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@fastify/autoload": "^6.3.1", + "@fastify/formbody": "^8.0.2", + "@fastify/multipart": "^9.0.3", + "@fastify/sensible": "^6.0.0", + "@fastify/static": "^8.2.0", + "@sinclair/typebox": "^0.34.40", + "fastify": "^5.0.0", + "fastify-cli": "^7.4.0", + "fastify-plugin": "^5.0.0", + "raw-body": "^3.0.0", + "sharp": "^0.34.2" + }, + "devDependencies": { + "@types/node": "^22.1.0", + "rollup-plugin-node-externals": "^8.0.1", + "vite": "^7.0.6", + "vite-tsconfig-paths": "^5.1.4" + } + }, "icons": { "version": "1.0.0", "license": "ISC", @@ -866,6 +896,54 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.2.tgz", + "integrity": "sha512-1jkwm1WY9VIb6WhdANRmWDkXQUcIRpxqZpSdS+HD9vhoVL3zwoFvP8doQNEgT6k3VST0Ewu50wZnXIceRYp5tw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, "node_modules/@img/sharp-darwin-arm64": { "version": "0.34.3", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", @@ -1622,6 +1700,27 @@ "win32" ] }, + "node_modules/@sinclair/typebox": { + "version": "0.34.40", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.40.tgz", + "integrity": "sha512-gwBNIP8ZAYev/ORDWW0QvxdwPXwxBtLsdsJgSc7eDIRt8ubP+rxUBzPsrwnu16fgEF8Bx4lh/+mvQvJzcTM6Kw==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/better-sqlite3": { "version": "7.6.13", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", @@ -1643,7 +1742,6 @@ "version": "22.17.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.0.tgz", "integrity": "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -1733,6 +1831,10 @@ "node": ">=8.0.0" } }, + "node_modules/auth": { + "resolved": "auth", + "link": true + }, "node_modules/avvio": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz", @@ -1770,6 +1872,20 @@ ], "license": "MIT" }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/better-sqlite3": { "version": "11.10.0", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", @@ -2687,6 +2803,24 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/joi": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.1.tgz", + "integrity": "sha512-IiQpRyypSnLisQf3PwuN2eIHAsAIGZIrLZkd4zdvIar2bDyhM91ubRjy8a3eYablXsh9BeI/c7dmPYHca5qtoA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.0.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -2940,6 +3074,26 @@ "node": ">=10" } }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/obliterator": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", @@ -3984,7 +4138,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { diff --git a/src/package.json b/src/package.json index 7676ba3..8e8f7ea 100644 --- a/src/package.json +++ b/src/package.json @@ -4,7 +4,8 @@ "private": true, "workspaces": [ "./@shared", - "./icons" + "./icons", + "./auth" ], "scripts": { "build": "npm run build --workspaces --if-present", diff --git a/src/pnpm-workspace.yaml b/src/pnpm-workspace.yaml index 44e6126..65c74c4 100644 --- a/src/pnpm-workspace.yaml +++ b/src/pnpm-workspace.yaml @@ -7,3 +7,4 @@ onlyBuiltDependencies: - better-sqlite3 - esbuild - sharp + - bcrypt diff --git a/src/tsconfig.base.json b/src/tsconfig.base.json index c2a6aeb..315f159 100644 --- a/src/tsconfig.base.json +++ b/src/tsconfig.base.json @@ -23,7 +23,7 @@ "lib": ["ESNext"], "paths": { "@shared/database": ["@shared/src/database"], - "@shared/uuid": ["@shared/src/uuid"] + "@shared/auth": ["@shared/src/auth"] } } }