diff --git a/.env.example b/.env.example index ee81ea5..088e8d0 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,11 @@ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/wedding_planner NEXTAUTH_SECRET=your-secret -NEXTAUTH_URL=http://localhost:3000 \ No newline at end of file +NEXTAUTH_URL=http://localhost:3000 +NEXT_PUBLIC_BASE_URL=http://localhost:3000 + +SMTP_HOST=smtp.example.com +SMTP_PORT=587 +SMTP_USER=your@email.com +SMTP_PASS=yourpassword +SMTP_FROM_NAME=Wedding Planner +SMTP_FROM_EMAIL=your@email.com \ No newline at end of file diff --git a/README.md b/README.md index 2937010..92464fa 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,15 @@ My goal for this project is to be an all-in-one self hosted event planner for ma ## Table of Contents - [Planned Features](#planned-features) +- [Updates](#updates) - [Getting Started](#getting-started) - [Built With](#built-with) ## Planned Features - [x] Account Creation - [x] First time setup to create the admin user - - [ ] Invite partner and or "Planner" via email (smtp) + - [x] Invite users via email (smtp) users can be COUPLE, PLANNER, GUEST + - [x] Create local accounts (no use of SMTP) - [ ] Creating custom events - [ ] Information about each event - Date/Time @@ -29,6 +31,15 @@ My goal for this project is to be an all-in-one self hosted event planner for ma - Seating Charts - Calendar/Timeline Builder +## Updates +#### 6.24.25 +- added ability to invite users via email making use of a smtp server and nodemailer + - inviteTokens added to db which are used to sign up and expire onDate and after use +- added ability to create local users if you don't want to use smtp `/admin/create-user` +- created user pages +- added usernames to `Users` table +- updated first time setup to include username creation + ## Getting Started This is very much a work in progress but this `README` will stay up to date on working features and steps to get it running **in its current state**. That being said if you're interested in starting it as is, you can follow these instructions. @@ -42,6 +53,14 @@ git clone https://github.com/briannelson95/wedding-planner.git DATABASE_URL=postgresql://postgres:postgres@localhost:5432/wedding_planner NEXTAUTH_SECRET=your-secret NEXTAUTH_URL=http://localhost:3000 +NEXT_PUBLIC_BASE_URL=http://localhost:3000 + +SMTP_HOST=smtp.example.com +SMTP_PORT=587 +SMTP_USER=your@email.com +SMTP_PASS=yourpassword +SMTP_FROM_NAME=Wedding Planner +SMTP_FROM_EMAIL=your@email.com ``` 3. Start the database @@ -49,7 +68,13 @@ NEXTAUTH_URL=http://localhost:3000 docker compose up -d ``` -4. Install dependencies and start the front end with `npm i && npm run dev` or `bun i && bun dev` +4. Migrate and Generate the Prima files +``` +npx prisma migrate dev --name init +npx prisma generate +``` + +5. Install dependencies and start the front end with `npm i && npm run dev` or `bun i && bun dev` ## Built With - NextJS 15 diff --git a/app/(auth)/admin/create-user/page.tsx b/app/(auth)/admin/create-user/page.tsx new file mode 100644 index 0000000..c0ac47b --- /dev/null +++ b/app/(auth)/admin/create-user/page.tsx @@ -0,0 +1,31 @@ +'use client' +import { useState } from 'react' + +export default function CreateUserPage() { + const [form, setForm] = useState({ username: '', password: '', role: 'GUEST' }) + const [success, setSuccess] = useState(false) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + await fetch('/api/admin/create-user', { + method: 'POST', + body: JSON.stringify(form), + }) + setSuccess(true) + } + + return ( +
+

Create Local User

+ setForm({ ...form, username: e.target.value })} /> + setForm({ ...form, password: e.target.value })} /> + + + {success &&

User created

} +
+ ) +} diff --git a/app/(auth)/dashboard/page.tsx b/app/(auth)/dashboard/page.tsx index 0696fb4..0a217c9 100644 --- a/app/(auth)/dashboard/page.tsx +++ b/app/(auth)/dashboard/page.tsx @@ -4,7 +4,7 @@ import React from 'react' export default async function DashboardPage() { const events = await queries.fetchEvents() - console.log(events) + // console.log(events) return (
@@ -12,15 +12,17 @@ export default async function DashboardPage() {

Your Events

{events.map((item) => ( -
-

ID: {item.id}

-

Name: {item.name}

-

Date: {item.date ? item.date.toISOString() : 'null'}

-

Location: {item.location ? item.location : 'null'}

-

Creator ID:{item.creatorId}

-

Created At:{item.createdAt.toISOString()}

- -
+ +
+

ID: {item.id}

+

Name: {item.name}

+

Date: {item.date ? item.date.toISOString() : 'null'}

+

Location: {item.location ? item.location : 'null'}

+

Creator ID:{item.creatorId}

+

Created At:{item.createdAt.toISOString()}

+ +
+ ))} - SINGLE EVENT PAGE +

+ {data?.name} +

) } diff --git a/app/(auth)/events/page.tsx b/app/(auth)/events/page.tsx index 1f97056..b50e4b3 100644 --- a/app/(auth)/events/page.tsx +++ b/app/(auth)/events/page.tsx @@ -10,10 +10,32 @@ export default async function EventsPage() {
Events
- {allEvents.length == 0 && ( + {allEvents.length == 0 ? ( <> You don't have any events yet. Create One! + ) : ( + + + + + + + + + + {allEvents.map((item) => ( + + + + + + ))} + +
Event NameEvent DateCreated by
{item.name}{item.date?.toDateString()}{item.creatorId}
)}
diff --git a/app/(auth)/user/[username]/page.tsx b/app/(auth)/user/[username]/page.tsx new file mode 100644 index 0000000..2725f60 --- /dev/null +++ b/app/(auth)/user/[username]/page.tsx @@ -0,0 +1,35 @@ +import SendInviteForm from '@/components/SendInviteForm'; +import { prisma } from '@/lib/prisma'; +import { notFound } from 'next/navigation'; +import React from 'react' + +export default async function UserPage({ params }: { params: { username: string } }) { + const raw = params.username + const username = raw.startsWith('@') ? raw.slice(1) : raw + + const user = await prisma.user.findUnique({ + where: { username }, + select: { + id: true, + email: true, + name: true, + role: true, + createdAt: true, + }, + }) + + if (!user) notFound() + + return ( + <> +
+

@{username}

+

Email: {user.email}

+

Role: {user.role}

+

Joined: {user.createdAt.toDateString()}

+
+

Invite More People

+ + + ) +} diff --git a/app/(public)/invite/accept/page.tsx b/app/(public)/invite/accept/page.tsx new file mode 100644 index 0000000..3477bfa --- /dev/null +++ b/app/(public)/invite/accept/page.tsx @@ -0,0 +1,12 @@ +import { verifyInvite } from '@/lib/invite'; +import { redirect } from 'next/navigation'; + +export default async function AcceptInvitePage({ searchParams }: { searchParams: { token?: string } }) { + const invite = searchParams.token ? await verifyInvite(searchParams.token) : null + + if (!invite || invite.accepted || new Date(invite.expiresAt) < new Date()) { + return
Invalid or expired invitation.
+ } + + redirect(`/signup?token=${invite.token}`) +} \ No newline at end of file diff --git a/app/(public)/setup/page.tsx b/app/(public)/setup/page.tsx index 77973ce..637c66b 100644 --- a/app/(public)/setup/page.tsx +++ b/app/(public)/setup/page.tsx @@ -7,6 +7,7 @@ export default function SetupPage() { const [role, setRole] = useState<'COUPLE' | 'PLANNER' | null>(null); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); + const [username, setUsername] = useState(''); async function handleSetup(e:React.FormEvent) { e.preventDefault(); @@ -31,13 +32,13 @@ export default function SetupPage() { className='hover:cursor-pointer dark:bg-white dark:text-black rounded px-4 py-2' onClick={() => setRole('COUPLE')} > - I'm part of the Couple + I'm part of the Couple
); @@ -45,6 +46,13 @@ export default function SetupPage() { return (

Create your account ({role})

+ setUsername(e.target.value)} + required + /> Invalid or expired invitation. + } + + return ( +
+

Complete Your Signup

+ +
+ ) +} diff --git a/app/api/admin/create-user/route.ts b/app/api/admin/create-user/route.ts new file mode 100644 index 0000000..3827f83 --- /dev/null +++ b/app/api/admin/create-user/route.ts @@ -0,0 +1,19 @@ +import { mutations } from '@/lib/mutations' +import { NextRequest, NextResponse } from 'next/server' + +export async function POST(req: NextRequest) { + const body = await req.json() + + if (!body.username || !body.password || !body.role) { + return new NextResponse('Missing fields', { status: 400 }) + } + + const user = await mutations.createUser({ + username: body.username, + password: body.password, + role: body.role, + email: '', + }) + + return NextResponse.json({ id: user.id }) +} diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index f58d6e0..8a35123 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -15,7 +15,6 @@ export const authOptions: NextAuthOptions = { }, async authorize(credentials) { if (!credentials?.email || !credentials?.password) { - console.log('[AUTH] Missing credentials') return null } @@ -24,28 +23,24 @@ export const authOptions: NextAuthOptions = { }) if (!user) { - console.log('[AUTH] User not found') return null } if (!user.password) { - console.log('[AUTH] User has no password set') return null } const isValid = await bcrypt.compare(credentials.password, user.password) if (!isValid) { - console.log('[AUTH] Invalid password') return null } - console.log('[AUTH] Successful login', user.email) - return { id: user.id, email: user.email, name: user.name, role: user.role, + username: user.username!, } }, }), @@ -58,6 +53,7 @@ export const authOptions: NextAuthOptions = { if (user) { token.id = user.id token.role = user.role + token.username = user.username } return token }, @@ -65,6 +61,7 @@ export const authOptions: NextAuthOptions = { if (session.user) { session.user.id = token.id as string session.user.role = token.role as "COUPLE" | "PLANNER" | "GUEST" + session.user.username = token.username as string } return session }, diff --git a/app/api/invite/send/route.ts b/app/api/invite/send/route.ts new file mode 100644 index 0000000..8f514fa --- /dev/null +++ b/app/api/invite/send/route.ts @@ -0,0 +1,52 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' +import { authOptions } from '../../auth/[...nextauth]/route' +import { sendInviteEmail } from '@/lib/email' +import { createInvite } from '@/lib/invite' +import { prisma } from '@/lib/prisma' + +export async function POST(req: NextRequest) { + try { + const session = await getServerSession(authOptions) + + if (!session?.user || !['COUPLE', 'PLANNER'].includes(session.user.role)) { + return new NextResponse('Unauthorized', { status: 403 }) + } + + const { email, role } = await req.json() + if (!email || !role) { + return NextResponse.json({ message: 'Missing email or role' }, { status: 400 }) + } + + const existingUser = await prisma.user.findUnique({ where: { email } }) + if (existingUser) { + return NextResponse.json({ message: 'User with this email already exists' }, { status: 400 }) + } + + const existingInvite = await prisma.inviteToken.findFirst({ + where: { + email, + accepted: false, + }, + }) + + if (existingInvite) { + return NextResponse.json({ message: 'An invite already exists for this email' }, { status: 400 }) + } + + const invite = await createInvite({ email, role }) + const inviteUrl = `${process.env.NEXT_PUBLIC_BASE_URL}/invite/accept?token=${invite.token}` + + await sendInviteEmail({ + to: email, + inviterName: session.user.email || 'A wedding planner', + inviteUrl, + role, + }) + + return NextResponse.json({ success: true }) + } catch (error) { + console.error('[INVITE SEND ERROR]', error) + return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 }) + } +} diff --git a/app/api/invite/validate/route.ts b/app/api/invite/validate/route.ts new file mode 100644 index 0000000..05690eb --- /dev/null +++ b/app/api/invite/validate/route.ts @@ -0,0 +1,20 @@ +// app/api/invite/validate/route.ts +import { prisma } from '@/lib/prisma' +import { NextRequest, NextResponse } from 'next/server' + +export async function POST(req: NextRequest) { + const { token } = await req.json() + + const invite = await prisma.inviteToken.findUnique({ + where: { token }, + }) + + if (!invite || invite.accepted) { + return new NextResponse('Invalid or expired invite', { status: 400 }) + } + + return NextResponse.json({ + email: invite.email, + role: invite.role, + }) +} diff --git a/app/api/setup/route.ts b/app/api/setup/route.ts index f628037..beba9cc 100644 --- a/app/api/setup/route.ts +++ b/app/api/setup/route.ts @@ -3,7 +3,14 @@ import { prisma } from '@/lib/prisma'; import bcrypt from 'bcrypt'; export async function POST(req: NextRequest) { - const { email, password, role } = await req.json(); + const { email, username, password, role } = await req.json(); + + const existingUsername = await prisma.user.findUnique({ + where: { username }, + }) + if (existingUsername) { + return new NextResponse('Username already taken', { status: 400 }) + } const existing = await prisma.user.findUnique({ where: { email }}); if (existing) return new NextResponse('User already exists', { status: 400 }); @@ -12,6 +19,7 @@ export async function POST(req: NextRequest) { const user = await prisma.user.create({ data: { email, + username, password: hashed, role } diff --git a/app/api/signup/from-invite/route.ts b/app/api/signup/from-invite/route.ts new file mode 100644 index 0000000..5809329 --- /dev/null +++ b/app/api/signup/from-invite/route.ts @@ -0,0 +1,55 @@ +import { prisma } from '@/lib/prisma' +import { NextRequest, NextResponse } from 'next/server' +import bcrypt from 'bcrypt' + +export async function POST(req: NextRequest) { + try { + const { token, username, password } = await req.json() + + if (!token || !username || !password) { + return NextResponse.json({ message: 'Missing fields' }, { status: 400 }) + } + + const invite = await prisma.inviteToken.findUnique({ + where: { token }, + }) + + if (!invite || invite.accepted || new Date(invite.expiresAt) < new Date()) { + return NextResponse.json({ message: 'Invalid or expired invite' }, { status: 400 }) + } + + const existingUser = await prisma.user.findFirst({ + where: { + OR: [ + { email: invite.email }, + { username }, + ], + }, + }) + + if (existingUser) { + return NextResponse.json({ message: 'A user with this email or username already exists' }, { status: 400 }) + } + + const hashedPassword = await bcrypt.hash(password, 10) + + await prisma.user.create({ + data: { + email: invite.email, + username, + role: invite.role, + password: hashedPassword, + }, + }) + + await prisma.inviteToken.update({ + where: { token }, + data: { accepted: true }, + }) + + return NextResponse.json({ success: true }) + } catch (err) { + console.error('[SIGNUP ERROR]', err) + return new NextResponse('Internal Server Error', { status: 500 }) + } +} diff --git a/app/api/test-email/route.ts b/app/api/test-email/route.ts new file mode 100644 index 0000000..acf0929 --- /dev/null +++ b/app/api/test-email/route.ts @@ -0,0 +1,12 @@ +import { sendInviteEmail } from '@/lib/email' +import { NextResponse } from 'next/server' + +export async function GET() { + await sendInviteEmail({ + to: 'brian@briannelson.dev', + token: 'testtoken123', + inviterName: 'Test Admin', + }) + + return NextResponse.json({ status: 'sent' }) +} diff --git a/bun.lockb b/bun.lockb index 15d4977..81907f5 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/Navbar.tsx b/components/Navbar.tsx index d2f344f..66cc537 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -24,10 +24,12 @@ export default function Navbar() {
- - - {session.user.email} ({session.user.role}) - + + + + {session.user.email} ({session.user.role}) + + + + ) +} diff --git a/components/SignupForm.tsx b/components/SignupForm.tsx new file mode 100644 index 0000000..f1e06b2 --- /dev/null +++ b/components/SignupForm.tsx @@ -0,0 +1,68 @@ +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' + +interface Props { + invite: { + token: string + email: string + role: 'COUPLE' | 'PLANNER' | 'GUEST' + } +} + +export default function SignupForm({ invite }: Props) { + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState('') + const router = useRouter() + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + + const res = await fetch('/api/signup/from-invite', { + method: 'POST', + body: JSON.stringify({ token: invite.token, username, password }), + headers: { 'Content-Type': 'application/json' }, + }) + + if (res.ok) { + router.push('/login') + } else { + const { message } = await res.json() + setError(message || 'Signup failed') + } + } + + return ( +
+

+ Invited as {invite.email} ({invite.role}) +

+ + setUsername(e.target.value)} + required + /> + + setPassword(e.target.value)} + required + /> + + {error &&

{error}

} + + +
+ ) +} diff --git a/lib/email.ts b/lib/email.ts new file mode 100644 index 0000000..b551cd4 --- /dev/null +++ b/lib/email.ts @@ -0,0 +1,38 @@ +import nodemailer from 'nodemailer' + +const transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: Number(process.env.SMTP_PORT), + secure: false, + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASS, + } +}) + +export async function sendInviteEmail({ + to, + inviterName, + inviteUrl, + role, +}: { + to: string + inviterName: string + inviteUrl: string + role: string +}) { + const fromName = process.env.SMTP_FROM_NAME || 'Wedding Planner' + const fromEmail = process.env.SMTP_FROM_EMAIL || 'noreply@example.com' + + await transporter.sendMail({ + from: `"${fromName}" <${fromEmail}>`, + to, + subject: `${inviterName} invited you to join their wedding planner`, + html: ` +

Hello!

+

${inviterName} invited you to join their wedding planning space as a ${role}.

+

Click here to accept your invite

+

This link will expire in 72 hours.

+ `, + }) +} \ No newline at end of file diff --git a/lib/invite.ts b/lib/invite.ts new file mode 100644 index 0000000..cd1ac51 --- /dev/null +++ b/lib/invite.ts @@ -0,0 +1,32 @@ +import { prisma } from './prisma'; +import { randomBytes } from 'crypto'; + +export async function createInvite({ + email, + role, + eventId +}: { + email: string + role: 'COUPLE' | 'PLANNER' | 'GUEST' + eventId?: string +}) { + const token = randomBytes(32).toString('hex'); + + const invite = await prisma.inviteToken.create({ + data: { + email, + role, + token, + eventId, + expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24), + }, + }) + + return invite +} + +export async function verifyInvite(token:string) { + return await prisma.inviteToken.findUnique({ + where: { token } + }) +} \ No newline at end of file diff --git a/lib/mutations.ts b/lib/mutations.ts index c97e1e5..3994602 100644 --- a/lib/mutations.ts +++ b/lib/mutations.ts @@ -1,4 +1,5 @@ import { prisma } from './prisma'; +import bcrypt from 'bcrypt' export const mutations = { async createEvent(data: { @@ -30,5 +31,30 @@ export const mutations = { email: data.email, }, }); - } + }, + + async createUser({ + username, + email, + password, + role, + }: { + username: string + email?: string + password: string + role: 'COUPLE' | 'PLANNER' | 'GUEST' + }) { + const hashedPassword = await bcrypt.hash(password, 10) + + const user = await prisma.user.create({ + data: { + username, + email: email || '', + password: hashedPassword, + role, + }, + }) + + return user + }, }; \ No newline at end of file diff --git a/lib/queries.ts b/lib/queries.ts index fd888df..270a517 100644 --- a/lib/queries.ts +++ b/lib/queries.ts @@ -5,5 +5,18 @@ export const queries = { const allEvents = await prisma.event.findMany() console.log(allEvents) return allEvents; + }, + + async singleEvent(id: string) { + const event = await prisma.event.findUnique({ + where: { id }, + include: { + creator: { + select: { id: true, email: true, name: true, role: true }, + }, + guests: true + } + }) + return event } } \ No newline at end of file diff --git a/package.json b/package.json index 61dd651..4e20437 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,12 @@ }, "dependencies": { "@next-auth/prisma-adapter": "^1.0.7", + "@types/nodemailer": "^6.4.17", "bcrypt": "^6.0.0", + "crypto": "^1.0.1", "next": "15.3.4", "next-auth": "^4.24.11", + "nodemailer": "^7.0.3", "prisma": "^6.10.1", "react": "^19.0.0", "react-dom": "^19.0.0" diff --git a/prisma/migrations/20250624131750_add_username/migration.sql b/prisma/migrations/20250624131750_add_username/migration.sql new file mode 100644 index 0000000..9e1a38e --- /dev/null +++ b/prisma/migrations/20250624131750_add_username/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - A unique constraint covering the columns `[username]` on the table `User` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "username" TEXT; + +-- CreateIndex +CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); diff --git a/prisma/migrations/20250624133959_add_username/migration.sql b/prisma/migrations/20250624133959_add_username/migration.sql new file mode 100644 index 0000000..9a69639 --- /dev/null +++ b/prisma/migrations/20250624133959_add_username/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Made the column `username` on table `User` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "username" SET NOT NULL; diff --git a/prisma/migrations/20250624161651_invite_tokens/migration.sql b/prisma/migrations/20250624161651_invite_tokens/migration.sql new file mode 100644 index 0000000..5f94e06 --- /dev/null +++ b/prisma/migrations/20250624161651_invite_tokens/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "InviteToken" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "role" "Role" NOT NULL, + "token" TEXT NOT NULL, + "eventId" TEXT, + "expiresAt" TIMESTAMP(3) NOT NULL, + "accepted" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "InviteToken_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "InviteToken_token_key" ON "InviteToken"("token"); diff --git a/prisma/migrations/20250624193636_make_email_unique/migration.sql b/prisma/migrations/20250624193636_make_email_unique/migration.sql new file mode 100644 index 0000000..13dd289 --- /dev/null +++ b/prisma/migrations/20250624193636_make_email_unique/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[email]` on the table `InviteToken` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "InviteToken_email_key" ON "InviteToken"("email"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4a1629f..e1829bf 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,6 +12,7 @@ model User { email String @unique password String? // hashed password name String? + username String @unique role Role @default(GUEST) events Event[] @relation("EventCreator") createdAt DateTime @default(now()) @@ -48,3 +49,14 @@ enum RsvpStatus { NO PENDING } + +model InviteToken { + id String @id @default(cuid()) + email String @unique + role Role + token String @unique + eventId String? + expiresAt DateTime + accepted Boolean @default(false) + createdAt DateTime @default(now()) +} diff --git a/types/next-auth.d.ts b/types/next-auth.d.ts index 618203b..f8d6db4 100644 --- a/types/next-auth.d.ts +++ b/types/next-auth.d.ts @@ -6,16 +6,19 @@ declare module "next-auth" { id: string email: string role: "COUPLE" | "PLANNER" | "GUEST" + username: string } } interface User { id: string role: "COUPLE" | "PLANNER" | "GUEST" + username: string } interface JWT { id: string role: "COUPLE" | "PLANNER" | "GUEST" + username: string } }