Frontend con Next.js 13
Para el consumo de nuestro backend vamos a utilizar Next.js, un framework de React que nos permite crear aplicaciones web con React de forma sencilla y rápida.
Además utilizaremos NextAuth para la autenticación de usuarios.
Utilizaremos el Backend construido con Nest.js, aquí dejo la playlist de Youtube:
Clonar proyecto Backend
sh
git clone https://github.com/bluuweb/nest-postgres-api-rest-jwt
sh
yarn install
compose up -d
.env
POSTGRES_HOST="localhost"
POSTGRES_PORT=5436
POSTGRES_USERNAME="postgres"
POSTGRES_PASSWORD="postgres"
POSTGRES_DATABASE="db_crud"
POSTGRES_SSL="false"
JWT_SECRET="no utilizar esta palabra en producción"
Crear proyecto Next.js
Tutorial de Next.js
Instalación
sh
npx create-next-app@latest
Bootstrap
sh
npm install bootstrap@5.3.1
src\app\layout.tsx
ts
import "bootstrap/dist/css/bootstrap.min.css";
.env.local
sh
NEXT_PUBLIC_BACKEND_URL=http://localhost:8000/api/v1
Next Auth
- next-auth.js
- NextAuth.js es una solución completa de autenticación de código abierto para aplicaciones Next.js.
- Fácil, flexible y Seguro.
- Soporta múltiples proveedores de autenticación.
- JWT, OAuth 1.0a, OAuth 2.0 y más.
Instalación
sh
npm install next-auth
Ruta de autenticación
/app/api/auth/[...nextauth]/route.ts
ts
import NextAuth from "next-auth"
const handler = NextAuth({
...
})
export { handler as GET, handler as POST }
Credentials
/app/api/auth/[...nextauth]/route.ts
ts
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
const handler = NextAuth({
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
email: { label: "email", type: "email", placeholder: "test@test.com" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
const res = await fetch(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/auth/login`,
{
method: "POST",
body: JSON.stringify({
email: credentials?.email,
password: credentials?.password,
}),
headers: { "Content-Type": "application/json" },
}
);
const user = await res.json();
if (user.error) throw user;
return user;
},
}),
],
});
export { handler as GET, handler as POST };
SessionProvider
src\context\SessionAuthProvider.tsx
ts
"use client";
import { SessionProvider } from "next-auth/react";
interface Props {
children: React.ReactNode;
}
const SessionAuthProvider = ({ children }: Props) => {
return <SessionProvider>{children}</SessionProvider>;
};
export default SessionAuthProvider;
src\components\ButtonAuth.tsx
tsx
"use client";
import { signIn, signOut, useSession } from "next-auth/react";
export default function ButtonAuth() {
const { data: session, status } = useSession();
if (status === "loading") {
return <p>Loading...</p>;
}
if (session) {
return (
<>
Signed in as {session.user?.email} <br />
<button
onClick={() => signOut()}
className="btn btn-danger"
>
Sign out
</button>
</>
);
}
return (
<>
Not signed in <br />
<button
onClick={() => signIn()}
className="btn btn-primary"
>
Sign in
</button>
</>
);
}
src\app\page.tsx
tsx
import ButtonAuth from "@/components/ButtonAuth";
const HomePage = () => {
return (
<div>
<h1>Home Page</h1>
<ButtonAuth />
</div>
);
};
export default HomePage;
Middleware
src\middleware.ts
ts
export { default } from "next-auth/middleware";
export const config = {
matcher: ["/dashboard/:path*"],
};
src\app\dashboard\page.tsx
tsx
"use client";
import { useSession } from "next-auth/react";
const Dashboard = () => {
const { data: session, status } = useSession();
if (status === "loading") {
return <p>Loading...</p>;
}
console.log(session);
return (
<div>
<h1>Dashboard</h1>
<pre>
<code>{JSON.stringify(session, null, 2)}</code>
</pre>
</div>
);
};
export default Dashboard;
JWT
src\app\dashboard\page.tsx
tsx
"use client";
import { useSession } from "next-auth/react";
const Dashboard = () => {
const { data: session, status } = useSession();
if (status === "loading") {
return <p>Loading...</p>;
}
console.log(session);
console.log(process.env.NEXT_PUBLIC_BACKEND_URL);
const getCats = async () => {
const res = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL}/cats`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${"coloque-un-token-valido"}`,
},
});
const data = await res.json();
console.log(data);
};
return (
<div>
<h1>Dashboard</h1>
<button
onClick={getCats}
className="btn btn-primary"
>
Get Cats
</button>
<pre>
<code>{JSON.stringify(session, null, 2)}</code>
</pre>
</div>
);
};
export default Dashboard;
src\app\api\auth[...nextauth]\route.ts
ts
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
const handler = NextAuth({
providers: [
CredentialsProvider({
...
],
callbacks: {
async jwt({ token, user }) {
return { ...token, ...user };
},
async session({ session, token }) {
session.user = token as any;
return session;
},
},
});
export { handler as GET, handler as POST };
src\app\dashboard\page.tsx
tsx
"use client";
import { useSession } from "next-auth/react";
const Dashboard = () => {
const { data: session, status } = useSession();
if (status === "loading") {
return <p>Loading...</p>;
}
console.log(session?.user?.token);
const getCats = async () => {
const res = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL}/cats`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${session?.user?.token}`,
},
});
const data = await res.json();
console.log(data);
};
return (
<div>
<h1>Dashboard</h1>
<button
onClick={getCats}
className="btn btn-primary"
>
Get Cats
</button>
<pre>
<code>{JSON.stringify(session, null, 2)}</code>
</pre>
</div>
);
};
export default Dashboard;
TS Adapter
src\types\next-auth.d.ts
ts
import "next-auth";
declare module "next-auth" {
interface Session {
user: {
email: string;
token: string;
};
}
}
Login
src\app\login\page.tsx
tsx
"use client";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useState } from "react";
const LoginPage = () => {
const [errors, setErrors] = useState<string[]>([]);
const [email, setEmail] = useState<string>("test@test.com");
const [password, setPassword] = useState<string>("123123");
const router = useRouter();
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setErrors([]);
const responseNextAuth = await signIn("credentials", {
email,
password,
redirect: false,
});
if (responseNextAuth?.error) {
setErrors(responseNextAuth.error.split(","));
return;
}
router.push("/dashboard");
};
return (
<div>
<h1>Login</h1>
<form onSubmit={handleSubmit}>
<input
type="email"
placeholder="test@test.com"
name="email"
className="form-control mb-2"
value={email}
onChange={(event) => setEmail(event.target.value)}
/>
<input
type="password"
placeholder="123123"
name="password"
className="form-control mb-2"
value={password}
onChange={(event) => setPassword(event.target.value)}
/>
<button
type="submit"
className="btn btn-primary"
>
Login
</button>
</form>
{errors.length > 0 && (
<div className="alert alert-danger mt-2">
<ul className="mb-0">
{errors.map((error) => (
<li key={error}>{error}</li>
))}
</ul>
</div>
)}
</div>
);
};
export default LoginPage;
src\app\api\auth[...nextauth]\route.ts
ts
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
const handler = NextAuth({
providers: [
...
],
callbacks: {
...
},
pages: {
signIn: "/login",
},
});
export { handler as GET, handler as POST };
Navbar
src\components\Navbar.tsx
tsx
"use client";
import { signOut, useSession } from "next-auth/react";
import Link from "next/link";
const Navbar = () => {
const { data: session } = useSession();
return (
<nav className="navbar navbar-dark bg-dark">
<div className="container">
<Link
href="/"
className="btn btn-primary btn-sm"
>
Home
</Link>
{session?.user ? (
<>
<Link
href="/dashboard"
className="btn btn-primary btn-sm"
>
Dashboard
</Link>
<button
onClick={() => signOut()}
className="btn btn-danger btn-sm"
>
Signout
</button>
</>
) : (
<>
<Link
href="/login"
className="btn btn-primary btn-sm"
>
Login
</Link>
<Link
href="/register"
className="btn btn-primary btn-sm"
>
Register
</Link>
</>
)}
</div>
</nav>
);
};
export default Navbar;
src\app\layout.tsx
tsx
<SessionAuthProvider>
<Navbar />
{children}
</SessionAuthProvider>
Register
src\app\register\page.tsx
tsx
"use client";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useState } from "react";
const RegisterPage = () => {
const [errors, setErrors] = useState<string[]>([]);
const [name, setName] = useState<string>("test");
const [email, setEmail] = useState<string>("test@test.com");
const [password, setPassword] = useState<string>("123123");
const router = useRouter();
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setErrors([]);
const res = await fetch(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/auth/register`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name,
email,
password,
}),
}
);
const responseAPI = await res.json();
if (!res.ok) {
setErrors(responseAPI.message);
return;
}
const responseNextAuth = await signIn("credentials", {
email,
password,
redirect: false,
});
if (responseNextAuth?.error) {
setErrors(responseNextAuth.error.split(","));
return;
}
router.push("/dashboard");
};
return (
<div>
<h1>Register</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="test"
name="name"
className="form-control mb-2"
value={name}
onChange={(event) => setName(event.target.value)}
/>
<input
type="email"
placeholder="test@test.com"
name="email"
className="form-control mb-2"
value={email}
onChange={(event) => setEmail(event.target.value)}
/>
<input
type="password"
placeholder="123123"
name="password"
className="form-control mb-2"
value={password}
onChange={(event) => setPassword(event.target.value)}
/>
<button
type="submit"
className="btn btn-primary"
>
Register
</button>
</form>
{errors.length > 0 && (
<div className="alert alert-danger mt-2">
<ul className="mb-0">
{errors.map((error) => (
<li key={error}>{error}</li>
))}
</ul>
</div>
)}
</div>
);
};
export default RegisterPage;
Deploy
Agregar variables de entorno
NEXT_PUBLIC_BACKEND_URL=url-api-rest/api/v1
NEXTAUTH_SECRET=no.utilizar.en.producción