flarecms 0.1.0 → 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/LICENSE +21 -0
- package/dist/auth/index.js +201 -1
- package/dist/cli/commands.js +5554 -55
- package/dist/cli/index.js +5554 -55
- package/dist/cli/mcp.js +30 -0
- package/dist/client/index.js +23576 -0
- package/dist/db/index.js +10392 -25
- package/dist/index.js +56776 -7582
- package/dist/server/index.js +43280 -0
- package/dist/style.css +5536 -0
- package/package.json +33 -30
- package/scripts/fix-api-paths.mjs +0 -32
- package/scripts/fix-imports.mjs +0 -38
- package/scripts/prefix-css.mjs +0 -45
- package/src/api/lib/cache.ts +0 -45
- package/src/api/lib/response.ts +0 -40
- package/src/api/middlewares/auth.ts +0 -186
- package/src/api/middlewares/cors.ts +0 -10
- package/src/api/middlewares/rbac.ts +0 -85
- package/src/api/routes/auth.ts +0 -377
- package/src/api/routes/collections.ts +0 -205
- package/src/api/routes/content.ts +0 -175
- package/src/api/routes/device.ts +0 -160
- package/src/api/routes/magic.ts +0 -150
- package/src/api/routes/mcp.ts +0 -273
- package/src/api/routes/oauth.ts +0 -160
- package/src/api/routes/settings.ts +0 -43
- package/src/api/routes/setup.ts +0 -307
- package/src/api/routes/tokens.ts +0 -80
- package/src/api/schemas/auth.ts +0 -15
- package/src/api/schemas/index.ts +0 -51
- package/src/api/schemas/tokens.ts +0 -24
- package/src/auth/index.ts +0 -28
- package/src/cli/commands.ts +0 -217
- package/src/cli/index.ts +0 -21
- package/src/cli/mcp.ts +0 -210
- package/src/cli/tests/cli.test.ts +0 -40
- package/src/cli/tests/create.test.ts +0 -87
- package/src/client/FlareAdminRouter.tsx +0 -47
- package/src/client/app.tsx +0 -175
- package/src/client/components/app-sidebar.tsx +0 -227
- package/src/client/components/collection-modal.tsx +0 -215
- package/src/client/components/content-list.tsx +0 -247
- package/src/client/components/dynamic-form.tsx +0 -190
- package/src/client/components/field-modal.tsx +0 -221
- package/src/client/components/settings/api-token-section.tsx +0 -400
- package/src/client/components/settings/general-section.tsx +0 -224
- package/src/client/components/settings/security-section.tsx +0 -154
- package/src/client/components/settings/seo-section.tsx +0 -200
- package/src/client/components/settings/signup-section.tsx +0 -257
- package/src/client/components/ui/accordion.tsx +0 -78
- package/src/client/components/ui/avatar.tsx +0 -107
- package/src/client/components/ui/badge.tsx +0 -52
- package/src/client/components/ui/button.tsx +0 -60
- package/src/client/components/ui/card.tsx +0 -103
- package/src/client/components/ui/checkbox.tsx +0 -27
- package/src/client/components/ui/collapsible.tsx +0 -19
- package/src/client/components/ui/dialog.tsx +0 -162
- package/src/client/components/ui/icon-picker.tsx +0 -485
- package/src/client/components/ui/icons-data.ts +0 -8476
- package/src/client/components/ui/input.tsx +0 -20
- package/src/client/components/ui/label.tsx +0 -20
- package/src/client/components/ui/popover.tsx +0 -91
- package/src/client/components/ui/select.tsx +0 -204
- package/src/client/components/ui/separator.tsx +0 -23
- package/src/client/components/ui/sheet.tsx +0 -141
- package/src/client/components/ui/sidebar.tsx +0 -722
- package/src/client/components/ui/skeleton.tsx +0 -13
- package/src/client/components/ui/sonner.tsx +0 -47
- package/src/client/components/ui/switch.tsx +0 -30
- package/src/client/components/ui/table.tsx +0 -116
- package/src/client/components/ui/tabs.tsx +0 -80
- package/src/client/components/ui/textarea.tsx +0 -18
- package/src/client/components/ui/tooltip.tsx +0 -68
- package/src/client/hooks/use-mobile.ts +0 -19
- package/src/client/index.css +0 -149
- package/src/client/index.ts +0 -7
- package/src/client/layouts/admin-layout.tsx +0 -93
- package/src/client/layouts/settings-layout.tsx +0 -104
- package/src/client/lib/api.ts +0 -72
- package/src/client/lib/utils.ts +0 -6
- package/src/client/main.tsx +0 -10
- package/src/client/pages/collection-detail.tsx +0 -634
- package/src/client/pages/collections.tsx +0 -180
- package/src/client/pages/dashboard.tsx +0 -133
- package/src/client/pages/device.tsx +0 -66
- package/src/client/pages/document-detail-page.tsx +0 -139
- package/src/client/pages/documents-page.tsx +0 -103
- package/src/client/pages/login.tsx +0 -345
- package/src/client/pages/settings.tsx +0 -65
- package/src/client/pages/setup.tsx +0 -129
- package/src/client/pages/signup.tsx +0 -188
- package/src/client/store/auth.ts +0 -30
- package/src/client/store/collections.ts +0 -13
- package/src/client/store/config.ts +0 -12
- package/src/client/store/fetcher.ts +0 -30
- package/src/client/store/router.ts +0 -95
- package/src/client/store/schema.ts +0 -39
- package/src/client/store/settings.ts +0 -31
- package/src/client/types.ts +0 -34
- package/src/db/dynamic.ts +0 -70
- package/src/db/index.ts +0 -16
- package/src/db/migrations/001_initial_schema.ts +0 -57
- package/src/db/migrations/002_auth_tables.ts +0 -84
- package/src/db/migrator.ts +0 -61
- package/src/db/schema.ts +0 -142
- package/src/index.ts +0 -12
- package/src/server/index.ts +0 -66
- package/src/types.ts +0 -20
- package/tests/css.test.ts +0 -21
- package/tests/modular.test.ts +0 -29
- package/tsconfig.json +0 -10
- /package/{style.css.d.ts → dist/style.css.d.ts} +0 -0
package/src/api/routes/mcp.ts
DELETED
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
import { Hono } from 'hono';
|
|
2
|
-
|
|
3
|
-
import { setupMiddleware, authMiddleware } from '../middlewares/auth';
|
|
4
|
-
import { createDb, ensureUniqueSlug, createCollectionTable, addFieldToTable } from '../../db';
|
|
5
|
-
import { ulid } from 'ulidx';
|
|
6
|
-
import { dynamicContentSchema, collectionSchema, fieldSchema } from '../schemas';
|
|
7
|
-
import { sql } from 'kysely';
|
|
8
|
-
import { cache } from '../lib/cache';
|
|
9
|
-
import type { Bindings, Variables } from 'src/types';
|
|
10
|
-
|
|
11
|
-
export const mcpRoutes = new Hono<{ Bindings: Bindings; Variables: Variables }>();
|
|
12
|
-
|
|
13
|
-
mcpRoutes.use('*', setupMiddleware);
|
|
14
|
-
mcpRoutes.use('*', authMiddleware);
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* In Cloudflare Workers, long-polling / SSE streams combined with separate POST endpoints can fail
|
|
18
|
-
* because a POST might route to a different worker isolate than the GET SSE connection, dropping the message.
|
|
19
|
-
*
|
|
20
|
-
* FlareCMS implements a "Stateless RPC Endpoint" that allows specialized proxy agents to
|
|
21
|
-
* execute standard MCP-like tool calls synchronously. This works 100% within the Edge environment.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
mcpRoutes.post("/execute", async (c) => {
|
|
25
|
-
try {
|
|
26
|
-
const db = createDb(c.env.DB);
|
|
27
|
-
const body = await c.req.json();
|
|
28
|
-
const { tool, arguments: args } = body;
|
|
29
|
-
|
|
30
|
-
if (!tool) {
|
|
31
|
-
return c.json({ error: "No tool specified" }, 400);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (tool === "list_collections") {
|
|
35
|
-
const collections = await db.selectFrom('fc_collections').selectAll().execute();
|
|
36
|
-
return c.json({
|
|
37
|
-
content: [{ type: "text", text: JSON.stringify(collections, null, 2) }]
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (tool === "read_content") {
|
|
42
|
-
const collectionSlug = args?.collection as string;
|
|
43
|
-
const limit = (args?.limit as number) || 10;
|
|
44
|
-
|
|
45
|
-
if (!collectionSlug) return c.json({ error: "Missing 'collection' argument" }, 400);
|
|
46
|
-
|
|
47
|
-
const collectionRecord = await db.selectFrom('fc_collections')
|
|
48
|
-
.select('id')
|
|
49
|
-
.where('slug', '=', collectionSlug)
|
|
50
|
-
.executeTakeFirst();
|
|
51
|
-
|
|
52
|
-
if (!collectionRecord) {
|
|
53
|
-
return c.json({ content: [{ type: "text", text: `Error: Collection '${collectionSlug}' not found.` }] });
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const tableName = `ec_${collectionSlug}`;
|
|
57
|
-
|
|
58
|
-
const content = await db.selectFrom(tableName as any)
|
|
59
|
-
.selectAll()
|
|
60
|
-
.where('status', '!=', 'deleted')
|
|
61
|
-
.limit(limit)
|
|
62
|
-
.execute().catch(() => []); // Graceful fail if table doesn't exist yet
|
|
63
|
-
|
|
64
|
-
return c.json({ content: [{ type: "text", text: JSON.stringify(content, null, 2) }] });
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (tool === "get_collection_schema") {
|
|
68
|
-
const collectionSlug = args?.collection as string;
|
|
69
|
-
if (!collectionSlug) return c.json({ error: "Missing 'collection' argument" }, 400);
|
|
70
|
-
|
|
71
|
-
const collection = await db.selectFrom('fc_collections')
|
|
72
|
-
.selectAll()
|
|
73
|
-
.where('slug', '=', collectionSlug)
|
|
74
|
-
.executeTakeFirst();
|
|
75
|
-
|
|
76
|
-
if (!collection) {
|
|
77
|
-
return c.json({ content: [{ type: "text", text: `Error: Collection '${collectionSlug}' not found.` }] });
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const fields = await db.selectFrom('fc_fields')
|
|
81
|
-
.selectAll()
|
|
82
|
-
.where('collection_id', '=', collection.id)
|
|
83
|
-
.execute();
|
|
84
|
-
|
|
85
|
-
const schema = {
|
|
86
|
-
metadata: {
|
|
87
|
-
...collection,
|
|
88
|
-
features: collection.features ? JSON.parse(collection.features) : []
|
|
89
|
-
},
|
|
90
|
-
fields
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
return c.json({
|
|
94
|
-
content: [{ type: "text", text: JSON.stringify(schema, null, 2) }]
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (tool === "create_document") {
|
|
99
|
-
const collectionName = args?.collection as string;
|
|
100
|
-
const data = args?.data;
|
|
101
|
-
|
|
102
|
-
if (!collectionName || !data) {
|
|
103
|
-
return c.json({ error: "Missing 'collection' or 'data' argument" }, 400);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const collection = await db.selectFrom('fc_collections')
|
|
107
|
-
.select('id')
|
|
108
|
-
.where('slug', '=', collectionName)
|
|
109
|
-
.executeTakeFirst();
|
|
110
|
-
|
|
111
|
-
if (!collection) return c.json({ error: `Collection '${collectionName}' not found` }, 404);
|
|
112
|
-
|
|
113
|
-
const parsed = dynamicContentSchema.safeParse(data);
|
|
114
|
-
if (!parsed.success) return c.json({ error: parsed.error.format() }, 400);
|
|
115
|
-
|
|
116
|
-
const id = ulid();
|
|
117
|
-
const docData = parsed.data;
|
|
118
|
-
|
|
119
|
-
const baseSlug = docData.slug || docData.title?.toLowerCase().replace(/[^a-z0-9]+/g, '-') || id;
|
|
120
|
-
const slug = await ensureUniqueSlug(db, collectionName, baseSlug);
|
|
121
|
-
const status = docData.status || 'draft';
|
|
122
|
-
|
|
123
|
-
const doc = {
|
|
124
|
-
...docData,
|
|
125
|
-
id,
|
|
126
|
-
slug,
|
|
127
|
-
status,
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
await db.insertInto(`ec_${collectionName}` as any)
|
|
131
|
-
.values(doc)
|
|
132
|
-
.execute();
|
|
133
|
-
|
|
134
|
-
return c.json({
|
|
135
|
-
content: [{ type: "text", text: `Success: Document created with ID ${id} and slug ${slug}` }]
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (tool === "update_document") {
|
|
140
|
-
const collectionName = args?.collection as string;
|
|
141
|
-
const id = args?.id as string;
|
|
142
|
-
const data = args?.data;
|
|
143
|
-
|
|
144
|
-
if (!collectionName || !id || !data) {
|
|
145
|
-
return c.json({ error: "Missing 'collection', 'id', or 'data' argument" }, 400);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const parsed = dynamicContentSchema.safeParse(data);
|
|
149
|
-
if (!parsed.success) return c.json({ error: parsed.error.format() }, 400);
|
|
150
|
-
|
|
151
|
-
// Handle slug change uniqueness
|
|
152
|
-
let finalData = { ...parsed.data };
|
|
153
|
-
if (finalData.slug) {
|
|
154
|
-
finalData.slug = await ensureUniqueSlug(db, collectionName, finalData.slug, id);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
await db.updateTable(`ec_${collectionName}` as any)
|
|
158
|
-
.set({
|
|
159
|
-
...finalData,
|
|
160
|
-
updated_at: sql`CURRENT_TIMESTAMP`
|
|
161
|
-
})
|
|
162
|
-
.where('id', '=', id)
|
|
163
|
-
.execute();
|
|
164
|
-
|
|
165
|
-
return c.json({
|
|
166
|
-
content: [{ type: "text", text: `Success: Document ${id} updated.` }]
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (tool === "create_collection") {
|
|
171
|
-
const data = args;
|
|
172
|
-
const parsed = collectionSchema.safeParse(data);
|
|
173
|
-
if (!parsed.success) return c.json({ error: parsed.error.format() }, 400);
|
|
174
|
-
|
|
175
|
-
const id = ulid();
|
|
176
|
-
const slug = parsed.data.slug;
|
|
177
|
-
|
|
178
|
-
await db.insertInto('fc_collections')
|
|
179
|
-
.values({
|
|
180
|
-
id,
|
|
181
|
-
slug,
|
|
182
|
-
label: parsed.data.label,
|
|
183
|
-
label_singular: parsed.data.labelSingular || null,
|
|
184
|
-
description: parsed.data.description || null,
|
|
185
|
-
icon: parsed.data.icon || null,
|
|
186
|
-
is_public: parsed.data.isPublic ? 1 : 0,
|
|
187
|
-
})
|
|
188
|
-
.execute();
|
|
189
|
-
|
|
190
|
-
await createCollectionTable(db, slug);
|
|
191
|
-
|
|
192
|
-
// Sync cache
|
|
193
|
-
await cache.invalidateSchema(c.env.KV, slug);
|
|
194
|
-
await cache.invalidateCollectionList(c.env.KV);
|
|
195
|
-
|
|
196
|
-
return c.json({
|
|
197
|
-
content: [{ type: "text", text: `Success: Collection '${slug}' created with ID ${id}` }]
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (tool === "update_collection") {
|
|
202
|
-
const id = args?.id as string;
|
|
203
|
-
const data = args?.data;
|
|
204
|
-
|
|
205
|
-
if (!id || !data) return c.json({ error: "Missing 'id' or 'data' argument" }, 400);
|
|
206
|
-
|
|
207
|
-
await db.updateTable('fc_collections')
|
|
208
|
-
.set({
|
|
209
|
-
...data,
|
|
210
|
-
updated_at: sql`CURRENT_TIMESTAMP`
|
|
211
|
-
})
|
|
212
|
-
.where('id', '=', id)
|
|
213
|
-
.execute();
|
|
214
|
-
|
|
215
|
-
// Sync cache
|
|
216
|
-
const updatedCol = await db.selectFrom('fc_collections')
|
|
217
|
-
.select('slug')
|
|
218
|
-
.where('id', '=', id)
|
|
219
|
-
.executeTakeFirst();
|
|
220
|
-
|
|
221
|
-
if (updatedCol) {
|
|
222
|
-
await cache.invalidateSchema(c.env.KV, updatedCol.slug);
|
|
223
|
-
await cache.invalidateCollectionList(c.env.KV);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return c.json({
|
|
227
|
-
content: [{ type: "text", text: `Success: Collection ${id} updated.` }]
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (tool === "add_field") {
|
|
232
|
-
const collectionId = args?.collection_id as string;
|
|
233
|
-
const data = args?.field;
|
|
234
|
-
|
|
235
|
-
if (!collectionId || !data) return c.json({ error: "Missing 'collection_id' or 'field' argument" }, 400);
|
|
236
|
-
|
|
237
|
-
const parsed = fieldSchema.safeParse(data);
|
|
238
|
-
if (!parsed.success) return c.json({ error: parsed.error.format() }, 400);
|
|
239
|
-
|
|
240
|
-
const collection = await db.selectFrom('fc_collections')
|
|
241
|
-
.select('slug')
|
|
242
|
-
.where('id', '=', collectionId)
|
|
243
|
-
.executeTakeFirst();
|
|
244
|
-
|
|
245
|
-
if (!collection) return c.json({ error: 'Collection not found' }, 404);
|
|
246
|
-
|
|
247
|
-
const fieldId = ulid();
|
|
248
|
-
await db.insertInto('fc_fields')
|
|
249
|
-
.values({
|
|
250
|
-
id: fieldId,
|
|
251
|
-
collection_id: collectionId,
|
|
252
|
-
slug: parsed.data.slug,
|
|
253
|
-
label: parsed.data.label,
|
|
254
|
-
type: parsed.data.type,
|
|
255
|
-
required: parsed.data.required ? 1 : 0,
|
|
256
|
-
})
|
|
257
|
-
.execute();
|
|
258
|
-
|
|
259
|
-
await addFieldToTable(db, collection.slug, parsed.data.slug, parsed.data.type);
|
|
260
|
-
|
|
261
|
-
// Sync cache
|
|
262
|
-
await cache.invalidateSchema(c.env.KV, collection.slug);
|
|
263
|
-
|
|
264
|
-
return c.json({
|
|
265
|
-
content: [{ type: "text", text: `Success: Field '${parsed.data.slug}' added to collection '${collection.slug}'` }]
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return c.json({ error: "Tool not found" }, 404);
|
|
270
|
-
} catch (error: any) {
|
|
271
|
-
return c.json({ error: `Server Error: ${error.message}` }, 500);
|
|
272
|
-
}
|
|
273
|
-
});
|
package/src/api/routes/oauth.ts
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { Hono } from 'hono';
|
|
2
|
-
import { createDb } from '../../db';
|
|
3
|
-
import { generateSessionToken } from '../../auth';
|
|
4
|
-
import { setCookie } from 'hono/cookie';
|
|
5
|
-
import { oauthCallbackSchema } from '../schemas/auth';
|
|
6
|
-
import { ulid } from 'ulidx';
|
|
7
|
-
import type { Bindings, Variables } from '../index';
|
|
8
|
-
import { apiResponse } from '../lib/response';
|
|
9
|
-
|
|
10
|
-
export const oauthRoutes = new Hono<{ Bindings: Bindings; Variables: Variables }>();
|
|
11
|
-
|
|
12
|
-
// OAuth Login (Initiate Redirect)
|
|
13
|
-
oauthRoutes.get('/github/login', (c) => {
|
|
14
|
-
const clientId = c.env.GITHUB_CLIENT_ID;
|
|
15
|
-
if (!clientId) return apiResponse.error(c, 'GitHub OAuth not configured', 500);
|
|
16
|
-
|
|
17
|
-
const redirectUri = encodeURIComponent(`https://${new URL(c.req.url).hostname}/api/oauth/github/callback`);
|
|
18
|
-
const scope = encodeURIComponent('read:user user:email');
|
|
19
|
-
|
|
20
|
-
// Create secure random state parameter here to store in cookies usually
|
|
21
|
-
const state = Math.random().toString(36).substring(2);
|
|
22
|
-
|
|
23
|
-
return c.redirect(`https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&state=${state}`);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
// OAuth Callback
|
|
27
|
-
oauthRoutes.get('/github/callback', async (c) => {
|
|
28
|
-
const code = c.req.query('code');
|
|
29
|
-
if (!code) return apiResponse.error(c, 'Missing code');
|
|
30
|
-
|
|
31
|
-
const clientId = c.env.GITHUB_CLIENT_ID;
|
|
32
|
-
const clientSecret = c.env.GITHUB_CLIENT_SECRET;
|
|
33
|
-
|
|
34
|
-
if (!clientId || !clientSecret) return apiResponse.error(c, 'GitHub OAuth not configured', 500);
|
|
35
|
-
|
|
36
|
-
const db = createDb(c.env.DB);
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
// 1. Exchange Code for Access Token
|
|
40
|
-
const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
|
|
41
|
-
method: 'POST',
|
|
42
|
-
headers: {
|
|
43
|
-
'Accept': 'application/json',
|
|
44
|
-
'Content-Type': 'application/json'
|
|
45
|
-
},
|
|
46
|
-
body: JSON.stringify({
|
|
47
|
-
client_id: clientId,
|
|
48
|
-
client_secret: clientSecret,
|
|
49
|
-
code
|
|
50
|
-
})
|
|
51
|
-
});
|
|
52
|
-
const tokenData: any = await tokenRes.json();
|
|
53
|
-
if (tokenData.error) throw new Error(tokenData.error_description || tokenData.error);
|
|
54
|
-
|
|
55
|
-
// 2. Fetch User Profile
|
|
56
|
-
const userRes = await fetch('https://api.github.com/user', {
|
|
57
|
-
headers: {
|
|
58
|
-
'Authorization': `Bearer ${tokenData.access_token}`,
|
|
59
|
-
'User-Agent': 'FlareCMS'
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
const userData: any = await userRes.json();
|
|
63
|
-
|
|
64
|
-
// 3. Fetch User Emails (GitHub separates primary email)
|
|
65
|
-
const emailRes = await fetch('https://api.github.com/user/emails', {
|
|
66
|
-
headers: {
|
|
67
|
-
'Authorization': `Bearer ${tokenData.access_token}`,
|
|
68
|
-
'User-Agent': 'FlareCMS'
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
const emailData: any[] = await emailRes.json();
|
|
72
|
-
const primaryEmail = emailData.find((e: any) => e.primary)?.email || userData.email;
|
|
73
|
-
|
|
74
|
-
if (!primaryEmail) throw new Error('No public email found in GitHub account');
|
|
75
|
-
|
|
76
|
-
// 4. Link or Provision User Account
|
|
77
|
-
const githubId = String(userData.id);
|
|
78
|
-
let user = await db.selectFrom('fc_oauth_accounts')
|
|
79
|
-
.innerJoin('fc_users', 'fc_oauth_accounts.user_id', 'fc_users.id')
|
|
80
|
-
.select(['fc_users.id', 'fc_users.disabled'])
|
|
81
|
-
.where('fc_oauth_accounts.provider_id', '=', 'github')
|
|
82
|
-
.where('fc_oauth_accounts.provider_user_id', '=', githubId)
|
|
83
|
-
.executeTakeFirst();
|
|
84
|
-
|
|
85
|
-
if (!user) {
|
|
86
|
-
// Look for existing user by email
|
|
87
|
-
let localUser = await db.selectFrom('fc_users').selectAll().where('email', '=', primaryEmail).executeTakeFirst();
|
|
88
|
-
|
|
89
|
-
if (!localUser) {
|
|
90
|
-
// 1. Check Registration Policy
|
|
91
|
-
const signupEnabled = await db.selectFrom('options').select('value').where('name', '=', 'flare:signup_enabled').executeTakeFirst();
|
|
92
|
-
const defaultRole = await db.selectFrom('options').select('value').where('name', '=', 'flare:signup_default_role').executeTakeFirst();
|
|
93
|
-
const domainRulesRaw = await db.selectFrom('options').select('value').where('name', '=', 'flare:signup_domain_rules').executeTakeFirst();
|
|
94
|
-
|
|
95
|
-
const isEnabled = signupEnabled?.value === 'true';
|
|
96
|
-
const roleDefault = defaultRole?.value || 'editor';
|
|
97
|
-
const domainRules = JSON.parse(domainRulesRaw?.value || '{}') as Record<string, string>;
|
|
98
|
-
|
|
99
|
-
if (!isEnabled) {
|
|
100
|
-
throw new Error('Signups are currently disabled');
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Determine role based on domain
|
|
104
|
-
const domain = primaryEmail.split('@')[1];
|
|
105
|
-
const assignedRole = domainRules[domain] || roleDefault;
|
|
106
|
-
|
|
107
|
-
// Provision new user
|
|
108
|
-
localUser = {
|
|
109
|
-
id: ulid(),
|
|
110
|
-
email: primaryEmail,
|
|
111
|
-
password: null,
|
|
112
|
-
role: assignedRole,
|
|
113
|
-
disabled: 0,
|
|
114
|
-
created_at: new Date().toISOString(),
|
|
115
|
-
updated_at: new Date().toISOString()
|
|
116
|
-
};
|
|
117
|
-
await db.insertInto('fc_users').values(localUser as any).execute();
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Link Account
|
|
121
|
-
await db.insertInto('fc_oauth_accounts')
|
|
122
|
-
.values({
|
|
123
|
-
provider_id: 'github',
|
|
124
|
-
provider_user_id: githubId,
|
|
125
|
-
user_id: localUser.id
|
|
126
|
-
})
|
|
127
|
-
.execute();
|
|
128
|
-
|
|
129
|
-
user = localUser as any;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (user!.disabled) return apiResponse.error(c, 'Account is disabled', 403);
|
|
133
|
-
|
|
134
|
-
// 5. Create Session
|
|
135
|
-
const sessionId = generateSessionToken();
|
|
136
|
-
const expiresAt = new Date();
|
|
137
|
-
expiresAt.setDate(expiresAt.getDate() + 30);
|
|
138
|
-
|
|
139
|
-
await db.insertInto('fc_sessions')
|
|
140
|
-
.values({
|
|
141
|
-
id: sessionId,
|
|
142
|
-
user_id: user!.id,
|
|
143
|
-
expires_at: expiresAt.toISOString(),
|
|
144
|
-
})
|
|
145
|
-
.execute();
|
|
146
|
-
|
|
147
|
-
setCookie(c, 'session', sessionId, {
|
|
148
|
-
httpOnly: true,
|
|
149
|
-
secure: true,
|
|
150
|
-
sameSite: 'Strict',
|
|
151
|
-
expires: expiresAt,
|
|
152
|
-
path: '/'
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
return c.redirect('/admin'); // Assuming frontend handles redirect logic to dashboard
|
|
156
|
-
} catch (error: any) {
|
|
157
|
-
return apiResponse.error(c, error.message);
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { Hono } from 'hono';
|
|
2
|
-
import { createDb } from '../../db';
|
|
3
|
-
import { requireRole } from '../middlewares/rbac';
|
|
4
|
-
import type { Bindings } from '../index';
|
|
5
|
-
import { apiResponse } from '../lib/response';
|
|
6
|
-
|
|
7
|
-
export const settingsRoutes = new Hono<{ Bindings: Bindings }>();
|
|
8
|
-
|
|
9
|
-
// All settings operations require admin role
|
|
10
|
-
settingsRoutes.use('/*', requireRole(['admin']));
|
|
11
|
-
|
|
12
|
-
// Only admins can modify core settings
|
|
13
|
-
settingsRoutes.get('/', async (c) => {
|
|
14
|
-
const db = createDb(c.env.DB);
|
|
15
|
-
const result = await db.selectFrom('options')
|
|
16
|
-
.selectAll()
|
|
17
|
-
.where('name', 'like', 'flare:%')
|
|
18
|
-
.execute();
|
|
19
|
-
return apiResponse.ok(c, result);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
settingsRoutes.patch('/', requireRole(['admin']), async (c) => {
|
|
23
|
-
const body = await c.req.json();
|
|
24
|
-
const db = createDb(c.env.DB);
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
for (const [key, value] of Object.entries(body)) {
|
|
28
|
-
const settingName = key.startsWith('flare:') ? key : `flare:${key}`;
|
|
29
|
-
await db.insertInto('options')
|
|
30
|
-
.values({
|
|
31
|
-
name: settingName,
|
|
32
|
-
value: String(value)
|
|
33
|
-
})
|
|
34
|
-
.onConflict((oc) => oc.column('name').doUpdateSet({
|
|
35
|
-
value: String(value)
|
|
36
|
-
}))
|
|
37
|
-
.execute();
|
|
38
|
-
}
|
|
39
|
-
return apiResponse.ok(c, { success: true });
|
|
40
|
-
} catch (e: any) {
|
|
41
|
-
return apiResponse.error(c, e.message);
|
|
42
|
-
}
|
|
43
|
-
});
|