diff --git a/src/@shared/src/database/index.ts b/src/@shared/src/database/index.ts index 1f6d237..38853d4 100644 --- a/src/@shared/src/database/index.ts +++ b/src/@shared/src/database/index.ts @@ -1,14 +1,16 @@ import fp from 'fastify-plugin'; import { FastifyInstance, FastifyPluginAsync } from 'fastify'; -import { Database as DbImpl } from './mixin/_base'; -import { UserImpl, IUserDb } from './mixin/user'; 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, 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 declare module 'fastify' { diff --git a/src/@shared/src/database/init.dbml b/src/@shared/src/database/init.dbml index 815cc23..c416aa7 100644 --- a/src/@shared/src/database/init.dbml +++ b/src/@shared/src/database/init.dbml @@ -17,9 +17,10 @@ Project Transcendance { Table user { 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"] 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" } @@ -27,7 +28,7 @@ Table user { Table auth { id integer [PK, not null, increment] 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: ''' This makes sure that an oauth2 login is the always the same `user` Aka can't have two account bound to the same account diff --git a/src/@shared/src/database/init.sql b/src/@shared/src/database/init.sql index 5f2deb2..b6f6134 100644 --- a/src/@shared/src/database/init.sql +++ b/src/@shared/src/database/init.sql @@ -1,6 +1,6 @@ CREATE TABLE IF NOT EXISTS user ( id TEXT PRIMARY KEY NOT NULL, - name TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, password TEXT, otp TEXT, guest INTEGER NOT NULL DEFAULT 0 diff --git a/src/@shared/src/database/mixin/oauth2.ts b/src/@shared/src/database/mixin/oauth2.ts new file mode 100644 index 0000000..38c0861 --- /dev/null +++ b/src/@shared/src/database/mixin/oauth2.ts @@ -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, +}; + +export const OauthImpl: Omit = { + getAllFromProvider(this: IOauthDb, provider: string): ProviderUser[] { + return this.prepare('SELECT * FROM auth WHERE provider = @provider') + .all({ provider }) + .map(r => providerUserFromRow(r as Partial | 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 | undefined); + }, + + getProviderUserFromId(this: IOauthDb, id: ProviderUserId): ProviderUser | undefined { + return providerUserFromRow(this.prepare('SELECT * FROM auth WHERE id = @id') + .get({ id }) as Partial | 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 | 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 | undefined, + ); + }, + + async createUserWithProvider(this: IOauthDb, provider: string, unique_id: string, username: string): Promise { + 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 | 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, + }; +} diff --git a/src/@shared/src/database/mixin/user.ts b/src/@shared/src/database/mixin/user.ts index 7cb3ef3..44d49f8 100644 --- a/src/@shared/src/database/mixin/user.ts +++ b/src/@shared/src/database/mixin/user.ts @@ -146,7 +146,7 @@ async function hashPassword( * * @returns The user if it exists, undefined otherwise */ -function userFromRow(row?: Partial): User | undefined { +export function userFromRow(row?: Partial): User | undefined { if (isNullish(row)) return undefined; if (isNullish(row.id)) return undefined; if (isNullish(row.name)) return undefined;