feat(users): Adding the profile page and the TOTP connection
Added user profile page (/profile) with display name, password, and TOTP management Implemented TOTP authentication flow in the login process Added backend APIs for changing display name and password Made user display names unique in the database Removed the entire icons service (server, routes, and Docker configuration) Added collision-avoidance logic for duplicate display names during user creation
This commit is contained in:
commit
492647b817
75 changed files with 3555 additions and 1497 deletions
|
|
@ -25,6 +25,6 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/node": "^22.19.1"
|
||||
"@types/node": "^22.19.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ const kRouteAuthDone = Symbol('shared-route-auth-done');
|
|||
type AuthedUser = {
|
||||
id: UserId;
|
||||
name: string;
|
||||
guest: boolean;
|
||||
};
|
||||
|
||||
declare module 'fastify' {
|
||||
|
|
@ -118,7 +119,7 @@ export const authPlugin = fp<{ onlySchema?: boolean }>(async (fastify, { onlySch
|
|||
.clearCookie('token', { path: '/' })
|
||||
.makeResponse(401, 'notLoggedIn', 'auth.noUser');
|
||||
}
|
||||
req.authUser = { id: user.id, name: user.display_name };
|
||||
req.authUser = { id: user.id, name: user.name, guest: user.guest };
|
||||
}
|
||||
catch {
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ Project Transcendance {
|
|||
Table user {
|
||||
id text [PK, not null]
|
||||
login text [unique]
|
||||
name text [not null]
|
||||
name text [not null, unique]
|
||||
password text [null, Note: "If password is NULL, this means that the user is created through OAUTH2 or guest login"]
|
||||
otp text [null, Note: "If otp is NULL, then the user didn't configure 2FA"]
|
||||
guest integer [not null, default: 0]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
CREATE TABLE IF NOT EXISTS user (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
login TEXT UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
password TEXT,
|
||||
otp TEXT,
|
||||
guest INTEGER NOT NULL DEFAULT 0,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Otp } from '@shared/auth';
|
|||
import { isNullish } from '@shared/utils';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { UUID, newUUID } from '@shared/utils/uuid';
|
||||
import { SqliteError } from 'better-sqlite3';
|
||||
|
||||
// never use this directly
|
||||
|
||||
|
|
@ -20,6 +21,10 @@ export interface IUserDb extends Database {
|
|||
getAllUserFromProvider(provider: string): User[] | undefined,
|
||||
getAllUsers(this: IUserDb): User[] | undefined,
|
||||
|
||||
|
||||
updateDisplayName(id: UserId, new_name: string): boolean,
|
||||
|
||||
getUserFromDisplayName(name: string): User | undefined,
|
||||
};
|
||||
|
||||
export const UserImpl: Omit<IUserDb, keyof Database> = {
|
||||
|
|
@ -159,6 +164,24 @@ export const UserImpl: Omit<IUserDb, keyof Database> = {
|
|||
const req = this.prepare('SELECT * FROM user WHERE oauth2 = @oauth2').get({ oauth2: `${provider}:${unique}` }) as Partial<User> | undefined;
|
||||
return userFromRow(req);
|
||||
},
|
||||
|
||||
updateDisplayName(this: IUserDb, id: UserId, new_name: string): boolean {
|
||||
try {
|
||||
this.prepare('UPDATE OR FAIL user SET name = @new_name WHERE id = @id').run({ id, new_name });
|
||||
return true;
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof SqliteError) {
|
||||
if (e.code === 'SQLITE_CONSTRAINT_UNIQUE') return false;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
getUserFromDisplayName(this: IUserDb, name: string) {
|
||||
const res = this.prepare('SELECT * FROM user WHERE name = @name LIMIT 1').get({ name }) as User | undefined;
|
||||
return userFromRow(res);
|
||||
},
|
||||
};
|
||||
|
||||
export type UserId = UUID;
|
||||
|
|
@ -170,7 +193,7 @@ export type User = {
|
|||
readonly password?: string;
|
||||
readonly otp?: string;
|
||||
readonly guest: boolean;
|
||||
// will be split/merged from the `provider` column
|
||||
// will be split/merged from the `oauth2` column
|
||||
readonly provider_name?: string;
|
||||
readonly provider_unique?: string;
|
||||
};
|
||||
|
|
@ -207,7 +230,7 @@ async function hashPassword(
|
|||
*
|
||||
* @returns The user if it exists, undefined otherwise
|
||||
*/
|
||||
export function userFromRow(row?: Partial<Omit<User, 'provider_name' | 'provider_unique'> & { provider?: string }>): User | undefined {
|
||||
export function userFromRow(row?: Partial<Omit<User, 'provider_name' | 'provider_unique'> & { oauth2?: string }>): User | undefined {
|
||||
if (isNullish(row)) return undefined;
|
||||
if (isNullish(row.id)) return undefined;
|
||||
if (isNullish(row.name)) return undefined;
|
||||
|
|
@ -216,9 +239,9 @@ export function userFromRow(row?: Partial<Omit<User, 'provider_name' | 'provider
|
|||
let provider_name = undefined;
|
||||
let provider_unique = undefined;
|
||||
|
||||
if (row.provider) {
|
||||
const splitted = row.provider.split(':', 1);
|
||||
if (splitted.length != 2) { return undefined; }
|
||||
if (row.oauth2) {
|
||||
const splitted = row.oauth2.split(/:(.*)/);
|
||||
if (splitted.length != 3) { return undefined; }
|
||||
provider_name = splitted[0];
|
||||
provider_unique = splitted[1];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue