fix
This commit is contained in:
@@ -183,11 +183,17 @@
|
||||
});
|
||||
|
||||
const showAdminNav = $derived(
|
||||
Boolean(me?.staff_access?.support_desk || shouldUnlockAllFeatures(me))
|
||||
Boolean(
|
||||
!me?.impersonating &&
|
||||
!me?.staff_tenant_acting &&
|
||||
(me?.staff_access?.support_desk || shouldUnlockAllFeatures(me))
|
||||
)
|
||||
);
|
||||
|
||||
/** Demo + full platform staff only — never unlock via legacy is_platform_admin when staff_access denies full_admin (A1/support). */
|
||||
const unlockAllFeatures = $derived(shouldUnlockAllFeatures(me));
|
||||
/** Demo + full platform staff only — never unlock while impersonating or staff-acting as a tenant. */
|
||||
const unlockAllFeatures = $derived(
|
||||
shouldUnlockAllFeatures(me) && !me?.impersonating && !me?.staff_tenant_acting
|
||||
);
|
||||
|
||||
/**
|
||||
* P1-15: platform-admin readiness strip on /admin only (API fail-closed).
|
||||
@@ -222,7 +228,11 @@
|
||||
});
|
||||
|
||||
const showHypercareAdminTriage = $derived(
|
||||
Boolean(me?.staff_access?.support_desk || shouldUnlockAllFeatures(me))
|
||||
Boolean(
|
||||
!me?.impersonating &&
|
||||
!me?.staff_tenant_acting &&
|
||||
(me?.staff_access?.support_desk || shouldUnlockAllFeatures(me))
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -74,9 +74,12 @@
|
||||
let companiesTotal = $state(0);
|
||||
let companiesOffset = $state(0);
|
||||
let plans = $state<AdminBillingPlan[]>([]);
|
||||
let devTools = $state(false);
|
||||
let userOps = $state(false);
|
||||
let staffRoleApiOk = $state(true);
|
||||
|
||||
let passwordOpen = $state(false);
|
||||
let passwordUser = $state<AdminOrgUser | null>(null);
|
||||
let passwordValue = $state("");
|
||||
let roleOpen = $state(false);
|
||||
let roleUser = $state<AdminOrgUser | null>(null);
|
||||
let roleValue = $state<"" | PlatformStaffRole>("");
|
||||
@@ -118,8 +121,8 @@
|
||||
}
|
||||
try {
|
||||
await Promise.all([reloadUsers(), reloadCompanies(), loadPlans()]);
|
||||
// Local-only password/impersonation helpers — never show in production builds.
|
||||
devTools = !import.meta.env.PROD;
|
||||
// Platform admins can set passwords + impersonate (API enforces staff_role=admin for switch).
|
||||
userOps = true;
|
||||
} catch (err) {
|
||||
error = failureMessage(err, "Failed to load directory");
|
||||
} finally {
|
||||
@@ -372,23 +375,41 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function setDevPassword(userId: string) {
|
||||
busyUserId = userId;
|
||||
function openPasswordDialog(user: AdminOrgUser) {
|
||||
passwordUser = user;
|
||||
passwordValue = "";
|
||||
passwordOpen = true;
|
||||
}
|
||||
|
||||
async function saveForcedPassword(event: Event) {
|
||||
event.preventDefault();
|
||||
if (!passwordUser) return;
|
||||
const pwd = passwordValue.trim();
|
||||
if (pwd.length < 8) {
|
||||
error = "Password must be at least 8 characters";
|
||||
return;
|
||||
}
|
||||
busyUserId = passwordUser.id;
|
||||
error = "";
|
||||
success = "";
|
||||
try {
|
||||
const res = await api<{ email?: string }>(`/api/admin/users/${userId}/dev-password`, {
|
||||
const res = await api<{ email?: string }>(`/api/admin/users/${passwordUser.id}/dev-password`, {
|
||||
method: "POST",
|
||||
body: {}
|
||||
body: { password: pwd }
|
||||
});
|
||||
success = i18n.t("flash.admin.localPasswordSet", { email: res.email ?? "user" });
|
||||
users = users.map((u) => (u.id === userId ? { ...u, must_set_password: false } : u));
|
||||
success = i18n.t("flash.admin.localPasswordSet", { email: res.email ?? passwordUser.email });
|
||||
users = users.map((u) =>
|
||||
u.id === passwordUser!.id ? { ...u, must_set_password: false } : u
|
||||
);
|
||||
passwordOpen = false;
|
||||
passwordUser = null;
|
||||
passwordValue = "";
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 404) {
|
||||
devTools = false;
|
||||
userOps = false;
|
||||
error = i18n.t("flash.admin.localPasswordUnavailable");
|
||||
} else {
|
||||
error = failureMessage(err, "Could not set local password");
|
||||
error = failureMessage(err, "Could not set password");
|
||||
}
|
||||
} finally {
|
||||
busyUserId = null;
|
||||
@@ -405,7 +426,7 @@
|
||||
window.location.assign("/dashboard");
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 404) {
|
||||
devTools = false;
|
||||
userOps = false;
|
||||
error = i18n.t("flash.admin.switchUnavailable");
|
||||
} else {
|
||||
error = failureMessage(err, "Could not switch user");
|
||||
@@ -579,13 +600,13 @@
|
||||
<span class="hidden lg:inline">{i18n.t("admin.users.reissueInvite")}</span>
|
||||
</Button>
|
||||
{/if}
|
||||
{#if devTools}
|
||||
{#if userOps}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
loading={busyUserId === user.id}
|
||||
onclick={() => setDevPassword(user.id)}
|
||||
aria-label={`Set local password for ${user.email}`}
|
||||
onclick={() => openPasswordDialog(user)}
|
||||
aria-label={`Set password for ${user.email}`}
|
||||
>
|
||||
<KeyRound class="h-3.5 w-3.5 lg:mr-1" aria-hidden="true" />
|
||||
<span class="hidden lg:inline">{i18n.t("admin.users.setLocalPassword")}</span>
|
||||
@@ -735,6 +756,35 @@
|
||||
{/if}
|
||||
</PageShell>
|
||||
|
||||
<Dialog
|
||||
bind:open={passwordOpen}
|
||||
title="Set password"
|
||||
description="Force-set a login password for this user (works for fake/legacy emails that cannot receive invites)."
|
||||
>
|
||||
<form class="space-y-4" onsubmit={saveForcedPassword}>
|
||||
{#if error}
|
||||
<p class="text-sm text-destructive" role="alert">{error}</p>
|
||||
{/if}
|
||||
{#if passwordUser}
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{passwordUser.name || "—"} · {passwordUser.email}
|
||||
</p>
|
||||
{/if}
|
||||
<div class="space-y-2">
|
||||
<Label for="forced-password">New password</Label>
|
||||
<Input
|
||||
id="forced-password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
minlength={8}
|
||||
required
|
||||
bind:value={passwordValue}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" loading={busyUserId === passwordUser?.id}>Set password</Button>
|
||||
</form>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
bind:open={roleOpen}
|
||||
title={i18n.t("admin.users.assignRoleTitle")}
|
||||
|
||||
Reference in New Issue
Block a user