Skip to content

Commit 519d3f6

Browse files
Add NSS-style registration and username-based auth
- Registration page at /idp/register creates account + pod - Username-based auth (email defaults to username@jss) - Login page shows requesting app name - createPodStructure() reusable for registration - No password minimum for testing Closes #3
1 parent ae36b0c commit 519d3f6

7 files changed

Lines changed: 307 additions & 83 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "javascript-solid-server",
3-
"version": "0.0.20",
3+
"version": "0.0.21",
44
"description": "A minimal, fast Solid server",
55
"main": "src/index.js",
66
"type": "module",

src/handlers/container.js

Lines changed: 62 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,63 @@ export async function handlePost(request, reply) {
121121
return reply.code(201).send();
122122
}
123123

124+
/**
125+
* Create pod directory structure (reusable for registration)
126+
* @param {string} name - Pod name (username)
127+
* @param {string} webId - User's WebID URI
128+
* @param {string} baseUrl - Base URL (without trailing slash)
129+
*/
130+
export async function createPodStructure(name, webId, baseUrl) {
131+
const podPath = `/${name}/`;
132+
const podUri = `${baseUrl}/${name}/`;
133+
const issuer = baseUrl + '/';
134+
135+
// Create pod directory structure
136+
await storage.createContainer(podPath);
137+
await storage.createContainer(`${podPath}inbox/`);
138+
await storage.createContainer(`${podPath}public/`);
139+
await storage.createContainer(`${podPath}private/`);
140+
await storage.createContainer(`${podPath}settings/`);
141+
142+
// Generate and write WebID profile as index.html at pod root
143+
const profileHtml = generateProfile({ webId, name, podUri, issuer });
144+
await storage.write(`${podPath}index.html`, profileHtml);
145+
146+
// Generate and write preferences
147+
const prefs = generatePreferences({ webId, podUri });
148+
await storage.write(`${podPath}settings/prefs`, serialize(prefs));
149+
150+
// Generate and write type indexes
151+
const publicTypeIndex = generateTypeIndex(`${podUri}settings/publicTypeIndex`);
152+
await storage.write(`${podPath}settings/publicTypeIndex`, serialize(publicTypeIndex));
153+
154+
const privateTypeIndex = generateTypeIndex(`${podUri}settings/privateTypeIndex`);
155+
await storage.write(`${podPath}settings/privateTypeIndex`, serialize(privateTypeIndex));
156+
157+
// Create default ACL files
158+
// Pod root: owner full control, public read
159+
const rootAcl = generateOwnerAcl(podUri, webId, true);
160+
await storage.write(`${podPath}.acl`, serializeAcl(rootAcl));
161+
162+
// Private folder: owner only (no public)
163+
const privateAcl = generatePrivateAcl(`${podUri}private/`, webId);
164+
await storage.write(`${podPath}private/.acl`, serializeAcl(privateAcl));
165+
166+
// Settings folder: owner only
167+
const settingsAcl = generatePrivateAcl(`${podUri}settings/`, webId);
168+
await storage.write(`${podPath}settings/.acl`, serializeAcl(settingsAcl));
169+
170+
// Inbox: owner full, public append
171+
const inboxAcl = generateInboxAcl(`${podUri}inbox/`, webId);
172+
await storage.write(`${podPath}inbox/.acl`, serializeAcl(inboxAcl));
173+
174+
// Public folder: owner full, public read (with inheritance)
175+
const publicAcl = generatePublicFolderAcl(`${podUri}public/`, webId);
176+
await storage.write(`${podPath}public/.acl`, serializeAcl(publicAcl));
177+
178+
return { podPath, podUri };
179+
}
180+
124181
/**
125182
* Create a pod (container) for a user
126183
* POST /.pods with { "name": "alice" }
@@ -149,8 +206,8 @@ export async function handleCreatePod(request, reply) {
149206
if (!email || typeof email !== 'string') {
150207
return reply.code(400).send({ error: 'Email required for account creation' });
151208
}
152-
if (!password || password.length < 8) {
153-
return reply.code(400).send({ error: 'Password required (minimum 8 characters)' });
209+
if (!password) {
210+
return reply.code(400).send({ error: 'Password required' });
154211
}
155212
}
156213

@@ -189,49 +246,8 @@ export async function handleCreatePod(request, reply) {
189246
const issuer = baseUri + '/';
190247

191248
try {
192-
// Create pod directory structure
193-
await storage.createContainer(podPath);
194-
await storage.createContainer(`${podPath}inbox/`);
195-
await storage.createContainer(`${podPath}public/`);
196-
await storage.createContainer(`${podPath}private/`);
197-
await storage.createContainer(`${podPath}settings/`);
198-
199-
// Generate and write WebID profile as index.html at pod root
200-
const profileHtml = generateProfile({ webId, name, podUri, issuer });
201-
await storage.write(`${podPath}index.html`, profileHtml);
202-
203-
// Generate and write preferences
204-
const prefs = generatePreferences({ webId, podUri });
205-
await storage.write(`${podPath}settings/prefs`, serialize(prefs));
206-
207-
// Generate and write type indexes
208-
const publicTypeIndex = generateTypeIndex(`${podUri}settings/publicTypeIndex`);
209-
await storage.write(`${podPath}settings/publicTypeIndex`, serialize(publicTypeIndex));
210-
211-
const privateTypeIndex = generateTypeIndex(`${podUri}settings/privateTypeIndex`);
212-
await storage.write(`${podPath}settings/privateTypeIndex`, serialize(privateTypeIndex));
213-
214-
// Create default ACL files
215-
// Pod root: owner full control, public read
216-
const rootAcl = generateOwnerAcl(podUri, webId, true);
217-
await storage.write(`${podPath}.acl`, serializeAcl(rootAcl));
218-
219-
// Private folder: owner only (no public)
220-
const privateAcl = generatePrivateAcl(`${podUri}private/`, webId);
221-
await storage.write(`${podPath}private/.acl`, serializeAcl(privateAcl));
222-
223-
// Settings folder: owner only
224-
const settingsAcl = generatePrivateAcl(`${podUri}settings/`, webId);
225-
await storage.write(`${podPath}settings/.acl`, serializeAcl(settingsAcl));
226-
227-
// Inbox: owner full, public append
228-
const inboxAcl = generateInboxAcl(`${podUri}inbox/`, webId);
229-
await storage.write(`${podPath}inbox/.acl`, serializeAcl(inboxAcl));
230-
231-
// Public folder: owner full, public read (with inheritance)
232-
const publicAcl = generatePublicFolderAcl(`${podUri}public/`, webId);
233-
await storage.write(`${podPath}public/.acl`, serializeAcl(publicAcl));
234-
249+
// Use shared pod creation function
250+
await createPodStructure(name, webId, baseUri);
235251
} catch (err) {
236252
console.error('Pod creation error:', err);
237253
// Cleanup on failure
@@ -249,7 +265,7 @@ export async function handleCreatePod(request, reply) {
249265
if (idpEnabled) {
250266
try {
251267
const { createAccount } = await import('../idp/accounts.js');
252-
await createAccount({ email, password, webId, podName: name });
268+
await createAccount({ username: name, email, password, webId, podName: name });
253269

254270
return reply.code(201).send({
255271
name,

src/idp/accounts.js

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
/**
22
* Account management for the Identity Provider
3-
* Handles user accounts with email/password authentication
3+
* Handles user accounts with username/password authentication
4+
* Email is optional - internally uses username@jss if not provided
45
*/
56

67
import bcrypt from 'bcrypt';
78
import crypto from 'crypto';
89
import fs from 'fs-extra';
910
import path from 'path';
1011

12+
// Internal domain for generated emails
13+
const INTERNAL_DOMAIN = 'jss';
14+
1115
/**
1216
* Get accounts directory (computed dynamically to support changing DATA_ROOT)
1317
*/
@@ -16,6 +20,10 @@ function getAccountsDir() {
1620
return path.join(dataRoot, '.idp', 'accounts');
1721
}
1822

23+
function getUsernameIndexPath() {
24+
return path.join(getAccountsDir(), '_username_index.json');
25+
}
26+
1927
function getEmailIndexPath() {
2028
return path.join(getAccountsDir(), '_email_index.json');
2129
}
@@ -55,21 +63,34 @@ async function saveIndex(indexPath, index) {
5563
/**
5664
* Create a new user account
5765
* @param {object} options - Account options
58-
* @param {string} options.email - User email
66+
* @param {string} options.username - Username (typically same as podName)
5967
* @param {string} options.password - Plain text password
6068
* @param {string} options.webId - User's WebID URI
6169
* @param {string} options.podName - Pod name
70+
* @param {string} [options.email] - Optional email (defaults to username@jss)
6271
* @returns {Promise<object>} - Created account (without password)
6372
*/
64-
export async function createAccount({ email, password, webId, podName }) {
73+
export async function createAccount({ username, password, webId, podName, email }) {
6574
await ensureDir();
6675

67-
const normalizedEmail = email.toLowerCase().trim();
76+
const normalizedUsername = username.toLowerCase().trim();
77+
// Use provided email or generate internal one
78+
const normalizedEmail = email
79+
? email.toLowerCase().trim()
80+
: `${normalizedUsername}@${INTERNAL_DOMAIN}`;
81+
82+
// Check username uniqueness
83+
const existingByUsername = await findByUsername(normalizedUsername);
84+
if (existingByUsername) {
85+
throw new Error('Username already taken');
86+
}
6887

69-
// Check email uniqueness
70-
const existingByEmail = await findByEmail(normalizedEmail);
71-
if (existingByEmail) {
72-
throw new Error('Email already registered');
88+
// Check email uniqueness (if real email provided)
89+
if (email) {
90+
const existingByEmail = await findByEmail(normalizedEmail);
91+
if (existingByEmail) {
92+
throw new Error('Email already registered');
93+
}
7394
}
7495

7596
// Check webId uniqueness
@@ -84,6 +105,7 @@ export async function createAccount({ email, password, webId, podName }) {
84105

85106
const account = {
86107
id,
108+
username: normalizedUsername,
87109
email: normalizedEmail,
88110
passwordHash,
89111
webId,
@@ -96,6 +118,11 @@ export async function createAccount({ email, password, webId, podName }) {
96118
const accountPath = path.join(getAccountsDir(), `${id}.json`);
97119
await fs.writeJson(accountPath, account, { spaces: 2 });
98120

121+
// Update username index
122+
const usernameIndex = await loadIndex(getUsernameIndexPath());
123+
usernameIndex[normalizedUsername] = id;
124+
await saveIndex(getUsernameIndexPath(), usernameIndex);
125+
99126
// Update email index
100127
const emailIndex = await loadIndex(getEmailIndexPath());
101128
emailIndex[normalizedEmail] = id;
@@ -112,13 +139,17 @@ export async function createAccount({ email, password, webId, podName }) {
112139
}
113140

114141
/**
115-
* Authenticate a user with email and password
116-
* @param {string} email - User email
142+
* Authenticate a user with username/email and password
143+
* @param {string} identifier - Username or email
117144
* @param {string} password - Plain text password
118145
* @returns {Promise<object|null>} - Account if valid, null if invalid
119146
*/
120-
export async function authenticate(email, password) {
121-
const account = await findByEmail(email);
147+
export async function authenticate(identifier, password) {
148+
// Try to find by username first, then by email
149+
let account = await findByUsername(identifier);
150+
if (!account) {
151+
account = await findByEmail(identifier);
152+
}
122153
if (!account) return null;
123154

124155
const valid = await bcrypt.compare(password, account.passwordHash);
@@ -149,6 +180,19 @@ export async function findById(id) {
149180
}
150181
}
151182

183+
/**
184+
* Find an account by username
185+
* @param {string} username - Username
186+
* @returns {Promise<object|null>} - Account or null
187+
*/
188+
export async function findByUsername(username) {
189+
const normalizedUsername = username.toLowerCase().trim();
190+
const usernameIndex = await loadIndex(getUsernameIndexPath());
191+
const id = usernameIndex[normalizedUsername];
192+
if (!id) return null;
193+
return findById(id);
194+
}
195+
152196
/**
153197
* Find an account by email
154198
* @param {string} email - User email
@@ -201,6 +245,12 @@ export async function deleteAccount(id) {
201245
if (!account) return;
202246

203247
// Remove from indexes
248+
if (account.username) {
249+
const usernameIndex = await loadIndex(getUsernameIndexPath());
250+
delete usernameIndex[account.username];
251+
await saveIndex(getUsernameIndexPath(), usernameIndex);
252+
}
253+
204254
const emailIndex = await loadIndex(getEmailIndexPath());
205255
delete emailIndex[account.email];
206256
await saveIndex(getEmailIndexPath(), emailIndex);

src/idp/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
handleLogin,
1212
handleConsent,
1313
handleAbort,
14+
handleRegisterGet,
15+
handleRegisterPost,
1416
} from './interactions.js';
1517
import {
1618
handleCredentials,
@@ -220,6 +222,15 @@ export async function idpPlugin(fastify, options) {
220222
return handleAbort(request, reply, provider);
221223
});
222224

225+
// Registration routes
226+
fastify.get('/idp/register', async (request, reply) => {
227+
return handleRegisterGet(request, reply);
228+
});
229+
230+
fastify.post('/idp/register', async (request, reply) => {
231+
return handleRegisterPost(request, reply, issuer);
232+
});
233+
223234
fastify.log.info(`IdP initialized with issuer: ${issuer}`);
224235
}
225236

0 commit comments

Comments
 (0)