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"
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 #
###############

View file

@ -1,13 +1,21 @@
FROM node:22-alpine AS pnpm_base
RUN npm install --global pnpm@10;
FROM pnpm_base AS deps
COPY ./package.json ./pnpm-lock.yaml ./pnpm-workspace.yaml /src/
WORKDIR /src
RUN pnpm install --frozen-lockfile;
FROM pnpm_base AS builder
COPY . /src
WORKDIR /src
RUN pnpm install --frozen-lockfile && pnpm run build;
COPY --from=deps /src/node_modules /src/node_modules
COPY . /src
FROM node:22-alpine
RUN pnpm run build;
FROM pnpm_base
COPY --from=builder /src/dist /dist
COPY ./run.sh /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">
<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="/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="/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>
</aside>

View file

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

266
frontend/pnpm-lock.yaml generated
View file

@ -10,13 +10,16 @@ importers:
dependencies:
'@tailwindcss/vite':
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:
specifier: ^3.0.5
version: 3.0.5
openapi-fetch:
specifier: ^0.15.0
version: 0.15.0
qrcode:
specifier: ^1.5.4
version: 1.5.4
socket.io-client:
specifier: ^4.8.1
version: 4.8.1
@ -31,11 +34,11 @@ importers:
specifier: ~5.9.3
version: 5.9.3
vite:
specifier: ^7.2.6
version: 7.2.6(jiti@2.6.1)(lightningcss@1.30.2)
specifier: ^7.2.7
version: 7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)
vite-tsconfig-paths:
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:
@ -420,6 +423,34 @@ packages:
'@types/js-cookie@3.0.6':
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:
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
engines: {node: '>=6.0'}
@ -438,10 +469,20 @@ packages:
supports-color:
optional: true
decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
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:
resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==}
@ -467,17 +508,29 @@ packages:
picomatch:
optional: true
find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
globrex@0.1.2:
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
graceful-fs@4.2.11:
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:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
@ -556,6 +609,10 @@ packages:
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
engines: {node: '>= 12.0.0'}
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@ -567,11 +624,21 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
openapi-fetch@0.15.0:
resolution: {integrity: sha512-OjQUdi61WO4HYhr9+byCPMj0+bgste/LtSBEcV6FzDdONTs7x0fWn8/ndoYwzqCsKWIxEZwo4FN/TG1c1rI8IQ==}
p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
openapi-typescript-helpers@0.0.15:
resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
p-locate@4.1.0:
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:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@ -580,15 +647,34 @@ packages:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
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:
resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
socket.io-client@4.8.1:
resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==}
engines: {node: '>=10.0.0'}
@ -601,6 +687,14 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
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:
resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==}
@ -627,6 +721,9 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
vite-tsconfig-paths@5.1.4:
resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==}
peerDependencies:
@ -635,8 +732,8 @@ packages:
vite:
optional: true
vite@7.2.6:
resolution: {integrity: sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==}
vite@7.2.7:
resolution: {integrity: sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
@ -675,6 +772,13 @@ packages:
yaml:
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:
resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
engines: {node: '>=10.0.0'}
@ -691,6 +795,17 @@ packages:
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
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:
'@esbuild/aix-ppc64@0.25.12':
@ -919,17 +1034,45 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-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:
'@tailwindcss/node': 4.1.17
'@tailwindcss/oxide': 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/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:
dependencies:
ms: 2.1.3
@ -938,8 +1081,14 @@ snapshots:
dependencies:
ms: 2.1.3
decamelize@1.2.0: {}
detect-libc@2.1.2: {}
dijkstrajs@1.0.3: {}
emoji-regex@8.0.0: {}
engine.io-client@6.6.3:
dependencies:
'@socket.io/component-emitter': 3.1.2
@ -992,13 +1141,22 @@ snapshots:
optionalDependencies:
picomatch: 4.0.3
find-up@4.1.0:
dependencies:
locate-path: 5.0.0
path-exists: 4.0.0
fsevents@2.3.3:
optional: true
get-caller-file@2.0.5: {}
globrex@0.1.2: {}
graceful-fs@4.2.11: {}
is-fullwidth-code-point@3.0.0: {}
jiti@2.6.1: {}
js-cookie@3.0.5: {}
@ -1052,6 +1210,10 @@ snapshots:
lightningcss-win32-arm64-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:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@ -1060,22 +1222,40 @@ snapshots:
nanoid@3.3.11: {}
openapi-fetch@0.15.0:
p-limit@2.3.0:
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: {}
picomatch@4.0.3: {}
pngjs@5.0.0: {}
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.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:
dependencies:
'@types/estree': 1.0.8
@ -1104,6 +1284,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.53.3
fsevents: 2.3.3
set-blocking@2.0.0: {}
socket.io-client@4.8.1:
dependencies:
'@socket.io/component-emitter': 3.1.2
@ -1124,6 +1306,16 @@ snapshots:
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: {}
tapable@2.3.0: {}
@ -1139,18 +1331,20 @@ snapshots:
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:
debug: 4.4.3
globrex: 0.1.2
tsconfck: 3.1.6(typescript@5.9.3)
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:
- supports-color
- 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:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.3)
@ -1159,10 +1353,40 @@ snapshots:
rollup: 4.53.3
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.10.2
fsevents: 2.3.3
jiti: 2.6.1
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: {}
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/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/ChatTest200ResponsePayload.ts
models/DisableOtp200Response.ts
models/DisableOtp401Response.ts
models/DisableOtp400Response.ts
models/DisableOtp500Response.ts
models/EnableOtp200Response.ts
models/EnableOtp200ResponsePayload.ts
models/EnableOtp400Response.ts
models/EnableOtp401Response.ts
models/EnableOtp401ResponseAnyOf.ts
models/GetUser200Response.ts
models/GetUser200ResponsePayload.ts
models/GetUser200ResponsePayloadSelfInfo.ts
models/GetUser403Response.ts
models/GetUser404Response.ts
models/GetUserUserParameter.ts
models/GuestLogin200Response.ts
models/GuestLogin200ResponsePayload.ts
models/GuestLogin400Response.ts
models/GuestLogin500Response.ts
models/GuestLoginRequest.ts
models/Login200Response.ts
models/Login202Response.ts
models/Login202ResponsePayload.ts
@ -41,6 +54,7 @@ models/Signin500Response.ts
models/StatusOtp200Response.ts
models/StatusOtp200ResponseAnyOf.ts
models/StatusOtp200ResponseAnyOf1.ts
models/StatusOtp200ResponseAnyOfPayload.ts
models/StatusOtp401Response.ts
models/StatusOtp500Response.ts
models/index.ts

View file

@ -15,18 +15,29 @@
import * as runtime from '../runtime';
import type {
ChangeDisplayName200Response,
ChangeDisplayName400Response,
ChangeDisplayNameRequest,
ChangePassword200Response,
ChangePassword400Response,
ChangePassword401Response,
ChangePassword500Response,
ChangePasswordRequest,
ChatTest200Response,
DisableOtp200Response,
DisableOtp401Response,
DisableOtp400Response,
DisableOtp500Response,
EnableOtp200Response,
EnableOtp400Response,
EnableOtp401Response,
GetUser200Response,
GetUser403Response,
GetUser404Response,
GetUserUserParameter,
GuestLogin200Response,
GuestLogin400Response,
GuestLogin500Response,
GuestLoginRequest,
Login200Response,
Login202Response,
Login400Response,
@ -47,16 +58,34 @@ import type {
StatusOtp500Response,
} from '../models/index';
import {
ChangeDisplayName200ResponseFromJSON,
ChangeDisplayName200ResponseToJSON,
ChangeDisplayName400ResponseFromJSON,
ChangeDisplayName400ResponseToJSON,
ChangeDisplayNameRequestFromJSON,
ChangeDisplayNameRequestToJSON,
ChangePassword200ResponseFromJSON,
ChangePassword200ResponseToJSON,
ChangePassword400ResponseFromJSON,
ChangePassword400ResponseToJSON,
ChangePassword401ResponseFromJSON,
ChangePassword401ResponseToJSON,
ChangePassword500ResponseFromJSON,
ChangePassword500ResponseToJSON,
ChangePasswordRequestFromJSON,
ChangePasswordRequestToJSON,
ChatTest200ResponseFromJSON,
ChatTest200ResponseToJSON,
DisableOtp200ResponseFromJSON,
DisableOtp200ResponseToJSON,
DisableOtp401ResponseFromJSON,
DisableOtp401ResponseToJSON,
DisableOtp400ResponseFromJSON,
DisableOtp400ResponseToJSON,
DisableOtp500ResponseFromJSON,
DisableOtp500ResponseToJSON,
EnableOtp200ResponseFromJSON,
EnableOtp200ResponseToJSON,
EnableOtp400ResponseFromJSON,
EnableOtp400ResponseToJSON,
EnableOtp401ResponseFromJSON,
EnableOtp401ResponseToJSON,
GetUser200ResponseFromJSON,
@ -69,8 +98,12 @@ import {
GetUserUserParameterToJSON,
GuestLogin200ResponseFromJSON,
GuestLogin200ResponseToJSON,
GuestLogin400ResponseFromJSON,
GuestLogin400ResponseToJSON,
GuestLogin500ResponseFromJSON,
GuestLogin500ResponseToJSON,
GuestLoginRequestFromJSON,
GuestLoginRequestToJSON,
Login200ResponseFromJSON,
Login200ResponseToJSON,
Login202ResponseFromJSON,
@ -109,10 +142,22 @@ import {
StatusOtp500ResponseToJSON,
} from '../models/index';
export interface ChangeDisplayNameOperationRequest {
changeDisplayNameRequest: ChangeDisplayNameRequest;
}
export interface ChangePasswordOperationRequest {
changePasswordRequest: ChangePasswordRequest;
}
export interface GetUserRequest {
user: GetUserUserParameter;
}
export interface GuestLoginOperationRequest {
guestLoginRequest?: GuestLoginRequest;
}
export interface LoginOperationRequest {
loginRequest: LoginRequest;
}
@ -130,6 +175,122 @@ export interface SigninRequest {
*/
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>> {
@ -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 headerParameters: runtime.HTTPHeaders = {};
@ -197,9 +358,13 @@ export class OpenapiOtherApi extends runtime.BaseAPI {
// Object response for status 200
return new runtime.JSONApiResponse(response, (jsonValue) => DisableOtp200ResponseFromJSON(jsonValue));
}
if (response.status === 400) {
// Object response for status 400
return new runtime.JSONApiResponse(response, (jsonValue) => DisableOtp400ResponseFromJSON(jsonValue));
}
if (response.status === 401) {
// Object response for status 401
return new runtime.JSONApiResponse(response, (jsonValue) => DisableOtp401ResponseFromJSON(jsonValue));
return new runtime.JSONApiResponse(response, (jsonValue) => ChangePassword401ResponseFromJSON(jsonValue));
}
if (response.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
// This ensures all code paths return a value and provides clear error messages for unexpected status codes
// Only throw if responses were defined but none matched the actual status code
throw new runtime.ResponseError(response, `Unexpected status code: ${response.status}. Expected one of: 200, 401, 500`);
throw new runtime.ResponseError(response, `Unexpected status code: ${response.status}. Expected one of: 200, 400, 401, 500`);
}
/**
*/
async disableOtp(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<DisableOtp200Response | DisableOtp401Response | DisableOtp500Response> {
async disableOtp(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<DisableOtp200Response | DisableOtp400Response | ChangePassword401Response | DisableOtp500Response> {
const response = await this.disableOtpRaw(initOverrides);
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 headerParameters: runtime.HTTPHeaders = {};
@ -243,6 +408,10 @@ export class OpenapiOtherApi extends runtime.BaseAPI {
// Object response for status 200
return new runtime.JSONApiResponse(response, (jsonValue) => EnableOtp200ResponseFromJSON(jsonValue));
}
if (response.status === 400) {
// Object response for status 400
return new runtime.JSONApiResponse(response, (jsonValue) => EnableOtp400ResponseFromJSON(jsonValue));
}
if (response.status === 401) {
// Object response for status 401
return new runtime.JSONApiResponse(response, (jsonValue) => EnableOtp401ResponseFromJSON(jsonValue));
@ -250,12 +419,12 @@ export class OpenapiOtherApi extends runtime.BaseAPI {
// CHANGED: Throw error if status code is not handled by any of the defined responses
// This ensures all code paths return a value and provides clear error messages for unexpected status codes
// Only throw if responses were defined but none matched the actual status code
throw new runtime.ResponseError(response, `Unexpected status code: ${response.status}. Expected one of: 200, 401`);
throw new runtime.ResponseError(response, `Unexpected status code: ${response.status}. Expected one of: 200, 400, 401`);
}
/**
*/
async enableOtp(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<EnableOtp200Response | EnableOtp401Response> {
async enableOtp(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<EnableOtp200Response | EnableOtp400Response | EnableOtp401Response> {
const response = await this.enableOtpRaw(initOverrides);
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 headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
let urlPath = `/api/auth/guest`;
@ -333,6 +504,7 @@ export class OpenapiOtherApi extends runtime.BaseAPI {
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: GuestLoginRequestToJSON(requestParameters['guestLoginRequest']),
}, initOverrides);
// CHANGED: Handle all status codes defined in the OpenAPI spec, not just 2xx responses
@ -343,6 +515,10 @@ export class OpenapiOtherApi extends runtime.BaseAPI {
// Object response for status 200
return new runtime.JSONApiResponse(response, (jsonValue) => GuestLogin200ResponseFromJSON(jsonValue));
}
if (response.status === 400) {
// Object response for status 400
return new runtime.JSONApiResponse(response, (jsonValue) => GuestLogin400ResponseFromJSON(jsonValue));
}
if (response.status === 500) {
// Object response for status 500
return new runtime.JSONApiResponse(response, (jsonValue) => GuestLogin500ResponseFromJSON(jsonValue));
@ -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
// This ensures all code paths return a value and provides clear error messages for unexpected status codes
// Only throw if responses were defined but none matched the actual status code
throw new runtime.ResponseError(response, `Unexpected status code: ${response.status}. Expected one of: 200, 500`);
throw new runtime.ResponseError(response, `Unexpected status code: ${response.status}. Expected one of: 200, 400, 500`);
}
/**
*/
async guestLogin(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<GuestLogin200Response | GuestLogin500Response> {
const response = await this.guestLoginRaw(initOverrides);
async guestLogin(requestParameters: GuestLoginOperationRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<GuestLogin200Response | GuestLogin400Response | GuestLogin500Response> {
const response = await this.guestLoginRaw(requestParameters, initOverrides);
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 type { GetUser200ResponsePayload } from './GetUser200ResponsePayload';
import type { ChatTest200ResponsePayload } from './ChatTest200ResponsePayload';
import {
GetUser200ResponsePayloadFromJSON,
GetUser200ResponsePayloadFromJSONTyped,
GetUser200ResponsePayloadToJSON,
GetUser200ResponsePayloadToJSONTyped,
} from './GetUser200ResponsePayload';
ChatTest200ResponsePayloadFromJSON,
ChatTest200ResponsePayloadFromJSONTyped,
ChatTest200ResponsePayloadToJSON,
ChatTest200ResponsePayloadToJSONTyped,
} from './ChatTest200ResponsePayload';
/**
*
@ -41,10 +41,10 @@ export interface ChatTest200Response {
msg: ChatTest200ResponseMsgEnum;
/**
*
* @type {GetUser200ResponsePayload}
* @type {ChatTest200ResponsePayload}
* @memberof ChatTest200Response
*/
payload: GetUser200ResponsePayload;
payload: ChatTest200ResponsePayload;
}
@ -87,7 +87,7 @@ export function ChatTest200ResponseFromJSONTyped(json: any, ignoreDiscriminator:
'kind': json['kind'],
'msg': json['msg'],
'payload': GetUser200ResponsePayloadFromJSON(json['payload']),
'payload': ChatTest200ResponsePayloadFromJSON(json['payload']),
};
}
@ -104,7 +104,7 @@ export function ChatTest200ResponseToJSONTyped(value?: ChatTest200Response | nul
'kind': value['kind'],
'msg': value['msg'],
'payload': GetUser200ResponsePayloadToJSON(value['payload']),
'payload': ChatTest200ResponsePayloadToJSON(value['payload']),
};
}

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 type { DisableOtp401Response } from './DisableOtp401Response';
import {
DisableOtp401ResponseFromJSON,
DisableOtp401ResponseFromJSONTyped,
DisableOtp401ResponseToJSON,
DisableOtp401ResponseToJSONTyped,
} from './DisableOtp401Response';
import type { EnableOtp401ResponseAnyOf } from './EnableOtp401ResponseAnyOf';
import {
EnableOtp401ResponseAnyOfFromJSON,
@ -27,6 +20,13 @@ import {
EnableOtp401ResponseAnyOfToJSON,
EnableOtp401ResponseAnyOfToJSONTyped,
} 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 type { GetUser200ResponsePayloadSelfInfo } from './GetUser200ResponsePayloadSelfInfo';
import {
GetUser200ResponsePayloadSelfInfoFromJSON,
GetUser200ResponsePayloadSelfInfoFromJSONTyped,
GetUser200ResponsePayloadSelfInfoToJSON,
GetUser200ResponsePayloadSelfInfoToJSONTyped,
} from './GetUser200ResponsePayloadSelfInfo';
/**
*
* @export
@ -37,6 +45,12 @@ export interface GetUser200ResponsePayload {
* @memberof GetUser200ResponsePayload
*/
guest: boolean;
/**
*
* @type {GetUser200ResponsePayloadSelfInfo}
* @memberof GetUser200ResponsePayload
*/
selfInfo?: GetUser200ResponsePayloadSelfInfo;
}
/**
@ -62,6 +76,7 @@ export function GetUser200ResponsePayloadFromJSONTyped(json: any, ignoreDiscrimi
'name': json['name'],
'id': json['id'],
'guest': json['guest'],
'selfInfo': json['selfInfo'] == null ? undefined : GetUser200ResponsePayloadSelfInfoFromJSON(json['selfInfo']),
};
}
@ -79,6 +94,7 @@ export function GetUser200ResponsePayloadToJSONTyped(value?: GetUser200ResponseP
'name': value['name'],
'id': value['id'],
'guest': value['guest'],
'selfInfo': GetUser200ResponsePayloadSelfInfoToJSON(value['selfInfo']),
};
}

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

View file

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

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 type { DisableOtp401Response } from './DisableOtp401Response';
import type { ChangePassword401Response } from './ChangePassword401Response';
import {
DisableOtp401ResponseFromJSON,
DisableOtp401ResponseFromJSONTyped,
DisableOtp401ResponseToJSON,
DisableOtp401ResponseToJSONTyped,
} from './DisableOtp401Response';
ChangePassword401ResponseFromJSON,
ChangePassword401ResponseFromJSONTyped,
ChangePassword401ResponseToJSON,
ChangePassword401ResponseToJSONTyped,
} from './ChangePassword401Response';
/**
*

View file

@ -1,21 +1,34 @@
/* tslint: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 './ChatTest200ResponsePayload';
export * from './DisableOtp200Response';
export * from './DisableOtp401Response';
export * from './DisableOtp400Response';
export * from './DisableOtp500Response';
export * from './EnableOtp200Response';
export * from './EnableOtp200ResponsePayload';
export * from './EnableOtp400Response';
export * from './EnableOtp401Response';
export * from './EnableOtp401ResponseAnyOf';
export * from './GetUser200Response';
export * from './GetUser200ResponsePayload';
export * from './GetUser200ResponsePayloadSelfInfo';
export * from './GetUser403Response';
export * from './GetUser404Response';
export * from './GetUserUserParameter';
export * from './GuestLogin200Response';
export * from './GuestLogin200ResponsePayload';
export * from './GuestLogin400Response';
export * from './GuestLogin500Response';
export * from './GuestLoginRequest';
export * from './Login200Response';
export * from './Login202Response';
export * from './Login202ResponsePayload';
@ -40,5 +53,6 @@ export * from './Signin500Response';
export * from './StatusOtp200Response';
export * from './StatusOtp200ResponseAnyOf';
export * from './StatusOtp200ResponseAnyOf1';
export * from './StatusOtp200ResponseAnyOfPayload';
export * from './StatusOtp401Response';
export * from './StatusOtp500Response';

View file

@ -5,6 +5,11 @@ export type User = {
id: string;
guest: boolean;
name: string;
selfInfo?: {
loginName?: string;
providerId?: string;
providerUser?: string;
}
};
let currentUser: User | null = null;

View file

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

View file

@ -5,14 +5,98 @@ import {
type RouteHandlerParams,
type RouteHandlerReturn,
} from "@app/routing";
import { showError, showInfo, showSuccess } from "@app/toast";
import Cookie from "js-cookie";
import authHtml from "./login.html?raw";
import client from "@app/api";
import { updateUser } from "@app/auth";
import Cookie from "js-cookie";
import loggedInHtml from "./alreadyLoggedin.html?raw";
import cuteCat from "./cuteCat.png";
import loggedInHtml from "./alreadyLoggedin.html?raw";
import totpHtml from "./totp.html?raw";
import { isNullish } from "@app/utils";
import { showError, showInfo, showSuccess } from "@app/toast";
import { updateUser } from "@app/auth";
const TOTP_LENGTH = 6;
async function handleOtp(app: HTMLElement, token: string, returnTo: string | null) {
app.innerHTML = totpHtml;
const container = app.querySelector("#totp-container")!;
container.innerHTML = "";
const inputs: HTMLInputElement[] = [];
for (let i = 0; i < TOTP_LENGTH; i++) {
const input = document.createElement("input");
input.maxLength = 1;
input.inputMode = "numeric";
input.className =
"w-12 h-12 text-center text-xl border border-gray-300 rounded " +
"focus:outline-none focus:ring-2 focus:ring-blue-500";
container.appendChild(input);
inputs.push(input);
// Handle typing a digit
input.addEventListener("input", async () => {
const value = input.value.replace(/\D/g, "");
input.value = value;
// Auto-advance when filled
if (value && i < TOTP_LENGTH - 1) {
inputs[i + 1].focus();
}
await checkComplete();
});
// Handle backspace
input.addEventListener("keydown", (e) => {
if (e.key === "Backspace" && !input.value && i > 0) {
inputs[i - 1].focus();
}
});
// Handle pasting a full code
input.addEventListener("paste", (e: ClipboardEvent) => {
const pasted = e.clipboardData?.getData("text") ?? "";
const digits = pasted.replace(/\D/g, "").slice(0, TOTP_LENGTH);
if (digits.length > 1) {
e.preventDefault();
digits.split("").forEach((d, idx) => {
if (inputs[idx]) inputs[idx].value = d;
});
if (digits.length === TOTP_LENGTH) checkComplete();
}
});
}
// Check if all digits are entered and then call totpSend
async function checkComplete() {
const code = inputs.map((i) => i.value).join("");
if (code.length === TOTP_LENGTH && /^[0-9]+$/.test(code)) {
let res = await client.loginOtp({
loginOtpRequest: {
code, token,
}
})
if (res.kind === "success") {
Cookie.set("token", res.payload.token, {
path: "/",
sameSite: "lax",
});
if (returnTo !== null) navigateTo(returnTo);
else navigateTo("/");
}
else if (res.kind === "failed") {
showError(`Failed to authenticate: ${res.msg}`);
}
}
}
inputs[0].focus();
}
async function handleLogin(
_url: string,
@ -67,7 +151,7 @@ async function handleLogin(
return showError(
"Error while rendering the page: no form found",
);
fLogin.addEventListener("submit", async function (e: SubmitEvent) {
fLogin.addEventListener("submit", async function(e: SubmitEvent) {
e.preventDefault();
let form = e.target as HTMLFormElement | null;
if (form === null) return showError("Failed to send form...");
@ -109,8 +193,7 @@ async function handleLogin(
break;
}
case "otpRequired": {
showInfo("Got ask OTP, not yet implemented");
break;
return await handleOtp(app!, res.payload.token, returnTo);
}
case "failed": {
showError(`Failed to login: ${res.msg}`);
@ -126,7 +209,7 @@ async function handleLogin(
document.querySelector<HTMLButtonElement>("#bGuestLogin");
bLoginAsGuest?.addEventListener("click", async () => {
try {
const res = await client.guestLogin();
const res = await client.guestLogin({ guestLoginRequest: { name: undefined } });
switch (res.kind) {
case "success": {
Cookie.set("token", res.payload.token, {

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

View file

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

View file

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

View file

@ -6,3 +6,7 @@ location /app {
location /assets {
root /volumes/static/app/;
}
location / {
return 301 https://$http_host/app;
}

View file

@ -25,6 +25,6 @@
},
"devDependencies": {
"@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 = {
id: UserId;
name: string;
guest: boolean;
};
declare module 'fastify' {
@ -118,7 +119,7 @@ export const authPlugin = fp<{ onlySchema?: boolean }>(async (fastify, { onlySch
.clearCookie('token', { path: '/' })
.makeResponse(401, 'notLoggedIn', 'auth.noUser');
}
req.authUser = { id: user.id, name: user.display_name };
req.authUser = { id: user.id, name: user.name, guest: user.guest };
}
catch {
return res

View file

@ -18,7 +18,7 @@ Project Transcendance {
Table user {
id text [PK, not null]
login text [unique]
name text [not null]
name text [not null, unique]
password text [null, Note: "If password is NULL, this means that the user is created through OAUTH2 or guest login"]
otp text [null, Note: "If otp is NULL, then the user didn't configure 2FA"]
guest integer [not null, default: 0]

View file

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

View file

@ -3,6 +3,7 @@ import { Otp } from '@shared/auth';
import { isNullish } from '@shared/utils';
import * as bcrypt from 'bcrypt';
import { UUID, newUUID } from '@shared/utils/uuid';
import { SqliteError } from 'better-sqlite3';
// never use this directly
@ -20,6 +21,10 @@ export interface IUserDb extends Database {
getAllUserFromProvider(provider: string): User[] | undefined,
getAllUsers(this: IUserDb): User[] | undefined,
updateDisplayName(id: UserId, new_name: string): boolean,
getUserFromDisplayName(name: string): User | undefined,
};
export const UserImpl: Omit<IUserDb, keyof Database> = {
@ -159,6 +164,24 @@ export const UserImpl: Omit<IUserDb, keyof Database> = {
const req = this.prepare('SELECT * FROM user WHERE oauth2 = @oauth2').get({ oauth2: `${provider}:${unique}` }) as Partial<User> | undefined;
return userFromRow(req);
},
updateDisplayName(this: IUserDb, id: UserId, new_name: string): boolean {
try {
this.prepare('UPDATE OR FAIL user SET name = @new_name WHERE id = @id').run({ id, new_name });
return true;
}
catch (e) {
if (e instanceof SqliteError) {
if (e.code === 'SQLITE_CONSTRAINT_UNIQUE') return false;
}
throw e;
}
},
getUserFromDisplayName(this: IUserDb, name: string) {
const res = this.prepare('SELECT * FROM user WHERE name = @name LIMIT 1').get({ name }) as User | undefined;
return userFromRow(res);
},
};
export type UserId = UUID;
@ -170,7 +193,7 @@ export type User = {
readonly password?: string;
readonly otp?: string;
readonly guest: boolean;
// will be split/merged from the `provider` column
// will be split/merged from the `oauth2` column
readonly provider_name?: string;
readonly provider_unique?: string;
};
@ -207,7 +230,7 @@ async function hashPassword(
*
* @returns The user if it exists, undefined otherwise
*/
export function userFromRow(row?: Partial<Omit<User, 'provider_name' | 'provider_unique'> & { provider?: string }>): User | undefined {
export function userFromRow(row?: Partial<Omit<User, 'provider_name' | 'provider_unique'> & { oauth2?: string }>): User | undefined {
if (isNullish(row)) return undefined;
if (isNullish(row.id)) return undefined;
if (isNullish(row.name)) return undefined;
@ -216,9 +239,9 @@ export function userFromRow(row?: Partial<Omit<User, 'provider_name' | 'provider
let provider_name = undefined;
let provider_unique = undefined;
if (row.provider) {
const splitted = row.provider.split(':', 1);
if (splitted.length != 2) { return undefined; }
if (row.oauth2) {
const splitted = row.oauth2.split(/:(.*)/);
if (splitted.length != 3) { return undefined; }
provider_name = splitted[0];
provider_unique = splitted[1];
}

View file

@ -8,6 +8,140 @@
"schemas": {}
},
"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": {
"put": {
"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": {
"description": "Default Response",
"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": {
"description": "Default Response",
"content": {
@ -278,6 +464,20 @@
"/api/auth/guest": {
"post": {
"operationId": "guestLogin",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"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": {
"description": "Default Response",
"content": {
@ -846,12 +1072,12 @@
"payload": {
"type": "object",
"required": [
"url"
"secret"
],
"properties": {
"url": {
"secret": {
"type": "string",
"description": "The otp url to feed into a 2fa app"
"description": "The otp secret"
}
}
}

View file

@ -30,9 +30,9 @@
"typebox": "^1.0.61"
},
"devDependencies": {
"@types/node": "^22.19.1",
"@types/node": "^22.19.2",
"rollup-plugin-node-externals": "^8.1.2",
"vite": "^7.2.6",
"vite": "^7.2.7",
"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 = {
'200': typeResponse('success', 'disableOtp.success'),
'500': typeResponse('failure', 'disableOtp.failure.generic'),
'400': typeResponse('failure', 'disableOtp.failure.guest'),
};
@ -18,6 +19,13 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
async function(req, res) {
void res;
if (isNullish(req.authUser)) { return res.makeResponse(500, 'failure', 'disableOtp.failure.generic'); }
if (req.authUser.guest) {
return res.makeResponse(
400,
'failure',
'disableOtp.failure.guest',
);
}
this.db.deleteUserOtpSecret(req.authUser.id);
return res.makeResponse(200, 'success', 'disableOtp.success');
},

View file

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

View file

@ -1,39 +1,95 @@
import { FastifyPluginAsync } from 'fastify';
import { Type } from 'typebox';
import { Static, Type } from 'typebox';
import { typeResponse, isNullish, MakeStaticResponse } from '@shared/utils';
export const GuestLoginRes = {
'500': typeResponse('failed', ['guestLogin.failed.generic.unknown', 'guestLogin.failed.generic.error']),
'500': typeResponse('failed', [
'guestLogin.failed.generic.unknown',
'guestLogin.failed.generic.error',
]),
'200': typeResponse('success', 'guestLogin.success', {
token: Type.String({
description: 'JWT that represent a logged in user',
}),
}),
'400': typeResponse('failed', 'guestLogin.failed.invalid'),
};
export type GuestLoginRes = MakeStaticResponse<typeof GuestLoginRes>;
export const GuestLoginReq = Type.Object({
name: Type.Optional(Type.String()),
});
export type GuestLoginReq = Static<typeof GuestLoginReq>;
const getRandomFromList = (list: string[]): string => {
return list[Math.floor(Math.random() * list.length)];
};
const USERNAME_CHECK: RegExp = /^[a-zA-Z_0-9]+$/;
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
void _opts;
fastify.post<{ Body: null, Reply: GuestLoginRes }>(
fastify.post<{ Body: GuestLoginReq; Reply: GuestLoginRes }>(
'/api/auth/guest',
{ schema: { response: GuestLoginRes, operationId: 'guestLogin' } },
{
schema: {
body: GuestLoginReq,
response: GuestLoginRes,
operationId: 'guestLogin',
},
},
async function(req, res) {
void req;
void res;
try {
console.log('DEBUG ----- guest login backend');
const adjective = getRandomFromList(fastify.words.adjectives);
let user_name: string | undefined = req.body?.name;
if (isNullish(user_name)) {
const adjective = getRandomFromList(
fastify.words.adjectives,
);
const noun = getRandomFromList(fastify.words.nouns);
user_name = `${adjective}${noun}`;
}
else {
if (user_name.length < 4 || user_name.length > 26) {
return res.makeResponse(
400,
'failed',
'guestLogin.failed.invalid',
);
}
if (!USERNAME_CHECK.test(user_name)) {
return res.makeResponse(
400,
'failed',
'guestLogin.failed.invalid',
);
}
user_name = `g_${user_name}`;
}
const user = await this.db.createGuestUser(`${adjective} ${noun}`);
const orig = user_name;
let i = 0;
while (
this.db.getUserFromDisplayName(user_name) !== undefined &&
i++ < 5
) {
user_name = `${orig}${Date.now() % 1000}`;
}
if (this.db.getUserFromDisplayName(user_name) !== undefined) {
user_name = `${orig}${Date.now()}`;
}
const user = await this.db.createGuestUser(user_name);
if (isNullish(user)) {
return res.makeResponse(500, 'failed', 'guestLogin.failed.generic.unknown');
return res.makeResponse(
500,
'failed',
'guestLogin.failed.generic.unknown',
);
}
return res.makeResponse(200, 'success', 'guestLogin.success', {
token: this.signJwt('auth', user.id.toString()),
@ -41,7 +97,11 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
}
catch (e: unknown) {
fastify.log.error(e);
return res.makeResponse(500, 'failed', 'guestLogin.failed.generic.error');
return res.makeResponse(
500,
'failed',
'guestLogin.failed.generic.error',
);
}
},
);

View file

@ -30,9 +30,23 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
const result = await creq.getCode();
const userinfo = await provider.getUserInfo(result);
let u = this.db.getOauth2User(provider.display_name, userinfo.unique_id);
if (isNullish(u)) {
u = await this.db.createOauth2User(userinfo.name, provider.display_name, userinfo.unique_id);
let user_name = userinfo.name;
const orig = user_name;
let i = 0;
while (
this.db.getUserFromDisplayName(user_name) !== undefined &&
i++ < 100
) {
user_name = `${orig}${Date.now() % 1000}`;
}
if (this.db.getUserFromDisplayName(user_name) !== undefined) {
user_name = `${orig}${Date.now()}`;
}
u = await this.db.createOauth2User(user_name, provider.display_name, userinfo.unique_id);
}
if (isNullish(u)) {
return res.code(500).send('failed to fetch or create user...');

View file

@ -47,7 +47,19 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
// password is good too !
if (this.db.getUserFromLoginName(name) !== undefined) { return res.makeResponse(400, 'failed', 'signin.failed.username.existing'); }
const u = await this.db.createUser(name, name, password);
let user_name = name;
const orig = user_name;
let i = 0;
while (
this.db.getUserFromDisplayName(user_name) !== undefined &&
i++ < 100
) {
user_name = `${orig}${Date.now() % 1000}`;
}
if (this.db.getUserFromDisplayName(user_name) !== undefined) {
user_name = `${orig}${Date.now()}`;
}
const u = await this.db.createUser(name, user_name, password);
if (isNullish(u)) { return res.makeResponse(500, 'failed', 'signin.failed.generic'); }
// every check has been passed, they are now logged in, using this token to say who they are...

View file

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

View file

@ -30,9 +30,9 @@
"typebox": "^1.0.61"
},
"devDependencies": {
"@types/node": "^22.19.1",
"@types/node": "^22.19.2",
"rollup-plugin-node-externals": "^8.1.2",
"vite": "^7.2.6",
"vite": "^7.2.7",
"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": {
"/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": {
"put": {
"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": {
"description": "Default Response",
"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": {
"description": "Default Response",
"content": {
@ -300,6 +489,20 @@
"/api/auth/guest": {
"post": {
"operationId": "guestLogin",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"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": {
"description": "Default Response",
"content": {
@ -883,12 +1112,12 @@
"payload": {
"type": "object",
"required": [
"url"
"secret"
],
"properties": {
"url": {
"secret": {
"type": "string",
"description": "The otp url to feed into a 2fa app"
"description": "The otp secret"
}
}
}
@ -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}": {
"get": {
"operationId": "getUser",
@ -1069,6 +1409,20 @@
},
"guest": {
"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": {
"@eslint/js": "^9.39.1",
"@openapitools/openapi-generator-cli": "^2.25.2",
"@typescript-eslint/eslint-plugin": "^8.48.1",
"@typescript-eslint/parser": "^8.48.1",
"@typescript-eslint/eslint-plugin": "^8.49.0",
"@typescript-eslint/parser": "^8.49.0",
"eslint": "^9.39.1",
"husky": "^9.1.7",
"lint-staged": "^16.2.7",
"openapi-generator-cli": "^1.0.0",
"openapi-typescript": "^7.10.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.48.1",
"vite": "^7.2.6"
"typescript-eslint": "^8.49.0",
"vite": "^7.2.7"
},
"dependencies": {
"@redocly/cli": "^2.12.3",
"@redocly/cli": "^2.12.5",
"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
onlyBuiltDependencies:
- better-sqlite3
- esbuild
- sharp
- bcrypt
- better-sqlite3
- core-js
- esbuild
- protobufjs
- sharp

View file

@ -8,6 +8,114 @@
"schemas": {}
},
"paths": {
"/api/user/changeDisplayName": {
"put": {
"operationId": "changeDisplayName",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "New Display Name"
}
}
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"success"
]
},
"msg": {
"enum": [
"changeDisplayName.success"
]
}
}
}
}
}
},
"400": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"failure"
]
},
"msg": {
"enum": [
"changeDisplayName.alreadyExist",
"changeDisplayName.invalid"
]
}
}
}
}
}
},
"401": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"notLoggedIn"
]
},
"msg": {
"enum": [
"auth.noCookie",
"auth.invalidKind",
"auth.noUser",
"auth.invalid"
]
}
}
}
}
}
}
}
}
},
"/api/user/info/{user}": {
"get": {
"operationId": "getUser",
@ -72,6 +180,20 @@
},
"guest": {
"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"
},
"devDependencies": {
"@types/node": "^22.19.1",
"@types/node": "^22.19.2",
"rollup-plugin-node-externals": "^8.1.2",
"vite": "^7.2.6",
"vite": "^7.2.7",
"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 = {
'200': typeResponse('success', 'userinfo.success', {
name: Type.String(), id: Type.String(), guest: Type.Boolean(),
selfInfo: Type.Optional(Type.Object({
login_name: Type.Optional(Type.String()),
provider_id: Type.Optional(Type.String()),
provider_user: Type.Optional(Type.String()),
})),
}),
'403': typeResponse('failure', 'userinfo.failure.notLoggedIn'),
'404': typeResponse('failure', 'userinfo.failure.unknownUser'),
@ -38,13 +43,12 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
if (req.params.user === 'me') {
req.params.user = req.authUser.id;
}
const askSelf = req.params.user === req.authUser.id;
const user = this.db.getUser(req.params.user);
if (isNullish(user)) {
return res.makeResponse(404, 'failure', 'userinfo.failure.unknownUser');
}
const payload = {
name: user.name,
id: user.id,
@ -57,6 +61,11 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
// ```
// is the same as `val = !!something`
guest: !!user.guest,
selfInfo: askSelf ? {
login_name: user.login,
provider_id: user.provider_name,
provider_user: user.provider_unique,
} : null,
};
return res.makeResponse(200, 'success', 'userinfo.success', payload);