feat(death): I want to die...
This commit is contained in:
parent
33e893ec58
commit
a16852c1b9
34 changed files with 761 additions and 321 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -6,3 +6,8 @@ node_modules/
|
||||||
**/dist/
|
**/dist/
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
# sqlite stuff
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
/db/
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,25 @@ services:
|
||||||
- DATABASE_DIR=/database
|
- 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:
|
volumes:
|
||||||
images-volume:
|
images-volume:
|
||||||
sqlite-volume:
|
sqlite-volume:
|
||||||
|
|
|
||||||
|
|
@ -25,16 +25,18 @@
|
||||||
podman
|
podman
|
||||||
podman-compose
|
podman-compose
|
||||||
gnumake
|
gnumake
|
||||||
nodejs_24
|
nodejs_22
|
||||||
pnpm
|
pnpm
|
||||||
typescript
|
typescript
|
||||||
dbmlSQLite.packages.${system}.default
|
dbmlSQLite.packages.${system}.default
|
||||||
|
sqlite-interactive
|
||||||
# allow building better-sqlite3
|
|
||||||
clang
|
clang
|
||||||
];
|
];
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
export PODMAN_COMPOSE_WARNING_LOGS="false";
|
export PODMAN_COMPOSE_WARNING_LOGS="false";
|
||||||
|
export DATABASE_DIR="$(realpath .)/db"
|
||||||
|
mkdir -p $DATABASE_DIR
|
||||||
|
export JWT_SECRET="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
nginx/conf/locations/auth.conf
Normal file
5
nginx/conf/locations/auth.conf
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#forward the post request to the microservice
|
||||||
|
location /api/auth/ {
|
||||||
|
rewrite ^/api/auth/(.*) $1 break;
|
||||||
|
proxy_pass http://auth/$uri;
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,8 @@
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/jwt": "^9.1.0",
|
"@fastify/jwt": "^9.1.0",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
"better-sqlite3": "^11.10.0",
|
"better-sqlite3": "^11.10.0",
|
||||||
"fastify": "^5.0.0",
|
"fastify": "^5.0.0",
|
||||||
"fastify-plugin": "^5.0.1",
|
"fastify-plugin": "^5.0.1",
|
||||||
|
|
|
||||||
43
src/@shared/src/auth/_inner.ts
Normal file
43
src/@shared/src/auth/_inner.ts
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,24 @@
|
||||||
import fastifyJwt from "@fastify/jwt";
|
import fastifyJwt from "@fastify/jwt";
|
||||||
import { FastifyPluginAsync } from "fastify";
|
import { FastifyPluginAsync } from "fastify";
|
||||||
import fp from 'fastify-plugin'
|
import fp from 'fastify-plugin'
|
||||||
|
import { user } from "@shared/database"
|
||||||
|
|
||||||
export const jwtPlugin = fp<FastifyPluginAsync>(async (fastify, _opts) => {
|
export const jwtPlugin = fp<FastifyPluginAsync>(async (fastify, _opts) => {
|
||||||
let env = process.env.JWT_SECRET;
|
let env = process.env.JWT_SECRET;
|
||||||
if (env === undefined || env === null)
|
if (env === undefined || env === null)
|
||||||
throw "JWT_SECRET is not defined"
|
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<FastifyPluginAsync>(async (fastify, _opts) => {
|
||||||
|
fastify.decorate('otp', {}, ["db"]);
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
import fp from 'fastify-plugin'
|
import fp from 'fastify-plugin'
|
||||||
import { FastifyInstance, FastifyPluginAsync } from 'fastify'
|
import { FastifyInstance, FastifyPluginAsync } from 'fastify'
|
||||||
|
|
||||||
import { Base } from "./mixin/_base";
|
import { Database as DbImpl } from "./mixin/_base";
|
||||||
import { UserDb } from "./mixin/user";
|
import { UserImpl, IUserDb } from "./mixin/user";
|
||||||
import { SessionDb } from "./mixin/session";
|
|
||||||
|
|
||||||
class Database extends UserDb(SessionDb(Base as any)) {
|
|
||||||
constructor(path: string) {
|
Object.assign(DbImpl.prototype, UserImpl);
|
||||||
super(path);
|
|
||||||
}
|
export interface Database extends DbImpl, IUserDb { }
|
||||||
}
|
|
||||||
|
|
||||||
// When using .decorate you have to specify added properties for Typescript
|
// When using .decorate you have to specify added properties for Typescript
|
||||||
declare module 'fastify' {
|
declare module 'fastify' {
|
||||||
|
|
@ -26,8 +24,10 @@ export const useDatabase = fp<FastifyPluginAsync>(async function(
|
||||||
if (path === null || path === undefined)
|
if (path === null || path === undefined)
|
||||||
throw "env `DATABASE_DIR` not defined";
|
throw "env `DATABASE_DIR` not defined";
|
||||||
f.log.info(`Opening database with path: ${path}/database.db`)
|
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;
|
export default useDatabase;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ Table user {
|
||||||
id integer [PK, not null, increment]
|
id integer [PK, not null, increment]
|
||||||
name text [unique, not null]
|
name text [unique, not null]
|
||||||
password text [null, Note: "If password is NULL, this means that the user is created through OAUTH2"]
|
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"
|
Note: "Represent a user"
|
||||||
}
|
}
|
||||||
|
|
@ -32,14 +33,3 @@ Table auth {
|
||||||
Aka can't have two account bound to the same <OAUTH2> account
|
Aka can't have two account bound to the same <OAUTH2> 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"
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
CREATE TABLE IF NOT EXISTS user (
|
CREATE TABLE IF NOT EXISTS user (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
name TEXT NOT NULL UNIQUE,
|
name TEXT NOT NULL UNIQUE,
|
||||||
password TEXT
|
password TEXT,
|
||||||
|
otp TEXT
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS auth (
|
CREATE TABLE IF NOT EXISTS auth (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
|
@ -10,13 +11,3 @@ CREATE TABLE IF NOT EXISTS auth (
|
||||||
oauth2_user TEXT NOT NULL UNIQUE,
|
oauth2_user TEXT NOT NULL UNIQUE,
|
||||||
FOREIGN KEY(user) REFERENCES user(id)
|
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)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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...
|
// @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 MixinBase<T = {}> = new (...args: any[]) => {
|
|
||||||
constructor(db_path: string): void,
|
|
||||||
destroy(): void,
|
|
||||||
prepare(s: string): sqlite.Statement,
|
|
||||||
db: sqlite.Database,
|
|
||||||
} & T;
|
|
||||||
|
|
||||||
export type SqliteReturn = object | undefined;
|
export type SqliteReturn = object | undefined;
|
||||||
|
|
||||||
// Only way to use the database. Everything must be done through this.
|
// 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.
|
// 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
|
// 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
|
// in the `index.ts` file, import the new class, and make the `Database` class inherit it
|
||||||
export class Base {
|
export class Database {
|
||||||
private db: sqlite.Database;
|
private db: sqlite.Database;
|
||||||
private st: Map<string, sqlite.Statement> = new Map();
|
private st: Map<string, sqlite.Statement> = new Map();
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new instance of the database, and init it to a known state
|
* 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
|
* the file ./init.sql will be ran onto the database, creating any table that might be missing
|
||||||
*/
|
*/
|
||||||
constructor(db_path: string) {
|
constructor(db_path: string) {
|
||||||
console.log("NEW DB :)");
|
this.db = sqlite(db_path, {});
|
||||||
this.db = sqlite(db_path, {});
|
this.db.pragma('journal_mode = WAL');
|
||||||
this.db.pragma('journal_mode = WAL');
|
this.db.transaction(() => this.db.exec(initSql))();
|
||||||
this.db.transaction(() => this.db.exec(initSql))();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* close the database
|
* close the database
|
||||||
*/
|
*/
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
// remove any statement from the cache
|
// remove any statement from the cache
|
||||||
this.st.clear();
|
this.st.clear();
|
||||||
// close the database
|
// close the database
|
||||||
this.db?.close();
|
this.db?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* use this to create queries. This will create statements (kinda expensive) and cache them
|
* 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,
|
* since they will be cached, this means that they are only created once,
|
||||||
* otherwise they'll be just spat out from the cache
|
* 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
|
* 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 WHERE id = ?')
|
||||||
* @example this.prepare('SELECT * FROM users LIMIT 100 OFFSET ?')
|
* @example this.prepare('SELECT * FROM users LIMIT 100 OFFSET ?')
|
||||||
*/
|
*/
|
||||||
protected prepare(query: string): sqlite.Statement {
|
protected prepare(query: string): sqlite.Statement {
|
||||||
let st = this.st.get(query);
|
let st = this.st.get(query);
|
||||||
if (st !== undefined) return st;
|
if (st !== undefined) return st;
|
||||||
|
|
||||||
st = this.db.prepare(query);
|
st = this.db.prepare(query);
|
||||||
this.st.set(query, st);
|
this.st.set(query, st);
|
||||||
return st;
|
return st;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,55 @@
|
||||||
//import sqlite from "better-sqlite3"
|
import type { Database } from "./_base";
|
||||||
import { MixinBase } from "./_base"
|
|
||||||
|
|
||||||
// never use this directly
|
// never use this directly
|
||||||
export const TemplateDb = function <TBase extends MixinBase>(Base: TBase) {
|
|
||||||
return class extends Base {
|
// describe every function in the object
|
||||||
constructor(...args: any[]) {
|
export interface ITemplateDb extends Database {
|
||||||
if (args.length != 1 && !(args[0] instanceof String))
|
normalFunction(id: TemplateId): TemplateData | null,
|
||||||
throw "Invalid arguments to mixing class"
|
asyncFunction(id: TemplateId): Promise<TemplateData | null>,
|
||||||
super(args[0]);
|
};
|
||||||
}
|
|
||||||
}
|
export const UserImpl: Omit<ITemplateDb, keyof Database> = {
|
||||||
}
|
/**
|
||||||
|
* 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<TemplateData | null> {
|
||||||
|
void id;
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export type TemplateId = number & { readonly __brand: unique symbol };
|
export type TemplateId = number & { readonly __brand: unique symbol };
|
||||||
|
|
||||||
export type TemplateType = {
|
export type TemplateData = {
|
||||||
readonly id: TemplateId,
|
readonly id: TemplateId;
|
||||||
readonly field: string,
|
readonly name: string;
|
||||||
readonly field2: number,
|
readonly password?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// this function will be able to be called from everywhere
|
||||||
|
export async function freeFloatingExportedFunction(): Promise<boolean> {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this function will never be able to be called outside of this module
|
||||||
|
async function privateFunction(): Promise<string | null> {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
//silence warnings
|
||||||
|
void privateFunction;
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
//import sqlite from "better-sqlite3"
|
|
||||||
import { MixinBase } from "./_base"
|
|
||||||
|
|
||||||
// never use this directly
|
|
||||||
|
|
||||||
export const SessionDb = function <TBase extends MixinBase>(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,
|
|
||||||
};
|
|
||||||
|
|
@ -1,57 +1,174 @@
|
||||||
//import sqlite from "better-sqlite3"
|
import type { Database, SqliteReturn } from "./_base";
|
||||||
import Joi from "joi"
|
import * as bcrypt from "bcrypt";
|
||||||
import { MixinBase, SqliteReturn } from "./_base"
|
|
||||||
|
|
||||||
// never use this directly
|
// never use this directly
|
||||||
|
|
||||||
const schema = Joi.object({
|
export interface IUserDb extends Database {
|
||||||
id: Joi.number(),
|
getUser(id: UserId): User | null,
|
||||||
name: Joi.string(),
|
getUserFromName(name: string): User | null,
|
||||||
password: Joi.string().optional().allow(null),
|
getUserFromRawId(id: number): User | null,
|
||||||
salt: Joi.string().optional().allow(null),
|
getUserOtpSecret(id: UserId): string | null,
|
||||||
})
|
createUser(name: string, password: string | null): Promise<User | null>,
|
||||||
|
setUserPassword(id: UserId, password: string | null): Promise<User | null>,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserImpl: Omit<IUserDb, keyof Database> = {
|
||||||
|
/**
|
||||||
|
* 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 <TBase extends MixinBase>(Base: TBase) {
|
/**
|
||||||
return class extends Base {
|
* Get a user from a username [string]
|
||||||
constructor(...args: any[]) {
|
*
|
||||||
if (args.length != 1 && !(args[0] instanceof String))
|
* @param name the username to fetch
|
||||||
throw "Invalid arguments to mixing class"
|
*
|
||||||
super(args[0]);
|
* @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);
|
* Get a user from a raw [UserId]
|
||||||
return {
|
*
|
||||||
id: v.id as UserId,
|
* @param id the userid to modify
|
||||||
name: v.name || null,
|
*
|
||||||
password: v.password || null,
|
* @returns The user if it exists, null otherwise
|
||||||
salt: v.salt,
|
*/
|
||||||
}
|
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<User | null> {
|
||||||
|
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;
|
* Set the hash of a password in the database for a specific user.
|
||||||
if (res === null || res === undefined) return null;
|
* You are required to hash the password before storing it in the database
|
||||||
return this.userFromRow(res);
|
*
|
||||||
}
|
* @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<User | null> {
|
||||||
|
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<Omit<User, 'id'>>): User | null {
|
getUserOtpSecret(this: IUserDb, id: UserId): string | null {
|
||||||
return 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 UserId = number & { readonly __brand: unique symbol };
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
readonly id: UserId,
|
readonly id: UserId;
|
||||||
readonly name: string,
|
readonly name: string;
|
||||||
readonly salt: string,
|
readonly password?: 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<boolean> {
|
||||||
|
// 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<string | null> {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
// ************************************************************************** //
|
|
||||||
// //
|
|
||||||
// ::: :::::::: //
|
|
||||||
// index.ts :+: :+: :+: //
|
|
||||||
// +:+ +:+ +:+ //
|
|
||||||
// By: maiboyer <maiboyer@student.42.fr> +#+ +:+ +#+ //
|
|
||||||
// +#+#+#+#+#+ +#+ //
|
|
||||||
// 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<UUIDv7, InvalidUUID> {
|
|
||||||
// if (!isUUIDv7(value)) return Result.error(new InvalidUUID());
|
|
||||||
//
|
|
||||||
// return Result.ok(value.toLowerCase() as UUIDv7);
|
|
||||||
//}
|
|
||||||
|
|
||||||
export function newUUIDv7(): UUIDv7 {
|
|
||||||
return uuidv7() as UUIDv7;
|
|
||||||
}
|
|
||||||
10
src/auth/entrypoint.sh
Normal file
10
src/auth/entrypoint.sh
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
set -x
|
||||||
|
# do anything here
|
||||||
|
|
||||||
|
cp -r /extra /files
|
||||||
|
|
||||||
|
# run the CMD [ ... ] from the dockerfile
|
||||||
|
exec "$@"
|
||||||
0
src/auth/extra/.gitkeep
Normal file
0
src/auth/extra/.gitkeep
Normal file
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
"@fastify/multipart": "^9.0.3",
|
"@fastify/multipart": "^9.0.3",
|
||||||
"@fastify/sensible": "^6.0.0",
|
"@fastify/sensible": "^6.0.0",
|
||||||
"@fastify/static": "^8.2.0",
|
"@fastify/static": "^8.2.0",
|
||||||
|
"@sinclair/typebox": "^0.34.40",
|
||||||
"fastify": "^5.0.0",
|
"fastify": "^5.0.0",
|
||||||
"fastify-cli": "^7.4.0",
|
"fastify-cli": "^7.4.0",
|
||||||
"fastify-plugin": "^5.0.0",
|
"fastify-plugin": "^5.0.0",
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,12 @@ import fastifyFormBody from '@fastify/formbody'
|
||||||
import fastifyMultipart from '@fastify/multipart'
|
import fastifyMultipart from '@fastify/multipart'
|
||||||
import { mkdir } from 'node:fs/promises'
|
import { mkdir } from 'node:fs/promises'
|
||||||
import fp from 'fastify-plugin'
|
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 });
|
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 });
|
const routes = import.meta.glob('./routes/**/*.ts', { eager: true });
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -21,15 +25,17 @@ const app: FastifyPluginAsync = async (
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
// Place here your custom code!
|
// Place here your custom code!
|
||||||
for (const plugin of Object.values(plugins)) {
|
for (const plugin of Object.values(plugins)) {
|
||||||
void fastify.register(plugin, {});
|
void fastify.register(plugin as any, {});
|
||||||
}
|
}
|
||||||
for (const route of Object.values(routes)) {
|
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(fastifyFormBody, {})
|
||||||
void fastify.register(fastifyMultipart, {})
|
void fastify.register(fastifyMultipart, {})
|
||||||
|
console.log(fastify.db.getUser(0 as any));
|
||||||
|
|
||||||
// The use of fastify-plugin is required to be able
|
// The use of fastify-plugin is required to be able
|
||||||
// to export the decorators to the outer scope
|
// to export the decorators to the outer scope
|
||||||
|
|
|
||||||
81
src/auth/src/routes/login.ts
Normal file
81
src/auth/src/routes/login.ts
Normal file
|
|
@ -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<typeof LoginReq>;
|
||||||
|
|
||||||
|
|
||||||
|
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<typeof LoginRes>;
|
||||||
|
|
||||||
|
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||||
|
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;
|
||||||
|
|
@ -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<void> => {
|
|
||||||
// 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: `/<USERID_HERE>`
|
|
||||||
// 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
|
|
||||||
|
|
||||||
99
src/auth/src/routes/signin.ts
Normal file
99
src/auth/src/routes/signin.ts
Normal file
|
|
@ -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<typeof SignInReq>;
|
||||||
|
|
||||||
|
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<typeof SignInRes>;
|
||||||
|
|
||||||
|
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||||
|
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;
|
||||||
20
src/auth/src/routes/whoami.ts
Normal file
20
src/auth/src/routes/whoami.ts
Normal file
|
|
@ -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<typeof WhoAmIRes>;
|
||||||
|
|
||||||
|
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||||
|
fastify.get(
|
||||||
|
"/whoami",
|
||||||
|
{ schema: { response: { "2xx": WhoAmIRes } } },
|
||||||
|
async function(req, res) {
|
||||||
|
return "yes";
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default route;
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// this sould only be used by the docker file !
|
// this sould only be used by the docker file !
|
||||||
|
|
||||||
import fastify, { FastifyInstance } from "fastify";
|
import fastify, { FastifyInstance } from "fastify";
|
||||||
import app from './app.js'
|
import app from "./app"
|
||||||
|
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
const envToLogger = {
|
const envToLogger = {
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,32 @@ import { defineConfig } from 'vite'
|
||||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||||
import nodeExternals from 'rollup-plugin-node-externals'
|
import nodeExternals from 'rollup-plugin-node-externals'
|
||||||
import path from 'node:path'
|
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({
|
export default defineConfig({
|
||||||
root: __dirname, // service root
|
root: __dirname, // service root
|
||||||
plugins: [tsconfigPaths(), nodeExternals()],
|
plugins: [tsconfigPaths(), nodeExternals()],
|
||||||
build: {
|
build: {
|
||||||
|
|
||||||
ssr: true,
|
ssr: true,
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
|
|
@ -17,9 +37,9 @@ export default defineConfig({
|
||||||
fileName: () => 'index.js',
|
fileName: () => 'index.js',
|
||||||
},
|
},
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: [],
|
external: externals,
|
||||||
},
|
},
|
||||||
target: 'node24', // or whatever Node version you use
|
target: 'node22', // or whatever Node version you use
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
minify: false, // for easier debugging
|
minify: false, // for easier debugging
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,10 @@ const app: FastifyPluginAsync = async (
|
||||||
void fastify.register(route as any, {});
|
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(fastifyFormBody, {})
|
||||||
void fastify.register(fastifyMultipart, {})
|
void fastify.register(fastifyMultipart, {})
|
||||||
|
console.log(fastify.db.getUser(1));
|
||||||
|
|
||||||
// The use of fastify-plugin is required to be able
|
// The use of fastify-plugin is required to be able
|
||||||
// to export the decorators to the outer scope
|
// to export the decorators to the outer scope
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// this sould only be used by the docker file !
|
// this sould only be used by the docker file !
|
||||||
|
|
||||||
import fastify, { FastifyInstance } from "fastify";
|
import fastify, { FastifyInstance } from "fastify";
|
||||||
import app from './app.js'
|
import app from "./app"
|
||||||
|
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
const envToLogger = {
|
const envToLogger = {
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export default defineConfig({
|
||||||
external: externals,
|
external: externals,
|
||||||
},
|
},
|
||||||
target: 'node22', // or whatever Node version you use
|
target: 'node22', // or whatever Node version you use
|
||||||
sourcemap: true,
|
sourcemap: false,
|
||||||
minify: false, // for easier debugging
|
minify: true, // for easier debugging
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
159
src/package-lock.json
generated
159
src/package-lock.json
generated
|
|
@ -9,8 +9,12 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"./@shared",
|
"./@shared",
|
||||||
"./icons"
|
"./icons",
|
||||||
|
"./auth"
|
||||||
],
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"bindings": "^1.5.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"rimraf": "^5.0.1"
|
"rimraf": "^5.0.1"
|
||||||
}
|
}
|
||||||
|
|
@ -21,9 +25,12 @@
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/jwt": "^9.1.0",
|
"@fastify/jwt": "^9.1.0",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
"better-sqlite3": "^11.10.0",
|
"better-sqlite3": "^11.10.0",
|
||||||
"fastify": "^5.0.0",
|
"fastify": "^5.0.0",
|
||||||
"fastify-plugin": "^5.0.1",
|
"fastify-plugin": "^5.0.1",
|
||||||
|
"joi": "^18.0.0",
|
||||||
"uuidv7": "^1.0.2"
|
"uuidv7": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -31,6 +38,29 @@
|
||||||
"@types/node": "^22.1.0"
|
"@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": {
|
"icons": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
|
@ -866,6 +896,54 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"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": {
|
"node_modules/@img/sharp-darwin-arm64": {
|
||||||
"version": "0.34.3",
|
"version": "0.34.3",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz",
|
||||||
|
|
@ -1622,6 +1700,27 @@
|
||||||
"win32"
|
"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": {
|
"node_modules/@types/better-sqlite3": {
|
||||||
"version": "7.6.13",
|
"version": "7.6.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
|
||||||
|
|
@ -1643,7 +1742,6 @@
|
||||||
"version": "22.17.0",
|
"version": "22.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.0.tgz",
|
||||||
"integrity": "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==",
|
"integrity": "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
|
|
@ -1733,6 +1831,10 @@
|
||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/auth": {
|
||||||
|
"resolved": "auth",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/avvio": {
|
"node_modules/avvio": {
|
||||||
"version": "9.1.0",
|
"version": "9.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz",
|
||||||
|
|
@ -1770,6 +1872,20 @@
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/better-sqlite3": {
|
||||||
"version": "11.10.0",
|
"version": "11.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
|
||||||
|
|
@ -2687,6 +2803,24 @@
|
||||||
"@pkgjs/parseargs": "^0.11.0"
|
"@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": {
|
"node_modules/joycon": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
|
||||||
|
|
@ -2940,6 +3074,26 @@
|
||||||
"node": ">=10"
|
"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": {
|
"node_modules/obliterator": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz",
|
||||||
|
|
@ -3984,7 +4138,6 @@
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unpipe": {
|
"node_modules/unpipe": {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"./@shared",
|
"./@shared",
|
||||||
"./icons"
|
"./icons",
|
||||||
|
"./auth"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build --workspaces --if-present",
|
"build": "npm run build --workspaces --if-present",
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,4 @@ onlyBuiltDependencies:
|
||||||
- better-sqlite3
|
- better-sqlite3
|
||||||
- esbuild
|
- esbuild
|
||||||
- sharp
|
- sharp
|
||||||
|
- bcrypt
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@shared/database": ["@shared/src/database"],
|
"@shared/database": ["@shared/src/database"],
|
||||||
"@shared/uuid": ["@shared/src/uuid"]
|
"@shared/auth": ["@shared/src/auth"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue