From a33854a926c1c211bb0ce469870dd46d12fb127f Mon Sep 17 00:00:00 2001 From: Local Dev Date: Wed, 7 Oct 2026 00:17:32 +0200 Subject: [PATCH] Pithos 0.3.23: rename S3 users --- bundled-addons/pithos/addon.json | 2 +- bundled-addons/pithos/core/cli.js | 2 +- bundled-addons/pithos/core/server.js | 31 +++++++++++++++++++++++++++- bundled-addons/pithos/ui/app.js | 18 ++++++++++++++++ 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/bundled-addons/pithos/addon.json b/bundled-addons/pithos/addon.json index 465d99c1..61ad32fd 100644 --- a/bundled-addons/pithos/addon.json +++ b/bundled-addons/pithos/addon.json @@ -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=", diff --git a/bundled-addons/pithos/core/cli.js b/bundled-addons/pithos/core/cli.js index 0a62edd2..01be2658 100644 --- a/bundled-addons/pithos/core/cli.js +++ b/bundled-addons/pithos/core/cli.js @@ -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'); } diff --git a/bundled-addons/pithos/core/server.js b/bundled-addons/pithos/core/server.js index 7bf05d3b..81572d31 100644 --- a/bundled-addons/pithos/core/server.js +++ b/bundled-addons/pithos/core/server.js @@ -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); diff --git a/bundled-addons/pithos/ui/app.js b/bundled-addons/pithos/ui/app.js index ea91c6a7..bd717975 100644 --- a/bundled-addons/pithos/ui/app.js +++ b/bundled-addons/pithos/ui/app.js @@ -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); }