Skip to content

Commit 40c906e

Browse files
committed
feat: close app mode with PUBLIC_CLOSED
- redirect all requests to /closed - remove layout if app is closed - add helper text with info on export rate limit - add the /closed page which let users login/logout & export data
1 parent e83f9e2 commit 40c906e

File tree

9 files changed

+295
-56
lines changed

9 files changed

+295
-56
lines changed

.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ PUBLIC_ANNOUNCEMENT_BANNERS=`[
135135
PUBLIC_SMOOTH_UPDATES=false # set to true to enable smoothing of messages client-side, can be CPU intensive
136136
PUBLIC_ORIGIN=#https://huggingface.co
137137
PUBLIC_SHARE_PREFIX=#https://hf.co/chat
138+
PUBLIC_CLOSED=false
138139

139140
# mostly huggingchat specific
140141
PUBLIC_GOOGLE_ANALYTICS_ID=#G-XXXXXXXX / Leave empty to disable

chart/env/prod.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,7 @@ envVars:
546546
PUBLIC_APP_GUEST_MESSAGE: "Sign in with a free Hugging Face account to continue using HuggingChat."
547547
PUBLIC_APP_DATA_SHARING: 0
548548
PUBLIC_APP_DISCLAIMER: 1
549+
PUBLIC_CLOSED: true
549550
PUBLIC_PLAUSIBLE_SCRIPT_URL: "/js/script.js"
550551
REQUIRE_FEATURED_ASSISTANTS: "true"
551552
TASK_MODEL: >

src/hooks.server.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,23 @@ export const handle: Handle = async ({ event, resolve }) => {
107107
});
108108
}
109109

110+
const isClosed = config.PUBLIC_CLOSED === "true";
111+
112+
// if server is in closed mode, only allow GET calls and POST to /login, /login/callback, /logout
113+
if (isClosed) {
114+
const allowedPaths = ["/login", "/login/callback", "/logout"];
115+
116+
const isAllowedPath = allowedPaths.some((path) => event.url.pathname === `${base}${path}`);
117+
118+
if (!isAllowedPath) {
119+
const isGetRequest = event.request.method === "GET";
120+
121+
if (!isGetRequest) {
122+
return errorResponse(403, "This server is closed");
123+
}
124+
}
125+
}
126+
110127
if (event.url.pathname.startsWith(`${base}/admin/`) || event.url.pathname === `${base}/admin`) {
111128
const ADMIN_SECRET = config.ADMIN_API_SECRET || config.PARQUET_EXPORT_SECRET;
112129

src/lib/server/api/routes/groups/user.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,5 +191,27 @@ export const userGroup = new Elysia()
191191
createdByMe:
192192
el.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(),
193193
}));
194+
})
195+
.get("/next-export", async ({ locals }) => {
196+
if (!locals.user) return Infinity;
197+
198+
const lastExport = await collections.messageEvents.findOne(
199+
{
200+
userId: locals.user._id,
201+
type: "export",
202+
},
203+
{
204+
sort: {
205+
expiresAt: -1,
206+
},
207+
}
208+
);
209+
210+
if (!lastExport) return 0;
211+
212+
const expiresAt = lastExport.expiresAt.getTime();
213+
const now = Date.now();
214+
215+
return Math.max(0, Math.floor((expiresAt - now) / 1000));
194216
});
195217
});

src/lib/utils/PublicConfig.svelte.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ class PublicConfigManager {
3030
return this.#configStore;
3131
}
3232

33+
get isClosed() {
34+
return this.#configStore.PUBLIC_CLOSED === "true";
35+
}
36+
3337
get isHuggingChat() {
3438
return this.#configStore.PUBLIC_APP_ASSETS === "huggingchat";
3539
}

src/routes/+layout.svelte

Lines changed: 57 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -206,61 +206,65 @@
206206
{/if}
207207
</svelte:head>
208208

209-
{#if showDisclaimer}
210-
<DisclaimerModal on:close={() => ($settings.ethicsModalAccepted = true)} />
211-
{/if}
209+
{#if publicConfig.isClosed}
210+
{@render children?.()}
211+
{:else}
212+
{#if showDisclaimer}
213+
<DisclaimerModal on:close={() => ($settings.ethicsModalAccepted = true)} />
214+
{/if}
212215

213-
{#if $loginModalOpen}
214-
<LoginModal
215-
on:close={() => {
216-
$loginModalOpen = false;
217-
}}
218-
/>
219-
{/if}
216+
{#if $loginModalOpen}
217+
<LoginModal
218+
on:close={() => {
219+
$loginModalOpen = false;
220+
}}
221+
/>
222+
{/if}
220223

221-
{#if overloadedModalOpen && publicConfig.isHuggingChat}
222-
<OverloadedModal onClose={() => (overloadedModalOpen = false)} />
223-
{/if}
224+
{#if overloadedModalOpen && publicConfig.isHuggingChat}
225+
<OverloadedModal onClose={() => (overloadedModalOpen = false)} />
226+
{/if}
224227

225-
<Search />
226-
227-
<div
228-
class="fixed grid h-full w-screen grid-cols-1 grid-rows-[auto,1fr] overflow-hidden text-smd {!isNavCollapsed
229-
? 'md:grid-cols-[290px,1fr]'
230-
: 'md:grid-cols-[0px,1fr]'} transition-[300ms] [transition-property:grid-template-columns] dark:text-gray-300 md:grid-rows-[1fr]"
231-
>
232-
<ExpandNavigation
233-
isCollapsed={isNavCollapsed}
234-
onClick={() => (isNavCollapsed = !isNavCollapsed)}
235-
classNames="absolute inset-y-0 z-10 my-auto {!isNavCollapsed
236-
? 'left-[290px]'
237-
: 'left-0'} *:transition-transform"
238-
/>
239-
240-
<MobileNav title={mobileNavTitle}>
241-
<NavMenu
242-
{conversations}
243-
user={data.user}
244-
canLogin={!data.user && data.loginEnabled}
245-
on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
246-
on:deleteConversation={(ev) => deleteConversation(ev.detail)}
247-
on:editConversationTitle={(ev) => editConversationTitle(ev.detail.id, ev.detail.title)}
248-
/>
249-
</MobileNav>
250-
<nav
251-
class="grid max-h-screen grid-cols-1 grid-rows-[auto,1fr,auto] overflow-hidden *:w-[290px] max-md:hidden"
228+
<Search />
229+
230+
<div
231+
class="fixed grid h-full w-screen grid-cols-1 grid-rows-[auto,1fr] overflow-hidden text-smd {!isNavCollapsed
232+
? 'md:grid-cols-[290px,1fr]'
233+
: 'md:grid-cols-[0px,1fr]'} transition-[300ms] [transition-property:grid-template-columns] dark:text-gray-300 md:grid-rows-[1fr]"
252234
>
253-
<NavMenu
254-
{conversations}
255-
user={data.user}
256-
canLogin={!data.user && data.loginEnabled}
257-
on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
258-
on:deleteConversation={(ev) => deleteConversation(ev.detail)}
259-
on:editConversationTitle={(ev) => editConversationTitle(ev.detail.id, ev.detail.title)}
235+
<ExpandNavigation
236+
isCollapsed={isNavCollapsed}
237+
onClick={() => (isNavCollapsed = !isNavCollapsed)}
238+
classNames="absolute inset-y-0 z-10 my-auto {!isNavCollapsed
239+
? 'left-[290px]'
240+
: 'left-0'} *:transition-transform"
260241
/>
261-
</nav>
262-
{#if currentError}
263-
<Toast message={currentError} />
264-
{/if}
265-
{@render children?.()}
266-
</div>
242+
243+
<MobileNav title={mobileNavTitle}>
244+
<NavMenu
245+
{conversations}
246+
user={data.user}
247+
canLogin={!data.user && data.loginEnabled}
248+
on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
249+
on:deleteConversation={(ev) => deleteConversation(ev.detail)}
250+
on:editConversationTitle={(ev) => editConversationTitle(ev.detail.id, ev.detail.title)}
251+
/>
252+
</MobileNav>
253+
<nav
254+
class="grid max-h-screen grid-cols-1 grid-rows-[auto,1fr,auto] overflow-hidden *:w-[290px] max-md:hidden"
255+
>
256+
<NavMenu
257+
{conversations}
258+
user={data.user}
259+
canLogin={!data.user && data.loginEnabled}
260+
on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
261+
on:deleteConversation={(ev) => deleteConversation(ev.detail)}
262+
on:editConversationTitle={(ev) => editConversationTitle(ev.detail.id, ev.detail.title)}
263+
/>
264+
</nav>
265+
{#if currentError}
266+
<Toast message={currentError} />
267+
{/if}
268+
{@render children?.()}
269+
</div>
270+
{/if}

src/routes/+layout.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { UrlDependency } from "$lib/types/UrlDependency";
22
import type { ConvSidebar } from "$lib/types/ConvSidebar";
33
import { useAPIClient, handleResponse } from "$lib/APIClient";
44
import { getConfigManager } from "$lib/utils/PublicConfig.svelte";
5+
import { redirect } from "@sveltejs/kit";
6+
import { base } from "$app/paths";
57

6-
export const load = async ({ depends, fetch }) => {
8+
export const load = async ({ depends, fetch, url }) => {
79
depends(UrlDependency.ConversationList);
810

911
const client = useAPIClient({ fetch });
@@ -16,7 +18,7 @@ export const load = async ({ depends, fetch }) => {
1618
tools,
1719
communityToolCount,
1820
user,
19-
publicConfig,
21+
publicConfigRaw,
2022
featureFlags,
2123
conversationsData,
2224
] = await Promise.all([
@@ -32,6 +34,13 @@ export const load = async ({ depends, fetch }) => {
3234
client.conversations.get({ query: { p: 0 } }).then(handleResponse),
3335
]);
3436

37+
const publicConfig = getConfigManager(publicConfigRaw);
38+
39+
const allowedPaths = ["/closed", "/login", "/login/callback", "/logout"];
40+
if (publicConfig.isClosed && !allowedPaths.some((path) => url.pathname === base + path)) {
41+
throw redirect(302, base + "/closed");
42+
}
43+
3544
const defaultModel = models[0];
3645

3746
const assistantActive = !models.map(({ id }) => id).includes(settings?.activeModel ?? "");
@@ -91,7 +100,7 @@ export const load = async ({ depends, fetch }) => {
91100
? new Date(settings.ethicsModalAcceptedAt)
92101
: null,
93102
},
94-
publicConfig: getConfigManager(publicConfig),
103+
publicConfig,
95104
...featureFlags,
96105
};
97106
};

0 commit comments

Comments
 (0)