flarecms 0.1.0
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/README.md +73 -0
- package/dist/auth/index.js +40 -0
- package/dist/cli/commands.js +389 -0
- package/dist/cli/index.js +403 -0
- package/dist/cli/mcp.js +209 -0
- package/dist/db/index.js +164 -0
- package/dist/index.js +17626 -0
- package/package.json +105 -0
- package/scripts/fix-api-paths.mjs +32 -0
- package/scripts/fix-imports.mjs +38 -0
- package/scripts/prefix-css.mjs +45 -0
- package/src/api/lib/cache.ts +45 -0
- package/src/api/lib/response.ts +40 -0
- package/src/api/middlewares/auth.ts +186 -0
- package/src/api/middlewares/cors.ts +10 -0
- package/src/api/middlewares/rbac.ts +85 -0
- package/src/api/routes/auth.ts +377 -0
- package/src/api/routes/collections.ts +205 -0
- package/src/api/routes/content.ts +175 -0
- package/src/api/routes/device.ts +160 -0
- package/src/api/routes/magic.ts +150 -0
- package/src/api/routes/mcp.ts +273 -0
- package/src/api/routes/oauth.ts +160 -0
- package/src/api/routes/settings.ts +43 -0
- package/src/api/routes/setup.ts +307 -0
- package/src/api/routes/tokens.ts +80 -0
- package/src/api/schemas/auth.ts +15 -0
- package/src/api/schemas/index.ts +51 -0
- package/src/api/schemas/tokens.ts +24 -0
- package/src/auth/index.ts +28 -0
- package/src/cli/commands.ts +217 -0
- package/src/cli/index.ts +21 -0
- package/src/cli/mcp.ts +210 -0
- package/src/cli/tests/cli.test.ts +40 -0
- package/src/cli/tests/create.test.ts +87 -0
- package/src/client/FlareAdminRouter.tsx +47 -0
- package/src/client/app.tsx +175 -0
- package/src/client/components/app-sidebar.tsx +227 -0
- package/src/client/components/collection-modal.tsx +215 -0
- package/src/client/components/content-list.tsx +247 -0
- package/src/client/components/dynamic-form.tsx +190 -0
- package/src/client/components/field-modal.tsx +221 -0
- package/src/client/components/settings/api-token-section.tsx +400 -0
- package/src/client/components/settings/general-section.tsx +224 -0
- package/src/client/components/settings/security-section.tsx +154 -0
- package/src/client/components/settings/seo-section.tsx +200 -0
- package/src/client/components/settings/signup-section.tsx +257 -0
- package/src/client/components/ui/accordion.tsx +78 -0
- package/src/client/components/ui/avatar.tsx +107 -0
- package/src/client/components/ui/badge.tsx +52 -0
- package/src/client/components/ui/button.tsx +60 -0
- package/src/client/components/ui/card.tsx +103 -0
- package/src/client/components/ui/checkbox.tsx +27 -0
- package/src/client/components/ui/collapsible.tsx +19 -0
- package/src/client/components/ui/dialog.tsx +162 -0
- package/src/client/components/ui/icon-picker.tsx +485 -0
- package/src/client/components/ui/icons-data.ts +8476 -0
- package/src/client/components/ui/input.tsx +20 -0
- package/src/client/components/ui/label.tsx +20 -0
- package/src/client/components/ui/popover.tsx +91 -0
- package/src/client/components/ui/select.tsx +204 -0
- package/src/client/components/ui/separator.tsx +23 -0
- package/src/client/components/ui/sheet.tsx +141 -0
- package/src/client/components/ui/sidebar.tsx +722 -0
- package/src/client/components/ui/skeleton.tsx +13 -0
- package/src/client/components/ui/sonner.tsx +47 -0
- package/src/client/components/ui/switch.tsx +30 -0
- package/src/client/components/ui/table.tsx +116 -0
- package/src/client/components/ui/tabs.tsx +80 -0
- package/src/client/components/ui/textarea.tsx +18 -0
- package/src/client/components/ui/tooltip.tsx +68 -0
- package/src/client/hooks/use-mobile.ts +19 -0
- package/src/client/index.css +149 -0
- package/src/client/index.ts +7 -0
- package/src/client/layouts/admin-layout.tsx +93 -0
- package/src/client/layouts/settings-layout.tsx +104 -0
- package/src/client/lib/api.ts +72 -0
- package/src/client/lib/utils.ts +6 -0
- package/src/client/main.tsx +10 -0
- package/src/client/pages/collection-detail.tsx +634 -0
- package/src/client/pages/collections.tsx +180 -0
- package/src/client/pages/dashboard.tsx +133 -0
- package/src/client/pages/device.tsx +66 -0
- package/src/client/pages/document-detail-page.tsx +139 -0
- package/src/client/pages/documents-page.tsx +103 -0
- package/src/client/pages/login.tsx +345 -0
- package/src/client/pages/settings.tsx +65 -0
- package/src/client/pages/setup.tsx +129 -0
- package/src/client/pages/signup.tsx +188 -0
- package/src/client/store/auth.ts +30 -0
- package/src/client/store/collections.ts +13 -0
- package/src/client/store/config.ts +12 -0
- package/src/client/store/fetcher.ts +30 -0
- package/src/client/store/router.ts +95 -0
- package/src/client/store/schema.ts +39 -0
- package/src/client/store/settings.ts +31 -0
- package/src/client/types.ts +34 -0
- package/src/db/dynamic.ts +70 -0
- package/src/db/index.ts +16 -0
- package/src/db/migrations/001_initial_schema.ts +57 -0
- package/src/db/migrations/002_auth_tables.ts +84 -0
- package/src/db/migrator.ts +61 -0
- package/src/db/schema.ts +142 -0
- package/src/index.ts +12 -0
- package/src/server/index.ts +66 -0
- package/src/types.ts +20 -0
- package/style.css.d.ts +8 -0
- package/tests/css.test.ts +21 -0
- package/tests/modular.test.ts +29 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Kysely, sql } from 'kysely';
|
|
2
|
+
|
|
3
|
+
export async function up(db: Kysely<any>): Promise<void> {
|
|
4
|
+
// Core Sessions
|
|
5
|
+
await db.schema
|
|
6
|
+
.createTable('fc_sessions')
|
|
7
|
+
.ifNotExists()
|
|
8
|
+
.addColumn('id', 'text', (col) => col.primaryKey())
|
|
9
|
+
.addColumn('user_id', 'text', (col) => col.notNull())
|
|
10
|
+
.addColumn('expires_at', 'text', (col) => col.notNull())
|
|
11
|
+
.execute();
|
|
12
|
+
|
|
13
|
+
// Biometric Passkeys
|
|
14
|
+
await db.schema
|
|
15
|
+
.createTable('fc_passkeys')
|
|
16
|
+
.ifNotExists()
|
|
17
|
+
.addColumn('id', 'text', (col) => col.primaryKey())
|
|
18
|
+
.addColumn('user_id', 'text', (col) => col.notNull())
|
|
19
|
+
.addColumn('name', 'text')
|
|
20
|
+
.addColumn('public_key', 'text', (col) => col.notNull())
|
|
21
|
+
.addColumn('counter', 'integer', (col) => col.notNull())
|
|
22
|
+
.addColumn('device_type', 'text', (col) => col.notNull())
|
|
23
|
+
.addColumn('backed_up', 'integer', (col) => col.notNull())
|
|
24
|
+
.addColumn('transports', 'text')
|
|
25
|
+
.addColumn('created_at', 'text', (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`))
|
|
26
|
+
.addColumn('last_used_at', 'text')
|
|
27
|
+
.execute();
|
|
28
|
+
|
|
29
|
+
// API Token Management
|
|
30
|
+
await db.schema
|
|
31
|
+
.createTable('fc_api_tokens')
|
|
32
|
+
.ifNotExists()
|
|
33
|
+
.addColumn('id', 'text', (col) => col.primaryKey())
|
|
34
|
+
.addColumn('user_id', 'text', (col) => col.notNull())
|
|
35
|
+
.addColumn('name', 'text', (col) => col.notNull())
|
|
36
|
+
.addColumn('hash', 'text', (col) => col.notNull())
|
|
37
|
+
.addColumn('scopes', 'text', (col) => col.notNull())
|
|
38
|
+
.addColumn('expires_at', 'text')
|
|
39
|
+
.addColumn('last_used_at', 'text')
|
|
40
|
+
.addColumn('created_at', 'text', (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`))
|
|
41
|
+
.execute();
|
|
42
|
+
|
|
43
|
+
// OAuth Providers Linkage
|
|
44
|
+
await db.schema
|
|
45
|
+
.createTable('fc_oauth_accounts')
|
|
46
|
+
.ifNotExists()
|
|
47
|
+
.addColumn('provider_id', 'text', (col) => col.notNull())
|
|
48
|
+
.addColumn('provider_user_id', 'text', (col) => col.notNull())
|
|
49
|
+
.addColumn('user_id', 'text', (col) => col.notNull())
|
|
50
|
+
.addPrimaryKeyConstraint('pk_oauth_accounts', ['provider_id', 'provider_user_id'])
|
|
51
|
+
.execute();
|
|
52
|
+
|
|
53
|
+
// Email/OTP Verification Tokens
|
|
54
|
+
await db.schema
|
|
55
|
+
.createTable('fc_verification_tokens')
|
|
56
|
+
.ifNotExists()
|
|
57
|
+
.addColumn('identifier', 'text', (col) => col.notNull())
|
|
58
|
+
.addColumn('token', 'text', (col) => col.notNull())
|
|
59
|
+
.addColumn('expires_at', 'text', (col) => col.notNull())
|
|
60
|
+
.addPrimaryKeyConstraint('pk_verification_tokens', ['identifier', 'token'])
|
|
61
|
+
.execute();
|
|
62
|
+
|
|
63
|
+
// Device Code Authorization (IoT/TV flows)
|
|
64
|
+
await db.schema
|
|
65
|
+
.createTable('fc_device_codes')
|
|
66
|
+
.ifNotExists()
|
|
67
|
+
.addColumn('device_code', 'text', (col) => col.primaryKey())
|
|
68
|
+
.addColumn('user_code', 'text', (col) => col.notNull().unique())
|
|
69
|
+
.addColumn('client_id', 'text', (col) => col.notNull())
|
|
70
|
+
.addColumn('user_id', 'text')
|
|
71
|
+
.addColumn('scopes', 'text', (col) => col.notNull())
|
|
72
|
+
.addColumn('expires_at', 'text', (col) => col.notNull())
|
|
73
|
+
.addColumn('created_at', 'text', (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`))
|
|
74
|
+
.execute();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function down(db: Kysely<any>): Promise<void> {
|
|
78
|
+
await db.schema.dropTable('fc_sessions').ifExists().execute();
|
|
79
|
+
await db.schema.dropTable('fc_passkeys').ifExists().execute();
|
|
80
|
+
await db.schema.dropTable('fc_api_tokens').ifExists().execute();
|
|
81
|
+
await db.schema.dropTable('fc_oauth_accounts').ifExists().execute();
|
|
82
|
+
await db.schema.dropTable('fc_verification_tokens').ifExists().execute();
|
|
83
|
+
await db.schema.dropTable('fc_device_codes').ifExists().execute();
|
|
84
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { sql, type Migration } from 'kysely';
|
|
2
|
+
import type { FlareDb } from './index';
|
|
3
|
+
|
|
4
|
+
import * as initialSchema from './migrations/001_initial_schema';
|
|
5
|
+
import * as authTables from './migrations/002_auth_tables';
|
|
6
|
+
|
|
7
|
+
const STATIC_MIGRATIONS: Record<string, any> = {
|
|
8
|
+
'001_initial_schema': initialSchema,
|
|
9
|
+
'002_auth_tables': authTables,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Manual D1-safe migration runner.
|
|
14
|
+
* We avoid Kysely's built-in Migrator because it uses introspection (sqlite_master)
|
|
15
|
+
* which Cloudflare D1 restricts, causing SQLITE_AUTH errors.
|
|
16
|
+
*/
|
|
17
|
+
export async function runMigrations(db: FlareDb) {
|
|
18
|
+
// 1. Ensure migrations table exists
|
|
19
|
+
await sql`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS _fc_migrations (
|
|
21
|
+
name TEXT PRIMARY KEY,
|
|
22
|
+
executed_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
23
|
+
)
|
|
24
|
+
`.execute(db);
|
|
25
|
+
|
|
26
|
+
// 2. Get executed migrations
|
|
27
|
+
const executed = await db
|
|
28
|
+
.selectFrom('_fc_migrations' as any)
|
|
29
|
+
.select('name')
|
|
30
|
+
.execute();
|
|
31
|
+
const executedNames = new Set(executed.map((m: any) => m.name));
|
|
32
|
+
|
|
33
|
+
const results: { name: string; status: 'Success' | 'Skipped' | 'Error' }[] = [];
|
|
34
|
+
|
|
35
|
+
// 3. Run missing migrations in order
|
|
36
|
+
const migrationNames = Object.keys(STATIC_MIGRATIONS).sort();
|
|
37
|
+
|
|
38
|
+
for (const name of migrationNames) {
|
|
39
|
+
if (executedNames.has(name)) {
|
|
40
|
+
results.push({ name, status: 'Skipped' });
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const migration = STATIC_MIGRATIONS[name];
|
|
46
|
+
await migration.up(db);
|
|
47
|
+
|
|
48
|
+
await db
|
|
49
|
+
.insertInto('_fc_migrations' as any)
|
|
50
|
+
.values({ name })
|
|
51
|
+
.execute();
|
|
52
|
+
|
|
53
|
+
results.push({ name, status: 'Success' });
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error(`Migration ${name} failed:`, err);
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return results;
|
|
61
|
+
}
|
package/src/db/schema.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { ColumnType, Generated, Selectable, Insertable, Updateable } from 'kysely';
|
|
2
|
+
|
|
3
|
+
export interface Database {
|
|
4
|
+
options: OptionsTable;
|
|
5
|
+
fc_collections: CollectionsTable;
|
|
6
|
+
fc_fields: FieldsTable;
|
|
7
|
+
fc_users: UsersTable;
|
|
8
|
+
fc_passkeys: PasskeysTable;
|
|
9
|
+
fc_sessions: SessionsTable;
|
|
10
|
+
fc_api_tokens: ApiTokensTable;
|
|
11
|
+
fc_oauth_accounts: OAuthAccountsTable;
|
|
12
|
+
fc_verification_tokens: VerificationTokensTable;
|
|
13
|
+
fc_device_codes: DeviceCodesTable;
|
|
14
|
+
// This helps typescript understand that we can have arbitrary string tables for dynamic content
|
|
15
|
+
[tableName: string]: any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface OptionsTable {
|
|
19
|
+
name: string;
|
|
20
|
+
value: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type Option = Selectable<OptionsTable>;
|
|
24
|
+
export type NewOption = Insertable<OptionsTable>;
|
|
25
|
+
|
|
26
|
+
export interface CollectionsTable {
|
|
27
|
+
id: string;
|
|
28
|
+
slug: string;
|
|
29
|
+
label: string;
|
|
30
|
+
label_singular: string | null;
|
|
31
|
+
description: string | null;
|
|
32
|
+
icon: string | null;
|
|
33
|
+
is_public: ColumnType<number, number | undefined, number>;
|
|
34
|
+
features: string | null; // JSON array of strings
|
|
35
|
+
url_pattern: string | null;
|
|
36
|
+
created_at: ColumnType<string, string | undefined, never>;
|
|
37
|
+
updated_at: ColumnType<string, string | undefined, string>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type Collection = Selectable<CollectionsTable>;
|
|
41
|
+
export type NewCollection = Insertable<CollectionsTable>;
|
|
42
|
+
export type CollectionUpdate = Updateable<CollectionsTable>;
|
|
43
|
+
|
|
44
|
+
export interface FieldsTable {
|
|
45
|
+
id: string;
|
|
46
|
+
collection_id: string;
|
|
47
|
+
label: string;
|
|
48
|
+
slug: string;
|
|
49
|
+
type: string;
|
|
50
|
+
required: ColumnType<number, number | undefined, number>; // sqlite boolean
|
|
51
|
+
created_at: ColumnType<string, string | undefined, never>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type Field = Selectable<FieldsTable>;
|
|
55
|
+
export type NewField = Insertable<FieldsTable>;
|
|
56
|
+
|
|
57
|
+
export interface UsersTable {
|
|
58
|
+
id: string;
|
|
59
|
+
email: string;
|
|
60
|
+
password: string | null; // Nullable if passkey only
|
|
61
|
+
role: string;
|
|
62
|
+
disabled: ColumnType<number, number | undefined, number>;
|
|
63
|
+
created_at: ColumnType<string, string | undefined, never>;
|
|
64
|
+
updated_at: ColumnType<string, string | undefined, string>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type User = Selectable<UsersTable>;
|
|
68
|
+
export type NewUser = Insertable<UsersTable>;
|
|
69
|
+
export type UserUpdate = Updateable<UsersTable>;
|
|
70
|
+
|
|
71
|
+
export interface PasskeysTable {
|
|
72
|
+
id: string; // credentialID base64url encoded
|
|
73
|
+
user_id: string;
|
|
74
|
+
name: string | null; // User-defined alias
|
|
75
|
+
public_key: string; // base64url encoded
|
|
76
|
+
counter: number;
|
|
77
|
+
device_type: string;
|
|
78
|
+
backed_up: number;
|
|
79
|
+
transports: string | null;
|
|
80
|
+
created_at: ColumnType<string, string | undefined, never>;
|
|
81
|
+
last_used_at: string | null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type Passkey = Selectable<PasskeysTable>;
|
|
85
|
+
export type NewPasskey = Insertable<PasskeysTable>;
|
|
86
|
+
export type PasskeyUpdate = Updateable<PasskeysTable>;
|
|
87
|
+
|
|
88
|
+
export interface SessionsTable {
|
|
89
|
+
id: string;
|
|
90
|
+
user_id: string;
|
|
91
|
+
expires_at: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type Session = Selectable<SessionsTable>;
|
|
95
|
+
export type NewSession = Insertable<SessionsTable>;
|
|
96
|
+
export type SessionUpdate = Updateable<SessionsTable>;
|
|
97
|
+
|
|
98
|
+
export interface ApiTokensTable {
|
|
99
|
+
id: string; // Token prefix (ec_pat_...)
|
|
100
|
+
user_id: string;
|
|
101
|
+
name: string;
|
|
102
|
+
hash: string; // SHA-256 hashed secret
|
|
103
|
+
scopes: string; // JSON string (array or structured object)
|
|
104
|
+
expires_at: string | null;
|
|
105
|
+
last_used_at: string | null;
|
|
106
|
+
created_at: ColumnType<string, string | undefined, never>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type ApiToken = Selectable<ApiTokensTable>;
|
|
110
|
+
export type NewApiToken = Insertable<ApiTokensTable>;
|
|
111
|
+
|
|
112
|
+
export interface OAuthAccountsTable {
|
|
113
|
+
provider_id: string;
|
|
114
|
+
provider_user_id: string;
|
|
115
|
+
user_id: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type OAuthAccount = Selectable<OAuthAccountsTable>;
|
|
119
|
+
export type NewOAuthAccount = Insertable<OAuthAccountsTable>;
|
|
120
|
+
|
|
121
|
+
export interface VerificationTokensTable {
|
|
122
|
+
identifier: string; // Email
|
|
123
|
+
token: string; // Hashed token
|
|
124
|
+
expires_at: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export type VerificationToken = Selectable<VerificationTokensTable>;
|
|
128
|
+
export type NewVerificationToken = Insertable<VerificationTokensTable>;
|
|
129
|
+
|
|
130
|
+
export interface DeviceCodesTable {
|
|
131
|
+
device_code: string;
|
|
132
|
+
user_code: string;
|
|
133
|
+
client_id: string;
|
|
134
|
+
user_id: string | null; // Null until user approves
|
|
135
|
+
scopes: string; // JSON string array
|
|
136
|
+
expires_at: string;
|
|
137
|
+
created_at: ColumnType<string, string | undefined, never>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type DeviceCode = Selectable<DeviceCodesTable>;
|
|
141
|
+
export type NewDeviceCode = Insertable<DeviceCodesTable>;
|
|
142
|
+
export type DeviceCodeUpdate = Updateable<DeviceCodesTable>;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './auth';
|
|
2
|
+
export * from './db';
|
|
3
|
+
export * from './server';
|
|
4
|
+
export * from './client';
|
|
5
|
+
export * from './types';
|
|
6
|
+
|
|
7
|
+
import { createFlareAPI } from './server';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @deprecated Use createFlareAPI from 'flarecms/server' for better modularity.
|
|
11
|
+
*/
|
|
12
|
+
export const flarecms = createFlareAPI;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { corsMiddleware } from '../api/middlewares/cors';
|
|
3
|
+
import { setupMiddleware, authMiddleware } from '../api/middlewares/auth';
|
|
4
|
+
import { authRoutes } from '../api/routes/auth';
|
|
5
|
+
import { setupRoutes } from '../api/routes/setup';
|
|
6
|
+
import { collectionsRoutes } from '../api/routes/collections';
|
|
7
|
+
import { contentRoutes } from '../api/routes/content';
|
|
8
|
+
import { tokenRoutes } from '../api/routes/tokens';
|
|
9
|
+
import { deviceRoutes } from '../api/routes/device';
|
|
10
|
+
import { magicRoutes } from '../api/routes/magic';
|
|
11
|
+
import { oauthRoutes } from '../api/routes/oauth';
|
|
12
|
+
import { settingsRoutes } from '../api/routes/settings';
|
|
13
|
+
import { mcpRoutes } from '../api/routes/mcp';
|
|
14
|
+
import { apiResponse } from '../api/lib/response';
|
|
15
|
+
import type { Bindings, Variables } from '../types';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates the modular FlareCMS API router.
|
|
19
|
+
* This can be mounted into any Hono application.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* app.route('/api', createFlareAPI({ base: '/admin' }));
|
|
23
|
+
*/
|
|
24
|
+
export function createFlareAPI(options: { base?: string } = {}) {
|
|
25
|
+
const base = options.base || '/admin';
|
|
26
|
+
const api = new Hono<{ Bindings: Bindings; Variables: Variables }>();
|
|
27
|
+
|
|
28
|
+
// Middlewares
|
|
29
|
+
api.use('*', corsMiddleware);
|
|
30
|
+
api.use('*', async (c, next) => {
|
|
31
|
+
// Collect reserved slugs from base and standard system paths
|
|
32
|
+
const adminPrefix = base.replace(/^\//, '') || 'admin';
|
|
33
|
+
c.set('reservedSlugs', [
|
|
34
|
+
adminPrefix,
|
|
35
|
+
'api',
|
|
36
|
+
'setup',
|
|
37
|
+
'auth',
|
|
38
|
+
'login',
|
|
39
|
+
'signup',
|
|
40
|
+
'collections',
|
|
41
|
+
'users',
|
|
42
|
+
'settings',
|
|
43
|
+
'oauth',
|
|
44
|
+
]);
|
|
45
|
+
await next();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
api.use('/*', setupMiddleware);
|
|
49
|
+
api.use('/*', authMiddleware);
|
|
50
|
+
|
|
51
|
+
// Routes
|
|
52
|
+
api.route('/auth', authRoutes);
|
|
53
|
+
api.route('/setup', setupRoutes);
|
|
54
|
+
api.route('/collections', collectionsRoutes);
|
|
55
|
+
api.route('/content', contentRoutes);
|
|
56
|
+
api.route('/tokens', tokenRoutes);
|
|
57
|
+
api.route('/device', deviceRoutes);
|
|
58
|
+
api.route('/magic', magicRoutes);
|
|
59
|
+
api.route('/oauth', oauthRoutes);
|
|
60
|
+
api.route('/settings', settingsRoutes);
|
|
61
|
+
api.route('/mcp', mcpRoutes);
|
|
62
|
+
|
|
63
|
+
api.get('/health', (c) => apiResponse.ok(c, { status: 'ok' }));
|
|
64
|
+
|
|
65
|
+
return api;
|
|
66
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type D1Database,
|
|
3
|
+
type KVNamespace,
|
|
4
|
+
type Fetcher,
|
|
5
|
+
} from '@cloudflare/workers-types';
|
|
6
|
+
|
|
7
|
+
export type Bindings = {
|
|
8
|
+
DB: D1Database;
|
|
9
|
+
KV: KVNamespace;
|
|
10
|
+
ASSETS: Fetcher;
|
|
11
|
+
AUTH_SECRET: string;
|
|
12
|
+
GITHUB_CLIENT_ID?: string;
|
|
13
|
+
GITHUB_CLIENT_SECRET?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type Variables = {
|
|
17
|
+
user: any;
|
|
18
|
+
scopes: string[];
|
|
19
|
+
reservedSlugs: string[];
|
|
20
|
+
};
|
package/style.css.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { expect, test, describe } from "bun:test";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
|
|
4
|
+
describe("CSS Modularization", () => {
|
|
5
|
+
test("CSS is built with flare-cms prefix", () => {
|
|
6
|
+
const css = fs.readFileSync("./dist/style.css", "utf8");
|
|
7
|
+
// Verify prefix exists
|
|
8
|
+
expect(css).toContain(".flare-cms");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("Global tags are scoped under flare-cms", () => {
|
|
12
|
+
const css = fs.readFileSync("./dist/style.css", "utf8");
|
|
13
|
+
|
|
14
|
+
// html/body are converted to .flare-cms
|
|
15
|
+
expect(css).toContain(".flare-cms {");
|
|
16
|
+
|
|
17
|
+
// Verify that utility classes are prefixed with the scope
|
|
18
|
+
// Example: .flare-cms .bg-background
|
|
19
|
+
expect(css).toContain(".flare-cms .bg-background");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { expect, test, describe } from "bun:test";
|
|
2
|
+
import { createFlareAPI } from "../src/server";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
|
|
5
|
+
describe("Modular API", () => {
|
|
6
|
+
test("createFlareAPI returns a Hono instance", () => {
|
|
7
|
+
const api = createFlareAPI();
|
|
8
|
+
expect(api).toBeInstanceOf(Hono);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("API has health check route", async () => {
|
|
12
|
+
const api = createFlareAPI();
|
|
13
|
+
const res = await api.request("/health");
|
|
14
|
+
expect(res.status).toBe(200);
|
|
15
|
+
const data: any = await res.json();
|
|
16
|
+
expect(data.data.status).toBe("ok");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("API injects reservedSlugs middleware", async () => {
|
|
20
|
+
const api = createFlareAPI({ base: '/custom-admin' });
|
|
21
|
+
api.get('/test-context', (c) => {
|
|
22
|
+
return c.json({ slugs: c.get('reservedSlugs') });
|
|
23
|
+
});
|
|
24
|
+
const res = await api.request("/test-context");
|
|
25
|
+
const data: any = await res.json();
|
|
26
|
+
expect(data.slugs).toContain('custom-admin');
|
|
27
|
+
expect(data.slugs).toContain('api');
|
|
28
|
+
});
|
|
29
|
+
});
|