javascript-solid-server 0.0.142 → 0.0.144
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/bin/jss.js +58 -31
- package/package.json +1 -1
- package/src/idp/index.js +1 -1
- package/src/idp/views.js +13 -6
- package/test/idp.test.js +53 -0
package/bin/jss.js
CHANGED
|
@@ -13,6 +13,7 @@ import { Command } from 'commander';
|
|
|
13
13
|
import { createServer } from '../src/server.js';
|
|
14
14
|
import { loadConfig, saveConfig, printConfig, defaults } from '../src/config.js';
|
|
15
15
|
import { createInvite, listInvites, revokeInvite } from '../src/idp/invites.js';
|
|
16
|
+
import { findByUsername, updatePassword, deleteAccount } from '../src/idp/accounts.js';
|
|
16
17
|
import { setQuotaLimit, getQuotaInfo, reconcileQuota, formatBytes } from '../src/storage/quota.js';
|
|
17
18
|
import { parseSize } from '../src/config.js';
|
|
18
19
|
import crypto from 'crypto';
|
|
@@ -637,32 +638,12 @@ program
|
|
|
637
638
|
process.env.DATA_ROOT = path.resolve(options.root);
|
|
638
639
|
}
|
|
639
640
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const accountsDir = path.join(dataRoot, '.idp', 'accounts');
|
|
643
|
-
const indexPath = path.join(accountsDir, '_username_index.json');
|
|
644
|
-
|
|
645
|
-
let usernameIndex;
|
|
646
|
-
try {
|
|
647
|
-
usernameIndex = await fs.readJson(indexPath);
|
|
648
|
-
} catch (err) {
|
|
649
|
-
if (err.code === 'ENOENT') {
|
|
650
|
-
console.error(`Error: No accounts found (missing ${indexPath})`);
|
|
651
|
-
process.exit(1);
|
|
652
|
-
}
|
|
653
|
-
throw err;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
const normalizedUsername = username.toLowerCase().trim();
|
|
657
|
-
const accountId = usernameIndex[normalizedUsername];
|
|
658
|
-
if (!accountId) {
|
|
641
|
+
const account = await findByUsername(username);
|
|
642
|
+
if (!account) {
|
|
659
643
|
console.error(`Error: User not found: ${username}`);
|
|
660
644
|
process.exit(1);
|
|
661
645
|
}
|
|
662
646
|
|
|
663
|
-
const accountPath = path.join(accountsDir, `${accountId}.json`);
|
|
664
|
-
const account = await fs.readJson(accountPath);
|
|
665
|
-
|
|
666
647
|
// Determine new password
|
|
667
648
|
let newPassword;
|
|
668
649
|
|
|
@@ -673,8 +654,8 @@ program
|
|
|
673
654
|
} else {
|
|
674
655
|
// Interactive prompt
|
|
675
656
|
newPassword = await promptPassword('New password: ');
|
|
676
|
-
const
|
|
677
|
-
if (newPassword !==
|
|
657
|
+
const confirmation = await promptPassword('Confirm password: ');
|
|
658
|
+
if (newPassword !== confirmation) {
|
|
678
659
|
console.error('Error: Passwords do not match');
|
|
679
660
|
process.exit(1);
|
|
680
661
|
}
|
|
@@ -685,17 +666,63 @@ program
|
|
|
685
666
|
process.exit(1);
|
|
686
667
|
}
|
|
687
668
|
|
|
688
|
-
|
|
689
|
-
const bcrypt = await import('bcryptjs').then(m => m.default);
|
|
690
|
-
account.passwordHash = await bcrypt.hash(newPassword, 10);
|
|
691
|
-
account.passwordChangedAt = new Date().toISOString();
|
|
692
|
-
await fs.writeJson(accountPath, account, { spaces: 2 });
|
|
669
|
+
await updatePassword(account.id, newPassword);
|
|
693
670
|
|
|
694
671
|
if (options.generate) {
|
|
695
|
-
console.log(`\nPassword updated for ${
|
|
672
|
+
console.log(`\nPassword updated for ${account.username}`);
|
|
696
673
|
console.log(`Generated password: ${newPassword}\n`);
|
|
697
674
|
} else {
|
|
698
|
-
console.log(`\nPassword updated for ${
|
|
675
|
+
console.log(`\nPassword updated for ${account.username}\n`);
|
|
676
|
+
}
|
|
677
|
+
} catch (err) {
|
|
678
|
+
console.error(`Error: ${err.message}`);
|
|
679
|
+
process.exit(1);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Account commands - manage user accounts
|
|
685
|
+
*/
|
|
686
|
+
const accountCmd = program
|
|
687
|
+
.command('account')
|
|
688
|
+
.description('Manage user accounts');
|
|
689
|
+
|
|
690
|
+
accountCmd
|
|
691
|
+
.command('delete <username>')
|
|
692
|
+
.description('Delete a user account from the IdP')
|
|
693
|
+
.option('-r, --root <path>', 'Data directory')
|
|
694
|
+
.option('-y, --yes', 'Skip the confirmation prompt')
|
|
695
|
+
.option('--purge', 'Also delete pod data at <dataRoot>/<username>/')
|
|
696
|
+
.action(async (username, options) => {
|
|
697
|
+
try {
|
|
698
|
+
if (options.root) {
|
|
699
|
+
process.env.DATA_ROOT = path.resolve(options.root);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const account = await findByUsername(username);
|
|
703
|
+
if (!account) {
|
|
704
|
+
console.error(`Error: User not found: ${username}`);
|
|
705
|
+
process.exit(1);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (!options.yes) {
|
|
709
|
+
const summary = `Delete account '${account.username}' (${account.webId})${options.purge ? ' AND purge pod data' : ''}?`;
|
|
710
|
+
const ok = await confirm(summary, false);
|
|
711
|
+
if (!ok) {
|
|
712
|
+
console.log('Cancelled.');
|
|
713
|
+
process.exit(0);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
await deleteAccount(account.id);
|
|
718
|
+
|
|
719
|
+
if (options.purge) {
|
|
720
|
+
const dataRoot = process.env.DATA_ROOT || './data';
|
|
721
|
+
const podPath = path.join(dataRoot, account.username);
|
|
722
|
+
await fs.remove(podPath);
|
|
723
|
+
console.log(`\nDeleted account ${account.username}. Pod data removed from ${podPath}.\n`);
|
|
724
|
+
} else {
|
|
725
|
+
console.log(`\nDeleted account ${account.username}. Pod data preserved at <dataRoot>/${account.username}/ (use --purge to remove).\n`);
|
|
699
726
|
}
|
|
700
727
|
} catch (err) {
|
|
701
728
|
console.error(`Error: ${err.message}`);
|
package/package.json
CHANGED
package/src/idp/index.js
CHANGED
|
@@ -183,7 +183,7 @@ export async function idpPlugin(fastify, options) {
|
|
|
183
183
|
// Pairs with the /idp/auth guard above so a human visitor lands here
|
|
184
184
|
// rather than on a raw OIDC error.
|
|
185
185
|
fastify.get('/idp', async (request, reply) => {
|
|
186
|
-
return reply.type('text/html').send(landingPage({ baseUri: issuer }));
|
|
186
|
+
return reply.type('text/html').send(landingPage({ baseUri: issuer, singleUser }));
|
|
187
187
|
});
|
|
188
188
|
|
|
189
189
|
// Token sub-paths
|
package/src/idp/views.js
CHANGED
|
@@ -555,12 +555,15 @@ export function errorPage(title, message) {
|
|
|
555
555
|
*
|
|
556
556
|
* The OIDC authorization endpoint (/idp/auth) requires a client_id; opening
|
|
557
557
|
* /idp manually used to drop the user into a raw OIDC error. This page is
|
|
558
|
-
* the human-navigable entry point
|
|
559
|
-
*
|
|
560
|
-
*
|
|
558
|
+
* the human-navigable entry point.
|
|
559
|
+
*
|
|
560
|
+
* In single-user mode (`ctx.singleUser`) the Create Account button is
|
|
561
|
+
* suppressed — pod creation is disabled and the button would lead to a
|
|
562
|
+
* 403. The sign-in note still names pilot as the example client.
|
|
561
563
|
*/
|
|
562
564
|
export function landingPage(ctx = {}) {
|
|
563
565
|
const issuer = ctx.baseUri || '';
|
|
566
|
+
const singleUser = !!ctx.singleUser;
|
|
564
567
|
return `
|
|
565
568
|
<!DOCTYPE html>
|
|
566
569
|
<html lang="en">
|
|
@@ -607,13 +610,17 @@ export function landingPage(ctx = {}) {
|
|
|
607
610
|
<div class="container landing">
|
|
608
611
|
<div class="landing-header">
|
|
609
612
|
<h1>Solid Pod Server</h1>
|
|
610
|
-
<p class="subtitle"
|
|
613
|
+
<p class="subtitle">${singleUser
|
|
614
|
+
? 'Single-user pod — sign in from any Solid app.'
|
|
615
|
+
: 'Create an account, then sign in from any Solid app.'}</p>
|
|
611
616
|
</div>
|
|
612
617
|
|
|
613
|
-
|
|
618
|
+
${singleUser
|
|
619
|
+
? '' /* Registration is disabled in single-user mode; suppress the dead-end button. */
|
|
620
|
+
: '<a href="/idp/register" class="btn btn-primary" style="text-decoration: none;">Create Account</a>'}
|
|
614
621
|
|
|
615
622
|
<div class="signin-note">
|
|
616
|
-
<strong
|
|
623
|
+
<strong>${singleUser ? 'Sign in' : 'Already have an account?'}</strong> ${singleUser ? 'from' : 'Sign in from'} a Solid app — for example, <a href="https://solid-apps.github.io/pilot/" target="_blank" rel="noopener">pilot</a> is a minimal console you can open right now. Point it at this server and click Sign In.
|
|
617
624
|
</div>
|
|
618
625
|
|
|
619
626
|
${issuer ? `<div class="issuer">Issuer: ${escapeHtml(issuer.replace(/\/$/, ''))}</div>` : ''}
|
package/test/idp.test.js
CHANGED
|
@@ -356,6 +356,59 @@ describe('Identity Provider - Subdomain mode register validation', () => {
|
|
|
356
356
|
});
|
|
357
357
|
});
|
|
358
358
|
|
|
359
|
+
// Single-user mode: registration is disabled, so the /idp landing must
|
|
360
|
+
// suppress the "Create Account" button rather than ship a known 403 trap.
|
|
361
|
+
// Regression coverage for #290.
|
|
362
|
+
describe('Identity Provider - Single-user mode landing', () => {
|
|
363
|
+
let server;
|
|
364
|
+
let baseUrl;
|
|
365
|
+
const SINGLE_USER_DATA_DIR = './test-data-idp-single-user';
|
|
366
|
+
|
|
367
|
+
before(async () => {
|
|
368
|
+
await fs.remove(SINGLE_USER_DATA_DIR);
|
|
369
|
+
await fs.ensureDir(SINGLE_USER_DATA_DIR);
|
|
370
|
+
|
|
371
|
+
const port = await getAvailablePort();
|
|
372
|
+
baseUrl = `http://${TEST_HOST}:${port}`;
|
|
373
|
+
|
|
374
|
+
server = createServer({
|
|
375
|
+
logger: false,
|
|
376
|
+
root: SINGLE_USER_DATA_DIR,
|
|
377
|
+
idp: true,
|
|
378
|
+
idpIssuer: baseUrl,
|
|
379
|
+
singleUser: true,
|
|
380
|
+
singleUserName: 'me',
|
|
381
|
+
forceCloseConnections: true,
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
await server.listen({ port, host: TEST_HOST });
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
after(async () => {
|
|
388
|
+
await server.close();
|
|
389
|
+
await fs.remove(SINGLE_USER_DATA_DIR);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('GET /idp omits the Create Account button', async () => {
|
|
393
|
+
const res = await fetch(`${baseUrl}/idp`);
|
|
394
|
+
assert.strictEqual(res.status, 200);
|
|
395
|
+
const body = await res.text();
|
|
396
|
+
// Sanity: the landing still rendered.
|
|
397
|
+
assert.match(body, /Solid Pod Server/);
|
|
398
|
+
// Button + register link must be absent — those would 403 in single-user mode.
|
|
399
|
+
assert.doesNotMatch(body, /Create Account/);
|
|
400
|
+
assert.doesNotMatch(body, /href="\/idp\/register"/);
|
|
401
|
+
// Sign-in note should still mention pilot as the example client.
|
|
402
|
+
assert.match(body, /solid-apps\.github\.io\/pilot/);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('subtitle reflects the single-user shape', async () => {
|
|
406
|
+
const res = await fetch(`${baseUrl}/idp`);
|
|
407
|
+
const body = await res.text();
|
|
408
|
+
assert.match(body, /Single-user pod/);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
359
412
|
describe('Identity Provider - Accounts', () => {
|
|
360
413
|
let server;
|
|
361
414
|
let accountsUrl;
|