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
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "flarecms",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "license": "MIT",
5
5
  "description": "FlareCMS Monorepo: A lightweight CsMS for the AI era.",
6
+ "files": [
7
+ "dist"
8
+ ],
6
9
  "keywords": [
7
10
  "cloudflare",
8
11
  "workers",
@@ -27,8 +30,7 @@
27
30
  "homepage": "https://flarecms.francy.dev",
28
31
  "scripts": {
29
32
  "build:style": "node ./scripts/prefix-css.mjs",
30
- "build:client": "bun build ./src/client/main.tsx --outfile ./dist/main.js --minify",
31
- "build:all": "bun run build:style && bun run build:client"
33
+ "build": "bun ../../scripts/build.ts"
32
34
  },
33
35
  "type": "module",
34
36
  "main": "./dist/index.js",
@@ -40,50 +42,43 @@
40
42
  "./server": "./dist/server/index.js",
41
43
  "./client": "./dist/client/index.js",
42
44
  "./style.css": {
43
- "types": "./style.css.d.ts",
45
+ "types": "./dist/style.css.d.ts",
44
46
  "default": "./dist/style.css"
45
47
  },
46
48
  "./auth": "./dist/auth/index.js",
47
49
  "./db": "./dist/db/index.js",
48
50
  "./cli": "./dist/cli/index.js"
49
51
  },
50
- "dependencies": {
51
- "@base-ui/react": "^1.3.0",
52
+ "dependencies": {},
53
+ "devDependencies": {
52
54
  "@clack/prompts": "^0.7.0",
55
+ "@cloudflare/workers-types": "^4.20241022.0",
53
56
  "@fontsource-variable/geist": "^5.2.8",
54
- "@hono/react-renderer": "^0.1.0",
55
57
  "@nanostores/persistent": "^1.3.3",
56
- "@nanostores/query": "^0.1.0",
57
- "@nanostores/react": "^0.8.4",
58
+ "@nanostores/query": "^0.3.4",
59
+ "@nanostores/react": "^1.1.0",
58
60
  "@nanostores/router": "^1.0.0",
59
61
  "@oslojs/crypto": "^1.0.1",
60
62
  "@oslojs/encoding": "^1.1.0",
61
- "@radix-ui/react-avatar": "^1.1.11",
62
- "@radix-ui/react-dialog": "^1.1.15",
63
- "@radix-ui/react-label": "^2.1.8",
64
- "@radix-ui/react-popover": "^1.1.15",
65
- "@radix-ui/react-separator": "^1.1.8",
66
- "@radix-ui/react-slot": "^1.2.4",
67
- "@radix-ui/react-tabs": "^1.1.13",
68
- "@radix-ui/react-tooltip": "^1.2.8",
69
63
  "@simplewebauthn/browser": "^13.3.0",
70
64
  "@simplewebauthn/server": "^13.3.0",
65
+ "@tailwindcss/cli": "^4.0.0",
66
+ "@tailwindcss/postcss": "^4.2.2",
71
67
  "@tanstack/react-virtual": "^3.13.23",
68
+ "@types/bun": "latest",
69
+ "autoprefixer": "^10.4.27",
72
70
  "class-variance-authority": "^0.7.1",
73
71
  "clsx": "^2.1.1",
74
72
  "fuse.js": "^7.3.0",
75
73
  "giget": "^3.2.0",
76
- "hono": "^4.6.0",
77
74
  "ky": "^2.0.0",
78
75
  "kysely": "^0.28.15",
79
76
  "kysely-d1": "^0.4.0",
80
- "lucide-react": "^1.7.0",
81
77
  "nanostores": "^1.2.0",
82
78
  "next-themes": "^0.4.6",
83
79
  "picocolors": "^1.0.0",
84
- "radix-ui": "^1.4.3",
85
- "react": "^18.3.1",
86
- "react-dom": "^18.3.1",
80
+ "postcss": "^8.5.9",
81
+ "postcss-prefix-selector": "^2.1.1",
87
82
  "shadcn": "^4.2.0",
88
83
  "sonner": "^2.0.7",
89
84
  "tailwind-merge": "^3.5.0",
@@ -93,13 +88,21 @@
93
88
  "usehooks-ts": "^3.1.1",
94
89
  "zod": "^4.3.6"
95
90
  },
96
- "devDependencies": {
97
- "@cloudflare/workers-types": "^4.20241022.0",
98
- "@tailwindcss/cli": "^4.0.0",
99
- "@tailwindcss/postcss": "^4.2.2",
100
- "@types/node": "^20.0.0",
101
- "autoprefixer": "^10.4.27",
102
- "postcss": "^8.5.9",
103
- "postcss-prefix-selector": "^2.1.1"
91
+ "peerDependencies": {
92
+ "@base-ui/react": "^1.3.0",
93
+ "@hono/react-renderer": "^0.1.0",
94
+ "@radix-ui/react-avatar": "^1.1.11",
95
+ "@radix-ui/react-dialog": "^1.1.15",
96
+ "@radix-ui/react-label": "^2.1.8",
97
+ "@radix-ui/react-popover": "^1.1.15",
98
+ "@radix-ui/react-separator": "^1.1.8",
99
+ "@radix-ui/react-slot": "^1.2.4",
100
+ "@radix-ui/react-tabs": "^1.1.13",
101
+ "@radix-ui/react-tooltip": "^1.2.8",
102
+ "hono": "^4.6.0",
103
+ "lucide-react": "^1.7.0",
104
+ "radix-ui": "^1.4.3",
105
+ "react": "^18.3.1",
106
+ "react-dom": "^18.3.1"
104
107
  }
105
108
  }
@@ -1,32 +0,0 @@
1
- import fs from 'node:fs';
2
- import { resolve, dirname } from 'node:path';
3
-
4
- function getFiles(dir) {
5
- const dirents = fs.readdirSync(dir, { withFileTypes: true });
6
- const files = dirents.map((dirent) => {
7
- const res = resolve(dir, dirent.name);
8
- return dirent.isDirectory() ? getFiles(res) : res;
9
- });
10
- return Array.prototype.concat(...files);
11
- }
12
-
13
- const clientRoot = resolve('src/client');
14
- const files = getFiles(clientRoot).filter(f => f.endsWith('.ts') || f.endsWith('.tsx'));
15
-
16
- files.forEach(filePath => {
17
- let content = fs.readFileSync(filePath, 'utf8');
18
- let changed = false;
19
-
20
- // Replace apiFetch('/api/...') with apiFetch('/...')
21
- const newContent = content.replace(/apiFetch\((['"`])\/api\//g, (match, quote) => {
22
- changed = true;
23
- return `apiFetch(${quote}/`;
24
- });
25
-
26
- if (changed) {
27
- fs.writeFileSync(filePath, newContent);
28
- console.log(`Updated API paths in: ${filePath}`);
29
- }
30
- });
31
-
32
- console.log('API path refactoring complete.');
@@ -1,38 +0,0 @@
1
- import fs from 'node:fs';
2
- import { resolve, relative, dirname } from 'node:path';
3
-
4
- function getFiles(dir) {
5
- const dirents = fs.readdirSync(dir, { withFileTypes: true });
6
- const files = dirents.map((dirent) => {
7
- const res = resolve(dir, dirent.name);
8
- return dirent.isDirectory() ? getFiles(res) : res;
9
- });
10
- return Array.prototype.concat(...files);
11
- }
12
-
13
- const clientRoot = resolve('src/client');
14
- const files = getFiles(clientRoot).filter(f => f.endsWith('.ts') || f.endsWith('.tsx'));
15
-
16
- files.forEach(filePath => {
17
- const fileDir = dirname(filePath);
18
- let content = fs.readFileSync(filePath, 'utf8');
19
-
20
- // Robust Multiline Regex for @/ imports
21
- // Matches (import|export) ... from '@/...'
22
- content = content.replace(/(import|export)([\s\S]*?)from\s+['"]@\/(.*?)['"]/g, (match, type, body, importPath) => {
23
- const targetPath = resolve(clientRoot, importPath);
24
- let relPath = relative(fileDir, targetPath).replace(/\\/g, '/');
25
-
26
- if (!relPath.startsWith('.')) {
27
- relPath = './' + relPath;
28
- }
29
-
30
- // We replace specifically the matched @/path part
31
- const fullPattern = `@/${importPath}`;
32
- return match.replace(fullPattern, relPath);
33
- });
34
-
35
- fs.writeFileSync(filePath, content);
36
- });
37
-
38
- console.log(`Aggressively fixed imports in ${files.length} files.`);
@@ -1,45 +0,0 @@
1
- import fs from 'node:fs';
2
- import postcss from 'postcss';
3
- import tailwindcss from '@tailwindcss/postcss';
4
- import autoprefixer from 'autoprefixer';
5
- import prefixer from 'postcss-prefix-selector';
6
-
7
- async function build() {
8
- const input = './src/client/index.css';
9
- const output = './dist/style.css';
10
-
11
- if (!fs.existsSync(input)) {
12
- console.error(`Input file ${input} not found`);
13
- process.exit(1);
14
- }
15
-
16
- const css = fs.readFileSync(input, 'utf8');
17
-
18
- try {
19
- const result = await postcss([
20
- tailwindcss(),
21
- autoprefixer(),
22
- prefixer({
23
- prefix: '.flare-admin',
24
- transform(prefix, selector, prefixedSelector, filePath, rule) {
25
- if (selector === ':root') {
26
- return selector;
27
- }
28
- if (selector === 'html' || selector === 'body') {
29
- return prefix;
30
- }
31
- return prefixedSelector;
32
- }
33
- })
34
- ]).process(css, { from: input, to: output });
35
-
36
- if (!fs.existsSync('./dist')) fs.mkdirSync('./dist', { recursive: true });
37
- fs.writeFileSync(output, result.css);
38
- console.log('Successfully built prefixed CSS to ./dist/style.css');
39
- } catch (err) {
40
- console.error('CSS Build failed:', err);
41
- process.exit(1);
42
- }
43
- }
44
-
45
- build();
@@ -1,45 +0,0 @@
1
- import type { KVNamespace } from "@cloudflare/workers-types";
2
-
3
- export interface SchemaCache {
4
- id: string;
5
- slug: string;
6
- label: string;
7
- is_public: number;
8
- features: string[];
9
- url_pattern: string | null;
10
- fields: any[];
11
- }
12
-
13
- export const cache = {
14
- async getSchema(kv: KVNamespace, slug: string): Promise<SchemaCache | null> {
15
- const data = await kv.get(`schema:${slug}`);
16
- if (!data) return null;
17
- return JSON.parse(data);
18
- },
19
-
20
- async setSchema(kv: KVNamespace, slug: string, schema: SchemaCache) {
21
- await kv.put(`schema:${slug}`, JSON.stringify(schema), {
22
- expirationTtl: 60 * 60 * 24, // 24 hours (metadata is stable)
23
- });
24
- },
25
-
26
- async invalidateSchema(kv: KVNamespace, slug: string) {
27
- await kv.delete(`schema:${slug}`);
28
- },
29
-
30
- async getCollectionList(kv: KVNamespace): Promise<any[] | null> {
31
- const data = await kv.get('collections:list');
32
- if (!data) return null;
33
- return JSON.parse(data);
34
- },
35
-
36
- async setCollectionList(kv: KVNamespace, collections: any[]) {
37
- await kv.put('collections:list', JSON.stringify(collections), {
38
- expirationTtl: 60 * 60 * 24, // 24 hours
39
- });
40
- },
41
-
42
- async invalidateCollectionList(kv: KVNamespace) {
43
- await kv.delete('collections:list');
44
- }
45
- };
@@ -1,40 +0,0 @@
1
- import type { Context } from 'hono';
2
-
3
- export interface PaginationMeta {
4
- page: number;
5
- limit: number;
6
- total: number;
7
- totalPages: number;
8
- hasNextPage: boolean;
9
- hasPrevPage: boolean;
10
- }
11
-
12
- export const apiResponse = {
13
- /**
14
- * Send a successful response with data and optional metadata
15
- */
16
- ok: (c: Context, data: any, meta?: any, status: number = 200) => {
17
- return c.json({ data, meta }, status as any);
18
- },
19
-
20
- /**
21
- * Send a paginated response
22
- */
23
- paginated: (c: Context, data: any[], meta: PaginationMeta, status: number = 200) => {
24
- return c.json({ data, meta }, status as any);
25
- },
26
-
27
- /**
28
- * Send an error response
29
- */
30
- error: (c: Context, message: string | any, status: number = 400) => {
31
- return c.json({ error: message }, status as any);
32
- },
33
-
34
- /**
35
- * Specialized success response for creations
36
- */
37
- created: (c: Context, data: any) => {
38
- return c.json({ data }, 201);
39
- }
40
- };
@@ -1,186 +0,0 @@
1
- import type { Context, Next } from 'hono';
2
- import { getCookie } from 'hono/cookie';
3
- import { createDb } from '../../db';
4
-
5
- export const authMiddleware = async (c: Context, next: Next) => {
6
- const path = c.req.path;
7
-
8
- // Skip auth for login, setup, device public flows, and magic/oauth routes
9
- // We check for segments to be path-agnostic (handles /api/auth/login, /cms/auth/login, etc.)
10
- const isPublicRoute =
11
- path.endsWith('/auth/login') ||
12
- path.endsWith('/auth/signup') ||
13
- path.endsWith('/auth/registration-settings') ||
14
- path.endsWith('/auth/passkey/options') ||
15
- path.endsWith('/auth/passkey/verify') ||
16
- path.includes('/setup') ||
17
- path.endsWith('/health') || // Match exactly /health or /api/health
18
- path.includes('/health/') ||
19
- path.includes('/device/code') ||
20
- path.includes('/device/token') ||
21
- path.includes('/magic') ||
22
- path.includes('/oauth');
23
-
24
- if (isPublicRoute) {
25
- return next();
26
- }
27
-
28
- // Allow test mock bypass for rapid test building
29
- const authHeader = c.req.header('Authorization');
30
- if (authHeader?.startsWith('Bearer test-secret')) {
31
- // Inject a dummy admin user into context for tests
32
- c.set('user', { id: 'test-user', role: 'admin' });
33
- c.set('scopes', ['*']);
34
- return next();
35
- }
36
-
37
- const db = createDb(c.env.DB);
38
-
39
- // 1. Try Token Auth (PATs)
40
- if (authHeader?.startsWith('Bearer ec_pat_')) {
41
- const rawToken = authHeader.split(' ')[1];
42
-
43
- if (!rawToken) return c.json({ error: 'Invalid API Token' }, 401);
44
-
45
- // Tokens are formatted as ec_pat_ULID_SECRET
46
- const lastUnderscore = rawToken.lastIndexOf('_');
47
- if (lastUnderscore === -1) return c.json({ error: 'Invalid Token Format' }, 401);
48
-
49
- const tokenId = rawToken.substring(0, lastUnderscore);
50
- const suffix = rawToken.substring(lastUnderscore + 1);
51
-
52
- const tokenRecord = await db.selectFrom('fc_api_tokens')
53
- .selectAll()
54
- .where('id', '=', tokenId)
55
- .executeTakeFirst();
56
-
57
-
58
- if (!tokenRecord) return c.json({ error: 'Invalid API Token' }, 401);
59
-
60
- // Verify the hash of the provided suffix
61
- const encoder = new TextEncoder();
62
- const hashBuffer = await crypto.subtle.digest("SHA-256", encoder.encode(suffix));
63
- const hashHex = Array.from(new Uint8Array(hashBuffer))
64
- .map(b => b.toString(16).padStart(2, '0'))
65
- .join('');
66
-
67
- if (hashHex !== tokenRecord.hash) {
68
- return c.json({ error: 'Invalid API Token' }, 401);
69
- }
70
-
71
- const user = await db.selectFrom('fc_users')
72
- .select(['id', 'role', 'email', 'disabled'])
73
- .where('id', '=', tokenRecord.user_id)
74
- .executeTakeFirst();
75
-
76
- if (!user || user.disabled) return c.json({ error: 'Account disabled or not found' }, 403);
77
-
78
- // Update last_used_at async
79
- const updateQuery = db.updateTable('fc_api_tokens')
80
- .set({ last_used_at: new Date().toISOString() })
81
- .where('id', '=', tokenId);
82
-
83
- let hasCtx = false;
84
- try {
85
- hasCtx = !!c.executionCtx;
86
- } catch {
87
- hasCtx = false;
88
- }
89
-
90
- if (hasCtx) {
91
- c.executionCtx.waitUntil(updateQuery.execute());
92
- } else {
93
- await updateQuery.execute();
94
- }
95
-
96
-
97
- c.set('user', user);
98
- c.set('scopes', JSON.parse(tokenRecord.scopes));
99
- return next();
100
- }
101
-
102
-
103
- // 2. Try Cookie Session Auth
104
- const sessionId = getCookie(c, 'session');
105
- if (!sessionId) {
106
- // Special case: Allow anonymous GET on content for Public API Visibility
107
- if (c.req.method === 'GET' && c.req.path.startsWith('/api/content')) {
108
- c.set('scopes', []); // Empty scopes for anonymous
109
- return next();
110
- }
111
- return c.json({ error: 'Unauthorized' }, 401);
112
- }
113
-
114
- const session = await db.selectFrom('fc_sessions')
115
- .innerJoin('fc_users', 'fc_sessions.user_id', 'fc_users.id')
116
- .select(['fc_users.id', 'fc_users.role', 'fc_users.email', 'fc_users.disabled', 'fc_sessions.expires_at'])
117
- .where('fc_sessions.id', '=', sessionId)
118
- .executeTakeFirst();
119
-
120
- if (!session) {
121
- return c.json({ error: 'Invalid session' }, 401);
122
- }
123
-
124
- if (new Date(session.expires_at) < new Date()) {
125
- // Delete expired session
126
- await db.deleteFrom('fc_sessions').where('id', '=', sessionId).execute();
127
- return c.json({ error: 'Session expired' }, 401);
128
- }
129
-
130
- if (session.disabled) {
131
- return c.json({ error: 'Account disabled' }, 403);
132
- }
133
-
134
- // Inject user into context for downstream usage
135
- c.set('user', session);
136
- // Full UI Sessions have 'all' scopes implicitly because they are user-driven
137
- c.set('scopes', ['*']);
138
-
139
- return next();
140
- };
141
-
142
- export const setupMiddleware = async (c: Context, next: Next) => {
143
- const path = c.req.path;
144
-
145
- // If hitting setup, let it pass
146
- if (path.includes('/setup')) return next();
147
-
148
-
149
- try {
150
- const db = createDb(c.env.DB);
151
- const setupComplete = await db.selectFrom('options')
152
- .select('value')
153
- .where('name', '=', 'flare:setup_complete')
154
- .executeTakeFirst();
155
-
156
- let isComplete = false;
157
- try {
158
- isComplete = setupComplete?.value === 'true' || setupComplete?.value === '1';
159
- } catch {
160
- isComplete = false;
161
- }
162
-
163
- if (!isComplete) {
164
- return c.json({ error: 'Setup required', code: 'SETUP_REQUIRED' }, 403);
165
- }
166
-
167
- // Check if any user exists
168
- const userCount = await db.selectFrom('fc_users')
169
- .select((eb) => eb.fn.countAll<number>().as('count'))
170
- .executeTakeFirst();
171
-
172
- if (!userCount || userCount.count === 0) {
173
- return c.json({ error: 'Setup required (Admin missing)', code: 'SETUP_REQUIRED' }, 403);
174
- }
175
- } catch (err: any) {
176
- // Table doesn't exist yet (fresh install)
177
- const msg = err.message?.toLowerCase() || '';
178
- if (msg.includes('no such table') || msg.includes('not found')) {
179
- return c.json({ error: 'Setup required', code: 'SETUP_REQUIRED' }, 403);
180
- }
181
- // If it's another error, we might want to log it or handle it, but for setup we'll pass it
182
- }
183
-
184
-
185
- return next();
186
- };
@@ -1,10 +0,0 @@
1
- import { cors } from 'hono/cors';
2
-
3
- export const corsMiddleware = cors({
4
- origin: (origin) => origin,
5
- credentials: true,
6
- allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
7
- allowHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
8
- exposeHeaders: ['Content-Length'],
9
- maxAge: 600,
10
- });
@@ -1,85 +0,0 @@
1
- import type { Context, Next } from 'hono';
2
- import { createDb } from '../../db';
3
-
4
- /**
5
- * Ensures the authenticated user has one of the allowed roles.
6
- * Must be used AFTER authMiddleware.
7
- */
8
- export const requireRole = (allowedRoles: string[]) => {
9
- return async (c: Context, next: Next) => {
10
- const user = c.get('user');
11
-
12
- if (!user) {
13
- return c.json({ error: 'Unauthorized' }, 401);
14
- }
15
-
16
- if (!allowedRoles.includes(user.role)) {
17
- return c.json({ error: 'Forbidden', requiredRoles: allowedRoles }, 403);
18
- }
19
-
20
- return next();
21
- };
22
- };
23
-
24
- export const requireScope = (action: string, resourceOrParamName: string = '*') => {
25
- return async (c: Context, next: Next) => {
26
- // Determine the actual resource ID (e.g. from param)
27
- const resource = resourceOrParamName.startsWith(':') || resourceOrParamName === 'collection_slug'
28
- ? c.req.param(resourceOrParamName.replace(':', '')) || c.req.param('collection')
29
- : resourceOrParamName;
30
-
31
- // Check if collection is public (only for 'read' action) - DO THIS FIRST for anonymous access
32
- if (action === 'read' && resource) {
33
- const db = createDb(c.env.DB);
34
- const collection = await db.selectFrom('fc_collections')
35
- .select('is_public')
36
- .where('slug', '=', resource)
37
- .executeTakeFirst();
38
-
39
- if (collection && collection.is_public === 1) {
40
- return next();
41
- }
42
- }
43
-
44
- const scopes = c.get('scopes');
45
-
46
- if (!scopes || !Array.isArray(scopes)) {
47
- return c.json({ error: 'Unauthorized', code: 'NO_SCOPES' }, 401);
48
- }
49
-
50
- // Fast path: Full access
51
- if (scopes.includes('*')) {
52
- return next();
53
- }
54
-
55
- if (!scopes || !Array.isArray(scopes)) {
56
- return c.json({ error: 'Unauthorized', code: 'NO_SCOPES' }, 401);
57
- }
58
-
59
- // Check for granular match
60
- const hasPermission = scopes.some((s: any) => {
61
- // Handle legacy string scopes (e.g. "content:read")
62
- if (typeof s === 'string') {
63
- return s === `${resource}:${action}` || s === `*:${action}` || s === `${resource}:*`;
64
- }
65
-
66
- // Handle new structured scopes { resource: "posts", actions: ["read"] }
67
- if (typeof s === 'object' && s !== null) {
68
- const matchesResource = s.resource === '*' || s.resource === resource;
69
- const matchesAction = s.actions?.includes('*') || s.actions?.includes(action);
70
- return matchesResource && matchesAction;
71
- }
72
-
73
- return false;
74
- });
75
-
76
- if (!hasPermission) {
77
- return c.json({
78
- error: 'Forbidden: Insufficient API Token Scope',
79
- required: { action, resource }
80
- }, 403);
81
- }
82
-
83
- return next();
84
- };
85
- };