From 3140da7bab2b211e2e19b1a3ebd6e34fdb468bc3 Mon Sep 17 00:00:00 2001 From: Maieul BOYER Date: Mon, 1 Dec 2025 19:26:22 +0100 Subject: [PATCH] wip --- .../api/generated/.openapi-generator/FILES | 5 + .../src/api/generated/apis/OpenapiOtherApi.ts | 26 ++- .../generated/models/ChatTest200Response.ts | 20 +-- .../models/ChatTest200ResponsePayload.ts | 84 +++++++++ .../generated/models/DisableOtp400Response.ts | 93 ++++++++++ .../generated/models/EnableOtp400Response.ts | 93 ++++++++++ .../models/GetUser200ResponsePayload.ts | 16 ++ .../GetUser200ResponsePayloadSelfInfo.ts | 81 +++++++++ .../generated/models/StatusOtp200Response.ts | 22 +-- .../models/StatusOtp200ResponseAnyOf.ts | 20 +-- .../StatusOtp200ResponseAnyOfPayload.ts | 66 ++++++++ .../generated/models/StatusOtp400Response.ts | 93 ++++++++++ frontend/src/api/generated/models/index.ts | 5 + frontend/src/auth/index.ts | 61 ++++--- frontend/src/pages/index.ts | 1 + frontend/src/pages/login/login.ts | 97 ++++++++++- frontend/src/pages/login/totp.html | 11 ++ frontend/src/pages/profile/profile.html | 66 ++++++++ frontend/src/pages/profile/profile.ts | 159 ++++++++++++++++++ frontend/src/routing/index.ts | 9 +- frontend/src/routing/special_routes.ts | 2 - frontend/vite.config.js | 4 + src/@shared/src/auth/index.ts | 3 +- src/auth/openapi.json | 58 ++++++- src/auth/src/routes/disableOtp.ts | 8 + src/auth/src/routes/enableOtp.ts | 8 + src/auth/src/routes/statusOtp.ts | 35 +++- src/openapi.json | 72 +++++++- src/user/openapi.json | 14 ++ src/user/src/routes/info.ts | 11 ++ 30 files changed, 1148 insertions(+), 95 deletions(-) create mode 100644 frontend/src/api/generated/models/ChatTest200ResponsePayload.ts create mode 100644 frontend/src/api/generated/models/DisableOtp400Response.ts create mode 100644 frontend/src/api/generated/models/EnableOtp400Response.ts create mode 100644 frontend/src/api/generated/models/GetUser200ResponsePayloadSelfInfo.ts create mode 100644 frontend/src/api/generated/models/StatusOtp200ResponseAnyOfPayload.ts create mode 100644 frontend/src/api/generated/models/StatusOtp400Response.ts create mode 100644 frontend/src/pages/login/totp.html create mode 100644 frontend/src/pages/profile/profile.html create mode 100644 frontend/src/pages/profile/profile.ts diff --git a/frontend/src/api/generated/.openapi-generator/FILES b/frontend/src/api/generated/.openapi-generator/FILES index af12e66..b05ee98 100644 --- a/frontend/src/api/generated/.openapi-generator/FILES +++ b/frontend/src/api/generated/.openapi-generator/FILES @@ -2,15 +2,19 @@ apis/OpenapiOtherApi.ts apis/index.ts index.ts models/ChatTest200Response.ts +models/ChatTest200ResponsePayload.ts models/DisableOtp200Response.ts +models/DisableOtp400Response.ts models/DisableOtp401Response.ts models/DisableOtp500Response.ts models/EnableOtp200Response.ts models/EnableOtp200ResponsePayload.ts +models/EnableOtp400Response.ts models/EnableOtp401Response.ts models/EnableOtp401ResponseAnyOf.ts models/GetUser200Response.ts models/GetUser200ResponsePayload.ts +models/GetUser200ResponsePayloadSelfInfo.ts models/GetUser403Response.ts models/GetUser404Response.ts models/GetUserUserParameter.ts @@ -41,6 +45,7 @@ models/Signin500Response.ts models/StatusOtp200Response.ts models/StatusOtp200ResponseAnyOf.ts models/StatusOtp200ResponseAnyOf1.ts +models/StatusOtp200ResponseAnyOfPayload.ts models/StatusOtp401Response.ts models/StatusOtp500Response.ts models/index.ts diff --git a/frontend/src/api/generated/apis/OpenapiOtherApi.ts b/frontend/src/api/generated/apis/OpenapiOtherApi.ts index fda4f60..c37580a 100644 --- a/frontend/src/api/generated/apis/OpenapiOtherApi.ts +++ b/frontend/src/api/generated/apis/OpenapiOtherApi.ts @@ -17,9 +17,11 @@ import * as runtime from '../runtime'; import type { ChatTest200Response, DisableOtp200Response, + DisableOtp400Response, DisableOtp401Response, DisableOtp500Response, EnableOtp200Response, + EnableOtp400Response, EnableOtp401Response, GetUser200Response, GetUser403Response, @@ -51,12 +53,16 @@ import { ChatTest200ResponseToJSON, DisableOtp200ResponseFromJSON, DisableOtp200ResponseToJSON, + DisableOtp400ResponseFromJSON, + DisableOtp400ResponseToJSON, DisableOtp401ResponseFromJSON, DisableOtp401ResponseToJSON, DisableOtp500ResponseFromJSON, DisableOtp500ResponseToJSON, EnableOtp200ResponseFromJSON, EnableOtp200ResponseToJSON, + EnableOtp400ResponseFromJSON, + EnableOtp400ResponseToJSON, EnableOtp401ResponseFromJSON, EnableOtp401ResponseToJSON, GetUser200ResponseFromJSON, @@ -174,7 +180,7 @@ export class OpenapiOtherApi extends runtime.BaseAPI { /** */ - async disableOtpRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + async disableOtpRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { const queryParameters: any = {}; const headerParameters: runtime.HTTPHeaders = {}; @@ -197,6 +203,10 @@ export class OpenapiOtherApi extends runtime.BaseAPI { // Object response for status 200 return new runtime.JSONApiResponse(response, (jsonValue) => DisableOtp200ResponseFromJSON(jsonValue)); } + if (response.status === 400) { + // Object response for status 400 + return new runtime.JSONApiResponse(response, (jsonValue) => DisableOtp400ResponseFromJSON(jsonValue)); + } if (response.status === 401) { // Object response for status 401 return new runtime.JSONApiResponse(response, (jsonValue) => DisableOtp401ResponseFromJSON(jsonValue)); @@ -208,19 +218,19 @@ 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, 401, 500`); + throw new runtime.ResponseError(response, `Unexpected status code: ${response.status}. Expected one of: 200, 400, 401, 500`); } /** */ - async disableOtp(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + async disableOtp(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { const response = await this.disableOtpRaw(initOverrides); return await response.value(); } /** */ - async enableOtpRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + async enableOtpRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { const queryParameters: any = {}; const headerParameters: runtime.HTTPHeaders = {}; @@ -243,6 +253,10 @@ export class OpenapiOtherApi extends runtime.BaseAPI { // Object response for status 200 return new runtime.JSONApiResponse(response, (jsonValue) => EnableOtp200ResponseFromJSON(jsonValue)); } + if (response.status === 400) { + // Object response for status 400 + return new runtime.JSONApiResponse(response, (jsonValue) => EnableOtp400ResponseFromJSON(jsonValue)); + } if (response.status === 401) { // Object response for status 401 return new runtime.JSONApiResponse(response, (jsonValue) => EnableOtp401ResponseFromJSON(jsonValue)); @@ -250,12 +264,12 @@ 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, 401`); + throw new runtime.ResponseError(response, `Unexpected status code: ${response.status}. Expected one of: 200, 400, 401`); } /** */ - async enableOtp(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + async enableOtp(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { const response = await this.enableOtpRaw(initOverrides); return await response.value(); } diff --git a/frontend/src/api/generated/models/ChatTest200Response.ts b/frontend/src/api/generated/models/ChatTest200Response.ts index 6feb66e..9fa3dd7 100644 --- a/frontend/src/api/generated/models/ChatTest200Response.ts +++ b/frontend/src/api/generated/models/ChatTest200Response.ts @@ -13,13 +13,13 @@ */ import { mapValues } from '../runtime'; -import type { GetUser200ResponsePayload } from './GetUser200ResponsePayload'; +import type { ChatTest200ResponsePayload } from './ChatTest200ResponsePayload'; import { - GetUser200ResponsePayloadFromJSON, - GetUser200ResponsePayloadFromJSONTyped, - GetUser200ResponsePayloadToJSON, - GetUser200ResponsePayloadToJSONTyped, -} from './GetUser200ResponsePayload'; + ChatTest200ResponsePayloadFromJSON, + ChatTest200ResponsePayloadFromJSONTyped, + ChatTest200ResponsePayloadToJSON, + ChatTest200ResponsePayloadToJSONTyped, +} from './ChatTest200ResponsePayload'; /** * @@ -41,10 +41,10 @@ export interface ChatTest200Response { msg: ChatTest200ResponseMsgEnum; /** * - * @type {GetUser200ResponsePayload} + * @type {ChatTest200ResponsePayload} * @memberof ChatTest200Response */ - payload: GetUser200ResponsePayload; + payload: ChatTest200ResponsePayload; } @@ -87,7 +87,7 @@ export function ChatTest200ResponseFromJSONTyped(json: any, ignoreDiscriminator: 'kind': json['kind'], 'msg': json['msg'], - 'payload': GetUser200ResponsePayloadFromJSON(json['payload']), + 'payload': ChatTest200ResponsePayloadFromJSON(json['payload']), }; } @@ -104,7 +104,7 @@ export function ChatTest200ResponseToJSONTyped(value?: ChatTest200Response | nul 'kind': value['kind'], 'msg': value['msg'], - 'payload': GetUser200ResponsePayloadToJSON(value['payload']), + 'payload': ChatTest200ResponsePayloadToJSON(value['payload']), }; } diff --git a/frontend/src/api/generated/models/ChatTest200ResponsePayload.ts b/frontend/src/api/generated/models/ChatTest200ResponsePayload.ts new file mode 100644 index 0000000..2017a31 --- /dev/null +++ b/frontend/src/api/generated/models/ChatTest200ResponsePayload.ts @@ -0,0 +1,84 @@ +/* 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 ChatTest200ResponsePayload + */ +export interface ChatTest200ResponsePayload { + /** + * + * @type {string} + * @memberof ChatTest200ResponsePayload + */ + name: string; + /** + * + * @type {string} + * @memberof ChatTest200ResponsePayload + */ + id: string; + /** + * + * @type {boolean} + * @memberof ChatTest200ResponsePayload + */ + guest: boolean; +} + +/** + * Check if a given object implements the ChatTest200ResponsePayload interface. + */ +export function instanceOfChatTest200ResponsePayload(value: object): value is ChatTest200ResponsePayload { + if (!('name' in value) || value['name'] === undefined) return false; + if (!('id' in value) || value['id'] === undefined) return false; + if (!('guest' in value) || value['guest'] === undefined) return false; + return true; +} + +export function ChatTest200ResponsePayloadFromJSON(json: any): ChatTest200ResponsePayload { + return ChatTest200ResponsePayloadFromJSONTyped(json, false); +} + +export function ChatTest200ResponsePayloadFromJSONTyped(json: any, ignoreDiscriminator: boolean): ChatTest200ResponsePayload { + if (json == null) { + return json; + } + return { + + 'name': json['name'], + 'id': json['id'], + 'guest': json['guest'], + }; +} + +export function ChatTest200ResponsePayloadToJSON(json: any): ChatTest200ResponsePayload { + return ChatTest200ResponsePayloadToJSONTyped(json, false); +} + +export function ChatTest200ResponsePayloadToJSONTyped(value?: ChatTest200ResponsePayload | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'name': value['name'], + 'id': value['id'], + 'guest': value['guest'], + }; +} + diff --git a/frontend/src/api/generated/models/DisableOtp400Response.ts b/frontend/src/api/generated/models/DisableOtp400Response.ts new file mode 100644 index 0000000..481e2be --- /dev/null +++ b/frontend/src/api/generated/models/DisableOtp400Response.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 DisableOtp400Response + */ +export interface DisableOtp400Response { + /** + * + * @type {string} + * @memberof DisableOtp400Response + */ + kind: DisableOtp400ResponseKindEnum; + /** + * + * @type {string} + * @memberof DisableOtp400Response + */ + msg: DisableOtp400ResponseMsgEnum; +} + + +/** + * @export + */ +export const DisableOtp400ResponseKindEnum = { + Failure: 'failure' +} as const; +export type DisableOtp400ResponseKindEnum = typeof DisableOtp400ResponseKindEnum[keyof typeof DisableOtp400ResponseKindEnum]; + +/** + * @export + */ +export const DisableOtp400ResponseMsgEnum = { + DisableOtpFailureGuest: 'disableOtp.failure.guest' +} as const; +export type DisableOtp400ResponseMsgEnum = typeof DisableOtp400ResponseMsgEnum[keyof typeof DisableOtp400ResponseMsgEnum]; + + +/** + * Check if a given object implements the DisableOtp400Response interface. + */ +export function instanceOfDisableOtp400Response(value: object): value is DisableOtp400Response { + if (!('kind' in value) || value['kind'] === undefined) return false; + if (!('msg' in value) || value['msg'] === undefined) return false; + return true; +} + +export function DisableOtp400ResponseFromJSON(json: any): DisableOtp400Response { + return DisableOtp400ResponseFromJSONTyped(json, false); +} + +export function DisableOtp400ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): DisableOtp400Response { + if (json == null) { + return json; + } + return { + + 'kind': json['kind'], + 'msg': json['msg'], + }; +} + +export function DisableOtp400ResponseToJSON(json: any): DisableOtp400Response { + return DisableOtp400ResponseToJSONTyped(json, false); +} + +export function DisableOtp400ResponseToJSONTyped(value?: DisableOtp400Response | 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/EnableOtp400Response.ts b/frontend/src/api/generated/models/EnableOtp400Response.ts new file mode 100644 index 0000000..ec0b8e7 --- /dev/null +++ b/frontend/src/api/generated/models/EnableOtp400Response.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 EnableOtp400Response + */ +export interface EnableOtp400Response { + /** + * + * @type {string} + * @memberof EnableOtp400Response + */ + kind: EnableOtp400ResponseKindEnum; + /** + * + * @type {string} + * @memberof EnableOtp400Response + */ + msg: EnableOtp400ResponseMsgEnum; +} + + +/** + * @export + */ +export const EnableOtp400ResponseKindEnum = { + Failure: 'failure' +} as const; +export type EnableOtp400ResponseKindEnum = typeof EnableOtp400ResponseKindEnum[keyof typeof EnableOtp400ResponseKindEnum]; + +/** + * @export + */ +export const EnableOtp400ResponseMsgEnum = { + EnableOtpFailureGuest: 'enableOtp.failure.guest' +} as const; +export type EnableOtp400ResponseMsgEnum = typeof EnableOtp400ResponseMsgEnum[keyof typeof EnableOtp400ResponseMsgEnum]; + + +/** + * Check if a given object implements the EnableOtp400Response interface. + */ +export function instanceOfEnableOtp400Response(value: object): value is EnableOtp400Response { + if (!('kind' in value) || value['kind'] === undefined) return false; + if (!('msg' in value) || value['msg'] === undefined) return false; + return true; +} + +export function EnableOtp400ResponseFromJSON(json: any): EnableOtp400Response { + return EnableOtp400ResponseFromJSONTyped(json, false); +} + +export function EnableOtp400ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): EnableOtp400Response { + if (json == null) { + return json; + } + return { + + 'kind': json['kind'], + 'msg': json['msg'], + }; +} + +export function EnableOtp400ResponseToJSON(json: any): EnableOtp400Response { + return EnableOtp400ResponseToJSONTyped(json, false); +} + +export function EnableOtp400ResponseToJSONTyped(value?: EnableOtp400Response | 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/GetUser200ResponsePayload.ts b/frontend/src/api/generated/models/GetUser200ResponsePayload.ts index f41f951..79b420a 100644 --- a/frontend/src/api/generated/models/GetUser200ResponsePayload.ts +++ b/frontend/src/api/generated/models/GetUser200ResponsePayload.ts @@ -13,6 +13,14 @@ */ import { mapValues } from '../runtime'; +import type { GetUser200ResponsePayloadSelfInfo } from './GetUser200ResponsePayloadSelfInfo'; +import { + GetUser200ResponsePayloadSelfInfoFromJSON, + GetUser200ResponsePayloadSelfInfoFromJSONTyped, + GetUser200ResponsePayloadSelfInfoToJSON, + GetUser200ResponsePayloadSelfInfoToJSONTyped, +} from './GetUser200ResponsePayloadSelfInfo'; + /** * * @export @@ -37,6 +45,12 @@ export interface GetUser200ResponsePayload { * @memberof GetUser200ResponsePayload */ guest: boolean; + /** + * + * @type {GetUser200ResponsePayloadSelfInfo} + * @memberof GetUser200ResponsePayload + */ + selfInfo?: GetUser200ResponsePayloadSelfInfo; } /** @@ -62,6 +76,7 @@ export function GetUser200ResponsePayloadFromJSONTyped(json: any, ignoreDiscrimi 'name': json['name'], 'id': json['id'], 'guest': json['guest'], + 'selfInfo': json['selfInfo'] == null ? undefined : GetUser200ResponsePayloadSelfInfoFromJSON(json['selfInfo']), }; } @@ -79,6 +94,7 @@ export function GetUser200ResponsePayloadToJSONTyped(value?: GetUser200ResponseP 'name': value['name'], 'id': value['id'], 'guest': value['guest'], + 'selfInfo': GetUser200ResponsePayloadSelfInfoToJSON(value['selfInfo']), }; } diff --git a/frontend/src/api/generated/models/GetUser200ResponsePayloadSelfInfo.ts b/frontend/src/api/generated/models/GetUser200ResponsePayloadSelfInfo.ts new file mode 100644 index 0000000..0a37bac --- /dev/null +++ b/frontend/src/api/generated/models/GetUser200ResponsePayloadSelfInfo.ts @@ -0,0 +1,81 @@ +/* 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 GetUser200ResponsePayloadSelfInfo + */ +export interface GetUser200ResponsePayloadSelfInfo { + /** + * + * @type {string} + * @memberof GetUser200ResponsePayloadSelfInfo + */ + loginName?: string; + /** + * + * @type {string} + * @memberof GetUser200ResponsePayloadSelfInfo + */ + providerId?: string; + /** + * + * @type {string} + * @memberof GetUser200ResponsePayloadSelfInfo + */ + providerUser?: string; +} + +/** + * Check if a given object implements the GetUser200ResponsePayloadSelfInfo interface. + */ +export function instanceOfGetUser200ResponsePayloadSelfInfo(value: object): value is GetUser200ResponsePayloadSelfInfo { + return true; +} + +export function GetUser200ResponsePayloadSelfInfoFromJSON(json: any): GetUser200ResponsePayloadSelfInfo { + return GetUser200ResponsePayloadSelfInfoFromJSONTyped(json, false); +} + +export function GetUser200ResponsePayloadSelfInfoFromJSONTyped(json: any, ignoreDiscriminator: boolean): GetUser200ResponsePayloadSelfInfo { + if (json == null) { + return json; + } + return { + + 'loginName': json['login_name'] == null ? undefined : json['login_name'], + 'providerId': json['provider_id'] == null ? undefined : json['provider_id'], + 'providerUser': json['provider_user'] == null ? undefined : json['provider_user'], + }; +} + +export function GetUser200ResponsePayloadSelfInfoToJSON(json: any): GetUser200ResponsePayloadSelfInfo { + return GetUser200ResponsePayloadSelfInfoToJSONTyped(json, false); +} + +export function GetUser200ResponsePayloadSelfInfoToJSONTyped(value?: GetUser200ResponsePayloadSelfInfo | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'login_name': value['loginName'], + 'provider_id': value['providerId'], + 'provider_user': value['providerUser'], + }; +} + diff --git a/frontend/src/api/generated/models/StatusOtp200Response.ts b/frontend/src/api/generated/models/StatusOtp200Response.ts index 7dec45c..522edc1 100644 --- a/frontend/src/api/generated/models/StatusOtp200Response.ts +++ b/frontend/src/api/generated/models/StatusOtp200Response.ts @@ -13,13 +13,6 @@ */ import { mapValues } from '../runtime'; -import type { EnableOtp200ResponsePayload } from './EnableOtp200ResponsePayload'; -import { - EnableOtp200ResponsePayloadFromJSON, - EnableOtp200ResponsePayloadFromJSONTyped, - EnableOtp200ResponsePayloadToJSON, - EnableOtp200ResponsePayloadToJSONTyped, -} from './EnableOtp200ResponsePayload'; import type { StatusOtp200ResponseAnyOf } from './StatusOtp200ResponseAnyOf'; import { StatusOtp200ResponseAnyOfFromJSON, @@ -27,6 +20,13 @@ import { StatusOtp200ResponseAnyOfToJSON, StatusOtp200ResponseAnyOfToJSONTyped, } from './StatusOtp200ResponseAnyOf'; +import type { StatusOtp200ResponseAnyOfPayload } from './StatusOtp200ResponseAnyOfPayload'; +import { + StatusOtp200ResponseAnyOfPayloadFromJSON, + StatusOtp200ResponseAnyOfPayloadFromJSONTyped, + StatusOtp200ResponseAnyOfPayloadToJSON, + StatusOtp200ResponseAnyOfPayloadToJSONTyped, +} from './StatusOtp200ResponseAnyOfPayload'; import type { StatusOtp200ResponseAnyOf1 } from './StatusOtp200ResponseAnyOf1'; import { StatusOtp200ResponseAnyOf1FromJSON, @@ -55,10 +55,10 @@ export interface StatusOtp200Response { msg: StatusOtp200ResponseMsgEnum; /** * - * @type {EnableOtp200ResponsePayload} + * @type {StatusOtp200ResponseAnyOfPayload} * @memberof StatusOtp200Response */ - payload: EnableOtp200ResponsePayload; + payload: StatusOtp200ResponseAnyOfPayload; } @@ -101,7 +101,7 @@ export function StatusOtp200ResponseFromJSONTyped(json: any, ignoreDiscriminator 'kind': json['kind'], 'msg': json['msg'], - 'payload': EnableOtp200ResponsePayloadFromJSON(json['payload']), + 'payload': StatusOtp200ResponseAnyOfPayloadFromJSON(json['payload']), }; } @@ -118,7 +118,7 @@ export function StatusOtp200ResponseToJSONTyped(value?: StatusOtp200Response | n 'kind': value['kind'], 'msg': value['msg'], - 'payload': EnableOtp200ResponsePayloadToJSON(value['payload']), + 'payload': StatusOtp200ResponseAnyOfPayloadToJSON(value['payload']), }; } diff --git a/frontend/src/api/generated/models/StatusOtp200ResponseAnyOf.ts b/frontend/src/api/generated/models/StatusOtp200ResponseAnyOf.ts index 5750e63..0ea3524 100644 --- a/frontend/src/api/generated/models/StatusOtp200ResponseAnyOf.ts +++ b/frontend/src/api/generated/models/StatusOtp200ResponseAnyOf.ts @@ -13,13 +13,13 @@ */ import { mapValues } from '../runtime'; -import type { EnableOtp200ResponsePayload } from './EnableOtp200ResponsePayload'; +import type { StatusOtp200ResponseAnyOfPayload } from './StatusOtp200ResponseAnyOfPayload'; import { - EnableOtp200ResponsePayloadFromJSON, - EnableOtp200ResponsePayloadFromJSONTyped, - EnableOtp200ResponsePayloadToJSON, - EnableOtp200ResponsePayloadToJSONTyped, -} from './EnableOtp200ResponsePayload'; + StatusOtp200ResponseAnyOfPayloadFromJSON, + StatusOtp200ResponseAnyOfPayloadFromJSONTyped, + StatusOtp200ResponseAnyOfPayloadToJSON, + StatusOtp200ResponseAnyOfPayloadToJSONTyped, +} from './StatusOtp200ResponseAnyOfPayload'; /** * @@ -41,10 +41,10 @@ export interface StatusOtp200ResponseAnyOf { msg: StatusOtp200ResponseAnyOfMsgEnum; /** * - * @type {EnableOtp200ResponsePayload} + * @type {StatusOtp200ResponseAnyOfPayload} * @memberof StatusOtp200ResponseAnyOf */ - payload: EnableOtp200ResponsePayload; + payload: StatusOtp200ResponseAnyOfPayload; } @@ -87,7 +87,7 @@ export function StatusOtp200ResponseAnyOfFromJSONTyped(json: any, ignoreDiscrimi 'kind': json['kind'], 'msg': json['msg'], - 'payload': EnableOtp200ResponsePayloadFromJSON(json['payload']), + 'payload': StatusOtp200ResponseAnyOfPayloadFromJSON(json['payload']), }; } @@ -104,7 +104,7 @@ export function StatusOtp200ResponseAnyOfToJSONTyped(value?: StatusOtp200Respons 'kind': value['kind'], 'msg': value['msg'], - 'payload': EnableOtp200ResponsePayloadToJSON(value['payload']), + 'payload': StatusOtp200ResponseAnyOfPayloadToJSON(value['payload']), }; } diff --git a/frontend/src/api/generated/models/StatusOtp200ResponseAnyOfPayload.ts b/frontend/src/api/generated/models/StatusOtp200ResponseAnyOfPayload.ts new file mode 100644 index 0000000..260af1d --- /dev/null +++ b/frontend/src/api/generated/models/StatusOtp200ResponseAnyOfPayload.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 StatusOtp200ResponseAnyOfPayload + */ +export interface StatusOtp200ResponseAnyOfPayload { + /** + * The otp secret + * @type {string} + * @memberof StatusOtp200ResponseAnyOfPayload + */ + secret: string; +} + +/** + * Check if a given object implements the StatusOtp200ResponseAnyOfPayload interface. + */ +export function instanceOfStatusOtp200ResponseAnyOfPayload(value: object): value is StatusOtp200ResponseAnyOfPayload { + if (!('secret' in value) || value['secret'] === undefined) return false; + return true; +} + +export function StatusOtp200ResponseAnyOfPayloadFromJSON(json: any): StatusOtp200ResponseAnyOfPayload { + return StatusOtp200ResponseAnyOfPayloadFromJSONTyped(json, false); +} + +export function StatusOtp200ResponseAnyOfPayloadFromJSONTyped(json: any, ignoreDiscriminator: boolean): StatusOtp200ResponseAnyOfPayload { + if (json == null) { + return json; + } + return { + + 'secret': json['secret'], + }; +} + +export function StatusOtp200ResponseAnyOfPayloadToJSON(json: any): StatusOtp200ResponseAnyOfPayload { + return StatusOtp200ResponseAnyOfPayloadToJSONTyped(json, false); +} + +export function StatusOtp200ResponseAnyOfPayloadToJSONTyped(value?: StatusOtp200ResponseAnyOfPayload | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'secret': value['secret'], + }; +} + diff --git a/frontend/src/api/generated/models/StatusOtp400Response.ts b/frontend/src/api/generated/models/StatusOtp400Response.ts new file mode 100644 index 0000000..a952478 --- /dev/null +++ b/frontend/src/api/generated/models/StatusOtp400Response.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 StatusOtp400Response + */ +export interface StatusOtp400Response { + /** + * + * @type {string} + * @memberof StatusOtp400Response + */ + kind: StatusOtp400ResponseKindEnum; + /** + * + * @type {string} + * @memberof StatusOtp400Response + */ + msg: StatusOtp400ResponseMsgEnum; +} + + +/** + * @export + */ +export const StatusOtp400ResponseKindEnum = { + Failure: 'failure' +} as const; +export type StatusOtp400ResponseKindEnum = typeof StatusOtp400ResponseKindEnum[keyof typeof StatusOtp400ResponseKindEnum]; + +/** + * @export + */ +export const StatusOtp400ResponseMsgEnum = { + StatusOtpFailureGuest: 'statusOtp.failure.guest' +} as const; +export type StatusOtp400ResponseMsgEnum = typeof StatusOtp400ResponseMsgEnum[keyof typeof StatusOtp400ResponseMsgEnum]; + + +/** + * Check if a given object implements the StatusOtp400Response interface. + */ +export function instanceOfStatusOtp400Response(value: object): value is StatusOtp400Response { + if (!('kind' in value) || value['kind'] === undefined) return false; + if (!('msg' in value) || value['msg'] === undefined) return false; + return true; +} + +export function StatusOtp400ResponseFromJSON(json: any): StatusOtp400Response { + return StatusOtp400ResponseFromJSONTyped(json, false); +} + +export function StatusOtp400ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): StatusOtp400Response { + if (json == null) { + return json; + } + return { + + 'kind': json['kind'], + 'msg': json['msg'], + }; +} + +export function StatusOtp400ResponseToJSON(json: any): StatusOtp400Response { + return StatusOtp400ResponseToJSONTyped(json, false); +} + +export function StatusOtp400ResponseToJSONTyped(value?: StatusOtp400Response | 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/index.ts b/frontend/src/api/generated/models/index.ts index a4059b0..a3cdb3a 100644 --- a/frontend/src/api/generated/models/index.ts +++ b/frontend/src/api/generated/models/index.ts @@ -1,15 +1,19 @@ /* tslint:disable */ /* eslint-disable */ export * from './ChatTest200Response'; +export * from './ChatTest200ResponsePayload'; export * from './DisableOtp200Response'; +export * from './DisableOtp400Response'; export * from './DisableOtp401Response'; export * from './DisableOtp500Response'; export * from './EnableOtp200Response'; export * from './EnableOtp200ResponsePayload'; +export * from './EnableOtp400Response'; export * from './EnableOtp401Response'; export * from './EnableOtp401ResponseAnyOf'; export * from './GetUser200Response'; export * from './GetUser200ResponsePayload'; +export * from './GetUser200ResponsePayloadSelfInfo'; export * from './GetUser403Response'; export * from './GetUser404Response'; export * from './GetUserUserParameter'; @@ -40,5 +44,6 @@ export * from './Signin500Response'; export * from './StatusOtp200Response'; export * from './StatusOtp200ResponseAnyOf'; export * from './StatusOtp200ResponseAnyOf1'; +export * from './StatusOtp200ResponseAnyOfPayload'; export * from './StatusOtp401Response'; export * from './StatusOtp500Response'; diff --git a/frontend/src/auth/index.ts b/frontend/src/auth/index.ts index 57159b7..7807bf9 100644 --- a/frontend/src/auth/index.ts +++ b/frontend/src/auth/index.ts @@ -2,49 +2,54 @@ import { showError } from "@app/toast"; import client from '@app/api'; export type User = { - id: string; - guest: boolean; - name: string; + id: string; + guest: boolean; + name: string; + selfInfo?: { + loginName?: string; + provider_id?: string; + provider_user?: string; + } }; let currentUser: User | null = null; export function getUser(): Readonly | null { - return currentUser; + return currentUser; } export function isLogged(): boolean { - return currentUser !== null; + return currentUser !== null; } export function setUser(newUser: User | null) { - currentUser = newUser; + currentUser = newUser; } export async function updateUser(): Promise | null> { - try { - let res = await client.getUser({ user: 'me' }); + try { + let res = await client.getUser({ user: 'me' }); - if (res.kind === "success") { - setUser(res.payload); - return res.payload; - } else if (res.kind === "failure") { - // well no user :D - setUser(null); - return null; - } else if (res.kind === "notLoggedIn") { - setUser(null); - return null; - } else { - setUser(null); - showError(`unknown response: ${JSON.stringify(res)}`); - return null; - } - } catch (e) { - setUser(null); - showError(`failed to get user: ${e}`); - return null; - } + if (res.kind === "success") { + setUser(res.payload); + return res.payload; + } else if (res.kind === "failure") { + // well no user :D + setUser(null); + return null; + } else if (res.kind === "notLoggedIn") { + setUser(null); + return null; + } else { + setUser(null); + showError(`unknown response: ${JSON.stringify(res)}`); + return null; + } + } catch (e) { + setUser(null); + showError(`failed to get user: ${e}`); + return null; + } } Object.assign(window as any, { getUser, setUser, updateUser, isLogged }); diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts index 5ad7a75..dfab156 100644 --- a/frontend/src/pages/index.ts +++ b/frontend/src/pages/index.ts @@ -4,6 +4,7 @@ import './chat/chat.ts' import './login/login.ts' import './signin/signin.ts' import './ttt/ttt.ts' +import './profile/profile.ts' // ---- Initial load ---- setTitle(""); diff --git a/frontend/src/pages/login/login.ts b/frontend/src/pages/login/login.ts index c98a84c..2cd0a14 100644 --- a/frontend/src/pages/login/login.ts +++ b/frontend/src/pages/login/login.ts @@ -5,14 +5,98 @@ import { type RouteHandlerParams, type RouteHandlerReturn, } from "@app/routing"; -import { showError, showInfo, showSuccess } from "@app/toast"; +import Cookie from "js-cookie"; import authHtml from "./login.html?raw"; import client from "@app/api"; -import { updateUser } from "@app/auth"; -import Cookie from "js-cookie"; -import loggedInHtml from "./alreadyLoggedin.html?raw"; import cuteCat from "./cuteCat.png"; +import loggedInHtml from "./alreadyLoggedin.html?raw"; +import totpHtml from "./totp.html?raw"; import { isNullish } from "@app/utils"; +import { showError, showInfo, showSuccess } from "@app/toast"; +import { updateUser } from "@app/auth"; + +const TOTP_LENGTH = 6; + +async function handleOtp(app: HTMLElement, token: string, returnTo: string | null) { + app.innerHTML = totpHtml; + + const container = app.querySelector("#totp-container")!; + container.innerHTML = ""; + + const inputs: HTMLInputElement[] = []; + + for (let i = 0; i < TOTP_LENGTH; i++) { + const input = document.createElement("input"); + input.maxLength = 1; + input.inputMode = "numeric"; + input.className = + "w-12 h-12 text-center text-xl border border-gray-300 rounded " + + "focus:outline-none focus:ring-2 focus:ring-blue-500"; + + container.appendChild(input); + inputs.push(input); + + // Handle typing a digit + input.addEventListener("input", async () => { + const value = input.value.replace(/\D/g, ""); + input.value = value; + + // Auto-advance when filled + if (value && i < TOTP_LENGTH - 1) { + inputs[i + 1].focus(); + } + await checkComplete(); + }); + + // Handle backspace + input.addEventListener("keydown", (e) => { + if (e.key === "Backspace" && !input.value && i > 0) { + inputs[i - 1].focus(); + } + }); + + // Handle pasting a full code + input.addEventListener("paste", (e: ClipboardEvent) => { + const pasted = e.clipboardData?.getData("text") ?? ""; + const digits = pasted.replace(/\D/g, "").slice(0, TOTP_LENGTH); + + if (digits.length > 1) { + e.preventDefault(); + digits.split("").forEach((d, idx) => { + if (inputs[idx]) inputs[idx].value = d; + }); + if (digits.length === TOTP_LENGTH) checkComplete(); + } + }); + } + + // Check if all digits are entered and then call totpSend + async function checkComplete() { + const code = inputs.map((i) => i.value).join(""); + if (code.length === TOTP_LENGTH && /^[0-9]+$/.test(code)) { + let res = await client.loginOtp({ + loginOtpRequest: { + code, token, + } + }) + + if (res.kind === "success") { + Cookie.set("token", res.payload.token, { + path: "/", + sameSite: "lax", + }); + if (returnTo !== null) navigateTo(returnTo); + else navigateTo("/"); + } + else if (res.kind === "failed") { + showError(`Failed to authenticate: ${res.msg}`); + } + } + } + + inputs[0].focus(); +} + async function handleLogin( _url: string, @@ -67,7 +151,7 @@ async function handleLogin( return showError( "Error while rendering the page: no form found", ); - fLogin.addEventListener("submit", async function (e: SubmitEvent) { + fLogin.addEventListener("submit", async function(e: SubmitEvent) { e.preventDefault(); let form = e.target as HTMLFormElement | null; if (form === null) return showError("Failed to send form..."); @@ -109,8 +193,7 @@ async function handleLogin( break; } case "otpRequired": { - showInfo("Got ask OTP, not yet implemented"); - break; + return await handleOtp(app!, res.payload.token, returnTo); } case "failed": { showError(`Failed to login: ${res.msg}`); diff --git a/frontend/src/pages/login/totp.html b/frontend/src/pages/login/totp.html new file mode 100644 index 0000000..48d3703 --- /dev/null +++ b/frontend/src/pages/login/totp.html @@ -0,0 +1,11 @@ +
+
+

+ Welcome to ft boules +

+
+ +
+
+
+
diff --git a/frontend/src/pages/profile/profile.html b/frontend/src/pages/profile/profile.html new file mode 100644 index 0000000..1c56014 --- /dev/null +++ b/frontend/src/pages/profile/profile.html @@ -0,0 +1,66 @@ +
+
+

Edit Profile

+ + + + +
+ +
+
+ + +
+ + + +
+ + + +
+ + + +
+ + +
+

Two-Factor Authentication (TOTP)

+ +
+ Status: Disabled + +
+ + + + + +
+
+ + +
+ +
+
diff --git a/frontend/src/pages/profile/profile.ts b/frontend/src/pages/profile/profile.ts new file mode 100644 index 0000000..b3d6e77 --- /dev/null +++ b/frontend/src/pages/profile/profile.ts @@ -0,0 +1,159 @@ +import { addRoute, navigateTo, setTitle } from "@app/routing"; +import { showError } from "@app/toast"; +import page from './profile.html?raw' +import { updateUser } from "@app/auth"; +import { isNullish } from "@app/utils"; +import client from "@app/api"; + + +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")!; + + 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; + } + + + // ---- 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 = () => { + secretBox.textContent = `TOTP Secret: ${totpSecret}`; + secretBox.classList.toggle("hidden"); + }; + + // Initialize UI state + refreshTotpUI(); + } + }; +} + + + +addRoute('/profile', route) diff --git a/frontend/src/routing/index.ts b/frontend/src/routing/index.ts index 0c0b9a4..02cd433 100644 --- a/frontend/src/routing/index.ts +++ b/frontend/src/routing/index.ts @@ -46,12 +46,11 @@ export class RouteHandlerData { constructor(url: string, handler: RouteHandler, special_args: Partial) { this.special_args = Object.assign({}, RouteHandlerData.SPECIAL_ARGS_DEFAULT); Object.assign(this.special_args, special_args); - console.log(url, this.special_args); let parsed = RouteHandlerData.parseUrl(url); this.handler = handler; - this.parts = parsed.parts; - this.url = parsed.parts.map((v, i) => v ?? `:${i}`).reduce((p, c) => `${p}/${c}`, ''); + this.parts = parsed.parts.filter(p => p?.length !== 0); + this.url = parsed.parts.filter(p => p?.length !== 0).map((v, i) => v ?? `:${i}`).reduce((p, c) => `${p}/${c}`, ''); this.args = parsed.args; this.orignal_url = parsed.original; } @@ -99,7 +98,7 @@ function urlToParts(url: string): string[] { let parts = trimed.split('/'); if (parts.at(0) === 'app') parts.shift(); - return parts; + return parts.filter(p => p.length !== 0); } function setupRoutes(): [ @@ -190,8 +189,6 @@ export async function handleRoute() { } let user = await updateUser(); - console.log(route_handler); - console.log(user, !route_handler.special_args.bypass_auth, user === null && !route_handler.special_args.bypass_auth); if (user === null && !route_handler.special_args.bypass_auth) return navigateTo(`/login?returnTo=${encodeURIComponent(window.location.pathname)}`) const app = document.getElementById('app')!; diff --git a/frontend/src/routing/special_routes.ts b/frontend/src/routing/special_routes.ts index b0c5ee8..5b7b7f6 100644 --- a/frontend/src/routing/special_routes.ts +++ b/frontend/src/routing/special_routes.ts @@ -2,8 +2,6 @@ import { escapeHTML } from "@app/utils"; import { getRoute, type RouteHandlerParams } from "@app/routing"; export async function route_404(url: string, _args: RouteHandlerParams): Promise { - console.log(`asked about route '${url}: not found'`) - console.log(getRoute()) return `
404 - Not Found

diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 7a6a7e3..00dab97 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -7,6 +7,10 @@ export default defineConfig({ tailwindcss(), tsconfigPaths(), ], + build: { + minify: false, + sourcemap: true, + }, server: { hmr: { protocol: 'ws', diff --git a/src/@shared/src/auth/index.ts b/src/@shared/src/auth/index.ts index 44a1312..dba527a 100644 --- a/src/@shared/src/auth/index.ts +++ b/src/@shared/src/auth/index.ts @@ -14,6 +14,7 @@ const kRouteAuthDone = Symbol('shared-route-auth-done'); type AuthedUser = { id: UserId; name: string; + guest: boolean; }; declare module 'fastify' { @@ -118,7 +119,7 @@ export const authPlugin = fp<{ onlySchema?: boolean }>(async (fastify, { onlySch .clearCookie('token', { path: '/' }) .makeResponse(401, 'notLoggedIn', 'auth.noUser'); } - req.authUser = { id: user.id, name: user.display_name }; + req.authUser = { id: user.id, name: user.name, guest: user.guest }; } catch { return res diff --git a/src/auth/openapi.json b/src/auth/openapi.json index 6522bdf..c34cda3 100644 --- a/src/auth/openapi.json +++ b/src/auth/openapi.json @@ -38,6 +38,32 @@ } } }, + "400": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "failure" + ] + }, + "msg": { + "enum": [ + "disableOtp.failure.guest" + ] + } + } + } + } + } + }, "401": { "description": "Default Response", "content": { @@ -139,6 +165,32 @@ } } }, + "400": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "failure" + ] + }, + "msg": { + "enum": [ + "enableOtp.failure.guest" + ] + } + } + } + } + } + }, "401": { "description": "Default Response", "content": { @@ -846,12 +898,12 @@ "payload": { "type": "object", "required": [ - "url" + "secret" ], "properties": { - "url": { + "secret": { "type": "string", - "description": "The otp url to feed into a 2fa app" + "description": "The otp secret" } } } diff --git a/src/auth/src/routes/disableOtp.ts b/src/auth/src/routes/disableOtp.ts index 2336362..4ef9292 100644 --- a/src/auth/src/routes/disableOtp.ts +++ b/src/auth/src/routes/disableOtp.ts @@ -7,6 +7,7 @@ import { typeResponse, isNullish } from '@shared/utils'; export const DisableOtpRes = { '200': typeResponse('success', 'disableOtp.success'), '500': typeResponse('failure', 'disableOtp.failure.generic'), + '400': typeResponse('failure', 'disableOtp.failure.guest'), }; @@ -18,6 +19,13 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise => { async function(req, res) { void res; if (isNullish(req.authUser)) { return res.makeResponse(500, 'failure', 'disableOtp.failure.generic'); } + if (req.authUser.guest) { + return res.makeResponse( + 400, + 'failure', + 'disableOtp.failure.guest', + ); + } this.db.deleteUserOtpSecret(req.authUser.id); return res.makeResponse(200, 'success', 'disableOtp.success'); }, diff --git a/src/auth/src/routes/enableOtp.ts b/src/auth/src/routes/enableOtp.ts index 0fe2d78..7732c2c 100644 --- a/src/auth/src/routes/enableOtp.ts +++ b/src/auth/src/routes/enableOtp.ts @@ -10,6 +10,7 @@ export const EnableOtpRes = { url: Type.String({ description: 'The otp url to feed into a 2fa app' }), }), '401': typeResponse('failure', ['enableOtp.failure.noUser', 'enableOtp.failure.noSecret']), + '400': typeResponse('failure', ['enableOtp.failure.guest']), }; export type EnableOtpRes = MakeStaticResponse; @@ -21,6 +22,13 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise => { { schema: { response: EnableOtpRes, operationId: 'enableOtp' }, config: { requireAuth: true } }, async function(req, res) { if (isNullish(req.authUser)) { return res.makeResponse(403, 'failure', 'enableOtp.failure.noUser'); } + if (req.authUser.guest) { + return res.makeResponse( + 400, + 'failure', + 'enableOtp.failure.guest', + ); + } const otpSecret = this.db.ensureUserOtpSecret(req.authUser!.id); if (isNullish(otpSecret)) { return res.makeResponse(403, 'failure', 'enableOtp.failure.noSecret'); } diff --git a/src/auth/src/routes/statusOtp.ts b/src/auth/src/routes/statusOtp.ts index 3236619..41d15a7 100644 --- a/src/auth/src/routes/statusOtp.ts +++ b/src/auth/src/routes/statusOtp.ts @@ -2,12 +2,12 @@ import { FastifyPluginAsync } from 'fastify'; import { Type } from 'typebox'; import { isNullish, MakeStaticResponse, typeResponse } from '@shared/utils'; -import { Otp } from '@shared/auth'; - export const StatusOtpRes = { 200: Type.Union([ - typeResponse('success', 'statusOtp.success.enabled', { url: Type.String({ description: 'The otp url to feed into a 2fa app' }) }), + typeResponse('success', 'statusOtp.success.enabled', { + secret: Type.String({ description: 'The otp secret' }), + }), typeResponse('success', 'statusOtp.success.disabled'), ]), 500: typeResponse('failure', 'statusOtp.failure.generic'), @@ -19,13 +19,32 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise => { void _opts; fastify.get( '/api/auth/statusOtp', - { schema: { response: StatusOtpRes, operationId: 'statusOtp' }, config: { requireAuth: true } }, + { + schema: { response: StatusOtpRes, operationId: 'statusOtp' }, + config: { requireAuth: true }, + }, async function(req, res) { - if (isNullish(req.authUser)) { return res.makeResponse(500, 'failure', 'statusOtp.failure.generic'); } + if (isNullish(req.authUser)) { + return res.makeResponse( + 500, + 'failure', + 'statusOtp.failure.generic', + ); + } const otpSecret = this.db.getUserOtpSecret(req.authUser.id); - if (isNullish(otpSecret)) { return res.makeResponse(200, 'success', 'statusOtp.success.disabled'); } - const otp = new Otp({ secret: otpSecret }); - return res.makeResponse(200, 'success', 'statusOtp.success.enabled', { url: otp.totpURL }); + if (isNullish(otpSecret)) { + return res.makeResponse( + 200, + 'success', + 'statusOtp.success.disabled', + ); + } + return res.makeResponse( + 200, + 'success', + 'statusOtp.success.enabled', + { secret: otpSecret }, + ); }, ); }; diff --git a/src/openapi.json b/src/openapi.json index 4327973..4ab32ff 100644 --- a/src/openapi.json +++ b/src/openapi.json @@ -51,6 +51,32 @@ } } }, + "400": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "failure" + ] + }, + "msg": { + "enum": [ + "disableOtp.failure.guest" + ] + } + } + } + } + } + }, "401": { "description": "Default Response", "content": { @@ -155,6 +181,32 @@ } } }, + "400": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "failure" + ] + }, + "msg": { + "enum": [ + "enableOtp.failure.guest" + ] + } + } + } + } + } + }, "401": { "description": "Default Response", "content": { @@ -883,12 +935,12 @@ "payload": { "type": "object", "required": [ - "url" + "secret" ], "properties": { - "url": { + "secret": { "type": "string", - "description": "The otp url to feed into a 2fa app" + "description": "The otp secret" } } } @@ -1069,6 +1121,20 @@ }, "guest": { "type": "boolean" + }, + "selfInfo": { + "type": "object", + "properties": { + "login_name": { + "type": "string" + }, + "provider_id": { + "type": "string" + }, + "provider_user": { + "type": "string" + } + } } } } diff --git a/src/user/openapi.json b/src/user/openapi.json index c785e10..87744dc 100644 --- a/src/user/openapi.json +++ b/src/user/openapi.json @@ -72,6 +72,20 @@ }, "guest": { "type": "boolean" + }, + "selfInfo": { + "type": "object", + "properties": { + "login_name": { + "type": "string" + }, + "provider_id": { + "type": "string" + }, + "provider_user": { + "type": "string" + } + } } } } diff --git a/src/user/src/routes/info.ts b/src/user/src/routes/info.ts index edf7c00..869835e 100644 --- a/src/user/src/routes/info.ts +++ b/src/user/src/routes/info.ts @@ -7,6 +7,11 @@ import { isNullish, MakeStaticResponse, typeResponse } from '@shared/utils'; export const UserInfoRes = { '200': typeResponse('success', 'userinfo.success', { name: Type.String(), id: Type.String(), guest: Type.Boolean(), + selfInfo: Type.Optional(Type.Object({ + login_name: Type.Optional(Type.String()), + provider_id: Type.Optional(Type.String()), + provider_user: Type.Optional(Type.String()), + })), }), '403': typeResponse('failure', 'userinfo.failure.notLoggedIn'), '404': typeResponse('failure', 'userinfo.failure.unknownUser'), @@ -38,6 +43,7 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise => { if (req.params.user === 'me') { req.params.user = req.authUser.id; } + const askSelf = req.params.user === req.authUser.id; const user = this.db.getUser(req.params.user); if (isNullish(user)) { @@ -57,6 +63,11 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise => { // ``` // is the same as `val = !!something` guest: !!user.guest, + selfInfo: askSelf ? { + login_name: user.login, + provider_id: user.provider_name, + provider_user: user.provider_unique, + } : null, }; return res.makeResponse(200, 'success', 'userinfo.success', payload);