feat(frontend): added returnTo to login and signin page

This commit is contained in:
Maieul BOYER 2025-11-14 21:59:25 +01:00 committed by Maix0
parent 033d399fcb
commit b1d4f68453
14 changed files with 352 additions and 1460 deletions

View file

@ -15,7 +15,6 @@
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4"
}, },
"dependencies": { "dependencies": {
"@openapitools/openapi-generator-cli": "^2.25.0",
"@tailwindcss/vite": "^4.1.17", "@tailwindcss/vite": "^4.1.17",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"openapi-fetch": "^0.15.0", "openapi-fetch": "^0.15.0",

1376
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@ import { setTitle, handleRoute } from '@app/routing';
import './root/root.ts' import './root/root.ts'
import './chat/chat.ts' import './chat/chat.ts'
import './login/login.ts' import './login/login.ts'
import './signin/signin.ts'
// ---- Initial load ---- // ---- Initial load ----
setTitle(""); setTitle("");

View file

@ -0,0 +1,24 @@
<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">You are already logged in</h1>
<div id="returnToDiv" hidden>
<p class="text-center text-sm text-gray-500 mt-4">
We were asked to redirect you to somewhere when you logged in,
but you already are !
<br />
You can click the button below to go there
</p>
<button id="bReturnTo"
class="w-full bg-green-600 text-white font-medium py-2 rounded-xl hover:bg-gray-700 transition">
Get redirected
</button>
</div>
<p class="text-center text-sm text-gray-500 mt-4">Want to logout ? Click the big button bellow !</p>
<button id="bLogout"
class="w-full bg-gray-600 text-white font-medium py-2 rounded-xl hover:bg-gray-700 transition">
Logout
</button>
<p class="text-center text-sm text-gray-500 mt-4">Otherwise, here is a cute cat picture</p>
<img class="" id="cuteCatImage" hidden />
</div>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 KiB

View file

@ -22,7 +22,7 @@
<button type="submit" <button type="submit"
class="w-full bg-blue-600 text-white font-medium py-2 rounded-xl hover:bg-blue-700 transition"> class="w-full bg-blue-600 text-white font-medium py-2 rounded-xl hover:bg-blue-700 transition">
Sign In Log In
</button> </button>
</form> </form>

View file

@ -1,86 +1,163 @@
import { addRoute, setTitle, type RouteHandlerParams, type RouteHandlerReturn } from "@app/routing"; import {
addRoute,
navigateTo,
setTitle,
type RouteHandlerParams,
type RouteHandlerReturn,
} from "@app/routing";
import { showError, showInfo, showSuccess } from "@app/toast"; 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'; import Cookie from "js-cookie";
import loggedInHtml from "./alreadyLoggedin.html?raw";
import cuteCat from "./cuteCat.png";
import { isNullish } from "@app/utils";
async function handleLogin(
type Providers = { _url: string,
name: string, _args: RouteHandlerParams,
display_name: string, ): Promise<RouteHandlerReturn> {
icon_url?: string, setTitle("Login");
color?: { default: string, hover: string }, let user = await updateUser();
}; const urlParams = new URLSearchParams(window.location.search);
const returnTo = urlParams.get("returnTo");
function handleLogin(_url: string, _args: RouteHandlerParams): RouteHandlerReturn { if (user !== null) {
setTitle('Login') return {
html: loggedInHtml,
postInsert: async (app) => {
const bLogoutButton =
app?.querySelector<HTMLButtonElement>("button#bLogout");
if (isNullish(bLogoutButton))
return showError("Error while rending page");
const iCuteCat =
app?.querySelector<HTMLImageElement>("img#cuteCatImage");
if (isNullish(iCuteCat))
return showError("Error while rending page");
const bReturnTo =
app?.querySelector<HTMLButtonElement>("button#bReturnTo");
if (isNullish(bReturnTo))
return showError("Error while rending page");
iCuteCat.src = cuteCat;
iCuteCat.hidden = false;
bLogoutButton.addEventListener("click", async () => {
await client.logout();
navigateTo("/login");
});
if (returnTo !== null) {
bReturnTo.parentElement!.hidden = false;
bReturnTo.addEventListener("click", async () => {
if (returnTo !== null) navigateTo(returnTo);
});
}
},
};
}
return { return {
html: authHtml, postInsert: async (app) => { html: authHtml,
const fLogin = document.querySelector<HTMLFormElement>('form#login-form'); postInsert: async (app) => {
const aHref =
app?.querySelector<HTMLAnchorElement>('a[href="/signin"]');
if (!isNullish(aHref) && returnTo !== null) {
aHref.href = `/signin?returnTo=${encodeURI(returnTo)}`;
}
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(
fLogin.addEventListener('submit', async function(e: SubmitEvent) { "Error while rendering the page: no form found",
);
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 (
if (!('login' in formData) || typeof formData['login'] !== 'string' || (formData['login'] as string).length === 0) !("login" in formData) ||
return showError('Please enter a Login'); typeof formData["login"] !== "string" ||
if (!('password' in formData) || typeof formData['password'] !== 'string' || (formData['password'] as string).length === 0) (formData["login"] as string).length === 0
return showError('Please enter a Password'); )
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 { 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": {
Cookie.set('token', res.payload.token, { path: '/', sameSite: 'lax' }); 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(
setTitle(`Welcome ${user.guest ? '[GUEST] ' : ''}${user.name}`); "Failed to get user: no user ?",
);
setTitle(
`Welcome ${user.guest ? "[GUEST] " : ""}${user.name}`,
);
if (returnTo !== null) navigateTo(returnTo);
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 =
bLoginAsGuest?.addEventListener('click', async () => { document.querySelector<HTMLButtonElement>("#bGuestLogin");
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": {
Cookie.set('token', res.payload.token, { path: '/', sameSite: 'lax' }); 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(
setTitle(`Welcome ${user.guest ? '[GUEST] ' : ''}${user.name}`); "Failed to get user: no user ?",
);
setTitle(
`Welcome ${user.guest ? "[GUEST] " : ""}${user.name}`,
);
if (returnTo !== null) navigateTo(returnTo);
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 providersReq = await client.providerList(); const providersReq = await client.providerList();
@ -92,37 +169,32 @@ function handleLogin(_url: string, _args: RouteHandlerParams): RouteHandlerRetur
]*/ ]*/
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 && providers.length % 2) b.classList.add('last:col-span-2'); if (first && providers.length % 2)
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 = p.colors; const col = p.colors;
for (const k of Object.keys(col)) { for (const k of Object.keys(col)) {
let c = (col as any)[k].trim(); let c = (col as any)[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) c = customVal[1];
else if (customVal) else if (c === "inherit") c = "inherit";
c = customVal[1]; else if (c === "current") c = "currentColor";
else if (c === 'inherit') else if (c === "transparent") c = "transparent";
c = 'inherit'; else c = `var(--color-${c})`;
else if (c === 'current')
c = 'currentColor';
else if (c === 'transparent')
c = 'transparent';
else
c = `var(--color-${c})`
} }
(col as any)[k] = c; (col as any)[k] = c;
} }
@ -134,19 +206,17 @@ function handleLogin(_url: string, _args: RouteHandlerParams): RouteHandlerRetur
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 = `<span class="">${p.displayName}</span>` b.innerHTML = `<span class="">${p.displayName}</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);
} }
} },
}; };
} }
addRoute("/login", handleLogin, { bypass_auth: true });
addRoute('/login', handleLogin, { bypass_auth: true })

View file

@ -1,14 +1,19 @@
import { addRoute, setTitle, type RouteHandlerParams } from "@app/routing"; import { addRoute, setTitle, type RouteHandlerParams } from "@app/routing";
import page from './root.html?raw' import page from './root.html?raw'
import { updateUser } from "@app/auth";
addRoute('/', (_: string) => { addRoute('/', async (_: string): Promise<string> => {
setTitle('ft boules') let user = await updateUser();
if (user === null)
setTitle(`Welcome`)
else
setTitle(`Welcome ${user.guest ? '[GUEST] ' : ''}${user.name}`);
return page; return page;
}) }, { bypass_auth: true })
addRoute('/with_title/:title', (_: string, args: RouteHandlerParams) => { addRoute('/with_title/:title', (_: string, args: RouteHandlerParams) => {
setTitle(args.title) setTitle(args.title)
console.log(`title should be '${args.title}'`); console.log(`title should be '${args.title}'`);
return page; return page;
}) }, { bypass_auth: false })

View file

@ -0,0 +1,24 @@
<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">You are already logged in</h1>
<div id="returnToDiv" hidden>
<p class="text-center text-sm text-gray-500 mt-4">
We were asked to redirect you to somewhere when you logged in,
but you already are !
<br />
You can click the button below to go there
</p>
<button id="bReturnTo"
class="w-full bg-green-600 text-white font-medium py-2 rounded-xl hover:bg-gray-700 transition">
Get redirected
</button>
</div>
<p class="text-center text-sm text-gray-500 mt-4">Want to logout ? Click the big button bellow !</p>
<button id="bLogout"
class="w-full bg-gray-600 text-white font-medium py-2 rounded-xl hover:bg-gray-700 transition">
Logout
</button>
<p class="text-center text-sm text-gray-500 mt-4">Otherwise, here is a cute cat picture</p>
<img class="" id="cuteCatImage" hidden />
</div>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 KiB

View file

@ -0,0 +1,24 @@
<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>
<form class="space-y-5 pt-3" id="signin-form">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Username</label>
<input type="text" placeholder="Enter your username" name="login"
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" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Password</label>
<input type="password" placeholder="Enter your 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" />
</div>
<button type="submit"
class="w-full bg-blue-600 text-white font-medium py-2 rounded-xl hover:bg-blue-700 transition">
Register
</button>
</form>
</div>
</div>
</div>

View file

@ -0,0 +1,88 @@
import { addRoute, setTitle, navigateTo, type RouteHandlerParams, type RouteHandlerReturn } from "@app/routing";
import { showError, showInfo, showSuccess } from "@app/toast";
import page from './signin.html?raw';
import client from '@app/api'
import { updateUser } from "@app/auth";
import Cookie from 'js-cookie';
import loggedInHtml from './alreadyLoggedin.html?raw';
import { isNullish } from "@app/utils";
import cuteCat from './cuteCat.png';
async function handleSignin(_url: string, _args: RouteHandlerParams): Promise<RouteHandlerReturn> {
setTitle('Signin')
let user = await updateUser();
const urlParams = new URLSearchParams(window.location.search);
const returnTo = urlParams.get("returnTo");
if (user !== null) {
return {
html: loggedInHtml,
postInsert: async (app) => {
const bLogoutButton =
app?.querySelector<HTMLButtonElement>("button#bLogout");
if (isNullish(bLogoutButton))
return showError("Error while rending page");
const iCuteCat =
app?.querySelector<HTMLImageElement>("img#cuteCatImage");
if (isNullish(iCuteCat))
return showError("Error while rending page");
const bReturnTo =
app?.querySelector<HTMLButtonElement>("button#bReturnTo");
if (isNullish(bReturnTo))
return showError("Error while rending page");
iCuteCat.src = cuteCat;
iCuteCat.hidden = false;
bLogoutButton.addEventListener("click", async () => {
await client.logout();
navigateTo("/signin");
});
if (returnTo !== null) {
bReturnTo.parentElement!.hidden = false;
bReturnTo.addEventListener("click", async () => {
if (returnTo !== null) navigateTo(returnTo);
});
}
},
};
}
return {
html: page, postInsert: async (app) => {
const fSignin = document.querySelector<HTMLFormElement>('form#signin-form');
if (fSignin === null)
return showError('Error while rendering the page: no form found');
fSignin.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.signin({ 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 ?');
navigateTo(returnTo !== null ? returnTo : '/')
break;
}
case 'failed': {
showError(`Failed to signin: ${res.msg}`);
}
}
} catch (e) {
console.error("Signin error:", e);
showError('Failed to signin: Unknown error');
}
});
}
}
};
addRoute('/signin', handleSignin, { bypass_auth: true })

View file

@ -1,8 +1,11 @@
import { updateUser } from '@app/auth';
import { route_404 } from './special_routes' import { route_404 } from './special_routes'
// ---- Router logic ---- // ---- Router logic ----
function navigateTo(url: string) { export function navigateTo(url: string) {
history.pushState(null, "", `${url.startsWith('/') ? '/app' : ""}${url}`); if (url.startsWith('/') && !url.startsWith('/app'))
url = `/app${url}`;
history.pushState(null, "", url);
handleRoute(); handleRoute();
} }
@ -40,8 +43,9 @@ export class RouteHandlerData {
} }
constructor(url: string, handler: RouteHandler, special_args: Partial<RouteHandlerSpecialArgs>) { constructor(url: string, handler: RouteHandler, special_args: Partial<RouteHandlerSpecialArgs>) {
this.special_args = RouteHandlerData.SPECIAL_ARGS_DEFAULT; this.special_args = Object.assign({}, RouteHandlerData.SPECIAL_ARGS_DEFAULT);
Object.assign(this.special_args, special_args); Object.assign(this.special_args, special_args);
console.log(url, this.special_args);
let parsed = RouteHandlerData.parseUrl(url); let parsed = RouteHandlerData.parseUrl(url);
this.handler = handler; this.handler = handler;
@ -184,6 +188,11 @@ export async function handleRoute() {
break; break;
} }
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')!; const app = document.getElementById('app')!;
let ret = await executeRouteHandler(route_handler, window.location.pathname, args) let ret = await executeRouteHandler(route_handler, window.location.pathname, args)
app.innerHTML = ret.html; app.innerHTML = ret.html;

View file

@ -0,0 +1,24 @@
<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">You are already logged in</h1>
<div id="returnToDiv" hidden>
<p class="text-center text-sm text-gray-500 mt-4">
We were asked to redirect you to somewhere when you logged in,
but you already are !
<br />
You can click the button below to go there
</p>
<button id="bReturnTo"
class="w-full bg-green-600 text-white font-medium py-2 rounded-xl hover:bg-gray-700 transition">
Get redirected
</button>
</div>
<p class="text-center text-sm text-gray-500 mt-4">Want to logout ? Click the big button bellow !</p>
<button id="bLogout"
class="w-full bg-gray-600 text-white font-medium py-2 rounded-xl hover:bg-gray-700 transition">
Logout
</button>
<p class="text-center text-sm text-gray-500 mt-4">Otherwise, here is a cute cat picture</p>
<img class="" id="cuteCatImage" hidden />
</div>
</div>