web: PRO needs a proven address — email verification gates the studio
A signed-in account is served exactly like a guest until it opens the verification link: watermarked 2048px export, no saving, no PRO frames, GPS stamp or HDF. SMTP is declared in .env; with SMTP_HOST unset the link goes to the container log. Allowlisted admins count as verified.
This commit is contained in:
@@ -4,29 +4,53 @@ import { api } from '../api';
|
||||
|
||||
// Email + password only: the cookie is set by the API container, this dialog
|
||||
// just drives it. The demo is always one click away — the app is usable signed
|
||||
// out, and only export resolution / saving are gated (see config/tiers.ts).
|
||||
// out, and only the PRO half is gated (see config/tiers.ts).
|
||||
//
|
||||
// Three faces, one form. `verify` is the third: the account exists but the
|
||||
// address it gave has not been confirmed, so until the visitor opens the link
|
||||
// it is served exactly like a guest. A fresh signup lands on that face by
|
||||
// itself — telling someone their account is ready when it is not is the one
|
||||
// thing this dialog must not do.
|
||||
export function AuthModal({
|
||||
initialMode = 'login',
|
||||
email,
|
||||
onClose,
|
||||
onDone,
|
||||
}: {
|
||||
initialMode?: 'login' | 'signup';
|
||||
initialMode?: 'login' | 'signup' | 'verify';
|
||||
// The account already signed in, when the studio opens the verify face from
|
||||
// its own banner.
|
||||
email?: string;
|
||||
onClose: () => void;
|
||||
onDone: () => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [mode, setMode] = useState<'login' | 'signup'>(initialMode);
|
||||
const [email, setEmail] = useState('');
|
||||
const [mode, setMode] = useState<'login' | 'signup' | 'verify'>(initialMode);
|
||||
const [address, setAddress] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [sent, setSent] = useState(false);
|
||||
|
||||
const submit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
await (mode === 'login' ? api.login(email, password) : api.signup(email, password));
|
||||
if (mode === 'verify') {
|
||||
await api.resendVerification();
|
||||
setSent(true);
|
||||
return;
|
||||
}
|
||||
const { user } = await (mode === 'login'
|
||||
? api.login(address, password)
|
||||
: api.signup(address, password));
|
||||
setAddress(user.email);
|
||||
if (!user.verified) {
|
||||
if (mode === 'signup') setSent(true); // signup mails the first link itself
|
||||
setMode('verify');
|
||||
return;
|
||||
}
|
||||
onDone();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
@@ -35,6 +59,26 @@ export function AuthModal({
|
||||
}
|
||||
};
|
||||
|
||||
if (mode === 'verify') {
|
||||
const who = email || address;
|
||||
return (
|
||||
<div className="modal-backdrop" onMouseDown={(e) => e.target === e.currentTarget && onClose()}>
|
||||
<form className="modal" onSubmit={submit}>
|
||||
<h2>{t('auth.verifyTitle')}</h2>
|
||||
<p className="hint">{t('auth.verifyBody', { email: who })}</p>
|
||||
<p className="hint">{t('auth.verifyProHint')}</p>
|
||||
{sent ? <p className="hint ok">{t('auth.verifySent')}</p> : null}
|
||||
{error ? <p className="err">{error}</p> : null}
|
||||
<button type="submit" className="btn primary" data-key="verify-resend" disabled={busy}>
|
||||
{busy ? t('auth.busy') : t('pro.resend')}
|
||||
</button>
|
||||
<button type="button" className="btn" onClick={onDone}>{t('auth.verifiedDone')}</button>
|
||||
<button type="button" className="btn ghost" onClick={onClose}>{t('auth.continueDemo')}</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" onMouseDown={(e) => e.target === e.currentTarget && onClose()}>
|
||||
<form className="modal" onSubmit={submit}>
|
||||
@@ -45,8 +89,8 @@ export function AuthModal({
|
||||
required
|
||||
autoComplete="email"
|
||||
placeholder={t('auth.email')}
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
value={address}
|
||||
onChange={(e) => setAddress(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="field"
|
||||
@@ -59,6 +103,7 @@ export function AuthModal({
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<p className="hint">{t('auth.passwordHint')}</p>
|
||||
{mode === 'signup' ? <p className="hint">{t('auth.signupVerifyHint')}</p> : null}
|
||||
{error ? <p className="err">{error}</p> : null}
|
||||
<button type="submit" className="btn primary" disabled={busy}>
|
||||
{busy ? t('auth.busy') : mode === 'login' ? t('nav.login') : t('nav.signup')}
|
||||
|
||||
Reference in New Issue
Block a user