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/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flarecms",
|
|
3
|
-
"version": "0.1.
|
|
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
|
|
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
|
-
|
|
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.
|
|
57
|
-
"@nanostores/react": "^
|
|
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
|
-
"
|
|
85
|
-
"
|
|
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
|
-
"
|
|
97
|
-
"@
|
|
98
|
-
"@
|
|
99
|
-
"@
|
|
100
|
-
"@
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"
|
|
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.');
|
package/scripts/fix-imports.mjs
DELETED
|
@@ -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.`);
|
package/scripts/prefix-css.mjs
DELETED
|
@@ -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();
|
package/src/api/lib/cache.ts
DELETED
|
@@ -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
|
-
};
|
package/src/api/lib/response.ts
DELETED
|
@@ -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
|
-
};
|