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