Initial SeConectai briefing system
This commit is contained in:
@@ -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 };
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user