kempo 4.5.4 → 4.5.5
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 +15 -4
- package/package.json +5 -3
- package/scripts/init-db.js +2 -213
- package/server/db/index.js +38 -2
- package/server/utils/setup/seedDatabase.js +227 -0
package/README.md
CHANGED
|
@@ -430,17 +430,28 @@ To edit the admin portal without rebuilding on every change, point the middlewar
|
|
|
430
430
|
|
|
431
431
|
### Running the tests
|
|
432
432
|
|
|
433
|
-
|
|
433
|
+
There are two tiers, and `npm test` needs nothing installed or running:
|
|
434
|
+
|
|
435
|
+
```bash
|
|
436
|
+
npm test # unit tier: no database service, nothing to start
|
|
437
|
+
npm run test:integration # integration tier: a real Postgres
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Unit tier.** The suites that use the database run against [PGlite](https://pglite.dev), real Postgres compiled to WebAssembly and running inside the test process (`KEMPO_DB=pglite`, set for you). It is not a mock: indexes, `ON CONFLICT`, jsonb, foreign keys and the system-protection triggers all behave as in production, and the schema is generated from the same Drizzle definitions. It starts in about a second and cannot be unreachable, so a database suite can never skip and read as a pass.
|
|
441
|
+
|
|
442
|
+
**Integration tier.** The suites that boot a second server process, drive the make-admin script as a child, or use `LISTEN`/`NOTIFY` across connections cannot share an in-memory database, so they need a real Postgres. They show up in the unit tier as `(integration tier)` entries. Here an unreachable database is an error, not a skip:
|
|
434
443
|
|
|
435
444
|
```bash
|
|
436
445
|
docker compose up -d # postgres on 5434, separate from kempo-demo's on 5433
|
|
437
446
|
npx drizzle-kit push --force # apply the schema
|
|
438
|
-
npm test
|
|
447
|
+
DATABASE_URL=postgresql://kempo:kempo_test_password@localhost:5434/kempo_test npm run test:integration
|
|
439
448
|
```
|
|
440
449
|
|
|
441
|
-
`.env.example` has the matching `DATABASE_URL`. The
|
|
450
|
+
`.env.example` has the matching `DATABASE_URL`. The suites seed their own groups and permissions (`server/utils/setup/seedDatabase.js`) and clean up the rows they create. CI runs the unit tier on every push with no service, the integration tier alongside it, and publishing is gated on both.
|
|
451
|
+
|
|
452
|
+
Both tiers run only the Node-side suite. Admin components that need a real DOM run in an actual browser via `npm run test:browser` — no database or running server required, since it exercises the real component/SDK code with only `window.fetch` intercepted for canned responses. It temporarily junctions `/kempo-ui`, `/kempo-css` and `/kempo` onto `node_modules`/`dist` at the project root so the admin bundle's absolute imports resolve against kempo-testing-framework's static test server, and removes them afterward regardless of pass/fail.
|
|
442
453
|
|
|
443
|
-
|
|
454
|
+
A test that needs the database imports `./helpers/db.js` (`reachable`, `seedDatabase`, `integrationOnly`). Put a suite in the integration tier by exporting `integrationOnly('name')` when `unitTier` is true.
|
|
444
455
|
|
|
445
456
|
### Building for Distribution
|
|
446
457
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kempo",
|
|
3
|
-
"version": "4.5.
|
|
3
|
+
"version": "4.5.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A fullstack CMS framework for creating websites with user authentication",
|
|
6
6
|
"scripts": {
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
"makeAdmin": "node ./scripts/make-admin.js",
|
|
13
13
|
"removeAdmin": "node ./scripts/remove-admin.js",
|
|
14
14
|
"test:email": "node ./scripts/test-email.js",
|
|
15
|
-
"test": "
|
|
16
|
-
"test:node": "
|
|
15
|
+
"test": "node scripts/run-tests.js unit",
|
|
16
|
+
"test:node": "node scripts/run-tests.js unit",
|
|
17
|
+
"test:integration": "node scripts/run-tests.js integration",
|
|
17
18
|
"test:browser": "node scripts/run-browser-tests.js",
|
|
18
19
|
"link:local": "npm install ../kempo-server ../kempo-ui --no-save",
|
|
19
20
|
"db:reset": "node ./scripts/reset-db.js"
|
|
@@ -62,6 +63,7 @@
|
|
|
62
63
|
"kempo-ui": ">=0.5.1"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
66
|
+
"@electric-sql/pglite": "^0.5.9",
|
|
65
67
|
"esbuild": "^0.28.2",
|
|
66
68
|
"html-minifier-terser": "^7.2.0",
|
|
67
69
|
"kempo-testing-framework": "^1.4.6",
|
package/scripts/init-db.js
CHANGED
|
@@ -1,215 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import db from '../server/db/index.js';
|
|
3
|
-
import { permission, group, groupPermission, setting, createSettingProtectionTrigger, createGroupProtectionTrigger, createPermissionProtectionTrigger } from '../server/db/schema.js';
|
|
4
|
-
import { eq, and, sql } from 'drizzle-orm';
|
|
1
|
+
import seedDatabase from '../server/utils/setup/seedDatabase.js';
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
{ name: 'system:user:create', description: 'Create new users' },
|
|
8
|
-
{ name: 'system:user:read', description: 'View users' },
|
|
9
|
-
{ name: 'system:user:update', description: 'Edit users' },
|
|
10
|
-
{ name: 'system:user:delete', description: 'Delete users' },
|
|
11
|
-
|
|
12
|
-
{ name: 'system:group:create', description: 'Create new groups' },
|
|
13
|
-
{ name: 'system:group:read', description: 'View groups' },
|
|
14
|
-
{ name: 'system:group:update', description: 'Edit groups and manage membership' },
|
|
15
|
-
{ name: 'system:group:delete', description: 'Delete groups' },
|
|
16
|
-
|
|
17
|
-
{ name: 'system:permissions:read', description: 'View permissions for all users' },
|
|
18
|
-
{ name: 'system:permissions:manage', description: 'Add or remove permissions' },
|
|
19
|
-
|
|
20
|
-
{ name: 'system:settings:read', description: 'View private settings' },
|
|
21
|
-
{ name: 'system:settings:update', description: 'Update settings' },
|
|
22
|
-
{ name: 'system:custom-settings:manage', description: 'Create and delete custom settings' },
|
|
23
|
-
|
|
24
|
-
{ name: 'system:pages:read', description: 'View pages in admin' },
|
|
25
|
-
{ name: 'system:pages:create', description: 'Create new pages' },
|
|
26
|
-
{ name: 'system:pages:update', description: 'Edit pages' },
|
|
27
|
-
{ name: 'system:pages:delete', description: 'Delete pages' },
|
|
28
|
-
|
|
29
|
-
{ name: 'system:menus:create', description: 'Create new menus' },
|
|
30
|
-
{ name: 'system:menus:read', description: 'View menus in admin' },
|
|
31
|
-
{ name: 'system:menus:update', description: 'Edit menus and menu items' },
|
|
32
|
-
{ name: 'system:menus:delete', description: 'Delete menus' },
|
|
33
|
-
|
|
34
|
-
{ name: 'system:fragments:read', description: 'View fragments in admin' },
|
|
35
|
-
{ name: 'system:fragments:create', description: 'Create new fragments' },
|
|
36
|
-
{ name: 'system:fragments:update', description: 'Edit fragments' },
|
|
37
|
-
{ name: 'system:fragments:delete', description: 'Delete fragments' },
|
|
38
|
-
|
|
39
|
-
{ name: 'system:globals:read', description: 'View global content in admin' },
|
|
40
|
-
{ name: 'system:globals:create', description: 'Create new global content' },
|
|
41
|
-
{ name: 'system:globals:update', description: 'Edit global content' },
|
|
42
|
-
{ name: 'system:globals:delete', description: 'Delete global content' },
|
|
43
|
-
|
|
44
|
-
{ name: 'system:extensions:read', description: 'View installed extensions' },
|
|
45
|
-
{ name: 'system:extensions:install', description: 'Install extensions' },
|
|
46
|
-
{ name: 'system:extensions:uninstall', description: 'Uninstall extensions' },
|
|
47
|
-
{ name: 'system:extensions:manage', description: 'Enable and disable extensions' },
|
|
48
|
-
|
|
49
|
-
{ name: 'system:realtime:read', description: 'View realtime connections and channels' },
|
|
50
|
-
|
|
51
|
-
{ name: 'system:admin:access', description: 'Access admin panel' },
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
const DEFAULT_GROUPS = {
|
|
55
|
-
'system:Users': {
|
|
56
|
-
description: 'Default group for all users',
|
|
57
|
-
permissions: []
|
|
58
|
-
},
|
|
59
|
-
'system:Administrators': {
|
|
60
|
-
description: 'Full system access',
|
|
61
|
-
permissions: [
|
|
62
|
-
'system:admin:access',
|
|
63
|
-
'system:user:create', 'system:user:read', 'system:user:update', 'system:user:delete',
|
|
64
|
-
'system:group:create', 'system:group:read', 'system:group:update', 'system:group:delete',
|
|
65
|
-
'system:permissions:list-own', 'system:permissions:check-others', 'system:permissions:list-others', 'system:permissions:manage',
|
|
66
|
-
'system:settings:read', 'system:settings:update', 'system:custom-settings:manage',
|
|
67
|
-
'system:pages:read', 'system:pages:create', 'system:pages:update', 'system:pages:delete',
|
|
68
|
-
'system:menus:create', 'system:menus:read', 'system:menus:update', 'system:menus:delete',
|
|
69
|
-
'system:fragments:read', 'system:fragments:create', 'system:fragments:update', 'system:fragments:delete',
|
|
70
|
-
'system:globals:read', 'system:globals:create', 'system:globals:update', 'system:globals:delete',
|
|
71
|
-
'system:extensions:read', 'system:extensions:install', 'system:extensions:uninstall', 'system:extensions:manage',
|
|
72
|
-
'system:realtime:read'
|
|
73
|
-
]
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
console.log('=== Seeding Permissions ===\n');
|
|
78
|
-
|
|
79
|
-
for(const perm of DEFAULT_PERMISSIONS) {
|
|
80
|
-
const existing = await db.select()
|
|
81
|
-
.from(permission)
|
|
82
|
-
.where(eq(permission.name, perm.name))
|
|
83
|
-
.limit(1);
|
|
84
|
-
|
|
85
|
-
if(existing.length === 0) {
|
|
86
|
-
await db.insert(permission).values({
|
|
87
|
-
name: perm.name,
|
|
88
|
-
description: perm.description,
|
|
89
|
-
owner: 'system',
|
|
90
|
-
createdAt: new Date()
|
|
91
|
-
});
|
|
92
|
-
console.log(`✓ Created permission: ${perm.name}`);
|
|
93
|
-
} else {
|
|
94
|
-
console.log(`- Permission exists: ${perm.name}`);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
console.log('\n=== Creating Groups ===\n');
|
|
99
|
-
|
|
100
|
-
const allPermissions = await db.select().from(permission);
|
|
101
|
-
const permissionMap = Object.fromEntries(
|
|
102
|
-
allPermissions.map(p => [p.name, p.name])
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
for(const [groupName, groupData] of Object.entries(DEFAULT_GROUPS)) {
|
|
106
|
-
let groupRecord = await db.select()
|
|
107
|
-
.from(group)
|
|
108
|
-
.where(eq(group.name, groupName))
|
|
109
|
-
.limit(1);
|
|
110
|
-
|
|
111
|
-
if(groupRecord.length === 0) {
|
|
112
|
-
groupRecord = await db.insert(group).values({
|
|
113
|
-
name: groupName,
|
|
114
|
-
description: groupData.description,
|
|
115
|
-
owner: 'system',
|
|
116
|
-
createdAt: new Date()
|
|
117
|
-
}).returning();
|
|
118
|
-
console.log(`✓ Created group: ${groupName}`);
|
|
119
|
-
} else {
|
|
120
|
-
console.log(`- Group exists: ${groupName}`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if(groupName === 'system:Administrators') {
|
|
124
|
-
console.log(` Skipping permission assignments (admin has all permissions)`);
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
console.log(` Assigning permissions to ${groupName}:`);
|
|
129
|
-
|
|
130
|
-
for(const permName of groupData.permissions) {
|
|
131
|
-
if(!permissionMap[permName]) {
|
|
132
|
-
console.log(` ⚠ Permission not found: ${permName}`);
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const existing = await db.select()
|
|
137
|
-
.from(groupPermission)
|
|
138
|
-
.where(and(
|
|
139
|
-
eq(groupPermission.groupName, groupName),
|
|
140
|
-
eq(groupPermission.permissionName, permName)
|
|
141
|
-
))
|
|
142
|
-
.limit(1);
|
|
143
|
-
|
|
144
|
-
if(existing.length === 0) {
|
|
145
|
-
await db.insert(groupPermission).values({
|
|
146
|
-
id: crypto.randomUUID(),
|
|
147
|
-
groupName: groupName,
|
|
148
|
-
permissionName: permName,
|
|
149
|
-
createdAt: new Date()
|
|
150
|
-
});
|
|
151
|
-
console.log(` ✓ ${permName}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
console.log('\n=== Creating Default Settings ===\n');
|
|
157
|
-
|
|
158
|
-
const DEFAULT_SETTINGS = [
|
|
159
|
-
{ owner: 'system', name: 'site_name', value: 'Kempo Site', type: 'string', isPublic: true, description: 'The name of the website' },
|
|
160
|
-
{ owner: 'system', name: 'session_duration_days', value: 7, type: 'number', isPublic: false, description: 'Session duration in days' },
|
|
161
|
-
{ owner: 'system', name: 'allow_registration', value: true, type: 'boolean', isPublic: true, description: 'Allow new users to register' },
|
|
162
|
-
{ owner: 'system', name: 'require_email_verification', value: false, type: 'boolean', isPublic: false, description: 'Require email verification before account access' },
|
|
163
|
-
{ owner: 'system', name: 'verification_url', value: 'http://localhost:3000/verify-email/{{token}}', type: 'string', isPublic: false, description: 'Email verification URL template' },
|
|
164
|
-
{ owner: 'system', name: 'notification_retention_days', value: 90, type: 'number', isPublic: false, description: 'Days to keep notifications before they are deleted (0 keeps them forever)' },
|
|
165
|
-
{ owner: 'system', name: 'password_reset_url', value: 'http://localhost:3000/reset-password/{{token}}', type: 'string', isPublic: false, description: 'Password reset URL template' },
|
|
166
|
-
];
|
|
167
|
-
|
|
168
|
-
const { default: setSetting } = await import('../server/utils/settings/setSetting.js');
|
|
169
|
-
for(const s of DEFAULT_SETTINGS){
|
|
170
|
-
await setSetting(s.owner, s.name, s.value, s.type, s.isPublic, s.description);
|
|
171
|
-
console.log(`✓ Created setting: ${s.owner}:${s.name} = ${s.value}`);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
console.log('\n=== Updating Existing Records ===\n');
|
|
175
|
-
|
|
176
|
-
const updatedPerms = await db.update(permission)
|
|
177
|
-
.set({ owner: 'system' })
|
|
178
|
-
.where(sql`name LIKE 'system:%'`)
|
|
179
|
-
.returning();
|
|
180
|
-
console.log(`✓ Updated ${updatedPerms.length} permissions to owner='system'`);
|
|
181
|
-
|
|
182
|
-
const updatedGroups = await db.update(group)
|
|
183
|
-
.set({ owner: 'system' })
|
|
184
|
-
.where(sql`name LIKE 'system:%'`)
|
|
185
|
-
.returning();
|
|
186
|
-
console.log(`✓ Updated ${updatedGroups.length} groups to owner='system'`);
|
|
187
|
-
|
|
188
|
-
console.log('\nCreating database triggers to protect system resources...');
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
await db.execute(sql.raw('DROP TRIGGER IF EXISTS protect_system_settings ON setting'));
|
|
192
|
-
await db.execute(sql.raw(createSettingProtectionTrigger()));
|
|
193
|
-
console.log('✓ System settings protection trigger created');
|
|
194
|
-
} catch(e) {
|
|
195
|
-
console.log('- Settings trigger already exists');
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
try {
|
|
199
|
-
await db.execute(sql.raw('DROP TRIGGER IF EXISTS protect_system_groups ON "group"'));
|
|
200
|
-
await db.execute(sql.raw(createGroupProtectionTrigger()));
|
|
201
|
-
console.log('✓ System groups protection trigger created');
|
|
202
|
-
} catch(e) {
|
|
203
|
-
console.log('- Groups trigger already exists');
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
try {
|
|
207
|
-
await db.execute(sql.raw('DROP TRIGGER IF EXISTS protect_system_permissions ON permission'));
|
|
208
|
-
await db.execute(sql.raw(createPermissionProtectionTrigger()));
|
|
209
|
-
console.log('✓ System permissions protection trigger created');
|
|
210
|
-
} catch(e) {
|
|
211
|
-
console.log('- Permissions trigger already exists');
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
console.log('\n=== Group & Permission Seeding Complete ===');
|
|
3
|
+
await seedDatabase();
|
|
215
4
|
process.exit(0);
|
package/server/db/index.js
CHANGED
|
@@ -18,7 +18,43 @@ if(existsSync(appSchemaPath) && realpathSync(appSchemaPath) !== realpathSync(fra
|
|
|
18
18
|
appSchema = await import(pathToFileURL(appSchemaPath).href);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const
|
|
22
|
-
|
|
21
|
+
const schema = { ...frameworkSchema, ...appSchema };
|
|
22
|
+
|
|
23
|
+
let db;
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
KEMPO_DB=pglite swaps the server for an in-process, in-memory Postgres (PGlite) with the schema
|
|
27
|
+
applied, so the unit tier needs no service and cannot skip for want of one. It is real Postgres
|
|
28
|
+
(jsonb, partial indexes, ON CONFLICT, triggers), not a mock. Nothing outside tests sets it, and
|
|
29
|
+
PGlite is a dev dependency that is only imported on this branch.
|
|
30
|
+
*/
|
|
31
|
+
if(process.env.KEMPO_DB === 'pglite'){
|
|
32
|
+
const [{ PGlite }, { drizzle: drizzlePglite }, { generateDrizzleJson, generateMigration }] = await Promise.all([
|
|
33
|
+
import('@electric-sql/pglite'),
|
|
34
|
+
import('drizzle-orm/pglite'),
|
|
35
|
+
import('drizzle-kit/api')
|
|
36
|
+
]);
|
|
37
|
+
const client = new PGlite();
|
|
38
|
+
db = drizzlePglite(client, { schema });
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
The database is empty, so the whole schema is the diff from nothing. This is drizzle-kit's own
|
|
42
|
+
generator, so indexes, unique constraints and foreign keys match `drizzle-kit push` exactly.
|
|
43
|
+
*/
|
|
44
|
+
const statements = await generateMigration(generateDrizzleJson({}), generateDrizzleJson(schema));
|
|
45
|
+
await client.exec(statements.join('\n'));
|
|
46
|
+
|
|
47
|
+
/*
|
|
48
|
+
postgres-js answers db.execute() with an array of rows and PGlite with `{ rows }`. Make PGlite look
|
|
49
|
+
like postgres-js (keeping `.rows`) so code and tests that read a raw result work on either.
|
|
50
|
+
*/
|
|
51
|
+
const execute = db.execute.bind(db);
|
|
52
|
+
db.execute = async query => {
|
|
53
|
+
const result = await execute(query);
|
|
54
|
+
return Object.assign([...result.rows], { rows: result.rows });
|
|
55
|
+
};
|
|
56
|
+
} else {
|
|
57
|
+
db = drizzle(postgres(process.env.DATABASE_URL), { schema });
|
|
58
|
+
}
|
|
23
59
|
|
|
24
60
|
export default db;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import db from '../../db/index.js';
|
|
3
|
+
import { permission, group, groupPermission, setting, createSettingProtectionTrigger, createGroupProtectionTrigger, createPermissionProtectionTrigger } from '../../db/schema.js';
|
|
4
|
+
import { eq, and, sql } from 'drizzle-orm';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_PERMISSIONS = [
|
|
7
|
+
{ name: 'system:user:create', description: 'Create new users' },
|
|
8
|
+
{ name: 'system:user:read', description: 'View users' },
|
|
9
|
+
{ name: 'system:user:update', description: 'Edit users' },
|
|
10
|
+
{ name: 'system:user:delete', description: 'Delete users' },
|
|
11
|
+
|
|
12
|
+
{ name: 'system:group:create', description: 'Create new groups' },
|
|
13
|
+
{ name: 'system:group:read', description: 'View groups' },
|
|
14
|
+
{ name: 'system:group:update', description: 'Edit groups and manage membership' },
|
|
15
|
+
{ name: 'system:group:delete', description: 'Delete groups' },
|
|
16
|
+
|
|
17
|
+
{ name: 'system:permissions:read', description: 'View permissions for all users' },
|
|
18
|
+
{ name: 'system:permissions:manage', description: 'Add or remove permissions' },
|
|
19
|
+
|
|
20
|
+
{ name: 'system:settings:read', description: 'View private settings' },
|
|
21
|
+
{ name: 'system:settings:update', description: 'Update settings' },
|
|
22
|
+
{ name: 'system:custom-settings:manage', description: 'Create and delete custom settings' },
|
|
23
|
+
|
|
24
|
+
{ name: 'system:pages:read', description: 'View pages in admin' },
|
|
25
|
+
{ name: 'system:pages:create', description: 'Create new pages' },
|
|
26
|
+
{ name: 'system:pages:update', description: 'Edit pages' },
|
|
27
|
+
{ name: 'system:pages:delete', description: 'Delete pages' },
|
|
28
|
+
|
|
29
|
+
{ name: 'system:menus:create', description: 'Create new menus' },
|
|
30
|
+
{ name: 'system:menus:read', description: 'View menus in admin' },
|
|
31
|
+
{ name: 'system:menus:update', description: 'Edit menus and menu items' },
|
|
32
|
+
{ name: 'system:menus:delete', description: 'Delete menus' },
|
|
33
|
+
|
|
34
|
+
{ name: 'system:fragments:read', description: 'View fragments in admin' },
|
|
35
|
+
{ name: 'system:fragments:create', description: 'Create new fragments' },
|
|
36
|
+
{ name: 'system:fragments:update', description: 'Edit fragments' },
|
|
37
|
+
{ name: 'system:fragments:delete', description: 'Delete fragments' },
|
|
38
|
+
|
|
39
|
+
{ name: 'system:globals:read', description: 'View global content in admin' },
|
|
40
|
+
{ name: 'system:globals:create', description: 'Create new global content' },
|
|
41
|
+
{ name: 'system:globals:update', description: 'Edit global content' },
|
|
42
|
+
{ name: 'system:globals:delete', description: 'Delete global content' },
|
|
43
|
+
|
|
44
|
+
{ name: 'system:extensions:read', description: 'View installed extensions' },
|
|
45
|
+
{ name: 'system:extensions:install', description: 'Install extensions' },
|
|
46
|
+
{ name: 'system:extensions:uninstall', description: 'Uninstall extensions' },
|
|
47
|
+
{ name: 'system:extensions:manage', description: 'Enable and disable extensions' },
|
|
48
|
+
|
|
49
|
+
{ name: 'system:realtime:read', description: 'View realtime connections and channels' },
|
|
50
|
+
|
|
51
|
+
{ name: 'system:admin:access', description: 'Access admin panel' },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const DEFAULT_GROUPS = {
|
|
55
|
+
'system:Users': {
|
|
56
|
+
description: 'Default group for all users',
|
|
57
|
+
permissions: []
|
|
58
|
+
},
|
|
59
|
+
'system:Administrators': {
|
|
60
|
+
description: 'Full system access',
|
|
61
|
+
permissions: [
|
|
62
|
+
'system:admin:access',
|
|
63
|
+
'system:user:create', 'system:user:read', 'system:user:update', 'system:user:delete',
|
|
64
|
+
'system:group:create', 'system:group:read', 'system:group:update', 'system:group:delete',
|
|
65
|
+
'system:permissions:list-own', 'system:permissions:check-others', 'system:permissions:list-others', 'system:permissions:manage',
|
|
66
|
+
'system:settings:read', 'system:settings:update', 'system:custom-settings:manage',
|
|
67
|
+
'system:pages:read', 'system:pages:create', 'system:pages:update', 'system:pages:delete',
|
|
68
|
+
'system:menus:create', 'system:menus:read', 'system:menus:update', 'system:menus:delete',
|
|
69
|
+
'system:fragments:read', 'system:fragments:create', 'system:fragments:update', 'system:fragments:delete',
|
|
70
|
+
'system:globals:read', 'system:globals:create', 'system:globals:update', 'system:globals:delete',
|
|
71
|
+
'system:extensions:read', 'system:extensions:install', 'system:extensions:uninstall', 'system:extensions:manage',
|
|
72
|
+
'system:realtime:read'
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/*
|
|
78
|
+
Each trigger definition is a function plus a trigger in one string. node-postgres style drivers send
|
|
79
|
+
that as a simple query; PGlite's execute() prepares it and refuses multiple commands, so it uses exec().
|
|
80
|
+
*/
|
|
81
|
+
const runScript = text => db.$client?.exec ? db.$client.exec(text) : db.execute(sql.raw(text));
|
|
82
|
+
|
|
83
|
+
/*
|
|
84
|
+
Seeds the system permissions, groups, settings and the triggers that protect them. Idempotent.
|
|
85
|
+
`scripts/init-db.js` runs it for a site; tests call it in-process so they can share a database
|
|
86
|
+
with the code under test (an in-memory PGlite cannot be reached from a child process).
|
|
87
|
+
*/
|
|
88
|
+
export default async ({ log = console.log } = {}) => {
|
|
89
|
+
log('=== Seeding Permissions ===\n');
|
|
90
|
+
|
|
91
|
+
for(const perm of DEFAULT_PERMISSIONS) {
|
|
92
|
+
const existing = await db.select()
|
|
93
|
+
.from(permission)
|
|
94
|
+
.where(eq(permission.name, perm.name))
|
|
95
|
+
.limit(1);
|
|
96
|
+
|
|
97
|
+
if(existing.length === 0) {
|
|
98
|
+
await db.insert(permission).values({
|
|
99
|
+
name: perm.name,
|
|
100
|
+
description: perm.description,
|
|
101
|
+
owner: 'system',
|
|
102
|
+
createdAt: new Date()
|
|
103
|
+
});
|
|
104
|
+
log(`✓ Created permission: ${perm.name}`);
|
|
105
|
+
} else {
|
|
106
|
+
log(`- Permission exists: ${perm.name}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
log('\n=== Creating Groups ===\n');
|
|
111
|
+
|
|
112
|
+
const allPermissions = await db.select().from(permission);
|
|
113
|
+
const permissionMap = Object.fromEntries(
|
|
114
|
+
allPermissions.map(p => [p.name, p.name])
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
for(const [groupName, groupData] of Object.entries(DEFAULT_GROUPS)) {
|
|
118
|
+
let groupRecord = await db.select()
|
|
119
|
+
.from(group)
|
|
120
|
+
.where(eq(group.name, groupName))
|
|
121
|
+
.limit(1);
|
|
122
|
+
|
|
123
|
+
if(groupRecord.length === 0) {
|
|
124
|
+
groupRecord = await db.insert(group).values({
|
|
125
|
+
name: groupName,
|
|
126
|
+
description: groupData.description,
|
|
127
|
+
owner: 'system',
|
|
128
|
+
createdAt: new Date()
|
|
129
|
+
}).returning();
|
|
130
|
+
log(`✓ Created group: ${groupName}`);
|
|
131
|
+
} else {
|
|
132
|
+
log(`- Group exists: ${groupName}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if(groupName === 'system:Administrators') {
|
|
136
|
+
log(` Skipping permission assignments (admin has all permissions)`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
log(` Assigning permissions to ${groupName}:`);
|
|
141
|
+
|
|
142
|
+
for(const permName of groupData.permissions) {
|
|
143
|
+
if(!permissionMap[permName]) {
|
|
144
|
+
log(` ⚠ Permission not found: ${permName}`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const existing = await db.select()
|
|
149
|
+
.from(groupPermission)
|
|
150
|
+
.where(and(
|
|
151
|
+
eq(groupPermission.groupName, groupName),
|
|
152
|
+
eq(groupPermission.permissionName, permName)
|
|
153
|
+
))
|
|
154
|
+
.limit(1);
|
|
155
|
+
|
|
156
|
+
if(existing.length === 0) {
|
|
157
|
+
await db.insert(groupPermission).values({
|
|
158
|
+
id: crypto.randomUUID(),
|
|
159
|
+
groupName: groupName,
|
|
160
|
+
permissionName: permName,
|
|
161
|
+
createdAt: new Date()
|
|
162
|
+
});
|
|
163
|
+
log(` ✓ ${permName}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
log('\n=== Creating Default Settings ===\n');
|
|
169
|
+
|
|
170
|
+
const DEFAULT_SETTINGS = [
|
|
171
|
+
{ owner: 'system', name: 'site_name', value: 'Kempo Site', type: 'string', isPublic: true, description: 'The name of the website' },
|
|
172
|
+
{ owner: 'system', name: 'session_duration_days', value: 7, type: 'number', isPublic: false, description: 'Session duration in days' },
|
|
173
|
+
{ owner: 'system', name: 'allow_registration', value: true, type: 'boolean', isPublic: true, description: 'Allow new users to register' },
|
|
174
|
+
{ owner: 'system', name: 'require_email_verification', value: false, type: 'boolean', isPublic: false, description: 'Require email verification before account access' },
|
|
175
|
+
{ owner: 'system', name: 'verification_url', value: 'http://localhost:3000/verify-email/{{token}}', type: 'string', isPublic: false, description: 'Email verification URL template' },
|
|
176
|
+
{ owner: 'system', name: 'notification_retention_days', value: 90, type: 'number', isPublic: false, description: 'Days to keep notifications before they are deleted (0 keeps them forever)' },
|
|
177
|
+
{ owner: 'system', name: 'password_reset_url', value: 'http://localhost:3000/reset-password/{{token}}', type: 'string', isPublic: false, description: 'Password reset URL template' },
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
const { default: setSetting } = await import('../settings/setSetting.js');
|
|
181
|
+
for(const s of DEFAULT_SETTINGS){
|
|
182
|
+
await setSetting(s.owner, s.name, s.value, s.type, s.isPublic, s.description);
|
|
183
|
+
log(`✓ Created setting: ${s.owner}:${s.name} = ${s.value}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
log('\n=== Updating Existing Records ===\n');
|
|
187
|
+
|
|
188
|
+
const updatedPerms = await db.update(permission)
|
|
189
|
+
.set({ owner: 'system' })
|
|
190
|
+
.where(sql`name LIKE 'system:%'`)
|
|
191
|
+
.returning();
|
|
192
|
+
log(`✓ Updated ${updatedPerms.length} permissions to owner='system'`);
|
|
193
|
+
|
|
194
|
+
const updatedGroups = await db.update(group)
|
|
195
|
+
.set({ owner: 'system' })
|
|
196
|
+
.where(sql`name LIKE 'system:%'`)
|
|
197
|
+
.returning();
|
|
198
|
+
log(`✓ Updated ${updatedGroups.length} groups to owner='system'`);
|
|
199
|
+
|
|
200
|
+
log('\nCreating database triggers to protect system resources...');
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
await db.execute(sql.raw('DROP TRIGGER IF EXISTS protect_system_settings ON setting'));
|
|
204
|
+
await runScript(createSettingProtectionTrigger());
|
|
205
|
+
log('✓ System settings protection trigger created');
|
|
206
|
+
} catch(e) {
|
|
207
|
+
log('- Settings trigger already exists');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
await db.execute(sql.raw('DROP TRIGGER IF EXISTS protect_system_groups ON "group"'));
|
|
212
|
+
await runScript(createGroupProtectionTrigger());
|
|
213
|
+
log('✓ System groups protection trigger created');
|
|
214
|
+
} catch(e) {
|
|
215
|
+
log('- Groups trigger already exists');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
await db.execute(sql.raw('DROP TRIGGER IF EXISTS protect_system_permissions ON permission'));
|
|
220
|
+
await runScript(createPermissionProtectionTrigger());
|
|
221
|
+
log('✓ System permissions protection trigger created');
|
|
222
|
+
} catch(e) {
|
|
223
|
+
log('- Permissions trigger already exists');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
log('\n=== Group & Permission Seeding Complete ===');
|
|
227
|
+
};
|