the owner can create accounts
POST /api/users plus an Add-account form in Settings > User management. Until now createUser had one call site — bootstrap, gated on an empty user table — so every non-owner account anywhere had been inserted into Postgres by hand. Created accounts are Active. The column defaults to Unverified and signin refuses anything else with a bare UNAUTHORIZED, which is exactly what made the hand-INSERT route look like a wrong password. Also closes a hole found while reading the write path: a second Super Admin was storable. The CHECK constraint pins user 1's role but cannot see other rows, and getOwnerUser() was LIMIT 1 with no ORDER BY, so two holders would have made "who owns this server" a question the query plan answered — and that answer feeds the agent sidecar's identity, vault access and origin scoping. Both write paths now refuse the role and getOwnerUser() orders by id. USER_DIRS and provisionUserDirs move into data-path.ts so the create handler and scripts/provision-user-dirs.ts cannot disagree about what an account's skeleton is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { CreateUserForm } from './CreateUserForm';
|
||||
|
||||
type ManagedUser = {
|
||||
id: number;
|
||||
@@ -27,7 +28,14 @@ type ManagedUser = {
|
||||
isOwner: boolean;
|
||||
};
|
||||
|
||||
type UsersResponse = { users: ManagedUser[]; roles: string[]; ownerId: number };
|
||||
type UsersResponse = {
|
||||
users: ManagedUser[];
|
||||
/** Every role, for displaying the owner's own value. */
|
||||
roles: string[];
|
||||
/** Roles the server will accept in a write. Excludes the owner role — both write paths refuse it. */
|
||||
assignableRoles: string[];
|
||||
ownerId: number;
|
||||
};
|
||||
|
||||
const USERS_KEY = ['MANAGED_USERS'];
|
||||
|
||||
@@ -86,9 +94,11 @@ export const UsersSection = () => {
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Every account on this server. The owner is fixed — the database itself refuses to demote or remove it — so that
|
||||
row cannot be changed from here.
|
||||
row cannot be changed from here, and no other account can be promoted into it.
|
||||
</p>
|
||||
|
||||
<CreateUserForm roles={data.assignableRoles} usersKey={USERS_KEY} />
|
||||
|
||||
<div className="rounded-lg border divide-y">
|
||||
{data.users.map((user) => {
|
||||
const busy = pendingId === user.id;
|
||||
@@ -114,7 +124,9 @@ export const UsersSection = () => {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{data.roles.map((role) => (
|
||||
{/* The owner's row needs its own value present to render at all, and it is disabled
|
||||
anyway. Every other row offers only what the server will accept. */}
|
||||
{(user.isOwner ? data.roles : data.assignableRoles).map((role) => (
|
||||
<SelectItem key={role} value={role}>
|
||||
{role}
|
||||
</SelectItem>
|
||||
|
||||
Reference in New Issue
Block a user