fix(shared/auth/icon): Fixed lots of small things

Icons: Fixed docker-compose to force JWT_SECRET for now
Auth: Fixed Guest Login to actually work
Auth: Added `Login as Guest` in the login_demo page
Shared: Fixed db/user + uuid modules
This commit is contained in:
Maieul BOYER 2025-10-08 17:24:46 +02:00 committed by Maix0
parent 5306ccfc60
commit 2074f8d8f1
6 changed files with 51 additions and 11 deletions

View file

@ -41,6 +41,7 @@ services:
- images-volume:/volumes/store - images-volume:/volumes/store
- sqlite-volume:/volumes/database - sqlite-volume:/volumes/database
environment: environment:
- JWT_SECRET=KRUGKIDROVUWG2ZAMJZG653OEBTG66BANJ2W24DTEBXXMZLSEB2GQZJANRQXU6JA
- USER_ICONS_STORE=/volumes/store - USER_ICONS_STORE=/volumes/store
- DATABASE_DIR=/volumes/database - DATABASE_DIR=/volumes/database

View file

@ -2,7 +2,7 @@ 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';
import { UUID } from 'uuidv7'; import { UUID, newUUID } from '@shared/utils/uuid';
// never use this directly // never use this directly
@ -56,12 +56,13 @@ export const UserImpl: Omit<IUserDb, keyof Database> = {
* *
* @returns The user struct * @returns The user struct
*/ */
async createUser(this: IUserDb, name: string, password: string | undefined): Promise<User | undefined> { async createUser(this: IUserDb, name: string, password: string | undefined, guest: boolean = false): Promise<User | undefined> {
password = await hashPassword(password); password = await hashPassword(password);
const id = newUUID();
return userFromRow( return userFromRow(
this.prepare( this.prepare(
'INSERT OR FAIL INTO user (name, password) VALUES (@name, @password) RETURNING *', 'INSERT OR FAIL INTO user (id, name, password, guest) VALUES (@id, @name, @password, @guest) RETURNING *',
).get({ name, password }) as (Partial<User> | undefined), ).get({ id, name, password, guest: guest ? 1 : 0 }) as (Partial<User> | undefined),
); );
}, },
@ -110,6 +111,7 @@ export type User = {
readonly name: string; readonly name: string;
readonly password?: string; readonly password?: string;
readonly otp?: string; readonly otp?: string;
readonly guest: boolean;
}; };
export async function verifyUserPassword( export async function verifyUserPassword(
@ -148,10 +150,12 @@ 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.id)) return undefined;
if (isNullish(row.name)) return undefined; if (isNullish(row.name)) return undefined;
if (isNullish(row.guest)) return undefined;
return { return {
id: row.id, id: row.id,
name: row.name, name: row.name,
password: row.password ?? undefined, password: row.password ?? undefined,
otp: row.otp ?? undefined, otp: row.otp ?? undefined,
guest: !!(row.guest ?? true),
}; };
} }

View file

@ -0,0 +1,22 @@
import * as uuidv7 from 'uuidv7';
export type UUID = `${string}-${string}-${string}-${string}-${string}` & { readonly __brand: unique symbol };
export default UUID;
export function newUUID(): UUID {
return uuidv7.uuidv7() as UUID;
}
export function isUUID(s: string): s is UUID {
try {
uuidv7.UUID.parse(s);
return true;
}
catch {
return false;
}
}
export function parseUUID(s: string): UUID {
return uuidv7.UUID.parse(s).toString() as UUID;
}

View file

@ -20,6 +20,7 @@
<br /> <br />
<br /> <br />
<button id="b-login">Login</button> <button id="b-login">Login</button>
<button id="b-login-guest">Login as Guest</button>
<br /> <br />
<button id="b-logout">Logout</button> <button id="b-logout">Logout</button>
<br /> <br />

View file

@ -11,6 +11,7 @@ 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 bLoginGuest = document.querySelector('#b-login-guest');
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');
@ -27,6 +28,16 @@ function setResponse(obj) {
} }
let otpToken = null; let otpToken = null;
bLoginGuest.addEventListener('click', async () => {
const res = await fetch('/api/auth/guest', { method: 'POST' });
const json = await res.json();
setResponse(json);
if (json.kind === 'success') {
if (json?.payload?.token) {document.cookie = `token=${json?.payload?.token}`;}
}
});
bOtpSend.addEventListener('click', async () => { bOtpSend.addEventListener('click', async () => {
const 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();

View file

@ -4,8 +4,8 @@ import { Static, Type } from '@sinclair/typebox';
import { typeResponse, makeResponse, isNullish } from '@shared/utils'; import { typeResponse, makeResponse, isNullish } from '@shared/utils';
export const GuestLoginRes = Type.Union([ export const GuestLoginRes = Type.Union([
typeResponse('failed', 'login.failed.generic'), typeResponse('failed', ['guestLogin.failed.generic.unknown', 'guestLogin.failed.generic.error']),
typeResponse('success', 'login.success', { typeResponse('success', 'guestLogin.success', {
token: Type.String({ token: Type.String({
description: 'JWT that represent a logged in user', description: 'JWT that represent a logged in user',
}), }),
@ -38,14 +38,15 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
true, true,
); );
if (isNullish(user)) { if (isNullish(user)) {
return makeResponse('failed', 'login.failed.generic'); return makeResponse('failed', 'guestLogin.failed.generic.unknown');
} }
return makeResponse('success', 'login.success', { return makeResponse('success', 'guestLogin.success', {
token: this.signJwt('auth', user.id), token: this.signJwt('auth', user.id.toString()),
}); });
} }
catch { catch (e: unknown) {
return makeResponse('failed', 'login.failed.generic'); fastify.log.error(e);
return makeResponse('failed', 'guestLogin.failed.generic.error');
} }
}, },
); );