From 37a33d8a7367b9c2a75a91d6c12c8801e5d91c67 Mon Sep 17 00:00:00 2001 From: Maieul BOYER Date: Wed, 10 Dec 2025 16:38:26 +0100 Subject: [PATCH] yes --- frontend/Dockerfile | 16 +- .../api/generated/.openapi-generator/FILES | 5 + .../src/api/generated/apis/OpenapiOtherApi.ts | 94 ++++- .../models/ChangeDisplayName200Response.ts | 93 +++++ .../models/ChangeDisplayName400Response.ts | 94 +++++ .../models/ChangeDisplayNameRequest.ts | 66 +++ .../generated/models/GuestLogin400Response.ts | 93 +++++ .../api/generated/models/GuestLoginRequest.ts | 65 +++ frontend/src/api/generated/models/index.ts | 5 + frontend/src/auth/index.ts | 4 +- frontend/src/pages/login/login.ts | 2 +- frontend/src/pages/profile/profile.html | 27 +- frontend/src/pages/profile/profile.ts | 379 ++++++++++-------- src/@shared/src/database/init.dbml | 2 +- src/@shared/src/database/init.sql | 2 +- src/@shared/src/database/mixin/user.ts | 33 +- src/auth/openapi.json | 40 ++ src/auth/src/routes/guestLogin.ts | 80 +++- src/auth/src/routes/oauth2/callback.ts | 16 +- src/auth/src/routes/signin.ts | 14 +- src/openapi.json | 151 +++++++ src/user/openapi.json | 108 +++++ src/user/src/routes/changeDisplayName.ts | 44 ++ src/user/src/routes/info.ts | 2 +- 24 files changed, 1233 insertions(+), 202 deletions(-) create mode 100644 frontend/src/api/generated/models/ChangeDisplayName200Response.ts create mode 100644 frontend/src/api/generated/models/ChangeDisplayName400Response.ts create mode 100644 frontend/src/api/generated/models/ChangeDisplayNameRequest.ts create mode 100644 frontend/src/api/generated/models/GuestLogin400Response.ts create mode 100644 frontend/src/api/generated/models/GuestLoginRequest.ts create mode 100644 src/user/src/routes/changeDisplayName.ts diff --git a/frontend/Dockerfile b/frontend/Dockerfile index d772c9e..f44fde2 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,15 +1,23 @@ FROM node:22-alpine AS pnpm_base RUN npm install --global pnpm@10; +FROM pnpm_base AS deps + +COPY ./package.json ./pnpm-lock.yaml ./pnpm-workspace.yaml /src/ +WORKDIR /src +RUN pnpm install --frozen-lockfile; + FROM pnpm_base AS builder -COPY . /src WORKDIR /src -RUN pnpm install --frozen-lockfile && pnpm run build; +COPY --from=deps /src/node_modules /src/node_modules +COPY . /src -FROM node:22-alpine +RUN pnpm run build; -COPY --from=builder /src/dist /dist +FROM pnpm_base + +COPY --from=builder /src/dist /dist COPY ./run.sh /bin/run.sh RUN chmod +x /bin/run.sh diff --git a/frontend/src/api/generated/.openapi-generator/FILES b/frontend/src/api/generated/.openapi-generator/FILES index b05ee98..ed23b0c 100644 --- a/frontend/src/api/generated/.openapi-generator/FILES +++ b/frontend/src/api/generated/.openapi-generator/FILES @@ -1,6 +1,9 @@ apis/OpenapiOtherApi.ts apis/index.ts index.ts +models/ChangeDisplayName200Response.ts +models/ChangeDisplayName400Response.ts +models/ChangeDisplayNameRequest.ts models/ChatTest200Response.ts models/ChatTest200ResponsePayload.ts models/DisableOtp200Response.ts @@ -20,7 +23,9 @@ models/GetUser404Response.ts models/GetUserUserParameter.ts models/GuestLogin200Response.ts models/GuestLogin200ResponsePayload.ts +models/GuestLogin400Response.ts models/GuestLogin500Response.ts +models/GuestLoginRequest.ts models/Login200Response.ts models/Login202Response.ts models/Login202ResponsePayload.ts diff --git a/frontend/src/api/generated/apis/OpenapiOtherApi.ts b/frontend/src/api/generated/apis/OpenapiOtherApi.ts index c37580a..9609421 100644 --- a/frontend/src/api/generated/apis/OpenapiOtherApi.ts +++ b/frontend/src/api/generated/apis/OpenapiOtherApi.ts @@ -15,6 +15,9 @@ import * as runtime from '../runtime'; import type { + ChangeDisplayName200Response, + ChangeDisplayName400Response, + ChangeDisplayNameRequest, ChatTest200Response, DisableOtp200Response, DisableOtp400Response, @@ -28,7 +31,9 @@ import type { GetUser404Response, GetUserUserParameter, GuestLogin200Response, + GuestLogin400Response, GuestLogin500Response, + GuestLoginRequest, Login200Response, Login202Response, Login400Response, @@ -49,6 +54,12 @@ import type { StatusOtp500Response, } from '../models/index'; import { + ChangeDisplayName200ResponseFromJSON, + ChangeDisplayName200ResponseToJSON, + ChangeDisplayName400ResponseFromJSON, + ChangeDisplayName400ResponseToJSON, + ChangeDisplayNameRequestFromJSON, + ChangeDisplayNameRequestToJSON, ChatTest200ResponseFromJSON, ChatTest200ResponseToJSON, DisableOtp200ResponseFromJSON, @@ -75,8 +86,12 @@ import { GetUserUserParameterToJSON, GuestLogin200ResponseFromJSON, GuestLogin200ResponseToJSON, + GuestLogin400ResponseFromJSON, + GuestLogin400ResponseToJSON, GuestLogin500ResponseFromJSON, GuestLogin500ResponseToJSON, + GuestLoginRequestFromJSON, + GuestLoginRequestToJSON, Login200ResponseFromJSON, Login200ResponseToJSON, Login202ResponseFromJSON, @@ -115,10 +130,18 @@ import { StatusOtp500ResponseToJSON, } from '../models/index'; +export interface ChangeDisplayNameOperationRequest { + changeDisplayNameRequest: ChangeDisplayNameRequest; +} + export interface GetUserRequest { user: GetUserUserParameter; } +export interface GuestLoginOperationRequest { + guestLoginRequest?: GuestLoginRequest; +} + export interface LoginOperationRequest { loginRequest: LoginRequest; } @@ -136,6 +159,62 @@ export interface SigninRequest { */ export class OpenapiOtherApi extends runtime.BaseAPI { + /** + */ + async changeDisplayNameRaw(requestParameters: ChangeDisplayNameOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['changeDisplayNameRequest'] == null) { + throw new runtime.RequiredError( + 'changeDisplayNameRequest', + 'Required parameter "changeDisplayNameRequest" was null or undefined when calling changeDisplayName().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + + let urlPath = `/api/user/changeDisplayName`; + + const response = await this.request({ + path: urlPath, + method: 'PUT', + headers: headerParameters, + query: queryParameters, + body: ChangeDisplayNameRequestToJSON(requestParameters['changeDisplayNameRequest']), + }, initOverrides); + + // CHANGED: Handle all status codes defined in the OpenAPI spec, not just 2xx responses + // This allows typed access to error responses (4xx, 5xx) and other status codes. + // The code routes responses based on the actual HTTP status code and returns + // appropriately typed ApiResponse wrappers for each status code. + if (response.status === 200) { + // Object response for status 200 + return new runtime.JSONApiResponse(response, (jsonValue) => ChangeDisplayName200ResponseFromJSON(jsonValue)); + } + if (response.status === 400) { + // Object response for status 400 + return new runtime.JSONApiResponse(response, (jsonValue) => ChangeDisplayName400ResponseFromJSON(jsonValue)); + } + if (response.status === 401) { + // Object response for status 401 + return new runtime.JSONApiResponse(response, (jsonValue) => DisableOtp401ResponseFromJSON(jsonValue)); + } + // CHANGED: Throw error if status code is not handled by any of the defined responses + // This ensures all code paths return a value and provides clear error messages for unexpected status codes + // Only throw if responses were defined but none matched the actual status code + throw new runtime.ResponseError(response, `Unexpected status code: ${response.status}. Expected one of: 200, 400, 401`); + } + + /** + */ + async changeDisplayName(requestParameters: ChangeDisplayNameOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.changeDisplayNameRaw(requestParameters, initOverrides); + return await response.value(); + } + /** */ async chatTestRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { @@ -334,11 +413,13 @@ export class OpenapiOtherApi extends runtime.BaseAPI { /** */ - async guestLoginRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + async guestLoginRaw(requestParameters: GuestLoginOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { const queryParameters: any = {}; const headerParameters: runtime.HTTPHeaders = {}; + headerParameters['Content-Type'] = 'application/json'; + let urlPath = `/api/auth/guest`; @@ -347,6 +428,7 @@ export class OpenapiOtherApi extends runtime.BaseAPI { method: 'POST', headers: headerParameters, query: queryParameters, + body: GuestLoginRequestToJSON(requestParameters['guestLoginRequest']), }, initOverrides); // CHANGED: Handle all status codes defined in the OpenAPI spec, not just 2xx responses @@ -357,6 +439,10 @@ export class OpenapiOtherApi extends runtime.BaseAPI { // Object response for status 200 return new runtime.JSONApiResponse(response, (jsonValue) => GuestLogin200ResponseFromJSON(jsonValue)); } + if (response.status === 400) { + // Object response for status 400 + return new runtime.JSONApiResponse(response, (jsonValue) => GuestLogin400ResponseFromJSON(jsonValue)); + } if (response.status === 500) { // Object response for status 500 return new runtime.JSONApiResponse(response, (jsonValue) => GuestLogin500ResponseFromJSON(jsonValue)); @@ -364,13 +450,13 @@ export class OpenapiOtherApi extends runtime.BaseAPI { // CHANGED: Throw error if status code is not handled by any of the defined responses // This ensures all code paths return a value and provides clear error messages for unexpected status codes // Only throw if responses were defined but none matched the actual status code - throw new runtime.ResponseError(response, `Unexpected status code: ${response.status}. Expected one of: 200, 500`); + throw new runtime.ResponseError(response, `Unexpected status code: ${response.status}. Expected one of: 200, 400, 500`); } /** */ - async guestLogin(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { - const response = await this.guestLoginRaw(initOverrides); + async guestLogin(requestParameters: GuestLoginOperationRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.guestLoginRaw(requestParameters, initOverrides); return await response.value(); } diff --git a/frontend/src/api/generated/models/ChangeDisplayName200Response.ts b/frontend/src/api/generated/models/ChangeDisplayName200Response.ts new file mode 100644 index 0000000..63168b7 --- /dev/null +++ b/frontend/src/api/generated/models/ChangeDisplayName200Response.ts @@ -0,0 +1,93 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * @fastify/swagger + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 9.6.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface ChangeDisplayName200Response + */ +export interface ChangeDisplayName200Response { + /** + * + * @type {string} + * @memberof ChangeDisplayName200Response + */ + kind: ChangeDisplayName200ResponseKindEnum; + /** + * + * @type {string} + * @memberof ChangeDisplayName200Response + */ + msg: ChangeDisplayName200ResponseMsgEnum; +} + + +/** + * @export + */ +export const ChangeDisplayName200ResponseKindEnum = { + Success: 'success' +} as const; +export type ChangeDisplayName200ResponseKindEnum = typeof ChangeDisplayName200ResponseKindEnum[keyof typeof ChangeDisplayName200ResponseKindEnum]; + +/** + * @export + */ +export const ChangeDisplayName200ResponseMsgEnum = { + ChangeDisplayNameSuccess: 'changeDisplayName.success' +} as const; +export type ChangeDisplayName200ResponseMsgEnum = typeof ChangeDisplayName200ResponseMsgEnum[keyof typeof ChangeDisplayName200ResponseMsgEnum]; + + +/** + * Check if a given object implements the ChangeDisplayName200Response interface. + */ +export function instanceOfChangeDisplayName200Response(value: object): value is ChangeDisplayName200Response { + if (!('kind' in value) || value['kind'] === undefined) return false; + if (!('msg' in value) || value['msg'] === undefined) return false; + return true; +} + +export function ChangeDisplayName200ResponseFromJSON(json: any): ChangeDisplayName200Response { + return ChangeDisplayName200ResponseFromJSONTyped(json, false); +} + +export function ChangeDisplayName200ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): ChangeDisplayName200Response { + if (json == null) { + return json; + } + return { + + 'kind': json['kind'], + 'msg': json['msg'], + }; +} + +export function ChangeDisplayName200ResponseToJSON(json: any): ChangeDisplayName200Response { + return ChangeDisplayName200ResponseToJSONTyped(json, false); +} + +export function ChangeDisplayName200ResponseToJSONTyped(value?: ChangeDisplayName200Response | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'kind': value['kind'], + 'msg': value['msg'], + }; +} + diff --git a/frontend/src/api/generated/models/ChangeDisplayName400Response.ts b/frontend/src/api/generated/models/ChangeDisplayName400Response.ts new file mode 100644 index 0000000..1487962 --- /dev/null +++ b/frontend/src/api/generated/models/ChangeDisplayName400Response.ts @@ -0,0 +1,94 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * @fastify/swagger + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 9.6.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface ChangeDisplayName400Response + */ +export interface ChangeDisplayName400Response { + /** + * + * @type {string} + * @memberof ChangeDisplayName400Response + */ + kind: ChangeDisplayName400ResponseKindEnum; + /** + * + * @type {string} + * @memberof ChangeDisplayName400Response + */ + msg: ChangeDisplayName400ResponseMsgEnum; +} + + +/** + * @export + */ +export const ChangeDisplayName400ResponseKindEnum = { + Failure: 'failure' +} as const; +export type ChangeDisplayName400ResponseKindEnum = typeof ChangeDisplayName400ResponseKindEnum[keyof typeof ChangeDisplayName400ResponseKindEnum]; + +/** + * @export + */ +export const ChangeDisplayName400ResponseMsgEnum = { + ChangeDisplayNameAlreadyExist: 'changeDisplayName.alreadyExist', + ChangeDisplayNameInvalid: 'changeDisplayName.invalid' +} as const; +export type ChangeDisplayName400ResponseMsgEnum = typeof ChangeDisplayName400ResponseMsgEnum[keyof typeof ChangeDisplayName400ResponseMsgEnum]; + + +/** + * Check if a given object implements the ChangeDisplayName400Response interface. + */ +export function instanceOfChangeDisplayName400Response(value: object): value is ChangeDisplayName400Response { + if (!('kind' in value) || value['kind'] === undefined) return false; + if (!('msg' in value) || value['msg'] === undefined) return false; + return true; +} + +export function ChangeDisplayName400ResponseFromJSON(json: any): ChangeDisplayName400Response { + return ChangeDisplayName400ResponseFromJSONTyped(json, false); +} + +export function ChangeDisplayName400ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): ChangeDisplayName400Response { + if (json == null) { + return json; + } + return { + + 'kind': json['kind'], + 'msg': json['msg'], + }; +} + +export function ChangeDisplayName400ResponseToJSON(json: any): ChangeDisplayName400Response { + return ChangeDisplayName400ResponseToJSONTyped(json, false); +} + +export function ChangeDisplayName400ResponseToJSONTyped(value?: ChangeDisplayName400Response | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'kind': value['kind'], + 'msg': value['msg'], + }; +} + diff --git a/frontend/src/api/generated/models/ChangeDisplayNameRequest.ts b/frontend/src/api/generated/models/ChangeDisplayNameRequest.ts new file mode 100644 index 0000000..c72b7bb --- /dev/null +++ b/frontend/src/api/generated/models/ChangeDisplayNameRequest.ts @@ -0,0 +1,66 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * @fastify/swagger + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 9.6.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface ChangeDisplayNameRequest + */ +export interface ChangeDisplayNameRequest { + /** + * New Display Name + * @type {string} + * @memberof ChangeDisplayNameRequest + */ + name: string; +} + +/** + * Check if a given object implements the ChangeDisplayNameRequest interface. + */ +export function instanceOfChangeDisplayNameRequest(value: object): value is ChangeDisplayNameRequest { + if (!('name' in value) || value['name'] === undefined) return false; + return true; +} + +export function ChangeDisplayNameRequestFromJSON(json: any): ChangeDisplayNameRequest { + return ChangeDisplayNameRequestFromJSONTyped(json, false); +} + +export function ChangeDisplayNameRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): ChangeDisplayNameRequest { + if (json == null) { + return json; + } + return { + + 'name': json['name'], + }; +} + +export function ChangeDisplayNameRequestToJSON(json: any): ChangeDisplayNameRequest { + return ChangeDisplayNameRequestToJSONTyped(json, false); +} + +export function ChangeDisplayNameRequestToJSONTyped(value?: ChangeDisplayNameRequest | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'name': value['name'], + }; +} + diff --git a/frontend/src/api/generated/models/GuestLogin400Response.ts b/frontend/src/api/generated/models/GuestLogin400Response.ts new file mode 100644 index 0000000..8f4c1ce --- /dev/null +++ b/frontend/src/api/generated/models/GuestLogin400Response.ts @@ -0,0 +1,93 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * @fastify/swagger + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 9.6.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface GuestLogin400Response + */ +export interface GuestLogin400Response { + /** + * + * @type {string} + * @memberof GuestLogin400Response + */ + kind: GuestLogin400ResponseKindEnum; + /** + * + * @type {string} + * @memberof GuestLogin400Response + */ + msg: GuestLogin400ResponseMsgEnum; +} + + +/** + * @export + */ +export const GuestLogin400ResponseKindEnum = { + Failed: 'failed' +} as const; +export type GuestLogin400ResponseKindEnum = typeof GuestLogin400ResponseKindEnum[keyof typeof GuestLogin400ResponseKindEnum]; + +/** + * @export + */ +export const GuestLogin400ResponseMsgEnum = { + GuestLoginFailedInvalid: 'guestLogin.failed.invalid' +} as const; +export type GuestLogin400ResponseMsgEnum = typeof GuestLogin400ResponseMsgEnum[keyof typeof GuestLogin400ResponseMsgEnum]; + + +/** + * Check if a given object implements the GuestLogin400Response interface. + */ +export function instanceOfGuestLogin400Response(value: object): value is GuestLogin400Response { + if (!('kind' in value) || value['kind'] === undefined) return false; + if (!('msg' in value) || value['msg'] === undefined) return false; + return true; +} + +export function GuestLogin400ResponseFromJSON(json: any): GuestLogin400Response { + return GuestLogin400ResponseFromJSONTyped(json, false); +} + +export function GuestLogin400ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): GuestLogin400Response { + if (json == null) { + return json; + } + return { + + 'kind': json['kind'], + 'msg': json['msg'], + }; +} + +export function GuestLogin400ResponseToJSON(json: any): GuestLogin400Response { + return GuestLogin400ResponseToJSONTyped(json, false); +} + +export function GuestLogin400ResponseToJSONTyped(value?: GuestLogin400Response | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'kind': value['kind'], + 'msg': value['msg'], + }; +} + diff --git a/frontend/src/api/generated/models/GuestLoginRequest.ts b/frontend/src/api/generated/models/GuestLoginRequest.ts new file mode 100644 index 0000000..15aa94b --- /dev/null +++ b/frontend/src/api/generated/models/GuestLoginRequest.ts @@ -0,0 +1,65 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * @fastify/swagger + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 9.6.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface GuestLoginRequest + */ +export interface GuestLoginRequest { + /** + * + * @type {string} + * @memberof GuestLoginRequest + */ + name?: string; +} + +/** + * Check if a given object implements the GuestLoginRequest interface. + */ +export function instanceOfGuestLoginRequest(value: object): value is GuestLoginRequest { + return true; +} + +export function GuestLoginRequestFromJSON(json: any): GuestLoginRequest { + return GuestLoginRequestFromJSONTyped(json, false); +} + +export function GuestLoginRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): GuestLoginRequest { + if (json == null) { + return json; + } + return { + + 'name': json['name'] == null ? undefined : json['name'], + }; +} + +export function GuestLoginRequestToJSON(json: any): GuestLoginRequest { + return GuestLoginRequestToJSONTyped(json, false); +} + +export function GuestLoginRequestToJSONTyped(value?: GuestLoginRequest | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'name': value['name'], + }; +} + diff --git a/frontend/src/api/generated/models/index.ts b/frontend/src/api/generated/models/index.ts index a3cdb3a..f00b24e 100644 --- a/frontend/src/api/generated/models/index.ts +++ b/frontend/src/api/generated/models/index.ts @@ -1,5 +1,8 @@ /* tslint:disable */ /* eslint-disable */ +export * from './ChangeDisplayName200Response'; +export * from './ChangeDisplayName400Response'; +export * from './ChangeDisplayNameRequest'; export * from './ChatTest200Response'; export * from './ChatTest200ResponsePayload'; export * from './DisableOtp200Response'; @@ -19,7 +22,9 @@ export * from './GetUser404Response'; export * from './GetUserUserParameter'; export * from './GuestLogin200Response'; export * from './GuestLogin200ResponsePayload'; +export * from './GuestLogin400Response'; export * from './GuestLogin500Response'; +export * from './GuestLoginRequest'; export * from './Login200Response'; export * from './Login202Response'; export * from './Login202ResponsePayload'; diff --git a/frontend/src/auth/index.ts b/frontend/src/auth/index.ts index 7807bf9..449dabc 100644 --- a/frontend/src/auth/index.ts +++ b/frontend/src/auth/index.ts @@ -7,8 +7,8 @@ export type User = { name: string; selfInfo?: { loginName?: string; - provider_id?: string; - provider_user?: string; + providerId?: string; + providerUser?: string; } }; diff --git a/frontend/src/pages/login/login.ts b/frontend/src/pages/login/login.ts index 2cd0a14..5eb5123 100644 --- a/frontend/src/pages/login/login.ts +++ b/frontend/src/pages/login/login.ts @@ -209,7 +209,7 @@ async function handleLogin( document.querySelector("#bGuestLogin"); bLoginAsGuest?.addEventListener("click", async () => { try { - const res = await client.guestLogin(); + const res = await client.guestLogin({ guestLoginRequest: { name: undefined } }); switch (res.kind) { case "success": { Cookie.set("token", res.payload.token, { diff --git a/frontend/src/pages/profile/profile.html b/frontend/src/pages/profile/profile.html index 1b2d180..6752e23 100644 --- a/frontend/src/pages/profile/profile.html +++ b/frontend/src/pages/profile/profile.html @@ -7,12 +7,33 @@ You can't change anything here +
+ + +
+ + -
+ - + +
@@ -35,7 +56,7 @@
-
+

Two-Factor Authentication (TOTP)

diff --git a/frontend/src/pages/profile/profile.ts b/frontend/src/pages/profile/profile.ts index c6a7bb0..39171ce 100644 --- a/frontend/src/pages/profile/profile.ts +++ b/frontend/src/pages/profile/profile.ts @@ -1,5 +1,5 @@ -import { addRoute, navigateTo, setTitle } from "@app/routing"; -import { showError } from "@app/toast"; +import { addRoute, handleRoute, navigateTo, setTitle } from "@app/routing"; +import { showError, showSuccess } from "@app/toast"; import page from "./profile.html?raw"; import { updateUser } from "@app/auth"; import { isNullish } from "@app/utils"; @@ -38,176 +38,219 @@ export async function renderOAuth2QRCode( }); canvas.style.width = ""; canvas.style.height = ""; -} - -async function route(url: string, _args: { [k: string]: string }) { - setTitle("Edit Profile"); - return { - html: page, - postInsert: async (app: HTMLElement | undefined) => { - const user = await updateUser(); - if (isNullish(user)) return showError("No User"); - if (isNullish(app)) return showError("Failed to render"); - let totpState = await (async () => { - let res = await client.statusOtp(); - if (res.kind === "success") - return { - enabled: - (res.msg as string) === "statusOtp.success.enabled", - secret: - (res.msg as string) === "statusOtp.success.enabled" - ? res.payload.secret - : null, - }; - else { - showError("Failed to get OTP status"); - return { - enabled: false, - secret: null, - }; - } - })(); - // ---- Simulated State ---- - let totpEnabled = totpState.enabled; - let totpSecret = totpState.secret; // would come from backend - - let guestBox = app.querySelector("#isGuestBox")!; - let displayNameWrapper = app.querySelector( - "#displayNameWrapper", - )!; - let displayNameBox = - app.querySelector("#displayNameBox")!; - let displayNameButton = - app.querySelector("#displayNameButton")!; - let loginNameWrapper = - app.querySelector("#loginNameWrapper")!; - let loginNameBox = - app.querySelector("#loginNameBox")!; - let passwordWrapper = - app.querySelector("#passwordWrapper")!; - let passwordBox = - app.querySelector("#passwordBox")!; - let passwordButton = - app.querySelector("#passwordButton")!; - - if (!isNullish(user.selfInfo?.loginName)) - loginNameBox.innerText = user.selfInfo?.loginName; - else - loginNameBox.innerHTML = - 'You don\'t have a login name'; - displayNameBox.value = user.name; - - guestBox.hidden = !user.guest; - - // ---- DOM Elements ---- - const totpStatusText = app.querySelector("#totpStatusText")!; - const enableBtn = - app.querySelector("#enableTotp")!; - const disableBtn = - app.querySelector("#disableTotp")!; - const showSecretBtn = - app.querySelector("#showSecret")!; - const secretBox = app.querySelector("#totpSecretBox")!; - const secretText = - app.querySelector("#totpSecretText")!; - const secretCanvas = - app.querySelector("#totpSecretCanvas")!; - - if (user.guest) { - for (let c of passwordButton.classList.values()) { - if (c.startsWith("bg-") || c.startsWith("hover:bg-")) - passwordButton.classList.remove(c); - } - passwordButton.disabled = true; - passwordButton.classList.add( - "bg-gray-700", - "hover:bg-gray-700", - ); - - passwordBox.disabled = true; - passwordBox.classList.add("color-white"); - - for (let c of displayNameButton.classList.values()) { - if (c.startsWith("bg-") || c.startsWith("hover:bg-")) - displayNameButton.classList.remove(c); - } - displayNameButton.disabled = true; - displayNameButton.classList.add("bg-gray-700"); - displayNameButton.classList.add("color-white"); - - displayNameBox.disabled = true; - displayNameBox.classList.add("color-white"); - - for (let c of enableBtn.classList.values()) { - if (c.startsWith("bg-") || c.startsWith("hover:bg-")) - enableBtn.classList.remove(c); - } - for (let c of disableBtn.classList.values()) { - if (c.startsWith("bg-") || c.startsWith("hover:bg-")) - disableBtn.classList.remove(c); - } - for (let c of showSecretBtn.classList.values()) { - if (c.startsWith("bg-") || c.startsWith("hover:bg-")) - showSecretBtn.classList.remove(c); - } - enableBtn.classList.add("bg-gray-700", "hover:bg-gray-700"); - disableBtn.classList.add("bg-gray-700", "hover:bg-gray-700"); - showSecretBtn.classList.add("bg-gray-700", "hover:bg-gray-700"); - - enableBtn.disabled = true; - disableBtn.disabled = true; - showSecretBtn.disabled = true; + function removeBgColor(...elem: HTMLElement[]) { + for (let e of elem) { + for (let c of e.classList.values()) { + if (c.startsWith("bg-") || c.startsWith("hover:bg-")) + e.classList.remove(c); } + } + } - // ---- Update UI ---- - function refreshTotpUI() { - if (totpEnabled) { - totpStatusText.textContent = "Status: Enabled"; + async function route(url: string, _args: { [k: string]: string }) { + setTitle("Edit Profile"); + return { + html: page, + postInsert: async (app: HTMLElement | undefined) => { + const user = await updateUser(); + if (isNullish(user)) return showError("No User"); + if (isNullish(app)) return showError("Failed to render"); + let totpState = await (async () => { + let res = await client.statusOtp(); + if (res.kind === "success") + return { + enabled: + (res.msg as string) === "statusOtp.success.enabled", + secret: + (res.msg as string) === "statusOtp.success.enabled" + ? res.payload.secret + : null, + }; + else { + showError("Failed to get OTP status"); + return { + enabled: false, + secret: null, + }; + } + })(); + // ---- Simulated State ---- + let totpEnabled = totpState.enabled; + let totpSecret = totpState.secret; // would come from backend - enableBtn.classList.add("hidden"); - disableBtn.classList.remove("hidden"); - showSecretBtn.classList.remove("hidden"); - } else { - totpStatusText.textContent = "Status: Disabled"; + let guestBox = app.querySelector("#isGuestBox")!; + let displayNameWrapper = app.querySelector( + "#displayNameWrapper", + )!; + let displayNameBox = + app.querySelector("#displayNameBox")!; + let displayNameButton = + app.querySelector("#displayNameButton")!; + let loginNameWrapper = + app.querySelector("#loginNameWrapper")!; + let loginNameBox = + app.querySelector("#loginNameBox")!; + let passwordWrapper = + app.querySelector("#passwordWrapper")!; + let passwordBox = + app.querySelector("#passwordBox")!; + let passwordButton = + app.querySelector("#passwordButton")!; - enableBtn.classList.remove("hidden"); - disableBtn.classList.add("hidden"); - showSecretBtn.classList.add("hidden"); - secretBox.classList.add("hidden"); - } - } + let providerWrapper = + app.querySelector("#providerWrapper")!; + let providerNameBox = + app.querySelector("#providerNameBox")!; + let providerUserBox = + app.querySelector("#providerUserBox")!; - // ---- Button Events ---- - enableBtn.onclick = async () => { - let res = await client.enableOtp(); - if (res.kind === "success") { - navigateTo(url); - } else { - showError(`failed to activate OTP: ${res.msg}`); - } + let accountTypeBox = + app.querySelector("#accountType")!; + displayNameBox.value = user.name; + + guestBox.hidden = !user.guest; + + // ---- DOM Elements ---- + const totpStatusText = app.querySelector("#totpStatusText")!; + const enableBtn = + app.querySelector("#enableTotp")!; + const disableBtn = + app.querySelector("#disableTotp")!; + const showSecretBtn = + app.querySelector("#showSecret")!; + const secretBox = app.querySelector("#totpSecretBox")!; + const secretText = + app.querySelector("#totpSecretText")!; + const secretCanvas = + app.querySelector("#totpSecretCanvas")!; + + if (user.guest) { + for (let c of passwordButton.classList.values()) { + if (c.startsWith("bg-") || c.startsWith("hover:bg-")) + passwordButton.classList.remove(c); + } + let totpWrapper = app.querySelector("#totpWrapper")!; + + if (user.guest) { + removeBgColor( + passwordButton, + displayNameButton, + enableBtn, + disableBtn, + showSecretBtn, + ); + + passwordButton.classList.add( + "bg-gray-700", + "hover:bg-gray-700", + ); + + passwordBox.disabled = true; + passwordBox.classList.add("color-white"); + + displayNameButton.disabled = true; + displayNameButton.classList.add("bg-gray-700", "color-white"); + + displayNameBox.disabled = true; + displayNameBox.classList.add("color-white"); + enableBtn.classList.add("bg-gray-700", "hover:bg-gray-700"); + disableBtn.classList.add("bg-gray-700", "hover:bg-gray-700"); + showSecretBtn.classList.add("bg-gray-700", "hover:bg-gray-700"); + + enableBtn.disabled = true; + disableBtn.disabled = true; + showSecretBtn.disabled = true; + + accountTypeBox.innerText = "Guest"; + } else if (!isNullish(user.selfInfo?.loginName)) { + loginNameWrapper.hidden = false; + loginNameBox.innerText = user.selfInfo.loginName; + + accountTypeBox.innerText = "Normal"; + } else if ( + !isNullish(user.selfInfo?.providerId) && + !isNullish(user.selfInfo?.providerUser) + ) { + providerWrapper.hidden = false; + providerNameBox.innerText = user.selfInfo.providerId; + providerUserBox.innerText = user.selfInfo.providerUser; + + enableBtn.classList.add("bg-gray-700", "hover:bg-gray-700"); + disableBtn.classList.add("bg-gray-700", "hover:bg-gray-700"); + showSecretBtn.classList.add("bg-gray-700", "hover:bg-gray-700"); + + enableBtn.disabled = true; + disableBtn.disabled = true; + showSecretBtn.disabled = true; + + removeBgColor(enableBtn, disableBtn, showSecretBtn); + passwordWrapper.hidden = true; + totpWrapper.hidden = true; + + accountTypeBox.innerText = "Provider"; + } + + // ---- Update UI ---- + function refreshTotpUI() { + if (totpEnabled) { + totpStatusText.textContent = "Status: Enabled"; + + enableBtn.classList.add("hidden"); + disableBtn.classList.remove("hidden"); + showSecretBtn.classList.remove("hidden"); + } else { + totpStatusText.textContent = "Status: Disabled"; + + enableBtn.classList.remove("hidden"); + disableBtn.classList.add("hidden"); + showSecretBtn.classList.add("hidden"); + secretBox.classList.add("hidden"); + } + } + + // ---- Button Events ---- + enableBtn.onclick = async () => { + let res = await client.enableOtp(); + if (res.kind === "success") { + navigateTo(url); + } else { + showError(`failed to activate OTP: ${res.msg}`); + } + }; + + disableBtn.onclick = async () => { + let res = await client.disableOtp(); + if (res.kind === "success") { + navigateTo(url); + } else { + showError(`failed to deactivate OTP: ${res.msg}`); + } + }; + + showSecretBtn.onclick = () => { + if (!isNullish(totpSecret)) { + secretText.textContent = totpSecret; + renderOAuth2QRCode(secretCanvas, totpSecret); + } + secretBox.classList.toggle("hidden"); + }; + + displayNameButton.onclick = async () => { + let req = await client.changeDisplayName({ + changeDisplayNameRequest: { name: displayNameBox.value }, + }); + if (req.kind === "success") { + showSuccess("Successfully changed display name"); + handleRoute(); + } else { + showError(`Failed to update: ${req.msg}`); + } + }; + + // Initialize UI state + refreshTotpUI(); + }, }; + } - disableBtn.onclick = async () => { - let res = await client.disableOtp(); - if (res.kind === "success") { - navigateTo(url); - } else { - showError(`failed to deactivate OTP: ${res.msg}`); - } - }; - - showSecretBtn.onclick = () => { - if (!isNullish(totpSecret)) { - secretText.textContent = totpSecret; - renderOAuth2QRCode(secretCanvas, totpSecret); - } - secretBox.classList.toggle("hidden"); - }; - - // Initialize UI state - refreshTotpUI(); - }, - }; -} - -addRoute("/profile", route); + addRoute("/profile", route); diff --git a/src/@shared/src/database/init.dbml b/src/@shared/src/database/init.dbml index 1a663de..133e482 100644 --- a/src/@shared/src/database/init.dbml +++ b/src/@shared/src/database/init.dbml @@ -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] diff --git a/src/@shared/src/database/init.sql b/src/@shared/src/database/init.sql index b77edfe..bf363fb 100644 --- a/src/@shared/src/database/init.sql +++ b/src/@shared/src/database/init.sql @@ -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, diff --git a/src/@shared/src/database/mixin/user.ts b/src/@shared/src/database/mixin/user.ts index a931aa8..fbdaba1 100644 --- a/src/@shared/src/database/mixin/user.ts +++ b/src/@shared/src/database/mixin/user.ts @@ -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 = { @@ -159,6 +164,24 @@ export const UserImpl: Omit = { const req = this.prepare('SELECT * FROM user WHERE oauth2 = @oauth2').get({ oauth2: `${provider}:${unique}` }) as Partial | 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 & { provider?: string }>): User | undefined { +export function userFromRow(row?: Partial & { 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; +export const GuestLoginReq = Type.Object({ + name: Type.Optional(Type.String()), +}); + +export type GuestLoginReq = Static; + 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 _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 => { } 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', + ); } }, ); diff --git a/src/auth/src/routes/oauth2/callback.ts b/src/auth/src/routes/oauth2/callback.ts index 10bc72a..36ec4a3 100644 --- a/src/auth/src/routes/oauth2/callback.ts +++ b/src/auth/src/routes/oauth2/callback.ts @@ -30,9 +30,23 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise => { 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...'); diff --git a/src/auth/src/routes/signin.ts b/src/auth/src/routes/signin.ts index f8b304a..fe09163 100644 --- a/src/auth/src/routes/signin.ts +++ b/src/auth/src/routes/signin.ts @@ -47,7 +47,19 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise => { // 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... diff --git a/src/openapi.json b/src/openapi.json index 4ab32ff..d9b52b6 100644 --- a/src/openapi.json +++ b/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", diff --git a/src/user/openapi.json b/src/user/openapi.json index 87744dc..8ad61f7 100644 --- a/src/user/openapi.json +++ b/src/user/openapi.json @@ -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", diff --git a/src/user/src/routes/changeDisplayName.ts b/src/user/src/routes/changeDisplayName.ts new file mode 100644 index 0000000..f089c59 --- /dev/null +++ b/src/user/src/routes/changeDisplayName.ts @@ -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; + +export const ChangeDisplayNameReq = Type.Object({ name: Type.String({ description: 'New Display Name' }) }); +type ChangeDisplayNameReq = Static; + +const USERNAME_CHECK: RegExp = /^[a-zA-Z_0-9]+$/; +const route: FastifyPluginAsync = async (fastify, _opts): Promise => { + 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; diff --git a/src/user/src/routes/info.ts b/src/user/src/routes/info.ts index 869835e..784ea23 100644 --- a/src/user/src/routes/info.ts +++ b/src/user/src/routes/info.ts @@ -49,7 +49,7 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise => { if (isNullish(user)) { return res.makeResponse(404, 'failure', 'userinfo.failure.unknownUser'); } - + console.log(user); const payload = { name: user.name,