feat(auth): split login_name and display_name for better oauth2/guest user handling

This commit is contained in:
Maieul BOYER 2025-10-25 17:07:17 +02:00 committed by Maix0
parent e0689143c4
commit 332086d5e2
8 changed files with 24 additions and 18 deletions

View file

@ -17,7 +17,8 @@ Project Transcendance {
Table user { Table user {
id text [PK, not null] id text [PK, not null]
name text [not null] login_name text [unique]
display_name text [not null]
password text [null, Note: "If password is NULL, this means that the user is created through OAUTH2"] password text [null, Note: "If password is NULL, this means that the user is created through OAUTH2"]
otp text [null, Note: "If otp is NULL, then the user didn't configure 2FA"] otp text [null, Note: "If otp is NULL, then the user didn't configure 2FA"]
guest integer [not null, default: 0] guest integer [not null, default: 0]

View file

@ -1,6 +1,7 @@
CREATE TABLE IF NOT EXISTS user ( CREATE TABLE IF NOT EXISTS user (
id TEXT PRIMARY KEY NOT NULL, id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL, login_name TEXT UNIQUE,
display_name TEXT NOT NULL,
password TEXT, password TEXT,
otp TEXT, otp TEXT,
guest INTEGER NOT NULL DEFAULT 0 guest INTEGER NOT NULL DEFAULT 0

View file

@ -50,7 +50,7 @@ export const OauthImpl: Omit<IOauthDb, keyof IUserDb> = {
async createUserWithProvider(this: IOauthDb, provider: string, unique_id: string, username: string): Promise<User | undefined> { async createUserWithProvider(this: IOauthDb, provider: string, unique_id: string, username: string): Promise<User | undefined> {
unique_id = `provider:${unique_id}`; unique_id = `provider:${unique_id}`;
const user = await this.createUser(username, undefined, false); const user = await this.createUser(null, username, undefined, false);
if (isNullish(user)) { return undefined; } if (isNullish(user)) { return undefined; }
this.prepare('INSERT INTO auth (provider, user, oauth2_user) VALUES (@provider, @user_id, @unique_id)').run({ provider, user_id: user.id, unique_id }); this.prepare('INSERT INTO auth (provider, user, oauth2_user) VALUES (@provider, @user_id, @unique_id)').run({ provider, user_id: user.id, unique_id });
return user; return user;

View file

@ -7,11 +7,11 @@ import { UUID, newUUID } from '@shared/utils/uuid';
// never use this directly // never use this directly
export interface IUserDb extends Database { export interface IUserDb extends Database {
getUserFromName(name: string): User | undefined, getUserFromLoginName(name: string): User | undefined,
getUser(id: string): User | undefined, getUser(id: string): User | undefined,
getUserOtpSecret(id: UserId): string | undefined, getUserOtpSecret(id: UserId): string | undefined,
createUser(name: string, password: string | undefined, guest: boolean): Promise<User | undefined>, createUser(login_name: string | null, display_name: string, password: string | undefined, guest: boolean): Promise<User | undefined>,
createUser(name: string, password: string | undefined): Promise<User | undefined>, createUser(login_name: string | null, display_name: string, password: string | undefined): Promise<User | undefined>,
setUserPassword(id: UserId, password: string | undefined): Promise<User | undefined>, setUserPassword(id: UserId, password: string | undefined): Promise<User | undefined>,
ensureUserOtpSecret(id: UserId): string | undefined, ensureUserOtpSecret(id: UserId): string | undefined,
deleteUserOtpSecret(id: UserId): void, deleteUserOtpSecret(id: UserId): void,
@ -25,10 +25,10 @@ export const UserImpl: Omit<IUserDb, keyof Database> = {
* *
* @returns The user if it exists, undefined otherwise * @returns The user if it exists, undefined otherwise
*/ */
getUserFromName(this: IUserDb, name: string): User | undefined { getUserFromLoginName(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 login_name = @name LIMIT 1',
).get({ name }) as (Partial<User> | undefined), ).get({ name }) as (Partial<User> | undefined),
); );
}, },
@ -56,13 +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, guest: boolean = false): Promise<User | undefined> { async createUser(this: IUserDb, login_name: string | null, display_name: string, password: string | undefined, guest: boolean = false): Promise<User | undefined> {
password = await hashPassword(password); password = await hashPassword(password);
const id = newUUID(); const id = newUUID();
return userFromRow( return userFromRow(
this.prepare( this.prepare(
'INSERT OR FAIL INTO user (id, name, password, guest) VALUES (@id, @name, @password, @guest) RETURNING *', 'INSERT OR FAIL INTO user (id, login_name, display_name, password, guest) VALUES (@id, @login_name, @display_name, @password, @guest) RETURNING *',
).get({ id, name, password, guest: guest ? 1 : 0 }) as (Partial<User> | undefined), ).get({ id, login_name, display_name, password, guest: guest ? 1 : 0 }) as (Partial<User> | undefined),
); );
}, },
@ -108,7 +108,8 @@ export type UserId = UUID;
export type User = { export type User = {
readonly id: UserId; readonly id: UserId;
readonly name: string; readonly login_name?: string;
readonly display_name: string;
readonly password?: string; readonly password?: string;
readonly otp?: string; readonly otp?: string;
readonly guest: boolean; readonly guest: boolean;
@ -149,11 +150,12 @@ async function hashPassword(
export function userFromRow(row?: Partial<User>): User | undefined { export 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.display_name)) return undefined;
if (isNullish(row.guest)) return undefined; if (isNullish(row.guest)) return undefined;
return { return {
id: row.id, id: row.id,
name: row.name, login_name: row.login_name ?? undefined,
display_name: row.display_name,
password: row.password ?? undefined, password: row.password ?? undefined,
otp: row.otp ?? undefined, otp: row.otp ?? undefined,
guest: !!(row.guest ?? true), guest: !!(row.guest ?? true),

View file

@ -31,6 +31,8 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
const noun = getRandomFromList(fastify.words.nouns); const noun = getRandomFromList(fastify.words.nouns);
const user = await this.db.createUser( const user = await this.db.createUser(
// no login_name => can't login
null,
`${adjective} ${noun}`, `${adjective} ${noun}`,
// no password // no password
undefined, undefined,

View file

@ -29,7 +29,7 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
void _res; void _res;
try { try {
const { name, password } = req.body; const { name, password } = req.body;
const user = this.db.getUserFromName(name); const user = this.db.getUserFromLoginName(name);
// does the user exist // does the user exist
// does it have a password setup ? // does it have a password setup ?

View file

@ -46,8 +46,8 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
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) {return makeResponse('failed', 'signin.failed.username.existing');} if (this.db.getUserFromLoginName(name) !== undefined) {return makeResponse('failed', 'signin.failed.username.existing');}
const u = await this.db.createUser(name, password, false); const u = await this.db.createUser(name, name, password, false);
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...

View file

@ -35,7 +35,7 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
const payload = { const payload = {
name: user.name, name: user.display_name,
id: user.id, id: user.id,
// the !! converts a value from <something> to either `true` or `false` // the !! converts a value from <something> to either `true` or `false`
// it uses the same convention from using <something> in a if, meaning that // it uses the same convention from using <something> in a if, meaning that