Initial SeConectai briefing system

This commit is contained in:
2026-10-04 10:42:23 -03:00
commit 45cd8d2f03
163 changed files with 12659 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
"use server";
import { createHash, randomBytes } from "crypto";
import { db } from "@/lib/db";
import { sendMail } from "@/lib/mail";
import { checkRateLimit } from "@/lib/rate-limit";
export type RecuperarSenhaState = {
ok: boolean;
message?: string;
};
function hashToken(token: string) {
return createHash("sha256").update(token).digest("hex");
}
export async function solicitarRecuperacao(
_state: RecuperarSenhaState,
formData: FormData
): Promise<RecuperarSenhaState> {
const email = formData.get("email")?.toString().trim().toLowerCase();
const genericMessage = "Se o e-mail estiver cadastrado, enviaremos as instrucoes de redefinicao.";
const limit = checkRateLimit(`recuperar-senha:${email ?? "sem-email"}`, 5, 60 * 60 * 1000);
if (!limit.allowed) {
return { ok: true, message: genericMessage };
}
if (!email) {
return { ok: true, message: genericMessage };
}
const user = await db.user.findUnique({
where: { email },
select: { id: true, email: true }
});
if (!user) {
return { ok: true, message: genericMessage };
}
const token = randomBytes(32).toString("base64url");
const tokenHash = hashToken(token);
const expiraEm = new Date(Date.now() + 60 * 60 * 1000);
await db.passwordResetToken.create({
data: {
userId: user.id,
tokenHash,
expiraEm
}
});
const baseUrl = process.env.APP_URL ?? "http://localhost:3000";
const resetUrl = `${baseUrl}/redefinir-senha?token=${token}`;
await sendMail({
to: user.email,
subject: "Redefinicao de senha - Briefing SeConectai",
text: `Use o link abaixo para redefinir sua senha. Ele expira em 1 hora.\n\n${resetUrl}`
});
return { ok: true, message: genericMessage };
}
export { hashToken };
+26
View File
@@ -0,0 +1,26 @@
import Link from "next/link";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { RecuperarSenhaForm } from "./recuperar-senha-form";
export default function RecuperarSenhaPage() {
return (
<main className="container flex min-h-screen items-center justify-center py-8">
<Card className="w-full max-w-md shadow-sm">
<CardHeader>
<CardTitle>Recuperar senha</CardTitle>
<p className="text-sm leading-6 text-muted-foreground">
Informe seu e-mail para receber as instrucoes de redefinicao.
</p>
</CardHeader>
<CardContent>
<RecuperarSenhaForm />
<p className="mt-6 text-center text-sm text-muted-foreground">
<Link href="/login" className="text-primary underline-offset-4 hover:underline">
Voltar para o login
</Link>
</p>
</CardContent>
</Card>
</main>
);
}
@@ -0,0 +1,39 @@
"use client";
import { useFormState, useFormStatus } from "react-dom";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { solicitarRecuperacao, type RecuperarSenhaState } from "./actions";
const initialState: RecuperarSenhaState = { ok: false };
function SubmitButton() {
const { pending } = useFormStatus();
return (
<Button type="submit" className="w-full" disabled={pending}>
{pending ? "Enviando..." : "Enviar instrucoes"}
</Button>
);
}
export function RecuperarSenhaForm() {
const [state, action] = useFormState(solicitarRecuperacao, initialState);
return (
<form action={action} className="space-y-4">
{state.message ? (
<Alert>
<AlertDescription>{state.message}</AlertDescription>
</Alert>
) : null}
<div className="space-y-2">
<Label htmlFor="email">E-mail</Label>
<Input id="email" name="email" type="email" autoComplete="email" required />
</div>
<SubmitButton />
</form>
);
}