@agenticmail/enterprise 0.2.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/ARCHITECTURE.md +183 -0
- package/agenticmail-enterprise.db +0 -0
- package/dashboards/README.md +120 -0
- package/dashboards/dotnet/Program.cs +261 -0
- package/dashboards/express/app.js +146 -0
- package/dashboards/go/main.go +513 -0
- package/dashboards/html/index.html +535 -0
- package/dashboards/java/AgenticMailDashboard.java +376 -0
- package/dashboards/php/index.php +414 -0
- package/dashboards/python/app.py +273 -0
- package/dashboards/ruby/app.rb +195 -0
- package/dist/chunk-77IDQJL3.js +7 -0
- package/dist/chunk-7RGCCHIT.js +115 -0
- package/dist/chunk-DXNKR3TG.js +1355 -0
- package/dist/chunk-IQWA44WT.js +970 -0
- package/dist/chunk-LCUZGIDH.js +965 -0
- package/dist/chunk-N2JVTNNJ.js +2553 -0
- package/dist/chunk-O462UJBH.js +363 -0
- package/dist/chunk-PNKVD2UK.js +26 -0
- package/dist/cli.js +218 -0
- package/dist/dashboard/index.html +558 -0
- package/dist/db-adapter-DEWEFNIV.js +7 -0
- package/dist/dynamodb-CCGL2E77.js +426 -0
- package/dist/engine/index.js +1261 -0
- package/dist/index.js +522 -0
- package/dist/mongodb-ODTXIVPV.js +319 -0
- package/dist/mysql-RM3S2FV5.js +521 -0
- package/dist/postgres-LN7A6MGQ.js +518 -0
- package/dist/routes-2JEPIIKC.js +441 -0
- package/dist/routes-74ZLKJKP.js +399 -0
- package/dist/server.js +7 -0
- package/dist/sqlite-3K5YOZ4K.js +439 -0
- package/dist/turso-LDWODSDI.js +442 -0
- package/package.json +49 -0
- package/src/admin/routes.ts +331 -0
- package/src/auth/routes.ts +130 -0
- package/src/cli.ts +260 -0
- package/src/dashboard/index.html +558 -0
- package/src/db/adapter.ts +230 -0
- package/src/db/dynamodb.ts +456 -0
- package/src/db/factory.ts +51 -0
- package/src/db/mongodb.ts +360 -0
- package/src/db/mysql.ts +472 -0
- package/src/db/postgres.ts +479 -0
- package/src/db/sql-schema.ts +123 -0
- package/src/db/sqlite.ts +391 -0
- package/src/db/turso.ts +411 -0
- package/src/deploy/fly.ts +368 -0
- package/src/deploy/managed.ts +213 -0
- package/src/engine/activity.ts +474 -0
- package/src/engine/agent-config.ts +429 -0
- package/src/engine/agenticmail-bridge.ts +296 -0
- package/src/engine/approvals.ts +278 -0
- package/src/engine/db-adapter.ts +682 -0
- package/src/engine/db-schema.ts +335 -0
- package/src/engine/deployer.ts +595 -0
- package/src/engine/index.ts +134 -0
- package/src/engine/knowledge.ts +486 -0
- package/src/engine/lifecycle.ts +635 -0
- package/src/engine/openclaw-hook.ts +371 -0
- package/src/engine/routes.ts +528 -0
- package/src/engine/skills.ts +473 -0
- package/src/engine/tenant.ts +345 -0
- package/src/engine/tool-catalog.ts +189 -0
- package/src/index.ts +64 -0
- package/src/lib/resilience.ts +326 -0
- package/src/middleware/index.ts +286 -0
- package/src/server.ts +310 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin API Routes
|
|
3
|
+
*
|
|
4
|
+
* CRUD for agents, users, audit logs, rules, settings.
|
|
5
|
+
* All routes are protected by auth middleware (applied in server.ts).
|
|
6
|
+
* Input validation on all mutations. RBAC on sensitive operations.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Hono } from 'hono';
|
|
10
|
+
import type { DatabaseAdapter } from '../db/adapter.js';
|
|
11
|
+
import { validate, requireRole, ValidationError } from '../middleware/index.js';
|
|
12
|
+
|
|
13
|
+
export function createAdminRoutes(db: DatabaseAdapter) {
|
|
14
|
+
const api = new Hono();
|
|
15
|
+
|
|
16
|
+
// ─── Dashboard Stats ────────────────────────────────
|
|
17
|
+
|
|
18
|
+
api.get('/stats', async (c) => {
|
|
19
|
+
const stats = await db.getStats();
|
|
20
|
+
return c.json(stats);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// ─── Agents ─────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
api.get('/agents', async (c) => {
|
|
26
|
+
const status = c.req.query('status') as any;
|
|
27
|
+
const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);
|
|
28
|
+
const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);
|
|
29
|
+
const agents = await db.listAgents({ status, limit, offset });
|
|
30
|
+
const total = await db.countAgents(status);
|
|
31
|
+
return c.json({ agents, total, limit, offset });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
api.get('/agents/:id', async (c) => {
|
|
35
|
+
const agent = await db.getAgent(c.req.param('id'));
|
|
36
|
+
if (!agent) return c.json({ error: 'Agent not found' }, 404);
|
|
37
|
+
return c.json(agent);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
api.post('/agents', async (c) => {
|
|
41
|
+
const body = await c.req.json();
|
|
42
|
+
validate(body, [
|
|
43
|
+
{ field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },
|
|
44
|
+
{ field: 'email', type: 'email' },
|
|
45
|
+
{ field: 'role', type: 'string', maxLength: 32 },
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
// Check for duplicate name
|
|
49
|
+
const existing = await db.getAgentByName(body.name);
|
|
50
|
+
if (existing) {
|
|
51
|
+
return c.json({ error: 'Agent name already exists' }, 409);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const userId = c.get('userId' as any) || 'system';
|
|
55
|
+
const agent = await db.createAgent({ ...body, createdBy: userId });
|
|
56
|
+
return c.json(agent, 201);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
api.patch('/agents/:id', async (c) => {
|
|
60
|
+
const id = c.req.param('id');
|
|
61
|
+
const existing = await db.getAgent(id);
|
|
62
|
+
if (!existing) return c.json({ error: 'Agent not found' }, 404);
|
|
63
|
+
|
|
64
|
+
const body = await c.req.json();
|
|
65
|
+
validate(body, [
|
|
66
|
+
{ field: 'name', type: 'string', minLength: 1, maxLength: 64 },
|
|
67
|
+
{ field: 'email', type: 'email' },
|
|
68
|
+
{ field: 'role', type: 'string', maxLength: 32 },
|
|
69
|
+
{ field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
// If renaming, check for conflicts
|
|
73
|
+
if (body.name && body.name !== existing.name) {
|
|
74
|
+
const conflict = await db.getAgentByName(body.name);
|
|
75
|
+
if (conflict) return c.json({ error: 'Agent name already exists' }, 409);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const agent = await db.updateAgent(id, body);
|
|
79
|
+
return c.json(agent);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
api.post('/agents/:id/archive', async (c) => {
|
|
83
|
+
const existing = await db.getAgent(c.req.param('id'));
|
|
84
|
+
if (!existing) return c.json({ error: 'Agent not found' }, 404);
|
|
85
|
+
if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);
|
|
86
|
+
|
|
87
|
+
await db.archiveAgent(c.req.param('id'));
|
|
88
|
+
return c.json({ ok: true, status: 'archived' });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
api.post('/agents/:id/restore', async (c) => {
|
|
92
|
+
const existing = await db.getAgent(c.req.param('id'));
|
|
93
|
+
if (!existing) return c.json({ error: 'Agent not found' }, 404);
|
|
94
|
+
if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);
|
|
95
|
+
|
|
96
|
+
await db.updateAgent(c.req.param('id'), { status: 'active' } as any);
|
|
97
|
+
return c.json({ ok: true, status: 'active' });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Permanent delete — owner/admin only
|
|
101
|
+
api.delete('/agents/:id', requireRole('admin'), async (c) => {
|
|
102
|
+
const existing = await db.getAgent(c.req.param('id'));
|
|
103
|
+
if (!existing) return c.json({ error: 'Agent not found' }, 404);
|
|
104
|
+
|
|
105
|
+
await db.deleteAgent(c.req.param('id'));
|
|
106
|
+
return c.json({ ok: true });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ─── Users ──────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
api.get('/users', requireRole('admin'), async (c) => {
|
|
112
|
+
const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);
|
|
113
|
+
const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);
|
|
114
|
+
const users = await db.listUsers({ limit, offset });
|
|
115
|
+
// Strip password hashes
|
|
116
|
+
const safe = users.map(({ passwordHash, ...u }) => u);
|
|
117
|
+
return c.json({ users: safe, limit, offset });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
api.post('/users', requireRole('admin'), async (c) => {
|
|
121
|
+
const body = await c.req.json();
|
|
122
|
+
validate(body, [
|
|
123
|
+
{ field: 'email', type: 'email', required: true },
|
|
124
|
+
{ field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },
|
|
125
|
+
{ field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },
|
|
126
|
+
{ field: 'password', type: 'string', minLength: 8, maxLength: 128 },
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
// Check duplicate email
|
|
130
|
+
const existing = await db.getUserByEmail(body.email);
|
|
131
|
+
if (existing) return c.json({ error: 'Email already registered' }, 409);
|
|
132
|
+
|
|
133
|
+
const user = await db.createUser(body);
|
|
134
|
+
const { passwordHash, ...safe } = user;
|
|
135
|
+
return c.json(safe, 201);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
api.patch('/users/:id', requireRole('admin'), async (c) => {
|
|
139
|
+
const existing = await db.getUser(c.req.param('id'));
|
|
140
|
+
if (!existing) return c.json({ error: 'User not found' }, 404);
|
|
141
|
+
|
|
142
|
+
const body = await c.req.json();
|
|
143
|
+
validate(body, [
|
|
144
|
+
{ field: 'email', type: 'email' },
|
|
145
|
+
{ field: 'name', type: 'string', minLength: 1, maxLength: 128 },
|
|
146
|
+
{ field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
const user = await db.updateUser(c.req.param('id'), body);
|
|
150
|
+
const { passwordHash, ...safe } = user;
|
|
151
|
+
return c.json(safe);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
api.delete('/users/:id', requireRole('owner'), async (c) => {
|
|
155
|
+
const existing = await db.getUser(c.req.param('id'));
|
|
156
|
+
if (!existing) return c.json({ error: 'User not found' }, 404);
|
|
157
|
+
|
|
158
|
+
// Cannot delete yourself
|
|
159
|
+
const requesterId = c.get('userId' as any);
|
|
160
|
+
if (requesterId === c.req.param('id')) {
|
|
161
|
+
return c.json({ error: 'Cannot delete your own account' }, 400);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await db.deleteUser(c.req.param('id'));
|
|
165
|
+
return c.json({ ok: true });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ─── Audit Log ──────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
api.get('/audit', requireRole('admin'), async (c) => {
|
|
171
|
+
const filters = {
|
|
172
|
+
actor: c.req.query('actor') || undefined,
|
|
173
|
+
action: c.req.query('action') || undefined,
|
|
174
|
+
resource: c.req.query('resource') || undefined,
|
|
175
|
+
from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,
|
|
176
|
+
to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,
|
|
177
|
+
limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),
|
|
178
|
+
offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Validate date params
|
|
182
|
+
if (filters.from && isNaN(filters.from.getTime())) {
|
|
183
|
+
return c.json({ error: 'Invalid "from" date' }, 400);
|
|
184
|
+
}
|
|
185
|
+
if (filters.to && isNaN(filters.to.getTime())) {
|
|
186
|
+
return c.json({ error: 'Invalid "to" date' }, 400);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const result = await db.queryAudit(filters);
|
|
190
|
+
return c.json(result);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ─── API Keys ───────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
api.get('/api-keys', requireRole('admin'), async (c) => {
|
|
196
|
+
const keys = await db.listApiKeys();
|
|
197
|
+
// Never expose key hashes
|
|
198
|
+
const safe = keys.map(({ keyHash, ...k }) => k);
|
|
199
|
+
return c.json({ keys: safe });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
api.post('/api-keys', requireRole('admin'), async (c) => {
|
|
203
|
+
const body = await c.req.json();
|
|
204
|
+
validate(body, [
|
|
205
|
+
{ field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
const userId = c.get('userId' as any) || 'system';
|
|
209
|
+
const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];
|
|
210
|
+
const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;
|
|
211
|
+
|
|
212
|
+
const { key, plaintext } = await db.createApiKey({
|
|
213
|
+
name: body.name,
|
|
214
|
+
scopes,
|
|
215
|
+
createdBy: userId,
|
|
216
|
+
expiresAt,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Only time the plaintext key is returned — emphasize this
|
|
220
|
+
const { keyHash, ...safeKey } = key;
|
|
221
|
+
return c.json({
|
|
222
|
+
key: safeKey,
|
|
223
|
+
plaintext,
|
|
224
|
+
warning: 'Store this key securely. It will not be shown again.',
|
|
225
|
+
}, 201);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
api.delete('/api-keys/:id', requireRole('admin'), async (c) => {
|
|
229
|
+
const existing = await db.getApiKey(c.req.param('id'));
|
|
230
|
+
if (!existing) return c.json({ error: 'API key not found' }, 404);
|
|
231
|
+
|
|
232
|
+
await db.revokeApiKey(c.req.param('id'));
|
|
233
|
+
return c.json({ ok: true, revoked: true });
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// ─── Email Rules ────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
api.get('/rules', async (c) => {
|
|
239
|
+
const agentId = c.req.query('agentId') || undefined;
|
|
240
|
+
const rules = await db.getRules(agentId);
|
|
241
|
+
return c.json({ rules });
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
api.post('/rules', async (c) => {
|
|
245
|
+
const body = await c.req.json();
|
|
246
|
+
validate(body, [
|
|
247
|
+
{ field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
// Validate conditions/actions are objects
|
|
251
|
+
if (body.conditions && typeof body.conditions !== 'object') {
|
|
252
|
+
return c.json({ error: 'conditions must be an object' }, 400);
|
|
253
|
+
}
|
|
254
|
+
if (body.actions && typeof body.actions !== 'object') {
|
|
255
|
+
return c.json({ error: 'actions must be an object' }, 400);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const rule = await db.createRule({
|
|
259
|
+
name: body.name,
|
|
260
|
+
agentId: body.agentId,
|
|
261
|
+
conditions: body.conditions || {},
|
|
262
|
+
actions: body.actions || {},
|
|
263
|
+
priority: body.priority ?? 0,
|
|
264
|
+
enabled: body.enabled ?? true,
|
|
265
|
+
});
|
|
266
|
+
return c.json(rule, 201);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
api.patch('/rules/:id', async (c) => {
|
|
270
|
+
const body = await c.req.json();
|
|
271
|
+
const rule = await db.updateRule(c.req.param('id'), body);
|
|
272
|
+
return c.json(rule);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
api.delete('/rules/:id', async (c) => {
|
|
276
|
+
await db.deleteRule(c.req.param('id'));
|
|
277
|
+
return c.json({ ok: true });
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// ─── Settings ───────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
api.get('/settings', async (c) => {
|
|
283
|
+
const settings = await db.getSettings();
|
|
284
|
+
if (!settings) return c.json({ error: 'Not configured' }, 404);
|
|
285
|
+
|
|
286
|
+
// Redact sensitive fields
|
|
287
|
+
const safe = { ...settings };
|
|
288
|
+
if (safe.smtpPass) safe.smtpPass = '***';
|
|
289
|
+
if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';
|
|
290
|
+
return c.json(safe);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
api.patch('/settings', requireRole('admin'), async (c) => {
|
|
294
|
+
const body = await c.req.json();
|
|
295
|
+
validate(body, [
|
|
296
|
+
{ field: 'name', type: 'string', minLength: 1, maxLength: 128 },
|
|
297
|
+
{ field: 'domain', type: 'string', maxLength: 253 },
|
|
298
|
+
{ field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },
|
|
299
|
+
{ field: 'logoUrl', type: 'url' },
|
|
300
|
+
]);
|
|
301
|
+
|
|
302
|
+
const settings = await db.updateSettings(body);
|
|
303
|
+
return c.json(settings);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// ─── Retention ──────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
api.get('/retention', requireRole('admin'), async (c) => {
|
|
309
|
+
const policy = await db.getRetentionPolicy();
|
|
310
|
+
return c.json(policy);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
api.put('/retention', requireRole('owner'), async (c) => {
|
|
314
|
+
const body = await c.req.json();
|
|
315
|
+
validate(body, [
|
|
316
|
+
{ field: 'enabled', type: 'boolean', required: true },
|
|
317
|
+
{ field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },
|
|
318
|
+
{ field: 'archiveFirst', type: 'boolean' },
|
|
319
|
+
]);
|
|
320
|
+
|
|
321
|
+
await db.setRetentionPolicy({
|
|
322
|
+
enabled: body.enabled,
|
|
323
|
+
retainDays: body.retainDays,
|
|
324
|
+
excludeTags: body.excludeTags || [],
|
|
325
|
+
archiveFirst: body.archiveFirst ?? true,
|
|
326
|
+
});
|
|
327
|
+
return c.json({ ok: true });
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
return api;
|
|
331
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication Routes
|
|
3
|
+
*
|
|
4
|
+
* Handles login (email/password), JWT issuance, SAML, and OIDC callbacks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Hono } from 'hono';
|
|
8
|
+
import type { DatabaseAdapter } from '../db/adapter.js';
|
|
9
|
+
|
|
10
|
+
export function createAuthRoutes(db: DatabaseAdapter, jwtSecret: string) {
|
|
11
|
+
const auth = new Hono();
|
|
12
|
+
|
|
13
|
+
// ─── Email/Password Login ───────────────────────────────
|
|
14
|
+
|
|
15
|
+
auth.post('/login', async (c) => {
|
|
16
|
+
const { email, password } = await c.req.json();
|
|
17
|
+
if (!email || !password) {
|
|
18
|
+
return c.json({ error: 'Email and password required' }, 400);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const user = await db.getUserByEmail(email);
|
|
22
|
+
if (!user || !user.passwordHash) {
|
|
23
|
+
return c.json({ error: 'Invalid credentials' }, 401);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { default: bcrypt } = await import('bcryptjs');
|
|
27
|
+
const valid = await bcrypt.compare(password, user.passwordHash);
|
|
28
|
+
if (!valid) {
|
|
29
|
+
return c.json({ error: 'Invalid credentials' }, 401);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Issue JWT
|
|
33
|
+
const { SignJWT } = await import('jose');
|
|
34
|
+
const secret = new TextEncoder().encode(jwtSecret);
|
|
35
|
+
const token = await new SignJWT({ sub: user.id, email: user.email, role: user.role })
|
|
36
|
+
.setProtectedHeader({ alg: 'HS256' })
|
|
37
|
+
.setIssuedAt()
|
|
38
|
+
.setExpirationTime('24h')
|
|
39
|
+
.sign(secret);
|
|
40
|
+
|
|
41
|
+
// Update last login
|
|
42
|
+
await db.updateUser(user.id, { lastLoginAt: new Date() } as any);
|
|
43
|
+
await db.logEvent({
|
|
44
|
+
actor: user.id, actorType: 'user', action: 'auth.login',
|
|
45
|
+
resource: `user:${user.id}`, details: { method: 'password' },
|
|
46
|
+
ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip'),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return c.json({
|
|
50
|
+
token,
|
|
51
|
+
user: { id: user.id, email: user.email, name: user.name, role: user.role },
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ─── Token Refresh ──────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
auth.post('/refresh', async (c) => {
|
|
58
|
+
const authHeader = c.req.header('Authorization');
|
|
59
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
60
|
+
return c.json({ error: 'Token required' }, 401);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const { jwtVerify, SignJWT } = await import('jose');
|
|
65
|
+
const secret = new TextEncoder().encode(jwtSecret);
|
|
66
|
+
const { payload } = await jwtVerify(authHeader.slice(7), secret);
|
|
67
|
+
|
|
68
|
+
const user = await db.getUser(payload.sub as string);
|
|
69
|
+
if (!user) return c.json({ error: 'User not found' }, 401);
|
|
70
|
+
|
|
71
|
+
const token = await new SignJWT({ sub: user.id, email: user.email, role: user.role })
|
|
72
|
+
.setProtectedHeader({ alg: 'HS256' })
|
|
73
|
+
.setIssuedAt()
|
|
74
|
+
.setExpirationTime('24h')
|
|
75
|
+
.sign(secret);
|
|
76
|
+
|
|
77
|
+
return c.json({ token });
|
|
78
|
+
} catch {
|
|
79
|
+
return c.json({ error: 'Invalid token' }, 401);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ─── Current User ───────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
auth.get('/me', async (c) => {
|
|
86
|
+
const authHeader = c.req.header('Authorization');
|
|
87
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
88
|
+
return c.json({ error: 'Token required' }, 401);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const { jwtVerify } = await import('jose');
|
|
93
|
+
const secret = new TextEncoder().encode(jwtSecret);
|
|
94
|
+
const { payload } = await jwtVerify(authHeader.slice(7), secret);
|
|
95
|
+
const user = await db.getUser(payload.sub as string);
|
|
96
|
+
if (!user) return c.json({ error: 'User not found' }, 404);
|
|
97
|
+
const { passwordHash, ...safe } = user;
|
|
98
|
+
return c.json(safe);
|
|
99
|
+
} catch {
|
|
100
|
+
return c.json({ error: 'Invalid token' }, 401);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ─── SAML 2.0 (Placeholder) ────────────────────────────
|
|
105
|
+
|
|
106
|
+
auth.post('/saml/callback', async (c) => {
|
|
107
|
+
// TODO: Implement SAML assertion parsing
|
|
108
|
+
// Will use saml2-js or passport-saml
|
|
109
|
+
return c.json({ error: 'SAML not yet configured' }, 501);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
auth.get('/saml/metadata', async (c) => {
|
|
113
|
+
// TODO: Generate SP metadata XML
|
|
114
|
+
return c.json({ error: 'SAML not yet configured' }, 501);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ─── OIDC (Placeholder) ────────────────────────────────
|
|
118
|
+
|
|
119
|
+
auth.get('/oidc/authorize', async (c) => {
|
|
120
|
+
// TODO: Redirect to IdP authorization endpoint
|
|
121
|
+
return c.json({ error: 'OIDC not yet configured' }, 501);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
auth.get('/oidc/callback', async (c) => {
|
|
125
|
+
// TODO: Handle OIDC callback, exchange code for tokens
|
|
126
|
+
return c.json({ error: 'OIDC not yet configured' }, 501);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return auth;
|
|
130
|
+
}
|