user creation and invites

This commit is contained in:
2025-06-24 16:31:13 -04:00
parent 23c8f468fe
commit a659401bde
32 changed files with 667 additions and 30 deletions

View File

@@ -1,3 +1,11 @@
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/wedding_planner DATABASE_URL=postgresql://postgres:postgres@localhost:5432/wedding_planner
NEXTAUTH_SECRET=your-secret NEXTAUTH_SECRET=your-secret
NEXTAUTH_URL=http://localhost:3000 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

View File

@@ -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 ## Table of Contents
- [Planned Features](#planned-features) - [Planned Features](#planned-features)
- [Updates](#updates)
- [Getting Started](#getting-started) - [Getting Started](#getting-started)
- [Built With](#built-with) - [Built With](#built-with)
## Planned Features ## Planned Features
- [x] Account Creation - [x] Account Creation
- [x] First time setup to create the admin user - [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 - [ ] Creating custom events
- [ ] Information about each event - [ ] Information about each event
- Date/Time - 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 - Seating Charts
- Calendar/Timeline Builder - 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 ## 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. 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 DATABASE_URL=postgresql://postgres:postgres@localhost:5432/wedding_planner
NEXTAUTH_SECRET=your-secret NEXTAUTH_SECRET=your-secret
NEXTAUTH_URL=http://localhost:3000 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 3. Start the database
@@ -49,7 +68,13 @@ NEXTAUTH_URL=http://localhost:3000
docker compose up -d 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 ## Built With
- NextJS 15 - NextJS 15

View File

@@ -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 (
<form onSubmit={handleSubmit} className="max-w-sm mx-auto mt-10 space-y-4">
<h1 className="text-xl font-bold">Create Local User</h1>
<input placeholder="Username" onChange={e => setForm({ ...form, username: e.target.value })} />
<input placeholder="Password" type="password" onChange={e => setForm({ ...form, password: e.target.value })} />
<select onChange={e => setForm({ ...form, role: e.target.value })}>
<option value="COUPLE">COUPLE</option>
<option value="PLANNER">PLANNER</option>
<option value="GUEST">GUEST</option>
</select>
<button className="btn">Create</button>
{success && <p className="text-green-600">User created</p>}
</form>
)
}

View File

@@ -4,7 +4,7 @@ import React from 'react'
export default async function DashboardPage() { export default async function DashboardPage() {
const events = await queries.fetchEvents() const events = await queries.fetchEvents()
console.log(events) // console.log(events)
return ( return (
<div> <div>
@@ -12,15 +12,17 @@ export default async function DashboardPage() {
<div className='border rounded-lg max-w-6xl mx-auto p-6 space-y-4'> <div className='border rounded-lg max-w-6xl mx-auto p-6 space-y-4'>
<h2 className='text-xl font-bold'>Your Events</h2> <h2 className='text-xl font-bold'>Your Events</h2>
{events.map((item) => ( {events.map((item) => (
<div key={item.id}> <Link key={item.id} href={`/events/${item.id}`} >
<p>ID: {item.id}</p> <div className='hover:cursor-pointer border rounded-xl p-2'>
<p>Name: {item.name}</p> <p>ID: {item.id}</p>
<p>Date: {item.date ? item.date.toISOString() : 'null'}</p> <p>Name: <strong>{item.name}</strong></p>
<p>Location: {item.location ? item.location : 'null'}</p> <p>Date: {item.date ? item.date.toISOString() : 'null'}</p>
<p>Creator ID:{item.creatorId}</p> <p>Location: {item.location ? item.location : 'null'}</p>
<p>Created At:{item.createdAt.toISOString()}</p> <p>Creator ID:{item.creatorId}</p>
<p>Created At:{item.createdAt.toISOString()}</p>
</div>
</div>
</Link>
))} ))}
<Link <Link
href={'/events'} href={'/events'}

View File

@@ -1,9 +1,15 @@
import { queries } from '@/lib/queries'
import React from 'react' import React from 'react'
export default function SingleEventPage() { export default async function SingleEventPage({ params }: { params: { eventId: string }}) {
console.log(params)
const data = await queries.singleEvent(params.eventId)
return ( return (
<div> <div>
SINGLE EVENT PAGE <h1 className='text-4xl font-bold'>
{data?.name}
</h1>
</div> </div>
) )
} }

View File

@@ -10,10 +10,32 @@ export default async function EventsPage() {
<div> <div>
Events Events
<div> <div>
{allEvents.length == 0 && ( {allEvents.length == 0 ? (
<> <>
You don't have any events yet. <Link href={'/events/create'} className='underline'>Create One!</Link> You don't have any events yet. <Link href={'/events/create'} className='underline'>Create One!</Link>
</> </>
) : (
<table className='table-auto w-full'>
<thead>
<tr>
<th>Event Name</th>
<th>Event Date</th>
<th>Created by</th>
</tr>
</thead>
<tbody>
{allEvents.map((item) => (
<tr
key={item.id}
className='text-center'
>
<td className=''><Link href={`/events/${item.id}`}>{item.name}</Link></td>
<td className=''>{item.date?.toDateString()}</td>
<td className=''>{item.creatorId}</td>
</tr>
))}
</tbody>
</table>
)} )}
</div> </div>
</div> </div>

View File

@@ -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 (
<>
<div className="max-w-2xl mx-auto py-10">
<h1 className="text-3xl font-bold mb-2">@{username}</h1>
<p className="text-gray-600">Email: {user.email}</p>
<p className="text-gray-600">Role: {user.role}</p>
<p className="text-gray-500 text-sm">Joined: {user.createdAt.toDateString()}</p>
</div>
<h2 className='text-xl font-bold'>Invite More People</h2>
<SendInviteForm />
</>
)
}

View File

@@ -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 <div className='text-center mt-10'>Invalid or expired invitation.</div>
}
redirect(`/signup?token=${invite.token}`)
}

View File

@@ -7,6 +7,7 @@ export default function SetupPage() {
const [role, setRole] = useState<'COUPLE' | 'PLANNER' | null>(null); const [role, setRole] = useState<'COUPLE' | 'PLANNER' | null>(null);
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [username, setUsername] = useState('');
async function handleSetup(e:React.FormEvent) { async function handleSetup(e:React.FormEvent) {
e.preventDefault(); 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' className='hover:cursor-pointer dark:bg-white dark:text-black rounded px-4 py-2'
onClick={() => setRole('COUPLE')} onClick={() => setRole('COUPLE')}
> >
I'm part of the Couple I&apos;m part of the Couple
</button> </button>
<button <button
className='hover:cursor-pointer dark:bg-white dark:text-black rounded px-4 py-2' className='hover:cursor-pointer dark:bg-white dark:text-black rounded px-4 py-2'
onClick={() => setRole('PLANNER')} onClick={() => setRole('PLANNER')}
> >
I'm the Planner I&apos;m the Planner
</button> </button>
</div> </div>
); );
@@ -45,6 +46,13 @@ export default function SetupPage() {
return ( return (
<form onSubmit={handleSetup} className="space-y-4"> <form onSubmit={handleSetup} className="space-y-4">
<h2>Create your account ({role})</h2> <h2>Create your account ({role})</h2>
<input
type="text"
placeholder="Choose a username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
<input <input
type="email" type="email"
placeholder="Email" placeholder="Email"

View File

@@ -0,0 +1,23 @@
import { verifyInvite } from '@/lib/invite'
import SignupForm from '@/components/SignupForm'
interface Props {
searchParams: {
token?: string
}
}
export default async function SignupPage({ searchParams }: Props) {
const invite = searchParams.token ? await verifyInvite(searchParams.token) : null
if (!invite || invite.accepted || new Date(invite.expiresAt) < new Date()) {
return <div className="text-center mt-10">Invalid or expired invitation.</div>
}
return (
<div className="max-w-md mx-auto mt-10">
<h1 className="text-2xl font-bold mb-4">Complete Your Signup</h1>
<SignupForm invite={invite} />
</div>
)
}

View File

@@ -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 })
}

View File

@@ -15,7 +15,6 @@ export const authOptions: NextAuthOptions = {
}, },
async authorize(credentials) { async authorize(credentials) {
if (!credentials?.email || !credentials?.password) { if (!credentials?.email || !credentials?.password) {
console.log('[AUTH] Missing credentials')
return null return null
} }
@@ -24,28 +23,24 @@ export const authOptions: NextAuthOptions = {
}) })
if (!user) { if (!user) {
console.log('[AUTH] User not found')
return null return null
} }
if (!user.password) { if (!user.password) {
console.log('[AUTH] User has no password set')
return null return null
} }
const isValid = await bcrypt.compare(credentials.password, user.password) const isValid = await bcrypt.compare(credentials.password, user.password)
if (!isValid) { if (!isValid) {
console.log('[AUTH] Invalid password')
return null return null
} }
console.log('[AUTH] Successful login', user.email)
return { return {
id: user.id, id: user.id,
email: user.email, email: user.email,
name: user.name, name: user.name,
role: user.role, role: user.role,
username: user.username!,
} }
}, },
}), }),
@@ -58,6 +53,7 @@ export const authOptions: NextAuthOptions = {
if (user) { if (user) {
token.id = user.id token.id = user.id
token.role = user.role token.role = user.role
token.username = user.username
} }
return token return token
}, },
@@ -65,6 +61,7 @@ export const authOptions: NextAuthOptions = {
if (session.user) { if (session.user) {
session.user.id = token.id as string session.user.id = token.id as string
session.user.role = token.role as "COUPLE" | "PLANNER" | "GUEST" session.user.role = token.role as "COUPLE" | "PLANNER" | "GUEST"
session.user.username = token.username as string
} }
return session return session
}, },

View File

@@ -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 })
}
}

View File

@@ -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,
})
}

View File

@@ -3,7 +3,14 @@ import { prisma } from '@/lib/prisma';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
export async function POST(req: NextRequest) { 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 }}); const existing = await prisma.user.findUnique({ where: { email }});
if (existing) return new NextResponse('User already exists', { status: 400 }); 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({ const user = await prisma.user.create({
data: { data: {
email, email,
username,
password: hashed, password: hashed,
role role
} }

View File

@@ -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 })
}
}

View File

@@ -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' })
}

BIN
bun.lockb

Binary file not shown.

View File

@@ -24,10 +24,12 @@ export default function Navbar() {
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<UserIcon /> <Link href={`/user/${session.user.username}`} className="flex items-center space-x-2">
<span className="text-sm text-gray-600"> <UserIcon />
{session.user.email} ({session.user.role}) <span className="text-sm text-gray-600">
</span> {session.user.email} ({session.user.role})
</span>
</Link>
<button <button
className="text-sm text-blue-600 underline" className="text-sm text-blue-600 underline"
onClick={() => signOut({ callbackUrl: '/login' })} onClick={() => signOut({ callbackUrl: '/login' })}

View File

@@ -0,0 +1,62 @@
'use client'
import { useState } from 'react'
export default function SendInviteForm() {
const [email, setEmail] = useState('')
const [role, setRole] = useState<'GUEST' | 'COUPLE' | 'PLANNER'>('GUEST')
const [message, setMessage] = useState('')
const [error, setError] = useState('')
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setError('')
setMessage('')
const res = await fetch('/api/invite/send', {
method: 'POST',
body: JSON.stringify({ email, role }),
headers: { 'Content-Type': 'application/json' },
})
if (res.ok) {
setMessage('Invite sent successfully!')
setEmail('')
} else {
const { message } = await res.json()
setError(message || 'Failed to send invite')
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4 max-w-md">
<h2 className="text-xl font-bold">Send an Invite</h2>
{message && <p className="text-green-600">{message}</p>}
{error && <p className="text-red-600">{error}</p>}
<input
type="email"
className="input input-bordered w-full"
placeholder="Recipient email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<select
className="select select-bordered w-full"
value={role}
onChange={(e) => setRole(e.target.value as 'COUPLE' | 'PLANNER' | 'GUEST')}
>
<option value="GUEST">Guest</option>
<option value="PLANNER">Planner</option>
<option value="COUPLE">Couple</option>
</select>
<button type="submit" className="btn btn-primary w-full">
Send Invite
</button>
</form>
)
}

68
components/SignupForm.tsx Normal file
View File

@@ -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 (
<form onSubmit={handleSubmit} className="space-y-4">
<p className="text-sm text-gray-600">
Invited as <strong>{invite.email}</strong> ({invite.role})
</p>
<input
type="text"
placeholder="Choose a username"
className="input input-bordered w-full"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
<input
type="password"
placeholder="Choose a password"
className="input input-bordered w-full"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
{error && <p className="text-red-500 text-sm">{error}</p>}
<button type="submit" className="btn btn-primary w-full">
Create Account
</button>
</form>
)
}

38
lib/email.ts Normal file
View File

@@ -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: `
<p>Hello!</p>
<p><strong>${inviterName}</strong> invited you to join their wedding planning space as a <strong>${role}</strong>.</p>
<p><a href="${inviteUrl}">Click here to accept your invite</a></p>
<p>This link will expire in 72 hours.</p>
`,
})
}

32
lib/invite.ts Normal file
View File

@@ -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 }
})
}

View File

@@ -1,4 +1,5 @@
import { prisma } from './prisma'; import { prisma } from './prisma';
import bcrypt from 'bcrypt'
export const mutations = { export const mutations = {
async createEvent(data: { async createEvent(data: {
@@ -30,5 +31,30 @@ export const mutations = {
email: data.email, 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
},
}; };

View File

@@ -5,5 +5,18 @@ export const queries = {
const allEvents = await prisma.event.findMany() const allEvents = await prisma.event.findMany()
console.log(allEvents) console.log(allEvents)
return 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
} }
} }

View File

@@ -12,9 +12,12 @@
}, },
"dependencies": { "dependencies": {
"@next-auth/prisma-adapter": "^1.0.7", "@next-auth/prisma-adapter": "^1.0.7",
"@types/nodemailer": "^6.4.17",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"crypto": "^1.0.1",
"next": "15.3.4", "next": "15.3.4",
"next-auth": "^4.24.11", "next-auth": "^4.24.11",
"nodemailer": "^7.0.3",
"prisma": "^6.10.1", "prisma": "^6.10.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0"

View File

@@ -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");

View File

@@ -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;

View File

@@ -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");

View File

@@ -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");

View File

@@ -12,6 +12,7 @@ model User {
email String @unique email String @unique
password String? // hashed password password String? // hashed password
name String? name String?
username String @unique
role Role @default(GUEST) role Role @default(GUEST)
events Event[] @relation("EventCreator") events Event[] @relation("EventCreator")
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -48,3 +49,14 @@ enum RsvpStatus {
NO NO
PENDING 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())
}

View File

@@ -6,16 +6,19 @@ declare module "next-auth" {
id: string id: string
email: string email: string
role: "COUPLE" | "PLANNER" | "GUEST" role: "COUPLE" | "PLANNER" | "GUEST"
username: string
} }
} }
interface User { interface User {
id: string id: string
role: "COUPLE" | "PLANNER" | "GUEST" role: "COUPLE" | "PLANNER" | "GUEST"
username: string
} }
interface JWT { interface JWT {
id: string id: string
role: "COUPLE" | "PLANNER" | "GUEST" role: "COUPLE" | "PLANNER" | "GUEST"
username: string
} }
} }