├── .env
├── .eslintrc.json
├── .gitignore
├── README.md
├── components.json
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── prisma
├── migrations
│ ├── 20240904103116_delete_users_model
│ │ └── migration.sql
│ ├── 20240904113418_delete_user_model
│ │ └── migration.sql
│ ├── 20240904113700_init
│ │ └── migration.sql
│ └── migration_lock.toml
└── schema.prisma
├── public
├── next.svg
└── vercel.svg
├── src
├── app
│ ├── (auth)
│ │ ├── forgot-password
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── otp-verifacation
│ │ │ └── page.tsx
│ │ ├── set-new-password
│ │ │ └── page.tsx
│ │ ├── sign-in
│ │ │ └── page.tsx
│ │ └── sign-up
│ │ │ └── page.tsx
│ ├── (dashboard)
│ │ └── admin
│ │ │ └── page.tsx
│ ├── api
│ │ ├── auth
│ │ │ └── [...nextauth]
│ │ │ │ └── route.ts
│ │ ├── reset-password
│ │ │ └── route.ts
│ │ ├── update-password
│ │ │ └── route.ts
│ │ ├── user
│ │ │ └── route.ts
│ │ └── verify-otp
│ │ │ └── route.ts
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── components
│ ├── GoogleSignInButton.tsx
│ ├── Navbar.tsx
│ ├── Provider.tsx
│ ├── User.tsx
│ ├── UserAccountnav.tsx
│ ├── form
│ │ ├── SignInForm.tsx
│ │ └── SignUpForm.tsx
│ ├── hooks
│ │ └── use-toast.ts
│ └── ui
│ │ ├── button.tsx
│ │ ├── form.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── toast.tsx
│ │ └── toaster.tsx
├── lib
│ ├── auth.ts
│ ├── prismadb.ts
│ └── utils.ts
├── styles
│ └── globals.css
└── types
│ └── next-auth.d.ts
├── tailwind.config.ts
└── tsconfig.json
/.env:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code-with-zain-hunzai/NextAuth/74fcefd1d38dbd568847d2735bb716b725da1cda/.env
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NextAuth
--------------------------------------------------------------------------------
/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": "src/app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | }
20 | }
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextauth",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@auth/prisma-adapter": "^2.4.2",
13 | "@hookform/resolvers": "^3.9.0",
14 | "@next-auth/prisma-adapter": "^1.0.7",
15 | "@prisma/client": "^5.19.1",
16 | "@radix-ui/react-label": "^2.1.0",
17 | "@radix-ui/react-slot": "^1.1.0",
18 | "@radix-ui/react-toast": "^1.2.1",
19 | "axios": "^1.7.7",
20 | "bcrypt": "^5.1.1",
21 | "bcryptjs": "^2.4.3",
22 | "class-variance-authority": "^0.7.0",
23 | "clsx": "^2.1.1",
24 | "lucide-react": "^0.438.0",
25 | "next": "14.2.7",
26 | "next-auth": "^4.24.7",
27 | "nodemailer": "^6.9.15",
28 | "react": "^18",
29 | "react-dom": "^18",
30 | "react-hook-form": "^7.53.0",
31 | "tailwind-merge": "^2.5.2",
32 | "tailwindcss-animate": "^1.0.7",
33 | "zod": "^3.23.8"
34 | },
35 | "devDependencies": {
36 | "@types/bcrypt": "^5.0.2",
37 | "@types/bcryptjs": "^2.4.6",
38 | "@types/node": "^20",
39 | "@types/nodemailer": "^6.4.15",
40 | "@types/react": "^18",
41 | "@types/react-dom": "^18",
42 | "eslint": "^8",
43 | "eslint-config-next": "14.2.7",
44 | "postcss": "^8",
45 | "prisma": "^5.19.1",
46 | "tailwindcss": "^3.4.1",
47 | "typescript": "^5"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/prisma/migrations/20240904103116_delete_users_model/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" SERIAL NOT NULL,
4 | "username" TEXT NOT NULL,
5 | "email" TEXT NOT NULL,
6 | "password" TEXT NOT NULL,
7 |
8 | CONSTRAINT "User_pkey" PRIMARY KEY ("id")
9 | );
10 |
11 | -- CreateIndex
12 | CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
13 |
14 | -- CreateIndex
15 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
16 |
--------------------------------------------------------------------------------
/prisma/migrations/20240904113418_delete_user_model/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost.
5 |
6 | */
7 | -- DropTable
8 | DROP TABLE "User";
9 |
--------------------------------------------------------------------------------
/prisma/migrations/20240904113700_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" SERIAL NOT NULL,
4 | "username" TEXT NOT NULL,
5 | "email" TEXT NOT NULL,
6 | "password" TEXT NOT NULL,
7 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
8 | "updateUt" TIMESTAMP(3) NOT NULL,
9 |
10 | CONSTRAINT "User_pkey" PRIMARY KEY ("id")
11 | );
12 |
13 | -- CreateIndex
14 | CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
15 |
16 | -- CreateIndex
17 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
18 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "postgresql"
7 | url = env("DATABASE_URL")
8 | }
9 |
10 | // model User {
11 | // id Int @id @default(autoincrement())
12 | // username String @unique
13 | // email String @unique
14 | // password String
15 | // createdAt DateTime @default(now())
16 | // updateUt DateTime @updatedAt
17 | // }
18 |
19 | model Account {
20 | id String @id @default(cuid())
21 | userId String @map("user_id")
22 | type String
23 | provider String
24 | providerAccountId String @map("provider_account_id")
25 | refresh_token String? @db.Text
26 | access_token String? @db.Text
27 | expires_at Int?
28 | token_type String?
29 | scope String?
30 | id_token String? @db.Text
31 | session_state String?
32 |
33 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
34 |
35 | @@unique([provider, providerAccountId])
36 | @@map("accounts")
37 | }
38 |
39 | model Session {
40 | id String @id @default(cuid())
41 | sessionToken String @unique @map("session_token")
42 | userId String @map("user_id")
43 | expires DateTime
44 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
45 |
46 | @@map("sessions")
47 | }
48 |
49 | model User {
50 | id String @id @default(cuid())
51 | username String? @unique
52 | password String?
53 | createdAt DateTime @default(now())
54 | updatedUt DateTime @updatedAt
55 | name String?
56 | email String? @unique
57 | emailVerified DateTime? @map("email_verified")
58 | image String?
59 | accounts Account[]
60 | sessions Session[]
61 |
62 | @@map("users")
63 | }
64 |
65 | model VerificationToken {
66 | identifier String
67 | token String
68 | expires DateTime
69 |
70 | @@unique([identifier, token])
71 | @@map("verificationtokens")
72 | }
73 |
74 | model OTP {
75 | id Int @id @default(autoincrement())
76 | email String @unique
77 | otp String
78 | expiresAt DateTime
79 | }
80 |
81 |
82 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/(auth)/forgot-password/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useForm } from 'react-hook-form';
4 | import { Input } from '@/components/ui/input';
5 | import { Button } from '@/components/ui/button';
6 | import { useToast } from '@/components/hooks/use-toast';
7 | import { useRouter } from 'next/navigation';
8 | import axios from 'axios';
9 |
10 | const ForgotPassword = () => {
11 | const { toast } = useToast();
12 | const router = useRouter();
13 | const form = useForm<{ email: string }>({
14 | defaultValues: { email: '' },
15 | });
16 |
17 | const onSubmit = async (values: { email: string }) => {
18 | try {
19 | const response = await axios.post('/api/auth/reset-password', values);
20 |
21 | if (response.status === 200) {
22 | toast({
23 | title: 'Success',
24 | description: 'OTP has been sent to your email.',
25 | variant: 'default',
26 | });
27 | router.push('/otp-verification');
28 | }
29 | } catch (error) {
30 | toast({
31 | title: 'Error',
32 | description: 'Failed to send OTP. Please try again.',
33 | variant: 'destructive',
34 | });
35 | }
36 | };
37 |
38 | return (
39 |
40 |
Forgot Password
41 |
48 |
49 | );
50 | };
51 |
52 | export default ForgotPassword;
53 |
--------------------------------------------------------------------------------
/src/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { FC, ReactNode } from 'react';
2 |
3 | interface AuthLayoutProps {
4 | children: ReactNode;
5 | }
6 |
7 | const AuthLayout: FC = ({ children }) => {
8 | return {children}
;
9 | };
10 |
11 | export default AuthLayout;
12 |
--------------------------------------------------------------------------------
/src/app/(auth)/otp-verifacation/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState } from 'react';
4 | import { useForm } from 'react-hook-form';
5 | import { Input } from '@/components/ui/input';
6 | import { Button } from '@/components/ui/button';
7 | import { useToast } from "@/components/hooks/use-toast";
8 | import { useRouter } from 'next/navigation';
9 |
10 | const OtpVerification = () => {
11 | const { toast } = useToast();
12 | const router = useRouter();
13 | const [otp, setOtp] = useState(new Array(6).fill(''));
14 |
15 | const { handleSubmit } = useForm<{ otp: string }>({
16 | defaultValues: { otp: otp.join('') },
17 | });
18 |
19 | const handleOtpChange = (index: number, value: string) => {
20 | const newOtp = [...otp];
21 | newOtp[index] = value;
22 | setOtp(newOtp);
23 |
24 | // Handle backspace
25 | if (value === '') {
26 | if (index > 0) {
27 | const prevInput = document.getElementById(`otp-input-${index - 1}`);
28 | prevInput?.focus();
29 | }
30 | } else if (index < 5 && value.length === 1) {
31 | const nextInput = document.getElementById(`otp-input-${index + 1}`);
32 | nextInput?.focus();
33 | }
34 | };
35 |
36 | const onSubmit = async () => {
37 | const otpValue = otp.join('');
38 | const res = await fetch('/api/auth/verify-otp', {
39 | method: 'POST',
40 | headers: { 'Content-Type': 'application/json' },
41 | body: JSON.stringify({ otp: otpValue }),
42 | });
43 |
44 | if (res.ok) {
45 | toast({
46 | title: 'Success',
47 | description: 'OTP verified. Please set a new password.',
48 | variant: 'default',
49 | });
50 | router.push('/set-new-password');
51 | } else {
52 | toast({
53 | title: 'Error',
54 | description: 'Invalid OTP. Please try again.',
55 | variant: 'destructive',
56 | });
57 | }
58 | };
59 |
60 | return (
61 |
62 |
OTP Verification
63 |
87 |
88 | );
89 | };
90 |
91 | export default OtpVerification;
92 |
--------------------------------------------------------------------------------
/src/app/(auth)/set-new-password/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useForm } from 'react-hook-form';
4 | import { Input } from '@/components/ui/input';
5 | import { Button } from '@/components/ui/button';
6 | import { useToast } from "@/components/hooks/use-toast";
7 | import { useRouter } from 'next/navigation';
8 |
9 | const ResetPasswordPage = () => {
10 | const { toast } = useToast();
11 | const router = useRouter();
12 |
13 | const { register, handleSubmit, watch, formState: { errors } } = useForm<{ password: string; confirmPassword: string }>({
14 | defaultValues: { password: '', confirmPassword: '' },
15 | });
16 |
17 | const onSubmit = async (values: { password: string; confirmPassword: string }) => {
18 | if (values.password !== values.confirmPassword) {
19 | toast({
20 | title: 'Error',
21 | description: 'Passwords do not match.',
22 | variant: 'destructive',
23 | });
24 | return;
25 | }
26 |
27 | const response = await fetch('/api/auth/reset-password', {
28 | method: 'POST',
29 | headers: { 'Content-Type': 'application/json' },
30 | body: JSON.stringify({ password: values.password }),
31 | });
32 |
33 | const result = await response.json();
34 |
35 | if (result.success) {
36 | toast({
37 | title: 'Password Reset',
38 | description: 'Your password has been updated successfully.',
39 | variant: 'default',
40 | });
41 | router.push('/sign-in');
42 | } else {
43 | toast({
44 | title: 'Error',
45 | description: 'Something went wrong!',
46 | variant: 'destructive',
47 | });
48 | }
49 | };
50 |
51 | return (
52 |
53 |
Set New Password
54 |
69 |
70 | );
71 | };
72 |
73 | export default ResetPasswordPage;
74 |
--------------------------------------------------------------------------------
/src/app/(auth)/sign-in/page.tsx:
--------------------------------------------------------------------------------
1 | import SignInForm from '@/components/form/SignInForm';
2 |
3 | const page = () => {
4 | return (
5 |
6 |
7 |
8 | );
9 | };
10 |
11 | export default page;
12 |
--------------------------------------------------------------------------------
/src/app/(auth)/sign-up/page.tsx:
--------------------------------------------------------------------------------
1 | import SignUpForm from '@/components/form/SignUpForm';
2 |
3 | const page = () => {
4 | return (
5 |
6 |
7 |
8 | );
9 | };
10 |
11 | export default page;
12 |
--------------------------------------------------------------------------------
/src/app/(dashboard)/admin/page.tsx:
--------------------------------------------------------------------------------
1 | import { authOptions } from '@/lib/auth'
2 | import { getServerSession } from 'next-auth'
3 |
4 | const page = async () => {
5 | const session = await getServerSession(authOptions)
6 |
7 | if (session?.user) {
8 | return Admin page welcome back {session?.user.username || session.user.name}
9 | }
10 | return Please login to see this admin page
11 | }
12 |
13 | export default page
14 |
--------------------------------------------------------------------------------
/src/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth";
2 | import { authOptions } from "@/lib/auth";
3 | const handler = NextAuth(authOptions);
4 |
5 | export { handler as GET, handler as POST };
6 |
--------------------------------------------------------------------------------
/src/app/api/reset-password/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import prisma from '@/lib/prismadb';
3 | import nodemailer from 'nodemailer';
4 | import crypto from 'crypto';
5 |
6 | export async function POST(request: Request) {
7 | try {
8 | const { email } = await request.json();
9 |
10 | console.log("EMAIL_USER:", process.env.EMAIL_USER);
11 | console.log("EMAIL_PASS:", process.env.EMAIL_PASS);
12 |
13 | const user = await prisma.user.findUnique({ where: { email } });
14 |
15 | if (!user) {
16 | return NextResponse.json({ error: 'User not found' }, { status: 404 });
17 | }
18 |
19 | const otp = crypto.randomInt(100000, 999999).toString();
20 |
21 | const transporter = nodemailer.createTransport({
22 | service: 'gmail',
23 | auth: {
24 | user: process.env.EMAIL_USER,
25 | pass: process.env.EMAIL_PASS,
26 | },
27 | });
28 |
29 | const mailOptions = {
30 | from: process.env.EMAIL_USER,
31 | to: email,
32 | subject: 'Password Reset OTP',
33 | text: `Your OTP is ${otp}`,
34 | };
35 |
36 | await transporter.sendMail(mailOptions);
37 |
38 | await prisma.oTP.create({
39 | data: {
40 | email,
41 | otp,
42 | expiresAt: new Date(Date.now() + 15 * 60 * 1000),
43 | },
44 | });
45 |
46 | return NextResponse.json({ message: 'OTP sent successfully' }, { status: 200 });
47 |
48 | } catch (error) {
49 | let errorMessage = 'Internal Server Error';
50 |
51 |
52 | if (error instanceof Error) {
53 | errorMessage = error.message;
54 | }
55 |
56 | console.error('Error:', errorMessage);
57 | return NextResponse.json({ error: errorMessage }, { status: 500 });
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/app/api/update-password/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import prisma from '@/lib/prismadb';
3 | import bcrypt from 'bcryptjs';
4 |
5 | export async function POST(request: Request) {
6 | const { email, newPassword } = await request.json();
7 |
8 | const hashedPassword = await bcrypt.hash(newPassword, 10);
9 |
10 | await prisma.user.update({
11 | where: { email },
12 | data: { password: hashedPassword },
13 | });
14 |
15 | return NextResponse.json({ message: 'Password updated successfully' });
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/api/user/route.ts:
--------------------------------------------------------------------------------
1 | import { prismadb } from "@/lib/prismadb";
2 | import { hash } from "bcrypt";
3 | import { NextResponse } from "next/server";
4 | import * as z from 'zod'
5 |
6 | const userSchema = z
7 | .object({
8 | username: z.string().min(1, 'Username is required').max(100),
9 | email: z.string().min(1, 'Email is required').email('Invalid email'),
10 | password: z
11 | .string()
12 | .min(1, 'Password is required')
13 | .min(8, 'Password must have more than 8 characters'),
14 | })
15 |
16 | export async function POST(req: Request) {
17 | try {
18 | const body = await req.json();
19 | console.log('Request body:', body);
20 | const { email, username, password } = userSchema.parse(body);
21 |
22 | // Check if email already exists
23 | const existingUserByEmail = await prismadb.user.findUnique({
24 | where: { email },
25 | });
26 | if (existingUserByEmail) {
27 | return NextResponse.json({ user: null, message: "User with this email already exists" }, { status: 409 });
28 | }
29 |
30 | // Check if username already exists
31 | const existingUserByUsername = await prismadb.user.findUnique({
32 | where: { username },
33 | });
34 | if (existingUserByUsername) {
35 | return NextResponse.json({ user: null, message: "User with this username already exists" }, { status: 409 });
36 | }
37 |
38 | // Hash the password
39 | const hashedPassword = await hash(password, 10);
40 |
41 | // Create the new user
42 | const newUser = await prismadb.user.create({
43 | data: {
44 | username,
45 | email,
46 | password: hashedPassword,
47 | },
48 | });
49 |
50 | const { password: newUserPassword, ...rest } = newUser;
51 |
52 | return NextResponse.json({ user: rest, message: "User created successfully" }, { status: 201 });
53 | } catch (error) {
54 | console.error("Error creating user:", error);
55 | return NextResponse.json({ message: "Something went wrong!" }, { status: 500 });
56 | }
57 | }
58 |
59 |
--------------------------------------------------------------------------------
/src/app/api/verify-otp/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import prisma from '@/lib/prismadb';
3 |
4 | export async function POST(request: Request) {
5 | const { email, otp } = await request.json();
6 |
7 | // Query OTP by email
8 | const storedOtp = await prisma.oTP.findUnique({ where: { email } });
9 |
10 | if (!storedOtp || storedOtp.otp !== otp || new Date() > storedOtp.expiresAt) {
11 | return NextResponse.json({ error: 'Invalid or expired OTP' }, { status: 400 });
12 | }
13 |
14 | return NextResponse.json({ message: 'OTP verified' });
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code-with-zain-hunzai/NextAuth/74fcefd1d38dbd568847d2735bb716b725da1cda/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | @media (prefers-color-scheme: dark) {
12 | :root {
13 | --foreground-rgb: 255, 255, 255;
14 | --background-start-rgb: 0, 0, 0;
15 | --background-end-rgb: 0, 0, 0;
16 | }
17 | }
18 |
19 | @layer base {
20 | :root {
21 | --background: 0 0% 100%;
22 | --foreground: 0 0% 3.9%;
23 | --card: 0 0% 100%;
24 | --card-foreground: 0 0% 3.9%;
25 | --popover: 0 0% 100%;
26 | --popover-foreground: 0 0% 3.9%;
27 | --primary: 0 0% 9%;
28 | --primary-foreground: 0 0% 98%;
29 | --secondary: 0 0% 96.1%;
30 | --secondary-foreground: 0 0% 9%;
31 | --muted: 0 0% 96.1%;
32 | --muted-foreground: 0 0% 45.1%;
33 | --accent: 0 0% 96.1%;
34 | --accent-foreground: 0 0% 9%;
35 | --destructive: 0 84.2% 60.2%;
36 | --destructive-foreground: 0 0% 98%;
37 | --border: 0 0% 89.8%;
38 | --input: 0 0% 89.8%;
39 | --ring: 0 0% 3.9%;
40 | --chart-1: 12 76% 61%;
41 | --chart-2: 173 58% 39%;
42 | --chart-3: 197 37% 24%;
43 | --chart-4: 43 74% 66%;
44 | --chart-5: 27 87% 67%;
45 | --radius: 0.5rem;
46 | }
47 | .dark {
48 | --background: 0 0% 3.9%;
49 | --foreground: 0 0% 98%;
50 | --card: 0 0% 3.9%;
51 | --card-foreground: 0 0% 98%;
52 | --popover: 0 0% 3.9%;
53 | --popover-foreground: 0 0% 98%;
54 | --primary: 0 0% 98%;
55 | --primary-foreground: 0 0% 9%;
56 | --secondary: 0 0% 14.9%;
57 | --secondary-foreground: 0 0% 98%;
58 | --muted: 0 0% 14.9%;
59 | --muted-foreground: 0 0% 63.9%;
60 | --accent: 0 0% 14.9%;
61 | --accent-foreground: 0 0% 98%;
62 | --destructive: 0 62.8% 30.6%;
63 | --destructive-foreground: 0 0% 98%;
64 | --border: 0 0% 14.9%;
65 | --input: 0 0% 14.9%;
66 | --ring: 0 0% 83.1%;
67 | --chart-1: 220 70% 50%;
68 | --chart-2: 160 60% 45%;
69 | --chart-3: 30 80% 55%;
70 | --chart-4: 280 65% 60%;
71 | --chart-5: 340 75% 55%;
72 | }
73 | }
74 |
75 | @layer base {
76 | * {
77 | @apply border-border;
78 | }
79 | body {
80 | @apply bg-background text-foreground;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import Navbar from '@/components/Navbar';
2 | import Provider from '@/components/Provider';
3 | import { Toaster } from '@/components/ui/toaster';
4 | import '@/styles/globals.css';
5 | import type { Metadata } from 'next';
6 | import { Inter } from 'next/font/google';
7 |
8 | const inter = Inter({ subsets: ['latin'] });
9 |
10 | export const metadata: Metadata = {
11 | title: 'NextAuth',
12 | description: 'Generated by create next app',
13 | };
14 |
15 | export default function RootLayout({
16 | children,
17 | }: {
18 | children: React.ReactNode;
19 | }) {
20 | return (
21 |
22 |
23 |
24 |
25 |
26 | {children}
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import User from "@/components/User"
2 | import { buttonVariants } from "@/components/ui/button"
3 | import { authOptions } from "@/lib/auth"
4 | import { getServerSession } from "next-auth"
5 | import Link from "next/link"
6 | export default async function Home() {
7 | const session = await getServerSession(authOptions)
8 | return
9 |
Home
10 |
11 | Open my admin
12 |
13 |
14 | Client Session
15 |
16 | Server Session
17 | {JSON.stringify(session)}
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/GoogleSignInButton.tsx:
--------------------------------------------------------------------------------
1 | import { FC, ReactNode } from 'react';
2 | import { Button } from './ui/button';
3 | import { signIn } from 'next-auth/react'
4 |
5 | interface GoogleSignInButtonProps {
6 | children: ReactNode;
7 | }
8 | const GoogleSignInButton: FC = ({ children }) => {
9 | const loginWithGoogle = () => signIn('google', { callbackUrl: 'http://localhost:3000/admin' });
10 |
11 | return (
12 |
15 | );
16 | };
17 |
18 | export default GoogleSignInButton;
19 |
--------------------------------------------------------------------------------
/src/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { buttonVariants } from './ui/button';
3 | import { HandMetal } from 'lucide-react';
4 | import { getServerSession } from 'next-auth';
5 | import { authOptions } from '@/lib/auth';
6 | import UserAccountnav from './UserAccountnav';
7 |
8 |
9 | const Navbar = async () => {
10 | const session = await getServerSession(authOptions)
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | {session?.user ? (
18 |
19 | ) : (
20 |
21 | Sign in
22 |
23 | )}
24 |
25 |
26 | );
27 | };
28 |
29 | export default Navbar;
30 |
--------------------------------------------------------------------------------
/src/components/Provider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { SessionProvider } from "next-auth/react"
3 | import { ReactNode, FC } from "react"
4 |
5 | interface ProviderProps {
6 | children: ReactNode
7 | }
8 |
9 | const Provider: FC = ({ children }) => {
10 | return (
11 | {children}
12 | )
13 | }
14 |
15 | export default Provider
16 |
--------------------------------------------------------------------------------
/src/components/User.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useSession } from "next-auth/react";
4 |
5 | const User = () => {
6 | const { data: session } = useSession();
7 |
8 | return (
9 | {JSON.stringify(session, null, 2)}
10 | );
11 | };
12 |
13 | export default User;
14 |
--------------------------------------------------------------------------------
/src/components/UserAccountnav.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { Button } from "./ui/button"
3 | import { signOut } from "next-auth/react"
4 | const UserAccountnav = () => {
5 | return (
6 |
7 |
12 |
13 | )
14 | }
15 |
16 | export default UserAccountnav
17 |
--------------------------------------------------------------------------------
/src/components/form/SignInForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useForm } from 'react-hook-form';
4 | import {
5 | Form,
6 | FormControl,
7 | FormField,
8 | FormItem,
9 | FormLabel,
10 | FormMessage,
11 | } from '../ui/form';
12 | import * as z from 'zod';
13 | import { zodResolver } from '@hookform/resolvers/zod';
14 | import { Input } from '../ui/input';
15 | import { Button } from '../ui/button';
16 | import Link from 'next/link';
17 | import GoogleSignInButton from '../GoogleSignInButton';
18 | import { signIn } from 'next-auth/react';
19 | import { useRouter } from 'next/navigation';
20 | import { useToast } from "@/components/hooks/use-toast"
21 |
22 |
23 |
24 | const FormSchema = z.object({
25 | email: z.string().min(1, 'Email is required').email('Invalid email'),
26 | password: z
27 | .string()
28 | .min(1, 'Password is required')
29 | .min(8, 'Password must have more than 8 characters'),
30 | });
31 |
32 | const SignInForm = () => {
33 | const router = useRouter()
34 | const { toast } = useToast()
35 | const form = useForm>({
36 | resolver: zodResolver(FormSchema),
37 | defaultValues: {
38 | email: '',
39 | password: '',
40 | },
41 | });
42 |
43 | const onSubmit = async (values: z.infer) => {
44 | try {
45 | const signInData = await signIn('credentials', {
46 | email: values.email,
47 | password: values.password,
48 | redirect: false,
49 | });
50 |
51 | if (signInData?.error) {
52 | toast({
53 | title: "Error",
54 | description: "Oops! something wents wrong!",
55 | variant: "destructive"
56 | })
57 | } else {
58 | router.refresh();
59 | router.push('./admin');
60 | }
61 | } catch (error) {
62 | console.error('SignIn error:', error);
63 | }
64 | };
65 |
66 | return (
67 |
105 |
106 | or
107 |
108 | Sign in with Google
109 |
110 | If you don't have an account, please
111 |
112 | Sign up
113 |
114 |
115 |
116 |
117 | Forgot Password?
118 |
119 |
120 |
121 | );
122 | };
123 |
124 | export default SignInForm;
125 |
--------------------------------------------------------------------------------
/src/components/form/SignUpForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useForm } from 'react-hook-form';
4 | import {
5 | Form,
6 | FormControl,
7 | FormField,
8 | FormItem,
9 | FormLabel,
10 | FormMessage,
11 | } from '../ui/form';
12 | import * as z from 'zod';
13 | import { zodResolver } from '@hookform/resolvers/zod';
14 | import { Input } from '../ui/input';
15 | import { Button } from '../ui/button';
16 | import Link from 'next/link';
17 | import GoogleSignInButton from '../GoogleSignInButton';
18 | import { useRouter } from 'next/navigation';
19 | import { useToast } from "@/components/hooks/use-toast"
20 |
21 | const FormSchema = z
22 | .object({
23 | username: z.string().min(1, 'Username is required').max(100),
24 | email: z.string().min(1, 'Email is required').email('Invalid email'),
25 | password: z
26 | .string()
27 | .min(1, 'Password is required')
28 | .min(8, 'Password must have than 8 characters'),
29 | confirmPassword: z.string().min(1, 'Password confirmation is required'),
30 | })
31 | .refine((data) => data.password === data.confirmPassword, {
32 | path: ['confirmPassword'],
33 | message: 'Password do not match',
34 | });
35 |
36 | const SignUpForm = () => {
37 | const router = useRouter()
38 | const { toast } = useToast()
39 | const form = useForm>({
40 | resolver: zodResolver(FormSchema),
41 | defaultValues: {
42 | username: '',
43 | email: '',
44 | password: '',
45 | confirmPassword: '',
46 | },
47 | });
48 |
49 | const onSubmit = async (values: z.infer) => {
50 | const response = await fetch('/api/user', {
51 | method: "POST",
52 | headers: {
53 | 'Content-Type': 'application/json'
54 | },
55 | body: JSON.stringify({
56 | username: values.username,
57 | email: values.email,
58 | password: values.password
59 | })
60 | })
61 |
62 | if (response.ok) {
63 | router.push('/sign-in')
64 | } else {
65 | toast({
66 | title: "Error",
67 | description: "Oops! something wents wrong!",
68 | variant:"destructive"
69 | })
70 | }
71 | };
72 |
73 | return (
74 |
142 |
143 | or
144 |
145 | Sign up with Google
146 |
147 | If you don't have an account, please
148 |
149 | Sign in
150 |
151 |
152 |
153 | );
154 | };
155 |
156 | export default SignUpForm;
157 |
--------------------------------------------------------------------------------
/src/components/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react"
5 |
6 | import type {
7 | ToastActionElement,
8 | ToastProps,
9 | } from "@/components/ui/toast"
10 |
11 | const TOAST_LIMIT = 1
12 | const TOAST_REMOVE_DELAY = 1000000
13 |
14 | type ToasterToast = ToastProps & {
15 | id: string
16 | title?: React.ReactNode
17 | description?: React.ReactNode
18 | action?: ToastActionElement
19 | }
20 |
21 | const actionTypes = {
22 | ADD_TOAST: "ADD_TOAST",
23 | UPDATE_TOAST: "UPDATE_TOAST",
24 | DISMISS_TOAST: "DISMISS_TOAST",
25 | REMOVE_TOAST: "REMOVE_TOAST",
26 | } as const
27 |
28 | let count = 0
29 |
30 | function genId() {
31 | count = (count + 1) % Number.MAX_SAFE_INTEGER
32 | return count.toString()
33 | }
34 |
35 | type ActionType = typeof actionTypes
36 |
37 | type Action =
38 | | {
39 | type: ActionType["ADD_TOAST"]
40 | toast: ToasterToast
41 | }
42 | | {
43 | type: ActionType["UPDATE_TOAST"]
44 | toast: Partial
45 | }
46 | | {
47 | type: ActionType["DISMISS_TOAST"]
48 | toastId?: ToasterToast["id"]
49 | }
50 | | {
51 | type: ActionType["REMOVE_TOAST"]
52 | toastId?: ToasterToast["id"]
53 | }
54 |
55 | interface State {
56 | toasts: ToasterToast[]
57 | }
58 |
59 | const toastTimeouts = new Map>()
60 |
61 | const addToRemoveQueue = (toastId: string) => {
62 | if (toastTimeouts.has(toastId)) {
63 | return
64 | }
65 |
66 | const timeout = setTimeout(() => {
67 | toastTimeouts.delete(toastId)
68 | dispatch({
69 | type: "REMOVE_TOAST",
70 | toastId: toastId,
71 | })
72 | }, TOAST_REMOVE_DELAY)
73 |
74 | toastTimeouts.set(toastId, timeout)
75 | }
76 |
77 | export const reducer = (state: State, action: Action): State => {
78 | switch (action.type) {
79 | case "ADD_TOAST":
80 | return {
81 | ...state,
82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83 | }
84 |
85 | case "UPDATE_TOAST":
86 | return {
87 | ...state,
88 | toasts: state.toasts.map((t) =>
89 | t.id === action.toast.id ? { ...t, ...action.toast } : t
90 | ),
91 | }
92 |
93 | case "DISMISS_TOAST": {
94 | const { toastId } = action
95 |
96 | // ! Side effects ! - This could be extracted into a dismissToast() action,
97 | // but I'll keep it here for simplicity
98 | if (toastId) {
99 | addToRemoveQueue(toastId)
100 | } else {
101 | state.toasts.forEach((toast) => {
102 | addToRemoveQueue(toast.id)
103 | })
104 | }
105 |
106 | return {
107 | ...state,
108 | toasts: state.toasts.map((t) =>
109 | t.id === toastId || toastId === undefined
110 | ? {
111 | ...t,
112 | open: false,
113 | }
114 | : t
115 | ),
116 | }
117 | }
118 | case "REMOVE_TOAST":
119 | if (action.toastId === undefined) {
120 | return {
121 | ...state,
122 | toasts: [],
123 | }
124 | }
125 | return {
126 | ...state,
127 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
128 | }
129 | }
130 | }
131 |
132 | const listeners: Array<(state: State) => void> = []
133 |
134 | let memoryState: State = { toasts: [] }
135 |
136 | function dispatch(action: Action) {
137 | memoryState = reducer(memoryState, action)
138 | listeners.forEach((listener) => {
139 | listener(memoryState)
140 | })
141 | }
142 |
143 | type Toast = Omit
144 |
145 | function toast({ ...props }: Toast) {
146 | const id = genId()
147 |
148 | const update = (props: ToasterToast) =>
149 | dispatch({
150 | type: "UPDATE_TOAST",
151 | toast: { ...props, id },
152 | })
153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
154 |
155 | dispatch({
156 | type: "ADD_TOAST",
157 | toast: {
158 | ...props,
159 | id,
160 | open: true,
161 | onOpenChange: (open) => {
162 | if (!open) dismiss()
163 | },
164 | },
165 | })
166 |
167 | return {
168 | id: id,
169 | dismiss,
170 | update,
171 | }
172 | }
173 |
174 | function useToast() {
175 | const [state, setState] = React.useState(memoryState)
176 |
177 | React.useEffect(() => {
178 | listeners.push(setState)
179 | return () => {
180 | const index = listeners.indexOf(setState)
181 | if (index > -1) {
182 | listeners.splice(index, 1)
183 | }
184 | }
185 | }, [state])
186 |
187 | return {
188 | ...state,
189 | toast,
190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
191 | }
192 | }
193 |
194 | export { useToast, toast }
195 |
--------------------------------------------------------------------------------
/src/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 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 |
--------------------------------------------------------------------------------
/src/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 |
95 | );
96 | });
97 | FormLabel.displayName = 'FormLabel';
98 |
99 | const FormControl = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ ...props }, ref) => {
103 | const { error, formItemId, formDescriptionId, formMessageId } =
104 | useFormField();
105 |
106 | return (
107 |
118 | );
119 | });
120 | FormControl.displayName = 'FormControl';
121 |
122 | const FormDescription = React.forwardRef<
123 | HTMLParagraphElement,
124 | React.HTMLAttributes
125 | >(({ className, ...props }, ref) => {
126 | const { formDescriptionId } = useFormField();
127 |
128 | return (
129 |
135 | );
136 | });
137 | FormDescription.displayName = 'FormDescription';
138 |
139 | const FormMessage = React.forwardRef<
140 | HTMLParagraphElement,
141 | React.HTMLAttributes
142 | >(({ className, children, ...props }, ref) => {
143 | const { error, formMessageId } = useFormField();
144 | const body = error ? String(error?.message) : children;
145 |
146 | if (!body) {
147 | return null;
148 | }
149 |
150 | return (
151 |
157 | {body}
158 |
159 | );
160 | });
161 | FormMessage.displayName = 'FormMessage';
162 |
163 | export {
164 | useFormField,
165 | Form,
166 | FormItem,
167 | FormLabel,
168 | FormControl,
169 | FormDescription,
170 | FormMessage,
171 | FormField,
172 | };
173 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/src/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToastPrimitives from "@radix-ui/react-toast"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const ToastProvider = ToastPrimitives.Provider
11 |
12 | const ToastViewport = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, ...props }, ref) => (
16 |
24 | ))
25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
26 |
27 | const toastVariants = cva(
28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
29 | {
30 | variants: {
31 | variant: {
32 | default: "border bg-background text-foreground",
33 | destructive:
34 | "destructive group border-destructive bg-destructive text-destructive-foreground",
35 | },
36 | },
37 | defaultVariants: {
38 | variant: "default",
39 | },
40 | }
41 | )
42 |
43 | const Toast = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef &
46 | VariantProps
47 | >(({ className, variant, ...props }, ref) => {
48 | return (
49 |
54 | )
55 | })
56 | Toast.displayName = ToastPrimitives.Root.displayName
57 |
58 | const ToastAction = React.forwardRef<
59 | React.ElementRef,
60 | React.ComponentPropsWithoutRef
61 | >(({ className, ...props }, ref) => (
62 |
70 | ))
71 | ToastAction.displayName = ToastPrimitives.Action.displayName
72 |
73 | const ToastClose = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >(({ className, ...props }, ref) => (
77 |
86 |
87 |
88 | ))
89 | ToastClose.displayName = ToastPrimitives.Close.displayName
90 |
91 | const ToastTitle = React.forwardRef<
92 | React.ElementRef,
93 | React.ComponentPropsWithoutRef
94 | >(({ className, ...props }, ref) => (
95 |
100 | ))
101 | ToastTitle.displayName = ToastPrimitives.Title.displayName
102 |
103 | const ToastDescription = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | ToastDescription.displayName = ToastPrimitives.Description.displayName
114 |
115 | type ToastProps = React.ComponentPropsWithoutRef
116 |
117 | type ToastActionElement = React.ReactElement
118 |
119 | export {
120 | type ToastProps,
121 | type ToastActionElement,
122 | ToastProvider,
123 | ToastViewport,
124 | Toast,
125 | ToastTitle,
126 | ToastDescription,
127 | ToastClose,
128 | ToastAction,
129 | }
130 |
--------------------------------------------------------------------------------
/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useToast } from "@/components/hooks/use-toast"
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from "@/components/ui/toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/src/lib/auth.ts:
--------------------------------------------------------------------------------
1 | import { PrismaAdapter } from "@next-auth/prisma-adapter";
2 | import { NextAuthOptions } from "next-auth";
3 | import CredentialsProvider from "next-auth/providers/credentials";
4 | import GoogleProvider from "next-auth/providers/google";
5 | import { prismadb } from "./prismadb";
6 | import bcrypt from "bcrypt";
7 |
8 | export const authOptions: NextAuthOptions = {
9 | adapter: PrismaAdapter(prismadb),
10 | secret: process.env.NEXTAUTH_SECRET,
11 | session: {
12 | strategy: "jwt",
13 | },
14 | pages: {
15 | signIn: "/sign-in",
16 | },
17 | providers: [
18 | GoogleProvider({
19 | clientId: process.env.GOOGLE_CLIENT_ID!,
20 | clientSecret: process.env.GOOGLE_CLIENT_SECRET!
21 | }),
22 | CredentialsProvider({
23 | name: "Credentials",
24 | credentials: {
25 | email: { label: "Email", type: "email", placeholder: "jhon@example.com" },
26 | password: { label: "Password", type: "password" },
27 | },
28 | async authorize(credentials) {
29 | if (!credentials?.email || !credentials?.password) {
30 | throw new Error("Email and password are required");
31 | }
32 |
33 | const existingUser = await prismadb.user.findUnique({
34 | where: { email: credentials.email },
35 | });
36 |
37 | if (!existingUser) {
38 | throw new Error("No user found with this email");
39 | }
40 |
41 | if(existingUser.password){
42 | const passwordMatch = await bcrypt.compare(credentials.password, existingUser.password);
43 | if (!passwordMatch) {
44 | throw new Error("Incorrect password");
45 | }
46 | }
47 |
48 | return {
49 | id: `${existingUser.id}`,
50 | username: existingUser.username,
51 | email: existingUser.email,
52 | };
53 | },
54 | }),
55 | ],
56 | callbacks: {
57 | async jwt({ token, user, }) {
58 | console.log(token, user)
59 | if (user) {
60 | return {
61 | ...token,
62 | username: user.username
63 | }
64 | }
65 | return token
66 | },
67 | async session({ session, token }) {
68 | return {
69 | ...session,
70 | user: {
71 | ...session.user,
72 | username: token.username
73 | }
74 | }
75 | },
76 | }
77 | };
78 |
--------------------------------------------------------------------------------
/src/lib/prismadb.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client'
2 |
3 | const prismaClientSingleton = () => {
4 | return new PrismaClient()
5 | }
6 |
7 | declare const globalThis: {
8 | prismaGlobal: ReturnType;
9 | } & typeof global;
10 |
11 | const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()
12 |
13 | export default prisma
14 |
15 | if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma
16 |
17 | export const prismadb = prisma
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src/styles/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 | --muted: 210 40% 96.1%;
11 | --muted-foreground: 215.4 16.3% 46.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 222.2 84% 4.9%;
15 |
16 | --card: 0 0% 100%;
17 | --card-foreground: 222.2 84% 4.9%;
18 |
19 | --border: 214.3 31.8% 91.4%;
20 | --input: 214.3 31.8% 91.4%;
21 |
22 | --primary: 222.2 47.4% 11.2%;
23 | --primary-foreground: 210 40% 98%;
24 |
25 | --secondary: 210 40% 96.1%;
26 | --secondary-foreground: 222.2 47.4% 11.2%;
27 |
28 | --accent: 210 40% 96.1%;
29 | --accent-foreground: 222.2 47.4% 11.2%;
30 |
31 | --destructive: 0 84.2% 60.2%;
32 | --destructive-foreground: 210 40% 98%;
33 |
34 | --ring: 215 20.2% 65.1%;
35 |
36 | --radius: 0.5rem;
37 | }
38 |
39 | .dark {
40 | --background: 222.2 84% 4.9%;
41 | --foreground: 210 40% 98%;
42 |
43 | --muted: 217.2 32.6% 17.5%;
44 | --muted-foreground: 215 20.2% 65.1%;
45 |
46 | --popover: 222.2 84% 4.9%;
47 | --popover-foreground: 210 40% 98%;
48 |
49 | --card: 222.2 84% 4.9%;
50 | --card-foreground: 210 40% 98%;
51 |
52 | --border: 217.2 32.6% 17.5%;
53 | --input: 217.2 32.6% 17.5%;
54 |
55 | --primary: 210 40% 98%;
56 | --primary-foreground: 222.2 47.4% 11.2%;
57 |
58 | --secondary: 217.2 32.6% 17.5%;
59 | --secondary-foreground: 210 40% 98%;
60 |
61 | --accent: 217.2 32.6% 17.5%;
62 | --accent-foreground: 210 40% 98%;
63 |
64 | --destructive: 0 62.8% 30.6%;
65 | --destructive-foreground: 0 85.7% 97.3%;
66 |
67 | --ring: 217.2 32.6% 17.5%;
68 | }
69 | }
70 |
71 | /* @layer base {
72 | * {
73 | @apply border-border;
74 | }
75 | body {
76 | @apply bg-background text-foreground;
77 | } */
78 |
--------------------------------------------------------------------------------
/src/types/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth"
2 |
3 | declare module "next-auth" {
4 | interface User {
5 | username: string | null
6 | }
7 | interface Session {
8 | user: User & {
9 | username: string
10 | }
11 | token: {
12 | username: string
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | darkMode: ["class"],
5 | content: [
6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
9 | ],
10 | theme: {
11 | extend: {
12 | backgroundImage: {
13 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
14 | 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))'
15 | },
16 | borderRadius: {
17 | lg: 'var(--radius)',
18 | md: 'calc(var(--radius) - 2px)',
19 | sm: 'calc(var(--radius) - 4px)'
20 | },
21 | colors: {
22 | background: 'hsl(var(--background))',
23 | foreground: 'hsl(var(--foreground))',
24 | card: {
25 | DEFAULT: 'hsl(var(--card))',
26 | foreground: 'hsl(var(--card-foreground))'
27 | },
28 | popover: {
29 | DEFAULT: 'hsl(var(--popover))',
30 | foreground: 'hsl(var(--popover-foreground))'
31 | },
32 | primary: {
33 | DEFAULT: 'hsl(var(--primary))',
34 | foreground: 'hsl(var(--primary-foreground))'
35 | },
36 | secondary: {
37 | DEFAULT: 'hsl(var(--secondary))',
38 | foreground: 'hsl(var(--secondary-foreground))'
39 | },
40 | muted: {
41 | DEFAULT: 'hsl(var(--muted))',
42 | foreground: 'hsl(var(--muted-foreground))'
43 | },
44 | accent: {
45 | DEFAULT: 'hsl(var(--accent))',
46 | foreground: 'hsl(var(--accent-foreground))'
47 | },
48 | destructive: {
49 | DEFAULT: 'hsl(var(--destructive))',
50 | foreground: 'hsl(var(--destructive-foreground))'
51 | },
52 | border: 'hsl(var(--border))',
53 | input: 'hsl(var(--input))',
54 | ring: 'hsl(var(--ring))',
55 | chart: {
56 | '1': 'hsl(var(--chart-1))',
57 | '2': 'hsl(var(--chart-2))',
58 | '3': 'hsl(var(--chart-3))',
59 | '4': 'hsl(var(--chart-4))',
60 | '5': 'hsl(var(--chart-5))'
61 | }
62 | }
63 | }
64 | },
65 | plugins: [require("tailwindcss-animate")],
66 | };
67 | export default config;
68 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------