yes
This commit is contained in:
parent
00e4f522ab
commit
37a33d8a73
24 changed files with 1233 additions and 202 deletions
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -330,6 +330,20 @@
|
|||
"/api/auth/guest": {
|
||||
"post": {
|
||||
"operationId": "guestLogin",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Default Response",
|
||||
|
|
@ -370,6 +384,32 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Default Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"msg"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"failed"
|
||||
]
|
||||
},
|
||||
"msg": {
|
||||
"enum": [
|
||||
"guestLogin.failed.invalid"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Default Response",
|
||||
"content": {
|
||||
|
|
|
|||
|
|
@ -1,39 +1,95 @@
|
|||
import { FastifyPluginAsync } from 'fastify';
|
||||
|
||||
import { Type } from 'typebox';
|
||||
import { Static, Type } from 'typebox';
|
||||
import { typeResponse, isNullish, MakeStaticResponse } from '@shared/utils';
|
||||
|
||||
export const GuestLoginRes = {
|
||||
'500': typeResponse('failed', ['guestLogin.failed.generic.unknown', 'guestLogin.failed.generic.error']),
|
||||
'500': typeResponse('failed', [
|
||||
'guestLogin.failed.generic.unknown',
|
||||
'guestLogin.failed.generic.error',
|
||||
]),
|
||||
'200': typeResponse('success', 'guestLogin.success', {
|
||||
token: Type.String({
|
||||
description: 'JWT that represent a logged in user',
|
||||
}),
|
||||
}),
|
||||
'400': typeResponse('failed', 'guestLogin.failed.invalid'),
|
||||
};
|
||||
|
||||
export type GuestLoginRes = MakeStaticResponse<typeof GuestLoginRes>;
|
||||
|
||||
export const GuestLoginReq = Type.Object({
|
||||
name: Type.Optional(Type.String()),
|
||||
});
|
||||
|
||||
export type GuestLoginReq = Static<typeof GuestLoginReq>;
|
||||
|
||||
const getRandomFromList = (list: string[]): string => {
|
||||
return list[Math.floor(Math.random() * list.length)];
|
||||
};
|
||||
|
||||
const USERNAME_CHECK: RegExp = /^[a-zA-Z_0-9]+$/;
|
||||
|
||||
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
||||
void _opts;
|
||||
fastify.post<{ Body: null, Reply: GuestLoginRes }>(
|
||||
fastify.post<{ Body: GuestLoginReq; Reply: GuestLoginRes }>(
|
||||
'/api/auth/guest',
|
||||
{ schema: { response: GuestLoginRes, operationId: 'guestLogin' } },
|
||||
{
|
||||
schema: {
|
||||
body: GuestLoginReq,
|
||||
response: GuestLoginRes,
|
||||
operationId: 'guestLogin',
|
||||
},
|
||||
},
|
||||
async function(req, res) {
|
||||
void req;
|
||||
void res;
|
||||
try {
|
||||
console.log('DEBUG ----- guest login backend');
|
||||
const adjective = getRandomFromList(fastify.words.adjectives);
|
||||
const noun = getRandomFromList(fastify.words.nouns);
|
||||
let user_name: string | undefined = req.body?.name;
|
||||
if (isNullish(user_name)) {
|
||||
const adjective = getRandomFromList(
|
||||
fastify.words.adjectives,
|
||||
);
|
||||
const noun = getRandomFromList(fastify.words.nouns);
|
||||
user_name = `${adjective}${noun}`;
|
||||
}
|
||||
else {
|
||||
if (user_name.length < 4 || user_name.length > 26) {
|
||||
return res.makeResponse(
|
||||
400,
|
||||
'failed',
|
||||
'guestLogin.failed.invalid',
|
||||
);
|
||||
}
|
||||
if (!USERNAME_CHECK.test(user_name)) {
|
||||
return res.makeResponse(
|
||||
400,
|
||||
'failed',
|
||||
'guestLogin.failed.invalid',
|
||||
);
|
||||
}
|
||||
user_name = `g_${user_name}`;
|
||||
}
|
||||
|
||||
const user = await this.db.createGuestUser(`${adjective} ${noun}`);
|
||||
const orig = user_name;
|
||||
let i = 0;
|
||||
while (
|
||||
this.db.getUserFromDisplayName(user_name) !== undefined &&
|
||||
i++ < 5
|
||||
) {
|
||||
user_name = `${orig}${Date.now() % 1000}`;
|
||||
}
|
||||
if (this.db.getUserFromDisplayName(user_name) !== undefined) {
|
||||
user_name = `${orig}${Date.now()}`;
|
||||
}
|
||||
|
||||
const user = await this.db.createGuestUser(user_name);
|
||||
if (isNullish(user)) {
|
||||
return res.makeResponse(500, 'failed', 'guestLogin.failed.generic.unknown');
|
||||
return res.makeResponse(
|
||||
500,
|
||||
'failed',
|
||||
'guestLogin.failed.generic.unknown',
|
||||
);
|
||||
}
|
||||
return res.makeResponse(200, 'success', 'guestLogin.success', {
|
||||
token: this.signJwt('auth', user.id.toString()),
|
||||
|
|
@ -41,7 +97,11 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
|||
}
|
||||
catch (e: unknown) {
|
||||
fastify.log.error(e);
|
||||
return res.makeResponse(500, 'failed', 'guestLogin.failed.generic.error');
|
||||
return res.makeResponse(
|
||||
500,
|
||||
'failed',
|
||||
'guestLogin.failed.generic.error',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -30,9 +30,23 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
|||
const result = await creq.getCode();
|
||||
|
||||
const userinfo = await provider.getUserInfo(result);
|
||||
|
||||
|
||||
let u = this.db.getOauth2User(provider.display_name, userinfo.unique_id);
|
||||
if (isNullish(u)) {
|
||||
u = await this.db.createOauth2User(userinfo.name, provider.display_name, userinfo.unique_id);
|
||||
let user_name = userinfo.name;
|
||||
const orig = user_name;
|
||||
let i = 0;
|
||||
while (
|
||||
this.db.getUserFromDisplayName(user_name) !== undefined &&
|
||||
i++ < 100
|
||||
) {
|
||||
user_name = `${orig}${Date.now() % 1000}`;
|
||||
}
|
||||
if (this.db.getUserFromDisplayName(user_name) !== undefined) {
|
||||
user_name = `${orig}${Date.now()}`;
|
||||
}
|
||||
u = await this.db.createOauth2User(user_name, provider.display_name, userinfo.unique_id);
|
||||
}
|
||||
if (isNullish(u)) {
|
||||
return res.code(500).send('failed to fetch or create user...');
|
||||
|
|
|
|||
|
|
@ -47,7 +47,19 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
|||
// password is good too !
|
||||
|
||||
if (this.db.getUserFromLoginName(name) !== undefined) { return res.makeResponse(400, 'failed', 'signin.failed.username.existing'); }
|
||||
const u = await this.db.createUser(name, name, password);
|
||||
let user_name = name;
|
||||
const orig = user_name;
|
||||
let i = 0;
|
||||
while (
|
||||
this.db.getUserFromDisplayName(user_name) !== undefined &&
|
||||
i++ < 100
|
||||
) {
|
||||
user_name = `${orig}${Date.now() % 1000}`;
|
||||
}
|
||||
if (this.db.getUserFromDisplayName(user_name) !== undefined) {
|
||||
user_name = `${orig}${Date.now()}`;
|
||||
}
|
||||
const u = await this.db.createUser(name, user_name, password);
|
||||
if (isNullish(u)) { return res.makeResponse(500, 'failed', 'signin.failed.generic'); }
|
||||
|
||||
// every check has been passed, they are now logged in, using this token to say who they are...
|
||||
|
|
|
|||
151
src/openapi.json
151
src/openapi.json
|
|
@ -352,6 +352,20 @@
|
|||
"/api/auth/guest": {
|
||||
"post": {
|
||||
"operationId": "guestLogin",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Default Response",
|
||||
|
|
@ -392,6 +406,32 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Default Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"msg"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"failed"
|
||||
]
|
||||
},
|
||||
"msg": {
|
||||
"enum": [
|
||||
"guestLogin.failed.invalid"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Default Response",
|
||||
"content": {
|
||||
|
|
@ -1057,6 +1097,117 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/api/user/changeDisplayName": {
|
||||
"put": {
|
||||
"operationId": "changeDisplayName",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "New Display Name"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Default Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"msg"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"success"
|
||||
]
|
||||
},
|
||||
"msg": {
|
||||
"enum": [
|
||||
"changeDisplayName.success"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Default Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"msg"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"failure"
|
||||
]
|
||||
},
|
||||
"msg": {
|
||||
"enum": [
|
||||
"changeDisplayName.alreadyExist",
|
||||
"changeDisplayName.invalid"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Default Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"msg"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"notLoggedIn"
|
||||
]
|
||||
},
|
||||
"msg": {
|
||||
"enum": [
|
||||
"auth.noCookie",
|
||||
"auth.invalidKind",
|
||||
"auth.noUser",
|
||||
"auth.invalid"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"openapi_other"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/user/info/{user}": {
|
||||
"get": {
|
||||
"operationId": "getUser",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,114 @@
|
|||
"schemas": {}
|
||||
},
|
||||
"paths": {
|
||||
"/api/user/changeDisplayName": {
|
||||
"put": {
|
||||
"operationId": "changeDisplayName",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "New Display Name"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Default Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"msg"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"success"
|
||||
]
|
||||
},
|
||||
"msg": {
|
||||
"enum": [
|
||||
"changeDisplayName.success"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Default Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"msg"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"failure"
|
||||
]
|
||||
},
|
||||
"msg": {
|
||||
"enum": [
|
||||
"changeDisplayName.alreadyExist",
|
||||
"changeDisplayName.invalid"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Default Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"msg"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"notLoggedIn"
|
||||
]
|
||||
},
|
||||
"msg": {
|
||||
"enum": [
|
||||
"auth.noCookie",
|
||||
"auth.invalidKind",
|
||||
"auth.noUser",
|
||||
"auth.invalid"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/user/info/{user}": {
|
||||
"get": {
|
||||
"operationId": "getUser",
|
||||
|
|
|
|||
44
src/user/src/routes/changeDisplayName.ts
Normal file
44
src/user/src/routes/changeDisplayName.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { FastifyPluginAsync } from 'fastify';
|
||||
|
||||
import { Static, Type } from 'typebox';
|
||||
import { isNullish, MakeStaticResponse, typeResponse } from '@shared/utils';
|
||||
|
||||
|
||||
export const ChangeDisplayNameRes = {
|
||||
'200': typeResponse('success', 'changeDisplayName.success'),
|
||||
'400': typeResponse('failure', ['changeDisplayName.alreadyExist', 'changeDisplayName.invalid']),
|
||||
};
|
||||
|
||||
export type ChangeDisplayNameRes = MakeStaticResponse<typeof ChangeDisplayNameRes>;
|
||||
|
||||
export const ChangeDisplayNameReq = Type.Object({ name: Type.String({ description: 'New Display Name' }) });
|
||||
type ChangeDisplayNameReq = Static<typeof ChangeDisplayNameReq>;
|
||||
|
||||
const USERNAME_CHECK: RegExp = /^[a-zA-Z_0-9]+$/;
|
||||
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
||||
void _opts;
|
||||
fastify.put<{ Body: ChangeDisplayNameReq }>(
|
||||
'/api/user/changeDisplayName',
|
||||
{ schema: { body: ChangeDisplayNameReq, response: ChangeDisplayNameRes, operationId: 'changeDisplayName' }, config: { requireAuth: true } },
|
||||
async function(req, res) {
|
||||
if (isNullish(req.authUser)) return;
|
||||
if (isNullish(req.body.name)) {
|
||||
return res.makeResponse(400, 'failure', 'changeDisplayName.invalid');
|
||||
}
|
||||
if (req.body.name.length < 4 || req.body.name.length > 32) {
|
||||
return res.makeResponse(400, 'failure', 'changeDisplayName.invalid');
|
||||
}
|
||||
if (!USERNAME_CHECK.test(req.body.name)) {
|
||||
return res.makeResponse(400, 'failure', 'changeDisplayName.invalid');
|
||||
}
|
||||
if (this.db.updateDisplayName(req.authUser.id, req.body.name)) {
|
||||
return res.makeResponse(200, 'success', 'changeDisplayName.success');
|
||||
}
|
||||
else {
|
||||
return res.makeResponse(400, 'failure', 'changeDisplayName.alreadyExist');
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export default route;
|
||||
|
|
@ -49,7 +49,7 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
|||
if (isNullish(user)) {
|
||||
return res.makeResponse(404, 'failure', 'userinfo.failure.unknownUser');
|
||||
}
|
||||
|
||||
console.log(user);
|
||||
|
||||
const payload = {
|
||||
name: user.name,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue