import type { Handler } from 'hono'; import { getUserById, OWNER_USER_ID } from 'officerdb'; import * as errors from '@@/custom-errors'; import { validatePublicKey } from '@@/os-user-ssh'; import { provisionOsAccount } from './provision-os'; // POST /api/users/:id/provision-linux — give an existing account its Linux side, or repair it. // // One route for what are the same operation from the owner's point of view: // // backfill an account created before per-user Linux accounts existed, or while the host was not set up // for them, gets one now. // retry the first attempt failed for a reason the owner has since fixed — the traversable-ancestor // chmod being the one everybody hits once. // re-key a new inbound public key replaces the old `authorized_keys`. // // Before this, the answer to all three was "delete the account and create it again", which throws away the // password, the dashboards and everything else keyed to the row to redo a retryable side effect. export const provisionLinuxHandler: Handler = async function (ctx) { const id = Number(ctx.req.param('id')); if (!Number.isInteger(id) || id < 1) throw errors.BAD_REQUEST('Invalid user id'); // The owner runs as the service user itself and its home is HOME_DIR — there is nothing to provision, and // creating a second Linux account for it would be actively confusing. if (id === OWNER_USER_ID) throw errors.BAD_REQUEST('The server owner already runs as the service account.'); const user = await getUserById(id); if (!user) throw errors.NOT_FOUND('User not found'); if (!user.username) throw errors.BAD_REQUEST('This account has no username to name a Linux user after.'); // Optional. Absent means "leave authorized_keys as it is" rather than "remove inbound access": clearing a // key should be a deliberate act, not the consequence of submitting a form with an empty field. const body = (ctx.get('body') ?? {}) as Record; const raw = typeof body.sshPublicKey === 'string' ? body.sshPublicKey.trim() : ''; let inboundKey: string | null = null; if (raw) { const checked = validatePublicKey(raw); if (!checked.ok) throw errors.BAD_REQUEST(`SSH public key: ${checked.error}`); inboundKey = checked.key; } const result = await provisionOsAccount({ userId: user.id, email: user.email, username: user.username, inboundKey, }); // 200 with the error in the body rather than a 4xx: the interesting cases are partial. "The Linux account // exists and is confined but the keys failed" is not an error the caller should treat as nothing having // happened, and the UI has to be able to show both halves. return ctx.json(result); };