core: adding a linter to the project

This update focuses on improving **code consistency, readability, and maintainability** across the project.  
No functional changes have been introduced.

---

##  New
- Added a dedicated **`eslint.config.js`** configuration file.  
  - Integrates recommended JavaScript and TypeScript ESLint rules.  
  - Enforces consistent use of single quotes, semicolons, spacing, and other style standards.

---

## 🔧 Improvements
- Applied unified formatting to the following directories and files:
  - `src/@shared/src/auth/`  
  - `src/@shared/src/database/`  
  - `src/@shared/src/utils/`  
  - `src/auth/extra/login_demo.js`
- Replaced `let` with `const` where applicable to improve scoping and code safety.
- Standardized error handling and minor syntax refinements for greater clarity.

---

## 📌 Impact
- No changes to application logic or functionality.  
- The codebase is now **easier to read, maintain, and extend**, reducing the risk of style-related issues in the future.
This commit is contained in:
Raphaël 2025-10-03 14:26:07 +02:00 committed by GitHub
commit ff0e218803
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 2081 additions and 455 deletions

40
.github/workflows/lint.yml vendored Normal file
View file

@ -0,0 +1,40 @@
name: Build & Linter
on:
push:
branches:
- "**"
pull_request:
branches:
- "**"
permissions:
contents: read
jobs:
build:
name: Build & Typecheck
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false
- name: Install dependencies with pnpm
working-directory: ./src
run: pnpm install
- name: Check linting
working-directory: ./src
run: pnpm exec eslint . --max-warnings=0
- name: Build
working-directory: ./src
run: pnpm run build

2
.husky/pre-commit Normal file
View file

@ -0,0 +1,2 @@
#!/usr/bin/env sh
pnpm --prefix=./src/ lint-staged

View file

@ -19,6 +19,24 @@
flake-utils.lib.eachDefaultSystem (
system: let
pkgs = nixpkgs.legacyPackages.${system};
tmux-setup = pkgs.writeShellScriptBin "tmux-setup" ''
#!/usr/bin/env sh
SESSION="transcendance"
DIR=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
if ! tmux has-session -t $SESSION 2>/dev/null; then
tmux new-session -d -s $SESSION -c "$DIR" -n dev
tmux send-keys -t $SESSION:0 'vim' C-m
tmux split-window -h -p 30 -t $SESSION:0 -c "$DIR"
tmux send-keys -t $SESSION:0.1 'exec zsh' C-m
tmux split-window -v -p 30 -t $SESSION:0.1 -c "$DIR"
tmux send-keys -t $SESSION:0.2 'watch -n0.5 npx --prefix=./src/ eslint .' C-m
tmux new-window -t $SESSION:1 -n git -c "$DIR"
tmux send-keys -t $SESSION:1 'lazygit' C-m
fi
tmux select-window -t $SESSION:0
tmux select-pane -t $SESSION:0.0
tmux attach -t $SESSION
'';
in {
devShell = pkgs.mkShellNoCC {
packages = with pkgs; [
@ -31,6 +49,7 @@
dbmlSQLite.packages.${system}.default
sqlite-interactive
clang
tmux-setup
];
shellHook = ''
export PODMAN_COMPOSE_WARNING_LOGS="false";

2
src/.husky/pre-commit Normal file
View file

@ -0,0 +1,2 @@
#!/usr/bin/env sh
pnpm lint-staged

View file

@ -1,23 +1,23 @@
import OTP from "otp";
import cookie from "@fastify/cookie";
import fastifyJwt from "@fastify/jwt";
import fp from "fastify-plugin";
import { FastifyPluginAsync, preValidationAsyncHookHandler } from "fastify";
import { Static, Type } from "@sinclair/typebox";
import { UserId } from "@shared/database/mixin/user";
import { useDatabase } from "@shared/database";
import { isNullish, makeResponse } from "@shared/utils";
import OTP from 'otp';
import cookie from '@fastify/cookie';
import fastifyJwt from '@fastify/jwt';
import fp from 'fastify-plugin';
import { FastifyPluginAsync, preValidationAsyncHookHandler } from 'fastify';
import { Static, Type } from '@sinclair/typebox';
import { UserId } from '@shared/database/mixin/user';
import { useDatabase } from '@shared/database';
import { isNullish, makeResponse } from '@shared/utils';
const kRouteAuthDone = Symbol("shared-route-auth-done");
const kRouteAuthDone = Symbol('shared-route-auth-done');
type AuthedUser = {
id: UserId;
name: string;
};
declare module "fastify" {
declare module 'fastify' {
export interface FastifyInstance {
signJwt: (kind: "auth" | "otp", who: string) => string;
signJwt: (kind: 'auth' | 'otp', who: string) => string;
[s: symbol]: boolean;
}
export interface FastifyRequest {
@ -26,17 +26,22 @@ declare module "fastify" {
export interface FastifyContextConfig {
requireAuth?: boolean;
}
export interface RouteOptions {
[kRouteAuthDone]: boolean;
}
}
export const Otp = OTP;
let jwtAdded = false;
export const jwtPlugin = fp<FastifyPluginAsync>(async (fastify, _opts) => {
void _opts;
if (jwtAdded) return;
jwtAdded = true;
let env = process.env.JWT_SECRET;
if (isNullish(env)) throw "JWT_SECRET is not defined";
if (!fastify.hasDecorator("signJwt")) {
void fastify.decorate("signJwt", (kind, who) =>
const env = process.env.JWT_SECRET;
if (isNullish(env)) throw 'JWT_SECRET is not defined';
if (!fastify.hasDecorator('signJwt')) {
void fastify.decorate('signJwt', (kind, who) =>
fastify.jwt.sign({ kind, who, createdAt: Date.now() }),
);
void fastify.register(fastifyJwt, {
@ -48,16 +53,16 @@ export const jwtPlugin = fp<FastifyPluginAsync>(async (fastify, _opts) => {
export const JwtType = Type.Object({
kind: Type.Union([
Type.Const("otp", {
description: "the token is only valid for otp call",
Type.Const('otp', {
description: 'the token is only valid for otp call',
}),
Type.Const("auth", {
description: "the token is valid for authentication",
Type.Const('auth', {
description: 'the token is valid for authentication',
}),
]),
who: Type.String({ description: "the login of the user" }),
who: Type.String({ description: 'the login of the user' }),
createdAt: Type.Integer({
description: "Unix timestamp of when the token as been created at",
description: 'Unix timestamp of when the token as been created at',
}),
});
@ -65,56 +70,63 @@ export type JwtType = Static<typeof JwtType>;
let authAdded = false;
export const authPlugin = fp<FastifyPluginAsync>(async (fastify, _opts) => {
if (authAdded) return void console.log("skipping");
void _opts;
if (authAdded) return void console.log('skipping');
authAdded = true;
await fastify.register(useDatabase as any, {});
await fastify.register(jwtPlugin as any, {});
await fastify.register(useDatabase as FastifyPluginAsync, {});
await fastify.register(jwtPlugin as FastifyPluginAsync, {});
await fastify.register(cookie);
if (!fastify.hasRequestDecorator("authUser"))
fastify.decorateRequest("authUser", undefined);
fastify.addHook("onRoute", (routeOpts) => {
if (!fastify.hasRequestDecorator('authUser')) { fastify.decorateRequest('authUser', undefined); }
fastify.addHook('onRoute', (routeOpts) => {
if (
routeOpts.config?.requireAuth &&
!(routeOpts as any)[kRouteAuthDone]
!routeOpts[kRouteAuthDone]
) {
let f: preValidationAsyncHookHandler = async function(req, res) {
const f: preValidationAsyncHookHandler = async function(req, res) {
try {
if (isNullish(req.cookies.token))
if (isNullish(req.cookies.token)) {
return res
.clearCookie("token")
.clearCookie('token')
.send(
JSON.stringify(makeResponse("notLoggedIn", "auth.noCookie")),
JSON.stringify(makeResponse('notLoggedIn', 'auth.noCookie')),
);
let tok = this.jwt.verify<JwtType>(req.cookies.token);
if (tok.kind != "auth")
}
const tok = this.jwt.verify<JwtType>(req.cookies.token);
if (tok.kind != 'auth') {
return res
.clearCookie("token")
.clearCookie('token')
.send(
JSON.stringify(makeResponse("notLoggedIn", "auth.invalidKind")),
JSON.stringify(makeResponse('notLoggedIn', 'auth.invalidKind')),
);
let user = this.db.getUserFromName(tok.who);
if (isNullish(user))
}
const user = this.db.getUserFromName(tok.who);
if (isNullish(user)) {
return res
.clearCookie("token")
.clearCookie('token')
.send(
JSON.stringify(makeResponse("notLoggedIn", "auth.noUser")),
JSON.stringify(makeResponse('notLoggedIn', 'auth.noUser')),
);
}
req.authUser = { id: user.id, name: tok.who };
} catch {
}
catch {
return res
.clearCookie("token")
.send(JSON.stringify(makeResponse("notLoggedIn", "auth.invalid")));
.clearCookie('token')
.send(JSON.stringify(makeResponse('notLoggedIn', 'auth.invalid')));
}
};
if (!routeOpts.preValidation) {
routeOpts.preValidation = [f];
} else if (Array.isArray(routeOpts.preValidation)) {
}
else if (Array.isArray(routeOpts.preValidation)) {
routeOpts.preValidation.push(f);
} else {
}
else {
routeOpts.preValidation = [routeOpts.preValidation, f];
}
(routeOpts as any)[kRouteAuthDone] = true;
routeOpts[kRouteAuthDone] = true;
}
});
});

View file

@ -1,8 +1,8 @@
import fp from 'fastify-plugin'
import { FastifyInstance, FastifyPluginAsync } from 'fastify'
import fp from 'fastify-plugin';
import { FastifyInstance, FastifyPluginAsync } from 'fastify';
import { Database as DbImpl } from "./mixin/_base";
import { UserImpl, IUserDb } from "./mixin/user";
import { Database as DbImpl } from './mixin/_base';
import { UserImpl, IUserDb } from './mixin/user';
import { isNullish } from '@shared/utils';
@ -21,17 +21,15 @@ let dbAdded = false;
export const useDatabase = fp<FastifyPluginAsync>(async function(
f: FastifyInstance,
_options: {}) {
if (dbAdded)
return;
_options: object) {
void _options;
if (dbAdded) { return; }
dbAdded = true;
let path = process.env.DATABASE_DIR;
if (isNullish(path))
throw "env `DATABASE_DIR` not defined";
f.log.info(`Opening database with path: ${path}/database.db`)
let db: Database = new DbImpl(`${path}/database.db`) as Database;
if (!f.hasDecorator("db"))
f.decorate('db', db);
const path = process.env.DATABASE_DIR;
if (isNullish(path)) { throw 'env `DATABASE_DIR` not defined'; }
f.log.info(`Opening database with path: ${path}/database.db`);
const db: Database = new DbImpl(`${path}/database.db`) as Database;
if (!f.hasDecorator('db')) { f.decorate('db', db); }
});
export default useDatabase;

View file

@ -1,7 +1,7 @@
import sqlite from "better-sqlite3";
import sqlite from 'better-sqlite3';
// @ts-ignore: this file is included using vite, typescript doesn't know how to include this...
import initSql from "../init.sql?raw"
// @ts-expect-error: this file is included using vite, typescript doesn't know how to include this...
import initSql from '../init.sql?raw';
export type SqliteReturn = object | undefined;
@ -12,31 +12,31 @@ export type SqliteReturn = object | undefined;
// To create a new query function, go open another file, create a class that inherit from this class
// in the `index.ts` file, import the new class, and make the `Database` class inherit it
export class Database {
private db: sqlite.Database;
private st: Map<string, sqlite.Statement> = new Map();
private db: sqlite.Database;
private st: Map<string, sqlite.Statement> = new Map();
/**
/**
* Create a new instance of the database, and init it to a known state
* the file ./init.sql will be ran onto the database, creating any table that might be missing
*/
constructor(db_path: string) {
this.db = sqlite(db_path, {});
this.db.pragma('journal_mode = WAL');
this.db.transaction(() => this.db.exec(initSql))();
}
constructor(db_path: string) {
this.db = sqlite(db_path, {});
this.db.pragma('journal_mode = WAL');
this.db.transaction(() => this.db.exec(initSql))();
}
/**
/**
* close the database
*/
public destroy(): void {
// remove any statement from the cache
this.st.clear();
// close the database
this.db?.close();
}
public destroy(): void {
// remove any statement from the cache
this.st.clear();
// close the database
this.db?.close();
}
/**
/**
* use this to create queries. This will create statements (kinda expensive) and cache them
* since they will be cached, this means that they are only created once,
* otherwise they'll be just spat out from the cache
@ -46,12 +46,12 @@ export class Database {
* @example this.prepare('SELECT * FROM users WHERE id = ?')
* @example this.prepare('SELECT * FROM users LIMIT 100 OFFSET ?')
*/
protected prepare(query: string): sqlite.Statement {
let st = this.st.get(query);
if (st !== undefined) return st;
protected prepare(query: string): sqlite.Statement {
let st = this.st.get(query);
if (st !== undefined) return st;
st = this.db.prepare(query);
this.st.set(query, st);
return st;
}
st = this.db.prepare(query);
this.st.set(query, st);
return st;
}
}

View file

@ -1,4 +1,4 @@
import type { Database } from "./_base";
import type { Database } from './_base';
// never use this directly
@ -9,28 +9,28 @@ export interface ITemplateDb extends Database {
};
export const UserImpl: Omit<ITemplateDb, keyof Database> = {
/**
/**
* whole function description
*
* @param id the argument description
*
* @returns what does the function return ?
*/
normalFunction(this: ITemplateDb, id: TemplateId): TemplateData | undefined {
void id;
return undefined;
},
/**
normalFunction(this: ITemplateDb, id: TemplateId): TemplateData | undefined {
void id;
return undefined;
},
/**
* whole function description
*
* @param id the argument description
*
* @returns what does the function return ?
*/
async asyncFunction(this: ITemplateDb, id: TemplateId): Promise<TemplateData | undefined> {
void id;
return undefined;
},
async asyncFunction(this: ITemplateDb, id: TemplateId): Promise<TemplateData | undefined> {
void id;
return undefined;
},
};
export type TemplateId = number & { readonly __brand: unique symbol };
@ -43,13 +43,13 @@ export type TemplateData = {
// this function will be able to be called from everywhere
export async function freeFloatingExportedFunction(): Promise<boolean> {
return false;
return false;
}
// this function will never be able to be called outside of this module
async function privateFunction(): Promise<string | undefined> {
return undefined
return undefined;
}
//silence warnings
// silence warnings
void privateFunction;

View file

@ -1,7 +1,7 @@
import type { Database, SqliteReturn } from "./_base";
import { Otp } from "@shared/auth";
import { isNullish } from "@shared/utils";
import * as bcrypt from "bcrypt";
import type { Database, SqliteReturn } from './_base';
import { Otp } from '@shared/auth';
import { isNullish } from '@shared/utils';
import * as bcrypt from 'bcrypt';
// never use this directly
@ -38,8 +38,8 @@ export const UserImpl: Omit<IUserDb, keyof Database> = {
getUserFromName(this: IUserDb, name: string): User | undefined {
return userFromRow(
this.prepare(
"SELECT * FROM user WHERE name = @name LIMIT 1",
).get({ name }),
'SELECT * FROM user WHERE name = @name LIMIT 1',
).get({ name }) as (Partial<User> | undefined),
);
},
@ -52,9 +52,9 @@ export const UserImpl: Omit<IUserDb, keyof Database> = {
*/
getUserFromRawId(this: IUserDb, id: number): User | undefined {
return userFromRow(
this.prepare("SELECT * FROM user WHERE id = @id LIMIT 1").get({
this.prepare('SELECT * FROM user WHERE id = @id LIMIT 1').get({
id,
}) as SqliteReturn,
}) as (Partial<User> | undefined),
);
},
@ -70,8 +70,8 @@ export const UserImpl: Omit<IUserDb, keyof Database> = {
password = await hashPassword(password);
return userFromRow(
this.prepare(
"INSERT OR FAIL INTO user (name, password) VALUES (@name, @password) RETURNING *",
).get({ name, password }),
'INSERT OR FAIL INTO user (name, password) VALUES (@name, @password) RETURNING *',
).get({ name, password }) as (Partial<User> | undefined),
);
},
@ -88,30 +88,29 @@ export const UserImpl: Omit<IUserDb, keyof Database> = {
password = await hashPassword(password);
return userFromRow(
this.prepare(
"UPDATE OR FAIL user SET password = @password WHERE id = @id RETURNING *",
'UPDATE OR FAIL user SET password = @password WHERE id = @id RETURNING *',
).get({ password, id }) as SqliteReturn,
);
},
getUserOtpSecret(this: IUserDb, id: UserId): string | undefined {
let otp: any = this.prepare("SELECT otp FROM user WHERE id = @id LIMIT 1").get({ id }) as SqliteReturn;
const otp = this.prepare('SELECT otp FROM user WHERE id = @id LIMIT 1').get({ id }) as ({ otp: string } | null | undefined);
if (isNullish(otp?.otp)) return undefined;
return otp.otp;
},
ensureUserOtpSecret(this: IUserDb, id: UserId): string | undefined {
let otp = this.getUserOtpSecret(id);
if (!isNullish(otp))
return otp;
let otpGen = new Otp();
const res: any = this.prepare("UPDATE OR IGNORE user SET otp = @otp WHERE id = @id RETURNING otp")
.get({ id, otp: otpGen.secret });
const otp = this.getUserOtpSecret(id);
if (!isNullish(otp)) { return otp; }
const otpGen = new Otp();
const res = this.prepare('UPDATE OR IGNORE user SET otp = @otp WHERE id = @id RETURNING otp')
.get({ id, otp: otpGen.secret }) as ({ otp: string } | null | undefined);
return res?.otp;
},
deleteUserOtpSecret(this: IUserDb, id: UserId): void {
this.prepare("UPDATE OR IGNORE user SET otp = NULL WHERE id = @id").run({ id });
}
this.prepare('UPDATE OR IGNORE user SET otp = NULL WHERE id = @id').run({ id });
},
};
export type UserId = number & { readonly __brand: unique symbol };
@ -155,12 +154,14 @@ async function hashPassword(
*
* @returns The user if it exists, undefined otherwise
*/
function userFromRow(row: any): User | undefined {
function userFromRow(row?: Partial<User>): User | undefined {
if (isNullish(row)) return undefined;
if (isNullish(row.id)) return undefined;
if (isNullish(row.name)) return undefined;
return {
id: row.id as UserId,
name: row.name || undefined,
password: row.password || undefined,
otp: row.otp || undefined,
id: row.id,
name: row.name,
password: row.password ?? undefined,
otp: row.otp ?? undefined,
};
}

View file

@ -1,4 +1,4 @@
import { Type } from "@sinclair/typebox";
import { TObject, TProperties, Type } from '@sinclair/typebox';
/**
* @description Represent a message key
@ -12,7 +12,7 @@ import { Type } from "@sinclair/typebox";
* @example `pong.you.lost`
*/
export type MessageKey = string;
export type ResponseBase<T = {}> = {
export type ResponseBase<T = object> = {
kind: string,
msg: MessageKey,
payload?: T,
@ -26,9 +26,9 @@ export type ResponseBase<T = {}> = {
* @example makeResponse("failure", "login.failure.invalid")
* @example makeResponse("success", "login.success", { token: "supersecrettoken" })
*/
export function makeResponse<T = {}>(kind: string, key: MessageKey, payload?: T): ResponseBase<T> {
console.log(`making response {kind: ${JSON.stringify(kind)}; key: ${JSON.stringify(key)}}`)
return { kind, msg: key, payload }
export function makeResponse<T = object>(kind: string, key: MessageKey, payload?: T): ResponseBase<T> {
console.log(`making response {kind: ${JSON.stringify(kind)}; key: ${JSON.stringify(key)}}`);
return { kind, msg: key, payload };
}
@ -39,11 +39,12 @@ export function makeResponse<T = {}>(kind: string, key: MessageKey, payload?: T)
* @example typeResponse("otpRequired", "login.otpRequired", { token: Type.String() })
* @example typeResponse("success", "login.success", { token: Type.String() })
*/
export function typeResponse(kind: string, key: MessageKey | MessageKey[], payload?: any): any {
export function typeResponse(kind: string, key: MessageKey | MessageKey[], payload?: TProperties): TObject<TProperties> {
let tKey;
if (key instanceof Array) {
tKey = Type.Union(key.map(l => Type.Const(l)));
} else {
}
else {
tKey = Type.Const(key);
}
@ -51,10 +52,9 @@ export function typeResponse(kind: string, key: MessageKey | MessageKey[], paylo
kind: Type.Const(kind),
msg: tKey,
};
if (payload !== undefined)
Object.assign(Ty, { payload: Type.Object(payload) })
if (payload !== undefined) {Object.assign(Ty, { payload: Type.Object(payload) });}
return Type.Object(Ty)
return Type.Object(Ty);
}
/**
@ -69,5 +69,5 @@ export function typeResponse(kind: string, key: MessageKey | MessageKey[], paylo
* @example assert_equal(isNullish(false), false);
*/
export function isNullish<T>(v: T | undefined | null): v is (null | undefined) {
return v === null || v === undefined
return v === null || v === undefined;
}

View file

@ -1,107 +1,104 @@
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
'Content-Type': 'application/json',
};
const tUsername = document.querySelector("#t-username")
const tUsername = document.querySelector('#t-username');
const iUsername = document.querySelector("#i-username");
const iPassword = document.querySelector("#i-password");
const iOtp = document.querySelector("#i-otp");
const iUsername = document.querySelector('#i-username');
const iPassword = document.querySelector('#i-password');
const iOtp = document.querySelector('#i-otp');
const bOtpSend = document.querySelector("#b-otpSend");
const bLogin = document.querySelector("#b-login");
const bLogout = document.querySelector("#b-logout");
const bSignin = document.querySelector("#b-signin");
const bWhoami = document.querySelector("#b-whoami");
const bOtpSend = document.querySelector('#b-otpSend');
const bLogin = document.querySelector('#b-login');
const bLogout = document.querySelector('#b-logout');
const bSignin = document.querySelector('#b-signin');
const bWhoami = document.querySelector('#b-whoami');
const bOtpStatus = document.querySelector("#b-otpStatus");
const bOtpEnable = document.querySelector("#b-otpEnable");
const bOtpDisable = document.querySelector("#b-otpDisable");
const bOtpStatus = document.querySelector('#b-otpStatus');
const bOtpEnable = document.querySelector('#b-otpEnable');
const bOtpDisable = document.querySelector('#b-otpDisable');
const dResponse = document.querySelector("#d-response");
const dResponse = document.querySelector('#d-response');
function setResponse(obj) {
let obj_str = JSON.stringify(obj, null, 4);
const obj_str = JSON.stringify(obj, null, 4);
dResponse.innerText = obj_str;
}
let otpToken = null;
bOtpSend.addEventListener("click", async () => {
let res = await fetch("/api/auth/otp", { method: "POST", body: JSON.stringify({ code: iOtp.value, token: otpToken }), headers });
bOtpSend.addEventListener('click', async () => {
const res = await fetch('/api/auth/otp', { method: 'POST', body: JSON.stringify({ code: iOtp.value, token: otpToken }), headers });
const json = await res.json();
setResponse(json);
if (json.kind === "success") {
if (json?.payload?.token)
document.cookie = `token=${json?.payload?.token}`;
if (json.kind === 'success') {
if (json?.payload?.token) {document.cookie = `token=${json?.payload?.token}`;}
}
});
bOtpStatus.addEventListener("click", async () => {
let res = await fetch("/api/auth/statusOtp");
bOtpStatus.addEventListener('click', async () => {
const res = await fetch('/api/auth/statusOtp');
const json = await res.json();
setResponse(json);
});
bOtpEnable.addEventListener("click", async () => {
let res = await fetch("/api/auth/enableOtp", { method: "PUT" });
bOtpEnable.addEventListener('click', async () => {
const res = await fetch('/api/auth/enableOtp', { method: 'PUT' });
const json = await res.json();
setResponse(json);
});
bOtpDisable.addEventListener("click", async () => {
let res = await fetch("/api/auth/disableOtp", { method: "PUT" });
bOtpDisable.addEventListener('click', async () => {
const res = await fetch('/api/auth/disableOtp', { method: 'PUT' });
const json = await res.json();
setResponse(json);
});
bWhoami.addEventListener("click", async () => {
let username = "";
bWhoami.addEventListener('click', async () => {
let username = '';
try {
let res = await fetch("/api/auth/whoami");
const res = await fetch('/api/auth/whoami');
const json = await res.json();
setResponse(json);
if (json?.kind === "success")
username = json?.payload?.name;
else
username = `<not logged in:${json.msg}>`
} catch {
username = `<not logged in: threw>`
if (json?.kind === 'success') {username = json?.payload?.name;}
else {username = `<not logged in:${json.msg}>`;}
}
catch {
username = '<not logged in: threw>';
}
tUsername.innerText = username;
});
bLogin.addEventListener("click", async () => {
bLogin.addEventListener('click', async () => {
const name = iUsername.value;
const password = iPassword.value;
let res = await fetch("/api/auth/login", { method: "POST", body: JSON.stringify({ name, password }), headers });
let json = await res.json();
if (json?.kind === "otpRequired") {
const res = await fetch('/api/auth/login', { method: 'POST', body: JSON.stringify({ name, password }), headers });
const json = await res.json();
if (json?.kind === 'otpRequired') {
otpToken = json?.payload?.token;
} else if (json?.kind === "success") {
if (json?.payload?.token)
document.cookie = `token=${json?.payload?.token}`;
}
else if (json?.kind === 'success') {
if (json?.payload?.token) {document.cookie = `token=${json?.payload?.token}`;}
}
setResponse(json);
})
});
bLogout.addEventListener("click", async () => {
let res = await fetch("/api/auth/logout", { method: "POST" });
bLogout.addEventListener('click', async () => {
const res = await fetch('/api/auth/logout', { method: 'POST' });
setResponse(await res.json());
})
});
bSignin.addEventListener("click", async () => {
bSignin.addEventListener('click', async () => {
const name = iUsername.value;
const password = iPassword.value;
let res = await fetch("/api/auth/signin", { method: "POST", body: JSON.stringify({ name, password }), headers });
let json = await res.json();
if (json?.payload?.token)
document.cookie = `token=${json?.payload?.token};`;
const res = await fetch('/api/auth/signin', { method: 'POST', body: JSON.stringify({ name, password }), headers });
const json = await res.json();
if (json?.payload?.token) {document.cookie = `token=${json?.payload?.token};`;}
setResponse(json);
})
});

View file

@ -1,17 +1,14 @@
import { FastifyPluginAsync } from 'fastify'
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'
import { FastifyPluginAsync } from 'fastify';
import fastifyFormBody from '@fastify/formbody';
import fastifyMultipart from '@fastify/multipart';
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...
// @ts-expect-error: 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...
// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this...
const routes = import.meta.glob('./routes/**/*.ts', { eager: true });
// When using .decorate you have to specify added properties for Typescript
declare module 'fastify' {
export interface FastifyInstance {
@ -19,25 +16,23 @@ declare module 'fastify' {
}
}
const app: FastifyPluginAsync = async (
fastify,
opts
): Promise<void> => {
await fastify.register(db.useDatabase as any, {})
await fastify.register(auth.jwtPlugin as any, {})
await fastify.register(auth.authPlugin as any, {})
const app: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
void opts;
await fastify.register(db.useDatabase as FastifyPluginAsync, {});
await fastify.register(auth.jwtPlugin as FastifyPluginAsync, {});
await fastify.register(auth.authPlugin as FastifyPluginAsync, {});
// Place here your custom code!
for (const plugin of Object.values(plugins)) {
void fastify.register(plugin as any, {});
void fastify.register(plugin as FastifyPluginAsync, {});
}
for (const route of Object.values(routes)) {
void fastify.register(route as any, {});
void fastify.register(route as FastifyPluginAsync, {});
}
void fastify.register(fastifyFormBody, {})
void fastify.register(fastifyMultipart, {})
}
void fastify.register(fastifyFormBody, {});
void fastify.register(fastifyMultipart, {});
};
export default app
export { app }
export default app;
export { app };

View file

@ -1,5 +1,5 @@
import fp from 'fastify-plugin'
import sensible, { FastifySensibleOptions } from '@fastify/sensible'
import fp from 'fastify-plugin';
import sensible, { FastifySensibleOptions } from '@fastify/sensible';
/**
* This plugins adds some utilities to handle http errors
@ -7,5 +7,5 @@ import sensible, { FastifySensibleOptions } from '@fastify/sensible'
* @see https://github.com/fastify/fastify-sensible
*/
export default fp<FastifySensibleOptions>(async (fastify) => {
fastify.register(sensible)
})
fastify.register(sensible);
});

View file

@ -1,25 +1,26 @@
import { FastifyPluginAsync } from "fastify";
import { FastifyPluginAsync } from 'fastify';
import { Static, Type } from "@sinclair/typebox";
import { makeResponse, typeResponse, isNullish } from "@shared/utils"
import { Static, Type } from '@sinclair/typebox';
import { makeResponse, typeResponse, isNullish } from '@shared/utils';
export const WhoAmIRes = Type.Union([
typeResponse("success", "disableOtp.success"),
typeResponse("failure", "disableOtp.failure.generic")
typeResponse('success', 'disableOtp.success'),
typeResponse('failure', 'disableOtp.failure.generic'),
]);
export type WhoAmIRes = Static<typeof WhoAmIRes>;
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
void _opts;
fastify.put(
"/api/auth/disableOtp",
{ schema: { response: { "2xx": WhoAmIRes } }, config: { requireAuth: true } },
'/api/auth/disableOtp',
{ schema: { response: { '2xx': WhoAmIRes } }, config: { requireAuth: true } },
async function(req, _res) {
if (isNullish(req.authUser))
return makeResponse("failure", "disableOtp.failure.generic");
void _res;
if (isNullish(req.authUser)) {return makeResponse('failure', 'disableOtp.failure.generic');}
this.db.deleteUserOtpSecret(req.authUser.id);
return makeResponse("success", "disableOtp.success");
return makeResponse('success', 'disableOtp.success');
},
);
};

View file

@ -1,29 +1,29 @@
import { FastifyPluginAsync } from "fastify";
import { FastifyPluginAsync } from 'fastify';
import { Static, Type } from "@sinclair/typebox";
import { isNullish, makeResponse, typeResponse } from "@shared/utils"
import { Otp } from "@shared/auth";
import { Static, Type } from '@sinclair/typebox';
import { isNullish, makeResponse, typeResponse } from '@shared/utils';
import { Otp } from '@shared/auth';
export const WhoAmIRes = Type.Union([
typeResponse("success", "enableOtp.success", { url: Type.String({ description: "The otp url to feed into a 2fa app" }) }),
typeResponse("failure", ["enableOtp.failure.noUser", "enableOtp.failure.noSecret"])
typeResponse('success', 'enableOtp.success', { url: Type.String({ description: 'The otp url to feed into a 2fa app' }) }),
typeResponse('failure', ['enableOtp.failure.noUser', 'enableOtp.failure.noSecret']),
]);
export type WhoAmIRes = Static<typeof WhoAmIRes>;
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
void _opts;
fastify.put(
"/api/auth/enableOtp",
{ schema: { response: { "2xx": WhoAmIRes } }, config: { requireAuth: true } },
'/api/auth/enableOtp',
{ schema: { response: { '2xx': WhoAmIRes } }, config: { requireAuth: true } },
async function(req, _res) {
if (isNullish(req.authUser))
return makeResponse("failure", "enableOtp.failure.noUser");
let otpSecret = this.db.ensureUserOtpSecret(req.authUser!.id);
if (isNullish(otpSecret))
return makeResponse("failure", "enableOtp.failure.noSecret");
let otp = new Otp({ secret: otpSecret });
return makeResponse("success", "enableOtp.success", { url: otp.totpURL });
void _res;
if (isNullish(req.authUser)) {return makeResponse('failure', 'enableOtp.failure.noUser');}
const otpSecret = this.db.ensureUserOtpSecret(req.authUser!.id);
if (isNullish(otpSecret)) {return makeResponse('failure', 'enableOtp.failure.noSecret');}
const otp = new Otp({ secret: otpSecret });
return makeResponse('success', 'enableOtp.success', { url: otp.totpURL });
},
);
};

View file

@ -1,8 +1,8 @@
import { FastifyPluginAsync } from "fastify";
import { FastifyPluginAsync } from 'fastify';
import { Static, Type } from "@sinclair/typebox";
import { typeResponse, makeResponse, isNullish } from "@shared/utils"
import { verifyUserPassword } from "@shared/database/mixin/user";
import { Static, Type } from '@sinclair/typebox';
import { typeResponse, makeResponse, isNullish } from '@shared/utils';
import { verifyUserPassword } from '@shared/database/mixin/user';
export const LoginReq = Type.Object({
name: Type.String(),
@ -12,44 +12,44 @@ export const LoginReq = Type.Object({
export type LoginReq = Static<typeof LoginReq>;
export const LoginRes = Type.Union([
typeResponse("failed", ["login.failed.generic", "login.failed.invalid"]),
typeResponse("otpRequired", "login.otpRequired", { token: Type.String({ description: "JWT to send with the OTP to finish login" }) }),
typeResponse("success", "login.success", { token: Type.String({ description: "JWT that represent a logged in user" }) }),
typeResponse('failed', ['login.failed.generic', 'login.failed.invalid']),
typeResponse('otpRequired', 'login.otpRequired', { token: Type.String({ description: 'JWT to send with the OTP to finish login' }) }),
typeResponse('success', 'login.success', { token: Type.String({ description: 'JWT that represent a logged in user' }) }),
]);
export type LoginRes = Static<typeof LoginRes>;
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
void _opts;
fastify.post<{ Body: LoginReq; Response: LoginRes }>(
"/api/auth/login",
{ schema: { body: LoginReq, response: { "2xx": LoginRes } }, },
'/api/auth/login',
{ schema: { body: LoginReq, response: { '2xx': LoginRes } } },
async function(req, _res) {
void _res;
try {
let { name, password } = req.body;
let user = this.db.getUserFromName(name);
const { name, password } = req.body;
const user = this.db.getUserFromName(name);
// does the user exist
// does it have a password setup ?
if (isNullish(user?.password))
return makeResponse("failed", "login.failed.invalid");
if (isNullish(user?.password)) {return makeResponse('failed', 'login.failed.invalid');}
// does the password he provided match the one we have
if (!(await verifyUserPassword(user, password)))
return makeResponse("failed", "login.failed.invalid");
if (!(await verifyUserPassword(user, password))) {return makeResponse('failed', 'login.failed.invalid');}
// does the user has 2FA up ?
if (!isNullish(user.otp)) {
// yes -> we ask them to fill it,
// send them somehting to verify that they indeed passed throught the user+password phase
return makeResponse("otpRequired", "login.otpRequired", { token: this.signJwt("otp", user.name) });
return makeResponse('otpRequired', 'login.otpRequired', { token: this.signJwt('otp', user.name) });
}
// every check has been passed, they are now logged in, using this token to say who they are...
return makeResponse("success", "login.success", { token: this.signJwt("auth", user.name) });
return makeResponse('success', 'login.success', { token: this.signJwt('auth', user.name) });
}
catch {
return makeResponse("failed", "login.failed.generic");
return makeResponse('failed', 'login.failed.generic');
}
},
);

View file

@ -1,10 +1,12 @@
import { FastifyPluginAsync } from "fastify";
import { FastifyPluginAsync } from 'fastify';
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
void _opts;
fastify.post(
"/api/auth/logout",
'/api/auth/logout',
async function(_req, res) {
return res.clearCookie("token").send("{}")
void _req;
return res.clearCookie('token').send('{}');
},
);
};

View file

@ -1,19 +1,19 @@
import { FastifyPluginAsync } from "fastify";
import { FastifyPluginAsync } from 'fastify';
import { Static, Type } from "@sinclair/typebox";
import { JwtType, Otp } from "@shared/auth";
import { typeResponse, makeResponse, isNullish } from "@shared/utils";
import { Static, Type } from '@sinclair/typebox';
import { JwtType, Otp } from '@shared/auth';
import { typeResponse, makeResponse, isNullish } from '@shared/utils';
const OtpReq = Type.Object({
token: Type.String({ description: "The token given at the login phase" }),
code: Type.String({ description: "The OTP given by the user" }),
token: Type.String({ description: 'The token given at the login phase' }),
code: Type.String({ description: 'The OTP given by the user' }),
});
type OtpReq = Static<typeof OtpReq>;
const OtpRes = Type.Union([
typeResponse("failed", ["otp.failed.generic", "otp.failed.invalid", "otp.failed.timeout", "otp.failed.noSecret"]),
typeResponse("success", "otp.success", { token: Type.String({ description: "the JWT Token" }) }),
typeResponse('failed', ['otp.failed.generic', 'otp.failed.invalid', 'otp.failed.timeout', 'otp.failed.noSecret']),
typeResponse('success', 'otp.success', { token: Type.String({ description: 'the JWT Token' }) }),
]);
type OtpRes = Static<typeof OtpRes>;
@ -21,35 +21,40 @@ type OtpRes = Static<typeof OtpRes>;
const OTP_TOKEN_TIMEOUT_SEC = 120;
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
void _opts;
fastify.post<{ Body: OtpReq }>(
"/api/auth/otp",
{ schema: { body: OtpReq, response: { "2xx": OtpRes } } },
'/api/auth/otp',
{ schema: { body: OtpReq, response: { '2xx': OtpRes } } },
async function(req, _res) {
void _res;
try {
const { token, code } = req.body;
// lets try to decode+verify the jwt
let dJwt = this.jwt.verify<JwtType>(token);
const dJwt = this.jwt.verify<JwtType>(token);
// is the jwt a valid `otp` jwt ?
if (dJwt.kind != "otp")
if (dJwt.kind != 'otp') {
// no ? fuck off then
return makeResponse("failed", "otp.failed.invalid");
return makeResponse('failed', 'otp.failed.invalid');
}
// is it too old ?
if (dJwt.createdAt + OTP_TOKEN_TIMEOUT_SEC * 1000 < Date.now())
if (dJwt.createdAt + OTP_TOKEN_TIMEOUT_SEC * 1000 < Date.now()) {
// yes ? fuck off then, redo the password
return makeResponse("failed", "otp.failed.timeout");
return makeResponse('failed', 'otp.failed.timeout');
}
// get the Otp sercret from the db
let user = this.db.getUserFromName(dJwt.who);
if (isNullish(user?.otp))
const user = this.db.getUserFromName(dJwt.who);
if (isNullish(user?.otp)) {
// oops, either no user, or user without otpSecret
// fuck off
return makeResponse("failed", "otp.failed.noSecret");
return makeResponse('failed', 'otp.failed.noSecret');
}
// good lets now verify the token you gave us is the correct one...
let otpHandle = new Otp({ secret: user.otp });
const otpHandle = new Otp({ secret: user.otp });
let now = Date.now();
const now = Date.now();
const tokens = [
// we also get the last code, to mitiage the delay between client<->server roundtrip...
otpHandle.totp(now - 30 * 1000),
@ -58,14 +63,16 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
];
// checking if any of the array match
if (tokens.some((c) => c === code))
if (tokens.some((c) => c === code)) {
// they do !
// gg you are now logged in !
return makeResponse("success", "otp.success", { token: this.signJwt("auth", dJwt.who) });
} catch {
return makeResponse("failed", "otp.failed.generic");
return makeResponse('success', 'otp.success', { token: this.signJwt('auth', dJwt.who) });
}
}
return makeResponse("failed", "otp.failed.generic");
catch {
return makeResponse('failed', 'otp.failed.generic');
}
return makeResponse('failed', 'otp.failed.generic');
},
);
};

View file

@ -1,9 +1,9 @@
import { FastifyPluginAsync } from "fastify";
import { FastifyPluginAsync } from 'fastify';
import { Static, Type } from "@sinclair/typebox";
import { typeResponse, makeResponse, isNullish } from "@shared/utils";
import { Static, Type } from '@sinclair/typebox';
import { typeResponse, makeResponse, isNullish } from '@shared/utils';
const USERNAME_CHECK: RegExp = /^[a-zA-Z\_0-9]+$/;
const USERNAME_CHECK: RegExp = /^[a-zA-Z_0-9]+$/;
const SignInReq = Type.Object({
name: Type.String(),
@ -13,51 +13,46 @@ const SignInReq = Type.Object({
type SignInReq = Static<typeof SignInReq>;
const SignInRes = Type.Union([
typeResponse("failed", [
"signin.failed.generic",
"signin.failed.username.existing",
"signin.failed.username.toolong",
"signin.failed.username.tooshort",
"signin.failed.username.invalid",
"signin.failed.password.toolong",
"signin.failed.password.tooshort",
"signin.failed.password.invalid",
typeResponse('failed', [
'signin.failed.generic',
'signin.failed.username.existing',
'signin.failed.username.toolong',
'signin.failed.username.tooshort',
'signin.failed.username.invalid',
'signin.failed.password.toolong',
'signin.failed.password.tooshort',
'signin.failed.password.invalid',
]),
typeResponse("success", "signin.success", { token: Type.String({ description: "the JWT token" }) }),
])
typeResponse('success', 'signin.success', { token: Type.String({ description: 'the JWT token' }) }),
]);
type SignInRes = Static<typeof SignInRes>;
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
void _opts;
fastify.post<{ Body: SignInReq }>(
"/api/auth/signin",
{ schema: { body: SignInReq, response: { "200": SignInRes, "5xx": Type.Object({}) } }, },
async function(req, res) {
'/api/auth/signin',
{ schema: { body: SignInReq, response: { '200': SignInRes, '5xx': Type.Object({}) } } },
async function(req, _res) {
void _res;
const { name, password } = req.body;
if (name.length < 4)
return makeResponse("failed", "signin.failed.username.tooshort");
if (name.length > 32)
return makeResponse("failed", "signin.failed.username.toolong");
if (!USERNAME_CHECK.test(name))
return makeResponse("failed", "signin.failed.username.invalid");
if (name.length < 4) {return makeResponse('failed', 'signin.failed.username.tooshort');}
if (name.length > 32) {return makeResponse('failed', 'signin.failed.username.toolong');}
if (!USERNAME_CHECK.test(name)) {return makeResponse('failed', 'signin.failed.username.invalid');}
// username if good now :)
if (password.length < 8)
return makeResponse("failed", "signin.failed.password.tooshort");
if (password.length > 64)
return makeResponse("failed", "signin.failed.password.toolong");
if (password.length < 8) {return makeResponse('failed', 'signin.failed.password.tooshort');}
if (password.length > 64) {return makeResponse('failed', 'signin.failed.password.toolong');}
// password is good too !
if (this.db.getUserFromName(name) !== undefined)
return makeResponse("failed", "signin.failed.username.existing");
let u = await this.db.createUser(name, password);
if (isNullish(u))
return makeResponse("failed", "signin.failed.generic");
if (this.db.getUserFromName(name) !== undefined) {return makeResponse('failed', 'signin.failed.username.existing');}
const u = await this.db.createUser(name, password);
if (isNullish(u)) {return makeResponse('failed', 'signin.failed.generic');}
// every check has been passed, they are now logged in, using this token to say who they are...
let userToken = this.signJwt('auth', u.name);
return makeResponse("success", "signin.success", { token: userToken });
const userToken = this.signJwt('auth', u.name);
return makeResponse('success', 'signin.success', { token: userToken });
},
);
};

View file

@ -1,30 +1,30 @@
import { FastifyPluginAsync } from "fastify";
import { FastifyPluginAsync } from 'fastify';
import { Static, Type } from "@sinclair/typebox";
import { isNullish, makeResponse, typeResponse } from "@shared/utils"
import { Otp } from "@shared/auth";
import { Static, Type } from '@sinclair/typebox';
import { isNullish, makeResponse, typeResponse } from '@shared/utils';
import { Otp } from '@shared/auth';
export const StatusOtpRes = Type.Union([
typeResponse("success", "statusOtp.success.enabled", { url: Type.String({ description: "The otp url to feed into a 2fa app" }) }),
typeResponse("success", "statusOtp.success.disabled"),
typeResponse("failure", "statusOtp.failure.generic")
typeResponse('success', 'statusOtp.success.enabled', { url: Type.String({ description: 'The otp url to feed into a 2fa app' }) }),
typeResponse('success', 'statusOtp.success.disabled'),
typeResponse('failure', 'statusOtp.failure.generic'),
]);
export type StatusOtpRes = Static<typeof StatusOtpRes>;
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
void _opts;
fastify.get(
"/api/auth/statusOtp",
{ schema: { response: { "2xx": StatusOtpRes } }, config: { requireAuth: true } },
'/api/auth/statusOtp',
{ schema: { response: { '2xx': StatusOtpRes } }, config: { requireAuth: true } },
async function(req, _res) {
if (isNullish(req.authUser))
return makeResponse("failure", "statusOtp.failure.generic");
let otpSecret = this.db.getUserOtpSecret(req.authUser.id);
if (isNullish(otpSecret))
return makeResponse("success", "statusOtp.success.disabled");
let otp = new Otp({ secret: otpSecret })
return makeResponse("success", "statusOtp.success.enabled", { url: otp.totpURL });
void _res;
if (isNullish(req.authUser)) {return makeResponse('failure', 'statusOtp.failure.generic');}
const otpSecret = this.db.getUserOtpSecret(req.authUser.id);
if (isNullish(otpSecret)) {return makeResponse('success', 'statusOtp.success.disabled');}
const otp = new Otp({ secret: otpSecret });
return makeResponse('success', 'statusOtp.success.enabled', { url: otp.totpURL });
},
);
};

View file

@ -1,24 +1,25 @@
import { FastifyPluginAsync } from "fastify";
import { FastifyPluginAsync } from 'fastify';
import { Static, Type } from "@sinclair/typebox";
import { isNullish, makeResponse, typeResponse } from "@shared/utils"
import { Static, Type } from '@sinclair/typebox';
import { isNullish, makeResponse, typeResponse } from '@shared/utils';
export const WhoAmIRes = Type.Union([
typeResponse("success", "whoami.success", { name: Type.String() }),
typeResponse("failure", "whoami.failure.generic")
typeResponse('success', 'whoami.success', { name: Type.String() }),
typeResponse('failure', 'whoami.failure.generic'),
]);
export type WhoAmIRes = Static<typeof WhoAmIRes>;
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
void _opts;
fastify.get(
"/api/auth/whoami",
{ schema: { response: { "2xx": WhoAmIRes } }, config: { requireAuth: true } },
'/api/auth/whoami',
{ schema: { response: { '2xx': WhoAmIRes } }, config: { requireAuth: true } },
async function(req, _res) {
if (isNullish(req.authUser))
return makeResponse("failure", "whoami.failure.generic")
return makeResponse("success", "whoami.success", { name: req.authUser.name })
void _res;
if (isNullish(req.authUser)) {return makeResponse('failure', 'whoami.failure.generic');}
return makeResponse('success', 'whoami.success', { name: req.authUser.name });
},
);
};

View file

@ -1,7 +1,7 @@
// this sould only be used by the docker file !
import fastify, { FastifyInstance } from "fastify";
import app from "./app"
import fastify, { FastifyInstance } from 'fastify';
import app from './app';
const start = async () => {
const envToLogger = {
@ -16,15 +16,16 @@ const start = async () => {
},
production: true,
test: false,
}
};
const f: FastifyInstance = fastify({ logger: envToLogger.development });
try {
await f.register(app, {});
await f.listen({ port: 80, host: '0.0.0.0' })
} catch (err) {
f.log.error(err)
process.exit(1)
await f.listen({ port: 80, host: '0.0.0.0' });
}
}
start()
catch (err) {
f.log.error(err);
process.exit(1);
}
};
start();

View file

@ -1,8 +1,8 @@
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'
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();
@ -20,27 +20,32 @@ function collectDeps(...pkgJsonPaths) {
const externals = collectDeps(
'./package.json',
'../@shared/package.json'
'../@shared/package.json',
);
export default defineConfig({
root: __dirname, // service root
root: __dirname,
// service root
plugins: [tsconfigPaths(), nodeExternals()],
build: {
ssr: true,
outDir: 'dist',
emptyOutDir: true,
lib: {
entry: path.resolve(__dirname, 'src/run.ts'), // adjust main entry
formats: ['cjs'], // CommonJS for Node.js
entry: path.resolve(__dirname, 'src/run.ts'),
// adjust main entry
formats: ['cjs'],
// CommonJS for Node.js
fileName: () => 'index.js',
},
rollupOptions: {
external: externals,
},
target: 'node22', // or whatever Node version you use
target: 'node22',
// or whatever Node version you use
sourcemap: true,
minify: false, // for easier debugging
}
})
minify: false,
// for easier debugging
},
});

55
src/eslint.config.js Normal file
View file

@ -0,0 +1,55 @@
import js from '@eslint/js';
import ts from 'typescript-eslint';
export default [
js.configs.recommended,
...ts.configs.recommended,
{
languageOptions: {
ecmaVersion: 'latest',
},
rules: {
'arrow-spacing': ['warn', { before: true, after: true }],
'brace-style': ['error', 'stroustrup', { allowSingleLine: true }],
'comma-dangle': ['error', 'always-multiline'],
'comma-spacing': 'error',
'comma-style': 'error',
curly: ['error', 'multi-line', 'consistent'],
'dot-location': ['error', 'property'],
'handle-callback-err': 'off',
indent: ['error', 'tab'],
'keyword-spacing': 'error',
'max-nested-callbacks': ['error', { max: 4 }],
'max-statements-per-line': ['error', { max: 2 }],
'no-console': 'off',
'no-empty-function': 'error',
'no-floating-decimal': 'error',
'no-inline-comments': 'error',
'no-lonely-if': 'error',
'no-multi-spaces': 'error',
'no-multiple-empty-lines': ['error', { max: 2, maxEOF: 1, maxBOF: 0 }],
'no-shadow': ['error', { allow: ['err', 'resolve', 'reject'] }],
'no-trailing-spaces': ['error'],
'no-var': 'error',
'no-undef': 'off',
'object-curly-spacing': ['error', 'always'],
'prefer-const': 'error',
quotes: ['error', 'single'],
semi: ['error', 'always'],
'space-before-blocks': 'error',
'space-before-function-paren': [
'error',
{
anonymous: 'never',
named: 'never',
asyncArrow: 'always',
},
],
'space-in-parens': 'error',
'space-infix-ops': 'error',
'space-unary-ops': 'error',
'spaced-comment': 'error',
yoda: 'error',
},
},
];

View file

@ -1,13 +1,14 @@
import { FastifyPluginAsync } from 'fastify'
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 { FastifyPluginAsync } from 'fastify';
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 { authPlugin, jwtPlugin } from '@shared/auth';
// @ts-ignore: import.meta.glob is a vite thing. Typescript doesn't know this...
// @ts-expect-error: 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...
// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this...
const routes = import.meta.glob('./routes/**/*.ts', { eager: true });
@ -20,30 +21,33 @@ declare module 'fastify' {
const app: FastifyPluginAsync = async (
fastify,
opts
_opts,
): Promise<void> => {
void _opts;
// Place here your custom code!
for (const plugin of Object.values(plugins)) {
void fastify.register(plugin as any, {});
void fastify.register(plugin as FastifyPluginAsync, {});
}
for (const route of Object.values(routes)) {
void fastify.register(route as any, {});
void fastify.register(route as FastifyPluginAsync, {});
}
await fastify.register(db.useDatabase as any, {})
void fastify.register(fastifyFormBody, {})
void fastify.register(fastifyMultipart, {})
console.log(fastify.db.getUser(1));
await fastify.register(db.useDatabase as FastifyPluginAsync, {});
await fastify.register(authPlugin as FastifyPluginAsync, {});
await fastify.register(jwtPlugin as FastifyPluginAsync, {});
void fastify.register(fastifyFormBody, {});
void fastify.register(fastifyMultipart, {});
// The use of fastify-plugin is required to be able
// to export the decorators to the outer scope
void fastify.register(fp(async (fastify) => {
const image_store = process.env.USER_ICONS_STORE ?? "/tmp/icons";
fastify.decorate('image_store', image_store)
await mkdir(fastify.image_store, { recursive: true })
}))
void fastify.register(fp(async (fastify2) => {
const image_store = process.env.USER_ICONS_STORE ?? '/tmp/icons';
fastify2.decorate('image_store', image_store);
await mkdir(fastify2.image_store, { recursive: true });
}));
}
};
export default app
export { app }
export default app;
export { app };

View file

@ -1,5 +1,5 @@
import fp from 'fastify-plugin'
import sensible, { FastifySensibleOptions } from '@fastify/sensible'
import fp from 'fastify-plugin';
import sensible, { FastifySensibleOptions } from '@fastify/sensible';
/**
* This plugins adds some utilities to handle http errors
@ -7,5 +7,5 @@ import sensible, { FastifySensibleOptions } from '@fastify/sensible'
* @see https://github.com/fastify/fastify-sensible
*/
export default fp<FastifySensibleOptions>(async (fastify) => {
fastify.register(sensible)
})
fastify.register(sensible);
});

View file

@ -1,49 +1,51 @@
import { FastifyPluginAsync } from 'fastify'
import { join } from 'node:path'
import { open } from 'node:fs/promises'
import sharp from 'sharp'
import rawBody from 'raw-body'
import { isNullish } from '@shared/utils'
import { FastifyPluginAsync } from 'fastify';
import { join } from 'node:path';
import { open } from 'node:fs/promises';
import sharp from 'sharp';
import rawBody from 'raw-body';
import { isNullish } from '@shared/utils';
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
void _opts;
// 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: any) {
done()
fastify.addContentTypeParser('*', function(request, payload, done) {
done(null);
});
fastify.post('/:userid', async function(request, reply) {
let buffer = await rawBody(request.raw);
fastify.post<{ Params: { userid: string } }>('/:userid', async function(request, reply) {
const buffer = await rawBody(request.raw);
// this is how we get the `:userid` part of things
const userid: string | undefined = (request.params as any)['userid'];
const userid: string | undefined = (request.params)['userid'];
if (isNullish(userid)) {
return await reply.code(403);
}
const image_store: string = fastify.getDecorator('image_store')
const image_path = join(image_store, userid)
const image_store: string = fastify.getDecorator('image_store');
const image_path = join(image_store, userid);
try {
let img = sharp(buffer);
const 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)
});
const data = await img.png({ compressionLevel: 6 }).toBuffer();
const image_file = await open(image_path, 'w', 0o666);
await image_file.write(data);
await image_file.close()
} catch (e: any) {
await image_file.close();
}
catch (e) {
fastify.log.error(`Error: ${e}`);
reply.code(400);
return { status: "error", message: e.toString() };
return { status: 'error' };
}
})
}
});
};
export default route
export default route;

View file

@ -1,7 +1,7 @@
// this sould only be used by the docker file !
import fastify, { FastifyInstance } from "fastify";
import app from "./app"
import fastify, { FastifyInstance } from 'fastify';
import app from './app';
const start = async () => {
const envToLogger = {
@ -16,15 +16,16 @@ const start = async () => {
},
production: true,
test: false,
}
};
const f: FastifyInstance = fastify({ logger: envToLogger.development });
try {
await f.register(app, {});
await f.listen({ port: 80, host: '0.0.0.0' })
} catch (err) {
f.log.error(err)
process.exit(1)
await f.listen({ port: 80, host: '0.0.0.0' });
}
}
start()
catch (err) {
f.log.error(err);
process.exit(1);
}
};
start();

View file

@ -1,8 +1,8 @@
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'
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();
@ -20,27 +20,32 @@ function collectDeps(...pkgJsonPaths) {
const externals = collectDeps(
'./package.json',
'../@shared/package.json'
'../@shared/package.json',
);
export default defineConfig({
root: __dirname, // service root
root: __dirname,
// service root
plugins: [tsconfigPaths(), nodeExternals()],
build: {
ssr: true,
outDir: 'dist',
emptyOutDir: true,
lib: {
entry: path.resolve(__dirname, 'src/run.ts'), // adjust main entry
formats: ['cjs'], // CommonJS for Node.js
entry: path.resolve(__dirname, 'src/run.ts'),
// adjust main entry
formats: ['cjs'],
// CommonJS for Node.js
fileName: () => 'index.js',
},
rollupOptions: {
external: externals,
},
target: 'node22', // or whatever Node version you use
target: 'node22',
// or whatever Node version you use
sourcemap: false,
minify: true, // for easier debugging
}
})
minify: true,
// for easier debugging
},
});

1469
src/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,6 @@
{
"name": "workspace",
"type": "module",
"version": "0.0.0",
"private": true,
"workspaces": [
@ -7,14 +8,27 @@
"./icons",
"./auth"
],
"lint-staged": {
"*": [
"eslint --fix"
]
},
"scripts": {
"build": "npm run build --workspaces --if-present",
"fclean": "rimraf \"**/dist\"",
"clean": "rimraf \"**/node_modules\"",
"install-all": "npm install"
"install-all": "npm install",
"dev:prepare": "husky"
},
"devDependencies": {
"rimraf": "^5.0.1"
"rimraf": "^5.0.1",
"@eslint/js": "^9.36.0",
"@typescript-eslint/eslint-plugin": "^8.44.1",
"@typescript-eslint/parser": "^8.44.1",
"eslint": "^9.36.0",
"lint-staged": "^16.1.5",
"husky": "^9.1.7",
"typescript-eslint": "^8.44.1"
},
"dependencies": {
"bindings": "^1.5.0"