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:
commit
ff0e218803
31 changed files with 2081 additions and 455 deletions
40
.github/workflows/lint.yml
vendored
Normal file
40
.github/workflows/lint.yml
vendored
Normal 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
2
.husky/pre-commit
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
pnpm --prefix=./src/ lint-staged
|
||||||
19
flake.nix
19
flake.nix
|
|
@ -19,6 +19,24 @@
|
||||||
flake-utils.lib.eachDefaultSystem (
|
flake-utils.lib.eachDefaultSystem (
|
||||||
system: let
|
system: let
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
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 {
|
in {
|
||||||
devShell = pkgs.mkShellNoCC {
|
devShell = pkgs.mkShellNoCC {
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
|
|
@ -31,6 +49,7 @@
|
||||||
dbmlSQLite.packages.${system}.default
|
dbmlSQLite.packages.${system}.default
|
||||||
sqlite-interactive
|
sqlite-interactive
|
||||||
clang
|
clang
|
||||||
|
tmux-setup
|
||||||
];
|
];
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
export PODMAN_COMPOSE_WARNING_LOGS="false";
|
export PODMAN_COMPOSE_WARNING_LOGS="false";
|
||||||
|
|
|
||||||
2
src/.husky/pre-commit
Normal file
2
src/.husky/pre-commit
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
pnpm lint-staged
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
import OTP from "otp";
|
import OTP from 'otp';
|
||||||
import cookie from "@fastify/cookie";
|
import cookie from '@fastify/cookie';
|
||||||
import fastifyJwt from "@fastify/jwt";
|
import fastifyJwt from '@fastify/jwt';
|
||||||
import fp from "fastify-plugin";
|
import fp from 'fastify-plugin';
|
||||||
import { FastifyPluginAsync, preValidationAsyncHookHandler } from "fastify";
|
import { FastifyPluginAsync, preValidationAsyncHookHandler } from 'fastify';
|
||||||
import { Static, Type } from "@sinclair/typebox";
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
import { UserId } from "@shared/database/mixin/user";
|
import { UserId } from '@shared/database/mixin/user';
|
||||||
import { useDatabase } from "@shared/database";
|
import { useDatabase } from '@shared/database';
|
||||||
import { isNullish, makeResponse } from "@shared/utils";
|
import { isNullish, makeResponse } from '@shared/utils';
|
||||||
|
|
||||||
const kRouteAuthDone = Symbol("shared-route-auth-done");
|
const kRouteAuthDone = Symbol('shared-route-auth-done');
|
||||||
|
|
||||||
type AuthedUser = {
|
type AuthedUser = {
|
||||||
id: UserId;
|
id: UserId;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
declare module "fastify" {
|
declare module 'fastify' {
|
||||||
export interface FastifyInstance {
|
export interface FastifyInstance {
|
||||||
signJwt: (kind: "auth" | "otp", who: string) => string;
|
signJwt: (kind: 'auth' | 'otp', who: string) => string;
|
||||||
[s: symbol]: boolean;
|
[s: symbol]: boolean;
|
||||||
}
|
}
|
||||||
export interface FastifyRequest {
|
export interface FastifyRequest {
|
||||||
|
|
@ -26,17 +26,22 @@ declare module "fastify" {
|
||||||
export interface FastifyContextConfig {
|
export interface FastifyContextConfig {
|
||||||
requireAuth?: boolean;
|
requireAuth?: boolean;
|
||||||
}
|
}
|
||||||
|
export interface RouteOptions {
|
||||||
|
[kRouteAuthDone]: boolean;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Otp = OTP;
|
export const Otp = OTP;
|
||||||
let jwtAdded = false;
|
let jwtAdded = false;
|
||||||
export const jwtPlugin = fp<FastifyPluginAsync>(async (fastify, _opts) => {
|
export const jwtPlugin = fp<FastifyPluginAsync>(async (fastify, _opts) => {
|
||||||
|
void _opts;
|
||||||
|
|
||||||
if (jwtAdded) return;
|
if (jwtAdded) return;
|
||||||
jwtAdded = true;
|
jwtAdded = true;
|
||||||
let env = process.env.JWT_SECRET;
|
const env = process.env.JWT_SECRET;
|
||||||
if (isNullish(env)) throw "JWT_SECRET is not defined";
|
if (isNullish(env)) throw 'JWT_SECRET is not defined';
|
||||||
if (!fastify.hasDecorator("signJwt")) {
|
if (!fastify.hasDecorator('signJwt')) {
|
||||||
void fastify.decorate("signJwt", (kind, who) =>
|
void fastify.decorate('signJwt', (kind, who) =>
|
||||||
fastify.jwt.sign({ kind, who, createdAt: Date.now() }),
|
fastify.jwt.sign({ kind, who, createdAt: Date.now() }),
|
||||||
);
|
);
|
||||||
void fastify.register(fastifyJwt, {
|
void fastify.register(fastifyJwt, {
|
||||||
|
|
@ -48,16 +53,16 @@ export const jwtPlugin = fp<FastifyPluginAsync>(async (fastify, _opts) => {
|
||||||
|
|
||||||
export const JwtType = Type.Object({
|
export const JwtType = Type.Object({
|
||||||
kind: Type.Union([
|
kind: Type.Union([
|
||||||
Type.Const("otp", {
|
Type.Const('otp', {
|
||||||
description: "the token is only valid for otp call",
|
description: 'the token is only valid for otp call',
|
||||||
}),
|
}),
|
||||||
Type.Const("auth", {
|
Type.Const('auth', {
|
||||||
description: "the token is valid for authentication",
|
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({
|
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;
|
let authAdded = false;
|
||||||
export const authPlugin = fp<FastifyPluginAsync>(async (fastify, _opts) => {
|
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;
|
authAdded = true;
|
||||||
await fastify.register(useDatabase as any, {});
|
await fastify.register(useDatabase as FastifyPluginAsync, {});
|
||||||
await fastify.register(jwtPlugin as any, {});
|
await fastify.register(jwtPlugin as FastifyPluginAsync, {});
|
||||||
await fastify.register(cookie);
|
await fastify.register(cookie);
|
||||||
if (!fastify.hasRequestDecorator("authUser"))
|
if (!fastify.hasRequestDecorator('authUser')) { fastify.decorateRequest('authUser', undefined); }
|
||||||
fastify.decorateRequest("authUser", undefined);
|
fastify.addHook('onRoute', (routeOpts) => {
|
||||||
fastify.addHook("onRoute", (routeOpts) => {
|
|
||||||
if (
|
if (
|
||||||
routeOpts.config?.requireAuth &&
|
routeOpts.config?.requireAuth &&
|
||||||
!(routeOpts as any)[kRouteAuthDone]
|
!routeOpts[kRouteAuthDone]
|
||||||
) {
|
) {
|
||||||
let f: preValidationAsyncHookHandler = async function(req, res) {
|
const f: preValidationAsyncHookHandler = async function(req, res) {
|
||||||
try {
|
try {
|
||||||
if (isNullish(req.cookies.token))
|
if (isNullish(req.cookies.token)) {
|
||||||
return res
|
return res
|
||||||
.clearCookie("token")
|
.clearCookie('token')
|
||||||
.send(
|
.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
|
return res
|
||||||
.clearCookie("token")
|
.clearCookie('token')
|
||||||
.send(
|
.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
|
return res
|
||||||
.clearCookie("token")
|
.clearCookie('token')
|
||||||
.send(
|
.send(
|
||||||
JSON.stringify(makeResponse("notLoggedIn", "auth.noUser")),
|
JSON.stringify(makeResponse('notLoggedIn', 'auth.noUser')),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
req.authUser = { id: user.id, name: tok.who };
|
req.authUser = { id: user.id, name: tok.who };
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
return res
|
return res
|
||||||
.clearCookie("token")
|
.clearCookie('token')
|
||||||
.send(JSON.stringify(makeResponse("notLoggedIn", "auth.invalid")));
|
.send(JSON.stringify(makeResponse('notLoggedIn', 'auth.invalid')));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (!routeOpts.preValidation) {
|
if (!routeOpts.preValidation) {
|
||||||
routeOpts.preValidation = [f];
|
routeOpts.preValidation = [f];
|
||||||
} else if (Array.isArray(routeOpts.preValidation)) {
|
}
|
||||||
|
else if (Array.isArray(routeOpts.preValidation)) {
|
||||||
routeOpts.preValidation.push(f);
|
routeOpts.preValidation.push(f);
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
routeOpts.preValidation = [routeOpts.preValidation, f];
|
routeOpts.preValidation = [routeOpts.preValidation, f];
|
||||||
}
|
}
|
||||||
|
|
||||||
(routeOpts as any)[kRouteAuthDone] = true;
|
routeOpts[kRouteAuthDone] = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import fp from 'fastify-plugin'
|
import fp from 'fastify-plugin';
|
||||||
import { FastifyInstance, FastifyPluginAsync } from 'fastify'
|
import { FastifyInstance, FastifyPluginAsync } from 'fastify';
|
||||||
|
|
||||||
import { Database as DbImpl } from "./mixin/_base";
|
import { Database as DbImpl } from './mixin/_base';
|
||||||
import { UserImpl, IUserDb } from "./mixin/user";
|
import { UserImpl, IUserDb } from './mixin/user';
|
||||||
import { isNullish } from '@shared/utils';
|
import { isNullish } from '@shared/utils';
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -21,17 +21,15 @@ let dbAdded = false;
|
||||||
|
|
||||||
export const useDatabase = fp<FastifyPluginAsync>(async function(
|
export const useDatabase = fp<FastifyPluginAsync>(async function(
|
||||||
f: FastifyInstance,
|
f: FastifyInstance,
|
||||||
_options: {}) {
|
_options: object) {
|
||||||
if (dbAdded)
|
void _options;
|
||||||
return;
|
if (dbAdded) { return; }
|
||||||
dbAdded = true;
|
dbAdded = true;
|
||||||
let path = process.env.DATABASE_DIR;
|
const path = process.env.DATABASE_DIR;
|
||||||
if (isNullish(path))
|
if (isNullish(path)) { 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`)
|
const db: Database = new DbImpl(`${path}/database.db`) as Database;
|
||||||
let db: Database = new DbImpl(`${path}/database.db`) as Database;
|
if (!f.hasDecorator('db')) { f.decorate('db', db); }
|
||||||
if (!f.hasDecorator("db"))
|
|
||||||
f.decorate('db', db);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default useDatabase;
|
export default useDatabase;
|
||||||
|
|
|
||||||
|
|
@ -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...
|
// @ts-expect-error: this file is included using vite, typescript doesn't know how to include this...
|
||||||
import initSql from "../init.sql?raw"
|
import initSql from '../init.sql?raw';
|
||||||
|
|
||||||
export type SqliteReturn = object | undefined;
|
export type SqliteReturn = object | undefined;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Database } from "./_base";
|
import type { Database } from './_base';
|
||||||
|
|
||||||
// never use this directly
|
// never use this directly
|
||||||
|
|
||||||
|
|
@ -48,7 +48,7 @@ export async function freeFloatingExportedFunction(): Promise<boolean> {
|
||||||
|
|
||||||
// this function will never be able to be called outside of this module
|
// this function will never be able to be called outside of this module
|
||||||
async function privateFunction(): Promise<string | undefined> {
|
async function privateFunction(): Promise<string | undefined> {
|
||||||
return undefined
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// silence warnings
|
// silence warnings
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { Database, SqliteReturn } from "./_base";
|
import type { Database, SqliteReturn } from './_base';
|
||||||
import { Otp } from "@shared/auth";
|
import { Otp } from '@shared/auth';
|
||||||
import { isNullish } from "@shared/utils";
|
import { isNullish } from '@shared/utils';
|
||||||
import * as bcrypt from "bcrypt";
|
import * as bcrypt from 'bcrypt';
|
||||||
|
|
||||||
// never use this directly
|
// never use this directly
|
||||||
|
|
||||||
|
|
@ -38,8 +38,8 @@ export const UserImpl: Omit<IUserDb, keyof Database> = {
|
||||||
getUserFromName(this: IUserDb, name: string): User | undefined {
|
getUserFromName(this: IUserDb, name: string): User | undefined {
|
||||||
return userFromRow(
|
return userFromRow(
|
||||||
this.prepare(
|
this.prepare(
|
||||||
"SELECT * FROM user WHERE name = @name LIMIT 1",
|
'SELECT * FROM user WHERE name = @name LIMIT 1',
|
||||||
).get({ name }),
|
).get({ name }) as (Partial<User> | undefined),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -52,9 +52,9 @@ export const UserImpl: Omit<IUserDb, keyof Database> = {
|
||||||
*/
|
*/
|
||||||
getUserFromRawId(this: IUserDb, id: number): User | undefined {
|
getUserFromRawId(this: IUserDb, id: number): User | undefined {
|
||||||
return userFromRow(
|
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,
|
id,
|
||||||
}) as SqliteReturn,
|
}) as (Partial<User> | undefined),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -70,8 +70,8 @@ export const UserImpl: Omit<IUserDb, keyof Database> = {
|
||||||
password = await hashPassword(password);
|
password = await hashPassword(password);
|
||||||
return userFromRow(
|
return userFromRow(
|
||||||
this.prepare(
|
this.prepare(
|
||||||
"INSERT OR FAIL INTO user (name, password) VALUES (@name, @password) RETURNING *",
|
'INSERT OR FAIL INTO user (name, password) VALUES (@name, @password) RETURNING *',
|
||||||
).get({ name, password }),
|
).get({ name, password }) as (Partial<User> | undefined),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -88,30 +88,29 @@ export const UserImpl: Omit<IUserDb, keyof Database> = {
|
||||||
password = await hashPassword(password);
|
password = await hashPassword(password);
|
||||||
return userFromRow(
|
return userFromRow(
|
||||||
this.prepare(
|
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,
|
).get({ password, id }) as SqliteReturn,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
getUserOtpSecret(this: IUserDb, id: UserId): string | undefined {
|
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;
|
if (isNullish(otp?.otp)) return undefined;
|
||||||
return otp.otp;
|
return otp.otp;
|
||||||
},
|
},
|
||||||
|
|
||||||
ensureUserOtpSecret(this: IUserDb, id: UserId): string | undefined {
|
ensureUserOtpSecret(this: IUserDb, id: UserId): string | undefined {
|
||||||
let otp = this.getUserOtpSecret(id);
|
const otp = this.getUserOtpSecret(id);
|
||||||
if (!isNullish(otp))
|
if (!isNullish(otp)) { return otp; }
|
||||||
return otp;
|
const otpGen = new Otp();
|
||||||
let otpGen = new Otp();
|
const res = this.prepare('UPDATE OR IGNORE user SET otp = @otp WHERE id = @id RETURNING otp')
|
||||||
const res: any = this.prepare("UPDATE OR IGNORE user SET otp = @otp WHERE id = @id RETURNING otp")
|
.get({ id, otp: otpGen.secret }) as ({ otp: string } | null | undefined);
|
||||||
.get({ id, otp: otpGen.secret });
|
|
||||||
return res?.otp;
|
return res?.otp;
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteUserOtpSecret(this: IUserDb, id: UserId): void {
|
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 };
|
export type UserId = number & { readonly __brand: unique symbol };
|
||||||
|
|
@ -155,12 +154,14 @@ async function hashPassword(
|
||||||
*
|
*
|
||||||
* @returns The user if it exists, undefined otherwise
|
* @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)) return undefined;
|
||||||
|
if (isNullish(row.id)) return undefined;
|
||||||
|
if (isNullish(row.name)) return undefined;
|
||||||
return {
|
return {
|
||||||
id: row.id as UserId,
|
id: row.id,
|
||||||
name: row.name || undefined,
|
name: row.name,
|
||||||
password: row.password || undefined,
|
password: row.password ?? undefined,
|
||||||
otp: row.otp || undefined,
|
otp: row.otp ?? undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Type } from "@sinclair/typebox";
|
import { TObject, TProperties, Type } from '@sinclair/typebox';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Represent a message key
|
* @description Represent a message key
|
||||||
|
|
@ -12,7 +12,7 @@ import { Type } from "@sinclair/typebox";
|
||||||
* @example `pong.you.lost`
|
* @example `pong.you.lost`
|
||||||
*/
|
*/
|
||||||
export type MessageKey = string;
|
export type MessageKey = string;
|
||||||
export type ResponseBase<T = {}> = {
|
export type ResponseBase<T = object> = {
|
||||||
kind: string,
|
kind: string,
|
||||||
msg: MessageKey,
|
msg: MessageKey,
|
||||||
payload?: T,
|
payload?: T,
|
||||||
|
|
@ -26,9 +26,9 @@ export type ResponseBase<T = {}> = {
|
||||||
* @example makeResponse("failure", "login.failure.invalid")
|
* @example makeResponse("failure", "login.failure.invalid")
|
||||||
* @example makeResponse("success", "login.success", { token: "supersecrettoken" })
|
* @example makeResponse("success", "login.success", { token: "supersecrettoken" })
|
||||||
*/
|
*/
|
||||||
export function makeResponse<T = {}>(kind: string, key: MessageKey, payload?: T): ResponseBase<T> {
|
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)}}`)
|
console.log(`making response {kind: ${JSON.stringify(kind)}; key: ${JSON.stringify(key)}}`);
|
||||||
return { kind, msg: key, payload }
|
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("otpRequired", "login.otpRequired", { token: Type.String() })
|
||||||
* @example typeResponse("success", "login.success", { 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;
|
let tKey;
|
||||||
if (key instanceof Array) {
|
if (key instanceof Array) {
|
||||||
tKey = Type.Union(key.map(l => Type.Const(l)));
|
tKey = Type.Union(key.map(l => Type.Const(l)));
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
tKey = Type.Const(key);
|
tKey = Type.Const(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,10 +52,9 @@ export function typeResponse(kind: string, key: MessageKey | MessageKey[], paylo
|
||||||
kind: Type.Const(kind),
|
kind: Type.Const(kind),
|
||||||
msg: tKey,
|
msg: tKey,
|
||||||
};
|
};
|
||||||
if (payload !== undefined)
|
if (payload !== undefined) {Object.assign(Ty, { payload: Type.Object(payload) });}
|
||||||
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);
|
* @example assert_equal(isNullish(false), false);
|
||||||
*/
|
*/
|
||||||
export function isNullish<T>(v: T | undefined | null): v is (null | undefined) {
|
export function isNullish<T>(v: T | undefined | null): v is (null | undefined) {
|
||||||
return v === null || v === undefined
|
return v === null || v === undefined;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,107 +1,104 @@
|
||||||
const headers = {
|
const headers = {
|
||||||
'Accept': 'application/json',
|
'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 iUsername = document.querySelector('#i-username');
|
||||||
const iPassword = document.querySelector("#i-password");
|
const iPassword = document.querySelector('#i-password');
|
||||||
const iOtp = document.querySelector("#i-otp");
|
const iOtp = document.querySelector('#i-otp');
|
||||||
|
|
||||||
const bOtpSend = document.querySelector("#b-otpSend");
|
const bOtpSend = document.querySelector('#b-otpSend');
|
||||||
const bLogin = document.querySelector("#b-login");
|
const bLogin = document.querySelector('#b-login');
|
||||||
const bLogout = document.querySelector("#b-logout");
|
const bLogout = document.querySelector('#b-logout');
|
||||||
const bSignin = document.querySelector("#b-signin");
|
const bSignin = document.querySelector('#b-signin');
|
||||||
const bWhoami = document.querySelector("#b-whoami");
|
const bWhoami = document.querySelector('#b-whoami');
|
||||||
|
|
||||||
const bOtpStatus = document.querySelector("#b-otpStatus");
|
const bOtpStatus = document.querySelector('#b-otpStatus');
|
||||||
const bOtpEnable = document.querySelector("#b-otpEnable");
|
const bOtpEnable = document.querySelector('#b-otpEnable');
|
||||||
const bOtpDisable = document.querySelector("#b-otpDisable");
|
const bOtpDisable = document.querySelector('#b-otpDisable');
|
||||||
|
|
||||||
const dResponse = document.querySelector("#d-response");
|
const dResponse = document.querySelector('#d-response');
|
||||||
|
|
||||||
function setResponse(obj) {
|
function setResponse(obj) {
|
||||||
let obj_str = JSON.stringify(obj, null, 4);
|
const obj_str = JSON.stringify(obj, null, 4);
|
||||||
dResponse.innerText = obj_str;
|
dResponse.innerText = obj_str;
|
||||||
}
|
}
|
||||||
let otpToken = null;
|
let otpToken = null;
|
||||||
|
|
||||||
bOtpSend.addEventListener("click", async () => {
|
bOtpSend.addEventListener('click', async () => {
|
||||||
let res = await fetch("/api/auth/otp", { method: "POST", body: JSON.stringify({ code: iOtp.value, token: otpToken }), headers });
|
const res = await fetch('/api/auth/otp', { method: 'POST', body: JSON.stringify({ code: iOtp.value, token: otpToken }), headers });
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
|
||||||
setResponse(json);
|
setResponse(json);
|
||||||
if (json.kind === "success") {
|
if (json.kind === 'success') {
|
||||||
if (json?.payload?.token)
|
if (json?.payload?.token) {document.cookie = `token=${json?.payload?.token}`;}
|
||||||
document.cookie = `token=${json?.payload?.token}`;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
bOtpStatus.addEventListener("click", async () => {
|
bOtpStatus.addEventListener('click', async () => {
|
||||||
let res = await fetch("/api/auth/statusOtp");
|
const res = await fetch('/api/auth/statusOtp');
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
|
||||||
setResponse(json);
|
setResponse(json);
|
||||||
});
|
});
|
||||||
|
|
||||||
bOtpEnable.addEventListener("click", async () => {
|
bOtpEnable.addEventListener('click', async () => {
|
||||||
let res = await fetch("/api/auth/enableOtp", { method: "PUT" });
|
const res = await fetch('/api/auth/enableOtp', { method: 'PUT' });
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
|
||||||
setResponse(json);
|
setResponse(json);
|
||||||
});
|
});
|
||||||
|
|
||||||
bOtpDisable.addEventListener("click", async () => {
|
bOtpDisable.addEventListener('click', async () => {
|
||||||
let res = await fetch("/api/auth/disableOtp", { method: "PUT" });
|
const res = await fetch('/api/auth/disableOtp', { method: 'PUT' });
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
|
||||||
setResponse(json);
|
setResponse(json);
|
||||||
});
|
});
|
||||||
|
|
||||||
bWhoami.addEventListener("click", async () => {
|
bWhoami.addEventListener('click', async () => {
|
||||||
let username = "";
|
let username = '';
|
||||||
try {
|
try {
|
||||||
let res = await fetch("/api/auth/whoami");
|
const res = await fetch('/api/auth/whoami');
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
setResponse(json);
|
setResponse(json);
|
||||||
if (json?.kind === "success")
|
if (json?.kind === 'success') {username = json?.payload?.name;}
|
||||||
username = json?.payload?.name;
|
else {username = `<not logged in:${json.msg}>`;}
|
||||||
else
|
}
|
||||||
username = `<not logged in:${json.msg}>`
|
catch {
|
||||||
} catch {
|
username = '<not logged in: threw>';
|
||||||
username = `<not logged in: threw>`
|
|
||||||
}
|
}
|
||||||
tUsername.innerText = username;
|
tUsername.innerText = username;
|
||||||
});
|
});
|
||||||
|
|
||||||
bLogin.addEventListener("click", async () => {
|
bLogin.addEventListener('click', async () => {
|
||||||
const name = iUsername.value;
|
const name = iUsername.value;
|
||||||
const password = iPassword.value;
|
const password = iPassword.value;
|
||||||
|
|
||||||
let res = await fetch("/api/auth/login", { method: "POST", body: JSON.stringify({ name, password }), headers });
|
const res = await fetch('/api/auth/login', { method: 'POST', body: JSON.stringify({ name, password }), headers });
|
||||||
let json = await res.json();
|
const json = await res.json();
|
||||||
if (json?.kind === "otpRequired") {
|
if (json?.kind === 'otpRequired') {
|
||||||
otpToken = json?.payload?.token;
|
otpToken = json?.payload?.token;
|
||||||
} else if (json?.kind === "success") {
|
}
|
||||||
if (json?.payload?.token)
|
else if (json?.kind === 'success') {
|
||||||
document.cookie = `token=${json?.payload?.token}`;
|
if (json?.payload?.token) {document.cookie = `token=${json?.payload?.token}`;}
|
||||||
}
|
}
|
||||||
setResponse(json);
|
setResponse(json);
|
||||||
})
|
});
|
||||||
|
|
||||||
bLogout.addEventListener("click", async () => {
|
bLogout.addEventListener('click', async () => {
|
||||||
let res = await fetch("/api/auth/logout", { method: "POST" });
|
const res = await fetch('/api/auth/logout', { method: 'POST' });
|
||||||
setResponse(await res.json());
|
setResponse(await res.json());
|
||||||
})
|
});
|
||||||
|
|
||||||
bSignin.addEventListener("click", async () => {
|
bSignin.addEventListener('click', async () => {
|
||||||
const name = iUsername.value;
|
const name = iUsername.value;
|
||||||
const password = iPassword.value;
|
const password = iPassword.value;
|
||||||
|
|
||||||
let res = await fetch("/api/auth/signin", { method: "POST", body: JSON.stringify({ name, password }), headers });
|
const res = await fetch('/api/auth/signin', { method: 'POST', body: JSON.stringify({ name, password }), headers });
|
||||||
let json = await res.json();
|
const json = await res.json();
|
||||||
if (json?.payload?.token)
|
if (json?.payload?.token) {document.cookie = `token=${json?.payload?.token};`;}
|
||||||
document.cookie = `token=${json?.payload?.token};`;
|
|
||||||
setResponse(json);
|
setResponse(json);
|
||||||
})
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,14 @@
|
||||||
import { FastifyPluginAsync } from 'fastify'
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
import fastifyFormBody from '@fastify/formbody'
|
import fastifyFormBody from '@fastify/formbody';
|
||||||
import fastifyMultipart from '@fastify/multipart'
|
import fastifyMultipart from '@fastify/multipart';
|
||||||
import { mkdir } from 'node:fs/promises'
|
import * as db from '@shared/database';
|
||||||
import fp from 'fastify-plugin'
|
import * as auth from '@shared/auth';
|
||||||
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 });
|
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 });
|
const routes = import.meta.glob('./routes/**/*.ts', { eager: true });
|
||||||
|
|
||||||
|
|
||||||
// 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' {
|
||||||
export interface FastifyInstance {
|
export interface FastifyInstance {
|
||||||
|
|
@ -19,25 +16,23 @@ declare module 'fastify' {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const app: FastifyPluginAsync = async (
|
const app: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||||
fastify,
|
void opts;
|
||||||
opts
|
await fastify.register(db.useDatabase as FastifyPluginAsync, {});
|
||||||
): Promise<void> => {
|
await fastify.register(auth.jwtPlugin as FastifyPluginAsync, {});
|
||||||
await fastify.register(db.useDatabase as any, {})
|
await fastify.register(auth.authPlugin as FastifyPluginAsync, {});
|
||||||
await fastify.register(auth.jwtPlugin as any, {})
|
|
||||||
await fastify.register(auth.authPlugin as any, {})
|
|
||||||
|
|
||||||
// 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 as any, {});
|
void fastify.register(plugin as FastifyPluginAsync, {});
|
||||||
}
|
}
|
||||||
for (const route of Object.values(routes)) {
|
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(fastifyFormBody, {});
|
||||||
void fastify.register(fastifyMultipart, {})
|
void fastify.register(fastifyMultipart, {});
|
||||||
}
|
};
|
||||||
|
|
||||||
export default app
|
export default app;
|
||||||
export { app }
|
export { app };
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import fp from 'fastify-plugin'
|
import fp from 'fastify-plugin';
|
||||||
import sensible, { FastifySensibleOptions } from '@fastify/sensible'
|
import sensible, { FastifySensibleOptions } from '@fastify/sensible';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This plugins adds some utilities to handle http errors
|
* 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
|
* @see https://github.com/fastify/fastify-sensible
|
||||||
*/
|
*/
|
||||||
export default fp<FastifySensibleOptions>(async (fastify) => {
|
export default fp<FastifySensibleOptions>(async (fastify) => {
|
||||||
fastify.register(sensible)
|
fastify.register(sensible);
|
||||||
})
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,26 @@
|
||||||
import { FastifyPluginAsync } from "fastify";
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
|
|
||||||
import { Static, Type } from "@sinclair/typebox";
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
import { makeResponse, typeResponse, isNullish } from "@shared/utils"
|
import { makeResponse, typeResponse, isNullish } from '@shared/utils';
|
||||||
|
|
||||||
|
|
||||||
export const WhoAmIRes = Type.Union([
|
export const WhoAmIRes = Type.Union([
|
||||||
typeResponse("success", "disableOtp.success"),
|
typeResponse('success', 'disableOtp.success'),
|
||||||
typeResponse("failure", "disableOtp.failure.generic")
|
typeResponse('failure', 'disableOtp.failure.generic'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type WhoAmIRes = Static<typeof WhoAmIRes>;
|
export type WhoAmIRes = Static<typeof WhoAmIRes>;
|
||||||
|
|
||||||
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
||||||
|
void _opts;
|
||||||
fastify.put(
|
fastify.put(
|
||||||
"/api/auth/disableOtp",
|
'/api/auth/disableOtp',
|
||||||
{ schema: { response: { "2xx": WhoAmIRes } }, config: { requireAuth: true } },
|
{ schema: { response: { '2xx': WhoAmIRes } }, config: { requireAuth: true } },
|
||||||
async function(req, _res) {
|
async function(req, _res) {
|
||||||
if (isNullish(req.authUser))
|
void _res;
|
||||||
return makeResponse("failure", "disableOtp.failure.generic");
|
if (isNullish(req.authUser)) {return makeResponse('failure', 'disableOtp.failure.generic');}
|
||||||
this.db.deleteUserOtpSecret(req.authUser.id);
|
this.db.deleteUserOtpSecret(req.authUser.id);
|
||||||
return makeResponse("success", "disableOtp.success");
|
return makeResponse('success', 'disableOtp.success');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,29 @@
|
||||||
import { FastifyPluginAsync } from "fastify";
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
|
|
||||||
import { Static, Type } from "@sinclair/typebox";
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
import { isNullish, makeResponse, typeResponse } from "@shared/utils"
|
import { isNullish, makeResponse, typeResponse } from '@shared/utils';
|
||||||
import { Otp } from "@shared/auth";
|
import { Otp } from '@shared/auth';
|
||||||
|
|
||||||
|
|
||||||
export const WhoAmIRes = Type.Union([
|
export const WhoAmIRes = Type.Union([
|
||||||
typeResponse("success", "enableOtp.success", { url: Type.String({ description: "The otp url to feed into a 2fa app" }) }),
|
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('failure', ['enableOtp.failure.noUser', 'enableOtp.failure.noSecret']),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type WhoAmIRes = Static<typeof WhoAmIRes>;
|
export type WhoAmIRes = Static<typeof WhoAmIRes>;
|
||||||
|
|
||||||
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
||||||
|
void _opts;
|
||||||
fastify.put(
|
fastify.put(
|
||||||
"/api/auth/enableOtp",
|
'/api/auth/enableOtp',
|
||||||
{ schema: { response: { "2xx": WhoAmIRes } }, config: { requireAuth: true } },
|
{ schema: { response: { '2xx': WhoAmIRes } }, config: { requireAuth: true } },
|
||||||
async function(req, _res) {
|
async function(req, _res) {
|
||||||
if (isNullish(req.authUser))
|
void _res;
|
||||||
return makeResponse("failure", "enableOtp.failure.noUser");
|
if (isNullish(req.authUser)) {return makeResponse('failure', 'enableOtp.failure.noUser');}
|
||||||
let otpSecret = this.db.ensureUserOtpSecret(req.authUser!.id);
|
const otpSecret = this.db.ensureUserOtpSecret(req.authUser!.id);
|
||||||
if (isNullish(otpSecret))
|
if (isNullish(otpSecret)) {return makeResponse('failure', 'enableOtp.failure.noSecret');}
|
||||||
return makeResponse("failure", "enableOtp.failure.noSecret");
|
const otp = new Otp({ secret: otpSecret });
|
||||||
let otp = new Otp({ secret: otpSecret });
|
return makeResponse('success', 'enableOtp.success', { url: otp.totpURL });
|
||||||
return makeResponse("success", "enableOtp.success", { url: otp.totpURL });
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { FastifyPluginAsync } from "fastify";
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
|
|
||||||
import { Static, Type } from "@sinclair/typebox";
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
import { typeResponse, makeResponse, isNullish } from "@shared/utils"
|
import { typeResponse, makeResponse, isNullish } from '@shared/utils';
|
||||||
import { verifyUserPassword } from "@shared/database/mixin/user";
|
import { verifyUserPassword } from '@shared/database/mixin/user';
|
||||||
|
|
||||||
export const LoginReq = Type.Object({
|
export const LoginReq = Type.Object({
|
||||||
name: Type.String(),
|
name: Type.String(),
|
||||||
|
|
@ -12,44 +12,44 @@ export const LoginReq = Type.Object({
|
||||||
export type LoginReq = Static<typeof LoginReq>;
|
export type LoginReq = Static<typeof LoginReq>;
|
||||||
|
|
||||||
export const LoginRes = Type.Union([
|
export const LoginRes = Type.Union([
|
||||||
typeResponse("failed", ["login.failed.generic", "login.failed.invalid"]),
|
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('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('success', 'login.success', { token: Type.String({ description: 'JWT that represent a logged in user' }) }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
export type LoginRes = Static<typeof LoginRes>;
|
export type LoginRes = Static<typeof LoginRes>;
|
||||||
|
|
||||||
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
||||||
|
void _opts;
|
||||||
fastify.post<{ Body: LoginReq; Response: LoginRes }>(
|
fastify.post<{ Body: LoginReq; Response: LoginRes }>(
|
||||||
"/api/auth/login",
|
'/api/auth/login',
|
||||||
{ schema: { body: LoginReq, response: { "2xx": LoginRes } }, },
|
{ schema: { body: LoginReq, response: { '2xx': LoginRes } } },
|
||||||
async function(req, _res) {
|
async function(req, _res) {
|
||||||
|
void _res;
|
||||||
try {
|
try {
|
||||||
let { name, password } = req.body;
|
const { name, password } = req.body;
|
||||||
let user = this.db.getUserFromName(name);
|
const user = this.db.getUserFromName(name);
|
||||||
|
|
||||||
// does the user exist
|
// does the user exist
|
||||||
// does it have a password setup ?
|
// does it have a password setup ?
|
||||||
if (isNullish(user?.password))
|
if (isNullish(user?.password)) {return makeResponse('failed', 'login.failed.invalid');}
|
||||||
return makeResponse("failed", "login.failed.invalid");
|
|
||||||
|
|
||||||
// does the password he provided match the one we have
|
// does the password he provided match the one we have
|
||||||
if (!(await verifyUserPassword(user, password)))
|
if (!(await verifyUserPassword(user, password))) {return makeResponse('failed', 'login.failed.invalid');}
|
||||||
return makeResponse("failed", "login.failed.invalid");
|
|
||||||
|
|
||||||
// does the user has 2FA up ?
|
// does the user has 2FA up ?
|
||||||
if (!isNullish(user.otp)) {
|
if (!isNullish(user.otp)) {
|
||||||
// yes -> we ask them to fill it,
|
// yes -> we ask them to fill it,
|
||||||
// send them somehting to verify that they indeed passed throught the user+password phase
|
// 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...
|
// 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 {
|
catch {
|
||||||
return makeResponse("failed", "login.failed.generic");
|
return makeResponse('failed', 'login.failed.generic');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import { FastifyPluginAsync } from "fastify";
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
|
|
||||||
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
||||||
|
void _opts;
|
||||||
fastify.post(
|
fastify.post(
|
||||||
"/api/auth/logout",
|
'/api/auth/logout',
|
||||||
async function(_req, res) {
|
async function(_req, res) {
|
||||||
return res.clearCookie("token").send("{}")
|
void _req;
|
||||||
|
return res.clearCookie('token').send('{}');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
import { FastifyPluginAsync } from "fastify";
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
|
|
||||||
import { Static, Type } from "@sinclair/typebox";
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
import { JwtType, Otp } from "@shared/auth";
|
import { JwtType, Otp } from '@shared/auth';
|
||||||
import { typeResponse, makeResponse, isNullish } from "@shared/utils";
|
import { typeResponse, makeResponse, isNullish } from '@shared/utils';
|
||||||
|
|
||||||
const OtpReq = Type.Object({
|
const OtpReq = Type.Object({
|
||||||
token: Type.String({ description: "The token given at the login phase" }),
|
token: Type.String({ description: 'The token given at the login phase' }),
|
||||||
code: Type.String({ description: "The OTP given by the user" }),
|
code: Type.String({ description: 'The OTP given by the user' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type OtpReq = Static<typeof OtpReq>;
|
type OtpReq = Static<typeof OtpReq>;
|
||||||
|
|
||||||
const OtpRes = Type.Union([
|
const OtpRes = Type.Union([
|
||||||
typeResponse("failed", ["otp.failed.generic", "otp.failed.invalid", "otp.failed.timeout", "otp.failed.noSecret"]),
|
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('success', 'otp.success', { token: Type.String({ description: 'the JWT Token' }) }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
type OtpRes = Static<typeof OtpRes>;
|
type OtpRes = Static<typeof OtpRes>;
|
||||||
|
|
@ -21,35 +21,40 @@ type OtpRes = Static<typeof OtpRes>;
|
||||||
const OTP_TOKEN_TIMEOUT_SEC = 120;
|
const OTP_TOKEN_TIMEOUT_SEC = 120;
|
||||||
|
|
||||||
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
||||||
|
void _opts;
|
||||||
fastify.post<{ Body: OtpReq }>(
|
fastify.post<{ Body: OtpReq }>(
|
||||||
"/api/auth/otp",
|
'/api/auth/otp',
|
||||||
{ schema: { body: OtpReq, response: { "2xx": OtpRes } } },
|
{ schema: { body: OtpReq, response: { '2xx': OtpRes } } },
|
||||||
async function(req, _res) {
|
async function(req, _res) {
|
||||||
|
void _res;
|
||||||
try {
|
try {
|
||||||
const { token, code } = req.body;
|
const { token, code } = req.body;
|
||||||
// lets try to decode+verify the jwt
|
// 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 ?
|
// is the jwt a valid `otp` jwt ?
|
||||||
if (dJwt.kind != "otp")
|
if (dJwt.kind != 'otp') {
|
||||||
// no ? fuck off then
|
// no ? fuck off then
|
||||||
return makeResponse("failed", "otp.failed.invalid");
|
return makeResponse('failed', 'otp.failed.invalid');
|
||||||
|
}
|
||||||
// is it too old ?
|
// 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
|
// 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
|
// get the Otp sercret from the db
|
||||||
let user = this.db.getUserFromName(dJwt.who);
|
const user = this.db.getUserFromName(dJwt.who);
|
||||||
if (isNullish(user?.otp))
|
if (isNullish(user?.otp)) {
|
||||||
// oops, either no user, or user without otpSecret
|
// oops, either no user, or user without otpSecret
|
||||||
// fuck off
|
// 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...
|
// 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 = [
|
const tokens = [
|
||||||
// we also get the last code, to mitiage the delay between client<->server roundtrip...
|
// we also get the last code, to mitiage the delay between client<->server roundtrip...
|
||||||
otpHandle.totp(now - 30 * 1000),
|
otpHandle.totp(now - 30 * 1000),
|
||||||
|
|
@ -58,14 +63,16 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
||||||
];
|
];
|
||||||
|
|
||||||
// checking if any of the array match
|
// checking if any of the array match
|
||||||
if (tokens.some((c) => c === code))
|
if (tokens.some((c) => c === code)) {
|
||||||
// they do !
|
// they do !
|
||||||
// gg you are now logged in !
|
// gg you are now logged in !
|
||||||
return makeResponse("success", "otp.success", { token: this.signJwt("auth", dJwt.who) });
|
return makeResponse('success', 'otp.success', { token: this.signJwt('auth', dJwt.who) });
|
||||||
} catch {
|
|
||||||
return makeResponse("failed", "otp.failed.generic");
|
|
||||||
}
|
}
|
||||||
return makeResponse("failed", "otp.failed.generic");
|
}
|
||||||
|
catch {
|
||||||
|
return makeResponse('failed', 'otp.failed.generic');
|
||||||
|
}
|
||||||
|
return makeResponse('failed', 'otp.failed.generic');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { FastifyPluginAsync } from "fastify";
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
|
|
||||||
import { Static, Type } from "@sinclair/typebox";
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
import { typeResponse, makeResponse, isNullish } from "@shared/utils";
|
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({
|
const SignInReq = Type.Object({
|
||||||
name: Type.String(),
|
name: Type.String(),
|
||||||
|
|
@ -13,51 +13,46 @@ const SignInReq = Type.Object({
|
||||||
type SignInReq = Static<typeof SignInReq>;
|
type SignInReq = Static<typeof SignInReq>;
|
||||||
|
|
||||||
const SignInRes = Type.Union([
|
const SignInRes = Type.Union([
|
||||||
typeResponse("failed", [
|
typeResponse('failed', [
|
||||||
"signin.failed.generic",
|
'signin.failed.generic',
|
||||||
"signin.failed.username.existing",
|
'signin.failed.username.existing',
|
||||||
"signin.failed.username.toolong",
|
'signin.failed.username.toolong',
|
||||||
"signin.failed.username.tooshort",
|
'signin.failed.username.tooshort',
|
||||||
"signin.failed.username.invalid",
|
'signin.failed.username.invalid',
|
||||||
"signin.failed.password.toolong",
|
'signin.failed.password.toolong',
|
||||||
"signin.failed.password.tooshort",
|
'signin.failed.password.tooshort',
|
||||||
"signin.failed.password.invalid",
|
'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>;
|
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 }>(
|
fastify.post<{ Body: SignInReq }>(
|
||||||
"/api/auth/signin",
|
'/api/auth/signin',
|
||||||
{ schema: { body: SignInReq, response: { "200": SignInRes, "5xx": Type.Object({}) } }, },
|
{ schema: { body: SignInReq, response: { '200': SignInRes, '5xx': Type.Object({}) } } },
|
||||||
async function(req, res) {
|
async function(req, _res) {
|
||||||
|
void _res;
|
||||||
const { name, password } = req.body;
|
const { name, password } = req.body;
|
||||||
|
|
||||||
if (name.length < 4)
|
if (name.length < 4) {return makeResponse('failed', 'signin.failed.username.tooshort');}
|
||||||
return makeResponse("failed", "signin.failed.username.tooshort");
|
if (name.length > 32) {return makeResponse('failed', 'signin.failed.username.toolong');}
|
||||||
if (name.length > 32)
|
if (!USERNAME_CHECK.test(name)) {return makeResponse('failed', 'signin.failed.username.invalid');}
|
||||||
return makeResponse("failed", "signin.failed.username.toolong");
|
|
||||||
if (!USERNAME_CHECK.test(name))
|
|
||||||
return makeResponse("failed", "signin.failed.username.invalid");
|
|
||||||
// username if good now :)
|
// username if good now :)
|
||||||
|
|
||||||
if (password.length < 8)
|
if (password.length < 8) {return makeResponse('failed', 'signin.failed.password.tooshort');}
|
||||||
return makeResponse("failed", "signin.failed.password.tooshort");
|
if (password.length > 64) {return makeResponse('failed', 'signin.failed.password.toolong');}
|
||||||
if (password.length > 64)
|
|
||||||
return makeResponse("failed", "signin.failed.password.toolong");
|
|
||||||
// password is good too !
|
// password is good too !
|
||||||
|
|
||||||
if (this.db.getUserFromName(name) !== undefined)
|
if (this.db.getUserFromName(name) !== undefined) {return makeResponse('failed', 'signin.failed.username.existing');}
|
||||||
return makeResponse("failed", "signin.failed.username.existing");
|
const u = await this.db.createUser(name, password);
|
||||||
let u = await this.db.createUser(name, password);
|
if (isNullish(u)) {return makeResponse('failed', 'signin.failed.generic');}
|
||||||
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...
|
// 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);
|
const userToken = this.signJwt('auth', u.name);
|
||||||
return makeResponse("success", "signin.success", { token: userToken });
|
return makeResponse('success', 'signin.success', { token: userToken });
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,30 @@
|
||||||
import { FastifyPluginAsync } from "fastify";
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
|
|
||||||
import { Static, Type } from "@sinclair/typebox";
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
import { isNullish, makeResponse, typeResponse } from "@shared/utils"
|
import { isNullish, makeResponse, typeResponse } from '@shared/utils';
|
||||||
import { Otp } from "@shared/auth";
|
import { Otp } from '@shared/auth';
|
||||||
|
|
||||||
|
|
||||||
export const StatusOtpRes = Type.Union([
|
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.enabled', { url: Type.String({ description: 'The otp url to feed into a 2fa app' }) }),
|
||||||
typeResponse("success", "statusOtp.success.disabled"),
|
typeResponse('success', 'statusOtp.success.disabled'),
|
||||||
typeResponse("failure", "statusOtp.failure.generic")
|
typeResponse('failure', 'statusOtp.failure.generic'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type StatusOtpRes = Static<typeof StatusOtpRes>;
|
export type StatusOtpRes = Static<typeof StatusOtpRes>;
|
||||||
|
|
||||||
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
||||||
|
void _opts;
|
||||||
fastify.get(
|
fastify.get(
|
||||||
"/api/auth/statusOtp",
|
'/api/auth/statusOtp',
|
||||||
{ schema: { response: { "2xx": StatusOtpRes } }, config: { requireAuth: true } },
|
{ schema: { response: { '2xx': StatusOtpRes } }, config: { requireAuth: true } },
|
||||||
async function(req, _res) {
|
async function(req, _res) {
|
||||||
if (isNullish(req.authUser))
|
void _res;
|
||||||
return makeResponse("failure", "statusOtp.failure.generic");
|
if (isNullish(req.authUser)) {return makeResponse('failure', 'statusOtp.failure.generic');}
|
||||||
let otpSecret = this.db.getUserOtpSecret(req.authUser.id);
|
const otpSecret = this.db.getUserOtpSecret(req.authUser.id);
|
||||||
if (isNullish(otpSecret))
|
if (isNullish(otpSecret)) {return makeResponse('success', 'statusOtp.success.disabled');}
|
||||||
return makeResponse("success", "statusOtp.success.disabled");
|
const otp = new Otp({ secret: otpSecret });
|
||||||
let otp = new Otp({ secret: otpSecret })
|
return makeResponse('success', 'statusOtp.success.enabled', { url: otp.totpURL });
|
||||||
return makeResponse("success", "statusOtp.success.enabled", { url: otp.totpURL });
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,25 @@
|
||||||
import { FastifyPluginAsync } from "fastify";
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
|
|
||||||
import { Static, Type } from "@sinclair/typebox";
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
import { isNullish, makeResponse, typeResponse } from "@shared/utils"
|
import { isNullish, makeResponse, typeResponse } from '@shared/utils';
|
||||||
|
|
||||||
|
|
||||||
export const WhoAmIRes = Type.Union([
|
export const WhoAmIRes = Type.Union([
|
||||||
typeResponse("success", "whoami.success", { name: Type.String() }),
|
typeResponse('success', 'whoami.success', { name: Type.String() }),
|
||||||
typeResponse("failure", "whoami.failure.generic")
|
typeResponse('failure', 'whoami.failure.generic'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type WhoAmIRes = Static<typeof WhoAmIRes>;
|
export type WhoAmIRes = Static<typeof WhoAmIRes>;
|
||||||
|
|
||||||
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
||||||
|
void _opts;
|
||||||
fastify.get(
|
fastify.get(
|
||||||
"/api/auth/whoami",
|
'/api/auth/whoami',
|
||||||
{ schema: { response: { "2xx": WhoAmIRes } }, config: { requireAuth: true } },
|
{ schema: { response: { '2xx': WhoAmIRes } }, config: { requireAuth: true } },
|
||||||
async function(req, _res) {
|
async function(req, _res) {
|
||||||
if (isNullish(req.authUser))
|
void _res;
|
||||||
return makeResponse("failure", "whoami.failure.generic")
|
if (isNullish(req.authUser)) {return makeResponse('failure', 'whoami.failure.generic');}
|
||||||
return makeResponse("success", "whoami.success", { name: req.authUser.name })
|
return makeResponse('success', 'whoami.success', { name: req.authUser.name });
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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"
|
import app from './app';
|
||||||
|
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
const envToLogger = {
|
const envToLogger = {
|
||||||
|
|
@ -16,15 +16,16 @@ const start = async () => {
|
||||||
},
|
},
|
||||||
production: true,
|
production: true,
|
||||||
test: false,
|
test: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
const f: FastifyInstance = fastify({ logger: envToLogger.development });
|
const f: FastifyInstance = fastify({ logger: envToLogger.development });
|
||||||
try {
|
try {
|
||||||
await f.register(app, {});
|
await f.register(app, {});
|
||||||
await f.listen({ port: 80, host: '0.0.0.0' })
|
await f.listen({ port: 80, host: '0.0.0.0' });
|
||||||
} catch (err) {
|
|
||||||
f.log.error(err)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
}
|
||||||
|
catch (err) {
|
||||||
|
f.log.error(err);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
start()
|
};
|
||||||
|
start();
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { defineConfig } from 'vite'
|
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'
|
import fs from 'node:fs';
|
||||||
|
|
||||||
function collectDeps(...pkgJsonPaths) {
|
function collectDeps(...pkgJsonPaths) {
|
||||||
const allDeps = new Set();
|
const allDeps = new Set();
|
||||||
|
|
@ -20,27 +20,32 @@ function collectDeps(...pkgJsonPaths) {
|
||||||
|
|
||||||
const externals = collectDeps(
|
const externals = collectDeps(
|
||||||
'./package.json',
|
'./package.json',
|
||||||
'../@shared/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,
|
||||||
lib: {
|
lib: {
|
||||||
entry: path.resolve(__dirname, 'src/run.ts'), // adjust main entry
|
entry: path.resolve(__dirname, 'src/run.ts'),
|
||||||
formats: ['cjs'], // CommonJS for Node.js
|
// adjust main entry
|
||||||
|
formats: ['cjs'],
|
||||||
|
// CommonJS for Node.js
|
||||||
fileName: () => 'index.js',
|
fileName: () => 'index.js',
|
||||||
},
|
},
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: externals,
|
external: externals,
|
||||||
},
|
},
|
||||||
target: 'node22', // 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
|
||||||
})
|
},
|
||||||
|
});
|
||||||
|
|
|
||||||
55
src/eslint.config.js
Normal file
55
src/eslint.config.js
Normal 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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import { FastifyPluginAsync } from 'fastify'
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
import fastifyFormBody from '@fastify/formbody'
|
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 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 });
|
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 });
|
const routes = import.meta.glob('./routes/**/*.ts', { eager: true });
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -20,30 +21,33 @@ declare module 'fastify' {
|
||||||
|
|
||||||
const app: FastifyPluginAsync = async (
|
const app: FastifyPluginAsync = async (
|
||||||
fastify,
|
fastify,
|
||||||
opts
|
_opts,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
|
void _opts;
|
||||||
// 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 as any, {});
|
void fastify.register(plugin as FastifyPluginAsync, {});
|
||||||
}
|
}
|
||||||
for (const route of Object.values(routes)) {
|
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, {})
|
await fastify.register(db.useDatabase as FastifyPluginAsync, {});
|
||||||
void fastify.register(fastifyFormBody, {})
|
await fastify.register(authPlugin as FastifyPluginAsync, {});
|
||||||
void fastify.register(fastifyMultipart, {})
|
await fastify.register(jwtPlugin as FastifyPluginAsync, {});
|
||||||
console.log(fastify.db.getUser(1));
|
|
||||||
|
void fastify.register(fastifyFormBody, {});
|
||||||
|
void fastify.register(fastifyMultipart, {});
|
||||||
|
|
||||||
// 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
|
||||||
void fastify.register(fp(async (fastify) => {
|
void fastify.register(fp(async (fastify2) => {
|
||||||
const image_store = process.env.USER_ICONS_STORE ?? "/tmp/icons";
|
const image_store = process.env.USER_ICONS_STORE ?? '/tmp/icons';
|
||||||
fastify.decorate('image_store', image_store)
|
fastify2.decorate('image_store', image_store);
|
||||||
await mkdir(fastify.image_store, { recursive: true })
|
await mkdir(fastify2.image_store, { recursive: true });
|
||||||
}))
|
}));
|
||||||
|
|
||||||
}
|
};
|
||||||
|
|
||||||
export default app
|
export default app;
|
||||||
export { app }
|
export { app };
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import fp from 'fastify-plugin'
|
import fp from 'fastify-plugin';
|
||||||
import sensible, { FastifySensibleOptions } from '@fastify/sensible'
|
import sensible, { FastifySensibleOptions } from '@fastify/sensible';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This plugins adds some utilities to handle http errors
|
* 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
|
* @see https://github.com/fastify/fastify-sensible
|
||||||
*/
|
*/
|
||||||
export default fp<FastifySensibleOptions>(async (fastify) => {
|
export default fp<FastifySensibleOptions>(async (fastify) => {
|
||||||
fastify.register(sensible)
|
fastify.register(sensible);
|
||||||
})
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,51 @@
|
||||||
import { FastifyPluginAsync } from 'fastify'
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
import { join } from 'node:path'
|
import { join } from 'node:path';
|
||||||
import { open } from 'node:fs/promises'
|
import { open } from 'node:fs/promises';
|
||||||
import sharp from 'sharp'
|
import sharp from 'sharp';
|
||||||
import rawBody from 'raw-body'
|
import rawBody from 'raw-body';
|
||||||
import { isNullish } from '@shared/utils'
|
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, {});
|
// await fastify.register(authMethod, {});
|
||||||
// here we register plugins that will be active for the current fastify instance (aka everything in this function)
|
// 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>`
|
// we register a route handler for: `/<USERID_HERE>`
|
||||||
// it sets some configuration options, and set the actual function that will handle the request
|
// it sets some configuration options, and set the actual function that will handle the request
|
||||||
|
|
||||||
fastify.addContentTypeParser('*', function(request, payload, done: any) {
|
fastify.addContentTypeParser('*', function(request, payload, done) {
|
||||||
done()
|
done(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post('/:userid', async function(request, reply) {
|
fastify.post<{ Params: { userid: string } }>('/:userid', async function(request, reply) {
|
||||||
let buffer = await rawBody(request.raw);
|
const buffer = await rawBody(request.raw);
|
||||||
// this is how we get the `:userid` part of things
|
// 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)) {
|
if (isNullish(userid)) {
|
||||||
return await reply.code(403);
|
return await reply.code(403);
|
||||||
}
|
}
|
||||||
const image_store: string = fastify.getDecorator('image_store')
|
const image_store: string = fastify.getDecorator('image_store');
|
||||||
const image_path = join(image_store, userid)
|
const image_path = join(image_store, userid);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let img = sharp(buffer);
|
const img = sharp(buffer);
|
||||||
img.resize({
|
img.resize({
|
||||||
height: 128,
|
height: 128,
|
||||||
width: 128,
|
width: 128,
|
||||||
fit: 'fill',
|
fit: 'fill',
|
||||||
})
|
});
|
||||||
const data = await img.png({ compressionLevel: 6 }).toBuffer()
|
const data = await img.png({ compressionLevel: 6 }).toBuffer();
|
||||||
let image_file = await open(image_path, "w", 0o666)
|
const image_file = await open(image_path, 'w', 0o666);
|
||||||
await image_file.write(data);
|
await image_file.write(data);
|
||||||
await image_file.close()
|
await image_file.close();
|
||||||
} catch (e: any) {
|
}
|
||||||
|
catch (e) {
|
||||||
fastify.log.error(`Error: ${e}`);
|
fastify.log.error(`Error: ${e}`);
|
||||||
reply.code(400);
|
reply.code(400);
|
||||||
return { status: "error", message: e.toString() };
|
return { status: 'error' };
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export default route
|
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"
|
import app from './app';
|
||||||
|
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
const envToLogger = {
|
const envToLogger = {
|
||||||
|
|
@ -16,15 +16,16 @@ const start = async () => {
|
||||||
},
|
},
|
||||||
production: true,
|
production: true,
|
||||||
test: false,
|
test: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
const f: FastifyInstance = fastify({ logger: envToLogger.development });
|
const f: FastifyInstance = fastify({ logger: envToLogger.development });
|
||||||
try {
|
try {
|
||||||
await f.register(app, {});
|
await f.register(app, {});
|
||||||
await f.listen({ port: 80, host: '0.0.0.0' })
|
await f.listen({ port: 80, host: '0.0.0.0' });
|
||||||
} catch (err) {
|
|
||||||
f.log.error(err)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
}
|
||||||
|
catch (err) {
|
||||||
|
f.log.error(err);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
start()
|
};
|
||||||
|
start();
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { defineConfig } from 'vite'
|
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'
|
import fs from 'node:fs';
|
||||||
|
|
||||||
function collectDeps(...pkgJsonPaths) {
|
function collectDeps(...pkgJsonPaths) {
|
||||||
const allDeps = new Set();
|
const allDeps = new Set();
|
||||||
|
|
@ -20,27 +20,32 @@ function collectDeps(...pkgJsonPaths) {
|
||||||
|
|
||||||
const externals = collectDeps(
|
const externals = collectDeps(
|
||||||
'./package.json',
|
'./package.json',
|
||||||
'../@shared/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,
|
||||||
lib: {
|
lib: {
|
||||||
entry: path.resolve(__dirname, 'src/run.ts'), // adjust main entry
|
entry: path.resolve(__dirname, 'src/run.ts'),
|
||||||
formats: ['cjs'], // CommonJS for Node.js
|
// adjust main entry
|
||||||
|
formats: ['cjs'],
|
||||||
|
// CommonJS for Node.js
|
||||||
fileName: () => 'index.js',
|
fileName: () => 'index.js',
|
||||||
},
|
},
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: externals,
|
external: externals,
|
||||||
},
|
},
|
||||||
target: 'node22', // or whatever Node version you use
|
target: 'node22',
|
||||||
|
// or whatever Node version you use
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
minify: true, // for easier debugging
|
minify: true,
|
||||||
}
|
// for easier debugging
|
||||||
})
|
},
|
||||||
|
});
|
||||||
|
|
|
||||||
1469
src/package-lock.json
generated
1469
src/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "workspace",
|
"name": "workspace",
|
||||||
|
"type": "module",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
|
@ -7,14 +8,27 @@
|
||||||
"./icons",
|
"./icons",
|
||||||
"./auth"
|
"./auth"
|
||||||
],
|
],
|
||||||
|
"lint-staged": {
|
||||||
|
"*": [
|
||||||
|
"eslint --fix"
|
||||||
|
]
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build --workspaces --if-present",
|
"build": "npm run build --workspaces --if-present",
|
||||||
"fclean": "rimraf \"**/dist\"",
|
"fclean": "rimraf \"**/dist\"",
|
||||||
"clean": "rimraf \"**/node_modules\"",
|
"clean": "rimraf \"**/node_modules\"",
|
||||||
"install-all": "npm install"
|
"install-all": "npm install",
|
||||||
|
"dev:prepare": "husky"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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": {
|
"dependencies": {
|
||||||
"bindings": "^1.5.0"
|
"bindings": "^1.5.0"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue