security: harden data auth and deployment

This commit is contained in:
2026-09-17 18:31:35 +12:00
parent a6d1c73837
commit 5639dbcb83
32 changed files with 1401 additions and 378 deletions
+15 -4
View File
@@ -11,6 +11,7 @@ import { authFetch, clearToken, getApiBase, getToken } from '../../lib/auth'
type ProfileInfo = { username: string; role: string; invite_management_enabled?: boolean }
type OwnedInvite = {
id: number; code: string; label?: string | null; description?: string | null
code_available?: boolean
recipient_email?: string | null; max_uses?: number | null; use_count: number
remaining_uses?: number | null; enabled: boolean; expires_at?: string | null
is_usable?: boolean; created_at?: string | null
@@ -212,12 +213,22 @@ export default function ProfileInvitesPage() {
}
const copyInviteLink = async (invite: OwnedInvite) => {
const url = `${signupBaseUrl}?code=${encodeURIComponent(invite.code)}`
try {
let usableInvite = invite
if (!invite.code_available) {
const response = await authFetch(`${getApiBase()}/auth/profile/invites/${invite.id}/rotate`, {
method: 'POST',
})
if (!response.ok) throw new Error((await response.text()) || 'Could not generate a replacement link.')
const data = await response.json()
usableInvite = data.invite as OwnedInvite
setInvites((current) => current.map((item) => item.id === invite.id ? usableInvite : item))
}
const url = `${signupBaseUrl}?code=${encodeURIComponent(usableInvite.code)}`
await navigator.clipboard.writeText(url)
setStatus(`Copied the link for ${invite.label || invite.code}.`)
setStatus(`Copied the link for ${invite.label || usableInvite.code}. Keep it safe; Magent will not display it again after this page reloads.`)
} catch {
window.prompt('Copy invite link', url)
setError('Could not generate or copy the invite link.')
}
}
@@ -297,7 +308,7 @@ export default function ProfileInvitesPage() {
<div className="profile-invites-list">
<div className="invite-flow-heading"><div><span className="eyebrow">Your invites</span><h2>Created invites</h2><p className="lede">Copy, edit, disable, or remove invitations you have made.</p></div></div>
{invites.length === 0 ? <div className="status-banner">You have not created any invites yet.</div> : <div className="admin-list">{invites.map((invite) => <div key={invite.id} className="admin-list-item"><div className="admin-list-item-main"><div className="admin-list-item-title-row"><strong>{invite.label || 'Unnamed invite'}</strong><code className="invite-code">{invite.code}</code><span className={`small-pill ${invite.is_usable ? '' : 'is-muted'}`}>{invite.is_usable ? 'Ready' : 'Unavailable'}</span></div>{invite.description && <p className="admin-list-item-text admin-list-item-text--muted">{invite.description}</p>}<div className="admin-meta-row"><span>Delivery: {invite.recipient_email || 'Manual link'}</span><span>Uses: {invite.use_count}{typeof invite.max_uses === 'number' ? ` / ${invite.max_uses}` : ''}</span><span>Expires: {formatDate(invite.expires_at)}</span><span>Created: {formatDate(invite.created_at)}</span></div></div><div className="admin-inline-actions"><button type="button" className="ghost-button" onClick={() => void copyInviteLink(invite)}>Copy link</button><button type="button" className="ghost-button" onClick={() => editInvite(invite)}>Edit</button><button type="button" onClick={() => void deleteInvite(invite)}>Delete</button></div></div>)}</div>}
{invites.length === 0 ? <div className="status-banner">You have not created any invites yet.</div> : <div className="admin-list">{invites.map((invite) => <div key={invite.id} className="admin-list-item"><div className="admin-list-item-main"><div className="admin-list-item-title-row"><strong>{invite.label || 'Unnamed invite'}</strong><code className="invite-code">{invite.code}</code><span className={`small-pill ${invite.is_usable ? '' : 'is-muted'}`}>{invite.is_usable ? 'Ready' : 'Unavailable'}</span></div>{invite.description && <p className="admin-list-item-text admin-list-item-text--muted">{invite.description}</p>}<div className="admin-meta-row"><span>Delivery: {invite.recipient_email || 'Manual link'}</span><span>Uses: {invite.use_count}{typeof invite.max_uses === 'number' ? ` / ${invite.max_uses}` : ''}</span><span>Expires: {formatDate(invite.expires_at)}</span><span>Created: {formatDate(invite.created_at)}</span></div></div><div className="admin-inline-actions"><button type="button" className="ghost-button" onClick={() => void copyInviteLink(invite)}>{invite.code_available ? 'Copy link' : 'Generate replacement link'}</button><button type="button" className="ghost-button" onClick={() => editInvite(invite)}>Edit</button><button type="button" onClick={() => void deleteInvite(invite)}>Delete</button></div></div>)}</div>}
</div>
</section>
)}