diff --git a/frontend/package.json b/frontend/package.json index fca330d..fc9a271 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,12 +9,14 @@ "preview": "vite preview" }, "devDependencies": { + "@types/js-cookie": "^3.0.6", "typescript": "~5.9.3", "vite": "^7.1.10", "vite-tsconfig-paths": "^5.1.4" }, "dependencies": { "@tailwindcss/vite": "^4.1.16", + "js-cookie": "^3.0.5", "openapi-fetch": "^0.15.0", "tailwindcss": "^4.1.16" } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index ba5d557..3abe5bd 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@tailwindcss/vite': specifier: ^4.1.16 version: 4.1.16(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)) + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 openapi-fetch: specifier: ^0.15.0 version: 0.15.0 @@ -18,6 +21,9 @@ importers: specifier: ^4.1.16 version: 4.1.16 devDependencies: + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 typescript: specifier: ~5.9.3 version: 5.9.3 @@ -405,6 +411,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/js-cookie@3.0.6': + resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -451,6 +460,10 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -867,6 +880,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/js-cookie@3.0.6': {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -920,6 +935,8 @@ snapshots: jiti@2.6.1: {} + js-cookie@3.0.5: {} + lightningcss-android-arm64@1.30.2: optional: true diff --git a/frontend/src/pages/login/login.ts b/frontend/src/pages/login/login.ts index 8f09e69..b5e2faa 100644 --- a/frontend/src/pages/login/login.ts +++ b/frontend/src/pages/login/login.ts @@ -3,148 +3,149 @@ import { showError, showInfo, showSuccess } from "@app/toast"; import authHtml from './login.html?raw'; import client from '@app/api' import { updateUser } from "@app/auth"; +import Cookie from 'js-cookie'; type Providers = { - name: string, - display_name: string, - icon_url?: string, - color?: { default: string, hover: string }, + name: string, + display_name: string, + icon_url?: string, + color?: { default: string, hover: string }, }; function handleLogin(_url: string, _args: RouteHandlerParams): RouteHandlerReturn { - setTitle('Login') - return { - html: authHtml, postInsert: async (app) => { - const fLogin = document.querySelector('form#login-form'); - if (fLogin === null) - return showError('Error while rendering the page: no form found'); - showSuccess('got the form !') - 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...'); - let formData = Object.fromEntries((new FormData(form)).entries()); - if (!('login' in formData) || typeof formData['login'] !== 'string' || (formData['login'] as string).length === 0) - return showError('Please enter a Login'); - if (!('password' in formData) || typeof formData['password'] !== 'string' || (formData['password'] as string).length === 0) - return showError('Please enter a Password'); - try { - const res = await client.login({ loginRequest: { name: formData.login, password: formData.password } }); - switch (res.kind) { - case 'success': { - document.cookie = `token=${res.payload.token}`; - let user = await updateUser(); - if (user === null) - return showError('Failed to get user: no user ?'); - setTitle(`Welcome ${user.guest ? '[GUEST] ' : ''}${user.name}`); - break; - } - case 'otpRequired': { - showInfo('Got ask OTP, not yet implemented'); - break; - } - case 'failed': { - showError(`Failed to login: ${res.msg}`); - } - } - } catch (e) { - console.error("Login error:", e); - showError('Failed to login: Unknown error'); - } - }); + setTitle('Login') + return { + html: authHtml, postInsert: async (app) => { + const fLogin = document.querySelector('form#login-form'); + if (fLogin === null) + return showError('Error while rendering the page: no form found'); + showSuccess('got the form !') + 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...'); + let formData = Object.fromEntries((new FormData(form)).entries()); + if (!('login' in formData) || typeof formData['login'] !== 'string' || (formData['login'] as string).length === 0) + return showError('Please enter a Login'); + if (!('password' in formData) || typeof formData['password'] !== 'string' || (formData['password'] as string).length === 0) + return showError('Please enter a Password'); + try { + const res = await client.login({ loginRequest: { name: formData.login, password: formData.password } }); + switch (res.kind) { + case 'success': { + Cookie.set('token', res.payload.token, { path: '/', sameSite: 'lax' }); + let user = await updateUser(); + if (user === null) + return showError('Failed to get user: no user ?'); + setTitle(`Welcome ${user.guest ? '[GUEST] ' : ''}${user.name}`); + break; + } + case 'otpRequired': { + showInfo('Got ask OTP, not yet implemented'); + break; + } + case 'failed': { + showError(`Failed to login: ${res.msg}`); + } + } + } catch (e) { + console.error("Login error:", e); + showError('Failed to login: Unknown error'); + } + }); - const bLoginAsGuest = document.querySelector('#bGuestLogin'); - bLoginAsGuest?.addEventListener('click', async () => { - try { - const res = await client.guestLogin(); - switch (res.kind) { - case 'success': { - document.cookie = `token=${res.payload.token}`; - let user = await updateUser(); - if (user === null) - return showError('Failed to get user: no user ?'); - setTitle(`Welcome ${user.guest ? '[GUEST] ' : ''}${user.name}`); - break; - } - case 'failed': { - showError(`Failed to login: ${res.msg}`); - } - } - } catch (e) { - console.error("Login error:", e); - showError('Failed to login: Unknown error'); - } - }); + const bLoginAsGuest = document.querySelector('#bGuestLogin'); + bLoginAsGuest?.addEventListener('click', async () => { + try { + const res = await client.guestLogin(); + switch (res.kind) { + case 'success': { + Cookie.set('token', res.payload.token, { path: '/', sameSite: 'lax' }); + let user = await updateUser(); + if (user === null) + return showError('Failed to get user: no user ?'); + setTitle(`Welcome ${user.guest ? '[GUEST] ' : ''}${user.name}`); + break; + } + case 'failed': { + showError(`Failed to login: ${res.msg}`); + } + } + } catch (e) { + console.error("Login error:", e); + showError('Failed to login: Unknown error'); + } + }); - const dOtherLoginArea = document.querySelector('#otherLogin'); - if (dOtherLoginArea) { - let styleSheetElement = document.createElement('style'); - styleSheetElement.innerText = ""; - // TODO: fetch all the providers from an API ? - const providers: Providers[] = [ - { name: 'discord', display_name: 'Discord', color: { default: 'bg-[#5865F2]', hover: '#FF65F2' } }, - { name: 'kanidm', display_name: 'Kanidm', color: { default: 'bg-red-500', hover: 'bg-red-700' } }, - { name: 'google', display_name: 'Google' }, - ] - let first = true; - for (const p of providers) { - let b = document.createElement('button'); - if (first) b.classList.add('last:col-span-2'); - first = false; - b.classList.add(...( - 'w-full text-white font-medium py-2 rounded-xl transition' - .split(' ') - )); - b.classList.add(`providerButton-${p.name}`) + const dOtherLoginArea = document.querySelector('#otherLogin'); + if (dOtherLoginArea) { + let styleSheetElement = document.createElement('style'); + styleSheetElement.innerText = ""; + // TODO: fetch all the providers from an API ? + const providers: Providers[] = [ + { name: 'discord', display_name: 'Discord', color: { default: 'bg-[#5865F2]', hover: '#FF65F2' } }, + { name: 'kanidm', display_name: 'Kanidm', color: { default: 'bg-red-500', hover: 'bg-red-700' } }, + { name: 'google', display_name: 'Google' }, + ] + let first = true; + for (const p of providers) { + let b = document.createElement('button'); + if (first) b.classList.add('last:col-span-2'); + first = false; + b.classList.add(...( + 'w-full text-white font-medium py-2 rounded-xl transition' + .split(' ') + )); + b.classList.add(`providerButton-${p.name}`) - const col = { default: p.color?.default ?? "bg-gray-600", hover: p.color?.hover ?? "bg-gray-700" }; + const col = { default: p.color?.default ?? "bg-gray-600", hover: p.color?.hover ?? "bg-gray-700" }; - for (const k of Object.keys(col)) { - let c = (col as { [k: string]: string })[k].trim(); - if (c.startsWith('bg-')) { - c = c.replace(/^bg-/, ''); - const customProp = c.match(/^\((.+)\)$/); - const customVal = c.match(/^\[(.+)\]$/); + for (const k of Object.keys(col)) { + let c = (col as { [k: string]: string })[k].trim(); + if (c.startsWith('bg-')) { + c = c.replace(/^bg-/, ''); + const customProp = c.match(/^\((.+)\)$/); + const customVal = c.match(/^\[(.+)\]$/); - if (customProp) - c = `var(${customProp[1]})` - else if (customVal) - c = customVal[1]; - else if (c === 'inherit') - c = 'inherit'; - else if (c === 'current') - c = 'currentColor'; - else if (c === 'transparent') - c = 'transparent'; - else - c = `var(--color-${c})` + if (customProp) + c = `var(${customProp[1]})` + else if (customVal) + c = customVal[1]; + else if (c === 'inherit') + c = 'inherit'; + else if (c === 'current') + c = 'currentColor'; + else if (c === 'transparent') + c = 'transparent'; + else + c = `var(--color-${c})` - } - (col as { [k: string]: string })[k] = c; - } + } + (col as { [k: string]: string })[k] = c; + } - styleSheetElement.innerText += `.providerButton-${p.name} { background-color: ${col.default}; }\n`; - styleSheetElement.innerText += `.providerButton-${p.name}:hover { background-color: ${col.hover}; }\n`; + styleSheetElement.innerText += `.providerButton-${p.name} { background-color: ${col.default}; }\n`; + styleSheetElement.innerText += `.providerButton-${p.name}:hover { background-color: ${col.hover}; }\n`; - b.dataset.display_name = p.display_name; - b.dataset.name = p.name; - if (p.icon_url) b.dataset.icon = p.icon_url; + b.dataset.display_name = p.display_name; + b.dataset.name = p.name; + if (p.icon_url) b.dataset.icon = p.icon_url; - b.innerHTML = ` + b.innerHTML = ` ${p.icon_url ? `${p.display_name} Logo` : ''} ${p.display_name} ` - b.addEventListener('click', () => { - location.href = `/api/auth/oauth2/${p.name}/login`; - }) + b.addEventListener('click', () => { + location.href = `/api/auth/oauth2/${p.name}/login`; + }) - dOtherLoginArea.insertAdjacentElement('afterbegin', b); - } - app?.appendChild(styleSheetElement); - } - } - }; + dOtherLoginArea.insertAdjacentElement('afterbegin', b); + } + app?.appendChild(styleSheetElement); + } + } + }; } diff --git a/nginx-dev/nginx.conf b/nginx-dev/nginx.conf index 93b15d1..766e220 100644 --- a/nginx-dev/nginx.conf +++ b/nginx-dev/nginx.conf @@ -41,7 +41,7 @@ http { } location / { proxy_ssl_verify off; - return 301 'https://$http_host/app/$request_uri'; + proxy_pass http://localhost:5173/; } } } diff --git a/src/@shared/src/auth/index.ts b/src/@shared/src/auth/index.ts index c788d90..71dcce9 100644 --- a/src/@shared/src/auth/index.ts +++ b/src/@shared/src/auth/index.ts @@ -90,10 +90,10 @@ export const authPlugin = fp<{ onlySchema?: boolean }>(async (fastify, { onlySch let schema: TSchema = authSchema; if ('401' in (routeOpts.schema.response as { [k: string]: TSchema })) { const schema_orig = (routeOpts.schema.response as { [k: string]: TSchema })['401']; - if (schema_orig[Typebox.Kind] === 'Union') { + if (Type.IsUnion(schema_orig)) { schema = Typebox.Union([...((schema_orig as Typebox.TUnion).anyOf), authSchema]); } - else if (schema_orig[Typebox.Kind] === 'Object') { + else if (Type.IsObject(schema_orig)) { schema = Typebox.Union([schema_orig, authSchema]); } } @@ -103,26 +103,26 @@ export const authPlugin = fp<{ onlySchema?: boolean }>(async (fastify, { onlySch try { if (isNullish(req.cookies.token)) { return res - .clearCookie('token') + .clearCookie('token', { path: '/' }) .makeResponse(401, 'notLoggedIn', 'auth.noCookie'); } const tok = this.jwt.verify(req.cookies.token); if (tok.kind != 'auth') { return res - .clearCookie('token') + .clearCookie('token', { path: '/' }) .makeResponse(401, 'notLoggedIn', 'auth.invalidKind'); } const user = this.db.getUser(tok.who); if (isNullish(user)) { return res - .clearCookie('token') + .clearCookie('token', { path: '/' }) .makeResponse(401, 'notLoggedIn', 'auth.noUser'); } req.authUser = { id: user.id, name: tok.who }; } catch { return res - .clearCookie('token') + .clearCookie('token', { path: '/' }) .makeResponse(401, 'notLoggedIn', 'auth.invalid'); } };