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
+86 -75
View File
@@ -1,103 +1,100 @@
'use client'
"use client";
import { Suspense, useEffect, useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import AuthLayout from '../ui/AuthLayout'
import { getApiBase } from '../lib/auth'
import { Suspense, useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import AuthLayout from "../ui/AuthLayout";
import { getApiBase } from "../lib/auth";
type ResetVerification = {
status: string
recipient_hint?: string
auth_provider?: string
expires_at?: string
}
status: string;
recipient_hint?: string;
auth_provider?: string;
expires_at?: string;
};
function ResetPasswordPageContent() {
const router = useRouter()
const searchParams = useSearchParams()
const token = searchParams.get('token') ?? ''
const [verification, setVerification] = useState<ResetVerification | null>(null)
const [loading, setLoading] = useState(false)
const [verifying, setVerifying] = useState(true)
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 token = searchParams.get("token") ?? "";
const [verification, setVerification] = useState<ResetVerification | null>(null);
const [loading, setLoading] = useState(false);
const [verifying, setVerifying] = useState(true);
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [status, setStatus] = useState<string | null>(null);
useEffect(() => {
const verifyToken = async () => {
if (!token) {
setError('Password reset link is invalid or missing.')
setVerifying(false)
return
setError("Password reset link is invalid or missing.");
setVerifying(false);
return;
}
setVerifying(true)
setError(null)
setStatus(null)
setVerifying(true);
setError(null);
setStatus(null);
try {
const baseUrl = getApiBase()
const response = await fetch(
`${baseUrl}/auth/password/reset/verify?token=${encodeURIComponent(token)}`,
)
const data = await response.json().catch(() => null)
const baseUrl = getApiBase();
const response = await fetch(`${baseUrl}/auth/password/reset/verify?token=${encodeURIComponent(token)}`);
const data = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(typeof data?.detail === 'string' ? data.detail : 'Password reset link is invalid.')
throw new Error(typeof data?.detail === "string" ? data.detail : "Password reset link is invalid.");
}
setVerification(data)
setVerification(data);
} catch (err) {
console.error(err)
setVerification(null)
setError(err instanceof Error ? err.message : 'Password reset link is invalid.')
console.error(err);
setVerification(null);
setError(err instanceof Error ? err.message : "Password reset link is invalid.");
} finally {
setVerifying(false)
setVerifying(false);
}
}
};
void verifyToken()
}, [token])
void verifyToken();
}, [token]);
const submit = async (event: React.FormEvent) => {
event.preventDefault()
event.preventDefault();
if (!token) {
setError('Password reset link is invalid or missing.')
return
setError("Password reset link is invalid or missing.");
return;
}
if (password.trim().length < 8) {
setError('Password must be at least 8 characters.')
return
setError("Password must be at least 8 characters.");
return;
}
if (password !== confirmPassword) {
setError('Passwords do not match.')
return
setError("Passwords do not match.");
return;
}
setLoading(true)
setError(null)
setStatus(null)
setLoading(true);
setError(null);
setStatus(null);
try {
const baseUrl = getApiBase()
const baseUrl = getApiBase();
const response = await fetch(`${baseUrl}/auth/password/reset`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token, new_password: password }),
})
const data = await response.json().catch(() => null)
});
const data = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(typeof data?.detail === 'string' ? data.detail : 'Unable to reset password.')
throw new Error(typeof data?.detail === "string" ? data.detail : "Unable to reset password.");
}
setStatus('Password updated. You can now sign in with the new password.')
setPassword('')
setConfirmPassword('')
window.setTimeout(() => router.push('/login'), 1200)
setStatus("Password updated. You can now sign in with the new password.");
setPassword("");
setConfirmPassword("");
window.setTimeout(() => router.push("/login"), 1200);
} catch (err) {
console.error(err)
setError(err instanceof Error ? err.message : 'Unable to reset password.')
console.error(err);
setError(err instanceof Error ? err.message : "Unable to reset password.");
} finally {
setLoading(false)
setLoading(false);
}
}
};
const providerLabel =
verification?.auth_provider === 'jellyfin' ? 'Jellyfin, Seerr, and Magent' : 'Magent'
const providerLabel = verification?.auth_provider === "jellyfin" ? "Jellyfin, Seerr, and Magent" : "Magent";
return (
<AuthLayout title="Reset password" description="Choose a new password of at least 8 characters.">
@@ -105,8 +102,8 @@ function ResetPasswordPageContent() {
{verifying && <div className="status-banner">Checking password reset link</div>}
{!verifying && verification && (
<div className="status-banner">
This reset link was sent to {verification.recipient_hint || 'your email'} and will update the password
used for {providerLabel}.
This reset link was sent to {verification.recipient_hint || "your email"} and will update the password used
for {providerLabel}.
</div>
)}
<label>
@@ -129,25 +126,39 @@ function ResetPasswordPageContent() {
disabled={!verification || loading}
/>
</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={loading || verifying || !verification}>
{loading ? 'Updating password…' : 'Reset password'}
{loading ? "Updating password…" : "Reset password"}
</button>
</div>
<button type="button" className="ghost-button" onClick={() => router.push('/login')} disabled={loading}>
<button type="button" className="ghost-button" onClick={() => router.push("/login")} disabled={loading}>
Back to sign in
</button>
</form>
</AuthLayout>
)
);
}
export default function ResetPasswordPage() {
return (
<Suspense fallback={<AuthLayout title="Reset password" description="Choose a new password for your account."><p role="status">Checking your reset link</p></AuthLayout>}>
<Suspense
fallback={
<AuthLayout title="Reset password" description="Choose a new password for your account.">
<p role="status">Checking your reset link</p>
</AuthLayout>
}
>
<ResetPasswordPageContent />
</Suspense>
)
);
}