remove the dead multi-user surface
Officer is single-user: the server owner is the only account, created once by /auth/bootstrap. Everything that existed to serve additional users was unreachable, so it is gone rather than left looking like it does something. Accounts: drop the invite / resend-invite / delete / list-users routes and the Users settings screen, the inert /auth/signup handler, and the account verification chain it fed (verify, resend-verification, VerifyScreen, the UserInvite + VerifyAdmin + VerifyRegistration templates). /auth/verify-token survives for password resets only, and now requires a reset-password token rather than accepting any signed JWT. Roles: drop the users.role column and the four-value USER_ROLES enum. The permissions table granted every role identical methods, and every role === 'Super Admin' check was permanently true. The JWT no longer carries a role claim. Sandbox: remove sidecar/sandbox.ts and its five call sites. bwrap was selected only for non-Super-Admin users, so it never ran. It was also not a usable agent jail as written — --share-net, the project root (with .env) bound read-only, and runuser dropping to the server's own uid. Rebuilding it for agent containment would be a different construction, and git history keeps this one. getHomeDir keeps its DATA_PATH meaning; the new getOwnerHomeDir resolves the owner's real login home, which is what terminals, chats and task runs use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
92de996412
commit
044aacf4d5
@@ -7,7 +7,7 @@ import { sendOpenCodeStreaming } from '@@/channels/send-opencode';
|
||||
import { ensureGeneralChatSessionsCwd } from './claude-sessions';
|
||||
import * as sidecar from '@@/sidecar-registry';
|
||||
import { join } from 'path';
|
||||
import { getHomeDirForRole, getEmailAccountsDir } from '../../../servers/data-path';
|
||||
import { getOwnerHomeDir, getEmailAccountsDir } from '../../../servers/data-path';
|
||||
import { getUserSettings, getEmailAccounts } from 'officerdb';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import { logger } from './logger';
|
||||
@@ -34,28 +34,21 @@ type WSData = {
|
||||
userId: number;
|
||||
email: string;
|
||||
username: string;
|
||||
role: string;
|
||||
sandboxed: boolean;
|
||||
provider: string;
|
||||
};
|
||||
|
||||
const IDLE_TIMEOUT_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
const resolveCwd = (email: string, role: string, cwd?: string) => {
|
||||
const root = getHomeDirForRole(email, role);
|
||||
const resolveCwd = (email: string, cwd?: string) => {
|
||||
const root = getOwnerHomeDir(email);
|
||||
if (!cwd || cwd === '~') return root;
|
||||
if (cwd.startsWith('~/')) return join(root, cwd.slice(2));
|
||||
if (cwd.startsWith('/')) {
|
||||
// Super Admin: trust absolute paths as-is
|
||||
if (role === 'Super Admin') return cwd;
|
||||
return join(root, cwd.slice(1));
|
||||
}
|
||||
// The server owner is the only account — absolute paths are theirs to use.
|
||||
if (cwd.startsWith('/')) return cwd;
|
||||
return join(root, cwd);
|
||||
};
|
||||
|
||||
export const resolveBaseCwd = (email: string, role: string, cwd?: string) => {
|
||||
return resolveCwd(email, role, cwd);
|
||||
};
|
||||
export const resolveBaseCwd = (email: string, cwd?: string) => resolveCwd(email, cwd);
|
||||
|
||||
// The email chat runs from the selected account's storage dir:
|
||||
// DATA_PATH/<owner>/email_accounts/<accountEmail>
|
||||
@@ -81,13 +74,11 @@ async function resolveEmailCwd(userId: number, ownerEmail: string, accountEmail?
|
||||
async function resolveChatCwd(
|
||||
msg: { context?: string; contextId?: string; cwd?: string },
|
||||
email: string,
|
||||
role: string,
|
||||
userId: number,
|
||||
): Promise<string> {
|
||||
if (msg.context === 'email') return resolveEmailCwd(userId, email, msg.contextId);
|
||||
if (msg.context === 'chat')
|
||||
return msg.cwd?.trim() ? resolveCwd(email, role, msg.cwd) : ensureGeneralChatSessionsCwd(email);
|
||||
return resolveCwd(email, role, msg.cwd);
|
||||
if (msg.context === 'chat') return msg.cwd?.trim() ? resolveCwd(email, msg.cwd) : ensureGeneralChatSessionsCwd(email);
|
||||
return resolveCwd(email, msg.cwd);
|
||||
}
|
||||
|
||||
const wsToSessionMap = new WeakMap<any, string>();
|
||||
@@ -295,7 +286,6 @@ async function handleChat(
|
||||
model?: string;
|
||||
cwd?: string;
|
||||
cwdRoot?: string;
|
||||
sandboxed?: boolean;
|
||||
groupSlug?: string;
|
||||
attachmentIds?: string[];
|
||||
thinking?: string;
|
||||
@@ -336,14 +326,13 @@ async function handleClaudeCodeChat(
|
||||
contextId?: string;
|
||||
cwd?: string;
|
||||
cwdRoot?: string;
|
||||
sandboxed?: boolean;
|
||||
resumeSessionId?: string;
|
||||
},
|
||||
effectivePrompt: string,
|
||||
): Promise<void> {
|
||||
const { email, username, userId } = ws.data;
|
||||
|
||||
const cwd = await resolveChatCwd(msg, email, ws.data.role, userId);
|
||||
const cwd = await resolveChatCwd(msg, email, userId);
|
||||
|
||||
const groupSlug = msg.groupSlug || null;
|
||||
|
||||
@@ -389,7 +378,6 @@ async function handleClaudeCodeChat(
|
||||
sessionKey: sessionId,
|
||||
cwd,
|
||||
model,
|
||||
role: ws.data.role,
|
||||
resumeSessionId: msg.resumeSessionId,
|
||||
onEvent,
|
||||
});
|
||||
@@ -416,14 +404,13 @@ async function handleOpenCodeChat(
|
||||
contextId?: string;
|
||||
cwd?: string;
|
||||
cwdRoot?: string;
|
||||
sandboxed?: boolean;
|
||||
resumeSessionId?: string;
|
||||
},
|
||||
effectivePrompt: string,
|
||||
): Promise<void> {
|
||||
const { email, username, userId } = ws.data;
|
||||
|
||||
const cwd = await resolveChatCwd(msg, email, ws.data.role, userId);
|
||||
const cwd = await resolveChatCwd(msg, email, userId);
|
||||
|
||||
const groupSlug = msg.groupSlug || null;
|
||||
|
||||
@@ -468,7 +455,6 @@ async function handleOpenCodeChat(
|
||||
sessionKey: sessionId,
|
||||
cwd,
|
||||
model,
|
||||
role: ws.data.role,
|
||||
resumeSessionId: msg.resumeSessionId,
|
||||
onEvent,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user