feat(users): Adding the profile page and the TOTP connection

Added user profile page (/profile) with display name, password, and TOTP management
Implemented TOTP authentication flow in the login process
Added backend APIs for changing display name and password
Made user display names unique in the database
Removed the entire icons service (server, routes, and Docker configuration)
Added collision-avoidance logic for duplicate display names during user creation
This commit is contained in:
Raphaël 2025-12-10 18:09:53 +01:00 committed by GitHub
commit 492647b817
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
75 changed files with 3555 additions and 1497 deletions

2
.envrc
View file

@ -1 +1 @@
use flake use flake "path:$(realpath -m .)"

View file

@ -48,33 +48,6 @@ services:
gelf-address: "udp://127.0.0.1:12201" gelf-address: "udp://127.0.0.1:12201"
tag: "{{.Name}}" tag: "{{.Name}}"
###############
# ICONS #
###############
icons:
build:
context: ./src/
args:
- SERVICE=icons
#- EXTRA_FILES=icons/extra
container_name: icons
restart: always
networks:
- transcendance-network
volumes:
- images-volume:/volumes/store
- sqlite-volume:/volumes/database
environment:
- JWT_SECRET=KRUGKIDROVUWG2ZAMJZG653OEBTG66BANJ2W24DTEBXXMZLSEB2GQZJANRQXU6JA
- USER_ICONS_STORE=/volumes/store
- DATABASE_DIR=/volumes/database
logging:
driver: gelf
options:
gelf-address: "udp://127.0.0.1:12201"
tag: "{{.Name}}"
############### ###############
# AUTH # # AUTH #
############### ###############

View file

@ -1,15 +1,23 @@
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;
COPY --from=builder /src/dist /dist FROM pnpm_base
COPY --from=builder /src/dist /dist
COPY ./run.sh /bin/run.sh COPY ./run.sh /bin/run.sh
RUN chmod +x /bin/run.sh RUN chmod +x /bin/run.sh

View file

@ -25,9 +25,11 @@
class="fixed top-14 left-0 w-64 h-full bg-gray-900 text-white transform -translate-x-full transition-transform duration-300 ease-in-out z-40"> class="fixed top-14 left-0 w-64 h-full bg-gray-900 text-white transform -translate-x-full transition-transform duration-300 ease-in-out z-40">
<nav class="flex flex-col p-4 space-y-3"> <nav class="flex flex-col p-4 space-y-3">
<a href="/" class="hover:bg-gray-700 rounded-md px-3 py-2">🏠 Home</a> <a href="/" class="hover:bg-gray-700 rounded-md px-3 py-2">🏠 Home</a>
<a href="/login" class="hover:bg-gray-700 rounded-md px-3 py-2">👤 Login</a>
<a href="/signin" class="hover:bg-gray-700 rounded-md px-3 py-2">👤 Signin</a>
<a href="/chat" class="hover:bg-gray-700 rounded-md px-3 py-2">👤 Chat</a> <a href="/chat" class="hover:bg-gray-700 rounded-md px-3 py-2">👤 Chat</a>
<a href="/contact" class="hover:bg-gray-700 rounded-md px-3 py-2">⚙️ Settings</a> <a href="/contact" class="hover:bg-gray-700 rounded-md px-3 py-2">⚙️ Settings</a>
<a href="/404" class="hover:bg-gray-700 rounded-md px-3 py-2">🚪 Logout</a> <a href="/logout" class="hover:bg-gray-700 rounded-md px-3 py-2">🚪 Logout</a>
</nav> </nav>
</aside> </aside>

View file

@ -11,13 +11,14 @@
"devDependencies": { "devDependencies": {
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"vite": "^7.2.6", "vite": "^7.2.7",
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.17", "@tailwindcss/vite": "^4.1.17",
"@types/qrcode": "^1.5.6",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"openapi-fetch": "^0.15.0", "qrcode": "^1.5.4",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"tailwindcss": "^4.1.17" "tailwindcss": "^4.1.17"
} }

266
frontend/pnpm-lock.yaml generated
View file

@ -10,13 +10,16 @@ importers:
dependencies: dependencies:
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.1.17 specifier: ^4.1.17
version: 4.1.17(vite@7.2.6(jiti@2.6.1)(lightningcss@1.30.2)) version: 4.1.17(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2))
'@types/qrcode':
specifier: ^1.5.6
version: 1.5.6
js-cookie: js-cookie:
specifier: ^3.0.5 specifier: ^3.0.5
version: 3.0.5 version: 3.0.5
openapi-fetch: qrcode:
specifier: ^0.15.0 specifier: ^1.5.4
version: 0.15.0 version: 1.5.4
socket.io-client: socket.io-client:
specifier: ^4.8.1 specifier: ^4.8.1
version: 4.8.1 version: 4.8.1
@ -31,11 +34,11 @@ importers:
specifier: ~5.9.3 specifier: ~5.9.3
version: 5.9.3 version: 5.9.3
vite: vite:
specifier: ^7.2.6 specifier: ^7.2.7
version: 7.2.6(jiti@2.6.1)(lightningcss@1.30.2) version: 7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)
vite-tsconfig-paths: vite-tsconfig-paths:
specifier: ^5.1.4 specifier: ^5.1.4
version: 5.1.4(typescript@5.9.3)(vite@7.2.6(jiti@2.6.1)(lightningcss@1.30.2)) version: 5.1.4(typescript@5.9.3)(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2))
packages: packages:
@ -420,6 +423,34 @@ packages:
'@types/js-cookie@3.0.6': '@types/js-cookie@3.0.6':
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
'@types/node@24.10.2':
resolution: {integrity: sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==}
'@types/qrcode@1.5.6':
resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
camelcase@5.3.1:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
cliui@6.0.0:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
debug@4.3.7: debug@4.3.7:
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
@ -438,10 +469,20 @@ packages:
supports-color: supports-color:
optional: true optional: true
decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
detect-libc@2.1.2: detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
dijkstrajs@1.0.3:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
engine.io-client@6.6.3: engine.io-client@6.6.3:
resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==} resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==}
@ -467,17 +508,29 @@ packages:
picomatch: picomatch:
optional: true optional: true
find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
fsevents@2.3.3: fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin] os: [darwin]
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
globrex@0.1.2: globrex@0.1.2:
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
graceful-fs@4.2.11: graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
jiti@2.6.1: jiti@2.6.1:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true hasBin: true
@ -556,6 +609,10 @@ packages:
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
magic-string@0.30.21: magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@ -567,11 +624,21 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
openapi-fetch@0.15.0: p-limit@2.3.0:
resolution: {integrity: sha512-OjQUdi61WO4HYhr9+byCPMj0+bgste/LtSBEcV6FzDdONTs7x0fWn8/ndoYwzqCsKWIxEZwo4FN/TG1c1rI8IQ==} resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
openapi-typescript-helpers@0.0.15: p-locate@4.1.0:
resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==} resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
p-try@2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
picocolors@1.1.1: picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@ -580,15 +647,34 @@ packages:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'} engines: {node: '>=12'}
pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
postcss@8.5.6: postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
qrcode@1.5.4:
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
engines: {node: '>=10.13.0'}
hasBin: true
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
rollup@4.53.3: rollup@4.53.3:
resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'} engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true hasBin: true
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
socket.io-client@4.8.1: socket.io-client@4.8.1:
resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==} resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
@ -601,6 +687,14 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
tailwindcss@4.1.17: tailwindcss@4.1.17:
resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==} resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==}
@ -627,6 +721,9 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
vite-tsconfig-paths@5.1.4: vite-tsconfig-paths@5.1.4:
resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==}
peerDependencies: peerDependencies:
@ -635,8 +732,8 @@ packages:
vite: vite:
optional: true optional: true
vite@7.2.6: vite@7.2.7:
resolution: {integrity: sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==} resolution: {integrity: sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@ -675,6 +772,13 @@ packages:
yaml: yaml:
optional: true optional: true
which-module@2.0.1:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
ws@8.17.1: ws@8.17.1:
resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
@ -691,6 +795,17 @@ packages:
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
yargs-parser@18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'}
yargs@15.4.1:
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
engines: {node: '>=8'}
snapshots: snapshots:
'@esbuild/aix-ppc64@0.25.12': '@esbuild/aix-ppc64@0.25.12':
@ -919,17 +1034,45 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.17 '@tailwindcss/oxide-win32-arm64-msvc': 4.1.17
'@tailwindcss/oxide-win32-x64-msvc': 4.1.17 '@tailwindcss/oxide-win32-x64-msvc': 4.1.17
'@tailwindcss/vite@4.1.17(vite@7.2.6(jiti@2.6.1)(lightningcss@1.30.2))': '@tailwindcss/vite@4.1.17(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2))':
dependencies: dependencies:
'@tailwindcss/node': 4.1.17 '@tailwindcss/node': 4.1.17
'@tailwindcss/oxide': 4.1.17 '@tailwindcss/oxide': 4.1.17
tailwindcss: 4.1.17 tailwindcss: 4.1.17
vite: 7.2.6(jiti@2.6.1)(lightningcss@1.30.2) vite: 7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)
'@types/estree@1.0.8': {} '@types/estree@1.0.8': {}
'@types/js-cookie@3.0.6': {} '@types/js-cookie@3.0.6': {}
'@types/node@24.10.2':
dependencies:
undici-types: 7.16.0
'@types/qrcode@1.5.6':
dependencies:
'@types/node': 24.10.2
ansi-regex@5.0.1: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
camelcase@5.3.1: {}
cliui@6.0.0:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.4: {}
debug@4.3.7: debug@4.3.7:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
@ -938,8 +1081,14 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
decamelize@1.2.0: {}
detect-libc@2.1.2: {} detect-libc@2.1.2: {}
dijkstrajs@1.0.3: {}
emoji-regex@8.0.0: {}
engine.io-client@6.6.3: engine.io-client@6.6.3:
dependencies: dependencies:
'@socket.io/component-emitter': 3.1.2 '@socket.io/component-emitter': 3.1.2
@ -992,13 +1141,22 @@ snapshots:
optionalDependencies: optionalDependencies:
picomatch: 4.0.3 picomatch: 4.0.3
find-up@4.1.0:
dependencies:
locate-path: 5.0.0
path-exists: 4.0.0
fsevents@2.3.3: fsevents@2.3.3:
optional: true optional: true
get-caller-file@2.0.5: {}
globrex@0.1.2: {} globrex@0.1.2: {}
graceful-fs@4.2.11: {} graceful-fs@4.2.11: {}
is-fullwidth-code-point@3.0.0: {}
jiti@2.6.1: {} jiti@2.6.1: {}
js-cookie@3.0.5: {} js-cookie@3.0.5: {}
@ -1052,6 +1210,10 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-arm64-msvc: 1.30.2
lightningcss-win32-x64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
magic-string@0.30.21: magic-string@0.30.21:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
@ -1060,22 +1222,40 @@ snapshots:
nanoid@3.3.11: {} nanoid@3.3.11: {}
openapi-fetch@0.15.0: p-limit@2.3.0:
dependencies: dependencies:
openapi-typescript-helpers: 0.0.15 p-try: 2.2.0
openapi-typescript-helpers@0.0.15: {} p-locate@4.1.0:
dependencies:
p-limit: 2.3.0
p-try@2.2.0: {}
path-exists@4.0.0: {}
picocolors@1.1.1: {} picocolors@1.1.1: {}
picomatch@4.0.3: {} picomatch@4.0.3: {}
pngjs@5.0.0: {}
postcss@8.5.6: postcss@8.5.6:
dependencies: dependencies:
nanoid: 3.3.11 nanoid: 3.3.11
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
qrcode@1.5.4:
dependencies:
dijkstrajs: 1.0.3
pngjs: 5.0.0
yargs: 15.4.1
require-directory@2.1.1: {}
require-main-filename@2.0.0: {}
rollup@4.53.3: rollup@4.53.3:
dependencies: dependencies:
'@types/estree': 1.0.8 '@types/estree': 1.0.8
@ -1104,6 +1284,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.53.3 '@rollup/rollup-win32-x64-msvc': 4.53.3
fsevents: 2.3.3 fsevents: 2.3.3
set-blocking@2.0.0: {}
socket.io-client@4.8.1: socket.io-client@4.8.1:
dependencies: dependencies:
'@socket.io/component-emitter': 3.1.2 '@socket.io/component-emitter': 3.1.2
@ -1124,6 +1306,16 @@ snapshots:
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
tailwindcss@4.1.17: {} tailwindcss@4.1.17: {}
tapable@2.3.0: {} tapable@2.3.0: {}
@ -1139,18 +1331,20 @@ snapshots:
typescript@5.9.3: {} typescript@5.9.3: {}
vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.2.6(jiti@2.6.1)(lightningcss@1.30.2)): undici-types@7.16.0: {}
vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)):
dependencies: dependencies:
debug: 4.4.3 debug: 4.4.3
globrex: 0.1.2 globrex: 0.1.2
tsconfck: 3.1.6(typescript@5.9.3) tsconfck: 3.1.6(typescript@5.9.3)
optionalDependencies: optionalDependencies:
vite: 7.2.6(jiti@2.6.1)(lightningcss@1.30.2) vite: 7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
- typescript - typescript
vite@7.2.6(jiti@2.6.1)(lightningcss@1.30.2): vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2):
dependencies: dependencies:
esbuild: 0.25.12 esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.3) fdir: 6.5.0(picomatch@4.0.3)
@ -1159,10 +1353,40 @@ snapshots:
rollup: 4.53.3 rollup: 4.53.3
tinyglobby: 0.2.15 tinyglobby: 0.2.15
optionalDependencies: optionalDependencies:
'@types/node': 24.10.2
fsevents: 2.3.3 fsevents: 2.3.3
jiti: 2.6.1 jiti: 2.6.1
lightningcss: 1.30.2 lightningcss: 1.30.2
which-module@2.0.1: {}
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
ws@8.17.1: {} ws@8.17.1: {}
xmlhttprequest-ssl@2.1.2: {} xmlhttprequest-ssl@2.1.2: {}
y18n@4.0.3: {}
yargs-parser@18.1.3:
dependencies:
camelcase: 5.3.1
decamelize: 1.2.0
yargs@15.4.1:
dependencies:
cliui: 6.0.0
decamelize: 1.2.0
find-up: 4.1.0
get-caller-file: 2.0.5
require-directory: 2.1.1
require-main-filename: 2.0.0
set-blocking: 2.0.0
string-width: 4.2.3
which-module: 2.0.1
y18n: 4.0.3
yargs-parser: 18.1.3

View file

@ -1,22 +1,35 @@
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/ChangePassword200Response.ts
models/ChangePassword400Response.ts
models/ChangePassword401Response.ts
models/ChangePassword500Response.ts
models/ChangePasswordRequest.ts
models/ChatTest200Response.ts models/ChatTest200Response.ts
models/ChatTest200ResponsePayload.ts
models/DisableOtp200Response.ts models/DisableOtp200Response.ts
models/DisableOtp401Response.ts models/DisableOtp400Response.ts
models/DisableOtp500Response.ts models/DisableOtp500Response.ts
models/EnableOtp200Response.ts models/EnableOtp200Response.ts
models/EnableOtp200ResponsePayload.ts models/EnableOtp200ResponsePayload.ts
models/EnableOtp400Response.ts
models/EnableOtp401Response.ts models/EnableOtp401Response.ts
models/EnableOtp401ResponseAnyOf.ts models/EnableOtp401ResponseAnyOf.ts
models/GetUser200Response.ts models/GetUser200Response.ts
models/GetUser200ResponsePayload.ts models/GetUser200ResponsePayload.ts
models/GetUser200ResponsePayloadSelfInfo.ts
models/GetUser403Response.ts models/GetUser403Response.ts
models/GetUser404Response.ts 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
@ -41,6 +54,7 @@ models/Signin500Response.ts
models/StatusOtp200Response.ts models/StatusOtp200Response.ts
models/StatusOtp200ResponseAnyOf.ts models/StatusOtp200ResponseAnyOf.ts
models/StatusOtp200ResponseAnyOf1.ts models/StatusOtp200ResponseAnyOf1.ts
models/StatusOtp200ResponseAnyOfPayload.ts
models/StatusOtp401Response.ts models/StatusOtp401Response.ts
models/StatusOtp500Response.ts models/StatusOtp500Response.ts
models/index.ts models/index.ts

View file

@ -15,18 +15,29 @@
import * as runtime from '../runtime'; import * as runtime from '../runtime';
import type { import type {
ChangeDisplayName200Response,
ChangeDisplayName400Response,
ChangeDisplayNameRequest,
ChangePassword200Response,
ChangePassword400Response,
ChangePassword401Response,
ChangePassword500Response,
ChangePasswordRequest,
ChatTest200Response, ChatTest200Response,
DisableOtp200Response, DisableOtp200Response,
DisableOtp401Response, DisableOtp400Response,
DisableOtp500Response, DisableOtp500Response,
EnableOtp200Response, EnableOtp200Response,
EnableOtp400Response,
EnableOtp401Response, EnableOtp401Response,
GetUser200Response, GetUser200Response,
GetUser403Response, GetUser403Response,
GetUser404Response, GetUser404Response,
GetUserUserParameter, GetUserUserParameter,
GuestLogin200Response, GuestLogin200Response,
GuestLogin400Response,
GuestLogin500Response, GuestLogin500Response,
GuestLoginRequest,
Login200Response, Login200Response,
Login202Response, Login202Response,
Login400Response, Login400Response,
@ -47,16 +58,34 @@ import type {
StatusOtp500Response, StatusOtp500Response,
} from '../models/index'; } from '../models/index';
import { import {
ChangeDisplayName200ResponseFromJSON,
ChangeDisplayName200ResponseToJSON,
ChangeDisplayName400ResponseFromJSON,
ChangeDisplayName400ResponseToJSON,
ChangeDisplayNameRequestFromJSON,
ChangeDisplayNameRequestToJSON,
ChangePassword200ResponseFromJSON,
ChangePassword200ResponseToJSON,
ChangePassword400ResponseFromJSON,
ChangePassword400ResponseToJSON,
ChangePassword401ResponseFromJSON,
ChangePassword401ResponseToJSON,
ChangePassword500ResponseFromJSON,
ChangePassword500ResponseToJSON,
ChangePasswordRequestFromJSON,
ChangePasswordRequestToJSON,
ChatTest200ResponseFromJSON, ChatTest200ResponseFromJSON,
ChatTest200ResponseToJSON, ChatTest200ResponseToJSON,
DisableOtp200ResponseFromJSON, DisableOtp200ResponseFromJSON,
DisableOtp200ResponseToJSON, DisableOtp200ResponseToJSON,
DisableOtp401ResponseFromJSON, DisableOtp400ResponseFromJSON,
DisableOtp401ResponseToJSON, DisableOtp400ResponseToJSON,
DisableOtp500ResponseFromJSON, DisableOtp500ResponseFromJSON,
DisableOtp500ResponseToJSON, DisableOtp500ResponseToJSON,
EnableOtp200ResponseFromJSON, EnableOtp200ResponseFromJSON,
EnableOtp200ResponseToJSON, EnableOtp200ResponseToJSON,
EnableOtp400ResponseFromJSON,
EnableOtp400ResponseToJSON,
EnableOtp401ResponseFromJSON, EnableOtp401ResponseFromJSON,
EnableOtp401ResponseToJSON, EnableOtp401ResponseToJSON,
GetUser200ResponseFromJSON, GetUser200ResponseFromJSON,
@ -69,8 +98,12 @@ import {
GetUserUserParameterToJSON, GetUserUserParameterToJSON,
GuestLogin200ResponseFromJSON, GuestLogin200ResponseFromJSON,
GuestLogin200ResponseToJSON, GuestLogin200ResponseToJSON,
GuestLogin400ResponseFromJSON,
GuestLogin400ResponseToJSON,
GuestLogin500ResponseFromJSON, GuestLogin500ResponseFromJSON,
GuestLogin500ResponseToJSON, GuestLogin500ResponseToJSON,
GuestLoginRequestFromJSON,
GuestLoginRequestToJSON,
Login200ResponseFromJSON, Login200ResponseFromJSON,
Login200ResponseToJSON, Login200ResponseToJSON,
Login202ResponseFromJSON, Login202ResponseFromJSON,
@ -109,10 +142,22 @@ import {
StatusOtp500ResponseToJSON, StatusOtp500ResponseToJSON,
} from '../models/index'; } from '../models/index';
export interface ChangeDisplayNameOperationRequest {
changeDisplayNameRequest: ChangeDisplayNameRequest;
}
export interface ChangePasswordOperationRequest {
changePasswordRequest: ChangePasswordRequest;
}
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;
} }
@ -130,6 +175,122 @@ 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 | ChangePassword401Response>> {
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) => ChangePassword401ResponseFromJSON(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 | ChangePassword401Response> {
const response = await this.changeDisplayNameRaw(requestParameters, initOverrides);
return await response.value();
}
/**
*/
async changePasswordRaw(requestParameters: ChangePasswordOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<ChangePassword200Response | ChangePassword400Response | ChangePassword401Response | ChangePassword500Response>> {
if (requestParameters['changePasswordRequest'] == null) {
throw new runtime.RequiredError(
'changePasswordRequest',
'Required parameter "changePasswordRequest" was null or undefined when calling changePassword().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
let urlPath = `/api/auth/changePassword`;
const response = await this.request({
path: urlPath,
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: ChangePasswordRequestToJSON(requestParameters['changePasswordRequest']),
}, 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) => ChangePassword200ResponseFromJSON(jsonValue));
}
if (response.status === 400) {
// Object response for status 400
return new runtime.JSONApiResponse(response, (jsonValue) => ChangePassword400ResponseFromJSON(jsonValue));
}
if (response.status === 401) {
// Object response for status 401
return new runtime.JSONApiResponse(response, (jsonValue) => ChangePassword401ResponseFromJSON(jsonValue));
}
if (response.status === 500) {
// Object response for status 500
return new runtime.JSONApiResponse(response, (jsonValue) => ChangePassword500ResponseFromJSON(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, 500`);
}
/**
*/
async changePassword(requestParameters: ChangePasswordOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<ChangePassword200Response | ChangePassword400Response | ChangePassword401Response | ChangePassword500Response> {
const response = await this.changePasswordRaw(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>> {
@ -174,7 +335,7 @@ export class OpenapiOtherApi extends runtime.BaseAPI {
/** /**
*/ */
async disableOtpRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<DisableOtp200Response | DisableOtp401Response | DisableOtp500Response>> { async disableOtpRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<DisableOtp200Response | DisableOtp400Response | ChangePassword401Response | DisableOtp500Response>> {
const queryParameters: any = {}; const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {}; const headerParameters: runtime.HTTPHeaders = {};
@ -197,9 +358,13 @@ export class OpenapiOtherApi extends runtime.BaseAPI {
// Object response for status 200 // Object response for status 200
return new runtime.JSONApiResponse(response, (jsonValue) => DisableOtp200ResponseFromJSON(jsonValue)); 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) { if (response.status === 401) {
// Object response for status 401 // Object response for status 401
return new runtime.JSONApiResponse(response, (jsonValue) => DisableOtp401ResponseFromJSON(jsonValue)); return new runtime.JSONApiResponse(response, (jsonValue) => ChangePassword401ResponseFromJSON(jsonValue));
} }
if (response.status === 500) { if (response.status === 500) {
// Object response for status 500 // Object response for status 500
@ -208,19 +373,19 @@ 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, 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<DisableOtp200Response | DisableOtp401Response | DisableOtp500Response> { async disableOtp(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<DisableOtp200Response | DisableOtp400Response | ChangePassword401Response | DisableOtp500Response> {
const response = await this.disableOtpRaw(initOverrides); const response = await this.disableOtpRaw(initOverrides);
return await response.value(); return await response.value();
} }
/** /**
*/ */
async enableOtpRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<EnableOtp200Response | EnableOtp401Response>> { async enableOtpRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<EnableOtp200Response | EnableOtp400Response | EnableOtp401Response>> {
const queryParameters: any = {}; const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {}; const headerParameters: runtime.HTTPHeaders = {};
@ -243,6 +408,10 @@ export class OpenapiOtherApi extends runtime.BaseAPI {
// Object response for status 200 // Object response for status 200
return new runtime.JSONApiResponse(response, (jsonValue) => EnableOtp200ResponseFromJSON(jsonValue)); 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) { if (response.status === 401) {
// Object response for status 401 // Object response for status 401
return new runtime.JSONApiResponse(response, (jsonValue) => EnableOtp401ResponseFromJSON(jsonValue)); return new runtime.JSONApiResponse(response, (jsonValue) => EnableOtp401ResponseFromJSON(jsonValue));
@ -250,12 +419,12 @@ 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, 401`); throw new runtime.ResponseError(response, `Unexpected status code: ${response.status}. Expected one of: 200, 400, 401`);
} }
/** /**
*/ */
async enableOtp(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<EnableOtp200Response | EnableOtp401Response> { async enableOtp(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<EnableOtp200Response | EnableOtp400Response | EnableOtp401Response> {
const response = await this.enableOtpRaw(initOverrides); const response = await this.enableOtpRaw(initOverrides);
return await response.value(); return await response.value();
} }
@ -320,11 +489,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`;
@ -333,6 +504,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
@ -343,6 +515,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));
@ -350,13 +526,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 ChangePassword200Response
*/
export interface ChangePassword200Response {
/**
*
* @type {string}
* @memberof ChangePassword200Response
*/
kind: ChangePassword200ResponseKindEnum;
/**
*
* @type {string}
* @memberof ChangePassword200Response
*/
msg: ChangePassword200ResponseMsgEnum;
}
/**
* @export
*/
export const ChangePassword200ResponseKindEnum = {
Success: 'success'
} as const;
export type ChangePassword200ResponseKindEnum = typeof ChangePassword200ResponseKindEnum[keyof typeof ChangePassword200ResponseKindEnum];
/**
* @export
*/
export const ChangePassword200ResponseMsgEnum = {
ChangePasswordSuccess: 'changePassword.success'
} as const;
export type ChangePassword200ResponseMsgEnum = typeof ChangePassword200ResponseMsgEnum[keyof typeof ChangePassword200ResponseMsgEnum];
/**
* Check if a given object implements the ChangePassword200Response interface.
*/
export function instanceOfChangePassword200Response(value: object): value is ChangePassword200Response {
if (!('kind' in value) || value['kind'] === undefined) return false;
if (!('msg' in value) || value['msg'] === undefined) return false;
return true;
}
export function ChangePassword200ResponseFromJSON(json: any): ChangePassword200Response {
return ChangePassword200ResponseFromJSONTyped(json, false);
}
export function ChangePassword200ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): ChangePassword200Response {
if (json == null) {
return json;
}
return {
'kind': json['kind'],
'msg': json['msg'],
};
}
export function ChangePassword200ResponseToJSON(json: any): ChangePassword200Response {
return ChangePassword200ResponseToJSONTyped(json, false);
}
export function ChangePassword200ResponseToJSONTyped(value?: ChangePassword200Response | null, ignoreDiscriminator: boolean = false): any {
if (value == null) {
return value;
}
return {
'kind': value['kind'],
'msg': value['msg'],
};
}

View file

@ -0,0 +1,95 @@
/* 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 ChangePassword400Response
*/
export interface ChangePassword400Response {
/**
*
* @type {string}
* @memberof ChangePassword400Response
*/
kind: ChangePassword400ResponseKindEnum;
/**
*
* @type {string}
* @memberof ChangePassword400Response
*/
msg: ChangePassword400ResponseMsgEnum;
}
/**
* @export
*/
export const ChangePassword400ResponseKindEnum = {
Failed: 'failed'
} as const;
export type ChangePassword400ResponseKindEnum = typeof ChangePassword400ResponseKindEnum[keyof typeof ChangePassword400ResponseKindEnum];
/**
* @export
*/
export const ChangePassword400ResponseMsgEnum = {
ChangePasswordFailedToolong: 'changePassword.failed.toolong',
ChangePasswordFailedTooshort: 'changePassword.failed.tooshort',
ChangePasswordFailedInvalid: 'changePassword.failed.invalid'
} as const;
export type ChangePassword400ResponseMsgEnum = typeof ChangePassword400ResponseMsgEnum[keyof typeof ChangePassword400ResponseMsgEnum];
/**
* Check if a given object implements the ChangePassword400Response interface.
*/
export function instanceOfChangePassword400Response(value: object): value is ChangePassword400Response {
if (!('kind' in value) || value['kind'] === undefined) return false;
if (!('msg' in value) || value['msg'] === undefined) return false;
return true;
}
export function ChangePassword400ResponseFromJSON(json: any): ChangePassword400Response {
return ChangePassword400ResponseFromJSONTyped(json, false);
}
export function ChangePassword400ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): ChangePassword400Response {
if (json == null) {
return json;
}
return {
'kind': json['kind'],
'msg': json['msg'],
};
}
export function ChangePassword400ResponseToJSON(json: any): ChangePassword400Response {
return ChangePassword400ResponseToJSONTyped(json, false);
}
export function ChangePassword400ResponseToJSONTyped(value?: ChangePassword400Response | null, ignoreDiscriminator: boolean = false): any {
if (value == null) {
return value;
}
return {
'kind': value['kind'],
'msg': value['msg'],
};
}

View file

@ -0,0 +1,96 @@
/* 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 ChangePassword401Response
*/
export interface ChangePassword401Response {
/**
*
* @type {string}
* @memberof ChangePassword401Response
*/
kind: ChangePassword401ResponseKindEnum;
/**
*
* @type {string}
* @memberof ChangePassword401Response
*/
msg: ChangePassword401ResponseMsgEnum;
}
/**
* @export
*/
export const ChangePassword401ResponseKindEnum = {
NotLoggedIn: 'notLoggedIn'
} as const;
export type ChangePassword401ResponseKindEnum = typeof ChangePassword401ResponseKindEnum[keyof typeof ChangePassword401ResponseKindEnum];
/**
* @export
*/
export const ChangePassword401ResponseMsgEnum = {
AuthNoCookie: 'auth.noCookie',
AuthInvalidKind: 'auth.invalidKind',
AuthNoUser: 'auth.noUser',
AuthInvalid: 'auth.invalid'
} as const;
export type ChangePassword401ResponseMsgEnum = typeof ChangePassword401ResponseMsgEnum[keyof typeof ChangePassword401ResponseMsgEnum];
/**
* Check if a given object implements the ChangePassword401Response interface.
*/
export function instanceOfChangePassword401Response(value: object): value is ChangePassword401Response {
if (!('kind' in value) || value['kind'] === undefined) return false;
if (!('msg' in value) || value['msg'] === undefined) return false;
return true;
}
export function ChangePassword401ResponseFromJSON(json: any): ChangePassword401Response {
return ChangePassword401ResponseFromJSONTyped(json, false);
}
export function ChangePassword401ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): ChangePassword401Response {
if (json == null) {
return json;
}
return {
'kind': json['kind'],
'msg': json['msg'],
};
}
export function ChangePassword401ResponseToJSON(json: any): ChangePassword401Response {
return ChangePassword401ResponseToJSONTyped(json, false);
}
export function ChangePassword401ResponseToJSONTyped(value?: ChangePassword401Response | null, ignoreDiscriminator: boolean = false): any {
if (value == null) {
return value;
}
return {
'kind': value['kind'],
'msg': value['msg'],
};
}

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 ChangePassword500Response
*/
export interface ChangePassword500Response {
/**
*
* @type {string}
* @memberof ChangePassword500Response
*/
kind: ChangePassword500ResponseKindEnum;
/**
*
* @type {string}
* @memberof ChangePassword500Response
*/
msg: ChangePassword500ResponseMsgEnum;
}
/**
* @export
*/
export const ChangePassword500ResponseKindEnum = {
Failed: 'failed'
} as const;
export type ChangePassword500ResponseKindEnum = typeof ChangePassword500ResponseKindEnum[keyof typeof ChangePassword500ResponseKindEnum];
/**
* @export
*/
export const ChangePassword500ResponseMsgEnum = {
ChangePasswordFailedGeneric: 'changePassword.failed.generic'
} as const;
export type ChangePassword500ResponseMsgEnum = typeof ChangePassword500ResponseMsgEnum[keyof typeof ChangePassword500ResponseMsgEnum];
/**
* Check if a given object implements the ChangePassword500Response interface.
*/
export function instanceOfChangePassword500Response(value: object): value is ChangePassword500Response {
if (!('kind' in value) || value['kind'] === undefined) return false;
if (!('msg' in value) || value['msg'] === undefined) return false;
return true;
}
export function ChangePassword500ResponseFromJSON(json: any): ChangePassword500Response {
return ChangePassword500ResponseFromJSONTyped(json, false);
}
export function ChangePassword500ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): ChangePassword500Response {
if (json == null) {
return json;
}
return {
'kind': json['kind'],
'msg': json['msg'],
};
}
export function ChangePassword500ResponseToJSON(json: any): ChangePassword500Response {
return ChangePassword500ResponseToJSONTyped(json, false);
}
export function ChangePassword500ResponseToJSONTyped(value?: ChangePassword500Response | 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 ChangePasswordRequest
*/
export interface ChangePasswordRequest {
/**
*
* @type {string}
* @memberof ChangePasswordRequest
*/
newPassword: string;
}
/**
* Check if a given object implements the ChangePasswordRequest interface.
*/
export function instanceOfChangePasswordRequest(value: object): value is ChangePasswordRequest {
if (!('newPassword' in value) || value['newPassword'] === undefined) return false;
return true;
}
export function ChangePasswordRequestFromJSON(json: any): ChangePasswordRequest {
return ChangePasswordRequestFromJSONTyped(json, false);
}
export function ChangePasswordRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): ChangePasswordRequest {
if (json == null) {
return json;
}
return {
'newPassword': json['new_password'],
};
}
export function ChangePasswordRequestToJSON(json: any): ChangePasswordRequest {
return ChangePasswordRequestToJSONTyped(json, false);
}
export function ChangePasswordRequestToJSONTyped(value?: ChangePasswordRequest | null, ignoreDiscriminator: boolean = false): any {
if (value == null) {
return value;
}
return {
'new_password': value['newPassword'],
};
}

View file

@ -13,13 +13,13 @@
*/ */
import { mapValues } from '../runtime'; import { mapValues } from '../runtime';
import type { GetUser200ResponsePayload } from './GetUser200ResponsePayload'; import type { ChatTest200ResponsePayload } from './ChatTest200ResponsePayload';
import { import {
GetUser200ResponsePayloadFromJSON, ChatTest200ResponsePayloadFromJSON,
GetUser200ResponsePayloadFromJSONTyped, ChatTest200ResponsePayloadFromJSONTyped,
GetUser200ResponsePayloadToJSON, ChatTest200ResponsePayloadToJSON,
GetUser200ResponsePayloadToJSONTyped, ChatTest200ResponsePayloadToJSONTyped,
} from './GetUser200ResponsePayload'; } from './ChatTest200ResponsePayload';
/** /**
* *
@ -41,10 +41,10 @@ export interface ChatTest200Response {
msg: ChatTest200ResponseMsgEnum; msg: ChatTest200ResponseMsgEnum;
/** /**
* *
* @type {GetUser200ResponsePayload} * @type {ChatTest200ResponsePayload}
* @memberof ChatTest200Response * @memberof ChatTest200Response
*/ */
payload: GetUser200ResponsePayload; payload: ChatTest200ResponsePayload;
} }
@ -87,7 +87,7 @@ export function ChatTest200ResponseFromJSONTyped(json: any, ignoreDiscriminator:
'kind': json['kind'], 'kind': json['kind'],
'msg': json['msg'], 'msg': json['msg'],
'payload': GetUser200ResponsePayloadFromJSON(json['payload']), 'payload': ChatTest200ResponsePayloadFromJSON(json['payload']),
}; };
} }
@ -104,7 +104,7 @@ export function ChatTest200ResponseToJSONTyped(value?: ChatTest200Response | nul
'kind': value['kind'], 'kind': value['kind'],
'msg': value['msg'], 'msg': value['msg'],
'payload': GetUser200ResponsePayloadToJSON(value['payload']), 'payload': ChatTest200ResponsePayloadToJSON(value['payload']),
}; };
} }

View file

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

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

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

View file

@ -13,13 +13,6 @@
*/ */
import { mapValues } from '../runtime'; import { mapValues } from '../runtime';
import type { DisableOtp401Response } from './DisableOtp401Response';
import {
DisableOtp401ResponseFromJSON,
DisableOtp401ResponseFromJSONTyped,
DisableOtp401ResponseToJSON,
DisableOtp401ResponseToJSONTyped,
} from './DisableOtp401Response';
import type { EnableOtp401ResponseAnyOf } from './EnableOtp401ResponseAnyOf'; import type { EnableOtp401ResponseAnyOf } from './EnableOtp401ResponseAnyOf';
import { import {
EnableOtp401ResponseAnyOfFromJSON, EnableOtp401ResponseAnyOfFromJSON,
@ -27,6 +20,13 @@ import {
EnableOtp401ResponseAnyOfToJSON, EnableOtp401ResponseAnyOfToJSON,
EnableOtp401ResponseAnyOfToJSONTyped, EnableOtp401ResponseAnyOfToJSONTyped,
} from './EnableOtp401ResponseAnyOf'; } from './EnableOtp401ResponseAnyOf';
import type { ChangePassword401Response } from './ChangePassword401Response';
import {
ChangePassword401ResponseFromJSON,
ChangePassword401ResponseFromJSONTyped,
ChangePassword401ResponseToJSON,
ChangePassword401ResponseToJSONTyped,
} from './ChangePassword401Response';
/** /**
* *

View file

@ -13,6 +13,14 @@
*/ */
import { mapValues } from '../runtime'; import { mapValues } from '../runtime';
import type { GetUser200ResponsePayloadSelfInfo } from './GetUser200ResponsePayloadSelfInfo';
import {
GetUser200ResponsePayloadSelfInfoFromJSON,
GetUser200ResponsePayloadSelfInfoFromJSONTyped,
GetUser200ResponsePayloadSelfInfoToJSON,
GetUser200ResponsePayloadSelfInfoToJSONTyped,
} from './GetUser200ResponsePayloadSelfInfo';
/** /**
* *
* @export * @export
@ -37,6 +45,12 @@ export interface GetUser200ResponsePayload {
* @memberof GetUser200ResponsePayload * @memberof GetUser200ResponsePayload
*/ */
guest: boolean; guest: boolean;
/**
*
* @type {GetUser200ResponsePayloadSelfInfo}
* @memberof GetUser200ResponsePayload
*/
selfInfo?: GetUser200ResponsePayloadSelfInfo;
} }
/** /**
@ -62,6 +76,7 @@ export function GetUser200ResponsePayloadFromJSONTyped(json: any, ignoreDiscrimi
'name': json['name'], 'name': json['name'],
'id': json['id'], 'id': json['id'],
'guest': json['guest'], 'guest': json['guest'],
'selfInfo': json['selfInfo'] == null ? undefined : GetUser200ResponsePayloadSelfInfoFromJSON(json['selfInfo']),
}; };
} }
@ -79,6 +94,7 @@ export function GetUser200ResponsePayloadToJSONTyped(value?: GetUser200ResponseP
'name': value['name'], 'name': value['name'],
'id': value['id'], 'id': value['id'],
'guest': value['guest'], 'guest': value['guest'],
'selfInfo': GetUser200ResponsePayloadSelfInfoToJSON(value['selfInfo']),
}; };
} }

View file

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

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

@ -13,13 +13,6 @@
*/ */
import { mapValues } from '../runtime'; import { mapValues } from '../runtime';
import type { EnableOtp200ResponsePayload } from './EnableOtp200ResponsePayload';
import {
EnableOtp200ResponsePayloadFromJSON,
EnableOtp200ResponsePayloadFromJSONTyped,
EnableOtp200ResponsePayloadToJSON,
EnableOtp200ResponsePayloadToJSONTyped,
} from './EnableOtp200ResponsePayload';
import type { StatusOtp200ResponseAnyOf } from './StatusOtp200ResponseAnyOf'; import type { StatusOtp200ResponseAnyOf } from './StatusOtp200ResponseAnyOf';
import { import {
StatusOtp200ResponseAnyOfFromJSON, StatusOtp200ResponseAnyOfFromJSON,
@ -27,6 +20,13 @@ import {
StatusOtp200ResponseAnyOfToJSON, StatusOtp200ResponseAnyOfToJSON,
StatusOtp200ResponseAnyOfToJSONTyped, StatusOtp200ResponseAnyOfToJSONTyped,
} from './StatusOtp200ResponseAnyOf'; } from './StatusOtp200ResponseAnyOf';
import type { StatusOtp200ResponseAnyOfPayload } from './StatusOtp200ResponseAnyOfPayload';
import {
StatusOtp200ResponseAnyOfPayloadFromJSON,
StatusOtp200ResponseAnyOfPayloadFromJSONTyped,
StatusOtp200ResponseAnyOfPayloadToJSON,
StatusOtp200ResponseAnyOfPayloadToJSONTyped,
} from './StatusOtp200ResponseAnyOfPayload';
import type { StatusOtp200ResponseAnyOf1 } from './StatusOtp200ResponseAnyOf1'; import type { StatusOtp200ResponseAnyOf1 } from './StatusOtp200ResponseAnyOf1';
import { import {
StatusOtp200ResponseAnyOf1FromJSON, StatusOtp200ResponseAnyOf1FromJSON,
@ -55,10 +55,10 @@ export interface StatusOtp200Response {
msg: StatusOtp200ResponseMsgEnum; msg: StatusOtp200ResponseMsgEnum;
/** /**
* *
* @type {EnableOtp200ResponsePayload} * @type {StatusOtp200ResponseAnyOfPayload}
* @memberof StatusOtp200Response * @memberof StatusOtp200Response
*/ */
payload: EnableOtp200ResponsePayload; payload: StatusOtp200ResponseAnyOfPayload;
} }
@ -101,7 +101,7 @@ export function StatusOtp200ResponseFromJSONTyped(json: any, ignoreDiscriminator
'kind': json['kind'], 'kind': json['kind'],
'msg': json['msg'], 'msg': json['msg'],
'payload': EnableOtp200ResponsePayloadFromJSON(json['payload']), 'payload': StatusOtp200ResponseAnyOfPayloadFromJSON(json['payload']),
}; };
} }
@ -118,7 +118,7 @@ export function StatusOtp200ResponseToJSONTyped(value?: StatusOtp200Response | n
'kind': value['kind'], 'kind': value['kind'],
'msg': value['msg'], 'msg': value['msg'],
'payload': EnableOtp200ResponsePayloadToJSON(value['payload']), 'payload': StatusOtp200ResponseAnyOfPayloadToJSON(value['payload']),
}; };
} }

View file

@ -13,13 +13,13 @@
*/ */
import { mapValues } from '../runtime'; import { mapValues } from '../runtime';
import type { EnableOtp200ResponsePayload } from './EnableOtp200ResponsePayload'; import type { StatusOtp200ResponseAnyOfPayload } from './StatusOtp200ResponseAnyOfPayload';
import { import {
EnableOtp200ResponsePayloadFromJSON, StatusOtp200ResponseAnyOfPayloadFromJSON,
EnableOtp200ResponsePayloadFromJSONTyped, StatusOtp200ResponseAnyOfPayloadFromJSONTyped,
EnableOtp200ResponsePayloadToJSON, StatusOtp200ResponseAnyOfPayloadToJSON,
EnableOtp200ResponsePayloadToJSONTyped, StatusOtp200ResponseAnyOfPayloadToJSONTyped,
} from './EnableOtp200ResponsePayload'; } from './StatusOtp200ResponseAnyOfPayload';
/** /**
* *
@ -41,10 +41,10 @@ export interface StatusOtp200ResponseAnyOf {
msg: StatusOtp200ResponseAnyOfMsgEnum; msg: StatusOtp200ResponseAnyOfMsgEnum;
/** /**
* *
* @type {EnableOtp200ResponsePayload} * @type {StatusOtp200ResponseAnyOfPayload}
* @memberof StatusOtp200ResponseAnyOf * @memberof StatusOtp200ResponseAnyOf
*/ */
payload: EnableOtp200ResponsePayload; payload: StatusOtp200ResponseAnyOfPayload;
} }
@ -87,7 +87,7 @@ export function StatusOtp200ResponseAnyOfFromJSONTyped(json: any, ignoreDiscrimi
'kind': json['kind'], 'kind': json['kind'],
'msg': json['msg'], 'msg': json['msg'],
'payload': EnableOtp200ResponsePayloadFromJSON(json['payload']), 'payload': StatusOtp200ResponseAnyOfPayloadFromJSON(json['payload']),
}; };
} }
@ -104,7 +104,7 @@ export function StatusOtp200ResponseAnyOfToJSONTyped(value?: StatusOtp200Respons
'kind': value['kind'], 'kind': value['kind'],
'msg': value['msg'], 'msg': value['msg'],
'payload': EnableOtp200ResponsePayloadToJSON(value['payload']), 'payload': StatusOtp200ResponseAnyOfPayloadToJSON(value['payload']),
}; };
} }

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

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

View file

@ -13,13 +13,13 @@
*/ */
import { mapValues } from '../runtime'; import { mapValues } from '../runtime';
import type { DisableOtp401Response } from './DisableOtp401Response'; import type { ChangePassword401Response } from './ChangePassword401Response';
import { import {
DisableOtp401ResponseFromJSON, ChangePassword401ResponseFromJSON,
DisableOtp401ResponseFromJSONTyped, ChangePassword401ResponseFromJSONTyped,
DisableOtp401ResponseToJSON, ChangePassword401ResponseToJSON,
DisableOtp401ResponseToJSONTyped, ChangePassword401ResponseToJSONTyped,
} from './DisableOtp401Response'; } from './ChangePassword401Response';
/** /**
* *

View file

@ -1,21 +1,34 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export * from './ChangeDisplayName200Response';
export * from './ChangeDisplayName400Response';
export * from './ChangeDisplayNameRequest';
export * from './ChangePassword200Response';
export * from './ChangePassword400Response';
export * from './ChangePassword401Response';
export * from './ChangePassword500Response';
export * from './ChangePasswordRequest';
export * from './ChatTest200Response'; export * from './ChatTest200Response';
export * from './ChatTest200ResponsePayload';
export * from './DisableOtp200Response'; export * from './DisableOtp200Response';
export * from './DisableOtp401Response'; export * from './DisableOtp400Response';
export * from './DisableOtp500Response'; export * from './DisableOtp500Response';
export * from './EnableOtp200Response'; export * from './EnableOtp200Response';
export * from './EnableOtp200ResponsePayload'; export * from './EnableOtp200ResponsePayload';
export * from './EnableOtp400Response';
export * from './EnableOtp401Response'; export * from './EnableOtp401Response';
export * from './EnableOtp401ResponseAnyOf'; export * from './EnableOtp401ResponseAnyOf';
export * from './GetUser200Response'; export * from './GetUser200Response';
export * from './GetUser200ResponsePayload'; export * from './GetUser200ResponsePayload';
export * from './GetUser200ResponsePayloadSelfInfo';
export * from './GetUser403Response'; export * from './GetUser403Response';
export * from './GetUser404Response'; 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';
@ -40,5 +53,6 @@ export * from './Signin500Response';
export * from './StatusOtp200Response'; export * from './StatusOtp200Response';
export * from './StatusOtp200ResponseAnyOf'; export * from './StatusOtp200ResponseAnyOf';
export * from './StatusOtp200ResponseAnyOf1'; export * from './StatusOtp200ResponseAnyOf1';
export * from './StatusOtp200ResponseAnyOfPayload';
export * from './StatusOtp401Response'; export * from './StatusOtp401Response';
export * from './StatusOtp500Response'; export * from './StatusOtp500Response';

View file

@ -2,49 +2,54 @@ import { showError } from "@app/toast";
import client from '@app/api'; import client from '@app/api';
export type User = { export type User = {
id: string; id: string;
guest: boolean; guest: boolean;
name: string; name: string;
selfInfo?: {
loginName?: string;
providerId?: string;
providerUser?: string;
}
}; };
let currentUser: User | null = null; let currentUser: User | null = null;
export function getUser(): Readonly<User> | null { export function getUser(): Readonly<User> | null {
return currentUser; return currentUser;
} }
export function isLogged(): boolean { export function isLogged(): boolean {
return currentUser !== null; return currentUser !== null;
} }
export function setUser(newUser: User | null) { export function setUser(newUser: User | null) {
currentUser = newUser; currentUser = newUser;
} }
export async function updateUser(): Promise<Readonly<User> | null> { export async function updateUser(): Promise<Readonly<User> | null> {
try { try {
let res = await client.getUser({ user: 'me' }); let res = await client.getUser({ user: 'me' });
if (res.kind === "success") { if (res.kind === "success") {
setUser(res.payload); setUser(res.payload);
return res.payload; return res.payload;
} else if (res.kind === "failure") { } else if (res.kind === "failure") {
// well no user :D // well no user :D
setUser(null); setUser(null);
return null; return null;
} else if (res.kind === "notLoggedIn") { } else if (res.kind === "notLoggedIn") {
setUser(null); setUser(null);
return null; return null;
} else { } else {
setUser(null); setUser(null);
showError(`unknown response: ${JSON.stringify(res)}`); showError(`unknown response: ${JSON.stringify(res)}`);
return null; return null;
} }
} catch (e) { } catch (e) {
setUser(null); setUser(null);
showError(`failed to get user: ${e}`); showError(`failed to get user: ${e}`);
return null; return null;
} }
} }
Object.assign(window as any, { getUser, setUser, updateUser, isLogged }); Object.assign(window as any, { getUser, setUser, updateUser, isLogged });

View file

@ -3,6 +3,7 @@ import './root/root.ts'
import './chat/chat.ts' import './chat/chat.ts'
import './login/login.ts' import './login/login.ts'
import './signin/signin.ts' import './signin/signin.ts'
import './profile/profile.ts'
// ---- Initial load ---- // ---- Initial load ----
setTitle(""); setTitle("");

View file

@ -5,14 +5,98 @@ import {
type RouteHandlerParams, type RouteHandlerParams,
type RouteHandlerReturn, type RouteHandlerReturn,
} from "@app/routing"; } from "@app/routing";
import { showError, showInfo, showSuccess } from "@app/toast"; import Cookie from "js-cookie";
import authHtml from "./login.html?raw"; import authHtml from "./login.html?raw";
import client from "@app/api"; 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 cuteCat from "./cuteCat.png";
import loggedInHtml from "./alreadyLoggedin.html?raw";
import totpHtml from "./totp.html?raw";
import { isNullish } from "@app/utils"; 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( async function handleLogin(
_url: string, _url: string,
@ -67,7 +151,7 @@ async function handleLogin(
return showError( return showError(
"Error while rendering the page: no form found", "Error while rendering the page: no form found",
); );
fLogin.addEventListener("submit", async function (e: SubmitEvent) { fLogin.addEventListener("submit", async function(e: SubmitEvent) {
e.preventDefault(); e.preventDefault();
let form = e.target as HTMLFormElement | null; let form = e.target as HTMLFormElement | null;
if (form === null) return showError("Failed to send form..."); if (form === null) return showError("Failed to send form...");
@ -109,8 +193,7 @@ async function handleLogin(
break; break;
} }
case "otpRequired": { case "otpRequired": {
showInfo("Got ask OTP, not yet implemented"); return await handleOtp(app!, res.payload.token, returnTo);
break;
} }
case "failed": { case "failed": {
showError(`Failed to login: ${res.msg}`); showError(`Failed to login: ${res.msg}`);
@ -126,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

@ -0,0 +1,11 @@
<div class="grid h-full place-items-center">
<div class="bg-white shadow-lg rounded-2xl p-8 w-full max-w-md">
<h1 class="text-2xl font-semibold text-center mb-6 text-gray-800">
Welcome to <span>ft boules</span>
</h1>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Code</label>
<div id="totp-container" class="flex gap-2 justify-center"></div>
</div>
</div>
</div>

View file

@ -0,0 +1,77 @@
<div class="grid h-full place-items-center">
<div class="bg-white shadow-lg rounded-2xl p-8 w-full max-w-md">
<h1 class="text-2xl font-semibold mb-6 text-gray-700">Edit Profile</h1>
<div id="isGuestBox" class="border-red-600 rounded-2xl border-2" hidden>
<h2 class="text-2xl font-semibold text-red-600">This is a guest Account</h2>
<span class="text-red-600">You can't change anything here</span>
</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 class="text-red-600">Unknown</span></span>
</div>
<!-- Login Name -->
<div id="loginNameWrapper" class="py-2" hidden>
<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>
<!-- Login Name -->
<div id="providerWrapper" class="py-2 mb-1 border-2 border-green-400 rounded-sm" hidden>
<div class="flex items-center justify-center gap-4">
<div class="flex-1">
<label class="block font-medium mb-1 text-gray-700">Name</label>
<div id="providerNameBox"
class="max-w-md p-3 border border-gray-300 rounded-lg overflow-y-auto bg-white text-gray-800 whitespace-pre-wrap">
</div>
</div>
<div class="flex-1">
<label class="block font-medium mb-1 text-gray-700">User</label>
<div id="providerUserBox"
class="max-w-md p-3 border border-gray-300 rounded-lg overflow-y-auto bg-white text-gray-800 whitespace-pre-wrap">
</div>
</div>
</div>
</div>
<!-- Display Name -->
<div id="displayNameWrapper" class="py-2">
<label class="block font-medium mb-1 text-gray-700">Display Name</label>
<input id="displayNameBox" type="text" placeholder="Display Name" name="DisplayName"
class="w-full px-4 py-2 border border-gray-300 text-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500" />
<button id="displayNameButton"
class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700">Update</button>
</div>
<!-- Password -->
<div id="passwordWrapper" class="py-2" hidden>
<label class="block font-medium mb-1 text-gray-700">Change Password</label>
<input id="passwordBox" type="password" placeholder="New Password" name="Password"
class="w-full px-4 py-2 border border-gray-300 text-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500" />
<button id="passwordButton"
class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700">Update</button>
</div>
<!-- TOTP -->
<div class="border rounded p-4" id="totpWrapper" hidden>
<h2 class="font-semibold text-lg mb-2">Two-Factor Authentication (TOTP)</h2>
<div class="flex items-center justify-between">
<span id="totpStatusText" class="font-medium text-gray-700">Status: Disabled</span>
<div class="flex gap-2">
<button id="enableTotp" type="button"
class="bg-green-600 text-white-700 px-3 py-1 rounded hover:bg-green-700">Enable</button>
<button id="disableTotp" type="button"
class="bg-red-600 text-white-700 px-3 py-1 rounded hover:bg-red-700 hidden">
Disable
</button>
<button id="showSecret" type="button"
class="bg-blue-600 text-white-700 px-3 py-1 rounded hover:bg-blue-700 hidden">
Show Secret
</button>
</div>
</div>
<div id="totpSecretBox" class="mt-3 text-sm bg-gray-100 border p-2 rounded hidden">
<canvas id="totpSecretCanvas" class="w-full h-full block">
</canvas>
<div id="totpSecretText"
class="w-full max-w-md p-3 border border-gray-300 rounded-lg overflow-y-auto bg-white text-gray-800 whitespace-pre-wrap">
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,267 @@
import { addRoute, getRoute, handleRoute, navigateTo, setTitle } from "@app/routing";
import { showError, showSuccess } from "@app/toast";
import page from "./profile.html?raw";
import { updateUser } from "@app/auth";
import { isNullish } from "@app/utils";
import client from "@app/api";
import QRCode from "qrcode";
/*
* Renders an OAuth2-compatible TOTP QR code into a canvas.
*
* @param canvas HTMLCanvasElement to draw into
* @param secret Base32-encoded shared secret
* @param options Meta data for QR (label, issuer, etc.)
*/
export async function renderOAuth2QRCode(
canvas: HTMLCanvasElement,
secret: string,
): Promise<void> {
// Encode the otpauth:// URL
const otpauthUrl = new URL(`otpauth://totp/ft_boule:totp`);
otpauthUrl.searchParams.set("secret", secret.replace(/=+$/, ""));
otpauthUrl.searchParams.set("issuer", "ft_boule");
// Render QR code into the canvas
await QRCode.toCanvas(canvas, otpauthUrl.toString(), {
margin: 1,
scale: 5,
});
canvas.style.width = "";
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 }) {
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<HTMLDivElement>("#isGuestBox")!;
let displayNameWrapper = app.querySelector<HTMLDivElement>(
"#displayNameWrapper",
)!;
let displayNameBox =
app.querySelector<HTMLInputElement>("#displayNameBox")!;
let displayNameButton =
app.querySelector<HTMLButtonElement>("#displayNameButton")!;
let loginNameWrapper =
app.querySelector<HTMLDivElement>("#loginNameWrapper")!;
let loginNameBox =
app.querySelector<HTMLDivElement>("#loginNameBox")!;
let passwordWrapper =
app.querySelector<HTMLDivElement>("#passwordWrapper")!;
let passwordBox =
app.querySelector<HTMLInputElement>("#passwordBox")!;
let passwordButton =
app.querySelector<HTMLButtonElement>("#passwordButton")!;
let providerWrapper =
app.querySelector<HTMLDivElement>("#providerWrapper")!;
let providerNameBox =
app.querySelector<HTMLDivElement>("#providerNameBox")!;
let providerUserBox =
app.querySelector<HTMLDivElement>("#providerUserBox")!;
let accountTypeBox =
app.querySelector<HTMLDivElement>("#accountType")!;
displayNameBox.value = user.name;
guestBox.hidden = !user.guest;
// ---- DOM Elements ----
const totpStatusText = app.querySelector("#totpStatusText")!;
const enableBtn =
app.querySelector<HTMLButtonElement>("#enableTotp")!;
const disableBtn =
app.querySelector<HTMLButtonElement>("#disableTotp")!;
const showSecretBtn =
app.querySelector<HTMLButtonElement>("#showSecret")!;
const secretBox = app.querySelector("#totpSecretBox")!;
const secretText =
app.querySelector<HTMLDivElement>("#totpSecretText")!;
const secretCanvas =
app.querySelector<HTMLCanvasElement>("#totpSecretCanvas")!;
let totpWrapper =
app.querySelector<HTMLDivElement>("#totpWrapper")!;
if (user.guest) {
for (let c of passwordButton.classList.values()) {
if (c.startsWith("bg-") || c.startsWith("hover:bg-"))
passwordButton.classList.remove(c);
}
}
if (user.guest) {
removeBgColor(
passwordButton,
displayNameButton,
enableBtn,
disableBtn,
showSecretBtn,
);
passwordButton.classList.add(
"bg-gray-700",
"hover:bg-gray-700",
);
passwordBox.disabled = true;
passwordBox.classList.add("color-white");
displayNameButton.disabled = true;
displayNameButton.classList.add("bg-gray-700", "color-white");
displayNameBox.disabled = true;
displayNameBox.classList.add("color-white");
enableBtn.classList.add("bg-gray-700", "hover:bg-gray-700");
disableBtn.classList.add("bg-gray-700", "hover:bg-gray-700");
showSecretBtn.classList.add("bg-gray-700", "hover:bg-gray-700");
enableBtn.disabled = true;
disableBtn.disabled = true;
showSecretBtn.disabled = true;
accountTypeBox.innerText = "Guest";
} else if (!isNullish(user.selfInfo?.loginName)) {
loginNameWrapper.hidden = false;
loginNameBox.innerText = user.selfInfo.loginName;
totpWrapper.hidden = false;
passwordWrapper.hidden = false;
accountTypeBox.innerText = "Normal";
} else if (
!isNullish(user.selfInfo?.providerId) &&
!isNullish(user.selfInfo?.providerUser)
) {
providerWrapper.hidden = false;
providerNameBox.innerText = user.selfInfo.providerId;
providerUserBox.innerText = user.selfInfo.providerUser;
enableBtn.classList.add("bg-gray-700", "hover:bg-gray-700");
disableBtn.classList.add("bg-gray-700", "hover:bg-gray-700");
showSecretBtn.classList.add("bg-gray-700", "hover:bg-gray-700");
enableBtn.disabled = true;
disableBtn.disabled = true;
showSecretBtn.disabled = true;
removeBgColor(enableBtn, disableBtn, showSecretBtn);
passwordWrapper.hidden = true;
totpWrapper.hidden = true;
accountTypeBox.innerText = "Provider";
}
// ---- Update UI ----
function refreshTotpUI() {
if (totpEnabled) {
totpStatusText.textContent = "Status: Enabled";
enableBtn.classList.add("hidden");
disableBtn.classList.remove("hidden");
showSecretBtn.classList.remove("hidden");
} else {
totpStatusText.textContent = "Status: Disabled";
enableBtn.classList.remove("hidden");
disableBtn.classList.add("hidden");
showSecretBtn.classList.add("hidden");
secretBox.classList.add("hidden");
}
}
// ---- Button Events ----
enableBtn.onclick = async () => {
let res = await client.enableOtp();
if (res.kind === "success") {
navigateTo(url);
} else {
showError(`failed to activate OTP: ${res.msg}`);
}
};
disableBtn.onclick = async () => {
let res = await client.disableOtp();
if (res.kind === "success") {
navigateTo(url);
} else {
showError(`failed to deactivate OTP: ${res.msg}`);
}
};
showSecretBtn.onclick = () => {
if (!isNullish(totpSecret)) {
secretText.textContent = totpSecret;
renderOAuth2QRCode(secretCanvas, totpSecret);
}
secretBox.classList.toggle("hidden");
};
displayNameButton.onclick = async () => {
let req = await client.changeDisplayName({
changeDisplayNameRequest: {
name: displayNameBox.value,
},
});
if (req.kind === "success") {
showSuccess("Successfully changed display name");
handleRoute();
} else {
showError(`Failed to update: ${req.msg}`);
}
};
passwordButton.onclick = async () => {
let req = await client.changePassword({
changePasswordRequest: {
newPassword: passwordBox.value,
},
});
if (req.kind === "success") {
showSuccess("Successfully changed password");
handleRoute();
} else {
showError(`Failed to update: ${req.msg}`);
}
};
// Initialize UI state
refreshTotpUI();
},
};
}
addRoute("/profile", route);

View file

@ -46,12 +46,11 @@ export class RouteHandlerData {
constructor(url: string, handler: RouteHandler, special_args: Partial<RouteHandlerSpecialArgs>) { constructor(url: string, handler: RouteHandler, special_args: Partial<RouteHandlerSpecialArgs>) {
this.special_args = Object.assign({}, RouteHandlerData.SPECIAL_ARGS_DEFAULT); this.special_args = Object.assign({}, RouteHandlerData.SPECIAL_ARGS_DEFAULT);
Object.assign(this.special_args, special_args); Object.assign(this.special_args, special_args);
console.log(url, this.special_args);
let parsed = RouteHandlerData.parseUrl(url); let parsed = RouteHandlerData.parseUrl(url);
this.handler = handler; this.handler = handler;
this.parts = parsed.parts; this.parts = parsed.parts.filter(p => p?.length !== 0);
this.url = parsed.parts.map((v, i) => v ?? `:${i}`).reduce((p, c) => `${p}/${c}`, ''); this.url = parsed.parts.filter(p => p?.length !== 0).map((v, i) => v ?? `:${i}`).reduce((p, c) => `${p}/${c}`, '');
this.args = parsed.args; this.args = parsed.args;
this.orignal_url = parsed.original; this.orignal_url = parsed.original;
} }
@ -99,7 +98,7 @@ function urlToParts(url: string): string[] {
let parts = trimed.split('/'); let parts = trimed.split('/');
if (parts.at(0) === 'app') if (parts.at(0) === 'app')
parts.shift(); parts.shift();
return parts; return parts.filter(p => p.length !== 0);
} }
function setupRoutes(): [ function setupRoutes(): [
@ -190,8 +189,6 @@ export async function handleRoute() {
} }
let user = await updateUser(); 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) if (user === null && !route_handler.special_args.bypass_auth)
return navigateTo(`/login?returnTo=${encodeURIComponent(window.location.pathname)}`) return navigateTo(`/login?returnTo=${encodeURIComponent(window.location.pathname)}`)
const app = document.getElementById('app')!; const app = document.getElementById('app')!;

View file

@ -2,8 +2,6 @@ import { escapeHTML } from "@app/utils";
import { getRoute, type RouteHandlerParams } from "@app/routing"; import { getRoute, type RouteHandlerParams } from "@app/routing";
export async function route_404(url: string, _args: RouteHandlerParams): Promise<string> { export async function route_404(url: string, _args: RouteHandlerParams): Promise<string> {
console.log(`asked about route '${url}: not found'`)
console.log(getRoute())
return ` return `
<div> 404 - Not Found </div> <div> 404 - Not Found </div>
<hr /> <hr />

View file

@ -7,6 +7,10 @@ export default defineConfig({
tailwindcss(), tailwindcss(),
tsconfigPaths(), tsconfigPaths(),
], ],
build: {
minify: false,
sourcemap: true,
},
server: { server: {
hmr: { hmr: {
protocol: 'ws', protocol: 'ws',

View file

@ -3,6 +3,10 @@ location /app {
try_files /index.html =404; try_files /index.html =404;
} }
location /assets { location /assets {
root /volumes/static/app/; root /volumes/static/app/;
} }
location / {
return 301 https://$http_host/app;
}

View file

@ -25,6 +25,6 @@
}, },
"devDependencies": { "devDependencies": {
"@types/better-sqlite3": "^7.6.13", "@types/better-sqlite3": "^7.6.13",
"@types/node": "^22.19.1" "@types/node": "^22.19.2"
} }
} }

View file

@ -14,6 +14,7 @@ const kRouteAuthDone = Symbol('shared-route-auth-done');
type AuthedUser = { type AuthedUser = {
id: UserId; id: UserId;
name: string; name: string;
guest: boolean;
}; };
declare module 'fastify' { declare module 'fastify' {
@ -118,7 +119,7 @@ export const authPlugin = fp<{ onlySchema?: boolean }>(async (fastify, { onlySch
.clearCookie('token', { path: '/' }) .clearCookie('token', { path: '/' })
.makeResponse(401, 'notLoggedIn', 'auth.noUser'); .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 { catch {
return res return res

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

@ -8,6 +8,140 @@
"schemas": {} "schemas": {}
}, },
"paths": { "paths": {
"/api/auth/changePassword": {
"post": {
"operationId": "changePassword",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"new_password"
],
"properties": {
"new_password": {
"type": "string"
}
}
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"success"
]
},
"msg": {
"enum": [
"changePassword.success"
]
}
}
}
}
}
},
"400": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"failed"
]
},
"msg": {
"enum": [
"changePassword.failed.toolong",
"changePassword.failed.tooshort",
"changePassword.failed.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"
]
}
}
}
}
}
},
"500": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"failed"
]
},
"msg": {
"enum": [
"changePassword.failed.generic"
]
}
}
}
}
}
}
}
}
},
"/api/auth/disableOtp": { "/api/auth/disableOtp": {
"put": { "put": {
"operationId": "disableOtp", "operationId": "disableOtp",
@ -38,6 +172,32 @@
} }
} }
}, },
"400": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"failure"
]
},
"msg": {
"enum": [
"disableOtp.failure.guest"
]
}
}
}
}
}
},
"401": { "401": {
"description": "Default Response", "description": "Default Response",
"content": { "content": {
@ -139,6 +299,32 @@
} }
} }
}, },
"400": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"failure"
]
},
"msg": {
"enum": [
"enableOtp.failure.guest"
]
}
}
}
}
}
},
"401": { "401": {
"description": "Default Response", "description": "Default Response",
"content": { "content": {
@ -278,6 +464,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",
@ -318,6 +518,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": {
@ -846,12 +1072,12 @@
"payload": { "payload": {
"type": "object", "type": "object",
"required": [ "required": [
"url" "secret"
], ],
"properties": { "properties": {
"url": { "secret": {
"type": "string", "type": "string",
"description": "The otp url to feed into a 2fa app" "description": "The otp secret"
} }
} }
} }

View file

@ -30,9 +30,9 @@
"typebox": "^1.0.61" "typebox": "^1.0.61"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.19.1", "@types/node": "^22.19.2",
"rollup-plugin-node-externals": "^8.1.2", "rollup-plugin-node-externals": "^8.1.2",
"vite": "^7.2.6", "vite": "^7.2.7",
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4"
} }
} }

View file

@ -0,0 +1,44 @@
import { FastifyPluginAsync } from 'fastify';
import { Static, Type } from 'typebox';
import { typeResponse, MakeStaticResponse } from '@shared/utils';
const ChangePasswordReq = Type.Object({
new_password: Type.String(),
});
type ChangePasswordReq = Static<typeof ChangePasswordReq>;
const ChangePasswordRes = {
'500': typeResponse('failed',
'changePassword.failed.generic'),
'400': typeResponse('failed', [
'changePassword.failed.toolong',
'changePassword.failed.tooshort',
'changePassword.failed.invalid',
]),
'200': typeResponse('success', 'changePassword.success'),
};
type ChangePasswordRes = MakeStaticResponse<typeof ChangePasswordRes>;
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
void _opts;
fastify.post<{ Body: ChangePasswordReq }>(
'/api/auth/changePassword',
{ schema: { body: ChangePasswordReq, response: ChangePasswordRes, operationId: 'changePassword' }, config: { requireAuth: true } },
async function(req, res) {
const password = req.body.new_password;
if (password.length < 8) { return res.makeResponse(400, 'failed', 'changePassword.failed.tooshort'); }
if (password.length > 64) { return res.makeResponse(400, 'failed', 'changePassword.failed.toolong'); }
// password is good too !
await this.db.setUserPassword(req.authUser!.id, password);
return res.makeResponse(200, 'success', 'changePassword.success');
},
);
};
export default route;

View file

@ -7,6 +7,7 @@ import { typeResponse, isNullish } from '@shared/utils';
export const DisableOtpRes = { export const DisableOtpRes = {
'200': typeResponse('success', 'disableOtp.success'), '200': typeResponse('success', 'disableOtp.success'),
'500': typeResponse('failure', 'disableOtp.failure.generic'), '500': typeResponse('failure', 'disableOtp.failure.generic'),
'400': typeResponse('failure', 'disableOtp.failure.guest'),
}; };
@ -18,6 +19,13 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
async function(req, res) { async function(req, res) {
void res; void res;
if (isNullish(req.authUser)) { return res.makeResponse(500, 'failure', 'disableOtp.failure.generic'); } 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); this.db.deleteUserOtpSecret(req.authUser.id);
return res.makeResponse(200, 'success', 'disableOtp.success'); return res.makeResponse(200, 'success', 'disableOtp.success');
}, },

View file

@ -10,6 +10,7 @@ export const EnableOtpRes = {
url: Type.String({ description: 'The otp url to feed into a 2fa app' }), url: Type.String({ description: 'The otp url to feed into a 2fa app' }),
}), }),
'401': typeResponse('failure', ['enableOtp.failure.noUser', 'enableOtp.failure.noSecret']), '401': typeResponse('failure', ['enableOtp.failure.noUser', 'enableOtp.failure.noSecret']),
'400': typeResponse('failure', ['enableOtp.failure.guest']),
}; };
export type EnableOtpRes = MakeStaticResponse<typeof EnableOtpRes>; export type EnableOtpRes = MakeStaticResponse<typeof EnableOtpRes>;
@ -21,6 +22,13 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
{ schema: { response: EnableOtpRes, operationId: 'enableOtp' }, config: { requireAuth: true } }, { schema: { response: EnableOtpRes, operationId: 'enableOtp' }, config: { requireAuth: true } },
async function(req, res) { async function(req, res) {
if (isNullish(req.authUser)) { return res.makeResponse(403, 'failure', 'enableOtp.failure.noUser'); } 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); const otpSecret = this.db.ensureUserOtpSecret(req.authUser!.id);
if (isNullish(otpSecret)) { return res.makeResponse(403, 'failure', 'enableOtp.failure.noSecret'); } if (isNullish(otpSecret)) { return res.makeResponse(403, 'failure', 'enableOtp.failure.noSecret'); }

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 noun = getRandomFromList(fastify.words.nouns); const adjective = getRandomFromList(
fastify.words.adjectives,
);
const noun = getRandomFromList(fastify.words.nouns);
user_name = `${adjective}${noun}`;
}
else {
if (user_name.length < 4 || user_name.length > 26) {
return res.makeResponse(
400,
'failed',
'guestLogin.failed.invalid',
);
}
if (!USERNAME_CHECK.test(user_name)) {
return res.makeResponse(
400,
'failed',
'guestLogin.failed.invalid',
);
}
user_name = `g_${user_name}`;
}
const user = await this.db.createGuestUser(`${adjective} ${noun}`); const orig = user_name;
let i = 0;
while (
this.db.getUserFromDisplayName(user_name) !== undefined &&
i++ < 5
) {
user_name = `${orig}${Date.now() % 1000}`;
}
if (this.db.getUserFromDisplayName(user_name) !== undefined) {
user_name = `${orig}${Date.now()}`;
}
const user = await this.db.createGuestUser(user_name);
if (isNullish(user)) { 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

@ -2,12 +2,12 @@ import { FastifyPluginAsync } from 'fastify';
import { Type } from 'typebox'; import { Type } from 'typebox';
import { isNullish, MakeStaticResponse, typeResponse } from '@shared/utils'; import { isNullish, MakeStaticResponse, typeResponse } from '@shared/utils';
import { Otp } from '@shared/auth';
export const StatusOtpRes = { export const StatusOtpRes = {
200: Type.Union([ 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'), typeResponse('success', 'statusOtp.success.disabled'),
]), ]),
500: typeResponse('failure', 'statusOtp.failure.generic'), 500: typeResponse('failure', 'statusOtp.failure.generic'),
@ -19,13 +19,32 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
void _opts; void _opts;
fastify.get( fastify.get(
'/api/auth/statusOtp', '/api/auth/statusOtp',
{ schema: { response: StatusOtpRes, operationId: 'statusOtp' }, config: { requireAuth: true } }, {
schema: { response: StatusOtpRes, operationId: 'statusOtp' },
config: { requireAuth: true },
},
async function(req, res) { 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); const otpSecret = this.db.getUserOtpSecret(req.authUser.id);
if (isNullish(otpSecret)) { return res.makeResponse(200, 'success', 'statusOtp.success.disabled'); } if (isNullish(otpSecret)) {
const otp = new Otp({ secret: otpSecret }); return res.makeResponse(
return res.makeResponse(200, 'success', 'statusOtp.success.enabled', { url: otp.totpURL }); 200,
'success',
'statusOtp.success.disabled',
);
}
return res.makeResponse(
200,
'success',
'statusOtp.success.enabled',
{ secret: otpSecret },
);
}, },
); );
}; };

View file

@ -30,9 +30,9 @@
"typebox": "^1.0.61" "typebox": "^1.0.61"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.19.1", "@types/node": "^22.19.2",
"rollup-plugin-node-externals": "^8.1.2", "rollup-plugin-node-externals": "^8.1.2",
"vite": "^7.2.6", "vite": "^7.2.7",
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4"
} }
} }

View file

@ -1,2 +0,0 @@
/dist
/node_modules

View file

@ -1,10 +0,0 @@
#!/bin/sh
set -e
set -x
# do anything here
cp -r /extra /files
# run the CMD [ ... ] from the dockerfile
exec "$@"

View file

@ -1,37 +0,0 @@
{
"type": "module",
"private": false,
"name": "icons",
"version": "1.0.0",
"description": "This project was bootstrapped with Fastify-CLI.",
"main": "app.ts",
"directories": {
"test": "test"
},
"scripts": {
"start": "npm run build && node dist/run.js",
"build": "vite build",
"build:prod": "vite build --outDir=/dist --minify=true --sourcemap=false"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@fastify/autoload": "^6.3.1",
"@fastify/formbody": "^8.0.2",
"@fastify/multipart": "^9.3.0",
"@fastify/sensible": "^6.0.4",
"@fastify/static": "^8.3.0",
"fastify": "^5.6.2",
"fastify-cli": "^7.4.1",
"fastify-plugin": "^5.1.0",
"raw-body": "^3.0.2",
"sharp": "^0.34.5"
},
"devDependencies": {
"@types/node": "^22.19.1",
"rollup-plugin-node-externals": "^8.1.2",
"vite": "^7.2.6",
"vite-tsconfig-paths": "^5.1.4"
}
}

View file

@ -1,54 +0,0 @@
import { FastifyPluginAsync } from 'fastify';
import fastifyFormBody from '@fastify/formbody';
import fastifyMultipart from '@fastify/multipart';
import { mkdir } from 'node:fs/promises';
import fp from 'fastify-plugin';
import * as db from '@shared/database';
import * as utils from '@shared/utils';
import { authPlugin, jwtPlugin } from '@shared/auth';
// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this...
const plugins = import.meta.glob('./plugins/**/*.ts', { eager: true });
// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this...
const routes = import.meta.glob('./routes/**/*.ts', { eager: true });
// When using .decorate you have to specify added properties for Typescript
declare module 'fastify' {
export interface FastifyInstance {
image_store: string;
}
}
const app: FastifyPluginAsync = async (
fastify,
_opts,
): Promise<void> => {
void _opts;
// Place here your custom code!
for (const plugin of Object.values(plugins)) {
void fastify.register(plugin as FastifyPluginAsync, {});
}
for (const route of Object.values(routes)) {
void fastify.register(route as FastifyPluginAsync, {});
}
await fastify.register(utils.useMonitoring);
await fastify.register(db.useDatabase as FastifyPluginAsync, {});
await fastify.register(authPlugin as FastifyPluginAsync, {});
await fastify.register(jwtPlugin as FastifyPluginAsync, {});
void fastify.register(fastifyFormBody, {});
void fastify.register(fastifyMultipart, {});
// The use of fastify-plugin is required to be able
// to export the decorators to the outer scope
void fastify.register(fp(async (fastify2) => {
const image_store = process.env.USER_ICONS_STORE ?? '/tmp/icons';
fastify2.decorate('image_store', image_store);
await mkdir(fastify2.image_store, { recursive: true });
}));
};
export default app;
export { app };

View file

@ -1,16 +0,0 @@
# Plugins Folder
Plugins define behavior that is common to all the routes in your
application. Authentication, caching, templates, and all the other cross
cutting concerns should be handled by plugins placed in this folder.
Files in this folder are typically defined through the
[`fastify-plugin`](https://github.com/fastify/fastify-plugin) module,
making them non-encapsulated. They can define decorators and set hooks
that will then be used in the rest of your application.
Check out:
* [The hitchhiker's guide to plugins](https://fastify.dev/docs/latest/Guides/Plugins-Guide/)
* [Fastify decorators](https://fastify.dev/docs/latest/Reference/Decorators/).
* [Fastify lifecycle](https://fastify.dev/docs/latest/Reference/Lifecycle/).

View file

@ -1,11 +0,0 @@
import fp from 'fastify-plugin';
import sensible, { FastifySensibleOptions } from '@fastify/sensible';
/**
* This plugins adds some utilities to handle http errors
*
* @see https://github.com/fastify/fastify-sensible
*/
export default fp<FastifySensibleOptions>(async (fastify) => {
fastify.register(sensible);
});

View file

@ -1,51 +0,0 @@
import { FastifyPluginAsync } from 'fastify';
import { join } from 'node:path';
import { open } from 'node:fs/promises';
import sharp from 'sharp';
import rawBody from 'raw-body';
import { isNullish } from '@shared/utils';
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
void _opts;
// await fastify.register(authMethod, {});
// here we register plugins that will be active for the current fastify instance (aka everything in this function)
// we register a route handler for: `/<USERID_HERE>`
// it sets some configuration options, and set the actual function that will handle the request
fastify.addContentTypeParser('*', function(request, payload, done) {
done(null);
});
fastify.post<{ Params: { userid: string } }>('/:userid', async function(request, reply) {
const buffer = await rawBody(request.raw);
// this is how we get the `:userid` part of things
const userid: string | undefined = (request.params)['userid'];
if (isNullish(userid)) {
return await reply.code(403);
}
const image_store: string = fastify.getDecorator('image_store');
const image_path = join(image_store, userid);
try {
const img = sharp(buffer);
img.resize({
height: 128,
width: 128,
fit: 'fill',
});
const data = await img.png({ compressionLevel: 6 }).toBuffer();
const image_file = await open(image_path, 'w', 0o666);
await image_file.write(data);
await image_file.close();
}
catch (e) {
fastify.log.error(`Error: ${e}`);
reply.code(400);
return { status: 'error' };
}
});
};
export default route;

View file

@ -1,35 +0,0 @@
// this sould only be used by the docker file !
import fastify, { FastifyInstance } from 'fastify';
import app from './app';
const start = async () => {
const envToLogger = {
development: {
transport: {
target: 'pino-pretty',
options: {
translateTime: 'HH:MM:ss Z',
ignore: 'pid,hostname',
},
},
},
production: true,
test: false,
};
const f: FastifyInstance = fastify({ logger: envToLogger.development });
process.on('SIGTERM', () => {
f.log.info('Requested to shutdown');
process.exit(134);
});
try {
await f.register(app, {});
await f.listen({ port: 80, host: '0.0.0.0' });
}
catch (err) {
f.log.error(err);
process.exit(1);
}
};
start();

View file

@ -1,5 +0,0 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {},
"include": ["src/**/*.ts"]
}

View file

@ -1,51 +0,0 @@
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
import nodeExternals from 'rollup-plugin-node-externals';
import path from 'node:path';
import fs from 'node:fs';
function collectDeps(...pkgJsonPaths) {
const allDeps = new Set();
for (const pkgPath of pkgJsonPaths) {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
for (const dep of Object.keys(pkg.dependencies || {})) {
allDeps.add(dep);
}
for (const peer of Object.keys(pkg.peerDependencies || {})) {
allDeps.add(peer);
}
}
return Array.from(allDeps);
}
const externals = collectDeps(
'./package.json',
'../@shared/package.json',
);
export default defineConfig({
root: __dirname,
// service root
plugins: [tsconfigPaths(), nodeExternals()],
build: {
ssr: true,
outDir: 'dist',
emptyOutDir: true,
lib: {
entry: path.resolve(__dirname, 'src/run.ts'),
// adjust main entry
formats: ['cjs'],
// CommonJS for Node.js
fileName: () => 'index.js',
},
rollupOptions: {
external: externals,
},
target: 'node22',
// or whatever Node version you use
sourcemap: false,
minify: true,
// for easier debugging
},
});

View file

@ -21,6 +21,143 @@
} }
], ],
"paths": { "paths": {
"/api/auth/changePassword": {
"post": {
"operationId": "changePassword",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"new_password"
],
"properties": {
"new_password": {
"type": "string"
}
}
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"success"
]
},
"msg": {
"enum": [
"changePassword.success"
]
}
}
}
}
}
},
"400": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"failed"
]
},
"msg": {
"enum": [
"changePassword.failed.toolong",
"changePassword.failed.tooshort",
"changePassword.failed.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"
]
}
}
}
}
}
},
"500": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"failed"
]
},
"msg": {
"enum": [
"changePassword.failed.generic"
]
}
}
}
}
}
}
},
"tags": [
"openapi_other"
]
}
},
"/api/auth/disableOtp": { "/api/auth/disableOtp": {
"put": { "put": {
"operationId": "disableOtp", "operationId": "disableOtp",
@ -51,6 +188,32 @@
} }
} }
}, },
"400": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"failure"
]
},
"msg": {
"enum": [
"disableOtp.failure.guest"
]
}
}
}
}
}
},
"401": { "401": {
"description": "Default Response", "description": "Default Response",
"content": { "content": {
@ -155,6 +318,32 @@
} }
} }
}, },
"400": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"failure"
]
},
"msg": {
"enum": [
"enableOtp.failure.guest"
]
}
}
}
}
}
},
"401": { "401": {
"description": "Default Response", "description": "Default Response",
"content": { "content": {
@ -300,6 +489,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",
@ -340,6 +543,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": {
@ -883,12 +1112,12 @@
"payload": { "payload": {
"type": "object", "type": "object",
"required": [ "required": [
"url" "secret"
], ],
"properties": { "properties": {
"url": { "secret": {
"type": "string", "type": "string",
"description": "The otp url to feed into a 2fa app" "description": "The otp secret"
} }
} }
} }
@ -1005,6 +1234,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",
@ -1069,6 +1409,20 @@
}, },
"guest": { "guest": {
"type": "boolean" "type": "boolean"
},
"selfInfo": {
"type": "object",
"properties": {
"login_name": {
"type": "string"
},
"provider_id": {
"type": "string"
},
"provider_user": {
"type": "string"
}
}
} }
} }
} }

View file

@ -23,20 +23,17 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
"@openapitools/openapi-generator-cli": "^2.25.2", "@typescript-eslint/eslint-plugin": "^8.49.0",
"@typescript-eslint/eslint-plugin": "^8.48.1", "@typescript-eslint/parser": "^8.49.0",
"@typescript-eslint/parser": "^8.48.1",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^16.2.7", "lint-staged": "^16.2.7",
"openapi-generator-cli": "^1.0.0",
"openapi-typescript": "^7.10.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.48.1", "typescript-eslint": "^8.49.0",
"vite": "^7.2.6" "vite": "^7.2.7"
}, },
"dependencies": { "dependencies": {
"@redocly/cli": "^2.12.3", "@redocly/cli": "^2.12.5",
"bindings": "^1.5.0" "bindings": "^1.5.0"
} }
} }

1168
src/pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,9 @@ packages:
nodeLinker: hoisted nodeLinker: hoisted
onlyBuiltDependencies: onlyBuiltDependencies:
- better-sqlite3
- esbuild
- sharp
- bcrypt - bcrypt
- better-sqlite3
- core-js
- esbuild
- protobufjs
- sharp

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",
@ -72,6 +180,20 @@
}, },
"guest": { "guest": {
"type": "boolean" "type": "boolean"
},
"selfInfo": {
"type": "object",
"properties": {
"login_name": {
"type": "string"
},
"provider_id": {
"type": "string"
},
"provider_user": {
"type": "string"
}
}
} }
} }
} }

View file

@ -29,9 +29,9 @@
"typebox": "^1.0.61" "typebox": "^1.0.61"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.19.1", "@types/node": "^22.19.2",
"rollup-plugin-node-externals": "^8.1.2", "rollup-plugin-node-externals": "^8.1.2",
"vite": "^7.2.6", "vite": "^7.2.7",
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4"
} }
} }

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

@ -7,6 +7,11 @@ import { isNullish, MakeStaticResponse, typeResponse } from '@shared/utils';
export const UserInfoRes = { export const UserInfoRes = {
'200': typeResponse('success', 'userinfo.success', { '200': typeResponse('success', 'userinfo.success', {
name: Type.String(), id: Type.String(), guest: Type.Boolean(), 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'), '403': typeResponse('failure', 'userinfo.failure.notLoggedIn'),
'404': typeResponse('failure', 'userinfo.failure.unknownUser'), '404': typeResponse('failure', 'userinfo.failure.unknownUser'),
@ -38,13 +43,12 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
if (req.params.user === 'me') { if (req.params.user === 'me') {
req.params.user = req.authUser.id; req.params.user = req.authUser.id;
} }
const askSelf = req.params.user === req.authUser.id;
const user = this.db.getUser(req.params.user); const user = this.db.getUser(req.params.user);
if (isNullish(user)) { if (isNullish(user)) {
return res.makeResponse(404, 'failure', 'userinfo.failure.unknownUser'); return res.makeResponse(404, 'failure', 'userinfo.failure.unknownUser');
} }
const payload = { const payload = {
name: user.name, name: user.name,
id: user.id, id: user.id,
@ -57,6 +61,11 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
// ``` // ```
// is the same as `val = !!something` // is the same as `val = !!something`
guest: !!user.guest, 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); return res.makeResponse(200, 'success', 'userinfo.success', payload);