Use the CLI (not yet supported)
npm ryo-auth@latest add session-next
Manual Installation (WIP, to be improved)
Start a new Next.js project with shadcn/ui
⚠️
Make sure to go through the installer, choose TypeScript, App Router and src folder.
npm create-next-app@latest
Install additional dependencies for UI
npm i more libraries
Configure Next Config
./next.config.ts
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
return [
{
source: "/api/:path*",
destination: `${process.env.NEXT_PUBLIC_AUTH_API}/:path*`,
},
];
},
};
module.exports = nextConfig;
Create the folder and file structure
- UserAuthForm.tsx
- UserSignupForm.tsx
- middleware.ts
Middleware
/src/middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const AUTH_ROUTES = ["/login", "/signup"];
export async function middleware(request: NextRequest) {
const cookie = request.cookies.get("RollYourOwnAuth")?.value;
const isAuthPage = Boolean(AUTH_ROUTES.includes(request.nextUrl.pathname));
if (isAuthPage) {
if (cookie) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
return null;
}
if (!cookie) {
return NextResponse.redirect(new URL("/login", request.url));
}
}
// See "Matching Paths" below to learn more
export const config = {
matcher: "/((?!api|_next/static|_next/image|favicon.ico).*)",
};
Setup env variables
.env.local
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_AUTH_API=http://localhost:4000
/login route
The /login
page.
/src/app/(auth)/login/page.tsx
import { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/ui/components/button";
import { UserAuthForm } from "../components/UserAuthForm";
import Logo from "@/ui/icons/Logo";
export const metadata: Metadata = {
title: "Login",
description: "Authentication forms built using the components.",
};
export default function LoginPage() {
return (
<>
<div className="container relative h-screen flex-col items-center justify-center grid lg:max-w-none lg:grid-cols-2 lg:px-0">
<Link
href="/signup"
className={cn(
buttonVariants({ variant: "ghost" }),
"absolute right-4 top-4 md:right-8 md:top-8"
)}
>
Signup
</Link>
<div className="relative hidden h-full flex-col bg-muted p-10 text-white dark:border-r lg:flex">
<div className="absolute inset-0 bg-zinc-900" />
<div className="relative z-20 flex items-center text-lg font-medium">
<Logo />
</div>
<div className="relative z-20 mt-auto">
<blockquote className="space-y-2">
<p className="text-lg">
“It's time to roll your own auth”
</p>
<footer className="text-sm">Smakosh</footer>
</blockquote>
</div>
</div>
<div className="lg:p-8">
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<div className="flex flex-col space-y-2 text-center">
<h1 className="text-2xl font-semibold tracking-tight">
Welcome back
</h1>
<p className="text-sm text-muted-foreground">
Login into your account
</p>
</div>
<UserAuthForm />
<p className="px-8 text-center text-sm text-muted-foreground">
By clicking continue, you agree to our{" "}
<Link
href="/terms"
className="underline underline-offset-4 hover:text-primary"
>
Terms of Service
</Link>{" "}
and{" "}
<Link
href="/privacy"
className="underline underline-offset-4 hover:text-primary"
>
Privacy Policy
</Link>
.
</p>
</div>
</div>
</div>
</>
);
}
The Login Form component
/src/app/(auth)/components/UserAuthForm.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import * as z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { Button } from "@/ui/components/button";
import { Input } from "@/ui/components/input";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/ui/components/form";
import Spinner from "@/ui/icons/Spinner";
import { useRouter } from "next/navigation";
import { APP_URL } from "@/lib/env";
interface UserAuthFormProps extends React.HTMLAttributes<HTMLDivElement> {}
const formSchema = z.object({
email: z.string().email(),
password: z.string().min(6, {
message: "Password must be at least 6 characters",
}),
});
export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
const router = useRouter();
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
mode: "onTouched",
});
async function onSubmit(values: z.infer<typeof formSchema>) {
setIsLoading(true);
const res = await fetch(`${APP_URL}/api/auth/login`, {
method: "POST",
body: JSON.stringify(values),
headers: {
"Content-Type": "application/json",
},
});
const user = await res.json();
if (user.id || user.access_token) {
router.push("/dashboard");
}
setIsLoading(false);
}
return (
<div className={cn("grid gap-6", className)} {...props}>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem className="mb-3">
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="john@doe.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem className="mb-3">
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={isLoading} className="mb-6">
{isLoading && <Spinner className="mr-2 h-4 w-4 animate-spin" />}
Signin
</Button>
</form>
</Form>
</div>
);
}
/signup route
The /signup
page.
/src/app/(auth)/signup/page.tsx
import { Metadata } from "next";
import Link from "next/link";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/ui/components/button";
import Logo from "@/ui/icons/Logo";
import { UserSignupForm } from "../components/UserSignupForm";
export const metadata: Metadata = {
title: "Signup",
description: "Authentication forms built using the components.",
};
export default function SignupPage() {
return (
<>
<div className="container relative h-screen flex-col items-center justify-center grid lg:max-w-none lg:grid-cols-2 lg:px-0">
<Link
href="/login"
className={cn(
buttonVariants({ variant: "ghost" }),
"absolute right-4 top-4 md:right-8 md:top-8"
)}
>
Login
</Link>
<div className="relative hidden h-full flex-col bg-muted p-10 text-white dark:border-r lg:flex">
<div className="absolute inset-0 bg-zinc-900" />
<div className="relative z-20 flex items-center text-lg font-medium">
<Logo />
</div>
<div className="relative z-20 mt-auto">
<blockquote className="space-y-2">
<p className="text-lg">
“It's time to roll your own auth”
</p>
<footer className="text-sm">Smakosh</footer>
</blockquote>
</div>
</div>
<div className="lg:p-8">
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<div className="flex flex-col space-y-2 text-center">
<h1 className="text-2xl font-semibold tracking-tight">
Create an account
</h1>
<p className="text-sm text-muted-foreground">
Enter your email below to create your account
</p>
</div>
<UserSignupForm />
<p className="px-8 text-center text-sm text-muted-foreground">
By clicking continue, you agree to our{" "}
<Link
href="/terms"
className="underline underline-offset-4 hover:text-primary"
>
Terms of Service
</Link>{" "}
and{" "}
<Link
href="/privacy"
className="underline underline-offset-4 hover:text-primary"
>
Privacy Policy
</Link>
.
</p>
</div>
</div>
</div>
</>
);
}
The Signup Form component
/src/app/(auth)/components/UserSignupForm.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import * as z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { Button } from "@/ui/components/button";
import { Input } from "@/ui/components/input";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/ui/components/form";
import Spinner from "@/ui/icons/Spinner";
import { useRouter } from "next/navigation";
import { APP_URL } from "@/lib/env";
interface UserSignupFormProps extends React.HTMLAttributes<HTMLDivElement> {}
const formSchema = z
.object({
name: z
.string()
.min(2, {
message: "Name must be at least 2 characters.",
})
.max(120),
email: z.string().email(),
password: z.string().min(6, {
message: "Password must be at least 6 characters",
}),
confirmPassword: z.string().min(6, {
message: "Password must be at least 6 characters",
}),
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "Passwords don't match",
});
export function UserSignupForm({ className, ...props }: UserSignupFormProps) {
const router = useRouter();
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
mode: "onTouched",
});
async function onSubmit(values: z.infer<typeof formSchema>) {
setIsLoading(true);
const res = await fetch(`${APP_URL}/api/auth/signup`, {
method: "POST",
body: JSON.stringify(values),
headers: {
"Content-Type": "application/json",
},
});
const user = await res.json();
if (user.id || user.access_token) {
router.push("/dashboard");
}
setIsLoading(false);
}
return (
<div className={cn("grid gap-6", className)} {...props}>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem className="mb-3">
<FormLabel>Name</FormLabel>
<FormControl>
<Input type="text" placeholder="John Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem className="mb-3">
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="john@doe.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem className="mb-3">
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem className="mb-6">
<FormLabel>Confirm password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={isLoading} className="mb-6">
{isLoading && <Spinner className="mr-2 h-4 w-4 animate-spin" />}
Sign up
</Button>
</form>
</Form>
</div>
);
}
Dashboard protected route
The /dashboard
page.
/src/app/dashboard/page.tsx
import { getCurrentUser } from "@/lib/session";
import { redirect } from "next/navigation";
import Logout from "./components/Logout";
export const dynamic = "force-dynamic";
const DashboardPage = async () => {
const user = await getCurrentUser();
if (!user) {
redirect("/login");
}
return (
<div>
Welcome back: <pre>{JSON.stringify(user, null, 2)}</pre>
<Logout />
</div>
);
};
export default DashboardPage;
Logout component
/src/app/dashboard/components/Logout.tsx
"use client";
import { APP_URL } from "@/lib/env";
import { Button } from "@/ui/components/button";
import { useRouter } from "next/navigation";
const Logout = () => {
const router = useRouter();
const logout = async () => {
const res = await fetch(`${APP_URL}/api/auth/logout`);
if (res.status === 200) {
router.push("/login");
}
};
return (
<Button type="button" onClick={logout}>
Logout
</Button>
);
};
export default Logout;
🚀
and that's a wrap! Congratulations on rolling your own auth!