Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
File size: 6,650 Bytes
22f163e c653f27 22f163e c653f27 22f163e c653f27 22f163e c653f27 22f163e c653f27 22f163e c653f27 22f163e c653f27 22f163e c653f27 22f163e c653f27 22f163e c653f27 22f163e c653f27 22f163e c653f27 22f163e c653f27 22f163e c653f27 22f163e c653f27 22f163e c653f27 22f163e c653f27 22f163e c653f27 22f163e c653f27 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | import { useState, useEffect, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import {
getTemplateStudioAuthStatus,
verifyTemplateStudioPassword,
} from "../../api/client";
const SESSION_KEY = "template_studio_auth";
function errorMessageFromAxios(err: unknown): string {
if (!axios.isAxiosError(err)) {
return err instanceof Error ? err.message : "Something went wrong. Try again.";
}
const status = err.response?.status;
const data = err.response?.data as { detail?: unknown } | undefined;
const detail = data?.detail;
if (typeof detail === "string") return detail;
if (Array.isArray(detail) && detail.length > 0 && typeof detail[0] === "object") {
const first = detail[0] as { msg?: string };
if (first.msg) return first.msg;
}
if (status === 429) {
const retry = err.response?.headers?.["retry-after"];
return retry
? `Too many attempts. Retry in ${retry}s.`
: "Too many attempts. Please wait before trying again.";
}
return err.message || "Incorrect password or server error.";
}
interface PasswordProtectedRouteProps {
children: React.ReactNode;
redirectTo?: string;
}
export default function PasswordProtectedRoute({
children,
redirectTo = "/",
}: PasswordProtectedRouteProps) {
const navigate = useNavigate();
const [authenticated, setAuthenticated] = useState<boolean>(() => {
return sessionStorage.getItem(SESSION_KEY) === "true";
});
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [shake, setShake] = useState(false);
const [checkingGate, setCheckingGate] = useState(true);
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
if (authenticated) {
setCheckingGate(false);
return;
}
let cancelled = false;
(async () => {
try {
const { gated } = await getTemplateStudioAuthStatus();
if (cancelled) return;
if (!gated) {
sessionStorage.setItem(SESSION_KEY, "true");
setAuthenticated(true);
}
} catch {
// Network / server error — show password UI (may still be gated server-side).
} finally {
if (!cancelled) setCheckingGate(false);
}
})();
return () => {
cancelled = true;
};
}, [authenticated]);
const handleSubmit = useCallback(async () => {
const trimmed = password.trim();
if (!trimmed || submitting) return;
setSubmitting(true);
setError("");
try {
const res = await verifyTemplateStudioPassword(trimmed);
if (res.ok) {
sessionStorage.setItem(SESSION_KEY, "true");
setAuthenticated(true);
setPassword("");
} else {
setError("Could not verify access.");
setShake(true);
setTimeout(() => setShake(false), 400);
}
} catch (err) {
setError(errorMessageFromAxios(err));
setShake(true);
setTimeout(() => setShake(false), 400);
setPassword("");
} finally {
setSubmitting(false);
}
}, [password, submitting]);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") void handleSubmit();
};
if (authenticated) return <>{children}</>;
if (checkingGate) {
return (
<div className="min-h-screen flex items-center justify-center p-6 bg-gradient-to-br from-gray-50 to-gray-100">
<div className="flex flex-col items-center gap-3 text-gray-500 text-sm">
<span className="w-8 h-8 border-2 border-purple-200 border-t-purple-600 rounded-full animate-spin" />
<span>Checking access…</span>
</div>
</div>
);
}
return (
<div className="min-h-screen flex items-center justify-center p-6 bg-gradient-to-br from-gray-50 to-gray-100">
<div
className={`w-full max-w-md p-10 rounded-2xl border border-white/40
backdrop-blur-xl bg-white/80 shadow-xl transition ${
shake ? "animate-[shake_.4s]" : ""
}`}
>
{/* Lock Icon */}
<div className="w-11 h-11 mb-5 flex items-center justify-center rounded-xl bg-purple-100 text-purple-600 text-xl">
🔒
</div>
{/* Title */}
<h2 className="text-xl font-semibold text-gray-900">
Template Studio
</h2>
<p className="text-sm text-gray-500 mt-1 mb-6">
Enter the password to access the template studio.
</p>
{/* Label */}
<label className="block text-xs font-medium uppercase tracking-wide text-gray-500 mb-1">
Access Password
</label>
{/* Input */}
<div className="relative">
<input
type="password"
value={password}
disabled={submitting}
autoFocus
onChange={(e) => {
setPassword(e.target.value);
setError("");
}}
onKeyDown={handleKeyDown}
className="w-full rounded-lg border border-gray-300 px-3 py-2 pr-10 text-sm
focus:outline-none focus:ring-2 focus:ring-purple-400 focus:border-purple-400"
/>
</div>
{/* Error */}
<div className="text-xs text-red-500 mt-2 min-h-[16px]">
{error}
</div>
{/* Button */}
<button
type="button"
onClick={() => void handleSubmit()}
disabled={!password.trim() || submitting}
className="w-full mt-4 py-2 rounded-lg text-sm font-medium text-white
bg-gradient-to-r from-purple-600 to-purple-700
hover:shadow-lg hover:-translate-y-[1px] transition
disabled:opacity-50 disabled:cursor-not-allowed"
>
{submitting ? (
<span className="inline-flex items-center justify-center gap-2">
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
Verifying…
</span>
) : (
"Unlock Access"
)}
</button>
{/* Back */}
<button
type="button"
onClick={() => navigate(redirectTo)}
disabled={submitting}
className="w-full mt-4 text-xs text-gray-500 hover:text-gray-700 disabled:opacity-50"
>
← Return to homepage
</button>
</div>
{/* Tailwind custom animation */}
<style>
{`
@keyframes shake {
0%,100%{transform:translateX(0)}
25%{transform:translateX(-6px)}
75%{transform:translateX(6px)}
}
`}
</style>
</div>
);
}
|