- {profile.email} + {profile.name}
- {/* Author Information */} -- by {profile.name} -
- {/* Content Section */}{profile.email}
- ) : ( -No content available for this post.
- )} + +Gender: {profile.gender}
+{profile.description}
diff --git a/README.md b/README.md index e5b62b5d..b917f6e6 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,15 @@ To contribute, please submit a pull request or issue, or fill out this [form](ht - [ ] Search through all the profile variables - [ ] (Set up chat / direct messaging) +#### Secondary To Do + +Any action item is open to anyone for collaboration, but the following ones are particularly easy to do for first-time contributors. + +- [ ] Clean up terms and conditions +- [ ] Clean up privacy notice +- [ ] Clean up learn more page +- [ ] Add dark theme + ## Implementation The web app is coded in Typescript using React as front-end and prisma as back-end. It includes: diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index 37d7347a..47105110 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -1,89 +1,5 @@ -import NextAuth, {type NextAuthOptions} from "next-auth"; -import GoogleProvider from "next-auth/providers/google"; -import CredentialsProvider from "next-auth/providers/credentials"; -import {PrismaAdapter} from "@auth/prisma-adapter"; -import {prisma} from "@/lib/prisma"; -import bcrypt from "bcryptjs"; - -export const authOptions: NextAuthOptions = { - adapter: PrismaAdapter(prisma), - session: { - strategy: "jwt", - }, - providers: [ - GoogleProvider({ - clientId: process.env.GOOGLE_CLIENT_ID!, - clientSecret: process.env.GOOGLE_CLIENT_SECRET!, - }), - CredentialsProvider({ - name: "credentials", - credentials: { - email: {label: "Email", type: "email"}, - password: {label: "Password", type: "password"}, - }, - async authorize(credentials) { - if (!credentials?.email || !credentials?.password) { - throw new Error("Email and password are required"); - } - - const user = await prisma.user.findUnique({ - where: {email: credentials.email}, - }); - - if (!user || !user.password) { - throw new Error("Invalid email or password"); - } - - const isCorrectPassword = await bcrypt.compare( - credentials.password, - user.password - ); - - if (!isCorrectPassword) { - throw new Error("Invalid email or password"); - } - - return { - id: user.id, - email: user.email, - name: user.name, - image: user.image, - }; - }, - }), - ], - pages: { - signIn: "/login", - error: "/login", - }, - callbacks: { - async jwt({token, user}) { - if (user) { - token.id = user.id; - token.email = user.email; - token.name = user.name; - token.picture = user.image; - } - return token; - }, - async session({session, token}) { - if (token && session.user) { - session.user.id = token.id as string; - session.user.name = token.name as string; - session.user.email = token.email as string; - session.user.image = token.picture as string; - } - return session; - }, - async redirect({url, baseUrl}) { - if (url.startsWith("/")) return `${baseUrl}${url}`; - else if (new URL(url).origin === baseUrl) return url; - return baseUrl; - }, - }, - secret: process.env.NEXTAUTH_SECRET, - debug: process.env.NODE_ENV === "development", -} satisfies NextAuthOptions; +import NextAuth from "next-auth"; +import {authOptions} from "@/lib/auth"; const authHandler = NextAuth(authOptions); export {authHandler as GET, authHandler as POST}; diff --git a/app/api/user/update-profile/route.ts b/app/api/user/update-profile/route.ts new file mode 100644 index 00000000..c695f4ea --- /dev/null +++ b/app/api/user/update-profile/route.ts @@ -0,0 +1,50 @@ +import {NextResponse} from "next/server"; +import {prisma} from "@/lib/prisma"; +import {getSession} from "@/lib/auth"; + +export async function POST(req: Request) { + try { + const session = await getSession(); + + if (!session?.user?.email) { + return NextResponse.json( + {error: "Not authenticated"}, + {status: 401} + ); + } + + const {description, gender} = await req.json(); + + // Validate required fields + if (!gender) { + return NextResponse.json( + {error: "Gender is required"}, + {status: 400} + ); + } + + // Update user with the new profile information + const updatedUser = await prisma.user.update({ + where: {email: session.user.email}, + data: { + description: description || null, + gender: gender || null, + }, + select: { + id: true, + email: true, + name: true, + description: true, + gender: true, + }, + }); + + return NextResponse.json(updatedUser); + } catch (error) { + console.error('Profile update error:', error); + return NextResponse.json( + {error: "Failed to update profile"}, + {status: 500} + ); + } +} diff --git a/app/complete-profile/page.tsx b/app/complete-profile/page.tsx new file mode 100644 index 00000000..ca9f23e4 --- /dev/null +++ b/app/complete-profile/page.tsx @@ -0,0 +1,151 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { useSession } from 'next-auth/react'; + +export default function CompleteProfile() { + const [description, setDescription] = useState(''); + const [gender, setGender] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(''); + const router = useRouter(); + const { data: session, update } = useSession(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!gender) { + setError('Please select your gender'); + return; + } + + try { + setIsSubmitting(true); + setError(''); + + const response = await fetch('/api/user/update-profile', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + description, + gender, + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Failed to update profile'); + } + + // Update the session to reflect the changes + await update(); + + // Redirect to the home page or dashboard + router.push('/'); + } catch (error) { + console.error('Profile update error:', error); + setError(error instanceof Error ? error.message : 'Failed to update profile'); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
*/} + {/* Help us know you better (this information can be updated later)*/} + {/*
*/} +{error}
+- by {profile.name} -
- {/* Content Section */}{profile.email}
- ) : ( -No content available for this post.
- )} + +Gender: {profile.gender}
+{profile.description}
by {profile.name}
+{new Date(profile.createdAt).toLocaleDateString("en-US", { year: "numeric", diff --git a/app/register/page.tsx b/app/register/page.tsx index 859c60ba..1bb573ee 100644 --- a/app/register/page.tsx +++ b/app/register/page.tsx @@ -58,13 +58,15 @@ export default function RegisterPage() { const response = await signIn("credentials", { email, password, - redirect: true, - callbackUrl: "/", + redirect: false, }); if (response?.error) { throw new Error("Failed to sign in after registration"); } + + // Redirect to complete profile page + window.location.href = '/complete-profile'; } catch (error) { setError(error instanceof Error ? error.message : "Registration failed"); setIsLoading(false); diff --git a/lib/auth.ts b/lib/auth.ts new file mode 100644 index 00000000..07171d09 --- /dev/null +++ b/lib/auth.ts @@ -0,0 +1,89 @@ +import type {NextAuthOptions} from "next-auth"; +import {getServerSession} from "next-auth"; +import {PrismaAdapter} from "@auth/prisma-adapter"; +import {prisma} from "@/lib/prisma"; +import GoogleProvider from "next-auth/providers/google"; +import CredentialsProvider from "next-auth/providers/credentials"; +import bcrypt from "bcryptjs"; + +export const authOptions: NextAuthOptions = { + adapter: PrismaAdapter(prisma), + session: { + strategy: "jwt", + }, + providers: [ + GoogleProvider({ + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }), + CredentialsProvider({ + name: "credentials", + credentials: { + email: {label: "Email", type: "email"}, + password: {label: "Password", type: "password"}, + }, + async authorize(credentials) { + if (!credentials?.email || !credentials?.password) { + throw new Error("Email and password are required"); + } + + const user = await prisma.user.findUnique({ + where: {email: credentials.email}, + }); + + if (!user || !user.password) { + throw new Error("Invalid email or password"); + } + + const isCorrectPassword = await bcrypt.compare( + credentials.password, + user.password + ); + + if (!isCorrectPassword) { + throw new Error("Invalid email or password"); + } + + return { + id: user.id, + email: user.email, + name: user.name, + image: user.image, + }; + }, + }), + ], + pages: { + signIn: "/login", + error: "/login", + }, + callbacks: { + async jwt({token, user}) { + if (user) { + token.id = user.id; + token.email = user.email; + token.name = user.name; + token.picture = user.image; + } + return token; + }, + async session({session, token}) { + if (token && session.user) { + session.user.id = token.id as string; + session.user.name = token.name as string; + session.user.email = token.email as string; + session.user.image = token.picture as string; + } + return session; + }, + async redirect({url, baseUrl}) { + if (url.startsWith("/")) return `${baseUrl}${url}`; + else if (new URL(url).origin === baseUrl) return url; + return baseUrl; + }, + }, + secret: process.env.NEXTAUTH_SECRET, + debug: process.env.NODE_ENV === "development", +} satisfies NextAuthOptions; + +export const getSession = () => getServerSession(authOptions); \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0ce0374c..4e7c1065 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -18,6 +18,11 @@ model User { image String? accounts Account[] sessions Session[] + + // Profile Information + gender String? + description String? + // Optional for WebAuthn support Authenticator Authenticator[] @@ -80,4 +85,4 @@ model Authenticator { user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@id([userId, credentialID]) -} \ No newline at end of file +} diff --git a/prisma/seed.ts b/prisma/seed.ts index 2b8951af..70dfc0eb 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -10,6 +10,7 @@ async function main() { email: 'alice@example.com', name: 'Alice', password: await bcrypt.hash('password123', 10), + description: 'Alice in Wonderland' }, }), ]);