chore: standardize security and quality foundations
Magent CI/CD / verify (push) Failing after 9m34s
Magent CI/CD / deploy-beta (push) Skipped

This commit is contained in:
2026-09-17 20:03:47 +12:00
parent 5639dbcb83
commit f852e7c941
127 changed files with 17928 additions and 10741 deletions
+154 -117
View File
@@ -1,139 +1,146 @@
'use client'
"use client";
import { Suspense, useEffect, useMemo, useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import AuthLayout from '../ui/AuthLayout'
import { clearToken, getApiBase, setToken } from '../lib/auth'
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import AuthLayout from "../ui/AuthLayout";
import { clearToken, getApiBase, setToken } from "../lib/auth";
type InviteInfo = {
code: string
email_bound?: boolean
label?: string | null
description?: string | null
enabled: boolean
is_expired?: boolean
is_usable?: boolean
expires_at?: string | null
max_uses?: number | null
use_count?: number | null
remaining_uses?: number | null
code: string;
email_bound?: boolean;
label?: string | null;
description?: string | null;
enabled: boolean;
is_expired?: boolean;
is_usable?: boolean;
expires_at?: string | null;
max_uses?: number | null;
use_count?: number | null;
remaining_uses?: number | null;
profile?: {
id: number
name: string
description?: string | null
} | null
}
id: number;
name: string;
description?: string | null;
} | null;
};
const formatDate = (value?: string | null) => {
if (!value) return 'Never'
const date = new Date(value)
if (Number.isNaN(date.valueOf())) return value
return date.toLocaleString()
}
if (!value) return "Never";
const date = new Date(value);
if (Number.isNaN(date.valueOf())) return value;
return date.toLocaleString();
};
function SignupPageContent() {
const router = useRouter()
const searchParams = useSearchParams()
const [inviteCode, setInviteCode] = useState(searchParams.get('code') ?? '')
const [invite, setInvite] = useState<InviteInfo | null>(null)
const [inviteLoading, setInviteLoading] = useState(false)
const [loading, setLoading] = useState(false)
const [username, setUsername] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [status, setStatus] = useState<string | null>(null)
const router = useRouter();
const searchParams = useSearchParams();
const [inviteCode, setInviteCode] = useState(searchParams.get("code") ?? "");
const [invite, setInvite] = useState<InviteInfo | null>(null);
const [inviteLoading, setInviteLoading] = useState(false);
const [loading, setLoading] = useState(false);
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [status, setStatus] = useState<string | null>(null);
const canSubmit = useMemo(() => {
return Boolean(invite?.is_usable && (invite.email_bound || email.trim()) && username.trim() && password && !loading && !inviteLoading)
}, [invite, email, username, password, loading, inviteLoading])
return Boolean(
invite?.is_usable &&
(invite.email_bound || email.trim()) &&
username.trim() &&
password &&
!loading &&
!inviteLoading,
);
}, [invite, email, username, password, loading, inviteLoading]);
const lookupInvite = async (code: string) => {
const trimmed = code.trim()
const lookupInvite = useCallback(async (code: string) => {
const trimmed = code.trim();
if (!trimmed) {
setInvite(null)
return
setInvite(null);
return;
}
setInviteLoading(true)
setError(null)
setStatus(null)
setInviteLoading(true);
setError(null);
setStatus(null);
try {
const baseUrl = getApiBase()
const response = await fetch(`${baseUrl}/auth/invites/${encodeURIComponent(trimmed)}`)
const baseUrl = getApiBase();
const response = await fetch(`${baseUrl}/auth/invites/${encodeURIComponent(trimmed)}`);
if (!response.ok) {
const text = await response.text()
throw new Error(text || 'Invite not found')
const text = await response.text();
throw new Error(text || "Invite not found");
}
const data = await response.json()
setInvite(data?.invite ?? null)
setStatus('Invite loaded.')
const data = await response.json();
setInvite(data?.invite ?? null);
setStatus("Invite loaded.");
} catch (err) {
console.error(err)
setInvite(null)
setError('Invite code not found or unavailable.')
console.error(err);
setInvite(null);
setError("Invite code not found or unavailable.");
} finally {
setInviteLoading(false)
setInviteLoading(false);
}
}
}, []);
useEffect(() => {
const initialCode = searchParams.get('code') ?? ''
const initialCode = searchParams.get("code") ?? "";
if (initialCode) {
setInviteCode(initialCode)
void lookupInvite(initialCode)
setInviteCode(initialCode);
void lookupInvite(initialCode);
}
}, [searchParams])
}, [lookupInvite, searchParams]);
const submit = async (event: React.FormEvent) => {
event.preventDefault()
event.preventDefault();
if (password !== confirmPassword) {
setError('Passwords do not match.')
return
setError("Passwords do not match.");
return;
}
if (!inviteCode.trim()) {
setError('Invite code is required.')
return
setError("Invite code is required.");
return;
}
if (!invite?.is_usable) {
setError('Invite is not usable. Refresh invite details or ask an admin for a new code.')
return
setError("Invite is not usable. Refresh invite details or ask an admin for a new code.");
return;
}
setLoading(true)
setError(null)
setStatus(null)
setLoading(true);
setError(null);
setStatus(null);
try {
clearToken()
const baseUrl = getApiBase()
clearToken();
const baseUrl = getApiBase();
const response = await fetch(`${baseUrl}/auth/signup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
invite_code: inviteCode,
username: username.trim(),
...(!invite.email_bound ? { email: email.trim() } : {}),
password,
}),
})
});
if (!response.ok) {
const text = await response.text()
throw new Error(text || 'Sign-up failed')
const text = await response.text();
throw new Error(text || "Sign-up failed");
}
const data = await response.json()
const data = await response.json();
if (data?.authenticated) {
setToken('cookie')
window.location.href = '/welcome'
return
setToken("cookie");
window.location.href = "/welcome";
return;
}
throw new Error('Sign-up did not complete')
throw new Error("Sign-up did not complete");
} catch (err) {
console.error(err)
setError(err instanceof Error ? err.message : 'Unable to create account.')
console.error(err);
setError(err instanceof Error ? err.message : "Unable to create account.");
} finally {
setLoading(false)
setLoading(false);
}
}
};
return (
<AuthLayout title="Create account" description="Your invite is the first step to Grizzlyflix.">
@@ -143,7 +150,11 @@ function SignupPageContent() {
<div className="invite-lookup-row">
<input
value={inviteCode}
onChange={(e) => { setInviteCode(e.target.value); setInvite(null); setEmail('') }}
onChange={(e) => {
setInviteCode(e.target.value);
setInvite(null);
setEmail("");
}}
placeholder="Paste your invite code"
autoCapitalize="characters"
/>
@@ -153,38 +164,50 @@ function SignupPageContent() {
disabled={inviteLoading}
onClick={() => void lookupInvite(inviteCode)}
>
{inviteLoading ? 'Checking…' : 'Check invite'}
{inviteLoading ? "Checking…" : "Check invite"}
</button>
</div>
</label>
{invite && (
<div className={`invite-summary ${invite.is_usable ? '' : 'is-disabled'}`}>
<div className={`invite-summary ${invite.is_usable ? "" : "is-disabled"}`}>
<div className="invite-summary-row">
<strong>{invite.label || invite.code}</strong>
<span className={`small-pill ${invite.is_usable ? '' : 'is-muted'}`}>
{invite.is_usable ? 'Ready' : 'Unavailable'}
<span className={`small-pill ${invite.is_usable ? "" : "is-muted"}`}>
{invite.is_usable ? "Ready" : "Unavailable"}
</span>
</div>
{invite.description && <p>{invite.description}</p>}
<details className="auth-invite-details"><summary>Invite details</summary><div className="admin-meta-row">
<span>Code: {invite.code}</span>
<span>Expires: {formatDate(invite.expires_at)}</span>
<span>Remaining uses: {invite.remaining_uses ?? 'Unlimited'}</span>
<span>Profile: {invite.profile?.name || 'None'}</span>
</div></details>
<details className="auth-invite-details">
<summary>Invite details</summary>
<div className="admin-meta-row">
<span>Code: {invite.code}</span>
<span>Expires: {formatDate(invite.expires_at)}</span>
<span>Remaining uses: {invite.remaining_uses ?? "Unlimited"}</span>
<span>Profile: {invite.profile?.name || "None"}</span>
</div>
</details>
</div>
)}
{invite?.email_bound ? <p className="account-hint">Your account will use the email address this invitation was sent to. This invitation can be used once.</p> : <label>
Email address
<input type="email" required value={email} onChange={(e) => setEmail(e.target.value)} autoComplete="email" placeholder="you@example.com" />
</label>}
{invite?.email_bound ? (
<p className="account-hint">
Your account will use the email address this invitation was sent to. This invitation can be used once.
</p>
) : (
<label>
Email address
<input
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
autoComplete="email"
placeholder="you@example.com"
/>
</label>
)}
<label>
Username
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
autoComplete="username"
/>
<input value={username} onChange={(e) => setUsername(e.target.value)} autoComplete="username" />
</label>
<label>
Password
@@ -204,25 +227,39 @@ function SignupPageContent() {
autoComplete="new-password"
/>
</label>
{error && <div className="account-notice is-error" role="alert">{error}</div>}
{status && <div className="account-notice is-status" role="status">{status}</div>}
{error && (
<div className="account-notice is-error" role="alert">
{error}
</div>
)}
{status && (
<div className="account-notice is-status" role="status">
{status}
</div>
)}
<div className="auth-actions">
<button type="submit" className="account-primary" disabled={!canSubmit}>
{loading ? 'Creating account…' : 'Create account'}
{loading ? "Creating account…" : "Create account"}
</button>
</div>
<button type="button" className="ghost-button" disabled={loading} onClick={() => router.push('/login')}>
<button type="button" className="ghost-button" disabled={loading} onClick={() => router.push("/login")}>
Back to sign in
</button>
</form>
</AuthLayout>
)
);
}
export default function SignupPage() {
return (
<Suspense fallback={<AuthLayout title="Create account" description="Your invite is the first step to Grizzlyflix."><p role="status">Loading sign-up</p></AuthLayout>}>
<Suspense
fallback={
<AuthLayout title="Create account" description="Your invite is the first step to Grizzlyflix.">
<p role="status">Loading sign-up</p>
</AuthLayout>
}
>
<SignupPageContent />
</Suspense>
)
);
}