feat(db/oauth2): Added oauth2 handling to database

- Database: edited dmbl/sql for oauth2 changes
- Database/oauth2: new oauth2 mixin
- Database/user: exported raw functions to be used in oauth2 mixin
This commit is contained in:
Maieul BOYER 2025-10-25 15:39:49 +02:00 committed by Maix0
parent 26627cd4d7
commit bc7a615dcf
5 changed files with 98 additions and 7 deletions

View file

@ -1,14 +1,16 @@
import fp from 'fastify-plugin'; import fp from 'fastify-plugin';
import { FastifyInstance, FastifyPluginAsync } from 'fastify'; import { FastifyInstance, FastifyPluginAsync } from 'fastify';
import { Database as DbImpl } from './mixin/_base';
import { UserImpl, IUserDb } from './mixin/user';
import { isNullish } from '@shared/utils'; import { isNullish } from '@shared/utils';
import { Database as DbImpl } from './mixin/_base';
import { IOauthDb, OauthImpl } from './mixin/oauth2';
import { IUserDb, UserImpl } from './mixin/user';
Object.assign(DbImpl.prototype, UserImpl); Object.assign(DbImpl.prototype, UserImpl);
Object.assign(DbImpl.prototype, OauthImpl);
export interface Database extends DbImpl, IUserDb { } export interface Database extends DbImpl, IUserDb, IOauthDb { }
// When using .decorate you have to specify added properties for Typescript // When using .decorate you have to specify added properties for Typescript
declare module 'fastify' { declare module 'fastify' {

View file

@ -17,9 +17,10 @@ Project Transcendance {
Table user { Table user {
id text [PK, not null] id text [PK, not null]
name text [unique, not null] 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]
Note: "Represent a user" Note: "Represent a user"
} }
@ -27,7 +28,7 @@ Table user {
Table auth { Table auth {
id integer [PK, not null, increment] id integer [PK, not null, increment]
provider text [not null] provider text [not null]
user integer [ref: > user.id, not null] user text [ref: > user.id, not null]
oauth2_user text [not null, unique, Note: ''' oauth2_user text [not null, unique, Note: '''
This makes sure that an oauth2 login is the always the same `user` This makes sure that an oauth2 login is the always the same `user`
Aka can't have two account bound to the same <OAUTH2> account Aka can't have two account bound to the same <OAUTH2> account

View file

@ -1,6 +1,6 @@
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 UNIQUE, 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

@ -0,0 +1,88 @@
// import type { Database } from './_base';
import { isNullish } from '@shared/utils';
import { UserId, IUserDb, userFromRow, User } from './user';
// never use this directly
export interface IOauthDb extends IUserDb {
getAllFromProvider(this: IOauthDb, provider: string): ProviderUser[],
getProviderUser(this: IOauthDb, provider: string, unique_id: string): ProviderUser | undefined,
getUserFromProviderUser(this: IOauthDb, provider: string, unique_id: string): User | undefined,
getUserFromProviderUserId(this: IOauthDb, id: ProviderUserId): User | undefined,
getProviderUserFromId(this: IOauthDb, id: ProviderUserId): ProviderUser | undefined,
createUserWithProvider(this: IOauthDb, provider: string, unique_id: string, username: string): Promise<User | undefined>,
};
export const OauthImpl: Omit<IOauthDb, keyof IUserDb> = {
getAllFromProvider(this: IOauthDb, provider: string): ProviderUser[] {
return this.prepare('SELECT * FROM auth WHERE provider = @provider')
.all({ provider })
.map(r => providerUserFromRow(r as Partial<ProviderUser> | undefined))
.filter(v => !isNullish(v)) ?? [];
},
getProviderUser(this: IOauthDb, provider: string, unique_id: string): ProviderUser | undefined {
unique_id = `provider:${unique_id}`;
return providerUserFromRow(this.prepare('SELECT * FROM auth WHERE provider = @provider AND oauth2_user = @unique_id')
.get({ provider, unique_id }) as Partial<ProviderUser> | undefined);
},
getProviderUserFromId(this: IOauthDb, id: ProviderUserId): ProviderUser | undefined {
return providerUserFromRow(this.prepare('SELECT * FROM auth WHERE id = @id')
.get({ id }) as Partial<ProviderUser> | undefined);
},
getUserFromProviderUser(this: IOauthDb, provider: string, unique_id: string): User | undefined {
unique_id = `provider:${unique_id}`;
return userFromRow(
this.prepare('SELECT user.* from auth INNER JOIN user ON user.id = auth.user WHERE auth.provider = @provider AND auth.oauth2_user = @unique_id')
.get({ provider, unique_id }) as Partial<User> | undefined,
);
},
getUserFromProviderUserId(this: IOauthDb, id: ProviderUserId): User | undefined {
return userFromRow(
this.prepare('SELECT user.* from auth INNER JOIN user ON user.id = auth.user WHERE auth.id = @id')
.get({ id }) as Partial<User> | undefined,
);
},
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);
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;
},
};
export type ProviderUserId = number & { readonly __brand: unique symbol };
export type ProviderUser = {
readonly id: ProviderUserId,
readonly provider: string,
readonly user: UserId,
readonly oauth2_user: string,
};
/**
* Get a user from a row
*
* @param row The data from sqlite
*
* @returns The user if it exists, undefined otherwise
*/
function providerUserFromRow(row?: Partial<ProviderUser>): ProviderUser | undefined {
if (isNullish(row)) return undefined;
if (isNullish(row.id)) return undefined;
if (isNullish(row.provider)) return undefined;
if (isNullish(row.user)) return undefined;
if (isNullish(row.oauth2_user)) return undefined;
return {
id: row.id,
provider: row.provider,
user: row.user,
oauth2_user: row.oauth2_user,
};
}

View file

@ -146,7 +146,7 @@ async function hashPassword(
* *
* @returns The user if it exists, undefined otherwise * @returns The user if it exists, undefined otherwise
*/ */
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.name)) return undefined;