@adminforge/core 0.3.1
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/.turbo/turbo-build.log +56 -0
- package/CHANGELOG.md +32 -0
- package/LICENSE +21 -0
- package/bin/adminforge.js +317 -0
- package/dist/auth-client.cjs +45 -0
- package/dist/auth-client.cjs.map +1 -0
- package/dist/auth-client.d.cts +17 -0
- package/dist/auth-client.d.ts +17 -0
- package/dist/auth-client.js +20 -0
- package/dist/auth-client.js.map +1 -0
- package/dist/auth.cjs +65 -0
- package/dist/auth.cjs.map +1 -0
- package/dist/auth.d.cts +21 -0
- package/dist/auth.d.ts +21 -0
- package/dist/auth.js +36 -0
- package/dist/auth.js.map +1 -0
- package/dist/client-D0cjJVsn.d.ts +20 -0
- package/dist/client-sRnmZ-Y9.d.cts +20 -0
- package/dist/index-CyzxaE7n.d.cts +124 -0
- package/dist/index-CyzxaE7n.d.ts +124 -0
- package/dist/index.cjs +453 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +65 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +410 -0
- package/dist/index.js.map +1 -0
- package/dist/next.cjs +839 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +84 -0
- package/dist/next.d.ts +84 -0
- package/dist/next.js +800 -0
- package/dist/next.js.map +1 -0
- package/dist/styles.css +763 -0
- package/dist/styles.css.map +1 -0
- package/dist/styles.d.cts +2 -0
- package/dist/styles.d.ts +2 -0
- package/dist/ui.cjs +2500 -0
- package/dist/ui.cjs.map +1 -0
- package/dist/ui.d.cts +119 -0
- package/dist/ui.d.ts +119 -0
- package/dist/ui.js +2448 -0
- package/dist/ui.js.map +1 -0
- package/eslint.config.js +35 -0
- package/package.json +99 -0
- package/src/api/controller.ts +234 -0
- package/src/api/index.ts +4 -0
- package/src/api/next.ts +281 -0
- package/src/api/security/agent-auth.ts +134 -0
- package/src/auth/config.ts +20 -0
- package/src/auth/index.ts +3 -0
- package/src/auth/middleware.ts +15 -0
- package/src/auth/provider.tsx +28 -0
- package/src/core/fields/index.ts +119 -0
- package/src/core/hooks/index.ts +60 -0
- package/src/core/index.ts +43 -0
- package/src/core/registry/index.ts +22 -0
- package/src/core/schema/collection.ts +12 -0
- package/src/core/schema/config.ts +11 -0
- package/src/core/schema/normalize.ts +32 -0
- package/src/core/types/index.ts +114 -0
- package/src/db/client.ts +146 -0
- package/src/db/index.ts +3 -0
- package/src/db/schema-generator.ts +104 -0
- package/src/fields/index.ts +1 -0
- package/src/index.ts +4 -0
- package/src/next.ts +3 -0
- package/src/styles/adminforge.css +840 -0
- package/src/ui/AdminDashboard.tsx +176 -0
- package/src/ui/AdminForgeContext.tsx +64 -0
- package/src/ui/components/AdminLayout.tsx +107 -0
- package/src/ui/form-engine/FormEngine.tsx +250 -0
- package/src/ui/form-engine/ImageUpload.tsx +68 -0
- package/src/ui/form-engine/RelationInput.tsx +215 -0
- package/src/ui/form-engine/RichTextEditor.tsx +708 -0
- package/src/ui/index.ts +18 -0
- package/src/ui/screens/AdminPage.tsx +162 -0
- package/src/ui/screens/AgentTokenPage.tsx +232 -0
- package/src/ui/screens/CollectionFormPage.tsx +135 -0
- package/src/ui/screens/CollectionListPage.tsx +170 -0
- package/src/ui/screens/CollectionSchemaPage.tsx +180 -0
- package/src/ui/screens/RoleDetailPage.tsx +147 -0
- package/src/ui/screens/RolesListPage.tsx +57 -0
- package/src/ui/table-engine/TableEngine.tsx +157 -0
- package/src/ui.ts +3 -0
- package/tsconfig.json +10 -0
- package/tsup.config.ts +54 -0
package/src/ui/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { AdminLayout } from "./components/AdminLayout.js";
|
|
2
|
+
export { AdminDashboard } from "./AdminDashboard.js";
|
|
3
|
+
export { AdminPage } from "./screens/AdminPage.js";
|
|
4
|
+
export { CollectionListPage } from "./screens/CollectionListPage.js";
|
|
5
|
+
export { CollectionFormPage } from "./screens/CollectionFormPage.js";
|
|
6
|
+
export { CollectionSchemaPage } from "./screens/CollectionSchemaPage.js";
|
|
7
|
+
export { RolesListPage } from "./screens/RolesListPage.js";
|
|
8
|
+
export { RoleDetailPage } from "./screens/RoleDetailPage.js";
|
|
9
|
+
export { AgentTokenPage } from "./screens/AgentTokenPage.js";
|
|
10
|
+
export { FormEngine } from "./form-engine/FormEngine.js";
|
|
11
|
+
export { TableEngine } from "./table-engine/TableEngine.js";
|
|
12
|
+
export { RichTextEditor } from "./form-engine/RichTextEditor.js";
|
|
13
|
+
export { ImageUpload } from "./form-engine/ImageUpload.js";
|
|
14
|
+
export { AdminForgeProvider, useAdminForge } from "./AdminForgeContext.js";
|
|
15
|
+
|
|
16
|
+
// Auth Client
|
|
17
|
+
export { AuthProvider, useAdminSession } from "../auth/provider.js";
|
|
18
|
+
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState, useEffect } from "react";
|
|
3
|
+
import type { AdminForgeConfig } from "../../core";
|
|
4
|
+
import { AdminLayout } from "../components/AdminLayout.js";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { useAdminSession } from "../../auth/provider.js";
|
|
7
|
+
|
|
8
|
+
interface AdminPageProps {
|
|
9
|
+
config: AdminForgeConfig;
|
|
10
|
+
role?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const iconMap: Record<string, string> = {
|
|
14
|
+
posts: "article",
|
|
15
|
+
categories: "category",
|
|
16
|
+
tags: "sell",
|
|
17
|
+
users: "person",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function formatRelativeTime(date: Date) {
|
|
21
|
+
const diff = Date.now() - date.getTime();
|
|
22
|
+
const minutes = Math.floor(diff / 60000);
|
|
23
|
+
const hours = Math.floor(minutes / 60);
|
|
24
|
+
const days = Math.floor(hours / 24);
|
|
25
|
+
|
|
26
|
+
if (isNaN(date.getTime())) return "Invalid date";
|
|
27
|
+
if (minutes < 1) return "Just now";
|
|
28
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
29
|
+
if (hours < 24) return `${hours}h ago`;
|
|
30
|
+
return `${days}d ago`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function CollectionActivity({ activity }: { activity?: { createdAt: string, updatedAt: string } }) {
|
|
34
|
+
if (!activity) return <span style={{ color: '#94a3b8', fontSize: '13px' }}>-</span>;
|
|
35
|
+
|
|
36
|
+
const createdAt = new Date(activity.createdAt);
|
|
37
|
+
const updatedAt = new Date(activity.updatedAt);
|
|
38
|
+
const isUpdated = updatedAt.getTime() > createdAt.getTime() + 1000;
|
|
39
|
+
const date = isUpdated ? updatedAt : createdAt;
|
|
40
|
+
const label = isUpdated ? "Updated" : "Created";
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' }}>
|
|
44
|
+
<span
|
|
45
|
+
style={{
|
|
46
|
+
fontSize: '9px',
|
|
47
|
+
padding: '1px 6px',
|
|
48
|
+
borderRadius: '10px',
|
|
49
|
+
background: isUpdated ? '#fef3c7' : '#92400e20',
|
|
50
|
+
color: isUpdated ? '#92400e' : '#166534',
|
|
51
|
+
fontWeight: 700,
|
|
52
|
+
textTransform: 'uppercase',
|
|
53
|
+
border: '1px solid',
|
|
54
|
+
borderColor: isUpdated ? '#fde68a' : '#bbf7d0'
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{label}
|
|
58
|
+
</span>
|
|
59
|
+
<span style={{ fontWeight: 500, color: 'var(--color-text)' }}>{formatRelativeTime(date)}</span>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function AdminPage({ config, role: propRole }: AdminPageProps) {
|
|
65
|
+
const session = useAdminSession();
|
|
66
|
+
const role = propRole || session?.role || session?.user?.role;
|
|
67
|
+
const schemaActivity = config.collections?.[0] && (config.collections[0] as any)?.schemaActivity;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<AdminLayout config={config} currentPath="/admin" role={role}>
|
|
71
|
+
<div className="adminforge-dashboard">
|
|
72
|
+
<div className="mb-10">
|
|
73
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '16px', marginBottom: '8px' }}>
|
|
74
|
+
<h2 className="adminforge-display-title" style={{ marginBottom: 0 }}>Collection Registry</h2>
|
|
75
|
+
</div>
|
|
76
|
+
<p className="adminforge-display-subtitle" style={{ marginBottom: '32px' }}>
|
|
77
|
+
Index of all data models defined in your system.
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{schemaActivity && (
|
|
82
|
+
<div style={{
|
|
83
|
+
display: 'flex',
|
|
84
|
+
alignItems: 'center',
|
|
85
|
+
gap: '12px',
|
|
86
|
+
padding: '12px 20px',
|
|
87
|
+
background: '#f0f9ff',
|
|
88
|
+
border: '1px solid #bae6fd',
|
|
89
|
+
borderRadius: '12px',
|
|
90
|
+
marginBottom: '24px',
|
|
91
|
+
boxShadow: '0 1px 2px rgba(0,0,0,0.05)'
|
|
92
|
+
}}>
|
|
93
|
+
<span className="material-symbols-outlined" style={{ color: '#0284c7', fontSize: '20px' }}>sync</span>
|
|
94
|
+
<div style={{ fontSize: '14px', color: '#0369a1' }}>
|
|
95
|
+
<span style={{ fontWeight: 600 }}>Schema Synced:</span> Your definitions from <code style={{ background: '#e0f2fe', padding: '2px 4px', borderRadius: '4px' }}>adminforge.ts</code> were last updated <span style={{ fontWeight: 500 }}>{formatRelativeTime(new Date(schemaActivity.updatedAt))}</span>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
<div className="adminforge-table-wrapper">
|
|
101
|
+
<div style={{ padding: '24px', borderBottom: '1px solid var(--color-border)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
102
|
+
<h3 style={{ fontSize: '18px', fontWeight: 600 }}>Registered Collections</h3>
|
|
103
|
+
</div>
|
|
104
|
+
<table className="adminforge-table">
|
|
105
|
+
<thead>
|
|
106
|
+
<tr>
|
|
107
|
+
<th style={{ width: 'auto' }}>Collection Name</th>
|
|
108
|
+
<th style={{ width: '180px' }}>Field Definitions</th>
|
|
109
|
+
<th style={{ width: '120px', textAlign: 'right' }}>Actions</th>
|
|
110
|
+
</tr>
|
|
111
|
+
</thead>
|
|
112
|
+
<tbody>
|
|
113
|
+
{config.collections
|
|
114
|
+
.filter((collection) => {
|
|
115
|
+
const a = collection.access;
|
|
116
|
+
if (!a?.read) return true;
|
|
117
|
+
if (!role) return false;
|
|
118
|
+
return a.read.includes(role);
|
|
119
|
+
})
|
|
120
|
+
.map((collection) => {
|
|
121
|
+
return (
|
|
122
|
+
<tr key={collection.name}>
|
|
123
|
+
<td>
|
|
124
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
125
|
+
<div style={{ width: '36px', height: '36px', borderRadius: '8px', background: '#f1f5f9', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#64748b' }}>
|
|
126
|
+
<span className="material-symbols-outlined" style={{ fontSize: '20px' }}>
|
|
127
|
+
{collection.icon || 'database'}
|
|
128
|
+
</span>
|
|
129
|
+
</div>
|
|
130
|
+
<span style={{ fontWeight: 600, color: 'var(--color-text)', fontSize: '15px' }}>{collection.label}</span>
|
|
131
|
+
</div>
|
|
132
|
+
</td>
|
|
133
|
+
<td>
|
|
134
|
+
<span className="adminforge-badge adminforge-badge-secondary" style={{ padding: '4px 10px' }}>
|
|
135
|
+
{Object.keys(collection.fields).length} mapped fields
|
|
136
|
+
</span>
|
|
137
|
+
</td>
|
|
138
|
+
<td style={{ textAlign: 'right' }}>
|
|
139
|
+
<div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
|
|
140
|
+
<Link href={`/admin/${collection.name}`} className="adminforge-btn-icon" title="View Data">
|
|
141
|
+
<span className="material-symbols-outlined" style={{ fontSize: '20px' }}>database</span>
|
|
142
|
+
</Link>
|
|
143
|
+
<Link href={`/admin/${collection.name}/schema`} className="adminforge-btn-icon" title="View Schema">
|
|
144
|
+
<span className="material-symbols-outlined" style={{ fontSize: '20px' }}>visibility</span>
|
|
145
|
+
</Link>
|
|
146
|
+
</div>
|
|
147
|
+
</td>
|
|
148
|
+
</tr>
|
|
149
|
+
);
|
|
150
|
+
})}
|
|
151
|
+
</tbody>
|
|
152
|
+
</table>
|
|
153
|
+
<div style={{ padding: '20px 24px', background: '#fcfcfd', borderTop: '1px solid var(--color-border)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
154
|
+
<p style={{ fontSize: '13px', color: 'var(--color-text-secondary)' }}>
|
|
155
|
+
Showing {config.collections.length} models from your current configuration.
|
|
156
|
+
</p>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</AdminLayout>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { AdminLayout } from "../components/AdminLayout.js";
|
|
5
|
+
import type { AdminForgeConfig } from "../../core/index.js";
|
|
6
|
+
import { useAdminSession } from "../../auth/provider.js";
|
|
7
|
+
import { useAdminForge } from "../AdminForgeContext.js";
|
|
8
|
+
|
|
9
|
+
interface AgentTokenPageProps {
|
|
10
|
+
config: AdminForgeConfig;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function AgentTokenPage({ config }: AgentTokenPageProps) {
|
|
14
|
+
const { apiBase } = useAdminForge();
|
|
15
|
+
const session = useAdminSession();
|
|
16
|
+
const [selectedScopes, setSelectedScopes] = useState<string[]>([]);
|
|
17
|
+
const [expiresIn, setExpiresIn] = useState(600); // Default 10 min
|
|
18
|
+
const [token, setToken] = useState<string | null>(null);
|
|
19
|
+
const [loading, setLoading] = useState(false);
|
|
20
|
+
|
|
21
|
+
const toggleScope = (scope: string) => {
|
|
22
|
+
setSelectedScopes(prev =>
|
|
23
|
+
prev.includes(scope) ? prev.filter(s => s !== scope) : [...prev, scope]
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const handleGenerate = async () => {
|
|
28
|
+
setLoading(true);
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetch(`${apiBase}/_tokens`, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: { "Content-Type": "application/json" },
|
|
33
|
+
body: JSON.stringify({ scope: selectedScopes, expiresIn }),
|
|
34
|
+
});
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
if (data.token) {
|
|
37
|
+
setToken(data.token);
|
|
38
|
+
} else {
|
|
39
|
+
alert(data.error || "Failed to generate token");
|
|
40
|
+
}
|
|
41
|
+
} catch (e: any) {
|
|
42
|
+
alert("Error connecting to API: " + e.message);
|
|
43
|
+
} finally {
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const role = session?.role || (session?.user as any)?.role;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<AdminLayout config={config} currentPath="/admin/settings/agent-tokens" role={role}>
|
|
52
|
+
<div className="adminforge-collection-page">
|
|
53
|
+
<div className="adminforge-page-header" style={{ marginBottom: '40px' }}>
|
|
54
|
+
<div>
|
|
55
|
+
<h1 style={{ fontSize: '24px', fontWeight: 700, margin: 0 }}>Agent Token Generator</h1>
|
|
56
|
+
<p style={{ color: '#64748b', fontSize: '14px', marginTop: '4px' }}>Issue secure, scoped passes for your AI agents.</p>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{token ? (
|
|
61
|
+
<div style={{
|
|
62
|
+
background: '#f0fdf4',
|
|
63
|
+
border: '1px solid #10b981',
|
|
64
|
+
borderRadius: '12px',
|
|
65
|
+
padding: '32px',
|
|
66
|
+
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)'
|
|
67
|
+
}}>
|
|
68
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '20px' }}>
|
|
69
|
+
<span className="material-symbols-outlined" style={{ color: '#059669', fontSize: '32px' }}>verified_user</span>
|
|
70
|
+
<h2 style={{ fontSize: '20px', fontWeight: 700, color: '#064e3b', margin: 0 }}>Token Generated Successfully</h2>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div style={{ background: '#dcfce7', padding: '16px', borderRadius: '8px', marginBottom: '24px', border: '1px solid #10b98140' }}>
|
|
74
|
+
<p style={{ fontSize: '14px', color: '#166534', lineHeight: 1.6, margin: 0 }}>
|
|
75
|
+
<strong>⚠️ Security Alert:</strong> Copy this token now. It is never stored and will only be shown once.
|
|
76
|
+
It will expire in <strong>{expiresIn / 60} minutes</strong>.
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div style={{ display: 'flex', gap: '12px' }}>
|
|
81
|
+
<input
|
|
82
|
+
readOnly
|
|
83
|
+
value={token}
|
|
84
|
+
className="adminforge-input"
|
|
85
|
+
style={{ fontFamily: 'monospace', fontSize: '12px', flex: 1, height: '48px', background: 'white' }}
|
|
86
|
+
/>
|
|
87
|
+
<button
|
|
88
|
+
onClick={() => {
|
|
89
|
+
navigator.clipboard.writeText(token);
|
|
90
|
+
alert("Copied to clipboard!");
|
|
91
|
+
}}
|
|
92
|
+
className="adminforge-btn adminforge-btn-primary"
|
|
93
|
+
style={{ height: '48px', padding: '0 24px' }}
|
|
94
|
+
>
|
|
95
|
+
<span className="material-symbols-outlined">content_copy</span>
|
|
96
|
+
Copy
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<button
|
|
101
|
+
onClick={() => setToken(null)}
|
|
102
|
+
className="adminforge-btn-text"
|
|
103
|
+
style={{ marginTop: '24px', display: 'flex', alignItems: 'center', gap: '8px', color: '#059669', background: 'none', border: 'none', cursor: 'pointer', fontWeight: 600 }}
|
|
104
|
+
>
|
|
105
|
+
<span className="material-symbols-outlined" style={{ fontSize: '18px' }}>refresh</span>
|
|
106
|
+
Generate another token
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
109
|
+
) : (
|
|
110
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '32px' }}>
|
|
111
|
+
<div className="adminforge-table-wrapper">
|
|
112
|
+
<table className="adminforge-table">
|
|
113
|
+
<thead>
|
|
114
|
+
<tr>
|
|
115
|
+
<th>Collection</th>
|
|
116
|
+
<th style={{ textAlign: 'center' }}>Create</th>
|
|
117
|
+
<th style={{ textAlign: 'center' }}>Read</th>
|
|
118
|
+
<th style={{ textAlign: 'center' }}>Update</th>
|
|
119
|
+
<th style={{ textAlign: 'center' }}>Delete</th>
|
|
120
|
+
</tr>
|
|
121
|
+
</thead>
|
|
122
|
+
<tbody>
|
|
123
|
+
{config.collections.map(c => (
|
|
124
|
+
<tr key={c.name}>
|
|
125
|
+
<td style={{ fontWeight: 600 }}>
|
|
126
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
|
127
|
+
<span className="material-symbols-outlined" style={{ fontSize: '20px', color: '#94a3b8' }}>{c.icon || 'database'}</span>
|
|
128
|
+
{c.label}
|
|
129
|
+
</div>
|
|
130
|
+
</td>
|
|
131
|
+
{["create", "read", "update", "delete"].map(action => {
|
|
132
|
+
const scope = `${c.name}:${action}`;
|
|
133
|
+
return (
|
|
134
|
+
<td key={action} style={{ textAlign: 'center' }}>
|
|
135
|
+
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
|
136
|
+
<input
|
|
137
|
+
type="checkbox"
|
|
138
|
+
checked={selectedScopes.includes(scope)}
|
|
139
|
+
onChange={() => toggleScope(scope)}
|
|
140
|
+
style={{ width: '18px', height: '18px', cursor: 'pointer' }}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
</td>
|
|
144
|
+
);
|
|
145
|
+
})}
|
|
146
|
+
</tr>
|
|
147
|
+
))}
|
|
148
|
+
</tbody>
|
|
149
|
+
</table>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div style={{
|
|
153
|
+
display: 'flex',
|
|
154
|
+
justifyContent: 'space-between',
|
|
155
|
+
alignItems: 'center',
|
|
156
|
+
background: '#fff',
|
|
157
|
+
padding: '24px',
|
|
158
|
+
borderRadius: '12px',
|
|
159
|
+
border: '1px solid var(--color-border)',
|
|
160
|
+
boxShadow: 'var(--shadow-sm)'
|
|
161
|
+
}}>
|
|
162
|
+
<div>
|
|
163
|
+
<div style={{ marginBottom: '16px' }}>
|
|
164
|
+
<p style={{ fontWeight: 700, fontSize: '16px', color: 'var(--color-text)', marginBottom: '8px', margin: 0 }}>
|
|
165
|
+
{selectedScopes.length} scopes selected
|
|
166
|
+
</p>
|
|
167
|
+
<div style={{ display: 'flex', gap: '8px', marginTop: '12px' }}>
|
|
168
|
+
{[
|
|
169
|
+
{ label: '10m', val: 600 },
|
|
170
|
+
{ label: '30m', val: 1800 },
|
|
171
|
+
{ label: '1h', val: 3600 }
|
|
172
|
+
].map(opt => (
|
|
173
|
+
<button
|
|
174
|
+
key={opt.val}
|
|
175
|
+
onClick={() => setExpiresIn(opt.val)}
|
|
176
|
+
style={{
|
|
177
|
+
padding: '6px 12px',
|
|
178
|
+
fontSize: '12px',
|
|
179
|
+
fontWeight: 600,
|
|
180
|
+
borderRadius: '6px',
|
|
181
|
+
border: '1px solid',
|
|
182
|
+
background: expiresIn === opt.val ? 'var(--color-primary)' : '#fff',
|
|
183
|
+
color: expiresIn === opt.val ? '#fff' : 'var(--color-text)',
|
|
184
|
+
borderColor: expiresIn === opt.val ? 'var(--color-primary)' : 'var(--color-border)',
|
|
185
|
+
cursor: 'pointer',
|
|
186
|
+
transition: 'all 0.2s'
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
{opt.label}
|
|
190
|
+
</button>
|
|
191
|
+
))}
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
<p style={{ fontSize: '13px', color: '#64748b', margin: 0 }}>
|
|
195
|
+
Token will expire in {expiresIn / 60} minutes.
|
|
196
|
+
</p>
|
|
197
|
+
</div>
|
|
198
|
+
<button
|
|
199
|
+
onClick={handleGenerate}
|
|
200
|
+
disabled={loading || selectedScopes.length === 0}
|
|
201
|
+
className="adminforge-btn adminforge-btn-primary"
|
|
202
|
+
style={{ padding: '12px 32px', fontSize: '15px' }}
|
|
203
|
+
>
|
|
204
|
+
{loading ? "Generating..." : "Generate Agent Token"}
|
|
205
|
+
</button>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<div style={{ marginTop: '24px' }}>
|
|
209
|
+
<h3 style={{ fontSize: '11px', fontWeight: 700, color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '16px', margin: 0 }}>
|
|
210
|
+
Security Protocol
|
|
211
|
+
</h3>
|
|
212
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px', marginTop: '16px' }}>
|
|
213
|
+
<div style={{ background: '#fff', padding: '20px', borderRadius: '8px', border: '1px solid var(--color-border)' }}>
|
|
214
|
+
<p style={{ fontSize: '13px', fontWeight: 600, marginBottom: '8px', margin: 0 }}>Short-Lived Keys</p>
|
|
215
|
+
<p style={{ fontSize: '13px', color: '#64748b', lineHeight: 1.6, margin: 0 }}>
|
|
216
|
+
Tokens expire after 10 minutes. This reduces the risk of long-term credential leakage.
|
|
217
|
+
</p>
|
|
218
|
+
</div>
|
|
219
|
+
<div style={{ background: '#fff', padding: '20px', borderRadius: '8px', border: '1px solid var(--color-border)' }}>
|
|
220
|
+
<p style={{ fontSize: '13px', fontWeight: 600, marginBottom: '8px', margin: 0 }}>Scoped Authority</p>
|
|
221
|
+
<p style={{ fontSize: '13px', color: '#64748b', lineHeight: 1.6, margin: 0 }}>
|
|
222
|
+
Agents are strictly limited to the checkboxes above. They cannot bypass RBAC rules.
|
|
223
|
+
</p>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
</AdminLayout>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { AdminForgeConfig, CollectionDefinition } from "../../core";
|
|
4
|
+
import { AdminLayout } from "../components/AdminLayout.js";
|
|
5
|
+
import { FormEngine } from "../form-engine/FormEngine.js";
|
|
6
|
+
import Link from "next/link";
|
|
7
|
+
import { useAdminSession } from "../../auth/provider.js";
|
|
8
|
+
|
|
9
|
+
interface CollectionFormPageProps {
|
|
10
|
+
config: AdminForgeConfig;
|
|
11
|
+
collection: CollectionDefinition;
|
|
12
|
+
record?: Record<string, unknown> | null;
|
|
13
|
+
isNew: boolean;
|
|
14
|
+
role?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatDate(date: Date) {
|
|
18
|
+
if (isNaN(date.getTime())) return "Invalid date";
|
|
19
|
+
return date.toLocaleString('en-US', {
|
|
20
|
+
month: 'short',
|
|
21
|
+
day: 'numeric',
|
|
22
|
+
year: 'numeric',
|
|
23
|
+
hour: 'numeric',
|
|
24
|
+
minute: '2-digit',
|
|
25
|
+
hour12: true
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function CollectionFormPage({ config, collection, record, isNew, role: propRole }: CollectionFormPageProps) {
|
|
30
|
+
const session = useAdminSession();
|
|
31
|
+
const role = propRole || session?.role || session?.user?.role;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<AdminLayout config={config} currentPath={`/admin/${collection.name}`} role={role}>
|
|
35
|
+
<div className="adminforge-collection-page">
|
|
36
|
+
<div className="adminforge-page-header">
|
|
37
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
|
38
|
+
<h2 style={{ fontSize: '24px', fontWeight: 700, margin: 0 }}>{isNew ? `Create ${collection.label}` : `Edit ${collection.label}`}</h2>
|
|
39
|
+
{!isNew && (
|
|
40
|
+
<span className="adminforge-badge" style={{ background: '#f8fafc', color: '#64748b', borderColor: '#e2e8f0', fontSize: '11px' }}>
|
|
41
|
+
ID: {String(record?.id).substring(0, 8)}...
|
|
42
|
+
</span>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
<div style={{ display: 'flex', gap: '12px' }}>
|
|
46
|
+
{!isNew && (
|
|
47
|
+
<button
|
|
48
|
+
className="adminforge-btn adminforge-btn-danger"
|
|
49
|
+
onClick={async () => {
|
|
50
|
+
if (confirm("Are you sure you want to delete this item?")) {
|
|
51
|
+
const res = await fetch(`/api/${collection.name}/${record?.id}`, { method: 'DELETE' });
|
|
52
|
+
if (res.ok) {
|
|
53
|
+
window.location.href = `/admin/${collection.name}`;
|
|
54
|
+
} else {
|
|
55
|
+
const err = await res.json();
|
|
56
|
+
alert(err.error || "Failed to delete item");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<span className="material-symbols-outlined" style={{ fontSize: '20px', marginRight: '8px' }}>delete</span>
|
|
62
|
+
Delete
|
|
63
|
+
</button>
|
|
64
|
+
)}
|
|
65
|
+
<Link href={`/admin/${collection.name}`} className="adminforge-btn adminforge-btn-secondary">
|
|
66
|
+
<span className="material-symbols-outlined" style={{ fontSize: '20px', marginRight: '8px' }}>arrow_back</span>
|
|
67
|
+
Back
|
|
68
|
+
</Link>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
|
|
73
|
+
<FormEngine collection={collection} record={record} isNew={isNew} role={role} />
|
|
74
|
+
|
|
75
|
+
{!isNew && record && (
|
|
76
|
+
<div style={{
|
|
77
|
+
padding: '24px',
|
|
78
|
+
background: 'var(--color-surface)',
|
|
79
|
+
border: '1px solid var(--color-border)',
|
|
80
|
+
borderRadius: 'var(--radius-lg)',
|
|
81
|
+
boxShadow: 'var(--shadow-sm)'
|
|
82
|
+
}}>
|
|
83
|
+
<h3 style={{
|
|
84
|
+
fontSize: '14px',
|
|
85
|
+
fontWeight: 600,
|
|
86
|
+
color: '#64748b',
|
|
87
|
+
marginBottom: '20px',
|
|
88
|
+
display: 'flex',
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
gap: '10px',
|
|
91
|
+
textTransform: 'uppercase',
|
|
92
|
+
letterSpacing: '0.025em'
|
|
93
|
+
}}>
|
|
94
|
+
<span className="material-symbols-outlined" style={{ fontSize: '20px', color: 'var(--color-primary)' }}>info</span>
|
|
95
|
+
System Information
|
|
96
|
+
</h3>
|
|
97
|
+
|
|
98
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '32px' }}>
|
|
99
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
100
|
+
<div style={{ fontSize: '11px', fontWeight: 700, color: '#94a3b8', textTransform: 'uppercase' }}>Created</div>
|
|
101
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
102
|
+
<div style={{ width: '32px', height: '32px', borderRadius: '50%', background: '#f1f5f9', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#64748b' }}>
|
|
103
|
+
<span className="material-symbols-outlined" style={{ fontSize: '18px' }}>person</span>
|
|
104
|
+
</div>
|
|
105
|
+
<div>
|
|
106
|
+
<div style={{ fontSize: '14px', color: 'var(--color-text)', fontWeight: 500 }}>Admin</div>
|
|
107
|
+
<div style={{ fontSize: '12px', color: '#64748b' }}>
|
|
108
|
+
{record.createdAt ? formatDate(new Date(record.createdAt as string)) : 'Unknown date'}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
115
|
+
<div style={{ fontSize: '11px', fontWeight: 700, color: '#94a3b8', textTransform: 'uppercase' }}>Last Updated</div>
|
|
116
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
117
|
+
<div style={{ width: '32px', height: '32px', borderRadius: '50%', background: '#f0f9ff', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#0284c7' }}>
|
|
118
|
+
<span className="material-symbols-outlined" style={{ fontSize: '18px' }}>edit_note</span>
|
|
119
|
+
</div>
|
|
120
|
+
<div>
|
|
121
|
+
<div style={{ fontSize: '14px', color: 'var(--color-text)', fontWeight: 500 }}>Admin</div>
|
|
122
|
+
<div style={{ fontSize: '12px', color: '#64748b' }}>
|
|
123
|
+
{(record.updatedAt || record.createdAt) ? formatDate(new Date((record.updatedAt || record.createdAt) as string)) : 'Unknown date'}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</AdminLayout>
|
|
134
|
+
);
|
|
135
|
+
}
|