create-fullstack-scaffold 0.4.23 → 0.4.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-fullstack-scaffold",
3
- "version": "0.4.23",
3
+ "version": "0.4.25",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "create-fullstack-scaffold": "dist/cli/index.js"
@@ -15,6 +15,7 @@
15
15
  "dev:forum": "VITE_PRESET=community vite",
16
16
  "dev:saas": "VITE_PRESET=saas vite",
17
17
  "build": "npm run build:client && npm run build:server",
18
+ "build:analyze": "ANALYZE=true vite build",
18
19
  "build:client": "vite build",
19
20
  "build:server": "tsup",
20
21
  "build:cli": "tsup --config tsup.config.ts",
@@ -131,7 +131,16 @@
131
131
  "confirmNewPassword": "Confirm New Password",
132
132
  "confirmNewPasswordPlaceholder": "Confirm new password",
133
133
  "saveChanges": "Save Changes",
134
- "saved": "Settings saved successfully!"
134
+ "saved": "Settings saved successfully!",
135
+ "email": "Email Settings",
136
+ "smtpHost": "SMTP Host",
137
+ "smtpHostPlaceholder": "e.g. smtp.example.com",
138
+ "smtpPort": "SMTP Port",
139
+ "emailFrom": "From Address",
140
+ "emailFromPlaceholder": "e.g. noreply@example.com",
141
+ "sessionTimeout": "Session Timeout",
142
+ "maxLoginAttempts": "Max Login Attempts",
143
+ "minutes": "minutes"
135
144
  },
136
145
  "roles": {
137
146
  "title": "Role Management",
@@ -131,7 +131,16 @@
131
131
  "confirmNewPassword": "确认新密码",
132
132
  "confirmNewPasswordPlaceholder": "请确认新密码",
133
133
  "saveChanges": "保存更改",
134
- "saved": "设置保存成功!"
134
+ "saved": "设置保存成功!",
135
+ "email": "邮件设置",
136
+ "smtpHost": "SMTP 主机",
137
+ "smtpHostPlaceholder": "例如 smtp.example.com",
138
+ "smtpPort": "SMTP 端口",
139
+ "emailFrom": "发件地址",
140
+ "emailFromPlaceholder": "例如 noreply@example.com",
141
+ "sessionTimeout": "会话超时",
142
+ "maxLoginAttempts": "最大登录尝试次数",
143
+ "minutes": "分钟"
135
144
  },
136
145
  "roles": {
137
146
  "title": "角色管理",
@@ -1,73 +1,122 @@
1
- import { Card, Form, Input, Button, Switch, Divider, message } from 'antd'
1
+ import { useState, useEffect, useCallback } from 'react'
2
+ import { Card, Form, Input, Button, Switch, Divider, message, InputNumber } from 'antd'
3
+ import { apiClient, api } from '../services/apiClient'
2
4
  import { useLanguage } from '../i18n/useLanguage'
3
-
4
- interface SettingsFormValues {
5
- siteName?: string
6
- siteDescription?: string
7
- currentPassword?: string
8
- newPassword?: string
9
- confirmPassword?: string
10
- }
5
+ import type { Settings } from '@shared/modules/admin'
11
6
 
12
7
  export const SettingsPage: React.FC = () => {
13
8
  const { t } = useLanguage()
14
- const [form] = Form.useForm<SettingsFormValues>()
9
+ const [form] = Form.useForm<Settings>()
10
+ const [loading, setLoading] = useState(false)
11
+
12
+ const fetchSettings = useCallback(async () => {
13
+ try {
14
+ const data = await api(apiClient.api.admin.settings.$get())
15
+ .withLoading(t('common.loading'))
16
+ .json()
17
+ form.setFieldsValue(data)
18
+ } catch {
19
+ // handled by api-request
20
+ }
21
+ }, [form, t])
15
22
 
16
- const handleSave = () => {
17
- message.success(t('settings.saved'))
23
+ useEffect(() => {
24
+ fetchSettings()
25
+ }, [fetchSettings])
26
+
27
+ const handleSave = async (values: Settings) => {
28
+ setLoading(true)
29
+ try {
30
+ await api(
31
+ apiClient.api.admin.settings.$put({
32
+ json: values,
33
+ })
34
+ )
35
+ .withLoading(t('common.loading'))
36
+ .json()
37
+ message.success(t('settings.saved'))
38
+ } catch {
39
+ // handled by api-request
40
+ } finally {
41
+ setLoading(false)
42
+ }
18
43
  }
19
44
 
20
45
  return (
21
46
  <div>
22
47
  <h1 className="text-2xl font-bold text-gray-900 mb-6">{t('settings.title')}</h1>
23
48
 
24
- <Card title={t('settings.general')} className="mb-6">
25
- <Form form={form} layout="vertical">
26
- <Form.Item label={t('settings.siteName')} name="siteName">
49
+ <Form form={form} layout="vertical" onFinish={handleSave}>
50
+ <Card title={t('settings.general')} className="mb-6">
51
+ <Form.Item label={t('settings.siteName')} name="siteName" rules={[{ required: true }]}>
27
52
  <Input placeholder={t('settings.siteNamePlaceholder')} />
28
53
  </Form.Item>
29
54
  <Form.Item label={t('settings.siteDescription')} name="siteDescription">
30
55
  <Input.TextArea rows={4} placeholder={t('settings.siteDescriptionPlaceholder')} />
31
56
  </Form.Item>
32
- </Form>
33
- </Card>
57
+ </Card>
34
58
 
35
- <Card title={t('settings.notification')} className="mb-6">
36
- <div className="space-y-4">
37
- <div className="flex items-center justify-between">
38
- <div>
39
- <p className="font-medium">{t('settings.emailNotifications')}</p>
40
- <p className="text-sm text-gray-500">{t('settings.emailNotificationsDesc')}</p>
59
+ <Card title={t('settings.email')} className="mb-6">
60
+ <Form.Item label={t('settings.smtpHost')} name="smtpHost" rules={[{ required: true }]}>
61
+ <Input placeholder={t('settings.smtpHostPlaceholder')} />
62
+ </Form.Item>
63
+ <Form.Item label={t('settings.smtpPort')} name="smtpPort" rules={[{ required: true }]}>
64
+ <InputNumber min={1} max={65535} className="w-full" />
65
+ </Form.Item>
66
+ <Form.Item
67
+ label={t('settings.emailFrom')}
68
+ name="emailFrom"
69
+ rules={[{ required: true, type: 'email' }]}
70
+ >
71
+ <Input placeholder={t('settings.emailFromPlaceholder')} />
72
+ </Form.Item>
73
+ </Card>
74
+
75
+ <Card title={t('settings.notification')} className="mb-6">
76
+ <div className="space-y-4">
77
+ <div className="flex items-center justify-between">
78
+ <div>
79
+ <p className="font-medium">{t('settings.emailNotifications')}</p>
80
+ <p className="text-sm text-gray-500">{t('settings.emailNotificationsDesc')}</p>
81
+ </div>
82
+ <Form.Item name="emailNotifications" valuePropName="checked" noStyle>
83
+ <Switch />
84
+ </Form.Item>
41
85
  </div>
42
- <Switch defaultChecked />
43
- </div>
44
- <Divider />
45
- <div className="flex items-center justify-between">
46
- <div>
47
- <p className="font-medium">{t('settings.pushNotifications')}</p>
48
- <p className="text-sm text-gray-500">{t('settings.pushNotificationsDesc')}</p>
86
+ <Divider />
87
+ <div className="flex items-center justify-between">
88
+ <div>
89
+ <p className="font-medium">{t('settings.pushNotifications')}</p>
90
+ <p className="text-sm text-gray-500">{t('settings.pushNotificationsDesc')}</p>
91
+ </div>
92
+ <Form.Item name="pushNotifications" valuePropName="checked" noStyle>
93
+ <Switch />
94
+ </Form.Item>
49
95
  </div>
50
- <Switch />
51
96
  </div>
52
- </div>
53
- </Card>
97
+ </Card>
54
98
 
55
- <Card title={t('settings.security')}>
56
- <Form form={form} layout="vertical">
57
- <Form.Item label={t('settings.currentPassword')} name="currentPassword">
58
- <Input.Password placeholder={t('settings.currentPasswordPlaceholder')} />
99
+ <Card title={t('settings.security')} className="mb-6">
100
+ <Form.Item
101
+ label={t('settings.sessionTimeout')}
102
+ name="sessionTimeout"
103
+ rules={[{ required: true }]}
104
+ >
105
+ <InputNumber min={5} max={1440} addonAfter={t('settings.minutes')} className="w-full" />
59
106
  </Form.Item>
60
- <Form.Item label={t('settings.newPassword')} name="newPassword">
61
- <Input.Password placeholder={t('settings.newPasswordPlaceholder')} />
107
+ <Form.Item
108
+ label={t('settings.maxLoginAttempts')}
109
+ name="maxLoginAttempts"
110
+ rules={[{ required: true }]}
111
+ >
112
+ <InputNumber min={1} max={20} className="w-full" />
62
113
  </Form.Item>
63
- <Form.Item label={t('settings.confirmNewPassword')} name="confirmPassword">
64
- <Input.Password placeholder={t('settings.confirmNewPasswordPlaceholder')} />
65
- </Form.Item>
66
- <Button type="primary" onClick={handleSave}>
67
- {t('settings.saveChanges')}
68
- </Button>
69
- </Form>
70
- </Card>
114
+ </Card>
115
+
116
+ <Button type="primary" htmlType="submit" loading={loading} size="large">
117
+ {t('settings.saveChanges')}
118
+ </Button>
119
+ </Form>
71
120
  </div>
72
121
  )
73
122
  }
@@ -9,6 +9,8 @@ import {
9
9
  HealthCheckSchema,
10
10
  RecentActivitySchema,
11
11
  ClearTodosResultSchema,
12
+ SettingsSchema,
13
+ UpdateSettingsSchema,
12
14
  } from '@shared/modules/admin'
13
15
 
14
16
  const getStatsRoute = createRoute({
@@ -68,7 +70,58 @@ const clearAllTodosRoute = createRoute({
68
70
  },
69
71
  })
70
72
 
73
+ const getSettingsRoute = createRoute({
74
+ method: 'get',
75
+ path: '/admin/settings',
76
+ tags: ['admin'],
77
+ security: [{ Bearer: [] }],
78
+ middleware: [authMiddleware({ requiredRole: Role.SUPER_ADMIN })],
79
+ responses: {
80
+ 200: successResponse(SettingsSchema, 'Get system settings'),
81
+ 401: errorResponse('Unauthorized'),
82
+ 403: errorResponse('Forbidden'),
83
+ },
84
+ })
85
+
86
+ const updateSettingsRoute = createRoute({
87
+ method: 'put',
88
+ path: '/admin/settings',
89
+ tags: ['admin'],
90
+ security: [{ Bearer: [] }],
91
+ middleware: [authMiddleware({ requiredRole: Role.SUPER_ADMIN })],
92
+ request: {
93
+ body: {
94
+ content: { 'application/json': { schema: UpdateSettingsSchema } },
95
+ },
96
+ },
97
+ responses: {
98
+ 200: successResponse(SettingsSchema, 'Settings updated'),
99
+ 401: errorResponse('Unauthorized'),
100
+ 403: errorResponse('Forbidden'),
101
+ },
102
+ })
103
+
104
+ let settingsState: z.infer<typeof SettingsSchema> = {
105
+ siteName: 'Biomimic Admin',
106
+ siteDescription: 'A full-stack admin dashboard',
107
+ smtpHost: 'smtp.example.com',
108
+ smtpPort: 587,
109
+ emailFrom: 'noreply@example.com',
110
+ sessionTimeout: 30,
111
+ maxLoginAttempts: 5,
112
+ emailNotifications: true,
113
+ pushNotifications: false,
114
+ }
115
+
71
116
  export const systemRoutes = new OpenAPIHono<{ Variables: { authUser: AuthUser } }>()
117
+ .openapi(getSettingsRoute, async c => {
118
+ return c.json(success(settingsState), 200)
119
+ })
120
+ .openapi(updateSettingsRoute, async c => {
121
+ const body = c.req.valid('json')
122
+ settingsState = { ...settingsState, ...body }
123
+ return c.json(success(settingsState), 200)
124
+ })
72
125
  .openapi(getStatsRoute, async c => {
73
126
  const stats = await adminService.getSystemStats()
74
127
  return c.json(success(stats), 200)
@@ -2,19 +2,36 @@
2
2
  import { describe, it, expect } from 'vitest'
3
3
  import { profileRoutes } from '../routes/profile-routes'
4
4
 
5
+ interface ProfileResponse {
6
+ success: boolean
7
+ data: {
8
+ id: string
9
+ username: string
10
+ email: string
11
+ bio: string
12
+ joinDate: string
13
+ stats: { posts: number; followers: number; following: number }
14
+ }
15
+ timestamp: string
16
+ }
17
+
18
+ async function fetchProfile(): Promise<ProfileResponse> {
19
+ const res = await profileRoutes.fetch(new Request('http://localhost/profile'))
20
+ return (await res.json()) as ProfileResponse
21
+ }
22
+
5
23
  describe('Profile Routes', () => {
6
24
  describe('GET /profile', () => {
7
25
  it('should return profile with 200', async () => {
8
26
  const res = await profileRoutes.fetch(new Request('http://localhost/profile'))
9
27
 
10
28
  expect(res.status).toBe(200)
11
- const data = (await res.json()) as Record<string, any>
29
+ const data = await fetchProfile()
12
30
  expect(data.success).toBe(true)
13
31
  })
14
32
 
15
33
  it('should return profile with required fields', async () => {
16
- const res = await profileRoutes.fetch(new Request('http://localhost/profile'))
17
- const data = (await res.json()) as Record<string, any>
34
+ const data = await fetchProfile()
18
35
 
19
36
  expect(data.success).toBe(true)
20
37
  expect(data.data.id).toBeDefined()
@@ -25,16 +42,14 @@ describe('Profile Routes', () => {
25
42
  })
26
43
 
27
44
  it('should return valid email format', async () => {
28
- const res = await profileRoutes.fetch(new Request('http://localhost/profile'))
29
- const data = (await res.json()) as Record<string, any>
45
+ const data = await fetchProfile()
30
46
 
31
47
  expect(data.success).toBe(true)
32
48
  expect(data.data.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
33
49
  })
34
50
 
35
51
  it('should return stats with numeric values', async () => {
36
- const res = await profileRoutes.fetch(new Request('http://localhost/profile'))
37
- const data = (await res.json()) as Record<string, any>
52
+ const data = await fetchProfile()
38
53
 
39
54
  expect(data.success).toBe(true)
40
55
  expect(typeof data.data.stats.posts).toBe('number')
@@ -43,18 +58,15 @@ describe('Profile Routes', () => {
43
58
  })
44
59
 
45
60
  it('should return a valid ISO date for joinDate', async () => {
46
- const res = await profileRoutes.fetch(new Request('http://localhost/profile'))
47
- const data = (await res.json()) as Record<string, any>
61
+ const data = await fetchProfile()
48
62
 
49
63
  expect(data.success).toBe(true)
50
64
  expect(data.data.joinDate).toMatch(/^\d{4}-\d{2}-\d{2}T/)
51
65
  })
52
66
 
53
67
  it('should return consistent data across requests', async () => {
54
- const res1 = await profileRoutes.fetch(new Request('http://localhost/profile'))
55
- const data1 = (await res1.json()) as Record<string, any>
56
- const res2 = await profileRoutes.fetch(new Request('http://localhost/profile'))
57
- const data2 = (await res2.json()) as Record<string, any>
68
+ const data1 = await fetchProfile()
69
+ const data2 = await fetchProfile()
58
70
 
59
71
  expect(data1.success).toBe(true)
60
72
  expect(data2.success).toBe(true)
@@ -64,8 +76,7 @@ describe('Profile Routes', () => {
64
76
  })
65
77
 
66
78
  it('should include timestamp in response', async () => {
67
- const res = await profileRoutes.fetch(new Request('http://localhost/profile'))
68
- const data = (await res.json()) as Record<string, any>
79
+ const data = await fetchProfile()
69
80
 
70
81
  expect(data.success).toBe(true)
71
82
  expect(data.timestamp).toBeDefined()
@@ -73,8 +84,7 @@ describe('Profile Routes', () => {
73
84
  })
74
85
 
75
86
  it('should return bio field', async () => {
76
- const res = await profileRoutes.fetch(new Request('http://localhost/profile'))
77
- const data = (await res.json()) as Record<string, any>
87
+ const data = await fetchProfile()
78
88
 
79
89
  expect(data.success).toBe(true)
80
90
  expect(data.data.bio).toBeDefined()
@@ -79,6 +79,20 @@ export const ClearTodosResultSchema = z.object({
79
79
  deletedCount: z.number(),
80
80
  })
81
81
 
82
+ export const SettingsSchema = z.object({
83
+ siteName: z.string(),
84
+ siteDescription: z.string(),
85
+ smtpHost: z.string(),
86
+ smtpPort: z.number(),
87
+ emailFrom: z.string(),
88
+ sessionTimeout: z.number(),
89
+ maxLoginAttempts: z.number(),
90
+ emailNotifications: z.boolean(),
91
+ pushNotifications: z.boolean(),
92
+ })
93
+
94
+ export const UpdateSettingsSchema = SettingsSchema
95
+
82
96
  export const AdminSuccessSchema = z.object({})
83
97
 
84
98
  export const DownloadTokenSchema = z.object({
@@ -98,3 +112,5 @@ export type RegisterRequest = z.infer<typeof RegisterRequestSchema>
98
112
  export type User = z.infer<typeof UserSchema>
99
113
  export type UpdateUserRequest = z.infer<typeof UpdateUserRequestSchema>
100
114
  export type ClearTodosResult = z.infer<typeof ClearTodosResultSchema>
115
+ export type Settings = z.infer<typeof SettingsSchema>
116
+ export type UpdateSettings = z.infer<typeof UpdateSettingsSchema>
@@ -1,8 +1,25 @@
1
1
  import path from 'path'
2
2
  import { existsSync } from 'fs'
3
- import { defineConfig } from 'vite'
3
+ import { defineConfig, type Plugin } from 'vite'
4
4
  import devServer from '@hono/vite-dev-server'
5
5
  import { websocketPlugin, dbPlugin } from './vite-plugins'
6
+ // Bundle analysis: npm install -D rollup-plugin-visualizer && npm run build:analyze
7
+ let visualizerPlugin: (() => Plugin) | undefined
8
+ if (process.env.ANALYZE === 'true') {
9
+ try {
10
+ // @ts-expect-error — optional dev dependency, installed via: npm install -D rollup-plugin-visualizer
11
+ const mod = await import('rollup-plugin-visualizer')
12
+ visualizerPlugin = () =>
13
+ mod.visualizer({
14
+ open: true,
15
+ filename: 'stats.html',
16
+ gzipSize: true,
17
+ brotliSize: true,
18
+ }) as Plugin
19
+ } catch {
20
+ // rollup-plugin-visualizer not installed — run: npm install -D rollup-plugin-visualizer
21
+ }
22
+ }
6
23
  // Prerender is optional — only needed during production build with puppeteer + Chrome installed
7
24
  let prerender: typeof import('@prerenderer/rollup-plugin').default | undefined
8
25
  let puppeteerRenderer: typeof import('@prerenderer/renderer-puppeteer').default | undefined
@@ -85,6 +102,7 @@ export default defineConfig({
85
102
  ],
86
103
  build: {
87
104
  outDir: 'dist/client',
105
+ chunkSizeWarningLimit: 1000,
88
106
  rollupOptions: {
89
107
  input: {
90
108
  main: path.resolve(__dirname, 'index.html'),
@@ -93,6 +111,12 @@ export default defineConfig({
93
111
  merchant: path.resolve(__dirname, 'merchant.html'),
94
112
  },
95
113
  output: {
114
+ manualChunks: {
115
+ 'vendor-react': ['react', 'react-dom', 'react-router-dom'],
116
+ 'vendor-antd': ['antd', '@ant-design/icons'],
117
+ 'vendor-hono': ['hono'],
118
+ 'vendor-zustand': ['zustand'],
119
+ },
96
120
  plugins: [
97
121
  ...(prerender && puppeteerRenderer
98
122
  ? [
@@ -105,6 +129,7 @@ export default defineConfig({
105
129
  }),
106
130
  ]
107
131
  : []),
132
+ ...(visualizerPlugin ? [visualizerPlugin()] : []),
108
133
  ],
109
134
  },
110
135
  onwarn(warning, defaultHandler) {
@@ -1,101 +0,0 @@
1
- CREATE TABLE `todos` (
2
- `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
3
- `title` text NOT NULL,
4
- `description` text,
5
- `status` text DEFAULT 'pending' NOT NULL,
6
- `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
7
- `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
8
- );
9
- --> statement-breakpoint
10
- CREATE TABLE `notifications` (
11
- `id` text PRIMARY KEY NOT NULL,
12
- `type` text NOT NULL,
13
- `title` text NOT NULL,
14
- `message` text NOT NULL,
15
- `read` integer DEFAULT false NOT NULL,
16
- `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
17
- );
18
- --> statement-breakpoint
19
- CREATE TABLE `permissions` (
20
- `id` text PRIMARY KEY NOT NULL,
21
- `code` text NOT NULL,
22
- `name` text NOT NULL,
23
- `label` text NOT NULL,
24
- `category` text NOT NULL,
25
- `description` text,
26
- `sort_order` integer DEFAULT 0,
27
- `is_active` integer DEFAULT true,
28
- `created_at` integer,
29
- `updated_at` integer
30
- );
31
- --> statement-breakpoint
32
- CREATE UNIQUE INDEX `permissions_code_unique` ON `permissions` (`code`);--> statement-breakpoint
33
- CREATE TABLE `roles` (
34
- `id` text PRIMARY KEY NOT NULL,
35
- `code` text NOT NULL,
36
- `name` text NOT NULL,
37
- `label` text NOT NULL,
38
- `description` text,
39
- `is_system` integer DEFAULT false,
40
- `is_active` integer DEFAULT true,
41
- `sort_order` integer DEFAULT 0,
42
- `created_at` integer,
43
- `updated_at` integer
44
- );
45
- --> statement-breakpoint
46
- CREATE UNIQUE INDEX `roles_code_unique` ON `roles` (`code`);--> statement-breakpoint
47
- CREATE TABLE `role_permissions` (
48
- `role_id` text NOT NULL,
49
- `permission_id` text NOT NULL,
50
- `created_at` integer,
51
- PRIMARY KEY(`role_id`, `permission_id`),
52
- FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON UPDATE no action ON DELETE cascade,
53
- FOREIGN KEY (`permission_id`) REFERENCES `permissions`(`id`) ON UPDATE no action ON DELETE cascade
54
- );
55
- --> statement-breakpoint
56
- CREATE TABLE `user_roles` (
57
- `id` text PRIMARY KEY NOT NULL,
58
- `user_id` text NOT NULL,
59
- `role_id` text NOT NULL,
60
- `assigned_by` text,
61
- `assigned_at` integer,
62
- `expires_at` integer,
63
- `is_active` integer DEFAULT true,
64
- FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON UPDATE no action ON DELETE cascade
65
- );
66
- --> statement-breakpoint
67
- CREATE TABLE `routes` (
68
- `id` text PRIMARY KEY NOT NULL,
69
- `path` text NOT NULL,
70
- `method` text NOT NULL,
71
- `name` text,
72
- `description` text,
73
- `module` text,
74
- `is_public` integer DEFAULT false,
75
- `is_active` integer DEFAULT true,
76
- `created_at` integer,
77
- `updated_at` integer
78
- );
79
- --> statement-breakpoint
80
- CREATE UNIQUE INDEX `routes_path_method_unique` ON `routes` (`path`,`method`);--> statement-breakpoint
81
- CREATE TABLE `permission_routes` (
82
- `permission_id` text NOT NULL,
83
- `route_id` text NOT NULL,
84
- `created_at` integer,
85
- PRIMARY KEY(`permission_id`, `route_id`),
86
- FOREIGN KEY (`permission_id`) REFERENCES `permissions`(`id`) ON UPDATE no action ON DELETE cascade,
87
- FOREIGN KEY (`route_id`) REFERENCES `routes`(`id`) ON UPDATE no action ON DELETE cascade
88
- );
89
- --> statement-breakpoint
90
- CREATE TABLE `permission_audit_logs` (
91
- `id` text PRIMARY KEY NOT NULL,
92
- `user_id` text NOT NULL,
93
- `action` text NOT NULL,
94
- `resource_type` text NOT NULL,
95
- `resource_id` text,
96
- `old_value` text,
97
- `new_value` text,
98
- `ip_address` text,
99
- `user_agent` text,
100
- `created_at` integer
101
- );
@@ -1,12 +0,0 @@
1
- CREATE TABLE `todo_attachments` (
2
- `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
3
- `todo_id` integer NOT NULL,
4
- `file_name` text NOT NULL,
5
- `original_name` text NOT NULL,
6
- `mime_type` text NOT NULL,
7
- `size` integer NOT NULL,
8
- `path` text NOT NULL,
9
- `uploaded_by` text,
10
- `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
11
- FOREIGN KEY (`todo_id`) REFERENCES `todos`(`id`) ON UPDATE no action ON DELETE cascade
12
- );