Compare commits
No commits in common. "0a62984e9107eec0158a0518f3f5ece34ffb5794" and "9d0326e3ea48d30b8dc73bd84251f4922d3a6b85" have entirely different histories.
0a62984e91
...
9d0326e3ea
8 changed files with 13 additions and 137 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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). */
|
||||
|
|
|
|||
|
|
@ -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 } {
|
||||
|
|
|
|||
|
|
@ -1,47 +1,27 @@
|
|||
---
|
||||
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');
|
||||
const name = String(data.get('name') ?? '').trim();
|
||||
const bio = String(data.get('bio') ?? '').trim();
|
||||
|
||||
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');
|
||||
}
|
||||
if (name.length < 2) {
|
||||
error = 'Name must be at least 2 characters.';
|
||||
} else if (bio.length > 280) {
|
||||
error = 'Bio must be 280 characters or fewer.';
|
||||
} 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) {
|
||||
error = 'Bio must be 280 characters or fewer.';
|
||||
} else {
|
||||
updateUserProfile(user.id, name, bio);
|
||||
return Astro.redirect('/account?saved=1');
|
||||
}
|
||||
updateUserProfile(user.id, name, bio);
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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 '';
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue