feat(death): I want to die...

This commit is contained in:
Maieul BOYER 2025-08-25 18:42:35 +02:00 committed by Maix0
parent 33e893ec58
commit a16852c1b9
34 changed files with 761 additions and 321 deletions

10
src/auth/entrypoint.sh Normal file
View file

@ -0,0 +1,10 @@
#!/bin/sh
set -e
set -x
# do anything here
cp -r /extra /files
# run the CMD [ ... ] from the dockerfile
exec "$@"

0
src/auth/extra/.gitkeep Normal file
View file

View file

@ -1,35 +0,0 @@
{
"type": "object",
"properties": {
"providers": {
"required": [],
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"redirect_url": {
"type": "string"
},
"token_url": {
"type": "string"
},
"client_id": {
"type": "string"
},
"secret_env": {
"type": "string"
}
},
"required": [
"redirect_url",
"token_url",
"client_id",
"secret_env"
]
}
}
},
"required": [
"providers"
]
}

View file

@ -1,7 +0,0 @@
#:schema ./providers-schema.json
[providers.42]
token_url = "" # which url to use
redirect_url = "" # redirect_url
client_id = "" # the client_id for the provider
secret_env = "" # env containing the secret for the provider

View file

@ -22,6 +22,7 @@
"@fastify/multipart": "^9.0.3",
"@fastify/sensible": "^6.0.0",
"@fastify/static": "^8.2.0",
"@sinclair/typebox": "^0.34.40",
"fastify": "^5.0.0",
"fastify-cli": "^7.4.0",
"fastify-plugin": "^5.0.0",

View file

@ -3,8 +3,12 @@ import fastifyFormBody from '@fastify/formbody'
import fastifyMultipart from '@fastify/multipart'
import { mkdir } from 'node:fs/promises'
import fp from 'fastify-plugin'
import * as db from '@shared/database'
import * as auth from '@shared/auth'
// @ts-ignore: import.meta.glob is a vite thing. Typescript doesn't know this...
const plugins = import.meta.glob('./plugins/**/*.ts', { eager: true });
// @ts-ignore: import.meta.glob is a vite thing. Typescript doesn't know this...
const routes = import.meta.glob('./routes/**/*.ts', { eager: true });
@ -21,15 +25,17 @@ const app: FastifyPluginAsync = async (
): Promise<void> => {
// Place here your custom code!
for (const plugin of Object.values(plugins)) {
void fastify.register(plugin, {});
void fastify.register(plugin as any, {});
}
for (const route of Object.values(routes)) {
void fastify.register(route, {});
void fastify.register(route as any, {});
}
//void fastify.register(MyPlugin, {})
await fastify.register(db.useDatabase as any, {})
await fastify.register(auth.jwtPlugin as any, {})
void fastify.register(fastifyFormBody, {})
void fastify.register(fastifyMultipart, {})
console.log(fastify.db.getUser(0 as any));
// The use of fastify-plugin is required to be able
// to export the decorators to the outer scope

View file

@ -0,0 +1,81 @@
import { FastifyPluginAsync } from "fastify";
import { Static, Type } from "@sinclair/typebox";
import { user as userDb } from "@shared/database";
import type { } from "@shared/auth";
export const LoginReq = Type.Object({
name: Type.String(),
password: Type.String({ minLength: 8, maxLength: 32 }),
});
export type LoginReq = Static<typeof LoginReq>;
export const LoginRes = Type.Union([
Type.Object({
kind: Type.Const("failed"),
msg_key: Type.Union([
Type.Const("login.failed.generic"),
Type.Const("login.failed.invalid"),
]),
}),
Type.Object({
kind: Type.Const("otpRequired"),
msg_key: Type.Const("login.otpRequired"),
token: Type.String({
description: "Code to send with the OTP to finish login",
}),
}),
Type.Object({
kind: Type.Const("success"),
msg_key: Type.Const("login.success"),
token: Type.String({ description: "The JWT token" }),
}),
]);
export type LoginRes = Static<typeof LoginRes>;
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.post<{ Body: LoginReq; Response: LoginRes }>(
"/login",
{
schema: {
body: LoginReq,
response: { "2xx": LoginRes },
},
},
async function(req, res) {
try {
let { name, password } = req.body;
let user = this.db.getUserFromName(name);
// does the user exist
// does it have a password setup ?
if (user === null || user.password === null)
return { kind: "failed", msg_key: "login.failed.invalid" };
// does the password he provided match the one we have
if (!(await userDb.verifyUserPassword(user, password)))
return { kind: "failed", msg_key: "login.failed.invalid" };
// does the user has 2FA up ?
if (user.otp !== null) {
// yes -> we ask them to fill it,
// send them somehting to verify that they indeed passed throught the user+password phase
let otpToken = this.jwt.sign({ kind: "otp", user: user.name, createAt: Date.now() / 1000 });
return { kind: "otpRequired", msg_key: "login.otpRequired", token: otpToken };
}
// every check has been passed, they are now logged in, using this token to say who they are...
let userToken = this.jwt.sign({ kind: "auth", user: user.name, createAt: Date.now() / 1000 });
return { kind: "success", msg_key: "login.success", token: userToken }
}
catch {
return { kind: "failed", msg_key: "login.failed.generic" };
}
},
);
};
export default route;

View file

@ -1,49 +0,0 @@
import { FastifyPluginAsync } from 'fastify'
import { join } from 'node:path'
import { open } from 'node:fs/promises'
import sharp from 'sharp'
import { newUUIDv7 } from '@shared/uuid'
import rawBody from 'raw-body'
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
// await fastify.register(authMethod, {});
// here we register plugins that will be active for the current fastify instance (aka everything in this function)
// we register a route handler for: `/<USERID_HERE>`
// it sets some configuration options, and set the actual function that will handle the request
fastify.addContentTypeParser('*', function(request, payload, done) {
done()
});
fastify.post('/:userid', async function(request, reply) {
let buffer = await rawBody(request.raw);
// this is how we get the `:userid` part of things
const userid: string | undefined = (request.params as any)['userid'];
if (userid === undefined) {
return await reply.code(403);
}
const image_store: string = fastify.getDecorator('image_store')
const image_path = join(image_store, userid)
try {
let img = sharp(buffer);
img.resize({
height: 128,
width: 128,
fit: 'fill',
})
const data = await img.png({ compressionLevel: 6 }).toBuffer()
let image_file = await open(image_path, "w", 0o666)
await image_file.write(data);
await image_file.close()
} catch (e: any) {
fastify.log.error(`Error: ${e}`);
reply.code(400);
return { status: "error", message: e.toString() };
}
})
}
export default route

View file

@ -0,0 +1,99 @@
import { FastifyPluginAsync } from "fastify";
import { Static, Type } from "@sinclair/typebox";
const USERNAME_CHECK: RegExp = /^[a-zA-Z\_0-9]+$/;
export const SignInReq = Type.Object({
name: Type.String(),
password: Type.String({ minLength: 8, maxLength: 32 }),
});
export type SignInReq = Static<typeof SignInReq>;
export const SignInRes = Type.Union([
Type.Object({
kind: Type.Const("failed"),
msg_key: Type.Union([
Type.Const("signin.failed.generic"),
Type.Const("signin.failed.username.existing"),
Type.Const("signin.failed.username.toolong"),
Type.Const("signin.failed.username.tooshort"),
Type.Const("signin.failed.username.invalid"),
Type.Const("signin.failed.password.toolong"),
Type.Const("signin.failed.password.tooshort"),
Type.Const("signin.failed.password.invalid"),
]),
}),
Type.Object({
kind: Type.Const("sucess"),
msg_key: Type.Const("signin.sucess"),
token: Type.String({ description: "The JWT token" }),
}),
]);
export type SignInRes = Static<typeof SignInRes>;
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.post<{ Body: SignInReq; Response: SignInRes }>(
"/signin",
{ schema: { body: SignInReq, response: { "2xx": SignInRes } } },
async function(req, res) {
const { name, password } = req.body;
if (name.length < 4)
return {
kind: "failed",
msg_key: "signin.failed.username.tooshort",
};
if (name.length > 32)
return {
kind: "failed",
msg_key: "signin.failed.username.toolong",
};
if (!USERNAME_CHECK.test(name))
return {
kind: "failed",
msg_key: "signin.failed.username.invalid",
};
// username if good now :)
if (password.length < 8)
return {
kind: "failed",
msg_key: "signin.failed.password.tooshort",
};
if (password.length > 64)
return {
kind: "failed",
msg_key: "signin.failed.password.toolong",
};
// password is good too !
if (this.db.getUserFromName(name) !== null)
return {
kind: "failed",
msg_key: "signin.failed.username.existing",
};
let u = await this.db.createUser(name, password);
if (u === null)
return { kind: "failed", msg_key: "signin.failed.generic" };
// every check has been passed, they are now logged in, using this token to say who they are...
let userToken = this.jwt.sign({
kind: "auth",
user: u.name,
createAt: Date.now() / 1000,
});
let out = {
kind: "success",
msg_key: "login.success",
token: userToken,
};
console.log(out)
return out;
},
);
};
export default route;

View file

@ -0,0 +1,20 @@
import { FastifyPluginAsync } from "fastify";
import { Static, Type } from "@sinclair/typebox";
export const WhoAmIRes = Type.String({ description: "username" });
export type WhoAmIRes = Static<typeof WhoAmIRes>;
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.get(
"/whoami",
{ schema: { response: { "2xx": WhoAmIRes } } },
async function(req, res) {
return "yes";
},
);
};
export default route;

View file

@ -1,7 +1,7 @@
// this sould only be used by the docker file !
import fastify, { FastifyInstance } from "fastify";
import app from './app.js'
import app from "./app"
const start = async () => {
const envToLogger = {

View file

@ -2,12 +2,32 @@ import { defineConfig } from 'vite'
import tsconfigPaths from 'vite-tsconfig-paths'
import nodeExternals from 'rollup-plugin-node-externals'
import path from 'node:path'
import fs from 'node:fs'
function collectDeps(...pkgJsonPaths) {
const allDeps = new Set();
for (const pkgPath of pkgJsonPaths) {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
for (const dep of Object.keys(pkg.dependencies || {})) {
allDeps.add(dep);
}
for (const peer of Object.keys(pkg.peerDependencies || {})) {
allDeps.add(peer);
}
}
return Array.from(allDeps);
}
const externals = collectDeps(
'./package.json',
'../@shared/package.json'
);
export default defineConfig({
root: __dirname, // service root
plugins: [tsconfigPaths(), nodeExternals()],
build: {
ssr: true,
outDir: 'dist',
emptyOutDir: true,
@ -17,9 +37,9 @@ export default defineConfig({
fileName: () => 'index.js',
},
rollupOptions: {
external: [],
external: externals,
},
target: 'node24', // or whatever Node version you use
target: 'node22', // or whatever Node version you use
sourcemap: true,
minify: false, // for easier debugging
}