@kyro-cms/admin 0.1.2
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/.astro/content.d.ts +154 -0
- package/.astro/settings.json +5 -0
- package/.astro/types.d.ts +2 -0
- package/astro.config.mjs +28 -0
- package/bun.lock +1374 -0
- package/dist/client/_astro/AdminLayout.DkDpng53.css +1 -0
- package/dist/client/_astro/AutoForm.3eJCmCJp.js +1 -0
- package/dist/client/_astro/client.DyczpTbx.js +9 -0
- package/dist/client/_astro/index.B02hbnpo.js +1 -0
- package/dist/client/fonts/Serotiva-Black.woff2 +0 -0
- package/dist/client/fonts/Serotiva-Bold.woff2 +0 -0
- package/dist/client/fonts/Serotiva-Medium.woff2 +0 -0
- package/dist/client/fonts/Serotiva-Regular.woff2 +0 -0
- package/dist/client/fonts/Serotiva-SemiBold.woff2 +0 -0
- package/dist/server/chunks/AdminLayout_D-_JeUqC.mjs +26 -0
- package/dist/server/chunks/_id__BzI_o0qT.mjs +50 -0
- package/dist/server/chunks/_id__Cd-jOuY3.mjs +238 -0
- package/dist/server/chunks/_id__DvbD--iR.mjs +992 -0
- package/dist/server/chunks/_id__vpVaEo16.mjs +128 -0
- package/dist/server/chunks/_virtual_astro_server-island-manifest_CQQ1F5PF.mjs +7 -0
- package/dist/server/chunks/_virtual_astro_session-driver_Bk3Q189E.mjs +4 -0
- package/dist/server/chunks/astro-component_Dbx3T2Nh.mjs +37 -0
- package/dist/server/chunks/audit-logs_DrnUMRvY.mjs +74 -0
- package/dist/server/chunks/config_CPXslElD.mjs +4221 -0
- package/dist/server/chunks/dataStore_Dl7cA2Qp.mjs +89 -0
- package/dist/server/chunks/index_CVqOkerS.mjs +2960 -0
- package/dist/server/chunks/index_CX8SQ4BF.mjs +55 -0
- package/dist/server/chunks/index_CYofDU51.mjs +58 -0
- package/dist/server/chunks/index_DdNRhuaM.mjs +55 -0
- package/dist/server/chunks/index_DupPvtIF.mjs +42 -0
- package/dist/server/chunks/index_YTS_M-B9.mjs +263 -0
- package/dist/server/chunks/index_YeCzuVps.mjs +53 -0
- package/dist/server/chunks/login_DLyqMRO8.mjs +93 -0
- package/dist/server/chunks/logout_CSbt5wea.mjs +50 -0
- package/dist/server/chunks/me_C04jlYhH.mjs +41 -0
- package/dist/server/chunks/new_BbQ9b55M.mjs +92 -0
- package/dist/server/chunks/node_9bvTewss.mjs +1014 -0
- package/dist/server/chunks/noop-entrypoint_BOlrdqWF.mjs +3 -0
- package/dist/server/chunks/sequence_9cl7AJy-.mjs +2503 -0
- package/dist/server/chunks/server_peBx9VXG.mjs +8117 -0
- package/dist/server/chunks/sharp_pmJ7nHES.mjs +142 -0
- package/dist/server/chunks/users_Dzddy_YR.mjs +137 -0
- package/dist/server/entry.mjs +5 -0
- package/dist/server/virtual_astro_middleware.mjs +48 -0
- package/package.json +33 -0
- package/public/fonts/Serotiva-Black.woff2 +0 -0
- package/public/fonts/Serotiva-Bold.woff2 +0 -0
- package/public/fonts/Serotiva-Medium.woff2 +0 -0
- package/public/fonts/Serotiva-Regular.woff2 +0 -0
- package/public/fonts/Serotiva-SemiBold.woff2 +0 -0
- package/src/collections/auth/index.ts +155 -0
- package/src/components/ActionBar.tsx +215 -0
- package/src/components/Admin.tsx +214 -0
- package/src/components/AutoForm.tsx +1123 -0
- package/src/components/BulkActionsBar.tsx +80 -0
- package/src/components/CreateView.tsx +99 -0
- package/src/components/DetailView.tsx +329 -0
- package/src/components/Icons.tsx +23 -0
- package/src/components/ListView.tsx +192 -0
- package/src/components/StatusBadge.tsx +76 -0
- package/src/components/ThemeProvider.tsx +155 -0
- package/src/components/VersionHistoryPanel.tsx +205 -0
- package/src/components/fields/CheckboxField.tsx +37 -0
- package/src/components/fields/DateField.tsx +42 -0
- package/src/components/fields/NumberField.tsx +44 -0
- package/src/components/fields/RelationshipField.tsx +87 -0
- package/src/components/fields/SelectField.tsx +56 -0
- package/src/components/fields/TextField.tsx +49 -0
- package/src/components/index.ts +30 -0
- package/src/components/layout/Breadcrumbs.tsx +36 -0
- package/src/components/layout/Header.tsx +37 -0
- package/src/components/layout/Layout.tsx +25 -0
- package/src/components/layout/Sidebar.tsx +462 -0
- package/src/components/ui/Badge.tsx +14 -0
- package/src/components/ui/Button.tsx +41 -0
- package/src/components/ui/Dropdown.tsx +82 -0
- package/src/components/ui/Modal.tsx +135 -0
- package/src/components/ui/SlidePanel.tsx +73 -0
- package/src/components/ui/Spinner.tsx +24 -0
- package/src/components/ui/Toast.tsx +78 -0
- package/src/layouts/AdminLayout.astro +197 -0
- package/src/lib/config.ts +68 -0
- package/src/lib/dataStore.ts +111 -0
- package/src/middleware.ts +48 -0
- package/src/pages/[collection]/[id].astro +176 -0
- package/src/pages/[collection]/index.astro +180 -0
- package/src/pages/api/[collection]/[id].ts +258 -0
- package/src/pages/api/[collection]/index.ts +289 -0
- package/src/pages/api/auth/[id].ts +142 -0
- package/src/pages/api/auth/audit-logs.ts +80 -0
- package/src/pages/api/auth/login.ts +101 -0
- package/src/pages/api/auth/logout.ts +48 -0
- package/src/pages/api/auth/me.ts +36 -0
- package/src/pages/api/auth/users.ts +150 -0
- package/src/pages/audit/index.astro +110 -0
- package/src/pages/index.astro +225 -0
- package/src/pages/roles/index.astro +114 -0
- package/src/pages/users/[id].astro +174 -0
- package/src/pages/users/index.astro +142 -0
- package/src/pages/users/new.astro +91 -0
- package/src/styles/main.css +1449 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
import AdminLayout from '../../layouts/AdminLayout.astro';
|
|
3
|
+
|
|
4
|
+
const { id } = Astro.params;
|
|
5
|
+
let user: any = null;
|
|
6
|
+
let error: string | null = null;
|
|
7
|
+
|
|
8
|
+
if (id) {
|
|
9
|
+
try {
|
|
10
|
+
const response = await fetch(`${Astro.url.origin}/api/users/${id}`);
|
|
11
|
+
if (response.ok) {
|
|
12
|
+
const data = await response.json();
|
|
13
|
+
user = data.data;
|
|
14
|
+
} else {
|
|
15
|
+
error = 'User not found';
|
|
16
|
+
}
|
|
17
|
+
} catch (e) {
|
|
18
|
+
error = 'Failed to fetch user';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const roleOptions = ['super_admin', 'admin', 'editor', 'author', 'customer', 'guest'];
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<AdminLayout title={user ? user.email : 'User'}>
|
|
26
|
+
<div class="flex-1 overflow-y-auto p-8 pr-12 space-y-8">
|
|
27
|
+
{error ? (
|
|
28
|
+
<div class="surface-tile p-8">
|
|
29
|
+
<div class="text-center">
|
|
30
|
+
<p class="text-lg font-bold text-red-600">{error}</p>
|
|
31
|
+
<a href="/users" class="mt-4 inline-block text-[#0b1222] font-bold underline">← Back to users</a>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
) : user ? (
|
|
35
|
+
<>
|
|
36
|
+
<!-- Header -->
|
|
37
|
+
<div class="surface-tile p-6 flex items-center justify-between">
|
|
38
|
+
<div class="flex items-center gap-4">
|
|
39
|
+
<div class="w-14 h-14 rounded-full bg-[#0b1222] text-white flex items-center justify-center font-bold text-xl">
|
|
40
|
+
{user.email.charAt(0).toUpperCase()}
|
|
41
|
+
</div>
|
|
42
|
+
<div>
|
|
43
|
+
<h1 class="text-2xl font-black tracking-tighter text-[#0b1222]">{user.email}</h1>
|
|
44
|
+
<p class="text-sm text-[#64748b] font-medium">User ID: {user.id}</p>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="flex gap-2">
|
|
48
|
+
<button id="btn-toggle-lock" class="px-4 py-2 border border-gray-200 rounded-xl text-sm font-bold text-[#64748b] hover:bg-gray-50 transition-colors">
|
|
49
|
+
{user.locked ? 'Unlock User' : 'Lock User'}
|
|
50
|
+
</button>
|
|
51
|
+
<button id="btn-delete-user" class="px-4 py-2 border border-red-200 rounded-xl text-sm font-bold text-red-600 hover:bg-red-50 transition-colors">
|
|
52
|
+
Delete
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<!-- User Details -->
|
|
58
|
+
<div class="surface-tile p-6">
|
|
59
|
+
<h2 class="text-lg font-black text-[#0b1222] tracking-tighter mb-6">Details</h2>
|
|
60
|
+
<div class="grid grid-cols-2 gap-6">
|
|
61
|
+
<div>
|
|
62
|
+
<label class="text-xs font-bold text-[#64748b] uppercase tracking-wider">Email</label>
|
|
63
|
+
<p class="mt-1 font-bold text-[#0b1222]" id="field-email">{user.email}</p>
|
|
64
|
+
</div>
|
|
65
|
+
<div>
|
|
66
|
+
<label class="text-xs font-bold text-[#64748b] uppercase tracking-wider">Role</label>
|
|
67
|
+
<select id="field-role" class="mt-1 w-full px-3 py-2 border border-gray-200 rounded-lg text-sm font-bold text-[#0b1222]">
|
|
68
|
+
{roleOptions.map((role) => (
|
|
69
|
+
<option value={role} selected={user.role === role}>{role}</option>
|
|
70
|
+
))}
|
|
71
|
+
</select>
|
|
72
|
+
</div>
|
|
73
|
+
<div>
|
|
74
|
+
<label class="text-xs font-bold text-[#64748b] uppercase tracking-wider">Email Verified</label>
|
|
75
|
+
<p class="mt-1">
|
|
76
|
+
<span class={`inline-flex items-center px-3 py-1 rounded-lg text-xs font-bold ${user.emailVerified ? 'bg-green-50 text-green-600' : 'bg-yellow-50 text-yellow-600'}`}>
|
|
77
|
+
{user.emailVerified ? 'Verified' : 'Not verified'}
|
|
78
|
+
</span>
|
|
79
|
+
</p>
|
|
80
|
+
</div>
|
|
81
|
+
<div>
|
|
82
|
+
<label class="text-xs font-bold text-[#64748b] uppercase tracking-wider">Status</label>
|
|
83
|
+
<p class="mt-1">
|
|
84
|
+
<span class={`inline-flex items-center px-3 py-1 rounded-lg text-xs font-bold ${user.locked ? 'bg-red-50 text-red-600' : 'bg-green-50 text-green-600'}`}>
|
|
85
|
+
{user.locked ? 'Locked' : 'Active'}
|
|
86
|
+
</span>
|
|
87
|
+
</p>
|
|
88
|
+
</div>
|
|
89
|
+
<div>
|
|
90
|
+
<label class="text-xs font-bold text-[#64748b] uppercase tracking-wider">Last Login</label>
|
|
91
|
+
<p class="mt-1 text-sm text-[#64748b]" id="field-last-login">
|
|
92
|
+
{user.lastLogin ? new Date(user.lastLogin).toLocaleString() : 'Never'}
|
|
93
|
+
</p>
|
|
94
|
+
</div>
|
|
95
|
+
<div>
|
|
96
|
+
<label class="text-xs font-bold text-[#64748b] uppercase tracking-wider">Failed Attempts</label>
|
|
97
|
+
<p class="mt-1 text-sm font-bold text-[#0b1222]" id="field-failed-attempts">
|
|
98
|
+
{user.failedLoginAttempts || 0}
|
|
99
|
+
</p>
|
|
100
|
+
</div>
|
|
101
|
+
<div>
|
|
102
|
+
<label class="text-xs font-bold text-[#64748b] uppercase tracking-wider">Created</label>
|
|
103
|
+
<p class="mt-1 text-sm text-[#64748b]">
|
|
104
|
+
{user.createdAt ? new Date(user.createdAt).toLocaleString() : '—'}
|
|
105
|
+
</p>
|
|
106
|
+
</div>
|
|
107
|
+
<div>
|
|
108
|
+
<label class="text-xs font-bold text-[#64748b] uppercase tracking-wider">Updated</label>
|
|
109
|
+
<p class="mt-1 text-sm text-[#64748b]">
|
|
110
|
+
{user.updatedAt ? new Date(user.updatedAt).toLocaleString() : '—'}
|
|
111
|
+
</p>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div class="mt-6 flex justify-end">
|
|
116
|
+
<button id="btn-save" class="px-6 py-2 bg-[#0b1222] text-white rounded-xl text-sm font-bold hover:bg-[#1a2332] transition-colors">
|
|
117
|
+
Save Changes
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</>
|
|
122
|
+
) : (
|
|
123
|
+
<div class="surface-tile p-8 text-center">
|
|
124
|
+
<p class="text-lg font-bold text-[#64748b]">Loading...</p>
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<script is:inline>
|
|
130
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
131
|
+
const userId = window.location.pathname.split('/').pop();
|
|
132
|
+
const saveBtn = document.getElementById('btn-save');
|
|
133
|
+
const lockBtn = document.getElementById('btn-toggle-lock');
|
|
134
|
+
const deleteBtn = document.getElementById('btn-delete-user');
|
|
135
|
+
const roleSelect = document.getElementById('field-role');
|
|
136
|
+
|
|
137
|
+
if (saveBtn && roleSelect) {
|
|
138
|
+
saveBtn.addEventListener('click', async () => {
|
|
139
|
+
const role = roleSelect.value;
|
|
140
|
+
const res = await fetch(`/api/users/${userId}`, {
|
|
141
|
+
method: 'PATCH',
|
|
142
|
+
headers: { 'Content-Type': 'application/json' },
|
|
143
|
+
body: JSON.stringify({ role }),
|
|
144
|
+
});
|
|
145
|
+
if (res.ok) {
|
|
146
|
+
saveBtn.textContent = 'Saved!';
|
|
147
|
+
setTimeout(() => { saveBtn.textContent = 'Save Changes'; }, 2000);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (lockBtn) {
|
|
153
|
+
lockBtn.addEventListener('click', async () => {
|
|
154
|
+
const isLocked = lockBtn.textContent?.includes('Unlock');
|
|
155
|
+
const res = await fetch(`/api/users/${userId}`, {
|
|
156
|
+
method: 'PATCH',
|
|
157
|
+
headers: { 'Content-Type': 'application/json' },
|
|
158
|
+
body: JSON.stringify({ locked: isLocked }),
|
|
159
|
+
});
|
|
160
|
+
if (res.ok) location.reload();
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (deleteBtn) {
|
|
165
|
+
deleteBtn.addEventListener('click', async () => {
|
|
166
|
+
if (confirm('Delete this user? This cannot be undone.')) {
|
|
167
|
+
const res = await fetch(`/api/users/${userId}`, { method: 'DELETE' });
|
|
168
|
+
if (res.ok) window.location.href = '/users';
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
</script>
|
|
174
|
+
</AdminLayout>
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
import AdminLayout from '../../layouts/AdminLayout.astro';
|
|
3
|
+
|
|
4
|
+
let users: any[] = [];
|
|
5
|
+
let totalUsers = 0;
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const response = await fetch(`${Astro.url.origin}/api/users?page=1&limit=100`);
|
|
9
|
+
if (response.ok) {
|
|
10
|
+
const data = await response.json();
|
|
11
|
+
users = data.docs || [];
|
|
12
|
+
totalUsers = data.totalDocs || 0;
|
|
13
|
+
}
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error('Failed to fetch users:', error);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const roleColors: Record<string, string> = {
|
|
19
|
+
super_admin: 'bg-red-50 text-red-600',
|
|
20
|
+
admin: 'bg-purple-50 text-purple-600',
|
|
21
|
+
editor: 'bg-blue-50 text-blue-600',
|
|
22
|
+
author: 'bg-green-50 text-green-600',
|
|
23
|
+
customer: 'bg-gray-50 text-gray-600',
|
|
24
|
+
guest: 'bg-yellow-50 text-yellow-600',
|
|
25
|
+
};
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<AdminLayout title="Users">
|
|
29
|
+
<div class="flex-1 overflow-y-auto p-8 pr-12 space-y-8">
|
|
30
|
+
<!-- Header -->
|
|
31
|
+
<div class="surface-tile p-6 flex items-center justify-between">
|
|
32
|
+
<div>
|
|
33
|
+
<h1 class="text-3xl font-black tracking-tighter text-[#0b1222]">Users</h1>
|
|
34
|
+
<p class="text-sm text-[#64748b] mt-1 font-medium">
|
|
35
|
+
Manage user accounts and permissions
|
|
36
|
+
<span class="ml-2 text-[#0b1222] font-bold">· {totalUsers} users</span>
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
<a href="/users/new" class="flex items-center gap-2 px-6 py-3 bg-[#0b1222] text-white rounded-xl font-bold transition-all hover:bg-[#1a2332] active:scale-95">
|
|
40
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
41
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 5v14M5 12h14"></path>
|
|
42
|
+
</svg>
|
|
43
|
+
Add User
|
|
44
|
+
</a>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<!-- Users Table -->
|
|
48
|
+
<div class="surface-tile overflow-hidden">
|
|
49
|
+
{users.length === 0 ? (
|
|
50
|
+
<div class="px-8 py-16 text-center">
|
|
51
|
+
<div class="flex flex-col items-center gap-4">
|
|
52
|
+
<div class="w-16 h-16 rounded-2xl bg-gray-50 flex items-center justify-center">
|
|
53
|
+
<svg class="w-8 h-8 text-[#9ca3af]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
54
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>
|
|
55
|
+
</svg>
|
|
56
|
+
</div>
|
|
57
|
+
<p class="font-bold text-[#0b1222] text-base">No users yet</p>
|
|
58
|
+
<p class="text-sm text-[#64748b]">Create your first user to get started.</p>
|
|
59
|
+
<a href="/users/new" class="mt-2 inline-flex items-center gap-2 px-5 py-2.5 bg-[#0b1222] text-white rounded-lg font-bold text-sm">
|
|
60
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
61
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 5v14M5 12h14"></path>
|
|
62
|
+
</svg>
|
|
63
|
+
Add User
|
|
64
|
+
</a>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
) : (
|
|
68
|
+
<table class="w-full text-left">
|
|
69
|
+
<thead>
|
|
70
|
+
<tr class="text-[#64748b] font-bold text-[10px] uppercase tracking-[0.3em] border-b border-gray-100">
|
|
71
|
+
<th class="px-8 py-6">Email</th>
|
|
72
|
+
<th class="px-6 py-6">Role</th>
|
|
73
|
+
<th class="px-6 py-6">Status</th>
|
|
74
|
+
<th class="px-6 py-6">Last Login</th>
|
|
75
|
+
<th class="px-6 py-6">Created</th>
|
|
76
|
+
<th class="px-6 py-6 text-right">Actions</th>
|
|
77
|
+
</tr>
|
|
78
|
+
</thead>
|
|
79
|
+
<tbody class="divide-y divide-gray-50">
|
|
80
|
+
{users.map((user) => (
|
|
81
|
+
<tr class="group hover:bg-gray-50/50 transition-colors cursor-pointer" onclick={`window.location='/users/${user.id}'`}>
|
|
82
|
+
<td class="px-8 py-5">
|
|
83
|
+
<div class="flex items-center gap-4">
|
|
84
|
+
<div class="w-10 h-10 rounded-full bg-[#0b1222] text-white flex items-center justify-center font-bold text-sm">
|
|
85
|
+
{user.email.charAt(0).toUpperCase()}
|
|
86
|
+
</div>
|
|
87
|
+
<div>
|
|
88
|
+
<div class="font-bold text-[#0b1222]">{user.email}</div>
|
|
89
|
+
{user.tenantId && <div class="text-xs text-[#64748b]">Tenant: {user.tenantId}</div>}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</td>
|
|
93
|
+
<td class="px-6 py-5">
|
|
94
|
+
<span class={`inline-flex items-center px-3 py-1 rounded-lg text-xs font-bold ${roleColors[user.role] || 'bg-gray-50 text-gray-600'}`}>
|
|
95
|
+
{user.role}
|
|
96
|
+
</span>
|
|
97
|
+
</td>
|
|
98
|
+
<td class="px-6 py-5">
|
|
99
|
+
{user.locked ? (
|
|
100
|
+
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg text-xs font-bold bg-red-50 text-red-600">
|
|
101
|
+
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
|
102
|
+
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/>
|
|
103
|
+
</svg>
|
|
104
|
+
Locked
|
|
105
|
+
</span>
|
|
106
|
+
) : (
|
|
107
|
+
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg text-xs font-bold bg-green-50 text-green-600">
|
|
108
|
+
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
|
109
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
|
110
|
+
</svg>
|
|
111
|
+
Active
|
|
112
|
+
</span>
|
|
113
|
+
)}
|
|
114
|
+
</td>
|
|
115
|
+
<td class="px-6 py-5 text-sm text-[#64748b]">
|
|
116
|
+
{user.lastLogin ? new Date(user.lastLogin).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : 'Never'}
|
|
117
|
+
</td>
|
|
118
|
+
<td class="px-6 py-5 text-sm text-[#64748b]">
|
|
119
|
+
{user.createdAt ? new Date(user.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'}
|
|
120
|
+
</td>
|
|
121
|
+
<td class="px-6 py-5 text-right">
|
|
122
|
+
<div class="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
123
|
+
<a href={`/users/${user.id}`} class="inline-flex items-center justify-center w-8 h-8 rounded-md text-[#64748b] hover:bg-gray-100 hover:text-[#0b1222] transition-colors" onclick="event.stopPropagation()">
|
|
124
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
125
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
|
126
|
+
</svg>
|
|
127
|
+
</a>
|
|
128
|
+
<button onclick={`event.stopPropagation(); if(confirm('Delete this user?')) { fetch('/api/users/${user.id}', { method: 'DELETE' }).then(() => location.reload()); }`} class="inline-flex items-center justify-center w-8 h-8 rounded-md text-[#64748b] hover:bg-red-50 hover:text-red-600 transition-colors">
|
|
129
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
130
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
|
131
|
+
</svg>
|
|
132
|
+
</button>
|
|
133
|
+
</div>
|
|
134
|
+
</td>
|
|
135
|
+
</tr>
|
|
136
|
+
))}
|
|
137
|
+
</tbody>
|
|
138
|
+
</table>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</AdminLayout>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
import AdminLayout from '../../layouts/AdminLayout.astro';
|
|
3
|
+
|
|
4
|
+
const roleOptions = ['super_admin', 'admin', 'editor', 'author', 'customer', 'guest'];
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<AdminLayout title="New User">
|
|
8
|
+
<div class="flex-1 overflow-y-auto p-8 pr-12 space-y-8">
|
|
9
|
+
<!-- Header -->
|
|
10
|
+
<div class="surface-tile p-6 flex items-center justify-between">
|
|
11
|
+
<div>
|
|
12
|
+
<h1 class="text-3xl font-black tracking-tighter text-[#0b1222]">Create User</h1>
|
|
13
|
+
<p class="text-sm text-[#64748b] mt-1 font-medium">Add a new user to the system</p>
|
|
14
|
+
</div>
|
|
15
|
+
<a href="/users" class="text-sm font-bold text-[#64748b] hover:text-[#0b1222] transition-colors">
|
|
16
|
+
← Back to users
|
|
17
|
+
</a>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<!-- Form -->
|
|
21
|
+
<div class="surface-tile p-6">
|
|
22
|
+
<form id="create-user-form" class="space-y-6 max-w-2xl">
|
|
23
|
+
<div>
|
|
24
|
+
<label for="email" class="block text-sm font-bold text-[#0b1222] mb-2">Email Address</label>
|
|
25
|
+
<input type="email" id="email" name="email" required class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[#0b1222]/20 focus:border-[#0b1222]" placeholder="user@example.com" />
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div>
|
|
29
|
+
<label for="password" class="block text-sm font-bold text-[#0b1222] mb-2">Password</label>
|
|
30
|
+
<input type="password" id="password" name="password" required minlength="12" class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[#0b1222]/20 focus:border-[#0b1222]" placeholder="Minimum 12 characters" />
|
|
31
|
+
<p class="text-xs text-[#64748b] mt-1">Must contain uppercase, lowercase, numbers, and special characters</p>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div>
|
|
35
|
+
<label for="role" class="block text-sm font-bold text-[#0b1222] mb-2">Role</label>
|
|
36
|
+
<select id="role" name="role" class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[#0b1222]/20 focus:border-[#0b1222]">
|
|
37
|
+
{roleOptions.map((role) => (
|
|
38
|
+
<option value={role}>{role}</option>
|
|
39
|
+
))}
|
|
40
|
+
</select>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div>
|
|
44
|
+
<label for="tenantId" class="block text-sm font-bold text-[#0b1222] mb-2">Tenant ID (optional)</label>
|
|
45
|
+
<input type="text" id="tenantId" name="tenantId" class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[#0b1222]/20 focus:border-[#0b1222]" placeholder="Leave empty for global user" />
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="flex items-center justify-end gap-3 pt-4 border-t border-gray-100">
|
|
49
|
+
<a href="/users" class="px-6 py-3 border border-gray-200 rounded-xl text-sm font-bold text-[#64748b] hover:bg-gray-50 transition-colors">
|
|
50
|
+
Cancel
|
|
51
|
+
</a>
|
|
52
|
+
<button type="submit" class="px-6 py-3 bg-[#0b1222] text-white rounded-xl text-sm font-bold hover:bg-[#1a2332] transition-colors">
|
|
53
|
+
Create User
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
</form>
|
|
57
|
+
|
|
58
|
+
<div id="form-message" class="hidden mt-4 p-4 rounded-xl text-sm font-bold"></div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<script is:inline>
|
|
63
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
64
|
+
const form = document.getElementById('create-user-form');
|
|
65
|
+
const message = document.getElementById('form-message');
|
|
66
|
+
|
|
67
|
+
form?.addEventListener('submit', async (e) => {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
const formData = new FormData(form);
|
|
70
|
+
const body = Object.fromEntries(formData.entries());
|
|
71
|
+
|
|
72
|
+
const res = await fetch('/api/users', {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: { 'Content-Type': 'application/json' },
|
|
75
|
+
body: JSON.stringify(body),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const data = await res.json();
|
|
79
|
+
|
|
80
|
+
if (res.ok) {
|
|
81
|
+
message.textContent = 'User created successfully!';
|
|
82
|
+
message.className = 'mt-4 p-4 rounded-xl text-sm font-bold bg-green-50 text-green-600';
|
|
83
|
+
setTimeout(() => { window.location.href = `/users/${data.data.id}`; }, 1000);
|
|
84
|
+
} else {
|
|
85
|
+
message.textContent = data.error || 'Failed to create user';
|
|
86
|
+
message.className = 'mt-4 p-4 rounded-xl text-sm font-bold bg-red-50 text-red-600';
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
</script>
|
|
91
|
+
</AdminLayout>
|