Repo passa a viver dentro de infra/, com três stacks isoladas que
compartilham uma network Docker externa ('infra-net'):
main/ infra base: postgres + redis + minio + minio-init + pgadmin
+ caddy. Postgres roda em modo multi-banco; o init script
cria os DBs 'wedding' e 'gitea' com roles dedicadas. MinIO
tem um bucket inicial criado pelo minio-init com anonymous
download + CORS. Caddy é o único container expondo 80/443
e roteia por hostname: gitea.{DOMAIN_BASE} / wedding.* /
pgadmin.* / minio.* / media.* (rewrite de bucket).
gitea/ gitea + act_runner. Gitea liga no postgres compartilhado e
usa redis pra cache+sessões. O runner ganha um Dockerfile
pequeno que adiciona docker CLI por cima do
gitea/act_runner pra workflows poderem chamar 'docker
build'. Bootstrap do token de runner documentado no
.env.example.
wedding_photo/ Só a aplicação: 'wedding_app' (FastAPI + SPA) +
postgres-backup + media-backup. Os bancos e o MinIO vêm
da stack main/. A app usa extra_hosts: host-gateway pra
alcançar media.{DOMAIN_BASE} via Caddy mesmo em dev local
— assim a assinatura S3 fecha com o host que o browser
usa pra fazer PUT.
Orquestração:
- Makefile no root: 'make up' sobe tudo na ordem (main -> gitea ->
wedding_photo). 'make up-{main,gitea,wedding}' pra controle
granular. 'make logs-*', 'make down', 'make status', 'make pull-*'.
- network.sh cria a 'infra-net' antes de qualquer up; idempotente.
- Cada stack tem seu próprio .env.example. As creds compartilhadas
(DOMAIN_BASE, MINIO_ROOT_*, WEDDING_DB_*) precisam casar entre
main/.env e o consumidor (gitea/.env ou wedding_photo/.env).
- .gitignore ignora todas as pastas data/ dos volumes.
98 lines
2.9 KiB
TypeScript
98 lines
2.9 KiB
TypeScript
import { useState } from 'react';
|
|
import { Link, useNavigate } from 'react-router-dom';
|
|
|
|
export default function AdminLogin() {
|
|
const navigate = useNavigate();
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
async function submit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const r = await fetch('/api/admin/login', {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({ email: email.trim(), password }),
|
|
});
|
|
if (r.status === 401) {
|
|
setError('Email ou senha incorretos.');
|
|
return;
|
|
}
|
|
if (!r.ok) {
|
|
setError('Erro inesperado. Tenta de novo.');
|
|
return;
|
|
}
|
|
navigate('/admin', { replace: true });
|
|
} catch {
|
|
setError('Falha de rede.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<main className="min-h-full flex items-center justify-center px-6 py-12">
|
|
<form
|
|
onSubmit={submit}
|
|
className="w-full max-w-sm bg-white border border-stone-200 rounded-lg p-6 space-y-4"
|
|
>
|
|
<h1 className="font-display text-2xl text-stone-800 text-center">
|
|
Painel dos noivos
|
|
</h1>
|
|
<p className="text-sm text-stone-500 text-center -mt-2">
|
|
Acesso restrito a Stefanie & Leandro.
|
|
</p>
|
|
|
|
<label className="block">
|
|
<span className="block text-xs font-medium text-stone-600 mb-1">Email</span>
|
|
<input
|
|
type="email"
|
|
required
|
|
autoFocus
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
className="w-full rounded border border-stone-300 px-3 py-2"
|
|
/>
|
|
</label>
|
|
|
|
<label className="block">
|
|
<span className="block text-xs font-medium text-stone-600 mb-1">Senha</span>
|
|
<input
|
|
type="password"
|
|
required
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
className="w-full rounded border border-stone-300 px-3 py-2"
|
|
/>
|
|
</label>
|
|
|
|
{error && (
|
|
<p className="text-sm text-rose-700 bg-rose-50 border border-rose-200 rounded p-2">
|
|
{error}
|
|
</p>
|
|
)}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={loading || !email || !password}
|
|
className="w-full bg-stone-800 text-cream rounded-full py-3 disabled:bg-stone-400 hover:bg-stone-700 transition"
|
|
>
|
|
{loading ? 'Entrando…' : 'Entrar'}
|
|
</button>
|
|
|
|
<Link
|
|
to="/"
|
|
className="block text-center text-xs text-stone-500 hover:text-stone-700"
|
|
>
|
|
← Voltar pra página pública
|
|
</Link>
|
|
</form>
|
|
</main>
|
|
);
|
|
}
|