This commit is contained in:
Maieul BOYER 2025-12-10 16:38:26 +01:00 committed by apetitco
parent 00e4f522ab
commit 37a33d8a73
24 changed files with 1233 additions and 202 deletions

View file

@ -1,13 +1,21 @@
FROM node:22-alpine AS pnpm_base FROM node:22-alpine AS pnpm_base
RUN npm install --global pnpm@10; 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 FROM pnpm_base AS builder
COPY . /src
WORKDIR /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;
FROM pnpm_base
COPY --from=builder /src/dist /dist COPY --from=builder /src/dist /dist
COPY ./run.sh /bin/run.sh COPY ./run.sh /bin/run.sh

View file

@ -1,6 +1,9 @@
apis/OpenapiOtherApi.ts apis/OpenapiOtherApi.ts
apis/index.ts apis/index.ts
index.ts index.ts
models/ChangeDisplayName200Response.ts
models/ChangeDisplayName400Response.ts
models/ChangeDisplayNameRequest.ts
models/ChatTest200Response.ts models/ChatTest200Response.ts
models/ChatTest200ResponsePayload.ts models/ChatTest200ResponsePayload.ts
models/DisableOtp200Response.ts models/DisableOtp200Response.ts
@ -20,7 +23,9 @@ models/GetUser404Response.ts
models/GetUserUserParameter.ts models/GetUserUserParameter.ts
models/GuestLogin200Response.ts models/GuestLogin200Response.ts
models/GuestLogin200ResponsePayload.ts models/GuestLogin200ResponsePayload.ts
models/GuestLogin400Response.ts
models/GuestLogin500Response.ts models/GuestLogin500Response.ts
models/GuestLoginRequest.ts
models/Login200Response.ts models/Login200Response.ts
models/Login202Response.ts models/Login202Response.ts
models/Login202ResponsePayload.ts models/Login202ResponsePayload.ts

View file

@ -15,6 +15,9 @@
import * as runtime from '../runtime'; import * as runtime from '../runtime';
import type { import type {
ChangeDisplayName200Response,
ChangeDisplayName400Response,
ChangeDisplayNameRequest,
ChatTest200Response, ChatTest200Response,
DisableOtp200Response, DisableOtp200Response,
DisableOtp400Response, DisableOtp400Response,
@ -28,7 +31,9 @@ import type {
GetUser404Response, GetUser404Response,
GetUserUserParameter, GetUserUserParameter,
GuestLogin200Response, GuestLogin200Response,
GuestLogin400Response,
GuestLogin500Response, GuestLogin500Response,
GuestLoginRequest,
Login200Response, Login200Response,
Login202Response, Login202Response,
Login400Response, Login400Response,
@ -49,6 +54,12 @@ import type {
StatusOtp500Response, StatusOtp500Response,
} from '../models/index'; } from '../models/index';
import { import {
ChangeDisplayName200ResponseFromJSON,
ChangeDisplayName200ResponseToJSON,
ChangeDisplayName400ResponseFromJSON,
ChangeDisplayName400ResponseToJSON,
ChangeDisplayNameRequestFromJSON,
ChangeDisplayNameRequestToJSON,
ChatTest200ResponseFromJSON, ChatTest200ResponseFromJSON,
ChatTest200ResponseToJSON, ChatTest200ResponseToJSON,
DisableOtp200ResponseFromJSON, DisableOtp200ResponseFromJSON,
@ -75,8 +86,12 @@ import {
GetUserUserParameterToJSON, GetUserUserParameterToJSON,
GuestLogin200ResponseFromJSON, GuestLogin200ResponseFromJSON,
GuestLogin200ResponseToJSON, GuestLogin200ResponseToJSON,
GuestLogin400ResponseFromJSON,
GuestLogin400ResponseToJSON,
GuestLogin500ResponseFromJSON, GuestLogin500ResponseFromJSON,
GuestLogin500ResponseToJSON, GuestLogin500ResponseToJSON,
GuestLoginRequestFromJSON,
GuestLoginRequestToJSON,
Login200ResponseFromJSON, Login200ResponseFromJSON,
Login200ResponseToJSON, Login200ResponseToJSON,
Login202ResponseFromJSON, Login202ResponseFromJSON,
@ -115,10 +130,18 @@ import {
StatusOtp500ResponseToJSON, StatusOtp500ResponseToJSON,
} from '../models/index'; } from '../models/index';
export interface ChangeDisplayNameOperationRequest {
changeDisplayNameRequest: ChangeDisplayNameRequest;
}
export interface GetUserRequest { export interface GetUserRequest {
user: GetUserUserParameter; user: GetUserUserParameter;
} }
export interface GuestLoginOperationRequest {
guestLoginRequest?: GuestLoginRequest;
}
export interface LoginOperationRequest { export interface LoginOperationRequest {
loginRequest: LoginRequest; loginRequest: LoginRequest;
} }
@ -136,6 +159,62 @@ export interface SigninRequest {
*/ */
export class OpenapiOtherApi extends runtime.BaseAPI { export class OpenapiOtherApi extends runtime.BaseAPI {
/**
*/
async changeDisplayNameRaw(requestParameters: ChangeDisplayNameOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<ChangeDisplayName200Response | ChangeDisplayName400Response | DisableOtp401Response>> {
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<ChangeDisplayName200Response | ChangeDisplayName400Response | DisableOtp401Response> {
const response = await this.changeDisplayNameRaw(requestParameters, initOverrides);
return await response.value();
}
/** /**
*/ */
async chatTestRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<ChatTest200Response | StatusOtp401Response>> { async chatTestRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<ChatTest200Response | StatusOtp401Response>> {
@ -334,11 +413,13 @@ export class OpenapiOtherApi extends runtime.BaseAPI {
/** /**
*/ */
async guestLoginRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<GuestLogin200Response | GuestLogin500Response>> { async guestLoginRaw(requestParameters: GuestLoginOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<GuestLogin200Response | GuestLogin400Response | GuestLogin500Response>> {
const queryParameters: any = {}; const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {}; const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
let urlPath = `/api/auth/guest`; let urlPath = `/api/auth/guest`;
@ -347,6 +428,7 @@ export class OpenapiOtherApi extends runtime.BaseAPI {
method: 'POST', method: 'POST',
headers: headerParameters, headers: headerParameters,
query: queryParameters, query: queryParameters,
body: GuestLoginRequestToJSON(requestParameters['guestLoginRequest']),
}, initOverrides); }, initOverrides);
// CHANGED: Handle all status codes defined in the OpenAPI spec, not just 2xx responses // 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 // Object response for status 200
return new runtime.JSONApiResponse(response, (jsonValue) => GuestLogin200ResponseFromJSON(jsonValue)); 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) { if (response.status === 500) {
// Object response for status 500 // Object response for status 500
return new runtime.JSONApiResponse(response, (jsonValue) => GuestLogin500ResponseFromJSON(jsonValue)); 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 // 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 // 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 // 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<GuestLogin200Response | GuestLogin500Response> { async guestLogin(requestParameters: GuestLoginOperationRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<GuestLogin200Response | GuestLogin400Response | GuestLogin500Response> {
const response = await this.guestLoginRaw(initOverrides); const response = await this.guestLoginRaw(requestParameters, initOverrides);
return await response.value(); return await response.value();
} }

View file

@ -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'],
};
}

View file

@ -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'],
};
}

View file

@ -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'],
};
}

View file

@ -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'],
};
}

View file

@ -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'],
};
}

View file

@ -1,5 +1,8 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export * from './ChangeDisplayName200Response';
export * from './ChangeDisplayName400Response';
export * from './ChangeDisplayNameRequest';
export * from './ChatTest200Response'; export * from './ChatTest200Response';
export * from './ChatTest200ResponsePayload'; export * from './ChatTest200ResponsePayload';
export * from './DisableOtp200Response'; export * from './DisableOtp200Response';
@ -19,7 +22,9 @@ export * from './GetUser404Response';
export * from './GetUserUserParameter'; export * from './GetUserUserParameter';
export * from './GuestLogin200Response'; export * from './GuestLogin200Response';
export * from './GuestLogin200ResponsePayload'; export * from './GuestLogin200ResponsePayload';
export * from './GuestLogin400Response';
export * from './GuestLogin500Response'; export * from './GuestLogin500Response';
export * from './GuestLoginRequest';
export * from './Login200Response'; export * from './Login200Response';
export * from './Login202Response'; export * from './Login202Response';
export * from './Login202ResponsePayload'; export * from './Login202ResponsePayload';

View file

@ -7,8 +7,8 @@ export type User = {
name: string; name: string;
selfInfo?: { selfInfo?: {
loginName?: string; loginName?: string;
provider_id?: string; providerId?: string;
provider_user?: string; providerUser?: string;
} }
}; };

View file

@ -209,7 +209,7 @@ async function handleLogin(
document.querySelector<HTMLButtonElement>("#bGuestLogin"); document.querySelector<HTMLButtonElement>("#bGuestLogin");
bLoginAsGuest?.addEventListener("click", async () => { bLoginAsGuest?.addEventListener("click", async () => {
try { try {
const res = await client.guestLogin(); const res = await client.guestLogin({ guestLoginRequest: { name: undefined } });
switch (res.kind) { switch (res.kind) {
case "success": { case "success": {
Cookie.set("token", res.payload.token, { Cookie.set("token", res.payload.token, {

View file

@ -7,12 +7,33 @@
<span class="text-red-600"> You can't change anything here </span> <span class="text-red-600"> You can't change anything here </span>
</div> </div>
<div class="mb-1 text-gray-700 rounded-sm border-2 outline-lime-100">
<label class="inline font-medium mb-1 text-gray-700">AccountType: </label>
<span id="accountType" class="font-medium"></span>
</div>
<!-- Login Name --> <!-- Login Name -->
<div id="loginNameWrapper" class="py-2"> <div id="loginNameWrapper" class="py-2" hidden>
<label class="block font-medium mb-1 text-gray-700">Login Name</label> <label class="block font-medium mb-1 text-gray-700">Login Name</label>
<div id="loginNameBox" class="font-medium mb-1 text-gray-700 rounded-sm border-2 outline-lime-100"></div> <div id="loginNameBox" class="font-medium mb-1 text-gray-700 rounded-sm border-2 outline-lime-100"></div>
</div> </div>
<!-- Login Name -->
<div id="providerWrapper" class="py-2 mb-1 border-2 border-green-400 rounded-sm" hidden>
<span class="py-2 mb-1 text-gray-700 text-lg">Provider</span>
<div class="flex items-center justify-center gap-4">
<div>
<label class="block font-medium mb-1 text-gray-700">Name</label>
<div id="providerNameBox"
class="font-medium mb-1 text-gray-700"></div>
</div>
<div>
<label class="block font-medium mb-1 text-gray-700">User</label>
<div id="providerUserBox"
class="font-medium mb-1 text-gray-700"></div>
</div>
</div>
</div>
<!-- Display Name --> <!-- Display Name -->
<div id="displayNameWrapper" class="py-2"> <div id="displayNameWrapper" class="py-2">
<label class="block font-medium mb-1 text-gray-700">Display Name</label> <label class="block font-medium mb-1 text-gray-700">Display Name</label>
@ -35,7 +56,7 @@
</div> </div>
<!-- TOTP --> <!-- TOTP -->
<div class="border rounded p-4"> <div class="border rounded p-4" id="totpWrapper">
<h2 class="font-semibold text-lg mb-2">Two-Factor Authentication (TOTP)</h2> <h2 class="font-semibold text-lg mb-2">Two-Factor Authentication (TOTP)</h2>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">

View file

@ -1,5 +1,5 @@
import { addRoute, navigateTo, setTitle } from "@app/routing"; import { addRoute, handleRoute, navigateTo, setTitle } from "@app/routing";
import { showError } from "@app/toast"; import { showError, showSuccess } from "@app/toast";
import page from "./profile.html?raw"; import page from "./profile.html?raw";
import { updateUser } from "@app/auth"; import { updateUser } from "@app/auth";
import { isNullish } from "@app/utils"; import { isNullish } from "@app/utils";
@ -38,9 +38,16 @@ export async function renderOAuth2QRCode(
}); });
canvas.style.width = ""; canvas.style.width = "";
canvas.style.height = ""; canvas.style.height = "";
} 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);
}
}
}
async function route(url: string, _args: { [k: string]: string }) { async function route(url: string, _args: { [k: string]: string }) {
setTitle("Edit Profile"); setTitle("Edit Profile");
return { return {
html: page, html: page,
@ -90,11 +97,15 @@ async function route(url: string, _args: { [k: string]: string }) {
let passwordButton = let passwordButton =
app.querySelector<HTMLButtonElement>("#passwordButton")!; app.querySelector<HTMLButtonElement>("#passwordButton")!;
if (!isNullish(user.selfInfo?.loginName)) let providerWrapper =
loginNameBox.innerText = user.selfInfo?.loginName; app.querySelector<HTMLDivElement>("#providerWrapper")!;
else let providerNameBox =
loginNameBox.innerHTML = app.querySelector<HTMLDivElement>("#providerNameBox")!;
'<span class="text-red-600 font-bold mb-1">You don\'t have a login name</span>'; let providerUserBox =
app.querySelector<HTMLDivElement>("#providerUserBox")!;
let accountTypeBox =
app.querySelector<HTMLDivElement>("#accountType")!;
displayNameBox.value = user.name; displayNameBox.value = user.name;
guestBox.hidden = !user.guest; guestBox.hidden = !user.guest;
@ -118,7 +129,17 @@ async function route(url: string, _args: { [k: string]: string }) {
if (c.startsWith("bg-") || c.startsWith("hover:bg-")) if (c.startsWith("bg-") || c.startsWith("hover:bg-"))
passwordButton.classList.remove(c); passwordButton.classList.remove(c);
} }
passwordButton.disabled = true; let totpWrapper = app.querySelector<HTMLDivElement>("#totpWrapper")!;
if (user.guest) {
removeBgColor(
passwordButton,
displayNameButton,
enableBtn,
disableBtn,
showSecretBtn,
);
passwordButton.classList.add( passwordButton.classList.add(
"bg-gray-700", "bg-gray-700",
"hover:bg-gray-700", "hover:bg-gray-700",
@ -127,29 +148,11 @@ async function route(url: string, _args: { [k: string]: string }) {
passwordBox.disabled = true; passwordBox.disabled = true;
passwordBox.classList.add("color-white"); 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.disabled = true;
displayNameButton.classList.add("bg-gray-700"); displayNameButton.classList.add("bg-gray-700", "color-white");
displayNameButton.classList.add("color-white");
displayNameBox.disabled = true; displayNameBox.disabled = true;
displayNameBox.classList.add("color-white"); 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"); enableBtn.classList.add("bg-gray-700", "hover:bg-gray-700");
disableBtn.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"); showSecretBtn.classList.add("bg-gray-700", "hover:bg-gray-700");
@ -157,6 +160,34 @@ async function route(url: string, _args: { [k: string]: string }) {
enableBtn.disabled = true; enableBtn.disabled = true;
disableBtn.disabled = true; disableBtn.disabled = true;
showSecretBtn.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 ---- // ---- Update UI ----
@ -204,10 +235,22 @@ async function route(url: string, _args: { [k: string]: string }) {
secretBox.classList.toggle("hidden"); 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 // Initialize UI state
refreshTotpUI(); refreshTotpUI();
}, },
}; };
} }
addRoute("/profile", route); addRoute("/profile", route);

View file

@ -18,7 +18,7 @@ Project Transcendance {
Table user { Table user {
id text [PK, not null] id text [PK, not null]
login text [unique] 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"] 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"] otp text [null, Note: "If otp is NULL, then the user didn't configure 2FA"]
guest integer [not null, default: 0] guest integer [not null, default: 0]

View file

@ -1,7 +1,7 @@
CREATE TABLE IF NOT EXISTS user ( CREATE TABLE IF NOT EXISTS user (
id TEXT PRIMARY KEY NOT NULL, id TEXT PRIMARY KEY NOT NULL,
login TEXT UNIQUE, login TEXT UNIQUE,
name TEXT NOT NULL, name TEXT NOT NULL UNIQUE,
password TEXT, password TEXT,
otp TEXT, otp TEXT,
guest INTEGER NOT NULL DEFAULT 0, guest INTEGER NOT NULL DEFAULT 0,

View file

@ -3,6 +3,7 @@ import { Otp } from '@shared/auth';
import { isNullish } from '@shared/utils'; import { isNullish } from '@shared/utils';
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
import { UUID, newUUID } from '@shared/utils/uuid'; import { UUID, newUUID } from '@shared/utils/uuid';
import { SqliteError } from 'better-sqlite3';
// never use this directly // never use this directly
@ -20,6 +21,10 @@ export interface IUserDb extends Database {
getAllUserFromProvider(provider: string): User[] | undefined, getAllUserFromProvider(provider: string): User[] | undefined,
getAllUsers(this: IUserDb): 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> = { 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; const req = this.prepare('SELECT * FROM user WHERE oauth2 = @oauth2').get({ oauth2: `${provider}:${unique}` }) as Partial<User> | undefined;
return userFromRow(req); 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; export type UserId = UUID;
@ -170,7 +193,7 @@ export type User = {
readonly password?: string; readonly password?: string;
readonly otp?: string; readonly otp?: string;
readonly guest: boolean; 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_name?: string;
readonly provider_unique?: string; readonly provider_unique?: string;
}; };
@ -207,7 +230,7 @@ async function hashPassword(
* *
* @returns The user if it exists, undefined otherwise * @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)) 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;
@ -216,9 +239,9 @@ export function userFromRow(row?: Partial<Omit<User, 'provider_name' | 'provider
let provider_name = undefined; let provider_name = undefined;
let provider_unique = undefined; let provider_unique = undefined;
if (row.provider) { if (row.oauth2) {
const splitted = row.provider.split(':', 1); const splitted = row.oauth2.split(/:(.*)/);
if (splitted.length != 2) { return undefined; } if (splitted.length != 3) { return undefined; }
provider_name = splitted[0]; provider_name = splitted[0];
provider_unique = splitted[1]; provider_unique = splitted[1];
} }

View file

@ -330,6 +330,20 @@
"/api/auth/guest": { "/api/auth/guest": {
"post": { "post": {
"operationId": "guestLogin", "operationId": "guestLogin",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}
}
}
},
"responses": { "responses": {
"200": { "200": {
"description": "Default Response", "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": { "500": {
"description": "Default Response", "description": "Default Response",
"content": { "content": {

View file

@ -1,39 +1,95 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { Type } from 'typebox'; import { Static, Type } from 'typebox';
import { typeResponse, isNullish, MakeStaticResponse } from '@shared/utils'; import { typeResponse, isNullish, MakeStaticResponse } from '@shared/utils';
export const GuestLoginRes = { 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', { '200': typeResponse('success', 'guestLogin.success', {
token: Type.String({ token: Type.String({
description: 'JWT that represent a logged in user', description: 'JWT that represent a logged in user',
}), }),
}), }),
'400': typeResponse('failed', 'guestLogin.failed.invalid'),
}; };
export type GuestLoginRes = MakeStaticResponse<typeof GuestLoginRes>; 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 => { const getRandomFromList = (list: string[]): string => {
return list[Math.floor(Math.random() * list.length)]; 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> => { const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
void _opts; void _opts;
fastify.post<{ Body: null, Reply: GuestLoginRes }>( fastify.post<{ Body: GuestLoginReq; Reply: GuestLoginRes }>(
'/api/auth/guest', '/api/auth/guest',
{ schema: { response: GuestLoginRes, operationId: 'guestLogin' } }, {
schema: {
body: GuestLoginReq,
response: GuestLoginRes,
operationId: 'guestLogin',
},
},
async function(req, res) { async function(req, res) {
void req; void req;
void res; void res;
try { try {
console.log('DEBUG ----- guest login backend'); let user_name: string | undefined = req.body?.name;
const adjective = getRandomFromList(fastify.words.adjectives); if (isNullish(user_name)) {
const adjective = getRandomFromList(
fastify.words.adjectives,
);
const noun = getRandomFromList(fastify.words.nouns); 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)) { 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', { return res.makeResponse(200, 'success', 'guestLogin.success', {
token: this.signJwt('auth', user.id.toString()), token: this.signJwt('auth', user.id.toString()),
@ -41,7 +97,11 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
} }
catch (e: unknown) { catch (e: unknown) {
fastify.log.error(e); fastify.log.error(e);
return res.makeResponse(500, 'failed', 'guestLogin.failed.generic.error'); return res.makeResponse(
500,
'failed',
'guestLogin.failed.generic.error',
);
} }
}, },
); );

View file

@ -30,9 +30,23 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
const result = await creq.getCode(); const result = await creq.getCode();
const userinfo = await provider.getUserInfo(result); const userinfo = await provider.getUserInfo(result);
let u = this.db.getOauth2User(provider.display_name, userinfo.unique_id); let u = this.db.getOauth2User(provider.display_name, userinfo.unique_id);
if (isNullish(u)) { 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)) { if (isNullish(u)) {
return res.code(500).send('failed to fetch or create user...'); return res.code(500).send('failed to fetch or create user...');

View file

@ -47,7 +47,19 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
// password is good too ! // password is good too !
if (this.db.getUserFromLoginName(name) !== undefined) { return res.makeResponse(400, 'failed', 'signin.failed.username.existing'); } 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'); } 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... // every check has been passed, they are now logged in, using this token to say who they are...

View file

@ -352,6 +352,20 @@
"/api/auth/guest": { "/api/auth/guest": {
"post": { "post": {
"operationId": "guestLogin", "operationId": "guestLogin",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}
}
}
},
"responses": { "responses": {
"200": { "200": {
"description": "Default Response", "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": { "500": {
"description": "Default Response", "description": "Default Response",
"content": { "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}": { "/api/user/info/{user}": {
"get": { "get": {
"operationId": "getUser", "operationId": "getUser",

View file

@ -8,6 +8,114 @@
"schemas": {} "schemas": {}
}, },
"paths": { "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}": { "/api/user/info/{user}": {
"get": { "get": {
"operationId": "getUser", "operationId": "getUser",

View 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;

View file

@ -49,7 +49,7 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
if (isNullish(user)) { if (isNullish(user)) {
return res.makeResponse(404, 'failure', 'userinfo.failure.unknownUser'); return res.makeResponse(404, 'failure', 'userinfo.failure.unknownUser');
} }
console.log(user);
const payload = { const payload = {
name: user.name, name: user.name,