Login flow (#193)
Browse files* replace EthicsModal by LoginModal and start binding auth logic
* start implementing login modal + give a try to
@auth
/sveltekit
using Github oauth as a test
* start custom auth implementation instead of auth.js for consistency with moon-landing
* fetch user data from provider
* add migration from anonymous to user + bind frontend
* add missing auth secret + only migrate conversations for pre-existing users
* remove email scope as it's not needed
Co-authored-by: Eliott C. <[email protected]>
* no need to define .well-known path
Co-authored-by: Eliott C. <[email protected]>
* use env var for hf hub website url
* move sessionId to signature on csrf token exchange
* remove ethic modal check and set default settings on new users
* anonymous users can read only + modal on write tries
* refresh session cookie when existing user signin again rather than use the old one
* allow users to keep using the app without loging-in if env var is not present
* typo
* handle denied login
* do not use a form action for login as there is nothing post-ed
* use requiresUser instead of env var to define login modal or not
* move back to a form action for login so user can't be linked to /login directly
* show login modal even for pre-existing users
* fix logic of account creation/updates
* settings insertOne instead of updateOne
* fix missing userId to settings creation
* fix missing updatedAt when updating settings of pre-existing users
* show login modal for everyone + add comments
* fix login modal condition for both required/not required login
* 🔨
* bring back missing form values in login modal
* refactor default settings spread around to a constant
* missing default settings
* typo
* rename a bunch of things to remove SSO references
* always migrate conversations
* remove unneeded sha256 Node specific function, replace with browser crypto API
* fix typings
* Update src/lib/components/LoginModal.svelte
* use authCondition() in callback
* add logout
* 🐛 Fix signout
cc @Grsmto, because "path" of the cookie should be "/"
* fixup! 🐛 Fix signout
* sign out button ui
* add hf logo in the button
---------
Co-authored-by: Eliott C. <[email protected]>
Co-authored-by: Victor Mustar <[email protected]>
- .env +3 -2
- package-lock.json +40 -3
- package.json +2 -1
- src/app.d.ts +2 -2
- src/hooks.server.ts +9 -17
- src/lib/components/{EthicsModal.svelte → LoginModal.svelte} +27 -11
- src/lib/components/NavMenu.svelte +23 -4
- src/lib/components/icons/LogoHuggingFaceBorderless.svelte +50 -0
- src/lib/server/auth.ts +106 -4
- src/lib/stores/errors.ts +1 -0
- src/lib/types/Settings.ts +7 -0
- src/lib/types/User.ts +1 -1
- src/lib/utils/sha256.ts +2 -0
- src/routes/+layout.server.ts +8 -3
- src/routes/+layout.svelte +5 -3
- src/routes/conversation/+server.ts +1 -1
- src/routes/login/+page.server.ts +14 -0
- src/routes/login/callback/+server.ts +109 -0
- src/routes/logout/+page.server.ts +17 -0
- src/routes/settings/+page.server.ts +6 -3
@@ -7,8 +7,9 @@ COOKIE_NAME=hf-chat
|
|
7 |
HF_ACCESS_TOKEN=#hf_<token> from from https://huggingface.co/settings/token
|
8 |
|
9 |
# Parameters to enable "Sign in with HF"
|
10 |
-
|
11 |
-
|
|
|
12 |
|
13 |
# 'name', 'userMessageToken', 'assistantMessageToken' are required
|
14 |
MODELS=`[
|
|
|
7 |
HF_ACCESS_TOKEN=#hf_<token> from from https://huggingface.co/settings/token
|
8 |
|
9 |
# Parameters to enable "Sign in with HF"
|
10 |
+
OPENID_CLIENT_ID=
|
11 |
+
OPENID_CLIENT_SECRET=
|
12 |
+
OPENID_PROVIDER_URL=https://huggingface.co
|
13 |
|
14 |
# 'name', 'userMessageToken', 'assistantMessageToken' are required
|
15 |
MODELS=`[
|
@@ -17,6 +17,7 @@
|
|
17 |
"marked": "^4.3.0",
|
18 |
"mongodb": "^5.3.0",
|
19 |
"nanoid": "^4.0.2",
|
|
|
20 |
"parquetjs": "^0.11.2",
|
21 |
"postcss": "^8.4.21",
|
22 |
"tailwind-scrollbar": "^3.0.0",
|
@@ -2485,6 +2486,14 @@
|
|
2485 |
"jiti": "bin/jiti.js"
|
2486 |
}
|
2487 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2488 |
"node_modules/js-sdsl": {
|
2489 |
"version": "4.3.0",
|
2490 |
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
|
@@ -2615,7 +2624,6 @@
|
|
2615 |
"version": "6.0.0",
|
2616 |
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
2617 |
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
2618 |
-
"dev": true,
|
2619 |
"dependencies": {
|
2620 |
"yallist": "^4.0.0"
|
2621 |
},
|
@@ -2916,6 +2924,14 @@
|
|
2916 |
"node": ">=0.10"
|
2917 |
}
|
2918 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2919 |
"node_modules/once": {
|
2920 |
"version": "1.4.0",
|
2921 |
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
@@ -2939,6 +2955,28 @@
|
|
2939 |
"url": "https://github.com/sponsors/sindresorhus"
|
2940 |
}
|
2941 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2942 |
"node_modules/optionator": {
|
2943 |
"version": "0.9.1",
|
2944 |
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
|
@@ -4458,8 +4496,7 @@
|
|
4458 |
"node_modules/yallist": {
|
4459 |
"version": "4.0.0",
|
4460 |
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
4461 |
-
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
4462 |
-
"dev": true
|
4463 |
},
|
4464 |
"node_modules/yaml": {
|
4465 |
"version": "1.10.2",
|
|
|
17 |
"marked": "^4.3.0",
|
18 |
"mongodb": "^5.3.0",
|
19 |
"nanoid": "^4.0.2",
|
20 |
+
"openid-client": "^5.4.2",
|
21 |
"parquetjs": "^0.11.2",
|
22 |
"postcss": "^8.4.21",
|
23 |
"tailwind-scrollbar": "^3.0.0",
|
|
|
2486 |
"jiti": "bin/jiti.js"
|
2487 |
}
|
2488 |
},
|
2489 |
+
"node_modules/jose": {
|
2490 |
+
"version": "4.14.4",
|
2491 |
+
"resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz",
|
2492 |
+
"integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==",
|
2493 |
+
"funding": {
|
2494 |
+
"url": "https://github.com/sponsors/panva"
|
2495 |
+
}
|
2496 |
+
},
|
2497 |
"node_modules/js-sdsl": {
|
2498 |
"version": "4.3.0",
|
2499 |
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
|
|
|
2624 |
"version": "6.0.0",
|
2625 |
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
2626 |
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
|
|
2627 |
"dependencies": {
|
2628 |
"yallist": "^4.0.0"
|
2629 |
},
|
|
|
2924 |
"node": ">=0.10"
|
2925 |
}
|
2926 |
},
|
2927 |
+
"node_modules/oidc-token-hash": {
|
2928 |
+
"version": "5.0.3",
|
2929 |
+
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz",
|
2930 |
+
"integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==",
|
2931 |
+
"engines": {
|
2932 |
+
"node": "^10.13.0 || >=12.0.0"
|
2933 |
+
}
|
2934 |
+
},
|
2935 |
"node_modules/once": {
|
2936 |
"version": "1.4.0",
|
2937 |
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
|
|
2955 |
"url": "https://github.com/sponsors/sindresorhus"
|
2956 |
}
|
2957 |
},
|
2958 |
+
"node_modules/openid-client": {
|
2959 |
+
"version": "5.4.2",
|
2960 |
+
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.4.2.tgz",
|
2961 |
+
"integrity": "sha512-lIhsdPvJ2RneBm3nGBBhQchpe3Uka//xf7WPHTIglery8gnckvW7Bd9IaQzekzXJvWthCMyi/xVEyGW0RFPytw==",
|
2962 |
+
"dependencies": {
|
2963 |
+
"jose": "^4.14.1",
|
2964 |
+
"lru-cache": "^6.0.0",
|
2965 |
+
"object-hash": "^2.2.0",
|
2966 |
+
"oidc-token-hash": "^5.0.3"
|
2967 |
+
},
|
2968 |
+
"funding": {
|
2969 |
+
"url": "https://github.com/sponsors/panva"
|
2970 |
+
}
|
2971 |
+
},
|
2972 |
+
"node_modules/openid-client/node_modules/object-hash": {
|
2973 |
+
"version": "2.2.0",
|
2974 |
+
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
|
2975 |
+
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
|
2976 |
+
"engines": {
|
2977 |
+
"node": ">= 6"
|
2978 |
+
}
|
2979 |
+
},
|
2980 |
"node_modules/optionator": {
|
2981 |
"version": "0.9.1",
|
2982 |
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
|
|
|
4496 |
"node_modules/yallist": {
|
4497 |
"version": "4.0.0",
|
4498 |
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
4499 |
+
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
|
|
4500 |
},
|
4501 |
"node_modules/yaml": {
|
4502 |
"version": "1.10.2",
|
@@ -36,8 +36,8 @@
|
|
36 |
},
|
37 |
"type": "module",
|
38 |
"dependencies": {
|
39 |
-
"@huggingface/inference": "^2.2.0",
|
40 |
"@huggingface/hub": "^0.5.1",
|
|
|
41 |
"autoprefixer": "^10.4.14",
|
42 |
"date-fns": "^2.29.3",
|
43 |
"dotenv": "^16.0.3",
|
@@ -45,6 +45,7 @@
|
|
45 |
"marked": "^4.3.0",
|
46 |
"mongodb": "^5.3.0",
|
47 |
"nanoid": "^4.0.2",
|
|
|
48 |
"parquetjs": "^0.11.2",
|
49 |
"postcss": "^8.4.21",
|
50 |
"tailwind-scrollbar": "^3.0.0",
|
|
|
36 |
},
|
37 |
"type": "module",
|
38 |
"dependencies": {
|
|
|
39 |
"@huggingface/hub": "^0.5.1",
|
40 |
+
"@huggingface/inference": "^2.2.0",
|
41 |
"autoprefixer": "^10.4.14",
|
42 |
"date-fns": "^2.29.3",
|
43 |
"dotenv": "^16.0.3",
|
|
|
45 |
"marked": "^4.3.0",
|
46 |
"mongodb": "^5.3.0",
|
47 |
"nanoid": "^4.0.2",
|
48 |
+
"openid-client": "^5.4.2",
|
49 |
"parquetjs": "^0.11.2",
|
50 |
"postcss": "^8.4.21",
|
51 |
"tailwind-scrollbar": "^3.0.0",
|
@@ -1,7 +1,7 @@
|
|
1 |
/// <reference types="@sveltejs/kit" />
|
2 |
/// <reference types="unplugin-icons/types/svelte" />
|
3 |
|
4 |
-
import type {
|
5 |
|
6 |
// See https://kit.svelte.dev/docs/types#app
|
7 |
// for information about these interfaces
|
@@ -10,7 +10,7 @@ declare global {
|
|
10 |
// interface Error {}
|
11 |
interface Locals {
|
12 |
sessionId: string;
|
13 |
-
|
14 |
}
|
15 |
// interface PageData {}
|
16 |
// interface Platform {}
|
|
|
1 |
/// <reference types="@sveltejs/kit" />
|
2 |
/// <reference types="unplugin-icons/types/svelte" />
|
3 |
|
4 |
+
import type { User } from "$lib/types/User";
|
5 |
|
6 |
// See https://kit.svelte.dev/docs/types#app
|
7 |
// for information about these interfaces
|
|
|
10 |
// interface Error {}
|
11 |
interface Locals {
|
12 |
sessionId: string;
|
13 |
+
user?: User;
|
14 |
}
|
15 |
// interface PageData {}
|
16 |
// interface Platform {}
|
@@ -1,14 +1,13 @@
|
|
1 |
-
import { dev } from "$app/environment";
|
2 |
import { COOKIE_NAME } from "$env/static/private";
|
3 |
import type { Handle } from "@sveltejs/kit";
|
4 |
import {
|
5 |
PUBLIC_GOOGLE_ANALYTICS_ID,
|
6 |
PUBLIC_DEPRECATED_GOOGLE_ANALYTICS_ID,
|
7 |
} from "$env/static/public";
|
8 |
-
import { addYears } from "date-fns";
|
9 |
import { collections } from "$lib/server/database";
|
10 |
import { base } from "$app/paths";
|
11 |
-
import { requiresUser } from "$lib/server/auth";
|
|
|
12 |
|
13 |
export const handle: Handle = async ({ event, resolve }) => {
|
14 |
const token = event.cookies.get(COOKIE_NAME);
|
@@ -18,10 +17,11 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
18 |
const user = await collections.users.findOne({ sessionId: event.locals.sessionId });
|
19 |
|
20 |
if (user) {
|
21 |
-
event.locals.
|
22 |
}
|
23 |
|
24 |
if (
|
|
|
25 |
!event.url.pathname.startsWith(`${base}/admin`) &&
|
26 |
!["GET", "OPTIONS", "HEAD"].includes(event.request.method)
|
27 |
) {
|
@@ -31,9 +31,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
31 |
|
32 |
if (!user && requiresUser) {
|
33 |
return new Response(
|
34 |
-
sendJson
|
35 |
-
? JSON.stringify({ error: "You need to be logged in first" })
|
36 |
-
: "You need to be logged in first",
|
37 |
{
|
38 |
status: 401,
|
39 |
headers: {
|
@@ -43,7 +41,9 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
43 |
);
|
44 |
}
|
45 |
|
46 |
-
if
|
|
|
|
|
47 |
const hasAcceptedEthicsModal = await collections.settings.countDocuments({
|
48 |
sessionId: event.locals.sessionId,
|
49 |
ethicsModalAcceptedAt: { $exists: true },
|
@@ -65,15 +65,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
65 |
}
|
66 |
}
|
67 |
|
68 |
-
|
69 |
-
event.cookies.set(COOKIE_NAME, event.locals.sessionId, {
|
70 |
-
path: "/",
|
71 |
-
// So that it works inside the space's iframe
|
72 |
-
sameSite: dev ? "lax" : "none",
|
73 |
-
secure: !dev,
|
74 |
-
httpOnly: true,
|
75 |
-
expires: addYears(new Date(), 1),
|
76 |
-
});
|
77 |
|
78 |
let replaced = false;
|
79 |
|
|
|
|
|
1 |
import { COOKIE_NAME } from "$env/static/private";
|
2 |
import type { Handle } from "@sveltejs/kit";
|
3 |
import {
|
4 |
PUBLIC_GOOGLE_ANALYTICS_ID,
|
5 |
PUBLIC_DEPRECATED_GOOGLE_ANALYTICS_ID,
|
6 |
} from "$env/static/public";
|
|
|
7 |
import { collections } from "$lib/server/database";
|
8 |
import { base } from "$app/paths";
|
9 |
+
import { refreshSessionCookie, requiresUser } from "$lib/server/auth";
|
10 |
+
import { ERROR_MESSAGES } from "$lib/stores/errors";
|
11 |
|
12 |
export const handle: Handle = async ({ event, resolve }) => {
|
13 |
const token = event.cookies.get(COOKIE_NAME);
|
|
|
17 |
const user = await collections.users.findOne({ sessionId: event.locals.sessionId });
|
18 |
|
19 |
if (user) {
|
20 |
+
event.locals.user = user;
|
21 |
}
|
22 |
|
23 |
if (
|
24 |
+
!event.url.pathname.startsWith(`${base}/login`) &&
|
25 |
!event.url.pathname.startsWith(`${base}/admin`) &&
|
26 |
!["GET", "OPTIONS", "HEAD"].includes(event.request.method)
|
27 |
) {
|
|
|
31 |
|
32 |
if (!user && requiresUser) {
|
33 |
return new Response(
|
34 |
+
sendJson ? JSON.stringify({ error: ERROR_MESSAGES.authOnly }) : ERROR_MESSAGES.authOnly,
|
|
|
|
|
35 |
{
|
36 |
status: 401,
|
37 |
headers: {
|
|
|
41 |
);
|
42 |
}
|
43 |
|
44 |
+
// if login is not required and the call is not from /settings, we check if the user has accepted the ethics modal first.
|
45 |
+
// If login is required, `ethicsModalAcceptedAt` is already true at this point, so do not pass this condition. This saves a DB call.
|
46 |
+
if (!requiresUser && !event.url.pathname.startsWith(`${base}/settings`)) {
|
47 |
const hasAcceptedEthicsModal = await collections.settings.countDocuments({
|
48 |
sessionId: event.locals.sessionId,
|
49 |
ethicsModalAcceptedAt: { $exists: true },
|
|
|
65 |
}
|
66 |
}
|
67 |
|
68 |
+
refreshSessionCookie(event.cookies, event.locals.sessionId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
|
70 |
let replaced = false;
|
71 |
|
@@ -1,8 +1,10 @@
|
|
1 |
<script lang="ts">
|
2 |
import { enhance } from "$app/forms";
|
3 |
import { base } from "$app/paths";
|
|
|
4 |
import { PUBLIC_VERSION } from "$env/static/public";
|
5 |
import Logo from "$lib/components/icons/Logo.svelte";
|
|
|
6 |
import Modal from "$lib/components/Modal.svelte";
|
7 |
import type { LayoutData } from "../../routes/$types";
|
8 |
|
@@ -31,17 +33,31 @@
|
|
31 |
<p class="px-2 text-sm text-gray-500">
|
32 |
Your conversations will be shared with model authors unless you disable it from your settings.
|
33 |
</p>
|
34 |
-
<form
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
</form>
|
46 |
</div>
|
47 |
</Modal>
|
|
|
1 |
<script lang="ts">
|
2 |
import { enhance } from "$app/forms";
|
3 |
import { base } from "$app/paths";
|
4 |
+
import { page } from "$app/stores";
|
5 |
import { PUBLIC_VERSION } from "$env/static/public";
|
6 |
import Logo from "$lib/components/icons/Logo.svelte";
|
7 |
+
import LogoHuggingFaceBorderless from "$lib/components/icons/LogoHuggingFaceBorderless.svelte";
|
8 |
import Modal from "$lib/components/Modal.svelte";
|
9 |
import type { LayoutData } from "../../routes/$types";
|
10 |
|
|
|
33 |
<p class="px-2 text-sm text-gray-500">
|
34 |
Your conversations will be shared with model authors unless you disable it from your settings.
|
35 |
</p>
|
36 |
+
<form
|
37 |
+
action="{base}/{$page.data.requiresLogin ? 'login' : 'settings'}"
|
38 |
+
use:enhance
|
39 |
+
method="POST"
|
40 |
+
>
|
41 |
+
{#if $page.data.requiresLogin}
|
42 |
+
<button
|
43 |
+
type="submit"
|
44 |
+
class="mt-2 flex items-center whitespace-nowrap rounded-full bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-yellow-500"
|
45 |
+
>
|
46 |
+
Sign in with <LogoHuggingFaceBorderless classNames="text-xl mr-1 ml-1.5" /> Hugging Face
|
47 |
+
</button>
|
48 |
+
<p class="mt-2 px-2 text-sm text-gray-500">to start chatting right away</p>
|
49 |
+
{:else}
|
50 |
+
<input type="hidden" name="ethicsModalAccepted" value={true} />
|
51 |
+
{#each Object.entries(settings) as [key, val]}
|
52 |
+
<input type="hidden" name={key} value={val} />
|
53 |
+
{/each}
|
54 |
+
<button
|
55 |
+
type="submit"
|
56 |
+
class="mt-2 rounded-full bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-yellow-500"
|
57 |
+
>
|
58 |
+
Start chatting
|
59 |
+
</button>
|
60 |
+
{/if}
|
61 |
</form>
|
62 |
</div>
|
63 |
</Modal>
|
@@ -10,12 +10,14 @@
|
|
10 |
const dispatch = createEventDispatcher<{
|
11 |
shareConversation: { id: string; title: string };
|
12 |
clickSettings: void;
|
|
|
13 |
}>();
|
14 |
|
15 |
export let conversations: Array<{
|
16 |
id: string;
|
17 |
title: string;
|
18 |
}> = [];
|
|
|
19 |
</script>
|
20 |
|
21 |
<div class="sticky top-0 flex flex-none items-center justify-between px-3 py-3.5 max-sm:pt-0">
|
@@ -40,17 +42,34 @@
|
|
40 |
<div
|
41 |
class="mt-0.5 flex flex-col gap-1 rounded-r-xl bg-gradient-to-l from-gray-50 p-3 text-sm dark:from-gray-800/30"
|
42 |
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
<button
|
44 |
on:click={switchTheme}
|
45 |
type="button"
|
46 |
-
class="
|
47 |
>
|
48 |
Theme
|
49 |
</button>
|
50 |
<button
|
51 |
on:click={() => dispatch("clickSettings")}
|
52 |
type="button"
|
53 |
-
class="
|
54 |
>
|
55 |
Settings
|
56 |
</button>
|
@@ -58,13 +77,13 @@
|
|
58 |
href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions"
|
59 |
target="_blank"
|
60 |
rel="noreferrer"
|
61 |
-
class="
|
62 |
>
|
63 |
Feedback
|
64 |
</a>
|
65 |
<a
|
66 |
href="{base}/privacy"
|
67 |
-
class="
|
68 |
>
|
69 |
About & Privacy
|
70 |
</a>
|
|
|
10 |
const dispatch = createEventDispatcher<{
|
11 |
shareConversation: { id: string; title: string };
|
12 |
clickSettings: void;
|
13 |
+
clickLogout: void;
|
14 |
}>();
|
15 |
|
16 |
export let conversations: Array<{
|
17 |
id: string;
|
18 |
title: string;
|
19 |
}> = [];
|
20 |
+
export let user: { username: string } | undefined;
|
21 |
</script>
|
22 |
|
23 |
<div class="sticky top-0 flex flex-none items-center justify-between px-3 py-3.5 max-sm:pt-0">
|
|
|
42 |
<div
|
43 |
class="mt-0.5 flex flex-col gap-1 rounded-r-xl bg-gradient-to-l from-gray-50 p-3 text-sm dark:from-gray-800/30"
|
44 |
>
|
45 |
+
{#if user?.username}
|
46 |
+
<form
|
47 |
+
action="{base}/logout"
|
48 |
+
method="post"
|
49 |
+
class="group flex items-center gap-1.5 rounded-lg pl-3 pr-2 hover:bg-gray-100 dark:hover:bg-gray-700"
|
50 |
+
>
|
51 |
+
<span class="flex h-9 flex-none items-center gap-1.5 pr-2 text-gray-500 dark:text-gray-400"
|
52 |
+
>{user?.username}</span
|
53 |
+
>
|
54 |
+
<button
|
55 |
+
type="submit"
|
56 |
+
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"
|
57 |
+
>
|
58 |
+
Sign Out
|
59 |
+
</button>
|
60 |
+
</form>
|
61 |
+
{/if}
|
62 |
<button
|
63 |
on:click={switchTheme}
|
64 |
type="button"
|
65 |
+
class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-3 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
66 |
>
|
67 |
Theme
|
68 |
</button>
|
69 |
<button
|
70 |
on:click={() => dispatch("clickSettings")}
|
71 |
type="button"
|
72 |
+
class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-3 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
73 |
>
|
74 |
Settings
|
75 |
</button>
|
|
|
77 |
href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions"
|
78 |
target="_blank"
|
79 |
rel="noreferrer"
|
80 |
+
class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-3 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
81 |
>
|
82 |
Feedback
|
83 |
</a>
|
84 |
<a
|
85 |
href="{base}/privacy"
|
86 |
+
class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-3 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
87 |
>
|
88 |
About & Privacy
|
89 |
</a>
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
export let classNames = "";
|
3 |
+
</script>
|
4 |
+
|
5 |
+
<svg
|
6 |
+
class={classNames}
|
7 |
+
xmlns="http://www.w3.org/2000/svg"
|
8 |
+
width="1em"
|
9 |
+
height="1em"
|
10 |
+
fill="none"
|
11 |
+
viewBox="0 0 95 88"
|
12 |
+
>
|
13 |
+
<path fill="#FFD21E" d="M47.21 76.5a34.75 34.75 0 1 0 0-69.5 34.75 34.75 0 0 0 0 69.5Z" />
|
14 |
+
<path
|
15 |
+
fill="#FF9D0B"
|
16 |
+
d="M81.96 41.75a34.75 34.75 0 1 0-69.5 0 34.75 34.75 0 0 0 69.5 0Zm-73.5 0a38.75 38.75 0 1 1 77.5 0 38.75 38.75 0 0 1-77.5 0Z"
|
17 |
+
/>
|
18 |
+
<path
|
19 |
+
fill="#3A3B45"
|
20 |
+
d="M58.5 32.3c1.28.44 1.78 3.06 3.07 2.38a5 5 0 1 0-6.76-2.07c.61 1.15 2.55-.72 3.7-.32ZM34.95 32.3c-1.28.44-1.79 3.06-3.07 2.38a5 5 0 1 1 6.76-2.07c-.61 1.15-2.56-.72-3.7-.32ZM46.96 56.29c9.83 0 13-8.76 13-13.26 0-2.34-1.57-1.6-4.09-.36-2.33 1.15-5.46 2.74-8.9 2.74-7.19 0-13-6.88-13-2.38s3.16 13.26 13 13.26Z"
|
21 |
+
/>
|
22 |
+
<mask id="a" width="27" height="16" x="33" y="41" maskUnits="userSpaceOnUse">
|
23 |
+
<path
|
24 |
+
fill="#fff"
|
25 |
+
d="M46.96 56.29c9.83 0 13-8.76 13-13.26 0-2.34-1.57-1.6-4.09-.36-2.33 1.15-5.46 2.74-8.9 2.74-7.19 0-13-6.88-13-2.38s3.16 13.26 13 13.26Z"
|
26 |
+
/>
|
27 |
+
</mask>
|
28 |
+
<g mask="url(#a)">
|
29 |
+
<path
|
30 |
+
fill="#F94040"
|
31 |
+
d="M47.21 66.5a8.67 8.67 0 0 0 2.65-16.94c-.84-.26-1.73 2.6-2.65 2.6-.86 0-1.7-2.88-2.48-2.65a8.68 8.68 0 0 0 2.48 16.99Z"
|
32 |
+
/>
|
33 |
+
</g>
|
34 |
+
<path
|
35 |
+
fill="#FF9D0B"
|
36 |
+
d="M70.71 37a3.25 3.25 0 1 0 0-6.5 3.25 3.25 0 0 0 0 6.5ZM24.21 37a3.25 3.25 0 1 0 0-6.5 3.25 3.25 0 0 0 0 6.5ZM17.52 48c-1.62 0-3.06.66-4.07 1.87a5.97 5.97 0 0 0-1.33 3.76 7.1 7.1 0 0 0-1.94-.3c-1.55 0-2.95.59-3.94 1.66a5.8 5.8 0 0 0-.8 7 5.3 5.3 0 0 0-1.79 2.82c-.24.9-.48 2.8.8 4.74a5.22 5.22 0 0 0-.37 5.02c1.02 2.32 3.57 4.14 8.52 6.1 3.07 1.22 5.89 2 5.91 2.01a44.33 44.33 0 0 0 10.93 1.6c5.86 0 10.05-1.8 12.46-5.34 3.88-5.69 3.33-10.9-1.7-15.92-2.77-2.78-4.62-6.87-5-7.77-.78-2.66-2.84-5.62-6.25-5.62a5.7 5.7 0 0 0-4.6 2.46c-1-1.26-1.98-2.25-2.86-2.82A7.4 7.4 0 0 0 17.52 48Zm0 4c.51 0 1.14.22 1.82.65 2.14 1.36 6.25 8.43 7.76 11.18.5.92 1.37 1.31 2.14 1.31 1.55 0 2.75-1.53.15-3.48-3.92-2.93-2.55-7.72-.68-8.01.08-.02.17-.02.24-.02 1.7 0 2.45 2.93 2.45 2.93s2.2 5.52 5.98 9.3c3.77 3.77 3.97 6.8 1.22 10.83-1.88 2.75-5.47 3.58-9.16 3.58-3.81 0-7.73-.9-9.92-1.46-.11-.03-13.45-3.8-11.76-7 .28-.54.75-.76 1.34-.76 2.38 0 6.7 3.54 8.57 3.54.41 0 .7-.17.83-.6.79-2.85-12.06-4.05-10.98-8.17.2-.73.71-1.02 1.44-1.02 3.14 0 10.2 5.53 11.68 5.53.11 0 .2-.03.24-.1.74-1.2.33-2.04-4.9-5.2-5.21-3.16-8.88-5.06-6.8-7.33.24-.26.58-.38 1-.38 3.17 0 10.66 6.82 10.66 6.82s2.02 2.1 3.25 2.1c.28 0 .52-.1.68-.38.86-1.46-8.06-8.22-8.56-11.01-.34-1.9.24-2.85 1.31-2.85Z"
|
37 |
+
/>
|
38 |
+
<path
|
39 |
+
fill="#FFD21E"
|
40 |
+
d="M38.6 76.69c2.75-4.04 2.55-7.07-1.22-10.84-3.78-3.77-5.98-9.3-5.98-9.3s-.82-3.2-2.69-2.9c-1.87.3-3.24 5.08.68 8.01 3.91 2.93-.78 4.92-2.29 2.17-1.5-2.75-5.62-9.82-7.76-11.18-2.13-1.35-3.63-.6-3.13 2.2.5 2.79 9.43 9.55 8.56 11-.87 1.47-3.93-1.71-3.93-1.71s-9.57-8.71-11.66-6.44c-2.08 2.27 1.59 4.17 6.8 7.33 5.23 3.16 5.64 4 4.9 5.2-.75 1.2-12.28-8.53-13.36-4.4-1.08 4.11 11.77 5.3 10.98 8.15-.8 2.85-9.06-5.38-10.74-2.18-1.7 3.21 11.65 6.98 11.76 7.01 4.3 1.12 15.25 3.49 19.08-2.12Z"
|
41 |
+
/>
|
42 |
+
<path
|
43 |
+
fill="#FF9D0B"
|
44 |
+
d="M77.4 48c1.62 0 3.07.66 4.07 1.87a5.97 5.97 0 0 1 1.33 3.76 7.1 7.1 0 0 1 1.95-.3c1.55 0 2.95.59 3.94 1.66a5.8 5.8 0 0 1 .8 7 5.3 5.3 0 0 1 1.78 2.82c.24.9.48 2.8-.8 4.74a5.22 5.22 0 0 1 .37 5.02c-1.02 2.32-3.57 4.14-8.51 6.1-3.08 1.22-5.9 2-5.92 2.01a44.33 44.33 0 0 1-10.93 1.6c-5.86 0-10.05-1.8-12.46-5.34-3.88-5.69-3.33-10.9 1.7-15.92 2.78-2.78 4.63-6.87 5.01-7.77.78-2.66 2.83-5.62 6.24-5.62a5.7 5.7 0 0 1 4.6 2.46c1-1.26 1.98-2.25 2.87-2.82A7.4 7.4 0 0 1 77.4 48Zm0 4c-.51 0-1.13.22-1.82.65-2.13 1.36-6.25 8.43-7.76 11.18a2.43 2.43 0 0 1-2.14 1.31c-1.54 0-2.75-1.53-.14-3.48 3.91-2.93 2.54-7.72.67-8.01a1.54 1.54 0 0 0-.24-.02c-1.7 0-2.45 2.93-2.45 2.93s-2.2 5.52-5.97 9.3c-3.78 3.77-3.98 6.8-1.22 10.83 1.87 2.75 5.47 3.58 9.15 3.58 3.82 0 7.73-.9 9.93-1.46.1-.03 13.45-3.8 11.76-7-.29-.54-.75-.76-1.34-.76-2.38 0-6.71 3.54-8.57 3.54-.42 0-.71-.17-.83-.6-.8-2.85 12.05-4.05 10.97-8.17-.19-.73-.7-1.02-1.44-1.02-3.14 0-10.2 5.53-11.68 5.53-.1 0-.19-.03-.23-.1-.74-1.2-.34-2.04 4.88-5.2 5.23-3.16 8.9-5.06 6.8-7.33-.23-.26-.57-.38-.98-.38-3.18 0-10.67 6.82-10.67 6.82s-2.02 2.1-3.24 2.1a.74.74 0 0 1-.68-.38c-.87-1.46 8.05-8.22 8.55-11.01.34-1.9-.24-2.85-1.31-2.85Z"
|
45 |
+
/>
|
46 |
+
<path
|
47 |
+
fill="#FFD21E"
|
48 |
+
d="M56.33 76.69c-2.75-4.04-2.56-7.07 1.22-10.84 3.77-3.77 5.97-9.3 5.97-9.3s.82-3.2 2.7-2.9c1.86.3 3.23 5.08-.68 8.01-3.92 2.93.78 4.92 2.28 2.17 1.51-2.75 5.63-9.82 7.76-11.18 2.13-1.35 3.64-.6 3.13 2.2-.5 2.79-9.42 9.55-8.55 11 .86 1.47 3.92-1.71 3.92-1.71s9.58-8.71 11.66-6.44c2.08 2.27-1.58 4.17-6.8 7.33-5.23 3.16-5.63 4-4.9 5.2.75 1.2 12.28-8.53 13.36-4.4 1.08 4.11-11.76 5.3-10.97 8.15.8 2.85 9.05-5.38 10.74-2.18 1.69 3.21-11.65 6.98-11.76 7.01-4.31 1.12-15.26 3.49-19.08-2.12Z"
|
49 |
+
/>
|
50 |
+
</svg>
|
@@ -1,9 +1,111 @@
|
|
1 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
-
export
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
|
5 |
export const authCondition = (locals: App.Locals) => {
|
6 |
-
return locals.
|
7 |
-
? { userId: locals.
|
8 |
: { sessionId: locals.sessionId, userId: { $exists: false } };
|
9 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Issuer, BaseClient, type UserinfoResponse, TokenSet } from "openid-client";
|
2 |
+
import { addDays, addYears } from "date-fns";
|
3 |
+
import {
|
4 |
+
COOKIE_NAME,
|
5 |
+
OPENID_CLIENT_ID,
|
6 |
+
OPENID_CLIENT_SECRET,
|
7 |
+
OPENID_PROVIDER_URL,
|
8 |
+
} from "$env/static/private";
|
9 |
+
import { PUBLIC_ORIGIN } from "$env/static/public";
|
10 |
+
import { sha256 } from "$lib/utils/sha256";
|
11 |
+
import { z } from "zod";
|
12 |
+
import { base } from "$app/paths";
|
13 |
+
import { dev } from "$app/environment";
|
14 |
+
import type { Cookies } from "@sveltejs/kit";
|
15 |
|
16 |
+
export interface OIDCSettings {
|
17 |
+
redirectURI: string;
|
18 |
+
}
|
19 |
+
|
20 |
+
export interface OIDCUserInfo {
|
21 |
+
token: TokenSet;
|
22 |
+
userData: UserinfoResponse;
|
23 |
+
}
|
24 |
+
|
25 |
+
export const requiresUser = !!OPENID_CLIENT_ID && !!OPENID_CLIENT_SECRET;
|
26 |
+
|
27 |
+
export function refreshSessionCookie(cookies: Cookies, sessionId: string) {
|
28 |
+
cookies.set(COOKIE_NAME, sessionId, {
|
29 |
+
path: "/",
|
30 |
+
// So that it works inside the space's iframe
|
31 |
+
sameSite: dev ? "lax" : "none",
|
32 |
+
secure: !dev,
|
33 |
+
httpOnly: true,
|
34 |
+
expires: addYears(new Date(), 1),
|
35 |
+
});
|
36 |
+
}
|
37 |
+
|
38 |
+
export const getRedirectURI = (url: URL) => `${PUBLIC_ORIGIN || url.origin}${base}/login/callback`;
|
39 |
+
|
40 |
+
export const OIDC_SCOPES = "openid profile";
|
41 |
|
42 |
export const authCondition = (locals: App.Locals) => {
|
43 |
+
return locals.user
|
44 |
+
? { userId: locals.user._id }
|
45 |
: { sessionId: locals.sessionId, userId: { $exists: false } };
|
46 |
};
|
47 |
+
|
48 |
+
/**
|
49 |
+
* Generates a CSRF token using the user sessionId. Note that we don't need a secret because sessionId is enough.
|
50 |
+
*/
|
51 |
+
export async function generateCsrfToken(sessionId: string): Promise<string> {
|
52 |
+
const data = { expiration: addDays(new Date(), 1).getTime() };
|
53 |
+
|
54 |
+
return Buffer.from(
|
55 |
+
JSON.stringify({
|
56 |
+
data,
|
57 |
+
signature: await sha256(JSON.stringify(data) + "##" + sessionId),
|
58 |
+
})
|
59 |
+
).toString("base64");
|
60 |
+
}
|
61 |
+
|
62 |
+
async function getOIDCClient(settings: OIDCSettings): Promise<BaseClient> {
|
63 |
+
const issuer = await Issuer.discover(OPENID_PROVIDER_URL);
|
64 |
+
return new issuer.Client({
|
65 |
+
client_id: OPENID_CLIENT_ID,
|
66 |
+
client_secret: OPENID_CLIENT_SECRET,
|
67 |
+
redirect_uris: [settings.redirectURI],
|
68 |
+
response_types: ["code"],
|
69 |
+
});
|
70 |
+
}
|
71 |
+
|
72 |
+
export async function getOIDCAuthorizationUrl(
|
73 |
+
settings: OIDCSettings,
|
74 |
+
params: { sessionId: string }
|
75 |
+
): Promise<string> {
|
76 |
+
const client = await getOIDCClient(settings);
|
77 |
+
const csrfToken = await generateCsrfToken(params.sessionId);
|
78 |
+
const url = client.authorizationUrl({
|
79 |
+
scope: OIDC_SCOPES,
|
80 |
+
state: csrfToken,
|
81 |
+
});
|
82 |
+
|
83 |
+
return url;
|
84 |
+
}
|
85 |
+
|
86 |
+
export async function getOIDCUserData(settings: OIDCSettings, code: string): Promise<OIDCUserInfo> {
|
87 |
+
const client = await getOIDCClient(settings);
|
88 |
+
const token = await client.callback(settings.redirectURI, { code });
|
89 |
+
const userData = await client.userinfo(token);
|
90 |
+
|
91 |
+
return { token, userData };
|
92 |
+
}
|
93 |
+
|
94 |
+
export async function validateCsrfToken(token: string, sessionId: string) {
|
95 |
+
try {
|
96 |
+
const { data, signature } = z
|
97 |
+
.object({
|
98 |
+
data: z.object({
|
99 |
+
expiration: z.number().int(),
|
100 |
+
}),
|
101 |
+
signature: z.string().length(64),
|
102 |
+
})
|
103 |
+
.parse(JSON.parse(token));
|
104 |
+
const reconstructSign = await sha256(JSON.stringify(data) + "##" + sessionId);
|
105 |
+
|
106 |
+
return data.expiration > Date.now() && signature === reconstructSign;
|
107 |
+
} catch (e) {
|
108 |
+
console.error(e);
|
109 |
+
return false;
|
110 |
+
}
|
111 |
+
}
|
@@ -2,6 +2,7 @@ import { writable } from "svelte/store";
|
|
2 |
|
3 |
export const ERROR_MESSAGES = {
|
4 |
default: "Oops, something went wrong.",
|
|
|
5 |
};
|
6 |
|
7 |
export const error = writable<string | null>(null);
|
|
|
2 |
|
3 |
export const ERROR_MESSAGES = {
|
4 |
default: "Oops, something went wrong.",
|
5 |
+
authOnly: "You have to be logged in.",
|
6 |
};
|
7 |
|
8 |
export const error = writable<string | null>(null);
|
@@ -1,3 +1,4 @@
|
|
|
|
1 |
import type { Timestamps } from "./Timestamps";
|
2 |
import type { User } from "./User";
|
3 |
|
@@ -14,3 +15,9 @@ export interface Settings extends Timestamps {
|
|
14 |
ethicsModalAcceptedAt: Date | null;
|
15 |
activeModel: string;
|
16 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { defaultModel } from "$lib/server/models";
|
2 |
import type { Timestamps } from "./Timestamps";
|
3 |
import type { User } from "./User";
|
4 |
|
|
|
15 |
ethicsModalAcceptedAt: Date | null;
|
16 |
activeModel: string;
|
17 |
}
|
18 |
+
|
19 |
+
// TODO: move this to a constant file along with other constants
|
20 |
+
export const DEFAULT_SETTINGS = {
|
21 |
+
shareConversationsWithModelAuthors: true,
|
22 |
+
activeModel: defaultModel.id,
|
23 |
+
};
|
@@ -10,5 +10,5 @@ export interface User extends Timestamps {
|
|
10 |
hfUserId: string;
|
11 |
|
12 |
// Session identifier, stored in the cookie
|
13 |
-
sessionId
|
14 |
}
|
|
|
10 |
hfUserId: string;
|
11 |
|
12 |
// Session identifier, stored in the cookie
|
13 |
+
sessionId: string;
|
14 |
}
|
@@ -1,3 +1,5 @@
|
|
|
|
|
|
1 |
export async function sha256(input: string): Promise<string> {
|
2 |
const utf8 = new TextEncoder().encode(input);
|
3 |
const hashBuffer = await crypto.subtle.digest("SHA-256", utf8);
|
|
|
1 |
+
import * as crypto from "crypto";
|
2 |
+
|
3 |
export async function sha256(input: string): Promise<string> {
|
4 |
const utf8 = new TextEncoder().encode(input);
|
5 |
const hashBuffer = await crypto.subtle.digest("SHA-256", utf8);
|
@@ -4,7 +4,8 @@ import { collections } from "$lib/server/database";
|
|
4 |
import type { Conversation } from "$lib/types/Conversation";
|
5 |
import { UrlDependency } from "$lib/types/UrlDependency";
|
6 |
import { defaultModel, models, oldModels, validateModel } from "$lib/server/models";
|
7 |
-
import { authCondition } from "$lib/server/auth";
|
|
|
8 |
|
9 |
export const load: LayoutServerLoad = async ({ locals, depends, url }) => {
|
10 |
const { conversations } = collections;
|
@@ -54,9 +55,11 @@ export const load: LayoutServerLoad = async ({ locals, depends, url }) => {
|
|
54 |
}))
|
55 |
.toArray(),
|
56 |
settings: {
|
57 |
-
shareConversationsWithModelAuthors:
|
|
|
|
|
58 |
ethicsModalAcceptedAt: settings?.ethicsModalAcceptedAt ?? null,
|
59 |
-
activeModel: settings?.activeModel ??
|
60 |
},
|
61 |
models: models.map((model) => ({
|
62 |
id: model.id,
|
@@ -69,5 +72,7 @@ export const load: LayoutServerLoad = async ({ locals, depends, url }) => {
|
|
69 |
parameters: model.parameters,
|
70 |
})),
|
71 |
oldModels,
|
|
|
|
|
72 |
};
|
73 |
};
|
|
|
4 |
import type { Conversation } from "$lib/types/Conversation";
|
5 |
import { UrlDependency } from "$lib/types/UrlDependency";
|
6 |
import { defaultModel, models, oldModels, validateModel } from "$lib/server/models";
|
7 |
+
import { authCondition, requiresUser } from "$lib/server/auth";
|
8 |
+
import { DEFAULT_SETTINGS } from "$lib/types/Settings";
|
9 |
|
10 |
export const load: LayoutServerLoad = async ({ locals, depends, url }) => {
|
11 |
const { conversations } = collections;
|
|
|
55 |
}))
|
56 |
.toArray(),
|
57 |
settings: {
|
58 |
+
shareConversationsWithModelAuthors:
|
59 |
+
settings?.shareConversationsWithModelAuthors ??
|
60 |
+
DEFAULT_SETTINGS.shareConversationsWithModelAuthors,
|
61 |
ethicsModalAcceptedAt: settings?.ethicsModalAcceptedAt ?? null,
|
62 |
+
activeModel: settings?.activeModel ?? DEFAULT_SETTINGS.activeModel,
|
63 |
},
|
64 |
models: models.map((model) => ({
|
65 |
id: model.id,
|
|
|
72 |
parameters: model.parameters,
|
73 |
})),
|
74 |
oldModels,
|
75 |
+
user: locals.user && { username: locals.user.username, avatarUrl: locals.user.avatarUrl },
|
76 |
+
requiresLogin: requiresUser,
|
77 |
};
|
78 |
};
|
@@ -13,8 +13,8 @@
|
|
13 |
import MobileNav from "$lib/components/MobileNav.svelte";
|
14 |
import NavMenu from "$lib/components/NavMenu.svelte";
|
15 |
import Toast from "$lib/components/Toast.svelte";
|
16 |
-
import EthicsModal from "$lib/components/EthicsModal.svelte";
|
17 |
import SettingsModal from "$lib/components/SettingsModal.svelte";
|
|
|
18 |
|
19 |
export let data;
|
20 |
|
@@ -113,6 +113,7 @@
|
|
113 |
>
|
114 |
<NavMenu
|
115 |
conversations={data.conversations}
|
|
|
116 |
on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
|
117 |
on:deleteConversation={(ev) => deleteConversation(ev.detail)}
|
118 |
on:clickSettings={() => (isSettingsOpen = true)}
|
@@ -122,6 +123,7 @@
|
|
122 |
<nav class="grid max-h-screen grid-cols-1 grid-rows-[auto,1fr,auto] max-md:hidden">
|
123 |
<NavMenu
|
124 |
conversations={data.conversations}
|
|
|
125 |
on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
|
126 |
on:deleteConversation={(ev) => deleteConversation(ev.detail)}
|
127 |
on:clickSettings={() => (isSettingsOpen = true)}
|
@@ -134,8 +136,8 @@
|
|
134 |
{#if isSettingsOpen}
|
135 |
<SettingsModal on:close={() => (isSettingsOpen = false)} settings={data.settings} />
|
136 |
{/if}
|
137 |
-
{#if !data.settings.ethicsModalAcceptedAt}
|
138 |
-
<
|
139 |
{/if}
|
140 |
<slot />
|
141 |
</div>
|
|
|
13 |
import MobileNav from "$lib/components/MobileNav.svelte";
|
14 |
import NavMenu from "$lib/components/NavMenu.svelte";
|
15 |
import Toast from "$lib/components/Toast.svelte";
|
|
|
16 |
import SettingsModal from "$lib/components/SettingsModal.svelte";
|
17 |
+
import LoginModal from "$lib/components/LoginModal.svelte";
|
18 |
|
19 |
export let data;
|
20 |
|
|
|
113 |
>
|
114 |
<NavMenu
|
115 |
conversations={data.conversations}
|
116 |
+
user={data.user}
|
117 |
on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
|
118 |
on:deleteConversation={(ev) => deleteConversation(ev.detail)}
|
119 |
on:clickSettings={() => (isSettingsOpen = true)}
|
|
|
123 |
<nav class="grid max-h-screen grid-cols-1 grid-rows-[auto,1fr,auto] max-md:hidden">
|
124 |
<NavMenu
|
125 |
conversations={data.conversations}
|
126 |
+
user={data.user}
|
127 |
on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
|
128 |
on:deleteConversation={(ev) => deleteConversation(ev.detail)}
|
129 |
on:clickSettings={() => (isSettingsOpen = true)}
|
|
|
136 |
{#if isSettingsOpen}
|
137 |
<SettingsModal on:close={() => (isSettingsOpen = false)} settings={data.settings} />
|
138 |
{/if}
|
139 |
+
{#if data.requiresLogin ? !data.user : !data.settings.ethicsModalAcceptedAt}
|
140 |
+
<LoginModal settings={data.settings} />
|
141 |
{/if}
|
142 |
<slot />
|
143 |
</div>
|
@@ -44,7 +44,7 @@ export const POST: RequestHandler = async ({ locals, request }) => {
|
|
44 |
model: values.model,
|
45 |
createdAt: new Date(),
|
46 |
updatedAt: new Date(),
|
47 |
-
...(locals.
|
48 |
...(values.fromShare ? { meta: { fromShareId: values.fromShare } } : {}),
|
49 |
});
|
50 |
|
|
|
44 |
model: values.model,
|
45 |
createdAt: new Date(),
|
46 |
updatedAt: new Date(),
|
47 |
+
...(locals.user ? { userId: locals.user._id } : { sessionId: locals.sessionId }),
|
48 |
...(values.fromShare ? { meta: { fromShareId: values.fromShare } } : {}),
|
49 |
});
|
50 |
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { redirect } from "@sveltejs/kit";
|
2 |
+
import { getOIDCAuthorizationUrl, getRedirectURI } from "$lib/server/auth";
|
3 |
+
|
4 |
+
export const actions = {
|
5 |
+
default: async function ({ url, locals }) {
|
6 |
+
// TODO: Handle errors if provider is not responding
|
7 |
+
const authorizationUrl = await getOIDCAuthorizationUrl(
|
8 |
+
{ redirectURI: getRedirectURI(url) },
|
9 |
+
{ sessionId: locals.sessionId }
|
10 |
+
);
|
11 |
+
|
12 |
+
throw redirect(303, authorizationUrl);
|
13 |
+
},
|
14 |
+
};
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { redirect, error } from "@sveltejs/kit";
|
2 |
+
import {
|
3 |
+
authCondition,
|
4 |
+
getOIDCUserData,
|
5 |
+
getRedirectURI,
|
6 |
+
refreshSessionCookie,
|
7 |
+
validateCsrfToken,
|
8 |
+
} from "$lib/server/auth";
|
9 |
+
import { z } from "zod";
|
10 |
+
import { collections } from "$lib/server/database";
|
11 |
+
import { ObjectId } from "mongodb";
|
12 |
+
import { base } from "$app/paths";
|
13 |
+
import { DEFAULT_SETTINGS } from "$lib/types/Settings";
|
14 |
+
|
15 |
+
export async function GET({ url, locals, cookies }) {
|
16 |
+
const { error: errorName } = z
|
17 |
+
.object({
|
18 |
+
error: z.string().optional(),
|
19 |
+
})
|
20 |
+
.parse(Object.fromEntries(url.searchParams.entries()));
|
21 |
+
|
22 |
+
if (errorName) {
|
23 |
+
// TODO: Display denied error on the UI
|
24 |
+
throw redirect(302, base || "/");
|
25 |
+
}
|
26 |
+
|
27 |
+
const { code, state } = z
|
28 |
+
.object({
|
29 |
+
code: z.string(),
|
30 |
+
state: z.string(),
|
31 |
+
})
|
32 |
+
.parse(Object.fromEntries(url.searchParams.entries()));
|
33 |
+
|
34 |
+
const csrfToken = Buffer.from(state, "base64").toString("utf-8");
|
35 |
+
|
36 |
+
const isValidToken = await validateCsrfToken(csrfToken, locals.sessionId);
|
37 |
+
|
38 |
+
if (!isValidToken) {
|
39 |
+
throw error(403, "Invalid or expired CSRF token");
|
40 |
+
}
|
41 |
+
|
42 |
+
const { userData } = await getOIDCUserData({ redirectURI: getRedirectURI(url) }, code);
|
43 |
+
|
44 |
+
const {
|
45 |
+
preferred_username: username,
|
46 |
+
name,
|
47 |
+
picture: avatarUrl,
|
48 |
+
sub: hfUserId,
|
49 |
+
} = z
|
50 |
+
.object({
|
51 |
+
preferred_username: z.string(),
|
52 |
+
name: z.string(),
|
53 |
+
picture: z.string(),
|
54 |
+
sub: z.string(),
|
55 |
+
})
|
56 |
+
.parse(userData);
|
57 |
+
|
58 |
+
const existingUser = await collections.users.findOne({ hfUserId });
|
59 |
+
let userId = existingUser?._id;
|
60 |
+
|
61 |
+
if (existingUser) {
|
62 |
+
// update existing user if any
|
63 |
+
await collections.users.updateOne(
|
64 |
+
{ _id: existingUser._id },
|
65 |
+
{ $set: { username, name, avatarUrl } }
|
66 |
+
);
|
67 |
+
// refresh session cookie
|
68 |
+
refreshSessionCookie(cookies, existingUser.sessionId);
|
69 |
+
} else {
|
70 |
+
// user doesn't exist yet, create a new one
|
71 |
+
const { insertedId } = await collections.users.insertOne({
|
72 |
+
_id: new ObjectId(),
|
73 |
+
createdAt: new Date(),
|
74 |
+
updatedAt: new Date(),
|
75 |
+
username,
|
76 |
+
name,
|
77 |
+
avatarUrl,
|
78 |
+
hfUserId,
|
79 |
+
sessionId: locals.sessionId,
|
80 |
+
});
|
81 |
+
|
82 |
+
userId = insertedId;
|
83 |
+
|
84 |
+
// update pre-existing settings
|
85 |
+
const { matchedCount } = await collections.settings.updateOne(authCondition(locals), {
|
86 |
+
$set: { userId, updatedAt: new Date() },
|
87 |
+
$unset: { sessionId: "" },
|
88 |
+
});
|
89 |
+
|
90 |
+
if (!matchedCount) {
|
91 |
+
// update settings if existing or create new default ones
|
92 |
+
await collections.settings.insertOne({
|
93 |
+
userId,
|
94 |
+
ethicsModalAcceptedAt: new Date(),
|
95 |
+
updatedAt: new Date(),
|
96 |
+
createdAt: new Date(),
|
97 |
+
...DEFAULT_SETTINGS,
|
98 |
+
});
|
99 |
+
}
|
100 |
+
}
|
101 |
+
|
102 |
+
// migrate pre-existing conversations
|
103 |
+
await collections.conversations.updateMany(authCondition(locals), {
|
104 |
+
$set: { userId },
|
105 |
+
$unset: { sessionId: "" },
|
106 |
+
});
|
107 |
+
|
108 |
+
throw redirect(302, base || "/");
|
109 |
+
}
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { dev } from "$app/environment";
|
2 |
+
import { base } from "$app/paths";
|
3 |
+
import { COOKIE_NAME } from "$env/static/private";
|
4 |
+
import { redirect } from "@sveltejs/kit";
|
5 |
+
|
6 |
+
export const actions = {
|
7 |
+
default: async function ({ cookies }) {
|
8 |
+
cookies.delete(COOKIE_NAME, {
|
9 |
+
path: "/",
|
10 |
+
// So that it works inside the space's iframe
|
11 |
+
sameSite: dev ? "lax" : "none",
|
12 |
+
secure: !dev,
|
13 |
+
httpOnly: true,
|
14 |
+
});
|
15 |
+
throw redirect(303, base || "/");
|
16 |
+
},
|
17 |
+
};
|
@@ -2,8 +2,9 @@ import { base } from "$app/paths";
|
|
2 |
import { collections } from "$lib/server/database";
|
3 |
import { redirect } from "@sveltejs/kit";
|
4 |
import { z } from "zod";
|
5 |
-
import {
|
6 |
import { authCondition } from "$lib/server/auth";
|
|
|
7 |
|
8 |
export const actions = {
|
9 |
default: async function ({ request, locals }) {
|
@@ -11,14 +12,16 @@ export const actions = {
|
|
11 |
|
12 |
const { ethicsModalAccepted, ...settings } = z
|
13 |
.object({
|
14 |
-
shareConversationsWithModelAuthors: z
|
|
|
|
|
15 |
ethicsModalAccepted: z.boolean({ coerce: true }).optional(),
|
16 |
activeModel: validateModel(models),
|
17 |
})
|
18 |
.parse({
|
19 |
shareConversationsWithModelAuthors: formData.get("shareConversationsWithModelAuthors"),
|
20 |
ethicsModalAccepted: formData.get("ethicsModalAccepted"),
|
21 |
-
activeModel: formData.get("activeModel") ??
|
22 |
});
|
23 |
|
24 |
await collections.settings.updateOne(
|
|
|
2 |
import { collections } from "$lib/server/database";
|
3 |
import { redirect } from "@sveltejs/kit";
|
4 |
import { z } from "zod";
|
5 |
+
import { models, validateModel } from "$lib/server/models";
|
6 |
import { authCondition } from "$lib/server/auth";
|
7 |
+
import { DEFAULT_SETTINGS } from "$lib/types/Settings";
|
8 |
|
9 |
export const actions = {
|
10 |
default: async function ({ request, locals }) {
|
|
|
12 |
|
13 |
const { ethicsModalAccepted, ...settings } = z
|
14 |
.object({
|
15 |
+
shareConversationsWithModelAuthors: z
|
16 |
+
.boolean({ coerce: true })
|
17 |
+
.default(DEFAULT_SETTINGS.shareConversationsWithModelAuthors),
|
18 |
ethicsModalAccepted: z.boolean({ coerce: true }).optional(),
|
19 |
activeModel: validateModel(models),
|
20 |
})
|
21 |
.parse({
|
22 |
shareConversationsWithModelAuthors: formData.get("shareConversationsWithModelAuthors"),
|
23 |
ethicsModalAccepted: formData.get("ethicsModalAccepted"),
|
24 |
+
activeModel: formData.get("activeModel") ?? DEFAULT_SETTINGS.activeModel,
|
25 |
});
|
26 |
|
27 |
await collections.settings.updateOne(
|