# Introdução

Por
Pedro Amorim de Gregori
Em 
Publicado 2024-04-16

Autenticação por credenciais é um método que utiliza dados do usuário para realizar o processo. Esses dados podem variar dependendo do sistema, mas os mais utilizados são email e senha. Nesse método, será necessário a criação de um banco de dados próprio.

# Configuração para credenciais

Para implementar a autenticação por credenciais será necessário a introdução de 2 novos campos na model user e vai ser necessário atualizar o lucia.

schema.prisma
//...
model User {
  //...
    username String
	email String @unique
    hashed_password String
  //...
}

//...
auth/lucia.ts
//...
const adapter = new PrismaAdapter(client.session, client.user);

export const lucia = new Lucia(adapter, {
	sessionCookie: {
		attributes: {
			secure: process.env.NODE_ENV === "production",
		},
	},
	// O getUserAttributes é definido aqui para determinar quais atributos do usuário serão retornados do banco de dados e cria um objeto do tipo User com eles
	getUserAttributes: (attributes) => { // Acrescente esse bloco
		return {
			username: attributes.username,
			email: attributes.email,
		};
	},
});

declare module "lucia" {
	interface Register {
		Lucia: typeof lucia;
		DatabaseUserAttributes: DatabaseUserAttributes; // Acrescentar essa linha
	}

}

interface DatabaseUserAttributes { // Adicionar essa interface
	// Atributos do usuário
	username: string;
	email: string;
}
//...

# Operações de autenticação

# Cadastrar

Para cadastrar um usuário, será pedido os dados necessários para a aplicação e enviados para uma api que efetuará o cadastramento no banco de dados.

components/formCadastro.tsx
"use client";

import { ChangeEvent, FormEvent, useState } from "react";
import { useRouter } from "next/navigation";

export default function FormCadastro() {
	const [dados, setDados] = useState({
		username: "",
		email: "",
		password: "",
	});

	const router = useRouter();

	const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
		e.preventDefault();
		const res = await fetch("/api/signup", {
			method: "POST",
			body: JSON.stringify(dados),
		});

		if (res.ok) {
			router.replace("/profile");
		}
	};

	const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
		setDados({ ...dados, [e.target.name]: e.target.value });
	};
	return (
		<form
			onSubmit={handleSubmit}
			className="flex flex-col text-center gap-3 w-72 p-12 bg-zinc-800 rounded-xl"
		>
			<h2 className="text-zinc-200 font-bold text-3xl mb-2">
				Criar conta
			</h2>
			<input
				placeholder="Username"
				name="username"
				onChange={handleChange}
				className="bg-zinc-700 text-zinc-100 placeholder:text-zinc-100 p-3 rounded-sm"
			></input>
			<input
				placeholder="Email"
				name="email"
				type="email"
				onChange={handleChange}
				className="bg-zinc-700 text-zinc-100 placeholder:text-zinc-100 p-3 rounded-sm"
			></input>
			<input
				placeholder="Password"
				name="password"
				type="password"
				onChange={handleChange}
				className="bg-zinc-700 text-zinc-100 placeholder:text-zinc-100 p-3 rounded-sm"
			></input>
			<button
				type="submit"
				className="bg-zinc-950 p-3 mt-3 rounded-3xl text-zinc-100 text-xl font-bold"
			>
				Cadastrar
			</button>
		</form>
	);
}
api/signup/route.ts
import bcrypt from "bcrypt";
import { lucia } from "@/../auth/lucia";
import prisma from "@/../prisma/index";
import isValidEmail from "@/utils/email_validation"; //Função para validar emails

export async function POST(req: Request) {
	const { password, email, username } = await req.json();

	// Verificação de email
	if (!isValidEmail(email)) {
		return Response.json("Email inválido", { status: 400 });
	}

	if (password.lenght < 8) {
		return Response.json("Senha inválida", { status: 400 });
	}

// Tenta criar um usuário
	try {
		// Salt é um valor pseudo aleatório utilizado em criptografia para que entradas iguais criem hashes diferentes.
		const salt = bcrypt.genSaltSync(10);

		// hashSync é o método que utiliza o algoritmo bcrypt para criar uma palavra criptografada utilizando a senha e o salt.
		const hashedPassword = bcrypt.hashSync(password, salt);

		// Agora um novo usuário será criado, assim como uma nova sessão para ele não precisar logar.
		const user = await prisma.user.create({
			data: {
				email: email,
				hashed_password: hashedPassword,
				username: username,
			},
		});

		const session = await lucia.createSession(user.id, {});

		// Cria o cookie para identificar a sessão
		const sessionCookie = lucia.createSessionCookie(session.id);

		// Retorna o cookie criado para o navegador do usuário
		return Response.json(session, {
			status: 200,
			headers: {
				Location: "/profile",
				"Set-Cookie": sessionCookie.serialize(),
			},
		});
	} catch (e) {
		return Response.json("Ocorreu um erro.", {
			status: 500,
		}); // Erro genérico.
	}
}

# Login

Para realizar o login do usuário, irá ser pedido os dados como email e senha ou qualquer outra variação na página e eles serão enviados para uma rota de login.

components/formLogin.tsx
"use client";

import { ChangeEvent, FormEvent, useState } from "react";
import { useRouter } from "next/navigation";

export default function FormLogin() {
	const [dados, setDados] = useState({
		email: "",
		password: "",
	});

	const router = useRouter();

	const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
		e.preventDefault();
		const res = await fetch("/api/signin", {
			method: "POST",
			body: JSON.stringify(dados),
		});

		if (res.ok) {
			router.replace("/profile");
		}
	};

	const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
		setDados({ ...dados, [e.target.name]: e.target.value });
	};
	return (
		<form
			onSubmit={handleSubmit}
			className="flex flex-col text-center gap-3 w-72 p-12 bg-zinc-800 rounded-xl"
		>
			<h2 className="text-zinc-200 font-bold text-3xl mb-2">Login</h2>
			<input
				placeholder="Email"
				name="email"
				type="email"
				onChange={handleChange}
				className="bg-zinc-700 text-zinc-100 placeholder:text-zinc-100 p-3 rounded-sm"
			></input>
			<input
				placeholder="Password"
				name="password"
				type="password"
				onChange={handleChange}
				className="bg-zinc-700 text-zinc-100 placeholder:text-zinc-100 p-3 rounded-sm"
			></input>
			<button
				type="submit"
				className="bg-zinc-950 p-3 rounded-3xl mt-16 text-zinc-100 text-xl font-bold"
			>
				Entrar
			</button>
		</form>
	);
}
api/signin/route.ts
import { lucia } from "@/../auth/lucia";
import bcrypt from "bcrypt";
import isValidEmail from "@/utils/email_validation";
import prisma from "@/../prisma/index";

export async function POST(req: Request) {
	const { email, password } = await req.json();

	if (!isValidEmail(email)) {
		return Response.json("Email inválido.", { status: 400 });
	}

	if (password.lenght < 8) {
		return Response.json("Senha inválida.", { status: 400 });
	}

	const user = await prisma.user.findUnique({ where: { email: email } });

	if (!user) {
		const salt = bcrypt.genSaltSync(10);
		bcrypt.hashSync(password, salt); // Criptografamos a senha de um usuário mesmo que inválido para deixar um tempo de resposta parecido com as outras operações de autenticação e, assim, não deixar brechas para ataques que buscam padrões de resposta do servidor
		return Response.json("Email ou senha não válidos", { status: 400 });
	}

	const validPassword = bcrypt.compareSync(password, hashed_password);
	if (!validPassword) {
		return Response.json("Email ou senha não válidos", { status: 400 });
	}

	const session = await lucia.createSession(user.id, {});
	const sessionCookie = lucia.createSessionCookie(session.id);
	return Response.json(null, {
		status: 302,
		headers: {
			Location: "/profile",
			"Set-Cookie": sessionCookie.serialize(),
		},
	});
}

# Validar sessão/ Verificar usuário

Como diversas páginas podem necessitar do usuário, uma boa ideia é criar uma função para não precisar escrever tudo em várias páginas. A função abaixo retornará um user se ele estiver "logado".

utils/getUser.ts
import { cookies } from "next/headers";
import { cache } from "react";

import type { Session, User } from "lucia";
import { lucia } from "../../auth/lucia";

export const getUser = cache( // Essa função
	async (): Promise<
		{ user: User; session: Session } | { user: null; session: null }
	> => { // Recolhe os dados de sessão nos cookies
		const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null;
		if (!sessionId) {
			return {
				user: null,
				session: null,
			};
		}
		// Chama a instancia lucia para realizar a verificação da integridade dessa sessão
		const result = await lucia.validateSession(sessionId);
		try {
			// Verifica a existencia da sessão e em seguida se a sessão não expirou
			if (result.session && result.session.fresh) {
				const sessionCookie = lucia.createSessionCookie(
					result.session.id
				);
				cookies().set(
					sessionCookie.name,
					sessionCookie.value,
					sessionCookie.attributes
				);
			}
		// Se a sessão expirou, limpa os cookies
			if (!result.session) {
				const sessionCookie = lucia.createBlankSessionCookie();
				cookies().set(
					sessionCookie.name,
					sessionCookie.value,
					sessionCookie.attributes
				);
			}
		} catch {}
		return result;
	}
);

# Desconectar/LogOut

Para desconectar, a página irá realizar um requisação para a API de logout que invalidará a sessão no banco de dados e vai retornar os cookies de usuário em branco.

components/logoutButton.tsx
"use client";

import { LogOut } from "lucide-react";
import { useRouter } from "next/navigation";

export default function LogoutButton() {
	const router = useRouter();
	const handleClick = async () => {
		const res = await fetch("/api/signout", { method: "POST" });
		if (res.ok) {
			router.replace("/");
		}
	};
	return (
		<button
			className="flex bg-zinc-800 p-6 pt-2 pb-2 rounded-3xl text-zinc-100 text-xl font-bold gap-4"
			onClick={handleClick}
		>
			Sair <LogOut size={32} color="#e10f44" />
		</button>
	);
}
api/singout/route.ts
import { getUser } from "@/utils/getUser";
import { lucia } from "@/../auth/lucia";

export async function POST(req: Request) {
	const session = (await getUser()).session;
	if (!session) {
		return Response.json(null, { status: 401 });
	}

	await lucia.invalidateSession(session.id);
	return Response.json(null, {
		status: 200,

		headers: {
			Location: "/",
			"Set-Cookie": lucia.createBlankSessionCookie().serialize(),
		},
	});
}