Skip to content

Commit 9a02c6f

Browse files
authored
feat(api): migrate login and logout to API routes (#1703)
* feat(auth): migrate login and logout to API routes - Replaced form-based login/logout with fetch-based API routes - Updated hooks and components to use new `/api/login` and `/api/logout` endpoints * fix: invalidate on logout * refactor: move `/api/login` routes back to `/login` and `/api/logout` to `/logout` remove breaing change to connected apps
1 parent d48431d commit 9a02c6f

File tree

8 files changed

+107
-88
lines changed

8 files changed

+107
-88
lines changed

src/lib/components/DisclaimerModal.svelte

+19-14
Original file line numberDiff line numberDiff line change
@@ -55,20 +55,25 @@
5555
{/if}
5656
</button>
5757
{#if page.data.loginEnabled}
58-
<form action="{base}/login" target="_parent" method="POST" class="w-full">
59-
<button
60-
type="submit"
61-
class="flex w-full flex-wrap items-center justify-center whitespace-nowrap rounded-full border-2 border-black bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
62-
>
63-
Sign in
64-
{#if envPublic.PUBLIC_APP_NAME === "HuggingChat"}
65-
<span class="flex items-center">
66-
&nbsp;with <LogoHuggingFaceBorderless classNames="text-xl mr-1 ml-1.5 flex-none" /> Hugging
67-
Face
68-
</span>
69-
{/if}
70-
</button>
71-
</form>
58+
<button
59+
onclick={async () => {
60+
const response = await fetch(`${base}/login`, {
61+
method: "POST",
62+
});
63+
if (response.ok) {
64+
window.location.href = await response.text();
65+
}
66+
}}
67+
class="flex w-full flex-wrap items-center justify-center whitespace-nowrap rounded-full border-2 border-black bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
68+
>
69+
Sign in
70+
{#if envPublic.PUBLIC_APP_NAME === "HuggingChat"}
71+
<span class="flex items-center">
72+
&nbsp;with <LogoHuggingFaceBorderless classNames="text-xl mr-1 ml-1.5 flex-none" /> Hugging
73+
Face
74+
</span>
75+
{/if}
76+
</button>
7277
{/if}
7378
</div>
7479
</div>

src/lib/components/LoginModal.svelte

+8-3
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,15 @@
2727
</p>
2828

2929
<form
30-
action="{base}/{page.data.loginRequired ? 'login' : 'settings'}"
31-
target="_parent"
32-
method="POST"
3330
class="flex w-full flex-col items-center gap-2"
31+
onsubmit={async () => {
32+
const response = await fetch(`${base}/login`, {
33+
method: "POST",
34+
});
35+
if (response.ok) {
36+
window.location.href = await response.text();
37+
}
38+
}}
3439
>
3540
{#if page.data.loginRequired}
3641
<button

src/lib/components/NavMenu.svelte

+25-15
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import InfiniteScroll from "./InfiniteScroll.svelte";
1414
import type { Conversation } from "$lib/types/Conversation";
1515
import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination";
16+
import { goto } from "$app/navigation";
1617
1718
interface Props {
1819
conversations: ConvSidebar[];
@@ -141,34 +142,43 @@
141142
class="mt-0.5 flex touch-none flex-col gap-1 rounded-r-xl p-3 text-sm md:bg-gradient-to-l md:from-gray-50 md:dark:from-gray-800/30"
142143
>
143144
{#if user?.username || user?.email}
144-
<form
145-
action="{base}/logout"
146-
method="post"
145+
<button
146+
onclick={async () => {
147+
await fetch(`${base}/logout`, {
148+
method: "POST",
149+
});
150+
await goto(base + "/", { invalidateAll: true });
151+
}}
147152
class="group flex items-center gap-1.5 rounded-lg pl-2.5 pr-2 hover:bg-gray-100 dark:hover:bg-gray-700"
148153
>
149154
<span
150155
class="flex h-9 flex-none shrink items-center gap-1.5 truncate pr-2 text-gray-500 dark:text-gray-400"
151156
>{user?.username || user?.email}</span
152157
>
153158
{#if !user.logoutDisabled}
154-
<button
155-
type="submit"
159+
<span
156160
class="ml-auto h-6 flex-none items-center gap-1.5 rounded-md border bg-white px-2 text-gray-700 shadow-sm group-hover:flex hover:shadow-none dark:border-gray-600 dark:bg-gray-600 dark:text-gray-400 dark:hover:text-gray-300 md:hidden"
157161
>
158162
Sign Out
159-
</button>
163+
</span>
160164
{/if}
161-
</form>
165+
</button>
162166
{/if}
163167
{#if canLogin}
164-
<form action="{base}/login" method="POST" target="_parent">
165-
<button
166-
type="submit"
167-
class="flex h-9 w-full flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
168-
>
169-
Login
170-
</button>
171-
</form>
168+
<button
169+
onclick={async () => {
170+
const response = await fetch(`${base}/login`, {
171+
method: "POST",
172+
credentials: "include",
173+
});
174+
if (response.ok) {
175+
window.location.href = await response.text();
176+
}
177+
}}
178+
class="flex h-9 w-full flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
179+
>
180+
Login
181+
</button>
172182
{/if}
173183
<button
174184
onclick={switchTheme}

src/routes/login/+page.server.ts

-27
This file was deleted.

src/routes/login/+server.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { getOIDCAuthorizationUrl } from "$lib/server/auth";
2+
import { base } from "$app/paths";
3+
import { env } from "$env/dynamic/private";
4+
5+
export async function POST({ request, url, locals }) {
6+
const referer = request.headers.get("referer");
7+
let redirectURI = `${(referer ? new URL(referer) : url).origin}${base}/login/callback`;
8+
9+
// TODO: Handle errors if provider is not responding
10+
11+
if (url.searchParams.has("callback")) {
12+
const callback = url.searchParams.get("callback") || redirectURI;
13+
if (env.ALTERNATIVE_REDIRECT_URLS.includes(callback)) {
14+
redirectURI = callback;
15+
}
16+
}
17+
18+
const authorizationUrl = await getOIDCAuthorizationUrl(
19+
{ redirectURI },
20+
{ sessionId: locals.sessionId }
21+
);
22+
23+
return new Response(authorizationUrl, {
24+
headers: {
25+
"Content-Type": "text/html",
26+
},
27+
});
28+
}

src/routes/login/callback/+page.server.ts src/routes/login/callback/+server.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { redirect, error } from "@sveltejs/kit";
1+
import { error, redirect } from "@sveltejs/kit";
22
import { getOIDCUserData, validateAndParseCsrfToken } from "$lib/server/auth";
33
import { z } from "zod";
44
import { base } from "$app/paths";
5-
import { updateUser } from "./updateUser";
65
import { env } from "$env/dynamic/private";
76
import JSON5 from "json5";
7+
import { updateUser } from "./updateUser.js";
88

99
const allowedUserEmails = z
1010
.array(z.string().email())
@@ -18,7 +18,7 @@ const allowedUserDomains = z
1818
.default([])
1919
.parse(JSON5.parse(env.ALLOWED_USER_DOMAINS));
2020

21-
export async function load({ url, locals, cookies, request, getClientAddress }) {
21+
export async function GET({ url, locals, cookies, request, getClientAddress }) {
2222
const { error: errorName, error_description: errorDescription } = z
2323
.object({
2424
error: z.string().optional(),
@@ -27,7 +27,7 @@ export async function load({ url, locals, cookies, request, getClientAddress })
2727
.parse(Object.fromEntries(url.searchParams.entries()));
2828

2929
if (errorName) {
30-
error(400, errorName + (errorDescription ? ": " + errorDescription : ""));
30+
throw error(400, errorName + (errorDescription ? ": " + errorDescription : ""));
3131
}
3232

3333
const { code, state, iss } = z
@@ -43,7 +43,7 @@ export async function load({ url, locals, cookies, request, getClientAddress })
4343
const validatedToken = await validateAndParseCsrfToken(csrfToken, locals.sessionId);
4444

4545
if (!validatedToken) {
46-
error(403, "Invalid or expired CSRF token");
46+
throw error(403, "Invalid or expired CSRF token");
4747
}
4848

4949
const { userData } = await getOIDCUserData(
@@ -55,19 +55,19 @@ export async function load({ url, locals, cookies, request, getClientAddress })
5555
// Filter by allowed user emails or domains
5656
if (allowedUserEmails.length > 0 || allowedUserDomains.length > 0) {
5757
if (!userData.email) {
58-
error(403, "User not allowed: email not returned");
58+
throw error(403, "User not allowed: email not returned");
5959
}
6060
const emailVerified = userData.email_verified ?? true;
6161
if (!emailVerified) {
62-
error(403, "User not allowed: email not verified");
62+
throw error(403, "User not allowed: email not verified");
6363
}
6464

6565
const emailDomain = userData.email.split("@")[1];
6666
const isEmailAllowed = allowedUserEmails.includes(userData.email);
6767
const isDomainAllowed = allowedUserDomains.includes(emailDomain);
6868

6969
if (!isEmailAllowed && !isDomainAllowed) {
70-
error(403, "User not allowed");
70+
throw error(403, "User not allowed");
7171
}
7272
}
7373

@@ -79,5 +79,5 @@ export async function load({ url, locals, cookies, request, getClientAddress })
7979
ip: getClientAddress(),
8080
});
8181

82-
redirect(302, `${base}/`);
82+
return redirect(302, `${base}/`);
8383
}

src/routes/logout/+page.server.ts

-20
This file was deleted.

src/routes/logout/+server.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { dev } from "$app/environment";
2+
import { base } from "$app/paths";
3+
import { env } from "$env/dynamic/private";
4+
import { collections } from "$lib/server/database";
5+
import { redirect } from "@sveltejs/kit";
6+
7+
export async function POST({ locals, cookies }) {
8+
await collections.sessions.deleteOne({ sessionId: locals.sessionId });
9+
10+
cookies.delete(env.COOKIE_NAME, {
11+
path: "/",
12+
// So that it works inside the space's iframe
13+
sameSite: dev || env.ALLOW_INSECURE_COOKIES === "true" ? "lax" : "none",
14+
secure: !dev && !(env.ALLOW_INSECURE_COOKIES === "true"),
15+
httpOnly: true,
16+
});
17+
return redirect(302, `${base}/`);
18+
}

0 commit comments

Comments
 (0)