ui(pool): pick an account's users from its own Edit
Each account row in the Account pool card has a pencil: tick the users whose sessions run on it (one moves off any other account, unticking sends one back to the balancer). PUT /api/accounts/:name/users saves the set in one transaction, audits each change and switches a Claude login into each added user's home at once. The card-level switch button and pinned list are gone.
This commit is contained in:
+45
-11
@@ -19,7 +19,9 @@
|
||||
* `PUT /pins` (`{tool, userId, account}`, `account: null` unpins) pins one
|
||||
* user's sessions of a tool to one pool account (account-pins.js) — a Claude
|
||||
* login goes into their home at once, so running panes move without a
|
||||
* restart (`switched`); `GET /` lists the pins under `pins`.
|
||||
* restart (`switched`); `PUT /:name/users` (`{tool, userIds}`) is the
|
||||
* Users page's Edit of one account — exactly those users run on it; `GET /`
|
||||
* lists the pins under `pins`.
|
||||
* `GET /profiles` lists the saved profiles that can be moved in (identity only). Like every Users-page change
|
||||
* they need the admin's own password again (`currentPassword`), are audited
|
||||
* (the account NAME only), and refuse in local mode. The secret only goes IN:
|
||||
@@ -77,6 +79,7 @@ const STATUS = {
|
||||
ELOCALMODE: 403,
|
||||
EREAUTH: 403,
|
||||
EBADTOOL: 400,
|
||||
EBADUSER: 400,
|
||||
EBADNAME: 400,
|
||||
EBADTOKEN: 400,
|
||||
ENOACCOUNT: 404,
|
||||
@@ -102,7 +105,9 @@ function poolChange(handler) {
|
||||
}
|
||||
await auth.confirmPassword(req.user, body.currentPassword);
|
||||
const out = await handler(req, body);
|
||||
recordAccountChange(req.user.id, out.target ?? null, req.method, out.action);
|
||||
for (const a of out.audits || [{ target: out.target, action: out.action }]) {
|
||||
recordAccountChange(req.user.id, a.target ?? null, req.method, a.action);
|
||||
}
|
||||
res.status(out.status || 200).json(out.json);
|
||||
} catch (e) {
|
||||
const status = STATUS[e.code];
|
||||
@@ -216,15 +221,9 @@ router.put(
|
||||
"/pins",
|
||||
poolChange(async (_req, body) => {
|
||||
const pin = pins.setPin(Number(body.userId), body.tool, body.account ?? null);
|
||||
let switched = null;
|
||||
if (pin.account) {
|
||||
try {
|
||||
switched = await pins.applyPin(pin.userId, body.tool);
|
||||
} catch (e) {
|
||||
console.warn(`[accounts] switch ${pin.username} → ${pin.account}: ${e.message}`);
|
||||
switched = "next-session";
|
||||
}
|
||||
}
|
||||
const switched = pin.account
|
||||
? await applyNow(pin.userId, pin.username, body.tool, pin.account)
|
||||
: null;
|
||||
return {
|
||||
json: { pin, switched },
|
||||
target: pin.userId,
|
||||
@@ -235,6 +234,41 @@ router.put(
|
||||
})
|
||||
);
|
||||
|
||||
/** Switch an added user now; a failed switch still leaves the pin for the next spawn. */
|
||||
async function applyNow(userId, username, tool, account) {
|
||||
try {
|
||||
return await pins.applyPin(userId, tool);
|
||||
} catch (e) {
|
||||
console.warn(`[accounts] switch ${username} → ${account}: ${e.message}`);
|
||||
return "next-session";
|
||||
}
|
||||
}
|
||||
|
||||
router.put(
|
||||
"/:name/users",
|
||||
poolChange(async (req, body) => {
|
||||
const account = req.params.name;
|
||||
const { added, removed } = pins.setAccountUsers(body.tool, account, body.userIds);
|
||||
const switched = [];
|
||||
for (const u of added) {
|
||||
switched.push({ ...u, switched: await applyNow(u.userId, u.username, body.tool, account) });
|
||||
}
|
||||
return {
|
||||
json: { added: switched, removed },
|
||||
audits: [
|
||||
...added.map((u) => ({
|
||||
target: u.userId,
|
||||
action: `pool: pin ${u.username} to ${body.tool} account ${account}`,
|
||||
})),
|
||||
...removed.map((u) => ({
|
||||
target: u.userId,
|
||||
action: `pool: unpin ${u.username} from ${body.tool}`,
|
||||
})),
|
||||
],
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:name",
|
||||
poolChange((req, body) => {
|
||||
|
||||
Reference in New Issue
Block a user