├── .env ├── .eslintrc.json ├── .gitignore ├── Dockerfile ├── README.md ├── actions └── auth.actions.ts ├── app ├── (pages) │ ├── sign-in │ │ └── page.tsx │ └── sign-up │ │ └── page.tsx ├── favicon.ico ├── globals.css ├── layout.tsx └── page.tsx ├── components.json ├── components ├── SignInForm.tsx ├── SignUpForm.tsx └── ui │ ├── button.tsx │ ├── form.tsx │ ├── input.tsx │ ├── label.tsx │ ├── toast.tsx │ ├── toaster.tsx │ └── use-toast.ts ├── docker-compose.yml ├── drizzle.config.ts ├── drizzle ├── 0000_conscious_harry_osborn.sql ├── 0001_bouncy_manta.sql ├── 0002_pale_winter_soldier.sql ├── 0003_romantic_peter_parker.sql └── meta │ ├── 0000_snapshot.json │ ├── 0001_snapshot.json │ ├── 0002_snapshot.json │ ├── 0003_snapshot.json │ └── _journal.json ├── lib ├── database │ ├── index.ts │ └── schema.ts ├── lucia │ ├── adapter.ts │ └── index.ts └── utils.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── next.svg └── vercel.svg ├── tailwind.config.ts ├── tsconfig.json └── types └── index.ts /.env: -------------------------------------------------------------------------------- 1 | DB_URL=postgres://username:password@localhost:5432/dbname -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use a specific Node.js 20.10 image 2 | FROM node:20.10-alpine 3 | 4 | # Create working directory 5 | WORKDIR /app 6 | 7 | # Copy your project files 8 | COPY package*.json ./ 9 | COPY ./ ./ 10 | 11 | # Install dependencies 12 | RUN npm install 13 | 14 | # Build your Next.js application 15 | RUN npm run build 16 | 17 | # Expose the port Next will use 18 | EXPOSE 3000 19 | 20 | # Command to start your application 21 | CMD ["npm", "run", "dev"] 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PLEASE DO NOT USE LUCIA FOR YOUR AUTH AS IT'S GOING TO BE DEPRECATED. 2 | # THIS REPOSITORY IS STALE. 3 | 4 | # Username and password authentication template for Next 14 5 | 6 | ## Youtube tutorial - username, password authentication 7 | https://www.youtube.com/watch?v=JIIy7VkiTqU 8 | 9 | ## Youtube tutorial - email, password with email verification 10 | 11 | - Checkout to email branch to see this implementation. 12 | 13 | https://www.youtube.com/watch?v=2sHsP_8YLHA&t=1s 14 | 15 | 16 | 17 | 18 | - Next 14 19 | - Lucia Auth Package 20 | - Postgre SQL 21 | - Drizzle ORM 22 | - Typescript 23 | - Tailwind CSS 24 | - Shadcn UI 25 | 26 | ## Installation 27 | 28 | install the package using npm or yarn 29 | 30 | ```bash 31 | npm install 32 | ``` 33 | 34 | or 35 | 36 | ```bash 37 | yarn 38 | ``` 39 | 40 | ## Usage 41 | 42 | ```bash 43 | npm run dev 44 | ``` 45 | 46 | or 47 | 48 | ```bash 49 | yarn dev 50 | ``` 51 | 52 | or you can use Docker-compose 53 | 54 | ```bash 55 | docker-compose up 56 | ``` 57 | 58 | It will start the server on http://localhost:3000 and create a local postgres database on port 5432 59 | 60 | ## Database 61 | You can create a local postgres database and add the credentials to the .env file 62 | 63 | ```bash 64 | docker run --name postgres -e POSTGRES_PASSWORD=password -e POSTGRES_USER=username -e POSTGRES_DB=dbname -p 5432:5432 -d postgres 65 | ``` 66 | 67 | ```bash 68 | DATABASE_URL=postgres://username:password@localhost:5432/dbname 69 | ``` 70 | 71 | ## License 72 | 73 | [MIT](https://choosealicense.com/licenses/mit/) 74 | -------------------------------------------------------------------------------- /actions/auth.actions.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | import { z } from "zod" 3 | import { SignInSchema, SignUpSchema } from "../types" 4 | import { generateId } from "lucia" 5 | import db from "@/lib/database" 6 | import { userTable } from "@/lib/database/schema" 7 | import { lucia, validateRequest } from "@/lib/lucia" 8 | import { cookies } from "next/headers" 9 | import { eq } from "drizzle-orm" 10 | import * as argon2 from "argon2" 11 | 12 | export const signUp = async (values: z.infer) => { 13 | console.log(values) 14 | 15 | const hashedPassword = await argon2.hash(values.password) 16 | const userId = generateId(15) 17 | 18 | try { 19 | await db 20 | .insert(userTable) 21 | .values({ 22 | id: userId, 23 | username: values.username, 24 | hashedPassword, 25 | }) 26 | .returning({ 27 | id: userTable.id, 28 | username: userTable.username, 29 | }) 30 | 31 | const session = await lucia.createSession(userId, { 32 | expiresIn: 60 * 60 * 24 * 30, 33 | }) 34 | 35 | const sessionCookie = lucia.createSessionCookie(session.id) 36 | 37 | cookies().set( 38 | sessionCookie.name, 39 | sessionCookie.value, 40 | sessionCookie.attributes 41 | ) 42 | 43 | return { 44 | success: true, 45 | data: { 46 | userId, 47 | }, 48 | } 49 | } catch (error: any) { 50 | return { 51 | error: error?.message, 52 | } 53 | } 54 | } 55 | 56 | export const signIn = async (values: z.infer) => { 57 | try { 58 | SignInSchema.parse(values) 59 | } catch (error: any) { 60 | return { 61 | error: error.message, 62 | } 63 | } 64 | 65 | const existingUser = await db.query.userTable.findFirst({ 66 | where: (table) => eq(table.username, values.username), 67 | }) 68 | 69 | if (!existingUser) { 70 | return { 71 | error: "User not found", 72 | } 73 | } 74 | 75 | if (!existingUser.hashedPassword) { 76 | return { 77 | error: "User not found", 78 | } 79 | } 80 | 81 | const isValidPassword = await argon2.verify( 82 | existingUser.hashedPassword, 83 | values.password 84 | ) 85 | 86 | if (!isValidPassword) { 87 | return { 88 | error: "Incorrect username or password", 89 | } 90 | } 91 | 92 | const session = await lucia.createSession(existingUser.id, { 93 | expiresIn: 60 * 60 * 24 * 30, 94 | }) 95 | 96 | const sessionCookie = lucia.createSessionCookie(session.id) 97 | 98 | cookies().set( 99 | sessionCookie.name, 100 | sessionCookie.value, 101 | sessionCookie.attributes 102 | ) 103 | 104 | return { 105 | success: "Logged in successfully", 106 | } 107 | } 108 | 109 | export const signOut = async () => { 110 | try { 111 | const { session } = await validateRequest() 112 | 113 | if (!session) { 114 | return { 115 | error: "Unauthorized", 116 | } 117 | } 118 | 119 | await lucia.invalidateSession(session.id) 120 | 121 | const sessionCookie = lucia.createBlankSessionCookie() 122 | 123 | cookies().set( 124 | sessionCookie.name, 125 | sessionCookie.value, 126 | sessionCookie.attributes 127 | ) 128 | } catch (error: any) { 129 | return { 130 | error: error?.message, 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /app/(pages)/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignInForm } from "@/components/SignInForm" 2 | import { validateRequest } from "@/lib/lucia" 3 | import { redirect } from "next/navigation" 4 | 5 | export default async function SignUpPage() { 6 | const { user } = await validateRequest() 7 | 8 | if (user) { 9 | return redirect("/") 10 | } 11 | 12 | return ( 13 |
14 | 18 | 19 | 20 |
21 |

22 | Sign in to your account 23 |

24 | 25 |
26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /app/(pages)/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUpForm } from "@/components/SignUpForm" 2 | import { validateRequest } from "@/lib/lucia" 3 | import { redirect } from "next/navigation" 4 | 5 | export default async function SignUpPage() { 6 | const { user } = await validateRequest() 7 | 8 | if (user) { 9 | return redirect("/") 10 | } 11 | 12 | return ( 13 |
14 | 18 | 19 | 20 |
21 |

22 | Create a Free Account 23 |

24 | 25 |
26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ugurkellecioglu/next-14-lucia-auth-postgresql-drizzle-typescript-example/99147e0ba6176903ac8f1bd7ce34fec8078a2f31/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next" 2 | import { Inter } from "next/font/google" 3 | import "./globals.css" 4 | import { Toaster } from "@/components/ui/toaster" 5 | 6 | const inter = Inter({ subsets: ["latin"] }) 7 | 8 | export const metadata: Metadata = { 9 | title: "Create Next App", 10 | description: "Generated by create next app", 11 | } 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: Readonly<{ 16 | children: React.ReactNode 17 | }>) { 18 | return ( 19 | 20 | 21 | {children} 22 | 23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button" 2 | import { validateRequest } from "@/lib/lucia" 3 | import { redirect } from "next/navigation" 4 | import { signOut } from "@/actions/auth.actions" 5 | 6 | export default async function Home() { 7 | const { user } = await validateRequest() 8 | 9 | if (!user) { 10 | return redirect("/sign-in") 11 | } 12 | 13 | return ( 14 |
15 |

Protected route

16 |

{JSON.stringify(user)}

17 |
18 | 19 |
20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /components/SignInForm.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { zodResolver } from "@hookform/resolvers/zod" 4 | import { useForm } from "react-hook-form" 5 | import { z } from "zod" 6 | import { Button } from "@/components/ui/button" 7 | import { 8 | Form, 9 | FormControl, 10 | FormDescription, 11 | FormField, 12 | FormItem, 13 | FormLabel, 14 | FormMessage, 15 | } from "@/components/ui/form" 16 | import { Input } from "@/components/ui/input" 17 | import { SignInSchema } from "../types" 18 | import { signIn, signUp } from "../actions/auth.actions" 19 | import { toast } from "@/components/ui/use-toast" 20 | import { useRouter } from "next/navigation" 21 | 22 | export function SignInForm() { 23 | const router = useRouter() 24 | 25 | const form = useForm>({ 26 | resolver: zodResolver(SignInSchema), 27 | defaultValues: { 28 | username: "", 29 | password: "", 30 | }, 31 | }) 32 | 33 | async function onSubmit(values: z.infer) { 34 | const res = await signIn(values) 35 | if (res.error) { 36 | toast({ 37 | variant: "destructive", 38 | description: res.error, 39 | }) 40 | } else if (res.success) { 41 | toast({ 42 | variant: "default", 43 | description: "Signed in successfully", 44 | }) 45 | 46 | router.push("/") 47 | } 48 | } 49 | return ( 50 |
51 | 52 | ( 56 | 57 | Username 58 | 59 | 60 | 61 | 62 | 63 | )} 64 | />{" "} 65 | ( 69 | 70 | Password 71 | 72 | 73 | 74 | 75 | 76 | )} 77 | /> 78 | 79 | 80 | 81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /components/SignUpForm.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { zodResolver } from "@hookform/resolvers/zod" 4 | import { useForm } from "react-hook-form" 5 | import { z } from "zod" 6 | import { Button } from "@/components/ui/button" 7 | import { 8 | Form, 9 | FormControl, 10 | FormDescription, 11 | FormField, 12 | FormItem, 13 | FormLabel, 14 | FormMessage, 15 | } from "@/components/ui/form" 16 | import { Input } from "@/components/ui/input" 17 | import { SignUpSchema } from "../types" 18 | import { signUp } from "../actions/auth.actions" 19 | import { toast } from "@/components/ui/use-toast" 20 | import { useRouter } from "next/navigation" 21 | 22 | export function SignUpForm() { 23 | const router = useRouter() 24 | 25 | const form = useForm>({ 26 | resolver: zodResolver(SignUpSchema), 27 | defaultValues: { 28 | username: "", 29 | password: "", 30 | confirmPassword: "", 31 | }, 32 | }) 33 | 34 | async function onSubmit(values: z.infer) { 35 | const res = await signUp(values) 36 | if (res.error) { 37 | toast({ 38 | variant: "destructive", 39 | description: res.error, 40 | }) 41 | } else if (res.success) { 42 | toast({ 43 | variant: "default", 44 | description: "Account created successfully", 45 | }) 46 | 47 | router.push("/") 48 | } 49 | } 50 | return ( 51 |
52 | 53 | ( 57 | 58 | Username 59 | 60 | 61 | 62 | 63 | 64 | )} 65 | />{" "} 66 | ( 70 | 71 | Password 72 | 73 | 74 | 75 | 76 | 77 | )} 78 | /> 79 | ( 83 | 84 | Confirm password 85 | 86 | 87 | 88 | 89 | 90 | )} 91 | /> 92 | 93 | 94 | 95 | ) 96 | } 97 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { Slot } from "@radix-ui/react-slot" 4 | import { 5 | Controller, 6 | ControllerProps, 7 | FieldPath, 8 | FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from "react-hook-form" 12 | 13 | import { cn } from "@/lib/utils" 14 | import { Label } from "@/components/ui/label" 15 | 16 | const Form = FormProvider 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath = FieldPath 21 | > = { 22 | name: TName 23 | } 24 | 25 | const FormFieldContext = React.createContext( 26 | {} as FormFieldContextValue 27 | ) 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath = FieldPath 32 | >({ 33 | ...props 34 | }: ControllerProps) => { 35 | return ( 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext) 44 | const itemContext = React.useContext(FormItemContext) 45 | const { getFieldState, formState } = useFormContext() 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState) 48 | 49 | if (!fieldContext) { 50 | throw new Error("useFormField should be used within ") 51 | } 52 | 53 | const { id } = itemContext 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState, 62 | } 63 | } 64 | 65 | type FormItemContextValue = { 66 | id: string 67 | } 68 | 69 | const FormItemContext = React.createContext( 70 | {} as FormItemContextValue 71 | ) 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId() 78 | 79 | return ( 80 | 81 |
82 | 83 | ) 84 | }) 85 | FormItem.displayName = "FormItem" 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField() 92 | 93 | return ( 94 |