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.
Files changed (113) hide show
  1. package/LICENSE +21 -0
  2. package/dist/auth/index.js +201 -1
  3. package/dist/cli/commands.js +5554 -55
  4. package/dist/cli/index.js +5554 -55
  5. package/dist/cli/mcp.js +30 -0
  6. package/dist/client/index.js +23576 -0
  7. package/dist/db/index.js +10392 -25
  8. package/dist/index.js +56776 -7582
  9. package/dist/server/index.js +43280 -0
  10. package/dist/style.css +5536 -0
  11. package/package.json +33 -30
  12. package/scripts/fix-api-paths.mjs +0 -32
  13. package/scripts/fix-imports.mjs +0 -38
  14. package/scripts/prefix-css.mjs +0 -45
  15. package/src/api/lib/cache.ts +0 -45
  16. package/src/api/lib/response.ts +0 -40
  17. package/src/api/middlewares/auth.ts +0 -186
  18. package/src/api/middlewares/cors.ts +0 -10
  19. package/src/api/middlewares/rbac.ts +0 -85
  20. package/src/api/routes/auth.ts +0 -377
  21. package/src/api/routes/collections.ts +0 -205
  22. package/src/api/routes/content.ts +0 -175
  23. package/src/api/routes/device.ts +0 -160
  24. package/src/api/routes/magic.ts +0 -150
  25. package/src/api/routes/mcp.ts +0 -273
  26. package/src/api/routes/oauth.ts +0 -160
  27. package/src/api/routes/settings.ts +0 -43
  28. package/src/api/routes/setup.ts +0 -307
  29. package/src/api/routes/tokens.ts +0 -80
  30. package/src/api/schemas/auth.ts +0 -15
  31. package/src/api/schemas/index.ts +0 -51
  32. package/src/api/schemas/tokens.ts +0 -24
  33. package/src/auth/index.ts +0 -28
  34. package/src/cli/commands.ts +0 -217
  35. package/src/cli/index.ts +0 -21
  36. package/src/cli/mcp.ts +0 -210
  37. package/src/cli/tests/cli.test.ts +0 -40
  38. package/src/cli/tests/create.test.ts +0 -87
  39. package/src/client/FlareAdminRouter.tsx +0 -47
  40. package/src/client/app.tsx +0 -175
  41. package/src/client/components/app-sidebar.tsx +0 -227
  42. package/src/client/components/collection-modal.tsx +0 -215
  43. package/src/client/components/content-list.tsx +0 -247
  44. package/src/client/components/dynamic-form.tsx +0 -190
  45. package/src/client/components/field-modal.tsx +0 -221
  46. package/src/client/components/settings/api-token-section.tsx +0 -400
  47. package/src/client/components/settings/general-section.tsx +0 -224
  48. package/src/client/components/settings/security-section.tsx +0 -154
  49. package/src/client/components/settings/seo-section.tsx +0 -200
  50. package/src/client/components/settings/signup-section.tsx +0 -257
  51. package/src/client/components/ui/accordion.tsx +0 -78
  52. package/src/client/components/ui/avatar.tsx +0 -107
  53. package/src/client/components/ui/badge.tsx +0 -52
  54. package/src/client/components/ui/button.tsx +0 -60
  55. package/src/client/components/ui/card.tsx +0 -103
  56. package/src/client/components/ui/checkbox.tsx +0 -27
  57. package/src/client/components/ui/collapsible.tsx +0 -19
  58. package/src/client/components/ui/dialog.tsx +0 -162
  59. package/src/client/components/ui/icon-picker.tsx +0 -485
  60. package/src/client/components/ui/icons-data.ts +0 -8476
  61. package/src/client/components/ui/input.tsx +0 -20
  62. package/src/client/components/ui/label.tsx +0 -20
  63. package/src/client/components/ui/popover.tsx +0 -91
  64. package/src/client/components/ui/select.tsx +0 -204
  65. package/src/client/components/ui/separator.tsx +0 -23
  66. package/src/client/components/ui/sheet.tsx +0 -141
  67. package/src/client/components/ui/sidebar.tsx +0 -722
  68. package/src/client/components/ui/skeleton.tsx +0 -13
  69. package/src/client/components/ui/sonner.tsx +0 -47
  70. package/src/client/components/ui/switch.tsx +0 -30
  71. package/src/client/components/ui/table.tsx +0 -116
  72. package/src/client/components/ui/tabs.tsx +0 -80
  73. package/src/client/components/ui/textarea.tsx +0 -18
  74. package/src/client/components/ui/tooltip.tsx +0 -68
  75. package/src/client/hooks/use-mobile.ts +0 -19
  76. package/src/client/index.css +0 -149
  77. package/src/client/index.ts +0 -7
  78. package/src/client/layouts/admin-layout.tsx +0 -93
  79. package/src/client/layouts/settings-layout.tsx +0 -104
  80. package/src/client/lib/api.ts +0 -72
  81. package/src/client/lib/utils.ts +0 -6
  82. package/src/client/main.tsx +0 -10
  83. package/src/client/pages/collection-detail.tsx +0 -634
  84. package/src/client/pages/collections.tsx +0 -180
  85. package/src/client/pages/dashboard.tsx +0 -133
  86. package/src/client/pages/device.tsx +0 -66
  87. package/src/client/pages/document-detail-page.tsx +0 -139
  88. package/src/client/pages/documents-page.tsx +0 -103
  89. package/src/client/pages/login.tsx +0 -345
  90. package/src/client/pages/settings.tsx +0 -65
  91. package/src/client/pages/setup.tsx +0 -129
  92. package/src/client/pages/signup.tsx +0 -188
  93. package/src/client/store/auth.ts +0 -30
  94. package/src/client/store/collections.ts +0 -13
  95. package/src/client/store/config.ts +0 -12
  96. package/src/client/store/fetcher.ts +0 -30
  97. package/src/client/store/router.ts +0 -95
  98. package/src/client/store/schema.ts +0 -39
  99. package/src/client/store/settings.ts +0 -31
  100. package/src/client/types.ts +0 -34
  101. package/src/db/dynamic.ts +0 -70
  102. package/src/db/index.ts +0 -16
  103. package/src/db/migrations/001_initial_schema.ts +0 -57
  104. package/src/db/migrations/002_auth_tables.ts +0 -84
  105. package/src/db/migrator.ts +0 -61
  106. package/src/db/schema.ts +0 -142
  107. package/src/index.ts +0 -12
  108. package/src/server/index.ts +0 -66
  109. package/src/types.ts +0 -20
  110. package/tests/css.test.ts +0 -21
  111. package/tests/modular.test.ts +0 -29
  112. package/tsconfig.json +0 -10
  113. /package/{style.css.d.ts → dist/style.css.d.ts} +0 -0
@@ -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
- });
@@ -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
- });