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 {
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"]
otp text [null, Note: "If otp is NULL, then the user didn't configure 2FA"]
guest integer [not null, default: 0]

View file

@ -1,6 +1,7 @@
CREATE TABLE IF NOT EXISTS user (
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
login_name TEXT UNIQUE,
display_name TEXT NOT NULL,
password TEXT,
otp TEXT,
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> {
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; }
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;

View file

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

View file

@ -29,7 +29,7 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
void _res;
try {
const { name, password } = req.body;
const user = this.db.getUserFromName(name);
const user = this.db.getUserFromLoginName(name);
// does the user exist
// 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');}
// password is good too !
if (this.db.getUserFromName(name) !== undefined) {return makeResponse('failed', 'signin.failed.username.existing');}
const u = await this.db.createUser(name, password, false);
if (this.db.getUserFromLoginName(name) !== undefined) {return makeResponse('failed', 'signin.failed.username.existing');}
const u = await this.db.createUser(name, name, password, false);
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...

View file

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