feat(auth/user): Finished User Rework to handle Guest

- Split userinfo APIs to their own service (`user`)
- Added user service to nginx and docker-compose
- Cleaned up package.json across the project to remove useless
  depedencies
- Added word list for Guest username generation (source in file itself)
- Reworked internal of `user` DB to not have a difference between "raw"
  id and normal ID (UUID)
This commit is contained in:
Maieul BOYER 2025-10-06 16:59:18 +02:00 committed by Maix0
parent 7d0f5c11d6
commit 1cbd778131
24 changed files with 4273 additions and 46 deletions

View file

@ -61,7 +61,7 @@ bOtpDisable.addEventListener('click', async () => {
bWhoami.addEventListener('click', async () => {
let username = '';
try {
const res = await fetch('/api/auth/whoami');
const res = await fetch('/api/user/info/me');
const json = await res.json();
setResponse(json);
if (json?.kind === 'success') {username = json?.payload?.name;}

View file

@ -25,8 +25,7 @@
"@sinclair/typebox": "^0.34.40",
"fastify": "^5.0.0",
"fastify-cli": "^7.4.0",
"fastify-plugin": "^5.0.0",
"sharp": "^0.34.2"
"fastify-plugin": "^5.0.0"
},
"devDependencies": {
"@types/node": "^22.1.0",

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,40 @@
// Why does this file exists ?
// We want to make random-ish username for the guest, but still reconizable usernames
// So we do `${adjective}_${nouns}`
// there is around 30k combinaison, so we should be fine :)
import fp from 'fastify-plugin';
// @ts-expect-error: Ts can't load raw txt files - vite does it
import _adjectives from './files/adjectives.txt?raw';
// @ts-expect-error: Ts can't load raw txt files - vite does it
import _nouns from './files/nouns.txt?raw';
type WordsCategory = 'adjectives' | 'nouns';
type Words = { [k in WordsCategory]: string[] };
function toTitleCase(str: string) {
return str.replace(
/\w\S*/g,
text => text.charAt(0).toUpperCase() + text.substring(1).toLowerCase(),
);
}
// strong typing those import :)
const RAW_WORDS: { [k in WordsCategory]: string } = { adjectives: _adjectives, nouns: _nouns };
const WORDS: Words = Object.fromEntries(Object.entries(RAW_WORDS).map(([k, v]) => {
const words = v.split('\n').map(s => s.trim()).filter(s => !(s.startsWith('#') || s.length === 0)).map(toTitleCase);
return [k, words];
})) as Words;
export default fp<object>(async (fastify) => {
fastify.decorate('words', WORDS);
});
// When using .decorate you have to specify added properties for Typescript
declare module 'fastify' {
export interface FastifyInstance {
words: Words;
}
}

View file

@ -0,0 +1,54 @@
import { FastifyPluginAsync } from 'fastify';
import { Static, Type } from '@sinclair/typebox';
import { typeResponse, makeResponse, isNullish } from '@shared/utils';
export const GuestLoginRes = Type.Union([
typeResponse('failed', 'login.failed.generic'),
typeResponse('success', 'login.success', {
token: Type.String({
description: 'JWT that represent a logged in user',
}),
}),
]);
export type GuestLoginRes = Static<typeof GuestLoginRes>;
const getRandomFromList = (list: string[]): string => {
return list[Math.floor(Math.random() * list.length)];
};
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
void _opts;
fastify.post(
'/api/auth/guest',
{ schema: { response: { '2xx': GuestLoginRes } } },
async function(req, res) {
void req;
void res;
try {
const adjective = getRandomFromList(fastify.words.adjectives);
const noun = getRandomFromList(fastify.words.nouns);
const user = await this.db.createUser(
`${adjective} ${noun}`,
// no password
undefined,
// is a guest
true,
);
if (isNullish(user)) {
return makeResponse('failed', 'login.failed.generic');
}
return makeResponse('success', 'login.success', {
token: this.signJwt('auth', user.id),
});
}
catch {
return makeResponse('failed', 'login.failed.generic');
}
},
);
};
export default route;

View file

@ -44,7 +44,7 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
}
// get the Otp sercret from the db
const user = this.db.getUserFromName(dJwt.who);
const user = this.db.getUser(dJwt.who);
if (isNullish(user?.otp)) {
// oops, either no user, or user without otpSecret
// fuck off

View file

@ -1,27 +0,0 @@
import { FastifyPluginAsync } from 'fastify';
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'),
]);
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 } },
async function(req, _res) {
void _res;
if (isNullish(req.authUser)) {return makeResponse('failure', 'whoami.failure.generic');}
return makeResponse('success', 'whoami.success', { name: req.authUser.name });
},
);
};
export default route;