javascript-solid-server 0.0.11 → 0.0.12
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/.claude/settings.local.json +6 -1
- package/README.md +46 -4
- package/bin/jss.js +22 -4
- package/package.json +5 -2
- package/src/config.js +7 -0
- package/src/handlers/container.js +35 -2
- package/src/idp/accounts.js +258 -0
- package/src/idp/adapter.js +204 -0
- package/src/idp/index.js +118 -0
- package/src/idp/interactions.js +180 -0
- package/src/idp/keys.js +157 -0
- package/src/idp/provider.js +246 -0
- package/src/idp/views.js +295 -0
- package/src/server.js +18 -2
- package/test/idp.test.js +258 -0
|
@@ -20,7 +20,12 @@
|
|
|
20
20
|
"WebFetch(domain:solid-contrib.github.io)",
|
|
21
21
|
"Bash(git clone:*)",
|
|
22
22
|
"Bash(chmod:*)",
|
|
23
|
-
"Bash(JSS_PORT=4000 JSS_CONNEG=true node bin/jss.js:*)"
|
|
23
|
+
"Bash(JSS_PORT=4000 JSS_CONNEG=true node bin/jss.js:*)",
|
|
24
|
+
"Bash(find:*)",
|
|
25
|
+
"Bash(timeout 5 node:*)",
|
|
26
|
+
"Bash(npm view:*)",
|
|
27
|
+
"Bash(npm ls:*)",
|
|
28
|
+
"Bash(timeout 10 node:*)"
|
|
24
29
|
]
|
|
25
30
|
}
|
|
26
31
|
}
|
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ npm run benchmark
|
|
|
54
54
|
|
|
55
55
|
## Features
|
|
56
56
|
|
|
57
|
-
### Implemented (v0.0.
|
|
57
|
+
### Implemented (v0.0.12)
|
|
58
58
|
|
|
59
59
|
- **LDP CRUD Operations** - GET, PUT, POST, DELETE, HEAD
|
|
60
60
|
- **N3 Patch** - Solid's native patch format for RDF updates
|
|
@@ -67,6 +67,7 @@ npm run benchmark
|
|
|
67
67
|
- **Multi-user Pods** - Create pods at `/<username>/`
|
|
68
68
|
- **WebID Profiles** - JSON-LD structured data in HTML at pod root
|
|
69
69
|
- **Web Access Control (WAC)** - `.acl` file-based authorization
|
|
70
|
+
- **Solid-OIDC Identity Provider** - Built-in IdP with DPoP, dynamic registration
|
|
70
71
|
- **Solid-OIDC Resource Server** - Accept DPoP-bound access tokens from external IdPs
|
|
71
72
|
- **Simple Auth Tokens** - Built-in token authentication for development
|
|
72
73
|
- **Content Negotiation** - Optional Turtle <-> JSON-LD conversion
|
|
@@ -132,6 +133,8 @@ jss --help # Show help
|
|
|
132
133
|
| `--ssl-cert <path>` | SSL certificate (PEM) | - |
|
|
133
134
|
| `--conneg` | Enable Turtle support | false |
|
|
134
135
|
| `--notifications` | Enable WebSocket | false |
|
|
136
|
+
| `--idp` | Enable built-in IdP | false |
|
|
137
|
+
| `--idp-issuer <url>` | IdP issuer URL | (auto) |
|
|
135
138
|
| `-q, --quiet` | Suppress logs | false |
|
|
136
139
|
|
|
137
140
|
### Environment Variables
|
|
@@ -274,9 +277,38 @@ Use the token returned from pod creation:
|
|
|
274
277
|
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3000/alice/private/
|
|
275
278
|
```
|
|
276
279
|
|
|
277
|
-
###
|
|
280
|
+
### Built-in Identity Provider (v0.0.12+)
|
|
278
281
|
|
|
279
|
-
|
|
282
|
+
Enable the built-in Solid-OIDC Identity Provider:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
jss start --idp
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
With IdP enabled, pod creation requires email and password:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
curl -X POST http://localhost:3000/.pods \
|
|
292
|
+
-H "Content-Type: application/json" \
|
|
293
|
+
-d '{"name": "alice", "email": "alice@example.com", "password": "secret123"}'
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Response:
|
|
297
|
+
```json
|
|
298
|
+
{
|
|
299
|
+
"name": "alice",
|
|
300
|
+
"webId": "http://localhost:3000/alice/#me",
|
|
301
|
+
"podUri": "http://localhost:3000/alice/",
|
|
302
|
+
"idpIssuer": "http://localhost:3000",
|
|
303
|
+
"loginUrl": "http://localhost:3000/idp/auth"
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
OIDC Discovery: `/.well-known/openid-configuration`
|
|
308
|
+
|
|
309
|
+
### Solid-OIDC (External IdP)
|
|
310
|
+
|
|
311
|
+
The server also accepts DPoP-bound access tokens from external Solid identity providers:
|
|
280
312
|
|
|
281
313
|
```bash
|
|
282
314
|
curl -H "Authorization: DPoP ACCESS_TOKEN" \
|
|
@@ -323,7 +355,7 @@ Server: pub http://localhost:3000/alice/public/data.json (on change)
|
|
|
323
355
|
npm test
|
|
324
356
|
```
|
|
325
357
|
|
|
326
|
-
Currently passing: **
|
|
358
|
+
Currently passing: **174 tests** (including 27 conformance tests)
|
|
327
359
|
|
|
328
360
|
## Project Structure
|
|
329
361
|
|
|
@@ -355,6 +387,14 @@ src/
|
|
|
355
387
|
│ ├── index.js # WebSocket plugin
|
|
356
388
|
│ ├── events.js # Event emitter
|
|
357
389
|
│ └── websocket.js # solid-0.1 protocol
|
|
390
|
+
├── idp/
|
|
391
|
+
│ ├── index.js # Identity Provider plugin
|
|
392
|
+
│ ├── provider.js # oidc-provider config
|
|
393
|
+
│ ├── adapter.js # Filesystem adapter
|
|
394
|
+
│ ├── accounts.js # User account management
|
|
395
|
+
│ ├── keys.js # JWKS key management
|
|
396
|
+
│ ├── interactions.js # Login/consent handlers
|
|
397
|
+
│ └── views.js # HTML templates
|
|
358
398
|
├── rdf/
|
|
359
399
|
│ ├── turtle.js # Turtle <-> JSON-LD
|
|
360
400
|
│ └── conneg.js # Content negotiation
|
|
@@ -372,6 +412,8 @@ Minimal dependencies for a fast, secure server:
|
|
|
372
412
|
- **fs-extra** - Enhanced file operations
|
|
373
413
|
- **jose** - JWT/JWK handling for Solid-OIDC
|
|
374
414
|
- **n3** - Turtle parsing (only used when conneg enabled)
|
|
415
|
+
- **oidc-provider** - OpenID Connect Identity Provider (only when IdP enabled)
|
|
416
|
+
- **bcrypt** - Password hashing (only when IdP enabled)
|
|
375
417
|
|
|
376
418
|
## License
|
|
377
419
|
|
package/bin/jss.js
CHANGED
|
@@ -44,6 +44,9 @@ program
|
|
|
44
44
|
.option('--no-conneg', 'Disable content negotiation')
|
|
45
45
|
.option('--notifications', 'Enable WebSocket notifications')
|
|
46
46
|
.option('--no-notifications', 'Disable WebSocket notifications')
|
|
47
|
+
.option('--idp', 'Enable built-in Identity Provider')
|
|
48
|
+
.option('--no-idp', 'Disable built-in Identity Provider')
|
|
49
|
+
.option('--idp-issuer <url>', 'IdP issuer URL (defaults to server URL)')
|
|
47
50
|
.option('-q, --quiet', 'Suppress log output')
|
|
48
51
|
.option('--print-config', 'Print configuration and exit')
|
|
49
52
|
.action(async (options) => {
|
|
@@ -55,11 +58,19 @@ program
|
|
|
55
58
|
process.exit(0);
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
// Determine IdP issuer URL
|
|
62
|
+
const protocol = config.ssl ? 'https' : 'http';
|
|
63
|
+
const serverHost = config.host === '0.0.0.0' ? 'localhost' : config.host;
|
|
64
|
+
const baseUrl = `${protocol}://${serverHost}:${config.port}`;
|
|
65
|
+
const idpIssuer = config.idpIssuer || baseUrl;
|
|
66
|
+
|
|
58
67
|
// Create and start server
|
|
59
68
|
const server = createServer({
|
|
60
69
|
logger: config.logger,
|
|
61
70
|
conneg: config.conneg,
|
|
62
71
|
notifications: config.notifications,
|
|
72
|
+
idp: config.idp,
|
|
73
|
+
idpIssuer: idpIssuer,
|
|
63
74
|
ssl: config.ssl ? {
|
|
64
75
|
key: await fs.readFile(config.sslKey),
|
|
65
76
|
cert: await fs.readFile(config.sslCert),
|
|
@@ -69,16 +80,14 @@ program
|
|
|
69
80
|
|
|
70
81
|
await server.listen({ port: config.port, host: config.host });
|
|
71
82
|
|
|
72
|
-
const protocol = config.ssl ? 'https' : 'http';
|
|
73
|
-
const address = config.host === '0.0.0.0' ? 'localhost' : config.host;
|
|
74
|
-
|
|
75
83
|
if (!config.quiet) {
|
|
76
84
|
console.log(`\n JavaScript Solid Server v${pkg.version}`);
|
|
77
|
-
console.log(` ${
|
|
85
|
+
console.log(` ${baseUrl}/`);
|
|
78
86
|
console.log(`\n Data: ${path.resolve(config.root)}`);
|
|
79
87
|
if (config.ssl) console.log(' SSL: enabled');
|
|
80
88
|
if (config.conneg) console.log(' Conneg: enabled');
|
|
81
89
|
if (config.notifications) console.log(' WebSocket: enabled');
|
|
90
|
+
if (config.idp) console.log(` IdP: ${idpIssuer}`);
|
|
82
91
|
console.log('\n Press Ctrl+C to stop\n');
|
|
83
92
|
}
|
|
84
93
|
|
|
@@ -142,6 +151,15 @@ program
|
|
|
142
151
|
config.sslCert = await prompt('SSL certificate path', './ssl/cert.pem');
|
|
143
152
|
}
|
|
144
153
|
|
|
154
|
+
// Ask about IdP
|
|
155
|
+
config.idp = await confirm('Enable built-in Identity Provider?', false);
|
|
156
|
+
if (config.idp) {
|
|
157
|
+
const customIssuer = await confirm('Use custom issuer URL?', false);
|
|
158
|
+
if (customIssuer) {
|
|
159
|
+
config.idpIssuer = await prompt('IdP issuer URL', 'https://example.com');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
145
163
|
console.log('');
|
|
146
164
|
}
|
|
147
165
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "javascript-solid-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "A minimal, fast Solid server",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -22,12 +22,15 @@
|
|
|
22
22
|
"benchmark": "node benchmark.js"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
+
"@fastify/middie": "^8.3.3",
|
|
25
26
|
"@fastify/websocket": "^8.3.1",
|
|
27
|
+
"bcrypt": "^6.0.0",
|
|
26
28
|
"commander": "^14.0.2",
|
|
27
29
|
"fastify": "^4.25.2",
|
|
28
30
|
"fs-extra": "^11.2.0",
|
|
29
31
|
"jose": "^6.1.3",
|
|
30
|
-
"n3": "^1.26.0"
|
|
32
|
+
"n3": "^1.26.0",
|
|
33
|
+
"oidc-provider": "^9.6.0"
|
|
31
34
|
},
|
|
32
35
|
"engines": {
|
|
33
36
|
"node": ">=18.0.0"
|
package/src/config.js
CHANGED
|
@@ -29,6 +29,10 @@ export const defaults = {
|
|
|
29
29
|
conneg: false,
|
|
30
30
|
notifications: false,
|
|
31
31
|
|
|
32
|
+
// Identity Provider
|
|
33
|
+
idp: false,
|
|
34
|
+
idpIssuer: null,
|
|
35
|
+
|
|
32
36
|
// Logging
|
|
33
37
|
logger: true,
|
|
34
38
|
quiet: false,
|
|
@@ -51,6 +55,8 @@ const envMap = {
|
|
|
51
55
|
JSS_NOTIFICATIONS: 'notifications',
|
|
52
56
|
JSS_QUIET: 'quiet',
|
|
53
57
|
JSS_CONFIG_PATH: 'configPath',
|
|
58
|
+
JSS_IDP: 'idp',
|
|
59
|
+
JSS_IDP_ISSUER: 'idpIssuer',
|
|
54
60
|
};
|
|
55
61
|
|
|
56
62
|
/**
|
|
@@ -181,5 +187,6 @@ export function printConfig(config) {
|
|
|
181
187
|
console.log(` Multi-user: ${config.multiuser}`);
|
|
182
188
|
console.log(` Conneg: ${config.conneg}`);
|
|
183
189
|
console.log(` Notifications: ${config.notifications}`);
|
|
190
|
+
console.log(` IdP: ${config.idp ? (config.idpIssuer || 'enabled') : 'disabled'}`);
|
|
184
191
|
console.log('─'.repeat(40));
|
|
185
192
|
}
|
|
@@ -110,6 +110,7 @@ export async function handlePost(request, reply) {
|
|
|
110
110
|
/**
|
|
111
111
|
* Create a pod (container) for a user
|
|
112
112
|
* POST /.pods with { "name": "alice" }
|
|
113
|
+
* With IdP enabled: { "name": "alice", "email": "alice@example.com", "password": "secret" }
|
|
113
114
|
*
|
|
114
115
|
* Creates the following structure:
|
|
115
116
|
* /{name}/
|
|
@@ -122,12 +123,23 @@ export async function handlePost(request, reply) {
|
|
|
122
123
|
* /{name}/settings/privateTypeIndex
|
|
123
124
|
*/
|
|
124
125
|
export async function handleCreatePod(request, reply) {
|
|
125
|
-
const { name } = request.body || {};
|
|
126
|
+
const { name, email, password } = request.body || {};
|
|
127
|
+
const idpEnabled = request.idpEnabled;
|
|
126
128
|
|
|
127
129
|
if (!name || typeof name !== 'string') {
|
|
128
130
|
return reply.code(400).send({ error: 'Pod name required' });
|
|
129
131
|
}
|
|
130
132
|
|
|
133
|
+
// If IdP is enabled, require email and password
|
|
134
|
+
if (idpEnabled) {
|
|
135
|
+
if (!email || typeof email !== 'string') {
|
|
136
|
+
return reply.code(400).send({ error: 'Email required for account creation' });
|
|
137
|
+
}
|
|
138
|
+
if (!password || password.length < 8) {
|
|
139
|
+
return reply.code(400).send({ error: 'Password required (minimum 8 characters)' });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
131
143
|
// Validate pod name (alphanumeric, dash, underscore)
|
|
132
144
|
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
133
145
|
return reply.code(400).send({ error: 'Invalid pod name. Use alphanumeric, dash, or underscore only.' });
|
|
@@ -200,7 +212,28 @@ export async function handleCreatePod(request, reply) {
|
|
|
200
212
|
|
|
201
213
|
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
202
214
|
|
|
203
|
-
//
|
|
215
|
+
// If IdP is enabled, create account instead of simple token
|
|
216
|
+
if (idpEnabled) {
|
|
217
|
+
try {
|
|
218
|
+
const { createAccount } = await import('../idp/accounts.js');
|
|
219
|
+
await createAccount({ email, password, webId, podName: name });
|
|
220
|
+
|
|
221
|
+
return reply.code(201).send({
|
|
222
|
+
name,
|
|
223
|
+
webId,
|
|
224
|
+
podUri,
|
|
225
|
+
idpIssuer: issuer,
|
|
226
|
+
loginUrl: `${issuer}/idp/auth`,
|
|
227
|
+
});
|
|
228
|
+
} catch (err) {
|
|
229
|
+
console.error('Account creation error:', err);
|
|
230
|
+
// Rollback pod creation on account failure
|
|
231
|
+
await storage.remove(podPath);
|
|
232
|
+
return reply.code(409).send({ error: err.message });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Generate token for the pod owner (simple auth mode)
|
|
204
237
|
const token = createToken(webId);
|
|
205
238
|
|
|
206
239
|
return reply.code(201).send({
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account management for the Identity Provider
|
|
3
|
+
* Handles user accounts with email/password authentication
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import bcrypt from 'bcrypt';
|
|
7
|
+
import crypto from 'crypto';
|
|
8
|
+
import fs from 'fs-extra';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get accounts directory (computed dynamically to support changing DATA_ROOT)
|
|
13
|
+
*/
|
|
14
|
+
function getAccountsDir() {
|
|
15
|
+
const dataRoot = process.env.DATA_ROOT || './data';
|
|
16
|
+
return path.join(dataRoot, '.idp', 'accounts');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getEmailIndexPath() {
|
|
20
|
+
return path.join(getAccountsDir(), '_email_index.json');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getWebIdIndexPath() {
|
|
24
|
+
return path.join(getAccountsDir(), '_webid_index.json');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const SALT_ROUNDS = 10;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initialize the accounts directory
|
|
31
|
+
*/
|
|
32
|
+
async function ensureDir() {
|
|
33
|
+
await fs.ensureDir(getAccountsDir());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Load an index file
|
|
38
|
+
*/
|
|
39
|
+
async function loadIndex(indexPath) {
|
|
40
|
+
try {
|
|
41
|
+
return await fs.readJson(indexPath);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err.code === 'ENOENT') return {};
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Save an index file
|
|
50
|
+
*/
|
|
51
|
+
async function saveIndex(indexPath, index) {
|
|
52
|
+
await fs.writeJson(indexPath, index, { spaces: 2 });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a new user account
|
|
57
|
+
* @param {object} options - Account options
|
|
58
|
+
* @param {string} options.email - User email
|
|
59
|
+
* @param {string} options.password - Plain text password
|
|
60
|
+
* @param {string} options.webId - User's WebID URI
|
|
61
|
+
* @param {string} options.podName - Pod name
|
|
62
|
+
* @returns {Promise<object>} - Created account (without password)
|
|
63
|
+
*/
|
|
64
|
+
export async function createAccount({ email, password, webId, podName }) {
|
|
65
|
+
await ensureDir();
|
|
66
|
+
|
|
67
|
+
const normalizedEmail = email.toLowerCase().trim();
|
|
68
|
+
|
|
69
|
+
// Check email uniqueness
|
|
70
|
+
const existingByEmail = await findByEmail(normalizedEmail);
|
|
71
|
+
if (existingByEmail) {
|
|
72
|
+
throw new Error('Email already registered');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check webId uniqueness
|
|
76
|
+
const existingByWebId = await findByWebId(webId);
|
|
77
|
+
if (existingByWebId) {
|
|
78
|
+
throw new Error('WebID already has an account');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Generate account ID and hash password
|
|
82
|
+
const id = crypto.randomUUID();
|
|
83
|
+
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
|
|
84
|
+
|
|
85
|
+
const account = {
|
|
86
|
+
id,
|
|
87
|
+
email: normalizedEmail,
|
|
88
|
+
passwordHash,
|
|
89
|
+
webId,
|
|
90
|
+
podName,
|
|
91
|
+
createdAt: new Date().toISOString(),
|
|
92
|
+
lastLogin: null,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Save account
|
|
96
|
+
const accountPath = path.join(getAccountsDir(), `${id}.json`);
|
|
97
|
+
await fs.writeJson(accountPath, account, { spaces: 2 });
|
|
98
|
+
|
|
99
|
+
// Update email index
|
|
100
|
+
const emailIndex = await loadIndex(getEmailIndexPath());
|
|
101
|
+
emailIndex[normalizedEmail] = id;
|
|
102
|
+
await saveIndex(getEmailIndexPath(), emailIndex);
|
|
103
|
+
|
|
104
|
+
// Update webId index
|
|
105
|
+
const webIdIndex = await loadIndex(getWebIdIndexPath());
|
|
106
|
+
webIdIndex[webId] = id;
|
|
107
|
+
await saveIndex(getWebIdIndexPath(), webIdIndex);
|
|
108
|
+
|
|
109
|
+
// Return account without password hash
|
|
110
|
+
const { passwordHash: _, ...safeAccount } = account;
|
|
111
|
+
return safeAccount;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Authenticate a user with email and password
|
|
116
|
+
* @param {string} email - User email
|
|
117
|
+
* @param {string} password - Plain text password
|
|
118
|
+
* @returns {Promise<object|null>} - Account if valid, null if invalid
|
|
119
|
+
*/
|
|
120
|
+
export async function authenticate(email, password) {
|
|
121
|
+
const account = await findByEmail(email);
|
|
122
|
+
if (!account) return null;
|
|
123
|
+
|
|
124
|
+
const valid = await bcrypt.compare(password, account.passwordHash);
|
|
125
|
+
if (!valid) return null;
|
|
126
|
+
|
|
127
|
+
// Update last login
|
|
128
|
+
account.lastLogin = new Date().toISOString();
|
|
129
|
+
const accountPath = path.join(getAccountsDir(), `${account.id}.json`);
|
|
130
|
+
await fs.writeJson(accountPath, account, { spaces: 2 });
|
|
131
|
+
|
|
132
|
+
// Return account without password hash
|
|
133
|
+
const { passwordHash: _, ...safeAccount } = account;
|
|
134
|
+
return safeAccount;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Find an account by ID
|
|
139
|
+
* @param {string} id - Account ID
|
|
140
|
+
* @returns {Promise<object|null>} - Account or null
|
|
141
|
+
*/
|
|
142
|
+
export async function findById(id) {
|
|
143
|
+
try {
|
|
144
|
+
const accountPath = path.join(getAccountsDir(), `${id}.json`);
|
|
145
|
+
return await fs.readJson(accountPath);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (err.code === 'ENOENT') return null;
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Find an account by email
|
|
154
|
+
* @param {string} email - User email
|
|
155
|
+
* @returns {Promise<object|null>} - Account or null
|
|
156
|
+
*/
|
|
157
|
+
export async function findByEmail(email) {
|
|
158
|
+
const normalizedEmail = email.toLowerCase().trim();
|
|
159
|
+
const emailIndex = await loadIndex(getEmailIndexPath());
|
|
160
|
+
const id = emailIndex[normalizedEmail];
|
|
161
|
+
if (!id) return null;
|
|
162
|
+
return findById(id);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Find an account by WebID
|
|
167
|
+
* @param {string} webId - User WebID
|
|
168
|
+
* @returns {Promise<object|null>} - Account or null
|
|
169
|
+
*/
|
|
170
|
+
export async function findByWebId(webId) {
|
|
171
|
+
const webIdIndex = await loadIndex(getWebIdIndexPath());
|
|
172
|
+
const id = webIdIndex[webId];
|
|
173
|
+
if (!id) return null;
|
|
174
|
+
return findById(id);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Update account password
|
|
179
|
+
* @param {string} id - Account ID
|
|
180
|
+
* @param {string} newPassword - New plain text password
|
|
181
|
+
*/
|
|
182
|
+
export async function updatePassword(id, newPassword) {
|
|
183
|
+
const account = await findById(id);
|
|
184
|
+
if (!account) {
|
|
185
|
+
throw new Error('Account not found');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
account.passwordHash = await bcrypt.hash(newPassword, SALT_ROUNDS);
|
|
189
|
+
account.passwordChangedAt = new Date().toISOString();
|
|
190
|
+
|
|
191
|
+
const accountPath = path.join(getAccountsDir(), `${id}.json`);
|
|
192
|
+
await fs.writeJson(accountPath, account, { spaces: 2 });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Delete an account
|
|
197
|
+
* @param {string} id - Account ID
|
|
198
|
+
*/
|
|
199
|
+
export async function deleteAccount(id) {
|
|
200
|
+
const account = await findById(id);
|
|
201
|
+
if (!account) return;
|
|
202
|
+
|
|
203
|
+
// Remove from indexes
|
|
204
|
+
const emailIndex = await loadIndex(getEmailIndexPath());
|
|
205
|
+
delete emailIndex[account.email];
|
|
206
|
+
await saveIndex(getEmailIndexPath(), emailIndex);
|
|
207
|
+
|
|
208
|
+
const webIdIndex = await loadIndex(getWebIdIndexPath());
|
|
209
|
+
delete webIdIndex[account.webId];
|
|
210
|
+
await saveIndex(getWebIdIndexPath(), webIdIndex);
|
|
211
|
+
|
|
212
|
+
// Delete account file
|
|
213
|
+
const accountPath = path.join(getAccountsDir(), `${id}.json`);
|
|
214
|
+
await fs.remove(accountPath);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get account for oidc-provider's findAccount
|
|
219
|
+
* This is the interface oidc-provider expects
|
|
220
|
+
* @param {string} id - Account ID
|
|
221
|
+
* @returns {Promise<object|undefined>} - Account interface for oidc-provider
|
|
222
|
+
*/
|
|
223
|
+
export async function getAccountForProvider(id) {
|
|
224
|
+
const account = await findById(id);
|
|
225
|
+
if (!account) return undefined;
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
accountId: id,
|
|
229
|
+
/**
|
|
230
|
+
* Return claims for the token
|
|
231
|
+
* @param {string} use - 'id_token' or 'userinfo'
|
|
232
|
+
* @param {string} scope - Requested scopes
|
|
233
|
+
* @param {object} claims - Requested claims
|
|
234
|
+
* @param {string[]} rejected - Rejected claims
|
|
235
|
+
*/
|
|
236
|
+
async claims(use, scope, claims, rejected) {
|
|
237
|
+
const result = {
|
|
238
|
+
sub: id,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// Always include webid for Solid-OIDC
|
|
242
|
+
result.webid = account.webId;
|
|
243
|
+
|
|
244
|
+
// Profile scope
|
|
245
|
+
if (scope.includes('profile')) {
|
|
246
|
+
result.name = account.podName;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Email scope
|
|
250
|
+
if (scope.includes('email')) {
|
|
251
|
+
result.email = account.email;
|
|
252
|
+
result.email_verified = false; // We don't have email verification yet
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return result;
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|