@promptbook/cli 0.103.0-48 → 0.103.0-50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/apps/agents-server/README.md +1 -1
  2. package/apps/agents-server/TODO.txt +6 -5
  3. package/apps/agents-server/config.ts +130 -0
  4. package/apps/agents-server/next.config.ts +1 -1
  5. package/apps/agents-server/public/fonts/OpenMoji-black-glyf.woff2 +0 -0
  6. package/apps/agents-server/public/fonts/download-font.js +22 -0
  7. package/apps/agents-server/src/app/[agentName]/[...rest]/page.tsx +11 -0
  8. package/apps/agents-server/src/app/[agentName]/page.tsx +1 -0
  9. package/apps/agents-server/src/app/actions.ts +37 -2
  10. package/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +68 -0
  11. package/apps/agents-server/src/app/agents/[agentName]/AgentQrCode.tsx +55 -0
  12. package/apps/agents-server/src/app/agents/[agentName]/AgentUrlCopy.tsx +4 -5
  13. package/apps/agents-server/src/app/agents/[agentName]/CopyField.tsx +44 -0
  14. package/apps/agents-server/src/app/agents/[agentName]/api/book/route.ts +8 -8
  15. package/apps/agents-server/src/app/agents/[agentName]/api/chat/route.ts +121 -25
  16. package/apps/agents-server/src/app/agents/[agentName]/api/feedback/route.ts +54 -0
  17. package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/route.ts +6 -6
  18. package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/systemMessage/route.ts +3 -3
  19. package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +29 -10
  20. package/apps/agents-server/src/app/agents/[agentName]/book/BookEditorWrapper.tsx +4 -5
  21. package/apps/agents-server/src/app/agents/[agentName]/book/page.tsx +9 -2
  22. package/apps/agents-server/src/app/agents/[agentName]/book+chat/AgentBookAndChat.tsx +23 -0
  23. package/apps/agents-server/src/app/agents/[agentName]/book+chat/{AgentBookAndChatComponent.tsx → AgentBookAndChatComponent.tsx.todo} +4 -4
  24. package/apps/agents-server/src/app/agents/[agentName]/book+chat/page.tsx +28 -17
  25. package/apps/agents-server/src/app/agents/[agentName]/book+chat/page.tsx.todo +21 -0
  26. package/apps/agents-server/src/app/agents/[agentName]/chat/AgentChatWrapper.tsx +34 -4
  27. package/apps/agents-server/src/app/agents/[agentName]/chat/page.tsx +4 -1
  28. package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +42 -0
  29. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +117 -106
  30. package/apps/agents-server/src/app/agents/page.tsx +1 -1
  31. package/apps/agents-server/src/app/api/agents/route.ts +34 -0
  32. package/apps/agents-server/src/app/api/auth/login/route.ts +65 -0
  33. package/apps/agents-server/src/app/api/auth/logout/route.ts +7 -0
  34. package/apps/agents-server/src/app/api/metadata/route.ts +116 -0
  35. package/apps/agents-server/src/app/api/upload/route.ts +7 -3
  36. package/apps/agents-server/src/app/api/users/[username]/route.ts +75 -0
  37. package/apps/agents-server/src/app/api/users/route.ts +71 -0
  38. package/apps/agents-server/src/app/globals.css +35 -1
  39. package/apps/agents-server/src/app/layout.tsx +43 -23
  40. package/apps/agents-server/src/app/metadata/MetadataClient.tsx +271 -0
  41. package/apps/agents-server/src/app/metadata/page.tsx +13 -0
  42. package/apps/agents-server/src/app/not-found.tsx +5 -0
  43. package/apps/agents-server/src/app/page.tsx +117 -46
  44. package/apps/agents-server/src/components/Auth/AuthControls.tsx +123 -0
  45. package/apps/agents-server/src/components/ErrorPage/ErrorPage.tsx +33 -0
  46. package/apps/agents-server/src/components/ForbiddenPage/ForbiddenPage.tsx +15 -0
  47. package/apps/agents-server/src/components/Header/Header.tsx +146 -0
  48. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +27 -0
  49. package/apps/agents-server/src/components/LoginDialog/LoginDialog.tsx +40 -0
  50. package/apps/agents-server/src/components/LoginForm/LoginForm.tsx +109 -0
  51. package/apps/agents-server/src/components/NotFoundPage/NotFoundPage.tsx +17 -0
  52. package/apps/agents-server/src/components/UsersList/UsersList.tsx +190 -0
  53. package/apps/agents-server/src/components/VercelDeploymentCard/VercelDeploymentCard.tsx +60 -0
  54. package/apps/agents-server/src/database/$getTableName.ts +18 -0
  55. package/apps/agents-server/src/database/$provideSupabase.ts +2 -2
  56. package/apps/agents-server/src/database/$provideSupabaseForServer.ts +3 -3
  57. package/apps/agents-server/src/database/getMetadata.ts +31 -0
  58. package/apps/agents-server/src/database/metadataDefaults.ts +37 -0
  59. package/apps/agents-server/src/database/schema.sql +81 -33
  60. package/apps/agents-server/src/database/schema.ts +35 -1
  61. package/apps/agents-server/src/middleware.ts +200 -0
  62. package/apps/agents-server/src/tools/$provideAgentCollectionForServer.ts +11 -7
  63. package/apps/agents-server/src/tools/$provideCdnForServer.ts +1 -1
  64. package/apps/agents-server/src/tools/$provideExecutionToolsForServer.ts +11 -13
  65. package/apps/agents-server/src/tools/$provideOpenAiAssistantExecutionToolsForServer.ts +7 -7
  66. package/apps/agents-server/src/tools/$provideServer.ts +39 -0
  67. package/apps/agents-server/src/utils/auth.ts +33 -0
  68. package/apps/agents-server/src/utils/cdn/utils/nameToSubfolderPath.ts +1 -1
  69. package/apps/agents-server/src/utils/getCurrentUser.ts +32 -0
  70. package/apps/agents-server/src/utils/getFederatedAgents.ts +66 -0
  71. package/apps/agents-server/src/utils/isIpAllowed.ts +101 -0
  72. package/apps/agents-server/src/utils/isUserAdmin.ts +31 -0
  73. package/apps/agents-server/src/utils/session.ts +50 -0
  74. package/apps/agents-server/tailwind.config.ts +2 -0
  75. package/esm/index.es.js +147 -31
  76. package/esm/index.es.js.map +1 -1
  77. package/esm/typings/servers.d.ts +1 -0
  78. package/esm/typings/src/_packages/components.index.d.ts +2 -0
  79. package/esm/typings/src/_packages/types.index.d.ts +2 -0
  80. package/esm/typings/src/_packages/utils.index.d.ts +2 -0
  81. package/esm/typings/src/book-2.0/agent-source/AgentBasicInformation.d.ts +12 -2
  82. package/esm/typings/src/book-components/PromptbookAgent/PromptbookAgent.d.ts +20 -0
  83. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +14 -8
  84. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabaseOptions.d.ts +10 -0
  85. package/esm/typings/src/commitments/MESSAGE/InitialMessageCommitmentDefinition.d.ts +28 -0
  86. package/esm/typings/src/commitments/index.d.ts +2 -1
  87. package/esm/typings/src/config.d.ts +1 -0
  88. package/esm/typings/src/errors/DatabaseError.d.ts +2 -2
  89. package/esm/typings/src/errors/WrappedError.d.ts +2 -2
  90. package/esm/typings/src/execution/ExecutionTask.d.ts +2 -2
  91. package/esm/typings/src/execution/LlmExecutionTools.d.ts +6 -1
  92. package/esm/typings/src/llm-providers/_common/register/$provideLlmToolsForWizardOrCli.d.ts +2 -2
  93. package/esm/typings/src/llm-providers/agent/Agent.d.ts +19 -3
  94. package/esm/typings/src/llm-providers/agent/AgentLlmExecutionTools.d.ts +13 -1
  95. package/esm/typings/src/llm-providers/agent/RemoteAgent.d.ts +11 -2
  96. package/esm/typings/src/llm-providers/openai/OpenAiAssistantExecutionTools.d.ts +6 -1
  97. package/esm/typings/src/remote-server/startAgentServer.d.ts +2 -2
  98. package/esm/typings/src/utils/color/Color.d.ts +7 -0
  99. package/esm/typings/src/utils/color/Color.test.d.ts +1 -0
  100. package/esm/typings/src/utils/environment/$getGlobalScope.d.ts +2 -2
  101. package/esm/typings/src/utils/misc/computeHash.d.ts +11 -0
  102. package/esm/typings/src/utils/misc/computeHash.test.d.ts +1 -0
  103. package/esm/typings/src/utils/organization/$sideEffect.d.ts +2 -2
  104. package/esm/typings/src/utils/organization/$side_effect.d.ts +2 -2
  105. package/esm/typings/src/utils/organization/TODO_USE.d.ts +2 -2
  106. package/esm/typings/src/utils/organization/keepUnused.d.ts +2 -2
  107. package/esm/typings/src/utils/organization/preserve.d.ts +3 -3
  108. package/esm/typings/src/utils/organization/really_any.d.ts +7 -0
  109. package/esm/typings/src/utils/serialization/asSerializable.d.ts +2 -2
  110. package/esm/typings/src/version.d.ts +1 -1
  111. package/package.json +1 -1
  112. package/umd/index.umd.js +147 -31
  113. package/umd/index.umd.js.map +1 -1
  114. package/apps/agents-server/config.ts.todo +0 -38
@@ -0,0 +1,71 @@
1
+ import { $getTableName } from '@/src/database/$getTableName';
2
+ import { $provideSupabaseForServer } from '../../../database/$provideSupabaseForServer';
3
+ import { hashPassword } from '../../../utils/auth';
4
+ import { isUserAdmin } from '../../../utils/isUserAdmin';
5
+ import { NextResponse } from 'next/server';
6
+
7
+ export async function GET() {
8
+ if (!(await isUserAdmin())) {
9
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
10
+ }
11
+
12
+ try {
13
+ const supabase = $provideSupabaseForServer();
14
+ const { data: users, error } = await supabase
15
+ .from(await $getTableName('User'))
16
+ .select('id, username, createdAt, updatedAt, isAdmin')
17
+ .order('username');
18
+
19
+ if (error) {
20
+ throw error;
21
+ }
22
+
23
+ return NextResponse.json(users);
24
+ } catch (error) {
25
+ console.error('List users error:', error);
26
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
27
+ }
28
+ }
29
+
30
+ export async function POST(request: Request) {
31
+ if (!(await isUserAdmin())) {
32
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
33
+ }
34
+
35
+ try {
36
+ const body = await request.json();
37
+ const { username, password, isAdmin } = body;
38
+
39
+ if (!username || !password) {
40
+ return NextResponse.json({ error: 'Username and password are required' }, { status: 400 });
41
+ }
42
+
43
+ const passwordHash = await hashPassword(password);
44
+ const supabase = $provideSupabaseForServer();
45
+
46
+ const { data: newUser, error } = await supabase
47
+ .from(await $getTableName('User'))
48
+ .insert({
49
+ username,
50
+ passwordHash,
51
+ isAdmin: !!isAdmin,
52
+ createdAt: new Date().toISOString(),
53
+ updatedAt: new Date().toISOString(),
54
+ })
55
+ .select('id, username, createdAt, updatedAt, isAdmin')
56
+ .single();
57
+
58
+ if (error) {
59
+ if (error.code === '23505') { // unique_violation
60
+ return NextResponse.json({ error: 'Username already exists' }, { status: 409 });
61
+ }
62
+ throw error;
63
+ }
64
+
65
+ return NextResponse.json(newUser);
66
+
67
+ } catch (error) {
68
+ console.error('Create user error:', error);
69
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
70
+ }
71
+ }
@@ -2,6 +2,40 @@
2
2
  @import 'tailwindcss/components';
3
3
  @import 'tailwindcss/utilities';
4
4
 
5
+ /**
6
+ * OpenMoji black and white CSS
7
+ *
8
+ * https://github.com/hfg-gmuend/openmoji/blob/master/font/OpenMoji-black-glyf/openmoji.css
9
+ */
10
+ @font-face {
11
+ font-family: 'OpenMojiBlack';
12
+ src: url('/fonts/OpenMoji-black-glyf.woff2') format('woff2');
13
+ unicode-range: U+23, U+2A, U+2D, U+30-39, U+A9, U+AE, U+200D, U+203C, U+2049, U+20E3, U+2117, U+2120, U+2122,
14
+ U+2139, U+2194-2199, U+21A9, U+21AA, U+229C, U+231A, U+231B, U+2328, U+23CF, U+23E9-23F3, U+23F8-23FE, U+24C2,
15
+ U+25A1, U+25AA-25AE, U+25B6, U+25C0, U+25C9, U+25D0, U+25D1, U+25E7-25EA, U+25ED, U+25EE, U+25FB-25FE,
16
+ U+2600-2605, U+260E, U+2611, U+2614, U+2615, U+2618, U+261D, U+2620, U+2622, U+2623, U+2626, U+262A, U+262E,
17
+ U+262F, U+2638-263A, U+2640, U+2642, U+2648-2653, U+265F, U+2660, U+2663, U+2665, U+2666, U+2668, U+267B,
18
+ U+267E, U+267F, U+2691-2697, U+2699, U+269B, U+269C, U+26A0, U+26A1, U+26A7, U+26AA, U+26AB, U+26B0, U+26B1,
19
+ U+26BD, U+26BE, U+26C4, U+26C5, U+26C8, U+26CE, U+26CF, U+26D1, U+26D3, U+26D4, U+26E9, U+26EA, U+26F0-26F5,
20
+ U+26F7-26FA, U+26FD, U+2702, U+2705, U+2708-270D, U+270F, U+2712, U+2714, U+2716, U+271D, U+2721, U+2728,
21
+ U+2733, U+2734, U+2744, U+2747, U+274C, U+274E, U+2753-2755, U+2757, U+2763, U+2764, U+2795-2797, U+27A1,
22
+ U+27B0, U+27BF, U+2934, U+2935, U+2B05-2B07, U+2B0C, U+2B0D, U+2B1B, U+2B1C, U+2B1F-2B24, U+2B2E, U+2B2F,
23
+ U+2B50, U+2B55, U+2B58, U+2B8F, U+2BBA-2BBC, U+2BC3, U+2BC4, U+2BEA, U+2BEB, U+3030, U+303D, U+3297, U+3299,
24
+ U+E000-E009, U+E010, U+E011, U+E040-E06D, U+E080-E0B4, U+E0C0-E0CC, U+E0FF-E10D, U+E140-E14A, U+E150-E157,
25
+ U+E181-E189, U+E1C0-E1C4, U+E1C6-E1D9, U+E200-E216, U+E240-E269, U+E280-E283, U+E2C0-E2C4, U+E2C6-E2DA,
26
+ U+E300-E303, U+E305-E30F, U+E312-E316, U+E318-E322, U+E324-E329, U+E32B, U+E340-E348, U+E380, U+E381, U+F000,
27
+ U+F77A, U+F8FF, U+FE0F, U+1F004, U+1F0CF, U+1F10D-1F10F, U+1F12F, U+1F16D-1F171, U+1F17E, U+1F17F, U+1F18E,
28
+ U+1F191-1F19A, U+1F1E6-1F1FF, U+1F201, U+1F202, U+1F21A, U+1F22F, U+1F232-1F23A, U+1F250, U+1F251,
29
+ U+1F260-1F265, U+1F300-1F321, U+1F324-1F393, U+1F396, U+1F397, U+1F399-1F39B, U+1F39E-1F3F0, U+1F3F3-1F3F5,
30
+ U+1F3F7-1F4FD, U+1F4FF-1F53D, U+1F549-1F54E, U+1F550-1F567, U+1F56F, U+1F570, U+1F573-1F57A, U+1F587,
31
+ U+1F58A-1F58D, U+1F590, U+1F595, U+1F596, U+1F5A4, U+1F5A5, U+1F5A8, U+1F5B1, U+1F5B2, U+1F5BC, U+1F5C2-1F5C4,
32
+ U+1F5D1-1F5D3, U+1F5DC-1F5DE, U+1F5E1, U+1F5E3, U+1F5E8, U+1F5EF, U+1F5F3, U+1F5FA-1F64F, U+1F680-1F6C5,
33
+ U+1F6CB-1F6D2, U+1F6D5-1F6D7, U+1F6DC-1F6E5, U+1F6E9, U+1F6EB, U+1F6EC, U+1F6F0, U+1F6F3-1F6FC, U+1F7E0-1F7EB,
34
+ U+1F7F0, U+1F90C-1F93A, U+1F93C-1F945, U+1F947-1F9FF, U+1FA70-1FA7C, U+1FA80-1FA88, U+1FA90-1FABD,
35
+ U+1FABF-1FAC5, U+1FACE-1FADB, U+1FAE0-1FAE8, U+1FAF0-1FAF8, U+1FBC5-1FBC9, U+E0061-E0067, U+E0069,
36
+ U+E006C-E0079, U+E007F;
37
+ }
38
+
5
39
  :root {
6
40
  --background: #ffffff;
7
41
  --foreground: #171717;
@@ -17,7 +51,7 @@
17
51
  body {
18
52
  background: var(--background);
19
53
  color: var(--foreground);
20
- font-family: 'Barlow Condensed', Arial, Helvetica, sans-serif;
54
+ font-family: var(--font-barlow-condensed), 'OpenMojiBlack', Arial, Helvetica, sans-serif;
21
55
  }
22
56
 
23
57
  /* Custom utilities */
@@ -1,24 +1,33 @@
1
1
  import faviconLogoImage from '@/public/favicon.ico';
2
+ import { LayoutWrapper } from '@/src/components/LayoutWrapper/LayoutWrapper';
2
3
  import type { Metadata } from 'next';
3
4
  import { Barlow_Condensed } from 'next/font/google';
5
+ import { getMetadata } from '../database/getMetadata';
6
+ import { isUserAdmin } from '../utils/isUserAdmin';
4
7
  import './globals.css';
5
8
 
6
9
  const barlowCondensed = Barlow_Condensed({
7
10
  subsets: ['latin'],
8
11
  weight: ['300', '400', '500', '600', '700'],
12
+ variable: '--font-barlow-condensed',
9
13
  });
10
14
 
11
- export const metadata: Metadata = {
12
- title: 'Promptbook agents server',
13
- description: '@@@',
14
- keywords: ['@@@'],
15
- authors: [{ name: 'Promptbook Team' }],
16
- openGraph: {
17
- title: 'Promptbook agents server',
18
- description: '@@@',
19
- type: 'website',
20
- images: [
21
- /*
15
+ export async function generateMetadata(): Promise<Metadata> {
16
+ const serverName = (await getMetadata('SERVER_NAME')) || 'Promptbook Agents Server';
17
+ const serverDescription = (await getMetadata('SERVER_DESCRIPTION')) || 'Agents server powered by Promptbook';
18
+ const serverUrl = (await getMetadata('SERVER_URL')) || 'https://ptbk.io';
19
+
20
+ return {
21
+ title: serverName,
22
+ description: serverDescription,
23
+ // TODO: keywords: ['@@@'],
24
+ authors: [{ name: 'Promptbook Team' }],
25
+ openGraph: {
26
+ title: serverName,
27
+ description: serverDescription,
28
+ type: 'website',
29
+ images: [
30
+ /*
22
31
  TODO:
23
32
  {
24
33
  url: 'https://www.ptbk.io/design',
@@ -27,21 +36,28 @@ export const metadata: Metadata = {
27
36
  alt: 'Promptbook agents server',
28
37
  },
29
38
  */
30
- ],
31
- },
32
- twitter: {
33
- card: 'summary_large_image',
34
- title: 'Promptbook agents server',
35
- description: '@@@',
36
- // TODO: images: ['https://www.ptbk.io/design'],
37
- },
38
- };
39
+ ],
40
+ },
41
+ twitter: {
42
+ card: 'summary_large_image',
43
+ title: serverName,
44
+ description: serverDescription,
45
+ // TODO: images: ['https://www.ptbk.io/design'],
46
+ },
47
+ metadataBase: new URL(serverUrl),
48
+ };
49
+ }
39
50
 
40
- export default function RootLayout({
51
+ export default async function RootLayout({
41
52
  children,
42
53
  }: Readonly<{
43
54
  children: React.ReactNode;
44
55
  }>) {
56
+ const isAdmin = await isUserAdmin();
57
+ const serverName = (await getMetadata('SERVER_NAME')) || 'Promptbook Agents Server';
58
+ const serverLogoUrl = (await getMetadata('SERVER_LOGO_URL')) || null;
59
+ const serverFaviconUrl = (await getMetadata('SERVER_FAVICON_URL')) || faviconLogoImage.src;
60
+
45
61
  return (
46
62
  <html lang="en">
47
63
  <head>
@@ -64,9 +80,13 @@ export default function RootLayout({
64
80
  />
65
81
  */}
66
82
  {/* Default favicon as a fallback */}
67
- <link rel="icon" href={faviconLogoImage.src} type="image/x-icon" />
83
+ <link rel="icon" href={serverFaviconUrl} type="image/x-icon" />
68
84
  </head>
69
- <body className={`${barlowCondensed.className} antialiased bg-white text-gray-900`}>{children}</body>
85
+ <body className={`${barlowCondensed.variable} antialiased bg-white text-gray-900`}>
86
+ <LayoutWrapper isAdmin={isAdmin} serverName={serverName} serverLogoUrl={serverLogoUrl}>
87
+ {children}
88
+ </LayoutWrapper>
89
+ </body>
70
90
  </html>
71
91
  );
72
92
  }
@@ -0,0 +1,271 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { metadataDefaults } from '../../database/metadataDefaults';
5
+
6
+ type MetadataEntry = {
7
+ id: number;
8
+ key: string;
9
+ value: string;
10
+ note: string | null;
11
+ createdAt: string;
12
+ updatedAt: string;
13
+ isDefault?: boolean;
14
+ };
15
+
16
+ export function MetadataClient() {
17
+ const [metadata, setMetadata] = useState<MetadataEntry[]>([]);
18
+ const [loading, setLoading] = useState(true);
19
+ const [error, setError] = useState<string | null>(null);
20
+ const [editingId, setEditingId] = useState<number | null>(null);
21
+ const [formState, setFormState] = useState<{
22
+ key: string;
23
+ value: string;
24
+ note: string;
25
+ }>({ key: '', value: '', note: '' });
26
+
27
+ const fetchMetadata = async () => {
28
+ try {
29
+ setLoading(true);
30
+ const response = await fetch('/api/metadata');
31
+ if (!response.ok) {
32
+ throw new Error('Failed to fetch metadata');
33
+ }
34
+ const data: MetadataEntry[] = await response.json();
35
+
36
+ // Merge defaults
37
+ const mergedData = [...data];
38
+ for (const def of metadataDefaults) {
39
+ if (!mergedData.find((m) => m.key === def.key)) {
40
+ mergedData.push({
41
+ id: -1,
42
+ key: def.key,
43
+ value: def.value,
44
+ note: def.note,
45
+ createdAt: new Date().toISOString(),
46
+ updatedAt: new Date().toISOString(),
47
+ isDefault: true,
48
+ });
49
+ }
50
+ }
51
+ // Sort by key
52
+ mergedData.sort((a, b) => a.key.localeCompare(b.key));
53
+
54
+ setMetadata(mergedData);
55
+ } catch (err) {
56
+ setError(err instanceof Error ? err.message : 'An error occurred');
57
+ } finally {
58
+ setLoading(false);
59
+ }
60
+ };
61
+
62
+ useEffect(() => {
63
+ fetchMetadata();
64
+ }, []);
65
+
66
+ const handleSubmit = async (e: React.FormEvent) => {
67
+ e.preventDefault();
68
+ setError(null);
69
+
70
+ try {
71
+ // If editingId is -1 (default value) or null (new value), use POST to create
72
+ // If editingId is > 0 (existing value), use PUT to update
73
+ const method = editingId && editingId !== -1 ? 'PUT' : 'POST';
74
+ const response = await fetch('/api/metadata', {
75
+ method,
76
+ headers: { 'Content-Type': 'application/json' },
77
+ body: JSON.stringify(formState),
78
+ });
79
+
80
+ if (!response.ok) {
81
+ const data = await response.json();
82
+ throw new Error(data.error || 'Failed to save metadata');
83
+ }
84
+
85
+ setFormState({ key: '', value: '', note: '' });
86
+ setEditingId(null);
87
+ fetchMetadata();
88
+ } catch (err) {
89
+ setError(err instanceof Error ? err.message : 'An error occurred');
90
+ }
91
+ };
92
+
93
+ const handleEdit = (entry: MetadataEntry) => {
94
+ setEditingId(entry.id);
95
+ setFormState({
96
+ key: entry.key,
97
+ value: entry.value,
98
+ note: entry.note || '',
99
+ });
100
+ };
101
+
102
+ const handleDelete = async (key: string) => {
103
+ if (!confirm('Are you sure you want to delete this metadata?')) return;
104
+
105
+ try {
106
+ const response = await fetch(`/api/metadata?key=${encodeURIComponent(key)}`, {
107
+ method: 'DELETE',
108
+ });
109
+
110
+ if (!response.ok) {
111
+ const data = await response.json();
112
+ throw new Error(data.error || 'Failed to delete metadata');
113
+ }
114
+
115
+ fetchMetadata();
116
+ } catch (err) {
117
+ setError(err instanceof Error ? err.message : 'An error occurred');
118
+ }
119
+ };
120
+
121
+ const handleCancel = () => {
122
+ setEditingId(null);
123
+ setFormState({ key: '', value: '', note: '' });
124
+ };
125
+
126
+ if (loading && metadata.length === 0) {
127
+ return <div className="p-8 text-center">Loading metadata...</div>;
128
+ }
129
+
130
+ return (
131
+ <div className="container mx-auto p-8 max-w-4xl">
132
+ <h1 className="text-3xl font-bold mb-8">Metadata Management</h1>
133
+
134
+ {error && (
135
+ <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6">
136
+ {error}
137
+ </div>
138
+ )}
139
+
140
+ <div className="bg-white shadow rounded-lg p-6 mb-8">
141
+ <h2 className="text-xl font-semibold mb-4">
142
+ {editingId ? 'Edit Metadata' : 'Add New Metadata'}
143
+ </h2>
144
+ <form onSubmit={handleSubmit} className="space-y-4">
145
+ <div>
146
+ <label htmlFor="key" className="block text-sm font-medium text-gray-700 mb-1">
147
+ Key
148
+ </label>
149
+ <input
150
+ type="text"
151
+ id="key"
152
+ value={formState.key}
153
+ onChange={(e) => setFormState({ ...formState, key: e.target.value })}
154
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
155
+ required
156
+ disabled={!!editingId} // Key cannot be changed during edit
157
+ placeholder="e.g., SERVER_NAME"
158
+ />
159
+ {editingId && <p className="text-xs text-gray-500 mt-1">Key cannot be changed.</p>}
160
+ </div>
161
+ <div>
162
+ <label htmlFor="value" className="block text-sm font-medium text-gray-700 mb-1">
163
+ Value
164
+ </label>
165
+ <textarea
166
+ id="value"
167
+ value={formState.value}
168
+ onChange={(e) => setFormState({ ...formState, value: e.target.value })}
169
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 min-h-[100px]"
170
+ required
171
+ placeholder="Metadata value..."
172
+ />
173
+ </div>
174
+ <div>
175
+ <label htmlFor="note" className="block text-sm font-medium text-gray-700 mb-1">
176
+ Note (Optional)
177
+ </label>
178
+ <input
179
+ type="text"
180
+ id="note"
181
+ value={formState.note}
182
+ onChange={(e) => setFormState({ ...formState, note: e.target.value })}
183
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
184
+ placeholder="Description of this metadata"
185
+ />
186
+ </div>
187
+ <div className="flex space-x-3">
188
+ <button
189
+ type="submit"
190
+ className="bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 transition-colors"
191
+ >
192
+ {editingId ? 'Update Metadata' : 'Add Metadata'}
193
+ </button>
194
+ {editingId && (
195
+ <button
196
+ type="button"
197
+ onClick={handleCancel}
198
+ className="bg-gray-200 text-gray-800 py-2 px-4 rounded-md hover:bg-gray-300 transition-colors"
199
+ >
200
+ Cancel
201
+ </button>
202
+ )}
203
+ </div>
204
+ </form>
205
+ </div>
206
+
207
+ <div className="bg-white shadow rounded-lg overflow-hidden">
208
+ <table className="min-w-full divide-y divide-gray-200">
209
+ <thead className="bg-gray-50">
210
+ <tr>
211
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
212
+ Key
213
+ </th>
214
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
215
+ Value
216
+ </th>
217
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
218
+ Note
219
+ </th>
220
+ <th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
221
+ Actions
222
+ </th>
223
+ </tr>
224
+ </thead>
225
+ <tbody className="bg-white divide-y divide-gray-200">
226
+ {metadata.length === 0 ? (
227
+ <tr>
228
+ <td colSpan={4} className="px-6 py-4 text-center text-gray-500">
229
+ No metadata found.
230
+ </td>
231
+ </tr>
232
+ ) : (
233
+ metadata.map((entry) => (
234
+ <tr key={entry.id}>
235
+ <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
236
+ {entry.key}
237
+ </td>
238
+ <td className="px-6 py-4 text-sm text-gray-500 max-w-xs truncate">
239
+ {entry.value}
240
+ </td>
241
+ <td className="px-6 py-4 text-sm text-gray-500">
242
+ {entry.note || '-'}
243
+ </td>
244
+ <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
245
+ <button
246
+ onClick={() => handleEdit(entry)}
247
+ className="text-blue-600 hover:text-blue-900 mr-4"
248
+ >
249
+ Edit
250
+ </button>
251
+ {!entry.isDefault && (
252
+ <button
253
+ onClick={() => handleDelete(entry.key)}
254
+ className="text-red-600 hover:text-red-900"
255
+ >
256
+ Delete
257
+ </button>
258
+ )}
259
+ {entry.isDefault && (
260
+ <span className="text-gray-400 text-xs italic ml-2">Default</span>
261
+ )}
262
+ </td>
263
+ </tr>
264
+ ))
265
+ )}
266
+ </tbody>
267
+ </table>
268
+ </div>
269
+ </div>
270
+ );
271
+ }
@@ -0,0 +1,13 @@
1
+ import { ForbiddenPage } from '../../components/ForbiddenPage/ForbiddenPage';
2
+ import { isUserAdmin } from '../../utils/isUserAdmin';
3
+ import { MetadataClient } from './MetadataClient';
4
+
5
+ export default async function MetadataPage() {
6
+ const isAdmin = await isUserAdmin();
7
+
8
+ if (!isAdmin) {
9
+ return <ForbiddenPage />;
10
+ }
11
+
12
+ return <MetadataClient />;
13
+ }
@@ -0,0 +1,5 @@
1
+ import { NotFoundPage } from '../components/NotFoundPage/NotFoundPage';
2
+
3
+ export default function NotFound() {
4
+ return <NotFoundPage />;
5
+ }