Compare commits

..

No commits in common. "0a62984e9107eec0158a0518f3f5ece34ffb5794" and "9d0326e3ea48d30b8dc73bd84251f4922d3a6b85" have entirely different histories.

8 changed files with 13 additions and 137 deletions

View file

@ -59,7 +59,7 @@ const hasAnyResources = groupedEntries.some((g) => g.entries.length > 0);
<!-- ── Top strip ──────────────────────────────────────────────── -->
<header class="bs-topbar" role="banner">
<a href="/" class="bs-brand" aria-label="Back to the main menu">
<a href="/admin" class="bs-brand" aria-label="Backstage — home">
<img src="/logo.svg" alt="Fenja AI" class="bs-brand-mark" />
<span class="bs-brand-sep" aria-hidden="true">·</span>
<span class="bs-brand-project">Project <em class="bs-brand-bifrost">Bifrost</em></span>

View file

@ -90,8 +90,6 @@ const summaryEntries = isReviewMode && item
// One-shot invite link surfaced after create/action — read from URL
const inviteUrl = Astro.url.searchParams.get('invite_url');
// One-shot temp password surfaced after a password reset — read from URL
const tempPassword = Astro.url.searchParams.get('temp_password');
// Build the close URL — drop edit/new but keep filter/q/page
const closeUrl = (() => {
@ -135,16 +133,6 @@ const formAction = Astro.url.pathname + Astro.url.search;
</section>
)}
{tempPassword && (
<section class="bs-invite-result" data-temp-password-block>
<p class="bs-invite-result-label">New temporary password — copy and send it to the user. They can change it from their account page. It will not be shown again.</p>
<div class="bs-invite-link-row">
<code class="bs-invite-link" id="bs-temp-password">{tempPassword}</code>
<button type="button" class="bs-copy-btn" data-copy-target="#bs-temp-password">Copy</button>
</div>
</section>
)}
{isReviewMode ? (
<dl class="bs-summary">
{summaryEntries.map((entry) => (

View file

@ -264,12 +264,7 @@ export interface ActionResultInviteLink {
kind: 'invite-link';
url: string;
}
/** Render a freshly-generated temp password in the panel with a Copy button. */
export interface ActionResultTempPassword {
kind: 'temp-password';
password: string;
}
export type ActionResult = ActionResultToast | ActionResultInviteLink | ActionResultTempPassword;
export type ActionResult = ActionResultToast | ActionResultInviteLink;
// ── Actions (publish, archive, approve, etc.) ───────────────────────────────
export interface ResourceAction<T> {

View file

@ -16,14 +16,12 @@ import {
getUserPublicById,
updateUserAdminFields,
updateUserEmail,
updateUserPassword,
updateUserProfile,
updateUserRole,
deactivateUser,
type Role,
type UserPublic,
} from '../../lib/db';
import { generateTempPassword, hashPassword } from '../../lib/auth';
import { parseFocusTags, readFocusTags } from '../../lib/format';
import type { Resource } from '../resource-types';
@ -328,20 +326,6 @@ export const usersResource: Resource<UserPublic> = {
delete: (id) => deactivateUser(id),
},
actions: [
{
key: 'reset-password',
label: 'Reset password',
confirmText:
'Generate a new temporary password for this user? Their current password stops working immediately. You will get a password to send them.',
handler: (id) => {
const temp = generateTempPassword();
updateUserPassword(id, hashPassword(temp));
return { kind: 'temp-password', password: temp };
},
},
],
notifyCount: {
// CAB members without focus tags read as half-finished profiles —
// surface them as something to attend to.

View file

@ -22,12 +22,6 @@ export function verifyPassword(password: string, hash: string): boolean {
return bcrypt.compareSync(password, hash);
}
/** A readable one-time password for admin resets. Give to the user; they
* change it from /account. */
export function generateTempPassword(): string {
return 'Bifrost-' + randomBytes(4).toString('hex');
}
// ── Invite tokens ────────────────────────────────────────────────
/** Returns the URL-safe token (give to user) and its hash (store in DB). */

View file

@ -161,11 +161,6 @@ export function updateUserEmail(id: number, email: string): void {
db.prepare('UPDATE users SET email = ? WHERE id = ?').run(email, id);
}
/** Set a user's password hash (self-service change or admin reset). */
export function updateUserPassword(id: number, passwordHash: string): void {
db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(passwordHash, id);
}
/** Returns the newly-allocated member_number when the transition lands on
* cab and the user had none; null otherwise. Callers may ignore. */
export function updateUserRole(id: number, role: Role): { allocated: number | null } {

View file

@ -1,34 +1,16 @@
---
import AppLayout from '../layouts/AppLayout.astro';
import { updateUserProfile, updateUserPassword, getUserByEmail } from '../lib/db';
import { verifyPassword, hashPassword } from '../lib/auth';
import { updateUserProfile } from '../lib/db';
const user = Astro.locals.user;
let success = false;
let error: string | null = null;
let pwError: string | null = null;
if (Astro.request.method === 'POST') {
const data = await Astro.request.formData();
const intent = String(data.get('intent') ?? 'profile');
if (intent === 'password') {
const current = String(data.get('current_password') ?? '');
const next = String(data.get('new_password') ?? '');
const confirm = String(data.get('confirm_password') ?? '');
const row = getUserByEmail(user.email);
if (!row || !verifyPassword(current, row.password_hash)) {
pwError = 'Current password is incorrect.';
} else if (next.length < 8) {
pwError = 'New password must be at least 8 characters.';
} else if (next !== confirm) {
pwError = 'New password and confirmation do not match.';
} else {
updateUserPassword(user.id, hashPassword(next));
return Astro.redirect('/account?pwchanged=1');
}
} else {
const name = String(data.get('name') ?? '').trim();
const bio = String(data.get('bio') ?? '').trim();
if (name.length < 2) {
error = 'Name must be at least 2 characters.';
} else if (bio.length > 280) {
@ -38,10 +20,8 @@ if (Astro.request.method === 'POST') {
return Astro.redirect('/account?saved=1');
}
}
}
const saved = Astro.url.searchParams.get('saved') === '1';
const pwChanged = Astro.url.searchParams.get('pwchanged') === '1';
---
<AppLayout title="Account" user={user}>
<div class="page">
@ -61,7 +41,6 @@ const pwChanged = Astro.url.searchParams.get('pwchanged') === '1';
)}
<form method="POST" class="account-form" novalidate>
<input type="hidden" name="intent" value="profile" />
<div class="field">
<label for="name" class="label-sm field-label">
Display name
@ -115,65 +94,9 @@ const pwChanged = Astro.url.searchParams.get('pwchanged') === '1';
</div>
</dl>
<p class="body-sm reset-note">
To change your email, contact Fenja — we will issue a new invite link.
To change your email or password, contact Fenja — we will issue a new invite link.
</p>
</div>
<form method="POST" class="account-form" novalidate>
<input type="hidden" name="intent" value="password" />
<h2 class="label-sm section-heading">Change password</h2>
{pwChanged && (
<p class="success-msg body-sm" role="status">Password updated.</p>
)}
{pwError && (
<p class="error-msg body-sm" role="alert">{pwError}</p>
)}
<div class="field">
<label for="current_password" class="label-sm field-label">Current password</label>
<input
type="password"
id="current_password"
name="current_password"
class="input body-md"
required
autocomplete="current-password"
/>
</div>
<div class="field">
<label for="new_password" class="label-sm field-label">
New password
<span class="label-sm field-hint">At least 8 characters</span>
</label>
<input
type="password"
id="new_password"
name="new_password"
class="input body-md"
required
minlength="8"
autocomplete="new-password"
/>
</div>
<div class="field">
<label for="confirm_password" class="label-sm field-label">Confirm new password</label>
<input
type="password"
id="confirm_password"
name="confirm_password"
class="input body-md"
required
minlength="8"
autocomplete="new-password"
/>
</div>
<div class="form-actions">
<button type="submit" class="btn-primary body-md">Update password</button>
</div>
</form>
</div>
</div>

View file

@ -189,9 +189,6 @@ function resultRedirectParam(r: ActionResult | undefined): string {
if (r.kind === 'invite-link') {
return `&invite_url=${encodeURIComponent(r.url)}`;
}
if (r.kind === 'temp-password') {
return `&temp_password=${encodeURIComponent(r.password)}`;
}
return '';
}