Two related gaps
1. No CLI for account deletion
`src/idp/accounts.js` already exports `deleteAccount(id)` — it removes the account JSON, prunes the username/email/webId indexes, the works. But there's no CLI surface for it. Operators currently delete accounts by hand-editing index JSON files and removing the account file, which is fragile.
2. `passwd` bypasses the wrapper it should use
The existing `jss passwd ` command (`bin/jss.js:628`) reads the username index and account file with raw `fs.readJson` / `fs.writeJson` instead of going through `accounts.js`. Notably, `accounts.js` already exports `updatePassword(id, newPassword)` — `passwd` re-implements that logic inline (including the bcrypt round) rather than calling it. The other `bin/jss.js` commands (`invite`, `quota`) all consume their respective `src/` modules via imports — `passwd` is the odd one out.
Proposal
Add an `account` command group with `delete` (and room for future siblings like `list`, `info`, `disable`), and refactor `passwd` to use the wrapper too.
`jss account delete `
Sketch:
```js
import { findByUsername, deleteAccount } from '../src/idp/accounts.js';
program
.command('account')
.description('Manage user accounts');
program
.command('account delete ')
.description('Delete a user account from the IdP')
.option('-r, --root ', 'Data directory')
.option('-y, --yes', 'Skip the confirmation prompt')
.option('--purge', 'Also delete pod data at //')
.action(async (username, opts) => {
if (opts.root) process.env.DATA_ROOT = path.resolve(opts.root);
const account = await findByUsername(username);
if (!account) { console.error(`User not found: ${username}`); process.exit(1); }
if (!opts.yes) {
const ok = await confirm(`Delete account '${username}' (${account.webId})?`);
if (!ok) process.exit(1);
}
await deleteAccount(account.id);
if (opts.purge) {
await fs.remove(path.join(process.env.DATA_ROOT || './data', username));
}
console.log(`Deleted ${username}.${opts.purge ? ' Pod data removed.' : ''}`);
});
```
`--purge` is opt-in for a reason
Account record removal revokes identity. Pod data removal is destructive and irreversible. Keeping them separate behind an explicit flag means the safe default (`account delete melvin`) frees the username for re-registration without touching files at `/melvin/`. `--purge` is for the "start over completely" case.
Refactor `passwd`
Drop the inline `bcrypt.hash` and `fs.readJson`/`writeJson` in favour of:
```js
const account = await findByUsername(username);
if (!account) { /* error */ }
await updatePassword(account.id, newPassword);
```
Same external behaviour, ~30 fewer lines, and a single source of truth for hashing parameters / index updates.
Why both in one issue
They're the same shape of fix — the CLI layer should consume `accounts.js` consistently. Doing both together makes the pattern obvious and removes the temptation to copy the inline-fs pattern into the new `delete` command.
Non-goals
- Account listing / info / disable (good follow-ups; out of scope here).
- HTTP `DELETE /idp/account` endpoint (requires an auth story for self-service deletion vs admin deletion; deserves its own issue).
- Federation / cleaning up sessions or refresh tokens issued to the deleted account (separate concern; tokens expire naturally, but a thorough deletion would revoke them).
Happy to PR.
Two related gaps
1. No CLI for account deletion
`src/idp/accounts.js` already exports `deleteAccount(id)` — it removes the account JSON, prunes the username/email/webId indexes, the works. But there's no CLI surface for it. Operators currently delete accounts by hand-editing index JSON files and removing the account file, which is fragile.
2. `passwd` bypasses the wrapper it should use
The existing `jss passwd ` command (`bin/jss.js:628`) reads the username index and account file with raw `fs.readJson` / `fs.writeJson` instead of going through `accounts.js`. Notably, `accounts.js` already exports `updatePassword(id, newPassword)` — `passwd` re-implements that logic inline (including the bcrypt round) rather than calling it. The other `bin/jss.js` commands (`invite`, `quota`) all consume their respective `src/` modules via imports — `passwd` is the odd one out.
Proposal
Add an `account` command group with `delete` (and room for future siblings like `list`, `info`, `disable`), and refactor `passwd` to use the wrapper too.
`jss account delete `
Sketch:
```js
import { findByUsername, deleteAccount } from '../src/idp/accounts.js';
program
.command('account')
.description('Manage user accounts');
program
.command('account delete ')
.description('Delete a user account from the IdP')
.option('-r, --root ', 'Data directory')
.option('-y, --yes', 'Skip the confirmation prompt')
.option('--purge', 'Also delete pod data at //')
.action(async (username, opts) => {
if (opts.root) process.env.DATA_ROOT = path.resolve(opts.root);
const account = await findByUsername(username);
if (!account) { console.error(`User not found: ${username}`); process.exit(1); }
if (!opts.yes) {
const ok = await confirm(`Delete account '${username}' (${account.webId})?`);
if (!ok) process.exit(1);
}
await deleteAccount(account.id);
if (opts.purge) {
await fs.remove(path.join(process.env.DATA_ROOT || './data', username));
}
console.log(`Deleted ${username}.${opts.purge ? ' Pod data removed.' : ''}`);
});
```
`--purge` is opt-in for a reason
Account record removal revokes identity. Pod data removal is destructive and irreversible. Keeping them separate behind an explicit flag means the safe default (`account delete melvin`) frees the username for re-registration without touching files at `/melvin/`. `--purge` is for the "start over completely" case.
Refactor `passwd`
Drop the inline `bcrypt.hash` and `fs.readJson`/`writeJson` in favour of:
```js
const account = await findByUsername(username);
if (!account) { /* error */ }
await updatePassword(account.id, newPassword);
```
Same external behaviour, ~30 fewer lines, and a single source of truth for hashing parameters / index updates.
Why both in one issue
They're the same shape of fix — the CLI layer should consume `accounts.js` consistently. Doing both together makes the pattern obvious and removes the temptation to copy the inline-fs pattern into the new `delete` command.
Non-goals
Happy to PR.