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:
brunorezio
2026-07-25 23:30:20 +01:00
co-authored by Claude Opus 5
parent 92de996412
commit 044aacf4d5
85 changed files with 2761 additions and 2121 deletions
@@ -0,0 +1 @@
ALTER TABLE "users" DROP COLUMN "role";
File diff suppressed because it is too large Load Diff
@@ -43,6 +43,13 @@
"when": 1784813792772,
"tag": "0005_small_the_phantom",
"breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1785013713432,
"tag": "0006_absurd_dormammu",
"breakpoints": true
}
]
}
@@ -15,7 +15,11 @@ export async function getServerIntegration(provider: string): Promise<ServerInte
return row;
}
export async function upsertServerIntegration(provider: string, config: Record<string, unknown>, enabled = true): Promise<ServerIntegrationSelect> {
export async function upsertServerIntegration(
provider: string,
config: Record<string, unknown>,
enabled = true,
): Promise<ServerIntegrationSelect> {
const [row] = await db
.insert(serverIntegrations)
.values({ provider, config, enabled, updatedAt: new Date() })
@@ -28,7 +32,10 @@ export async function upsertServerIntegration(provider: string, config: Record<s
}
export async function deleteServerIntegration(provider: string): Promise<boolean> {
const result = await db.delete(serverIntegrations).where(eq(serverIntegrations.provider, provider)).returning({ id: serverIntegrations.id });
const result = await db
.delete(serverIntegrations)
.where(eq(serverIntegrations.provider, provider))
.returning({ id: serverIntegrations.id });
return result.length > 0;
}
@@ -57,7 +64,12 @@ type UpsertUserIntegrationParams = {
config: Record<string, unknown>;
};
export async function upsertUserIntegration({ userId, provider, serverIntegrationId, config }: UpsertUserIntegrationParams): Promise<UserIntegrationSelect> {
export async function upsertUserIntegration({
userId,
provider,
serverIntegrationId,
config,
}: UpsertUserIntegrationParams): Promise<UserIntegrationSelect> {
const [row] = await db
.insert(userIntegrations)
.values({ userId, provider, serverIntegrationId: serverIntegrationId ?? null, config, updatedAt: new Date() })
@@ -80,7 +92,7 @@ export async function deleteUserIntegration(userId: number, provider: string): P
// ── Cross-table lookup ──
type UserIntegrationWithUser = UserIntegrationSelect & {
user: { id: number; email: string; username: string | null; role: string };
user: { id: number; email: string; username: string | null };
};
export async function findUserByIntegrationConfig(
@@ -101,16 +113,12 @@ export async function findUserByIntegrationConfig(
id: users.id,
email: users.email,
username: users.username,
role: users.role,
},
})
.from(userIntegrations)
.innerJoin(users, eq(userIntegrations.userId, users.id))
.where(
and(
eq(userIntegrations.provider, provider),
sql`${userIntegrations.config}->>${configKey} = ${configValue}`,
),
and(eq(userIntegrations.provider, provider), sql`${userIntegrations.config}->>${configKey} = ${configValue}`),
);
return row as UserIntegrationWithUser | undefined;
}
+17 -10
View File
@@ -4,8 +4,9 @@ export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: text('email').notNull().unique(),
password: text('password'),
role: text('role', { enum: ['Member', 'Admin', 'Owner', 'Super Admin'] }).notNull().default('Member'),
status: text('status', { enum: ['Unverified', 'Active', 'Prospect', 'Invited', 'Blocked', 'Banned', 'Deleted'] }).notNull().default('Unverified'),
status: text('status', { enum: ['Unverified', 'Active', 'Prospect', 'Invited', 'Blocked', 'Banned', 'Deleted'] })
.notNull()
.default('Unverified'),
name: text('name'),
username: text('username').unique(),
avatar: text('avatar'),
@@ -16,7 +17,9 @@ export const users = pgTable('users', {
export const passkeys = pgTable('passkeys', {
id: serial('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
origin: text('origin'),
credentialId: text('credential_id'),
publicKey: text('public_key'),
@@ -26,16 +29,20 @@ export const passkeys = pgTable('passkeys', {
export const passkeyChallenges = pgTable('passkey_challenges', {
id: serial('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
origin: text('origin').notNull(),
challenge: text('challenge').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
});
export const tokenBlacklist = pgTable('token_blacklist', {
jti: text('jti').primaryKey(),
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
}, (table) => [
index('idx_token_blacklist_expires').on(table.expiresAt),
]);
export const tokenBlacklist = pgTable(
'token_blacklist',
{
jti: text('jti').primaryKey(),
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
},
(table) => [index('idx_token_blacklist_expires').on(table.expiresAt)],
);