@promptbook/cli 0.103.0-48 → 0.103.0-49

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 (110) 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 +6 -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 +100 -24
  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 +6 -7
  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 +109 -106
  30. package/apps/agents-server/src/app/agents/page.tsx +1 -1
  31. package/apps/agents-server/src/app/api/auth/login/route.ts +65 -0
  32. package/apps/agents-server/src/app/api/auth/logout/route.ts +7 -0
  33. package/apps/agents-server/src/app/api/metadata/route.ts +116 -0
  34. package/apps/agents-server/src/app/api/upload/route.ts +7 -3
  35. package/apps/agents-server/src/app/api/users/[username]/route.ts +75 -0
  36. package/apps/agents-server/src/app/api/users/route.ts +71 -0
  37. package/apps/agents-server/src/app/globals.css +35 -1
  38. package/apps/agents-server/src/app/layout.tsx +43 -23
  39. package/apps/agents-server/src/app/metadata/MetadataClient.tsx +271 -0
  40. package/apps/agents-server/src/app/metadata/page.tsx +13 -0
  41. package/apps/agents-server/src/app/not-found.tsx +5 -0
  42. package/apps/agents-server/src/app/page.tsx +84 -46
  43. package/apps/agents-server/src/components/Auth/AuthControls.tsx +123 -0
  44. package/apps/agents-server/src/components/ErrorPage/ErrorPage.tsx +33 -0
  45. package/apps/agents-server/src/components/ForbiddenPage/ForbiddenPage.tsx +15 -0
  46. package/apps/agents-server/src/components/Header/Header.tsx +146 -0
  47. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +27 -0
  48. package/apps/agents-server/src/components/LoginDialog/LoginDialog.tsx +40 -0
  49. package/apps/agents-server/src/components/LoginForm/LoginForm.tsx +109 -0
  50. package/apps/agents-server/src/components/NotFoundPage/NotFoundPage.tsx +17 -0
  51. package/apps/agents-server/src/components/UsersList/UsersList.tsx +190 -0
  52. package/apps/agents-server/src/components/VercelDeploymentCard/VercelDeploymentCard.tsx +60 -0
  53. package/apps/agents-server/src/database/$getTableName.ts +18 -0
  54. package/apps/agents-server/src/database/$provideSupabase.ts +2 -2
  55. package/apps/agents-server/src/database/$provideSupabaseForServer.ts +3 -3
  56. package/apps/agents-server/src/database/getMetadata.ts +31 -0
  57. package/apps/agents-server/src/database/metadataDefaults.ts +32 -0
  58. package/apps/agents-server/src/database/schema.sql +81 -33
  59. package/apps/agents-server/src/database/schema.ts +35 -1
  60. package/apps/agents-server/src/middleware.ts +162 -0
  61. package/apps/agents-server/src/tools/$provideAgentCollectionForServer.ts +11 -7
  62. package/apps/agents-server/src/tools/$provideCdnForServer.ts +1 -1
  63. package/apps/agents-server/src/tools/$provideExecutionToolsForServer.ts +11 -13
  64. package/apps/agents-server/src/tools/$provideOpenAiAssistantExecutionToolsForServer.ts +7 -7
  65. package/apps/agents-server/src/tools/$provideServer.ts +39 -0
  66. package/apps/agents-server/src/utils/auth.ts +33 -0
  67. package/apps/agents-server/src/utils/cdn/utils/nameToSubfolderPath.ts +1 -1
  68. package/apps/agents-server/src/utils/getCurrentUser.ts +32 -0
  69. package/apps/agents-server/src/utils/isIpAllowed.ts +101 -0
  70. package/apps/agents-server/src/utils/isUserAdmin.ts +31 -0
  71. package/apps/agents-server/src/utils/session.ts +50 -0
  72. package/apps/agents-server/tailwind.config.ts +2 -0
  73. package/esm/index.es.js +147 -31
  74. package/esm/index.es.js.map +1 -1
  75. package/esm/typings/servers.d.ts +1 -0
  76. package/esm/typings/src/_packages/types.index.d.ts +2 -0
  77. package/esm/typings/src/_packages/utils.index.d.ts +2 -0
  78. package/esm/typings/src/book-2.0/agent-source/AgentBasicInformation.d.ts +12 -2
  79. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +14 -8
  80. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabaseOptions.d.ts +10 -0
  81. package/esm/typings/src/commitments/MESSAGE/InitialMessageCommitmentDefinition.d.ts +28 -0
  82. package/esm/typings/src/commitments/index.d.ts +2 -1
  83. package/esm/typings/src/config.d.ts +1 -0
  84. package/esm/typings/src/errors/DatabaseError.d.ts +2 -2
  85. package/esm/typings/src/errors/WrappedError.d.ts +2 -2
  86. package/esm/typings/src/execution/ExecutionTask.d.ts +2 -2
  87. package/esm/typings/src/execution/LlmExecutionTools.d.ts +6 -1
  88. package/esm/typings/src/llm-providers/_common/register/$provideLlmToolsForWizardOrCli.d.ts +2 -2
  89. package/esm/typings/src/llm-providers/agent/Agent.d.ts +11 -3
  90. package/esm/typings/src/llm-providers/agent/AgentLlmExecutionTools.d.ts +6 -1
  91. package/esm/typings/src/llm-providers/agent/RemoteAgent.d.ts +6 -2
  92. package/esm/typings/src/llm-providers/openai/OpenAiAssistantExecutionTools.d.ts +6 -1
  93. package/esm/typings/src/remote-server/startAgentServer.d.ts +2 -2
  94. package/esm/typings/src/utils/color/Color.d.ts +7 -0
  95. package/esm/typings/src/utils/color/Color.test.d.ts +1 -0
  96. package/esm/typings/src/utils/environment/$getGlobalScope.d.ts +2 -2
  97. package/esm/typings/src/utils/misc/computeHash.d.ts +11 -0
  98. package/esm/typings/src/utils/misc/computeHash.test.d.ts +1 -0
  99. package/esm/typings/src/utils/organization/$sideEffect.d.ts +2 -2
  100. package/esm/typings/src/utils/organization/$side_effect.d.ts +2 -2
  101. package/esm/typings/src/utils/organization/TODO_USE.d.ts +2 -2
  102. package/esm/typings/src/utils/organization/keepUnused.d.ts +2 -2
  103. package/esm/typings/src/utils/organization/preserve.d.ts +3 -3
  104. package/esm/typings/src/utils/organization/really_any.d.ts +7 -0
  105. package/esm/typings/src/utils/serialization/asSerializable.d.ts +2 -2
  106. package/esm/typings/src/version.d.ts +1 -1
  107. package/package.json +1 -1
  108. package/umd/index.umd.js +147 -31
  109. package/umd/index.umd.js.map +1 -1
  110. package/apps/agents-server/config.ts.todo +0 -38
@@ -1,16 +1,25 @@
1
1
  'use server';
2
2
 
3
3
  import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
4
- import { PromptbookQrCode } from '@promptbook-local/components';
5
4
  // import { BookEditor } from '@promptbook-local/components';
5
+ import { $provideServer } from '@/src/tools/$provideServer';
6
6
  import { parseAgentSource } from '@promptbook-local/core';
7
+ import { Columns2Icon, Edit2Icon } from 'lucide-react';
7
8
  import { headers } from 'next/headers';
9
+ import { notFound } from 'next/navigation';
10
+ import { Color } from '../../../../../../src/utils/color/Color';
11
+ import { withAlpha } from '../../../../../../src/utils/color/operators/withAlpha';
8
12
  import { $sideEffect } from '../../../../../../src/utils/organization/$sideEffect';
9
- import { AgentUrlCopy } from './AgentUrlCopy';
13
+ import { AgentChatWrapper } from './AgentChatWrapper';
14
+ import { AgentQrCode } from './AgentQrCode';
15
+ import { CopyField } from './CopyField';
16
+ import { generateAgentMetadata } from './generateAgentMetadata';
10
17
  // import { Agent } from '@promptbook-local/core';
11
18
  // import { RemoteLlmExecutionTools } from '@promptbook-local/remote-client';
12
19
  // import { OpenAiAssistantExecutionTools } from '@promptbook-local/openai';
13
20
 
21
+ export const generateMetadata = generateAgentMetadata;
22
+
14
23
  export default async function AgentPage({ params }: { params: Promise<{ agentName: string }> }) {
15
24
  // const [apiKey, setApiKey] = useStateInLocalStorage<string>('openai-apiKey', () => '');
16
25
  // const [isApiKeyVisible, setIsApiKeyVisible] = useState(false);
@@ -22,141 +31,135 @@ export default async function AgentPage({ params }: { params: Promise<{ agentNam
22
31
  agentName = decodeURIComponent(agentName);
23
32
 
24
33
  const collection = await $provideAgentCollectionForServer();
25
- const agentSource = await collection.getAgentSource(agentName);
34
+ let agentSource;
35
+ try {
36
+ agentSource = await collection.getAgentSource(agentName);
37
+ } catch (error) {
38
+ if (
39
+ error instanceof Error &&
40
+ // Note: This is a bit hacky, but valid way to check for specific error message
41
+ (error.message.includes('Cannot coerce the result to a single JSON object') ||
42
+ error.message.includes('JSON object requested, multiple (or no) results returned'))
43
+ ) {
44
+ notFound();
45
+ }
46
+ throw error;
47
+ }
26
48
  const agentProfile = parseAgentSource(agentSource);
27
49
 
50
+ const { publicUrl } = await $provideServer();
51
+
28
52
  // Build agent page URL for QR and copy
29
- const pageUrl = `${process.env.NEXT_PUBLIC_URL}/agents/${encodeURIComponent(agentName)}`;
30
- // <- TODO: !!! Better
53
+ const agentUrl = `${publicUrl.href}${encodeURIComponent(agentName)}`;
54
+ // <- TODO: [🐱‍🚀] Better
55
+
56
+ const agentEmail = `${agentName}@${publicUrl.hostname}`;
57
+
58
+ console.log('[🐱‍🚀]', { pageUrl: agentUrl });
31
59
 
32
60
  // Extract brand color from meta
33
- const brandColor = agentProfile.meta.color || '#3b82f6'; // Default to blue-600
61
+ const brandColor = Color.from(agentProfile.meta.color || '#3b82f6'); // Default to blue-600
34
62
 
35
63
  // Mock agent actions
36
- const agentActions = ['Emails', 'Web', 'Documents', 'Browser', 'WhatsApp', 'Coding'];
37
-
38
- // Render agent profile fields
39
- const renderProfileFields = () => {
40
- const renderValue = (value: unknown): React.ReactNode => {
41
- if (value === null || value === undefined) {
42
- return <span className="text-gray-400 italic">Not specified</span>;
43
- }
44
- if (typeof value === 'object' && !Array.isArray(value)) {
45
- const objValue = value as Record<string, unknown>;
46
- return (
47
- <div className="space-y-1 pl-3 border-l-2 border-gray-200">
48
- {Object.entries(objValue).map(([subKey, subValue]) => (
49
- <div key={subKey} className="flex gap-2">
50
- <span className="text-xs text-gray-600 font-medium">{subKey}:</span>
51
- <span className="text-sm text-gray-700">{String(subValue)}</span>
52
- </div>
53
- ))}
54
- </div>
55
- );
56
- }
57
- if (Array.isArray(value)) {
58
- return (
59
- <ul className="list-disc list-inside space-y-0.5">
60
- {value.map((item, idx) => (
61
- <li key={idx} className="text-sm text-gray-700">
62
- {String(item)}
63
- </li>
64
- ))}
65
- </ul>
66
- );
67
- }
68
- return <span className="text-base text-gray-800 break-words">{String(value)}</span>;
69
- };
70
-
71
- return (
72
- <div className="space-y-4">
73
- {Object.entries(agentProfile).map(([key, value]) => (
74
- <div key={key} className="flex flex-col gap-1">
75
- <span className="text-xs text-gray-500 font-semibold uppercase tracking-wide">{key}</span>
76
- {renderValue(value)}
77
- </div>
78
- ))}
79
- </div>
80
- );
81
- };
64
+ const agentActions = ['Emails', 'Web chat', 'Read documents', 'Browser', 'WhatsApp', '<Coding/>'];
82
65
 
83
66
  return (
84
- <div
85
- className="w-full min-h-screen bg-gray-50 py-10 px-4 flex items-center justify-center"
86
- style={{ backgroundColor: brandColor }}
87
- >
88
- <div className="max-w-5xl w-full bg-white rounded-xl shadow-lg p-8 flex flex-col md:flex-row gap-8">
89
- {/* Left column: Profile info */}
90
- <div className="flex-1 flex flex-col gap-6">
91
- <div className="flex items-center gap-4">
92
- {agentProfile.meta.image && (
93
- // eslint-disable-next-line @next/next/no-img-element
94
- <img
95
- src={agentProfile.meta.image as string}
96
- alt={agentProfile.agentName || 'Agent'}
97
- width={64}
98
- height={64}
99
- className="rounded-full object-cover border-2"
100
- style={{ borderColor: brandColor }}
101
- />
102
- )}
103
- <div className="flex-1">
104
- <h1 className="text-3xl font-bold text-gray-900">{agentProfile.agentName}</h1>
67
+ <div className="flex flex-col md:flex-row h-[calc(100vh-60px)] w-full overflow-hidden">
68
+ {/* Left sidebar: Profile info */}
69
+ <div
70
+ className="w-full md:w-[400px] flex flex-col gap-6 p-6 overflow-y-auto border-r bg-gray-50 flex-shrink-0"
71
+ style={{
72
+ backgroundColor: brandColor.then(withAlpha(0.05)).toHex(),
73
+ borderColor: brandColor.then(withAlpha(0.1)).toHex(),
74
+ }}
75
+ >
76
+ <div className="flex items-center gap-4">
77
+ {agentProfile.meta.image && (
78
+ // eslint-disable-next-line @next/next/no-img-element
79
+ <img
80
+ src={agentProfile.meta.image as string}
81
+ alt={agentProfile.agentName || 'Agent'}
82
+ width={64}
83
+ height={64}
84
+ className="rounded-full object-cover border-2 aspect-square w-16 h-16"
85
+ style={{ borderColor: brandColor.toHex() }}
86
+ />
87
+ )}
88
+ <div className="flex-1">
89
+ <h1 className="text-3xl font-bold text-gray-900 break-words">{agentProfile.agentName}</h1>
90
+ <span
91
+ className="inline-block mt-1 px-2 py-1 rounded text-xs font-semibold text-white"
92
+ style={{ backgroundColor: brandColor.toHex() }}
93
+ >
94
+ Agent
95
+ </span>
96
+ </div>
97
+ </div>
98
+
99
+ <p className="text-gray-700">{agentProfile.personaDescription}</p>
100
+
101
+ <div className="flex flex-col gap-2">
102
+ <h2 className="text-sm font-semibold text-gray-700 uppercase tracking-wide">Capabilities</h2>
103
+ <div className="flex flex-wrap gap-2">
104
+ {agentActions.map((action) => (
105
105
  <span
106
- className="inline-block mt-1 px-2 py-1 rounded text-xs font-semibold text-white"
107
- style={{ backgroundColor: brandColor }}
106
+ key={action}
107
+ className="px-3 py-1 bg-white text-gray-700 rounded-full text-xs font-medium border border-gray-200 shadow-sm"
108
108
  >
109
- Agent
109
+ {action}
110
110
  </span>
111
- </div>
112
- </div>
113
- {renderProfileFields()}
114
- <div className="flex flex-col gap-2">
115
- <h2 className="text-sm font-semibold text-gray-700 uppercase tracking-wide">Actions</h2>
116
- <div className="flex flex-wrap gap-2">
117
- {agentActions.map((action) => (
118
- <span
119
- key={action}
120
- className="px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-xs font-medium border border-gray-200"
121
- >
122
- {action}
123
- </span>
124
- ))}
125
- </div>
111
+ ))}
126
112
  </div>
127
- <div className="flex gap-4 mt-6">
113
+ </div>
114
+
115
+ <div className="flex flex-col gap-2 mt-auto">
116
+ <div className="flex gap-2">
128
117
  <a
129
- href={`${pageUrl}/chat`}
118
+ href={`/agents/${encodeURIComponent(agentName)}/book+chat`}
130
119
  // <- TODO: [🧠] Can I append path like this on current browser URL in href?
131
- className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded shadow font-semibold transition"
120
+ className="flex-1 inline-flex items-center justify-center whitespace-nowrap bg-white hover:bg-gray-100 text-gray-800 px-4 py-2 rounded shadow font-semibold transition border border-gray-200"
132
121
  >
133
- 💬 Chat
122
+ <Columns2Icon className="ml-2 w-4 h-4 mr-2" />
123
+ Book + Chat
134
124
  </a>
135
125
  <a
136
- href={`${pageUrl}/book`}
126
+ href={`/agents/${encodeURIComponent(agentName)}/book`}
137
127
  // <- TODO: [🧠] Can I append path like this on current browser URL in href?
138
- className="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded shadow font-semibold transition"
128
+ className="flex-1 inline-flex items-center justify-center whitespace-nowrap bg-white hover:bg-gray-100 text-gray-800 px-4 py-2 rounded shadow font-semibold transition border border-gray-200"
139
129
  >
140
- ✏️ Edit Agent Book
130
+ <Edit2Icon className="ml-2 w-4 h-4 mr-2" />
131
+ Edit
141
132
  </a>
142
133
  </div>
143
134
  </div>
144
- {/* Right column: QR, source, copy */}
145
- <div className="flex flex-col items-center gap-6 min-w-[260px]">
146
- <div className="bg-gray-100 rounded-lg p-4 flex flex-col items-center">
147
- <PromptbookQrCode value={pageUrl} />
148
- <span className="mt-2 text-xs text-gray-500">Scan to open agent page</span>
135
+
136
+ <div className="flex flex-col items-center gap-4 pt-6 border-t border-gray-200 w-full">
137
+ <div className="bg-white rounded-lg p-4 flex flex-col items-center shadow-sm border border-gray-100">
138
+ <AgentQrCode
139
+ agentName={agentProfile.agentName || 'Agent'}
140
+ personaDescription={agentProfile.personaDescription}
141
+ agentUrl={agentUrl}
142
+ agentEmail={agentEmail}
143
+ />
144
+ </div>
145
+ <div className="flex flex-col gap-2 w-full">
146
+ <CopyField label="Agent Page URL" value={agentUrl} />
147
+ <CopyField label="Agent Email" value={agentEmail} />
149
148
  </div>
150
- <AgentUrlCopy url={pageUrl} />
151
149
  </div>
152
150
  </div>
151
+
152
+ {/* Main content: Chat */}
153
+ <div className="flex-1 relative h-full bg-white">
154
+ <AgentChatWrapper agentUrl={agentUrl} />
155
+ </div>
153
156
  </div>
154
157
  );
155
158
  }
156
159
 
157
160
  /**
158
- * TODO: !!! Make this page look nice - 🃏
159
- * TODO: !!! Show usage of LLM
161
+ * TODO: [🐱‍🚀] Make this page look nice - 🃏
162
+ * TODO: [🐱‍🚀] Show usage of LLM
160
163
  * TODO: [🚗] Components and pages here should be just tiny UI wraper around proper agent logic and conponents
161
164
  * TODO: [🎣][🧠] Maybe do API / Page for transpilers, Allow to export each agent
162
165
  */
@@ -7,5 +7,5 @@ export default async function AgentsPage() {
7
7
  }
8
8
 
9
9
  /**
10
- * TODO: !!! Distinguish between `/` and `/agents` pages
10
+ * TODO: [🐱‍🚀] Distinguish between `/` and `/agents` pages
11
11
  */
@@ -0,0 +1,65 @@
1
+ import { $getTableName } from '@/src/database/$getTableName';
2
+ import { $provideSupabaseForServer } from '../../../../database/$provideSupabaseForServer';
3
+ import { AgentsServerDatabase } from '../../../../database/schema';
4
+ import { verifyPassword } from '../../../../utils/auth';
5
+ import { setSession } from '../../../../utils/session';
6
+ import { NextResponse } from 'next/server';
7
+
8
+ export async function POST(request: Request) {
9
+ try {
10
+ const body = await request.json();
11
+ const { username, password } = body;
12
+
13
+ if (!username || !password) {
14
+ return NextResponse.json({ error: 'Username and password are required' }, { status: 400 });
15
+ }
16
+
17
+ // 1. Check if it's the environment admin
18
+ if (process.env.ADMIN_PASSWORD && password === process.env.ADMIN_PASSWORD && username === 'admin') {
19
+ // Or maybe allow any username if password matches admin password?
20
+ // The task says "process.env.ADMIN_PASSWORD is like one of the admin users"
21
+ // Assuming username 'admin' for environment password login.
22
+ await setSession({ username: 'admin', isAdmin: true });
23
+ return NextResponse.json({ success: true });
24
+ }
25
+
26
+ // 2. Check DB users
27
+ const supabase = $provideSupabaseForServer();
28
+ const { data: user, error } = await supabase
29
+ .from(await $getTableName('User'))
30
+ .select('*')
31
+ .eq('username', username)
32
+ .single();
33
+
34
+ if (error || !user) {
35
+ // Check if password matches ADMIN_PASSWORD even if user doesn't exist?
36
+ // "The table User should work together with the process.env.ADMIN_PASSWORD"
37
+ // If the user enters a password that matches process.env.ADMIN_PASSWORD, should they get admin access regardless of username?
38
+ // "process.env.ADMIN_PASSWORD is like one of the admin users" implies it's a specific credential.
39
+ // Let's stick to: if username is 'admin' and password is ADMIN_PASSWORD, it works.
40
+ // Or if the password matches ADMIN_PASSWORD, maybe we grant admin access?
41
+ // "Non-admin users can only log in... cannot see list of users"
42
+
43
+ // Re-reading: "process.env.ADMIN_PASSWORD is like one of the admin users in the User table"
44
+ // This suggests it's treated as a user.
45
+
46
+ // If I login with a valid user from DB, I check password hash.
47
+ // If I login with 'admin' and ADMIN_PASSWORD, I get admin.
48
+
49
+ return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
50
+ }
51
+
52
+ const isValid = await verifyPassword(password, (user as AgentsServerDatabase['public']['Tables']['User']['Row']).passwordHash);
53
+
54
+ if (!isValid) {
55
+ return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
56
+ }
57
+
58
+ await setSession({ username: (user as AgentsServerDatabase['public']['Tables']['User']['Row']).username, isAdmin: (user as AgentsServerDatabase['public']['Tables']['User']['Row']).isAdmin });
59
+ return NextResponse.json({ success: true });
60
+
61
+ } catch (error) {
62
+ console.error('Login error:', error);
63
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
64
+ }
65
+ }
@@ -0,0 +1,7 @@
1
+ import { clearSession } from '../../../../utils/session';
2
+ import { NextResponse } from 'next/server';
3
+
4
+ export async function POST() {
5
+ await clearSession();
6
+ return NextResponse.json({ success: true });
7
+ }
@@ -0,0 +1,116 @@
1
+ import { $getTableName } from '../../../database/$getTableName';
2
+ import { $provideSupabase } from '../../../database/$provideSupabase';
3
+ import { isUserAdmin } from '../../../utils/isUserAdmin';
4
+ import { NextRequest, NextResponse } from 'next/server';
5
+
6
+ export async function GET(request: NextRequest) {
7
+ if (!(await isUserAdmin())) {
8
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
9
+ }
10
+
11
+ const supabase = $provideSupabase();
12
+ const table = await $getTableName('Metadata');
13
+
14
+ const { data, error } = await supabase.from(table).select('*').order('key');
15
+
16
+ if (error) {
17
+ return NextResponse.json({ error: error.message }, { status: 500 });
18
+ }
19
+
20
+ return NextResponse.json(data);
21
+ }
22
+
23
+ export async function POST(request: NextRequest) {
24
+ if (!(await isUserAdmin())) {
25
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
26
+ }
27
+
28
+ try {
29
+ const body = await request.json();
30
+ const { key, value, note } = body;
31
+
32
+ if (!key || !value) {
33
+ return NextResponse.json({ error: 'Key and value are required' }, { status: 400 });
34
+ }
35
+
36
+ const supabase = $provideSupabase();
37
+ const table = await $getTableName('Metadata');
38
+
39
+ const { data, error } = await supabase
40
+ .from(table)
41
+ .insert({ key, value, note })
42
+ .select()
43
+ .single();
44
+
45
+ if (error) {
46
+ return NextResponse.json({ error: error.message }, { status: 500 });
47
+ }
48
+
49
+ return NextResponse.json(data);
50
+ } catch (e) {
51
+ return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
52
+ }
53
+ }
54
+
55
+ export async function PUT(request: NextRequest) {
56
+ if (!(await isUserAdmin())) {
57
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
58
+ }
59
+
60
+ try {
61
+ const body = await request.json();
62
+ const { key, value, note } = body;
63
+
64
+ if (!key || !value) {
65
+ return NextResponse.json({ error: 'Key and value are required' }, { status: 400 });
66
+ }
67
+
68
+ const supabase = $provideSupabase();
69
+ const table = await $getTableName('Metadata');
70
+
71
+ // Using upsert if it exists or update if strict
72
+ // Since key is unique, upsert works well, but usually PUT implies update.
73
+ // Let's use update to be safe and explicit about editing existing.
74
+ // Actually, for editing, we identify by ID or Key.
75
+ // Let's use Key as identifier since it is unique.
76
+
77
+ const { data, error } = await supabase
78
+ .from(table)
79
+ .update({ value, note, updatedAt: new Date().toISOString() })
80
+ .eq('key', key)
81
+ .select()
82
+ .single();
83
+
84
+ if (error) {
85
+ return NextResponse.json({ error: error.message }, { status: 500 });
86
+ }
87
+
88
+ return NextResponse.json(data);
89
+ } catch (e) {
90
+ return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
91
+ }
92
+ }
93
+
94
+ export async function DELETE(request: NextRequest) {
95
+ if (!(await isUserAdmin())) {
96
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
97
+ }
98
+
99
+ const searchParams = request.nextUrl.searchParams;
100
+ const key = searchParams.get('key');
101
+
102
+ if (!key) {
103
+ return NextResponse.json({ error: 'Key is required' }, { status: 400 });
104
+ }
105
+
106
+ const supabase = $provideSupabase();
107
+ const table = await $getTableName('Metadata');
108
+
109
+ const { error } = await supabase.from(table).delete().eq('key', key);
110
+
111
+ if (error) {
112
+ return NextResponse.json({ error: error.message }, { status: 500 });
113
+ }
114
+
115
+ return NextResponse.json({ success: true });
116
+ }
@@ -4,6 +4,7 @@ import { serializeError } from '@promptbook-local/utils';
4
4
  import formidable from 'formidable';
5
5
  import { readFile } from 'fs/promises';
6
6
  import { NextRequest, NextResponse } from 'next/server';
7
+ import { forTime } from 'waitasecond';
7
8
  import { assertsError } from '../../../../../../src/errors/assertsError';
8
9
  import { string_url } from '../../../../../../src/types/typeAliases';
9
10
  import { keepUnused } from '../../../../../../src/utils/organization/keepUnused';
@@ -13,6 +14,9 @@ import { validateMimeType } from '../../../../src/utils/validators/validateMimeT
13
14
 
14
15
  export async function POST(request: NextRequest) {
15
16
  try {
17
+ await forTime(1);
18
+ // await forTime(5000);
19
+
16
20
  const nodeRequest = await nextRequestToNodeRequest(request);
17
21
 
18
22
  const files = await new Promise<formidable.Files>((resolve, reject) => {
@@ -57,13 +61,13 @@ export async function POST(request: NextRequest) {
57
61
  return new Response(
58
62
  JSON.stringify(
59
63
  serializeError(error),
60
- // <- TODO: !!! Rename `serializeError` to `errorToJson`
64
+ // <- TODO: [🐱‍🚀] Rename `serializeError` to `errorToJson`
61
65
  null,
62
66
  4,
63
- // <- TODO: !!! Allow to configure pretty print for agent server
67
+ // <- TODO: [🐱‍🚀] Allow to configure pretty print for agent server
64
68
  ),
65
69
  {
66
- status: 400, // <- TODO: !!! Make `errorToHttpStatusCode`
70
+ status: 400, // <- TODO: [🐱‍🚀] Make `errorToHttpStatusCode`
67
71
  headers: { 'Content-Type': 'application/json' },
68
72
  },
69
73
  );
@@ -0,0 +1,75 @@
1
+ import { $getTableName } from '@/src/database/$getTableName';
2
+ import { NextResponse } from 'next/server';
3
+ import { $provideSupabaseForServer } from '../../../../database/$provideSupabaseForServer';
4
+ import { hashPassword } from '../../../../utils/auth';
5
+ import { isUserAdmin } from '../../../../utils/isUserAdmin';
6
+
7
+ export async function PATCH(request: Request, { params }: { params: Promise<{ username: string }> }) {
8
+ if (!(await isUserAdmin())) {
9
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
10
+ }
11
+
12
+ try {
13
+ const { username: usernameParam } = await params;
14
+ const body = await request.json();
15
+ const { password, isAdmin } = body;
16
+
17
+ const updates: { updatedAt: string; passwordHash?: string; isAdmin?: boolean } = {
18
+ updatedAt: new Date().toISOString(),
19
+ };
20
+
21
+ if (password) {
22
+ updates.passwordHash = await hashPassword(password);
23
+ }
24
+
25
+ if (typeof isAdmin === 'boolean') {
26
+ updates.isAdmin = isAdmin;
27
+ }
28
+
29
+ const supabase = $provideSupabaseForServer();
30
+ const { data: updatedUser, error } = await supabase
31
+ .from(await $getTableName('User'))
32
+ .update(updates)
33
+ .eq('username', usernameParam)
34
+ .select('id, username, createdAt, updatedAt, isAdmin')
35
+ .single();
36
+
37
+ if (error) {
38
+ throw error;
39
+ }
40
+
41
+ if (!updatedUser) {
42
+ return NextResponse.json({ error: 'User not found' }, { status: 404 });
43
+ }
44
+
45
+ return NextResponse.json(updatedUser);
46
+ } catch (error) {
47
+ console.error('Update user error:', error);
48
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
49
+ }
50
+ }
51
+
52
+ export async function DELETE(request: Request, { params }: { params: Promise<{ username: string }> }) {
53
+ if (!(await isUserAdmin())) {
54
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
55
+ }
56
+
57
+ try {
58
+ const { username: usernameParam } = await params;
59
+ const supabase = $provideSupabaseForServer();
60
+
61
+ const { error } = await supabase
62
+ .from(await $getTableName('User'))
63
+ .delete()
64
+ .eq('username', usernameParam);
65
+
66
+ if (error) {
67
+ throw error;
68
+ }
69
+
70
+ return NextResponse.json({ success: true });
71
+ } catch (error) {
72
+ console.error('Delete user error:', error);
73
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
74
+ }
75
+ }
@@ -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
+ }