marketplace: minesweeper in the catalogue
Without an entry the plugin is invisible to every server but this one — the catalogue is what a remote host reads, and the bundled constant is only the fallback when marketplace.officer.dev cannot be reached. Its git remote was the tailnet address, which marketplace-sync would have published verbatim: a clone URL that works on this network and nowhere else, failing at authentication in a way that reads as a broken plugin rather than a wrong catalogue. Repointed at gitea.officer.dev, which is the rule the sync script's own comment already spells out. Synced. 18/18, and the entry is live with the right source, permissions and six osDependencies; the repo answers an anonymous https clone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import type { Handler } from 'hono';
|
||||
import { getUsers, getUserById, updateUser, deleteUser, USER_ROLES, OWNER_USER_ID } from 'officerdb';
|
||||
import type { UserRole } from 'officerdb';
|
||||
import { getUsers, getUserById, updateUser, deleteUser, listRoles, getRole, OWNER_USER_ID } from 'officerdb';
|
||||
import * as errors from '@@/custom-errors';
|
||||
import { deprovisionOsAccount } from '@@/os-user-deprovision';
|
||||
import { dropPostgresRole, provisionPostgresRole, rolePermitsDevTools } from '@@/os-user-postgres';
|
||||
@@ -50,13 +49,16 @@ export const toPublicUser = (u: Awaited<ReturnType<typeof getUsers>>[number]): P
|
||||
});
|
||||
|
||||
export const listUsersHandler: Handler = async function (ctx) {
|
||||
const users = await getUsers();
|
||||
const [users, roles] = await Promise.all([getUsers(), listRoles()]);
|
||||
const roleNames = roles.map((r) => r.name);
|
||||
return ctx.json({
|
||||
users: users.sort((a, b) => a.id - b.id).map(toPublicUser),
|
||||
// Every role, so the owner's own row can display its value. The UI must not offer 'Super Admin' in a
|
||||
// picker — both write paths refuse it — which is what `assignableRoles` is for.
|
||||
roles: USER_ROLES,
|
||||
assignableRoles: USER_ROLES.filter((r) => r !== 'Super Admin'),
|
||||
// picker — both write paths refuse it — which is what `assignableRoles` is for. Read from the `roles`
|
||||
// table rather than a hardcoded list (docs/roles-and-teams.md phase 2 part 4) — a role created at
|
||||
// runtime through /api/roles must show up here without a deploy.
|
||||
roles: roleNames,
|
||||
assignableRoles: roleNames.filter((r) => r !== 'Super Admin'),
|
||||
ownerId: OWNER_USER_ID,
|
||||
});
|
||||
};
|
||||
@@ -66,8 +68,11 @@ export const updateUserRoleHandler: Handler = async function (ctx) {
|
||||
if (!Number.isInteger(id) || id < 1) throw errors.BAD_REQUEST('Invalid user id');
|
||||
|
||||
const { role } = ctx.get('body') as { role?: unknown };
|
||||
if (typeof role !== 'string' || !(USER_ROLES as readonly string[]).includes(role)) {
|
||||
throw errors.BAD_REQUEST(`Role must be one of: ${USER_ROLES.join(', ')}`);
|
||||
// The runtime replacement for what USER_ROLES used to guard at the type level (docs/roles-and-teams.md
|
||||
// phase 2 part 4): a role that is not a row in `roles` must never reach `updateUser`, or the FK on
|
||||
// `users.role` throws a raw Postgres error instead of this message.
|
||||
if (typeof role !== 'string' || !(await getRole(role))) {
|
||||
throw errors.BAD_REQUEST(`Unknown role '${String(role)}'`);
|
||||
}
|
||||
|
||||
// ck_users_owner_is_super_admin would reject this anyway — the point of catching it here is the
|
||||
@@ -105,7 +110,7 @@ export const updateUserRoleHandler: Handler = async function (ctx) {
|
||||
// the account still says Developer and the whole thing can be retried. Doing it the other way round would
|
||||
// leave a Member holding database access with nothing in the row to indicate it.
|
||||
const had = rolePermitsDevTools(existing.role);
|
||||
const wants = rolePermitsDevTools(role as UserRole);
|
||||
const wants = rolePermitsDevTools(role);
|
||||
|
||||
if (had && !wants && existing.osUser) {
|
||||
const dropped = await dropPostgresRole(existing.osUser);
|
||||
@@ -125,7 +130,7 @@ export const updateUserRoleHandler: Handler = async function (ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await updateUser(id, { role: role as UserRole });
|
||||
const updated = await updateUser(id, { role });
|
||||
if (!updated) throw errors.NOT_FOUND('User not found');
|
||||
|
||||
// Promotion. Non-fatal and reported, like every other provisioning step: the role change itself has
|
||||
|
||||
Reference in New Issue
Block a user