@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.
- package/apps/agents-server/README.md +1 -1
- package/apps/agents-server/TODO.txt +6 -5
- package/apps/agents-server/config.ts +130 -0
- package/apps/agents-server/next.config.ts +1 -1
- package/apps/agents-server/public/fonts/OpenMoji-black-glyf.woff2 +0 -0
- package/apps/agents-server/public/fonts/download-font.js +22 -0
- package/apps/agents-server/src/app/[agentName]/[...rest]/page.tsx +11 -0
- package/apps/agents-server/src/app/[agentName]/page.tsx +1 -0
- package/apps/agents-server/src/app/actions.ts +37 -2
- package/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +68 -0
- package/apps/agents-server/src/app/agents/[agentName]/AgentQrCode.tsx +55 -0
- package/apps/agents-server/src/app/agents/[agentName]/AgentUrlCopy.tsx +4 -5
- package/apps/agents-server/src/app/agents/[agentName]/CopyField.tsx +44 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/book/route.ts +8 -8
- package/apps/agents-server/src/app/agents/[agentName]/api/chat/route.ts +121 -25
- package/apps/agents-server/src/app/agents/[agentName]/api/feedback/route.ts +54 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/route.ts +6 -6
- package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/systemMessage/route.ts +3 -3
- package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +29 -10
- package/apps/agents-server/src/app/agents/[agentName]/book/BookEditorWrapper.tsx +4 -5
- package/apps/agents-server/src/app/agents/[agentName]/book/page.tsx +9 -2
- package/apps/agents-server/src/app/agents/[agentName]/book+chat/AgentBookAndChat.tsx +23 -0
- package/apps/agents-server/src/app/agents/[agentName]/book+chat/{AgentBookAndChatComponent.tsx → AgentBookAndChatComponent.tsx.todo} +4 -4
- package/apps/agents-server/src/app/agents/[agentName]/book+chat/page.tsx +28 -17
- package/apps/agents-server/src/app/agents/[agentName]/book+chat/page.tsx.todo +21 -0
- package/apps/agents-server/src/app/agents/[agentName]/chat/AgentChatWrapper.tsx +34 -4
- package/apps/agents-server/src/app/agents/[agentName]/chat/page.tsx +4 -1
- package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +42 -0
- package/apps/agents-server/src/app/agents/[agentName]/page.tsx +117 -106
- package/apps/agents-server/src/app/agents/page.tsx +1 -1
- package/apps/agents-server/src/app/api/agents/route.ts +34 -0
- package/apps/agents-server/src/app/api/auth/login/route.ts +65 -0
- package/apps/agents-server/src/app/api/auth/logout/route.ts +7 -0
- package/apps/agents-server/src/app/api/metadata/route.ts +116 -0
- package/apps/agents-server/src/app/api/upload/route.ts +7 -3
- package/apps/agents-server/src/app/api/users/[username]/route.ts +75 -0
- package/apps/agents-server/src/app/api/users/route.ts +71 -0
- package/apps/agents-server/src/app/globals.css +35 -1
- package/apps/agents-server/src/app/layout.tsx +43 -23
- package/apps/agents-server/src/app/metadata/MetadataClient.tsx +271 -0
- package/apps/agents-server/src/app/metadata/page.tsx +13 -0
- package/apps/agents-server/src/app/not-found.tsx +5 -0
- package/apps/agents-server/src/app/page.tsx +117 -46
- package/apps/agents-server/src/components/Auth/AuthControls.tsx +123 -0
- package/apps/agents-server/src/components/ErrorPage/ErrorPage.tsx +33 -0
- package/apps/agents-server/src/components/ForbiddenPage/ForbiddenPage.tsx +15 -0
- package/apps/agents-server/src/components/Header/Header.tsx +146 -0
- package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +27 -0
- package/apps/agents-server/src/components/LoginDialog/LoginDialog.tsx +40 -0
- package/apps/agents-server/src/components/LoginForm/LoginForm.tsx +109 -0
- package/apps/agents-server/src/components/NotFoundPage/NotFoundPage.tsx +17 -0
- package/apps/agents-server/src/components/UsersList/UsersList.tsx +190 -0
- package/apps/agents-server/src/components/VercelDeploymentCard/VercelDeploymentCard.tsx +60 -0
- package/apps/agents-server/src/database/$getTableName.ts +18 -0
- package/apps/agents-server/src/database/$provideSupabase.ts +2 -2
- package/apps/agents-server/src/database/$provideSupabaseForServer.ts +3 -3
- package/apps/agents-server/src/database/getMetadata.ts +31 -0
- package/apps/agents-server/src/database/metadataDefaults.ts +37 -0
- package/apps/agents-server/src/database/schema.sql +81 -33
- package/apps/agents-server/src/database/schema.ts +35 -1
- package/apps/agents-server/src/middleware.ts +200 -0
- package/apps/agents-server/src/tools/$provideAgentCollectionForServer.ts +11 -7
- package/apps/agents-server/src/tools/$provideCdnForServer.ts +1 -1
- package/apps/agents-server/src/tools/$provideExecutionToolsForServer.ts +11 -13
- package/apps/agents-server/src/tools/$provideOpenAiAssistantExecutionToolsForServer.ts +7 -7
- package/apps/agents-server/src/tools/$provideServer.ts +39 -0
- package/apps/agents-server/src/utils/auth.ts +33 -0
- package/apps/agents-server/src/utils/cdn/utils/nameToSubfolderPath.ts +1 -1
- package/apps/agents-server/src/utils/getCurrentUser.ts +32 -0
- package/apps/agents-server/src/utils/getFederatedAgents.ts +66 -0
- package/apps/agents-server/src/utils/isIpAllowed.ts +101 -0
- package/apps/agents-server/src/utils/isUserAdmin.ts +31 -0
- package/apps/agents-server/src/utils/session.ts +50 -0
- package/apps/agents-server/tailwind.config.ts +2 -0
- package/esm/index.es.js +147 -31
- package/esm/index.es.js.map +1 -1
- package/esm/typings/servers.d.ts +1 -0
- package/esm/typings/src/_packages/components.index.d.ts +2 -0
- package/esm/typings/src/_packages/types.index.d.ts +2 -0
- package/esm/typings/src/_packages/utils.index.d.ts +2 -0
- package/esm/typings/src/book-2.0/agent-source/AgentBasicInformation.d.ts +12 -2
- package/esm/typings/src/book-components/PromptbookAgent/PromptbookAgent.d.ts +20 -0
- package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +14 -8
- package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabaseOptions.d.ts +10 -0
- package/esm/typings/src/commitments/MESSAGE/InitialMessageCommitmentDefinition.d.ts +28 -0
- package/esm/typings/src/commitments/index.d.ts +2 -1
- package/esm/typings/src/config.d.ts +1 -0
- package/esm/typings/src/errors/DatabaseError.d.ts +2 -2
- package/esm/typings/src/errors/WrappedError.d.ts +2 -2
- package/esm/typings/src/execution/ExecutionTask.d.ts +2 -2
- package/esm/typings/src/execution/LlmExecutionTools.d.ts +6 -1
- package/esm/typings/src/llm-providers/_common/register/$provideLlmToolsForWizardOrCli.d.ts +2 -2
- package/esm/typings/src/llm-providers/agent/Agent.d.ts +19 -3
- package/esm/typings/src/llm-providers/agent/AgentLlmExecutionTools.d.ts +13 -1
- package/esm/typings/src/llm-providers/agent/RemoteAgent.d.ts +11 -2
- package/esm/typings/src/llm-providers/openai/OpenAiAssistantExecutionTools.d.ts +6 -1
- package/esm/typings/src/remote-server/startAgentServer.d.ts +2 -2
- package/esm/typings/src/utils/color/Color.d.ts +7 -0
- package/esm/typings/src/utils/color/Color.test.d.ts +1 -0
- package/esm/typings/src/utils/environment/$getGlobalScope.d.ts +2 -2
- package/esm/typings/src/utils/misc/computeHash.d.ts +11 -0
- package/esm/typings/src/utils/misc/computeHash.test.d.ts +1 -0
- package/esm/typings/src/utils/organization/$sideEffect.d.ts +2 -2
- package/esm/typings/src/utils/organization/$side_effect.d.ts +2 -2
- package/esm/typings/src/utils/organization/TODO_USE.d.ts +2 -2
- package/esm/typings/src/utils/organization/keepUnused.d.ts +2 -2
- package/esm/typings/src/utils/organization/preserve.d.ts +3 -3
- package/esm/typings/src/utils/organization/really_any.d.ts +7 -0
- package/esm/typings/src/utils/serialization/asSerializable.d.ts +2 -2
- package/esm/typings/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/umd/index.umd.js +147 -31
- package/umd/index.umd.js.map +1 -1
- 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:
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
title:
|
|
18
|
-
description:
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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={
|
|
83
|
+
<link rel="icon" href={serverFaviconUrl} type="image/x-icon" />
|
|
68
84
|
</head>
|
|
69
|
-
<body className={`${barlowCondensed.
|
|
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
|
+
}
|