nsarrazin commited on
Commit
afbe0de
·
unverified ·
1 Parent(s): a94ce92

feat: admin CLI login (#1789)

Browse files

* feat: implement admin token management and CLI login functionality

- Added `ADMIN_CLI_LOGIN` and `ADMIN_TOKEN` to the environment configuration.
- Introduced `AdminTokenManager` for handling admin sessions and token validation.
- Updated user session handling to include admin status.
- Enhanced API routes to utilize admin checks based on the new token management.
- Modified frontend components to reflect admin status correctly.
- Added a new endpoint for validating admin tokens.

* fix: remove token after validation

.env CHANGED
@@ -85,6 +85,11 @@ COOKIE_SAMESITE=# can be "lax", "strict", "none" or left empty
85
  COOKIE_SECURE=# set to true to only allow cookies over https
86
 
87
 
 
 
 
 
 
88
  ### Websearch ###
89
  ## API Keys used to activate search with web functionality. websearch is disabled if none are defined. choose one of the following:
90
  YDC_API_KEY=#your docs.you.com api key here
 
85
  COOKIE_SECURE=# set to true to only allow cookies over https
86
 
87
 
88
+ ### Admin stuff ###
89
+ ADMIN_CLI_LOGIN=true # set to false to disable the CLI login
90
+ ADMIN_TOKEN=#We recommend leaving this empty, you can get the token from the terminal.
91
+
92
+
93
  ### Websearch ###
94
  ## API Keys used to activate search with web functionality. websearch is disabled if none are defined. choose one of the following:
95
  YDC_API_KEY=#your docs.you.com api key here
chart/env/prod.yaml CHANGED
@@ -30,6 +30,7 @@ ingress:
30
 
31
  envVars:
32
  ADDRESS_HEADER: 'X-Forwarded-For'
 
33
  ALTERNATIVE_REDIRECT_URLS: '["huggingchat://login/callback"]'
34
  APP_BASE: "/chat"
35
  ALLOW_IFRAME: "false"
 
30
 
31
  envVars:
32
  ADDRESS_HEADER: 'X-Forwarded-For'
33
+ ADMIN_CLI_LOGIN: "false"
34
  ALTERNATIVE_REDIRECT_URLS: '["huggingchat://login/callback"]'
35
  APP_BASE: "/chat"
36
  ALLOW_IFRAME: "false"
package-lock.json CHANGED
@@ -6263,9 +6263,9 @@
6263
  }
6264
  },
6265
  "node_modules/caniuse-lite": {
6266
- "version": "1.0.30001659",
6267
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001659.tgz",
6268
- "integrity": "sha512-Qxxyfv3RdHAfJcXelgf0hU4DFUVXBGTjqrBUZLUh8AtlGnsDo+CnncYtTd95+ZKfnANUOzxyIQCuU/UeBZBYoA==",
6269
  "funding": [
6270
  {
6271
  "type": "opencollective",
@@ -6279,7 +6279,8 @@
6279
  "type": "github",
6280
  "url": "https://github.com/sponsors/ai"
6281
  }
6282
- ]
 
6283
  },
6284
  "node_modules/chai": {
6285
  "version": "5.2.0",
 
6263
  }
6264
  },
6265
  "node_modules/caniuse-lite": {
6266
+ "version": "1.0.30001712",
6267
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001712.tgz",
6268
+ "integrity": "sha512-MBqPpGYYdQ7/hfKiet9SCI+nmN5/hp4ZzveOJubl5DTAMa5oggjAuoi0Z4onBpKPFI2ePGnQuQIzF3VxDjDJig==",
6269
  "funding": [
6270
  {
6271
  "type": "opencollective",
 
6279
  "type": "github",
6280
  "url": "https://github.com/sponsors/ai"
6281
  }
6282
+ ],
6283
+ "license": "CC-BY-4.0"
6284
  },
6285
  "node_modules/chai": {
6286
  "version": "5.2.0",
scripts/updateLocalEnv.ts CHANGED
@@ -32,6 +32,7 @@ full_config = full_config.replaceAll(
32
 
33
  full_config = full_config.replaceAll("COOKIE_SECURE=`true`", "COOKIE_SECURE=`false`");
34
  full_config = full_config.replaceAll("LOG_LEVEL=`debug`", "LOG_LEVEL=`info`");
 
35
 
36
  // Write full_config to .env.local
37
  fs.writeFileSync(".env.local", full_config);
 
32
 
33
  full_config = full_config.replaceAll("COOKIE_SECURE=`true`", "COOKIE_SECURE=`false`");
34
  full_config = full_config.replaceAll("LOG_LEVEL=`debug`", "LOG_LEVEL=`info`");
35
+ full_config = full_config.replaceAll("NODE_ENV=`prod`", "NODE_ENV=`development`");
36
 
37
  // Write full_config to .env.local
38
  fs.writeFileSync(".env.local", full_config);
src/app.d.ts CHANGED
@@ -11,6 +11,7 @@ declare global {
11
  interface Locals {
12
  sessionId: string;
13
  user?: User & { logoutDisabled?: boolean };
 
14
  }
15
 
16
  interface Error {
 
11
  interface Locals {
12
  sessionId: string;
13
  user?: User & { logoutDisabled?: boolean };
14
+ isAdmin: boolean;
15
  }
16
 
17
  interface Error {
src/hooks.server.ts CHANGED
@@ -16,6 +16,7 @@ import { initExitHandler } from "$lib/server/exitHandler";
16
  import { ObjectId } from "mongodb";
17
  import { refreshAssistantsCounts } from "$lib/jobs/refresh-assistants-counts";
18
  import { refreshConversationStats } from "$lib/jobs/refresh-conversation-stats";
 
19
 
20
  // TODO: move this code on a started server hook, instead of using a "building" flag
21
  if (!building) {
@@ -37,6 +38,8 @@ if (!building) {
37
  // Init AbortedGenerations refresh process
38
  AbortedGenerations.getInstance();
39
 
 
 
40
  if (env.EXPOSE_API) {
41
  logger.warn(
42
  "The EXPOSE_API flag has been deprecated. The API is now required for chat-ui to work."
@@ -209,6 +212,9 @@ export const handle: Handle = async ({ event, resolve }) => {
209
 
210
  event.locals.sessionId = sessionId;
211
 
 
 
 
212
  // CSRF protection
213
  const requestContentType = event.request.headers.get("content-type")?.split(";")[0] ?? "";
214
  /** https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-enctype */
 
16
  import { ObjectId } from "mongodb";
17
  import { refreshAssistantsCounts } from "$lib/jobs/refresh-assistants-counts";
18
  import { refreshConversationStats } from "$lib/jobs/refresh-conversation-stats";
19
+ import { adminTokenManager } from "$lib/server/adminToken";
20
 
21
  // TODO: move this code on a started server hook, instead of using a "building" flag
22
  if (!building) {
 
38
  // Init AbortedGenerations refresh process
39
  AbortedGenerations.getInstance();
40
 
41
+ adminTokenManager.displayToken();
42
+
43
  if (env.EXPOSE_API) {
44
  logger.warn(
45
  "The EXPOSE_API flag has been deprecated. The API is now required for chat-ui to work."
 
212
 
213
  event.locals.sessionId = sessionId;
214
 
215
+ event.locals.isAdmin =
216
+ event.locals.user?.isAdmin || adminTokenManager.isAdmin(event.locals.sessionId);
217
+
218
  // CSRF protection
219
  const requestContentType = event.request.headers.get("content-type")?.split(";")[0] ?? "";
220
  /** https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-enctype */
src/lib/jobs/refresh-conversation-stats.ts CHANGED
@@ -41,7 +41,7 @@ async function computeStats(params: {
41
  // In those cases we need to compute the stats from before the last month as everything is one aggregation
42
  const minDate = lastComputed ? lastComputed.date.at : new Date(0);
43
 
44
- logger.info(
45
  { minDate, dateField: params.dateField, span: params.span, type: params.type },
46
  "Computing conversation stats"
47
  );
@@ -228,7 +228,7 @@ async function computeStats(params: {
228
 
229
  await collections.conversations.aggregate(pipeline, { allowDiskUse: true }).next();
230
 
231
- logger.info(
232
  { minDate, dateField: params.dateField, span: params.span, type: params.type },
233
  "Computed conversation stats"
234
  );
 
41
  // In those cases we need to compute the stats from before the last month as everything is one aggregation
42
  const minDate = lastComputed ? lastComputed.date.at : new Date(0);
43
 
44
+ logger.debug(
45
  { minDate, dateField: params.dateField, span: params.span, type: params.type },
46
  "Computing conversation stats"
47
  );
 
228
 
229
  await collections.conversations.aggregate(pipeline, { allowDiskUse: true }).next();
230
 
231
+ logger.debug(
232
  { minDate, dateField: params.dateField, span: params.span, type: params.type },
233
  "Computed conversation stats"
234
  );
src/lib/migrations/migrations.ts CHANGED
@@ -18,7 +18,7 @@ export async function checkAndRunMigrations() {
18
  .migrationResults.find()
19
  .toArray();
20
 
21
- logger.info("[MIGRATIONS] Begin check...");
22
 
23
  // connect to the database
24
  const connectedClient = await (await Database.getInstance()).getClient().connect();
@@ -27,7 +27,7 @@ export async function checkAndRunMigrations() {
27
 
28
  if (!lockId) {
29
  // another instance already has the lock, so we exit early
30
- logger.info(
31
  "[MIGRATIONS] Another instance already has the lock. Waiting for DB to be unlocked."
32
  );
33
 
@@ -54,21 +54,21 @@ export async function checkAndRunMigrations() {
54
 
55
  // check if the migration has already been applied
56
  if (!shouldRun) {
57
- logger.info(`[MIGRATIONS] "${migration.name}" already applied. Skipping...`);
58
  } else {
59
  // check the modifiers to see if some cases match
60
  if (
61
  (migration.runForHuggingChat === "only" && !isHuggingChat) ||
62
  (migration.runForHuggingChat === "never" && isHuggingChat)
63
  ) {
64
- logger.info(
65
  `[MIGRATIONS] "${migration.name}" should not be applied for this run. Skipping...`
66
  );
67
  continue;
68
  }
69
 
70
  // otherwise all is good and we can run the migration
71
- logger.info(
72
  `[MIGRATIONS] "${migration.name}" ${
73
  migration.runEveryTime ? "should run every time" : "not applied yet"
74
  }. Applying...`
@@ -93,7 +93,7 @@ export async function checkAndRunMigrations() {
93
  result = await migration.up(await Database.getInstance());
94
  });
95
  } catch (e) {
96
- logger.info(`[MIGRATIONS] "${migration.name}" failed!`);
97
  logger.error(e);
98
  } finally {
99
  await session.endSession();
@@ -112,7 +112,7 @@ export async function checkAndRunMigrations() {
112
  }
113
  }
114
 
115
- logger.info("[MIGRATIONS] All migrations applied. Releasing lock");
116
 
117
  clearInterval(refreshInterval);
118
  await releaseLock(LOCK_KEY, lockId);
 
18
  .migrationResults.find()
19
  .toArray();
20
 
21
+ logger.debug("[MIGRATIONS] Begin check...");
22
 
23
  // connect to the database
24
  const connectedClient = await (await Database.getInstance()).getClient().connect();
 
27
 
28
  if (!lockId) {
29
  // another instance already has the lock, so we exit early
30
+ logger.debug(
31
  "[MIGRATIONS] Another instance already has the lock. Waiting for DB to be unlocked."
32
  );
33
 
 
54
 
55
  // check if the migration has already been applied
56
  if (!shouldRun) {
57
+ logger.debug(`[MIGRATIONS] "${migration.name}" already applied. Skipping...`);
58
  } else {
59
  // check the modifiers to see if some cases match
60
  if (
61
  (migration.runForHuggingChat === "only" && !isHuggingChat) ||
62
  (migration.runForHuggingChat === "never" && isHuggingChat)
63
  ) {
64
+ logger.debug(
65
  `[MIGRATIONS] "${migration.name}" should not be applied for this run. Skipping...`
66
  );
67
  continue;
68
  }
69
 
70
  // otherwise all is good and we can run the migration
71
+ logger.debug(
72
  `[MIGRATIONS] "${migration.name}" ${
73
  migration.runEveryTime ? "should run every time" : "not applied yet"
74
  }. Applying...`
 
93
  result = await migration.up(await Database.getInstance());
94
  });
95
  } catch (e) {
96
+ logger.debug(`[MIGRATIONS] "${migration.name}" failed!`);
97
  logger.error(e);
98
  } finally {
99
  await session.endSession();
 
112
  }
113
  }
114
 
115
+ logger.debug("[MIGRATIONS] All migrations applied. Releasing lock");
116
 
117
  clearInterval(refreshInterval);
118
  await releaseLock(LOCK_KEY, lockId);
src/lib/server/adminToken.ts ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { env } from "$env/dynamic/private";
2
+ import { PUBLIC_ORIGIN } from "$env/static/public";
3
+ import type { Session } from "$lib/types/Session";
4
+ import { logger } from "./logger";
5
+ import { v4 } from "uuid";
6
+
7
+ class AdminTokenManager {
8
+ private token = env.ADMIN_TOKEN || v4();
9
+ // contains all session ids that are currently admin sessions
10
+ private adminSessions: Array<Session["sessionId"]> = [];
11
+
12
+ public get enabled() {
13
+ // if open id is configured, disable the feature
14
+ return env.ADMIN_CLI_LOGIN === "true";
15
+ }
16
+ public isAdmin(sessionId: Session["sessionId"]) {
17
+ if (!this.enabled) return false;
18
+ return this.adminSessions.includes(sessionId);
19
+ }
20
+
21
+ public checkToken(token: string, sessionId: Session["sessionId"]) {
22
+ if (!this.enabled) return false;
23
+ if (token === this.token) {
24
+ logger.info(`[ADMIN] Token validated`);
25
+ this.adminSessions.push(sessionId);
26
+ this.token = env.ADMIN_TOKEN || v4();
27
+ return true;
28
+ }
29
+
30
+ return false;
31
+ }
32
+
33
+ public removeSession(sessionId: Session["sessionId"]) {
34
+ this.adminSessions = this.adminSessions.filter((id) => id !== sessionId);
35
+ }
36
+
37
+ public displayToken() {
38
+ // if admin token is set, don't display it
39
+ if (!this.enabled || env.ADMIN_TOKEN) return;
40
+
41
+ const port = import.meta.env.PORT ?? 5173;
42
+ const url = (PUBLIC_ORIGIN || `http://localhost:${port}`) + "?token=";
43
+ logger.info(`[ADMIN] You can login with ${url + this.token} on port ${port}.`);
44
+ }
45
+ }
46
+
47
+ export const adminTokenManager = new AdminTokenManager();
src/lib/types/Session.ts CHANGED
@@ -9,4 +9,5 @@ export interface Session extends Timestamps {
9
  userAgent?: string;
10
  ip?: string;
11
  expiresAt: Date;
 
12
  }
 
9
  userAgent?: string;
10
  ip?: string;
11
  expiresAt: Date;
12
+ admin?: boolean;
13
  }
src/routes/+layout.server.ts CHANGED
@@ -270,9 +270,9 @@ export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => {
270
  avatarUrl: locals.user.avatarUrl,
271
  email: locals.user.email,
272
  logoutDisabled: locals.user.logoutDisabled,
273
- isAdmin: locals.user.isAdmin ?? false,
274
  isEarlyAccess: locals.user.isEarlyAccess ?? false,
275
  },
 
276
  assistant: assistant ? JSON.parse(JSON.stringify(assistant)) : null,
277
  enableAssistants,
278
  enableAssistantsRAG: env.ENABLE_ASSISTANTS_RAG === "true",
 
270
  avatarUrl: locals.user.avatarUrl,
271
  email: locals.user.email,
272
  logoutDisabled: locals.user.logoutDisabled,
 
273
  isEarlyAccess: locals.user.isEarlyAccess ?? false,
274
  },
275
+ isAdmin: locals.isAdmin,
276
  assistant: assistant ? JSON.parse(JSON.stringify(assistant)) : null,
277
  enableAssistants,
278
  enableAssistantsRAG: env.ENABLE_ASSISTANTS_RAG === "true",
src/routes/+layout.svelte CHANGED
@@ -156,6 +156,17 @@
156
  });
157
  });
158
  }
 
 
 
 
 
 
 
 
 
 
 
159
  });
160
 
161
  let mobileNavTitle = $derived(
 
156
  });
157
  });
158
  }
159
+
160
+ if ($page.url.searchParams.has("token")) {
161
+ const token = $page.url.searchParams.get("token");
162
+
163
+ await fetch(`${base}/api/user/validate-token`, {
164
+ method: "POST",
165
+ body: JSON.stringify({ token }),
166
+ }).then(() => {
167
+ goto(`${base}/`, { invalidateAll: true });
168
+ });
169
+ }
170
  });
171
 
172
  let mobileNavTitle = $derived(
src/routes/api/assistant/[id]/+server.ts CHANGED
@@ -148,7 +148,7 @@ export async function DELETE({ params, locals }) {
148
 
149
  if (
150
  assistant.createdById.toString() !== (locals.user?._id ?? locals.sessionId).toString() &&
151
- !locals.user?.isAdmin
152
  ) {
153
  return error(403, "You are not the author of this assistant");
154
  }
 
148
 
149
  if (
150
  assistant.createdById.toString() !== (locals.user?._id ?? locals.sessionId).toString() &&
151
+ !locals.isAdmin
152
  ) {
153
  return error(403, "You are not the author of this assistant");
154
  }
src/routes/api/assistant/[id]/review/+server.ts CHANGED
@@ -30,7 +30,7 @@ export async function PATCH({ params, request, locals, url }) {
30
 
31
  if (
32
  !locals.user ||
33
- (!locals.user.isAdmin && assistant.createdById.toString() !== locals.user._id.toString())
34
  ) {
35
  return error(403, "Permission denied");
36
  }
@@ -43,7 +43,7 @@ export async function PATCH({ params, request, locals, url }) {
43
  status === ReviewStatus.DENIED ||
44
  assistant.review === ReviewStatus.APPROVED ||
45
  assistant.review === ReviewStatus.DENIED) &&
46
- !locals.user?.isAdmin
47
  ) {
48
  return error(403, "Permission denied");
49
  }
 
30
 
31
  if (
32
  !locals.user ||
33
+ (!locals.isAdmin && assistant.createdById.toString() !== locals.user._id.toString())
34
  ) {
35
  return error(403, "Permission denied");
36
  }
 
43
  status === ReviewStatus.DENIED ||
44
  assistant.review === ReviewStatus.APPROVED ||
45
  assistant.review === ReviewStatus.DENIED) &&
46
+ !locals.isAdmin
47
  ) {
48
  return error(403, "Permission denied");
49
  }
src/routes/api/assistants/+server.ts CHANGED
@@ -30,7 +30,7 @@ export async function GET({ url, locals }) {
30
  // if we require featured assistants, that we are not on a user page and we are not an admin who wants to see unfeatured assistants, we show featured assistants
31
  let shouldBeFeatured = {};
32
 
33
- if (env.REQUIRE_FEATURED_ASSISTANTS === "true" && !(locals.user?.isAdmin && showUnfeatured)) {
34
  if (!user) {
35
  // only show featured assistants on the community page
36
  shouldBeFeatured = { review: ReviewStatus.APPROVED };
 
30
  // if we require featured assistants, that we are not on a user page and we are not an admin who wants to see unfeatured assistants, we show featured assistants
31
  let shouldBeFeatured = {};
32
 
33
+ if (env.REQUIRE_FEATURED_ASSISTANTS === "true" && !(locals.isAdmin && showUnfeatured)) {
34
  if (!user) {
35
  // only show featured assistants on the community page
36
  shouldBeFeatured = { review: ReviewStatus.APPROVED };
src/routes/api/tools/[toolId]/+server.ts CHANGED
@@ -113,7 +113,7 @@ export async function DELETE({ params, locals }) {
113
 
114
  if (
115
  tool.createdById.toString() !== (locals.user?._id ?? locals.sessionId).toString() &&
116
- !locals.user?.isAdmin
117
  ) {
118
  return new Response("You are not the creator of this tool", { status: 403 });
119
  }
 
113
 
114
  if (
115
  tool.createdById.toString() !== (locals.user?._id ?? locals.sessionId).toString() &&
116
+ !locals.isAdmin
117
  ) {
118
  return new Response("You are not the creator of this tool", { status: 403 });
119
  }
src/routes/api/tools/[toolId]/review/+server.ts CHANGED
@@ -30,7 +30,7 @@ export async function PATCH({ params, request, locals, url }) {
30
 
31
  if (
32
  !locals.user ||
33
- (!locals.user.isAdmin && tool.createdById.toString() !== locals.user._id.toString())
34
  ) {
35
  return error(403, "Permission denied");
36
  }
@@ -43,7 +43,7 @@ export async function PATCH({ params, request, locals, url }) {
43
  status === ReviewStatus.DENIED ||
44
  tool.review === ReviewStatus.APPROVED ||
45
  tool.review === ReviewStatus.DENIED) &&
46
- !locals.user?.isAdmin
47
  ) {
48
  return error(403, "Permission denied");
49
  }
 
30
 
31
  if (
32
  !locals.user ||
33
+ (!locals.isAdmin && tool.createdById.toString() !== locals.user._id.toString())
34
  ) {
35
  return error(403, "Permission denied");
36
  }
 
43
  status === ReviewStatus.DENIED ||
44
  tool.review === ReviewStatus.APPROVED ||
45
  tool.review === ReviewStatus.DENIED) &&
46
+ !locals.isAdmin
47
  ) {
48
  return error(403, "Permission denied");
49
  }
src/routes/api/user/validate-token/+server.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { adminTokenManager } from "$lib/server/adminToken";
2
+ import { z } from "zod";
3
+
4
+ const validateTokenSchema = z.object({
5
+ token: z.string(),
6
+ });
7
+
8
+ export const POST = async ({ request, locals }) => {
9
+ const { success, data } = validateTokenSchema.safeParse(await request.json());
10
+
11
+ if (!success) {
12
+ return new Response(JSON.stringify({ error: "Invalid token" }), { status: 400 });
13
+ }
14
+
15
+ if (adminTokenManager.checkToken(data.token, locals.sessionId)) {
16
+ return new Response(JSON.stringify({ valid: true }));
17
+ }
18
+
19
+ return new Response(JSON.stringify({ valid: false }));
20
+ };
src/routes/assistants/+page.server.ts CHANGED
@@ -36,7 +36,7 @@ export const load = async ({ url, locals }) => {
36
  // if we require featured assistants, that we are not on a user page and we are not an admin who wants to see unfeatured assistants, we show featured assistants
37
  let shouldBeFeatured = {};
38
 
39
- if (env.REQUIRE_FEATURED_ASSISTANTS === "true" && !(locals.user?.isAdmin && showUnfeatured)) {
40
  if (!user) {
41
  // only show featured assistants on the community page
42
  shouldBeFeatured = { review: ReviewStatus.APPROVED };
 
36
  // if we require featured assistants, that we are not on a user page and we are not an admin who wants to see unfeatured assistants, we show featured assistants
37
  let shouldBeFeatured = {};
38
 
39
+ if (env.REQUIRE_FEATURED_ASSISTANTS === "true" && !(locals.isAdmin && showUnfeatured)) {
40
  if (!user) {
41
  // only show featured assistants on the community page
42
  shouldBeFeatured = { review: ReviewStatus.APPROVED };
src/routes/assistants/+page.svelte CHANGED
@@ -151,7 +151,7 @@
151
  <option value={model.name}>{model.name}</option>
152
  {/each}
153
  </select>
154
- {#if data.user?.isAdmin}
155
  <label class="mr-auto flex items-center gap-1 text-red-500" title="Admin only feature">
156
  <input type="checkbox" checked={showUnfeatured} onchange={toggleShowUnfeatured} />
157
  Show unfeatured assistants
@@ -266,7 +266,7 @@
266
 
267
  <button
268
  class="relative flex flex-col items-center justify-center overflow-hidden text-balance rounded-xl border bg-gray-50/50 px-4 py-6 text-center shadow hover:bg-gray-50 hover:shadow-inner dark:border-gray-800/70 dark:bg-gray-950/20 dark:hover:bg-gray-950/40 max-sm:px-4 sm:h-64 sm:pb-4 xl:pt-8
269
- {!(assistant.review === ReviewStatus.APPROVED) && !createdByMe && data.user?.isAdmin
270
  ? 'border !border-red-500/30'
271
  : ''}"
272
  onclick={() => {
 
151
  <option value={model.name}>{model.name}</option>
152
  {/each}
153
  </select>
154
+ {#if data.isAdmin}
155
  <label class="mr-auto flex items-center gap-1 text-red-500" title="Admin only feature">
156
  <input type="checkbox" checked={showUnfeatured} onchange={toggleShowUnfeatured} />
157
  Show unfeatured assistants
 
266
 
267
  <button
268
  class="relative flex flex-col items-center justify-center overflow-hidden text-balance rounded-xl border bg-gray-50/50 px-4 py-6 text-center shadow hover:bg-gray-50 hover:shadow-inner dark:border-gray-800/70 dark:bg-gray-950/20 dark:hover:bg-gray-950/40 max-sm:px-4 sm:h-64 sm:pb-4 xl:pt-8
269
+ {!(assistant.review === ReviewStatus.APPROVED) && !createdByMe && data.isAdmin
270
  ? 'border !border-red-500/30'
271
  : ''}"
272
  onclick={() => {
src/routes/login/callback/updateUser.spec.ts CHANGED
@@ -19,6 +19,7 @@ Object.freeze(userData);
19
  const locals = {
20
  userId: "1234567890",
21
  sessionId: "1234567890",
 
22
  };
23
 
24
  // @ts-expect-error SvelteKit cookies dumb mock
 
19
  const locals = {
20
  userId: "1234567890",
21
  sessionId: "1234567890",
22
+ isAdmin: false,
23
  };
24
 
25
  // @ts-expect-error SvelteKit cookies dumb mock
src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte CHANGED
@@ -212,7 +212,7 @@
212
  >
213
  {/if}
214
  {/if}
215
- {#if data?.user?.isAdmin}
216
  <span class="rounded-full border px-2 py-0.5 text-sm leading-none text-gray-500"
217
  >{assistant?.review?.toLocaleUpperCase()}</span
218
  >
 
212
  >
213
  {/if}
214
  {/if}
215
+ {#if data?.isAdmin}
216
  <span class="rounded-full border px-2 py-0.5 text-sm leading-none text-gray-500"
217
  >{assistant?.review?.toLocaleUpperCase()}</span
218
  >
src/routes/tools/+page.server.ts CHANGED
@@ -48,7 +48,7 @@ export const load = async ({ url, locals }) => {
48
  const filter: Filter<CommunityToolDB> = {
49
  ...(!createdByCurrentUser &&
50
  !activeOnly &&
51
- !(locals.user?.isAdmin && showUnfeatured) && { review: ReviewStatus.APPROVED }),
52
  ...(user && { createdById: user._id }),
53
  ...(queryTokens && { searchTokens: { $all: queryTokens } }),
54
  ...(activeOnly && {
 
48
  const filter: Filter<CommunityToolDB> = {
49
  ...(!createdByCurrentUser &&
50
  !activeOnly &&
51
+ !(locals.isAdmin && showUnfeatured) && { review: ReviewStatus.APPROVED }),
52
  ...(user && { createdById: user._id }),
53
  ...(queryTokens && { searchTokens: { $all: queryTokens } }),
54
  ...(activeOnly && {
src/routes/tools/+page.svelte CHANGED
@@ -144,7 +144,7 @@
144
  >
145
  </h3>
146
  <div class="ml-auto mt-6 flex justify-between gap-2 max-sm:flex-col sm:items-center">
147
- {#if data.user?.isAdmin}
148
  <label class="mr-auto flex items-center gap-1 text-red-500" title="Admin only feature">
149
  <input type="checkbox" checked={showUnfeatured} onchange={toggleShowUnfeatured} />
150
  Show unfeatured tools
 
144
  >
145
  </h3>
146
  <div class="ml-auto mt-6 flex justify-between gap-2 max-sm:flex-col sm:items-center">
147
+ {#if data.isAdmin}
148
  <label class="mr-auto flex items-center gap-1 text-red-500" title="Admin only feature">
149
  <input type="checkbox" checked={showUnfeatured} onchange={toggleShowUnfeatured} />
150
  Show unfeatured tools
src/routes/tools/[toolId]/+page.svelte CHANGED
@@ -207,7 +207,7 @@
207
  >
208
  {/if}
209
  {/if}
210
- {#if data?.user?.isAdmin}
211
  <span class="rounded-full border px-2 py-0.5 text-sm leading-none text-gray-500"
212
  >{data.tool?.review?.toLocaleUpperCase()}</span
213
  >
 
207
  >
208
  {/if}
209
  {/if}
210
+ {#if data?.isAdmin}
211
  <span class="rounded-full border px-2 py-0.5 text-sm leading-none text-gray-500"
212
  >{data.tool?.review?.toLocaleUpperCase()}</span
213
  >