Pithos 0.3.23: rename S3 users

This commit is contained in:
Local Dev 2026-10-07 00:17:32 +02:00
parent d162b745f7
commit a33854a926
4 changed files with 50 additions and 3 deletions

View file

@ -1,7 +1,7 @@
{
"id": "pithos",
"name": "Pithos",
"version": "0.3.22",
"version": "0.3.23",
"description": "Run s3d, the Sia S3 gateway, from the Theseus sidebar: connect it to a Sia indexer, create S3 users and access keys, browse and share buckets, and watch uploads reach Sia.",
"author": "Silent Mode",
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM2RkYzk3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMWU4ZjZhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIyIDZoMjB2NWMwIDItMiAzLTIgNSA4IDMgMTQgMTEgMTQgMjEgMCAxMi05IDIxLTIyIDIxUzEwIDQ5IDEwIDM3YzAtMTAgNi0xOCAxNC0yMSAwLTItMi0zLTItNXoiIGZpbGw9InVybCgjZykiLz48cGF0aCBkPSJNMTQgMzBoMzZNMTMgNDBoMzgiIHN0cm9rZT0iIzBiMWYxOCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9Ii41NSIvPjwvc3ZnPgo=",

View file

@ -86,7 +86,7 @@ export function makeCli(daemon) {
// Users are passed as argv, never through a shell, but a leading '-' would be
// read as a flag by s3d's parser.
function validateName(name) {
export function validateName(name) {
if (typeof name !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._@-]{0,63}$/.test(name)) {
throw new CliError('user names are 1-64 characters: letters, digits, . _ @ -, not starting with a symbol');
}

View file

@ -16,7 +16,7 @@ import { fileURLToPath } from 'node:url';
import { resolveConfigPath, resolveBinary } from './paths.js';
import { readConfig, writeConfig, ensureConfig, EDITABLE } from './config.js';
import { Daemon } from './daemon.js';
import { makeCli, CliError } from './cli.js';
import { makeCli, CliError, validateName } from './cli.js';
import { LoginSession } from './login.js';
import { S3Client, S3Error, publicReadPolicy, readBody } from './s3.js';
import * as admin from './admin.js';
@ -293,6 +293,35 @@ export async function createPithos(opts = {}) {
return { name, key, bucket: created, bucketError };
});
route('DELETE', '/api/users/:name', async (req, url, p) => { await cli.deleteUser(p.name); return { ok: true }; });
// s3d has no rename. A user is one row (id, name); keys and drives point at
// the id, so with s3d stopped the name is changed in place: keys, secrets
// and drives stay as they are, and S3 apps keep working.
route('POST', '/api/users/:name/rename', async (req, url, p) => {
if (hosted.isHosted()) throw new HttpError(409, 'Users on Silent Mode cannot be renamed yet.');
const { name } = await jsonBody(req);
try { validateName(p.name); validateName(name); } catch (e) { throw new HttpError(400, e.message); }
if (name === p.name) return { name };
await accountGuard();
const file = path.join(cfg().directory, 's3d.db');
if (!fs.existsSync(file)) throw new HttpError(409, 's3d has no database yet');
const wasRunning = !!daemon.child;
if (wasRunning) await daemon.stop();
try {
const { DatabaseSync } = await import('node:sqlite');
const db = new DatabaseSync(file);
try {
if (db.prepare('SELECT 1 FROM users WHERE name = ?').get(name)) throw new HttpError(409, `a user named ${name} already exists`);
if (!db.prepare('UPDATE users SET name = ? WHERE name = ?').run(name, p.name).changes) throw new HttpError(404, `no user named ${p.name}`);
} finally { db.close(); }
// Pithos's own records that name the user
const links = readSiaLinks();
if (links.some((l) => l.user === p.name)) writeSiaLinks(links.map((l) => (l.user === p.name ? { ...l, user: name } : l)));
} finally {
if (wasRunning) { try { daemon.start(); } catch (e) { daemon.pushLog('sys', `could not start s3d: ${e.message}`); } }
}
daemon.pushLog('sys', `renamed user ${p.name} to ${name}`);
return { name };
});
route('GET', '/api/keys', (req, url) => cli.listKeys(url.searchParams.get('user') || undefined));
route('POST', '/api/keys', async (req) => {
const { user, accessKey, secretKey } = await jsonBody(req);

View file

@ -1034,6 +1034,8 @@ function users(main) {
' ',
h('button', { class: 'btn small', onclick: (e) => { e.stopPropagation(); newKey(u.name); } }, 'New key'),
' ',
!isHosted() && h('button', { class: 'btn small', onclick: (e) => { e.stopPropagation(); renameUser(u.name); } }, 'Rename…'),
' ',
h('button', { class: 'btn small danger', onclick: (e) => { e.stopPropagation(); deleteUser(u.name); } }, 'Delete')));
if (!open) return [main];
const detail = h('tr', {}, h('td', { colspan: 3, style: 'background:var(--surface-2)' },
@ -1081,6 +1083,22 @@ function users(main) {
} catch (e) { fail(e); }
}
async function renameUser(current) {
const input = h('input', { type: 'text', required: true, value: current, pattern: '[A-Za-z0-9][A-Za-z0-9._@\\-]{0,63}', spellcheck: 'false', autocomplete: 'off' });
const name = await modal('Rename user', h('div', { class: 'stack' },
h('label', { class: 'field' }, h('span', {}, 'New name'), input, h('small', {}, '1-64 characters: letters, digits, . _ @ -, not starting with a symbol.')),
h('p', { class: 'small muted', style: 'margin:0' }, 'Access keys and secrets stay the same, so S3 apps keep working, and the drives stay with this user. s3d stops for a moment while the name changes (after any upload in progress) and starts again.')),
[{ label: 'Cancel', result: null }, { label: 'Rename', kind: 'primary', submit: true, value: () => input.value.trim() }]);
if (!name || name === current) return;
try {
await api('POST', `/api/users/${encodeURIComponent(current)}/rename`, { name });
if (state.selectedUser === current) { state.selectedUser = name; try { localStorage.setItem('pithos.user', name); } catch {} }
expanded.delete(current);
toast(`Renamed to ${name}`);
load();
} catch (e) { fail(e); load(); }
}
async function deleteUser(name) {
if (!(await confirmModal(`Delete ${name}?`, 'Its access keys stop working immediately. s3d refuses if the user still owns buckets.'))) return;
try { await api('DELETE', `/api/users/${encodeURIComponent(name)}`); toast(`Deleted ${name}`); load(); } catch (e) { fail(e); }