kempo 4.4.1 → 4.4.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/README.md +7 -1
- package/app-public/setup/api/GET.js +23 -0
- package/app-public/setup/api/POST.js +28 -0
- package/app-public/setup/index.page.html +59 -0
- package/bin/cli.js +37 -20
- package/package.json +1 -1
- package/server/utils/setup/completeSetup.js +49 -0
- package/server/utils/setup/hasAdmin.js +12 -0
- package/server/utils/setup/removeSetupFiles.js +24 -0
package/README.md
CHANGED
|
@@ -43,7 +43,12 @@ Your site is running at `http://localhost:3000`.
|
|
|
43
43
|
|
|
44
44
|
### Create an Admin User
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
`npx kempo init` asks how to create the first admin:
|
|
47
|
+
|
|
48
|
+
- **In the terminal** - the wizard prompts for a name, email and password.
|
|
49
|
+
- **In the browser** - visit `/setup` on your new site and fill in the form. The setup page and its API route are deleted from `public/setup/` as soon as the admin is created, and they refuse to run (and delete themselves) if an administrator already exists, so they can only be used once.
|
|
50
|
+
|
|
51
|
+
To promote an existing user instead, register through the UI at `/register`, then run:
|
|
47
52
|
|
|
48
53
|
```bash
|
|
49
54
|
node node_modules/kempo/scripts/make-admin.js
|
|
@@ -64,6 +69,7 @@ Kempo scaffolds these pages into your `public/` directory. All are fully functio
|
|
|
64
69
|
| `/` | Home page |
|
|
65
70
|
| `/login` | Login form |
|
|
66
71
|
| `/register` | Registration form |
|
|
72
|
+
| `/setup` | First-run admin setup (deletes itself once an admin exists) |
|
|
67
73
|
| `/forgot-password` | Password recovery |
|
|
68
74
|
| `/reset-password/[token]` | Password reset (from email link) |
|
|
69
75
|
| `/verify-email` | Email verification notice |
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { dirname, join } from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import hasAdmin from 'kempo/server/utils/setup/hasAdmin.js';
|
|
4
|
+
import removeSetupFiles from 'kempo/server/utils/setup/removeSetupFiles.js';
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
Tells the setup page whether it is still needed. If an administrator was created some other way
|
|
8
|
+
(init, makeAdmin) the setup files are deleted here, so they never outlive their purpose.
|
|
9
|
+
*/
|
|
10
|
+
export default async (request, response) => {
|
|
11
|
+
const [error, adminExists] = await hasAdmin();
|
|
12
|
+
|
|
13
|
+
if(error){
|
|
14
|
+
return response.status(error.code).json({ error: error.msg });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if(adminExists){
|
|
18
|
+
await removeSetupFiles({ setupDir: join(dirname(fileURLToPath(import.meta.url)), '..') });
|
|
19
|
+
return response.status(404).json({ error: 'Setup is already complete' });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
response.json({ available: true });
|
|
23
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { dirname, join } from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import completeSetup from 'kempo/server/utils/setup/completeSetup.js';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Creates the first administrator and then deletes the whole setup directory, this route included.
|
|
7
|
+
It only ever works once: completeSetup refuses when an administrator already exists.
|
|
8
|
+
*/
|
|
9
|
+
export default async (request, response) => {
|
|
10
|
+
try {
|
|
11
|
+
const { name, email, password } = request.body || {};
|
|
12
|
+
const [error, result] = await completeSetup({
|
|
13
|
+
name,
|
|
14
|
+
email,
|
|
15
|
+
password,
|
|
16
|
+
setupDir: join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if(error){
|
|
20
|
+
return response.status(error.code).json({ error: error.msg });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
response.json({ user: result.user });
|
|
24
|
+
} catch(error){
|
|
25
|
+
console.error('Setup error:', error);
|
|
26
|
+
response.status(500).json({ error: 'Internal server error' });
|
|
27
|
+
}
|
|
28
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
owner: system
|
|
3
|
+
title: Setup
|
|
4
|
+
description: First-run page that creates the initial administrator and then deletes itself
|
|
5
|
+
-->
|
|
6
|
+
<page title="Set up your site">
|
|
7
|
+
<content>
|
|
8
|
+
<main class="p">
|
|
9
|
+
<div>
|
|
10
|
+
<h2>Welcome to Kempo</h2>
|
|
11
|
+
<p>Create the administrator account for your site. This page deletes itself once you are done.</p>
|
|
12
|
+
<form id="setupForm">
|
|
13
|
+
<div class="mb">
|
|
14
|
+
<label for="name" class="d-b mb-sm">Name</label>
|
|
15
|
+
<input type="text" id="name" name="name" required class="w-full p-sm br">
|
|
16
|
+
</div>
|
|
17
|
+
<div class="mb">
|
|
18
|
+
<label for="email" class="d-b mb-sm">Email</label>
|
|
19
|
+
<input type="email" id="email" name="email" required class="w-full p-sm br">
|
|
20
|
+
</div>
|
|
21
|
+
<div class="mb">
|
|
22
|
+
<label for="password" class="d-b mb-sm">Password</label>
|
|
23
|
+
<input type="password" id="password" name="password" minlength="8" required class="w-full p-sm br">
|
|
24
|
+
</div>
|
|
25
|
+
<button type="submit" class="w-full p-sm br">Create administrator</button>
|
|
26
|
+
</form>
|
|
27
|
+
</div>
|
|
28
|
+
</main>
|
|
29
|
+
</content>
|
|
30
|
+
<content location="scripts">
|
|
31
|
+
<script type="module">
|
|
32
|
+
import Toast from '{{pathToRoot}}kempo-ui/components/Toast.js';
|
|
33
|
+
|
|
34
|
+
const status = await fetch('{{pathToRoot}}setup/api');
|
|
35
|
+
if(!status.ok){
|
|
36
|
+
window.location.href = '{{pathToRoot}}';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
document.getElementById('setupForm').addEventListener('submit', async (e) => {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
const response = await fetch('{{pathToRoot}}setup/api', {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
name: document.getElementById('name').value,
|
|
46
|
+
email: document.getElementById('email').value,
|
|
47
|
+
password: document.getElementById('password').value
|
|
48
|
+
})
|
|
49
|
+
});
|
|
50
|
+
const body = await response.json();
|
|
51
|
+
if(!response.ok){
|
|
52
|
+
Toast.error(body.error);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
window.location.href = '{{pathToRoot}}login';
|
|
56
|
+
});
|
|
57
|
+
</script>
|
|
58
|
+
</content>
|
|
59
|
+
</page>
|
package/bin/cli.js
CHANGED
|
@@ -82,6 +82,7 @@ const printManualSteps = (databaseUrl) => {
|
|
|
82
82
|
console.log(' docker compose up -d');
|
|
83
83
|
console.log(' npx drizzle-kit push');
|
|
84
84
|
console.log(' node node_modules/kempo/scripts/init-db.js');
|
|
85
|
+
console.log(' Then create your admin at http://localhost:9876/setup, or register and run:');
|
|
85
86
|
console.log(` DATABASE_URL="${databaseUrl}" node node_modules/kempo/scripts/make-admin.js`);
|
|
86
87
|
console.log(' npm run dev\n');
|
|
87
88
|
};
|
|
@@ -222,7 +223,19 @@ const databaseUrl = customDatabaseUrl || `postgresql://kempo:${dbPassword}@local
|
|
|
222
223
|
const generatedAdminPassword = generatePassword();
|
|
223
224
|
let adminName, adminEmail, adminPassword;
|
|
224
225
|
|
|
225
|
-
|
|
226
|
+
const adminSetupChoice = useDefaults ? 'terminal' : (await inquirer.prompt([{
|
|
227
|
+
type: 'select',
|
|
228
|
+
name: 'adminSetup',
|
|
229
|
+
message: 'How would you like to create the first admin user?',
|
|
230
|
+
choices: [
|
|
231
|
+
{ name: 'Here in the terminal', value: 'terminal' },
|
|
232
|
+
{ name: 'In the browser (a /setup page that deletes itself once used)', value: 'browser' }
|
|
233
|
+
]
|
|
234
|
+
}])).adminSetup;
|
|
235
|
+
|
|
236
|
+
if(adminSetupChoice === 'browser'){
|
|
237
|
+
// The admin is created on the /setup page instead
|
|
238
|
+
} else if(useDefaults){
|
|
226
239
|
adminName = 'Admin';
|
|
227
240
|
adminEmail = 'admin@example.com';
|
|
228
241
|
adminPassword = generatedAdminPassword;
|
|
@@ -245,7 +258,8 @@ if(useDefaults){
|
|
|
245
258
|
type: 'password',
|
|
246
259
|
name: 'adminPassword',
|
|
247
260
|
message: 'Admin user password (leave blank to generate one):',
|
|
248
|
-
mask: '*'
|
|
261
|
+
mask: '*',
|
|
262
|
+
validate: v => !v || v.length >= 8 || 'Password must be at least 8 characters'
|
|
249
263
|
}
|
|
250
264
|
]);
|
|
251
265
|
adminName = adminAnswers.adminName;
|
|
@@ -467,26 +481,26 @@ execSync('node node_modules/kempo/scripts/init-db.js', { cwd: projectDir, stdio:
|
|
|
467
481
|
Step 8: Create admin user
|
|
468
482
|
*/
|
|
469
483
|
|
|
470
|
-
console.log('\n--- Creating admin user ---\n');
|
|
471
|
-
|
|
472
484
|
process.env.DATABASE_URL = databaseUrl;
|
|
473
485
|
|
|
474
|
-
|
|
475
|
-
|
|
486
|
+
if(adminSetupChoice === 'terminal'){
|
|
487
|
+
console.log('\n--- Creating admin user ---\n');
|
|
476
488
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
});
|
|
489
|
+
/*
|
|
490
|
+
The same routine the /setup page uses: it creates the admin and then deletes public/setup/, so
|
|
491
|
+
this path leaves no setup page behind. If it fails no admin exists, so the setup page stays.
|
|
492
|
+
*/
|
|
493
|
+
const { default: completeSetup } = await import(pathToFileURL(join(moduleRoot, 'server', 'utils', 'setup', 'completeSetup.js')).href);
|
|
483
494
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
495
|
+
const [setupError] = await completeSetup({
|
|
496
|
+
name: adminName,
|
|
497
|
+
email: adminEmail,
|
|
498
|
+
password: adminPassword,
|
|
499
|
+
setupDir: join(projectDir, 'public', 'setup')
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
if(setupError){
|
|
503
|
+
console.error(`Failed to create admin user: ${setupError.msg}`);
|
|
490
504
|
} else {
|
|
491
505
|
console.log(`Admin user created: ${adminEmail}`);
|
|
492
506
|
}
|
|
@@ -501,10 +515,13 @@ console.log(`
|
|
|
501
515
|
║ Kempo setup complete! ║
|
|
502
516
|
╚══════════════════════════════════════════╝
|
|
503
517
|
|
|
504
|
-
|
|
518
|
+
${adminSetupChoice === 'browser' ? `Create your admin user in the browser:
|
|
519
|
+
http://localhost:9876/setup
|
|
520
|
+
(the setup page deletes itself once you finish)
|
|
521
|
+
` : `Admin credentials:
|
|
505
522
|
Email: ${adminEmail}
|
|
506
523
|
Password: ${adminPassword}
|
|
507
|
-
${emailServiceSkipped ? '\n Note: Email is not configured. Update RESEND_API_KEY in .env when ready.\n' : ''}
|
|
524
|
+
`}${emailServiceSkipped ? '\n Note: Email is not configured. Update RESEND_API_KEY in .env when ready.\n' : ''}
|
|
508
525
|
Next steps:
|
|
509
526
|
npm run dev
|
|
510
527
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import createUser from '../users/createUser.js';
|
|
2
|
+
import addUserToGroup from '../groups/addUserToGroup.js';
|
|
3
|
+
import hasAdmin from './hasAdmin.js';
|
|
4
|
+
import removeSetupFiles from './removeSetupFiles.js';
|
|
5
|
+
|
|
6
|
+
let running = false;
|
|
7
|
+
|
|
8
|
+
export default async ({ name, email, password, setupDir }) => {
|
|
9
|
+
if(!password || password.length < 8){
|
|
10
|
+
return [{ code: 400, msg: 'Password must be at least 8 characters' }, null];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
The admin check and the user insert are not one transaction, so two simultaneous requests on a
|
|
15
|
+
fresh install could both pass the check. Setup runs once per install, so a process-level lock
|
|
16
|
+
is enough to close that.
|
|
17
|
+
*/
|
|
18
|
+
if(running){
|
|
19
|
+
return [{ code: 409, msg: 'Setup is already in progress' }, null];
|
|
20
|
+
}
|
|
21
|
+
running = true;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const [checkError, adminExists] = await hasAdmin();
|
|
25
|
+
if(checkError){
|
|
26
|
+
return [checkError, null];
|
|
27
|
+
}
|
|
28
|
+
if(adminExists){
|
|
29
|
+
await removeSetupFiles({ setupDir });
|
|
30
|
+
return [{ code: 404, msg: 'Setup is already complete' }, null];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const [createError, created] = await createUser({ name, email, password, emailVerified: true });
|
|
34
|
+
if(createError){
|
|
35
|
+
return [createError, null];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const [groupError] = await addUserToGroup(created.user.id, 'system:Administrators');
|
|
39
|
+
if(groupError){
|
|
40
|
+
return [groupError, null];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const [removeError] = await removeSetupFiles({ setupDir });
|
|
44
|
+
|
|
45
|
+
return [null, { user: created.user, setupFilesRemoved: !removeError }];
|
|
46
|
+
} finally {
|
|
47
|
+
running = false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import db from '../../db/index.js';
|
|
2
|
+
import { userGroup } from '../../db/schema.js';
|
|
3
|
+
import { eq } from 'drizzle-orm';
|
|
4
|
+
|
|
5
|
+
export default async () => {
|
|
6
|
+
try {
|
|
7
|
+
const [found] = await db.select().from(userGroup).where(eq(userGroup.groupName, 'system:Administrators')).limit(1);
|
|
8
|
+
return [null, !!found];
|
|
9
|
+
} catch(error){
|
|
10
|
+
return [{ code: 500, msg: 'Failed to check for an administrator' }, null];
|
|
11
|
+
}
|
|
12
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { rm } from 'fs/promises';
|
|
2
|
+
|
|
3
|
+
export default async ({ setupDir }) => {
|
|
4
|
+
if(!setupDir){
|
|
5
|
+
return [{ code: 400, msg: 'Setup directory is required' }, null];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
await rm(setupDir, { recursive: true, force: true });
|
|
10
|
+
} catch(error){
|
|
11
|
+
return [{ code: 500, msg: 'Failed to remove the setup files' }, null];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
A running server only learns the files are gone on its next scan; without this the deleted
|
|
16
|
+
page and route would keep answering until the process restarts.
|
|
17
|
+
*/
|
|
18
|
+
try {
|
|
19
|
+
const { default: rescan } = await import('kempo-server/rescan');
|
|
20
|
+
await rescan();
|
|
21
|
+
} catch {}
|
|
22
|
+
|
|
23
|
+
return [null, { removed: true }];
|
|
24
|
+
};
|