-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: set up database and auth #178
Changes from 4 commits
16aa49c
30f0857
187c333
fcc512c
977da61
2c002f9
1101d24
e980b7a
17886a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { NextAuthOptions } from "next-auth"; | ||
import CredentialsProvider from "next-auth/providers/credentials"; | ||
import { PrismaAdapter } from "@next-auth/prisma-adapter"; | ||
import db from "../../../database/db"; | ||
import { compare } from "bcrypt"; | ||
|
||
export const authOptions: NextAuthOptions = { | ||
adapter: PrismaAdapter(db), | ||
secret: process.env.NEXTAUTH_SECRET, | ||
session: { | ||
strategy: 'jwt' //change to session in db? | ||
}, | ||
pages: { | ||
signIn: '/signin' | ||
/* | ||
signOut: '/auth/signout', | ||
error: '/auth/error', // Error code passed in query string as ?error= | ||
verifyRequest: '/auth/verify-request', // (used for check email message) | ||
newUser: '/auth/new-user' | ||
*/ | ||
//TODO: add signOut, error pages | ||
}, | ||
providers: [ | ||
//TODO: add email provider setup | ||
CredentialsProvider({ | ||
name: "Credentials", | ||
credentials: { | ||
email: { label: "Email", type: "email", placeholder: "[email protected]" }, | ||
password: { label: "Password", type: "password" } | ||
}, | ||
async authorize(credentials) { | ||
if(!credentials?.email || !credentials?.password) { | ||
return null; | ||
} | ||
|
||
const existingUser = await db.user.findUnique({ | ||
where: { email : credentials?.email } | ||
}); | ||
|
||
if(!existingUser) { | ||
return null; | ||
} | ||
|
||
const passwordMatch = await compare(credentials.password, existingUser.password); | ||
|
||
if(!passwordMatch) { | ||
return null; | ||
} | ||
|
||
return { | ||
id: `${existingUser.user_id}`, | ||
username: existingUser.first_name + "_" + existingUser.last_name, //do we really need this here? | ||
email: existingUser.email | ||
} | ||
} | ||
}) | ||
], | ||
callbacks: { | ||
//TODO: add more callbacks, setup proper session management | ||
async jwt({ token, user }) { | ||
console.log(token, user); | ||
|
||
if(user) { | ||
return { | ||
...token, | ||
username: user.username | ||
} | ||
} | ||
return token | ||
}, | ||
async session({ session, token }) { | ||
return { | ||
...session, | ||
user : { | ||
...session.user, | ||
username: token.username | ||
} | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import NextAuth from "next-auth" | ||
import { authOptions } from "../auth"; | ||
|
||
const handler = NextAuth(authOptions); | ||
|
||
export { handler as GET, handler as POST } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
digraph G { | ||
softdevtumai marked this conversation as resolved.
Show resolved
Hide resolved
|
||
node [shape=record]; | ||
|
||
department [label="{Department|+ department_id: int (PK)\l| name: varchar\l mission: text\l department_type: varchar\l creation_date: date\l created_at: timestamp\l updated_at: timestamp\l}"]; | ||
department_membership [label="{Department Membership|+ department_membership_id: int (PK)\l| user_id: int (FK) (@onDelete=SetNull)\l department_id: int (FK) (@onDelete=SetNull)\l membership_start: date\l membership_end: date\l position: enum\l created_at: timestamp\l updated_at: timestamp\l}"]; | ||
user [label="{User|+ user_id: int (PK)\l| email: varchar\l password: varchar\l first_name: varchar\l last_name: varchar\l permission: enum\l created_at: timestamp\l updated_at: timestamp\l}"]; | ||
profile [label="{Profile|+ profile_id: int (PK)\l| user_id: int (FK)\l phone: varchar\l birthday: date\l nationality: varchar\l description: text\l activity_status: varchar\l degree_level: varchar\l degree_name: varchar\l degree_semester: int\l degree_last_update: timestamp\l university: varchar\l full_name: varchar\l profile_picture: text\l created_at: timestamp\l updated_at: timestamp\l}"]; | ||
contact [label="{Contact|+ contact_id: int (PK)\l| profile_id: int (FK)\l contact_type: enum\l contact_username: varchar\l created_at: timestamp\l updated_at: timestamp\l}"]; | ||
|
||
opportunity [label="{Opportunity|+ opportunity_id: int (PK)\l| title: varchar\l description: text\l department_id: int (FK)\l opportunity_start: date\l opportunity_end: date\l created_at: timestamp\l created_by: int (FK) (@onDelete=SetNull)\l commands: json\l questions: json\l}"]; | ||
opportunity_participation [label="{Opportunity Participation|+ user_id: int (PK, FK) (@onDelete=Cascade)\l+ opportunity_id: int (PK, FK) (@onDelete=Cascade)\l| permission: enum\l created_at: timestamp\l}"]; | ||
|
||
review [label="{Review|+ review_id: int (PK)\l| opportunity_id: int (FK) (@onDelete=Cascade)\l assignee_id: int (FK, nullable)\l review_text: text\l created_at: timestamp\l content: json\l}"]; | ||
|
||
user_permission_enum [label="{User Permission| admin\l member\l}"]; | ||
opportunity_permission_enum [label="{Opportunity Permission| owner\l admin\l member\l guest\l}"]; | ||
department_position_enum [label="{Department Position| president\l head_of_department\l board_member\l advisor\l taskforce_lead\l project_lead\l active_member\l alumni\l}"]; | ||
contact_type_enum [label="{Contact Type| email\l slack\l github\l facebook\l instagram\l phone\l}"]; | ||
|
||
user -> user_permission_enum [label="permission"]; | ||
profile -> user [label="user_id"]; | ||
contact -> profile [label="profile_id"]; | ||
contact -> contact_type_enum [label="contact_type"]; | ||
opportunity -> user [label="created_by"]; | ||
opportunity -> department [label="department_id"]; | ||
opportunity_participation -> opportunity [label="opportunity_id"]; | ||
opportunity_participation -> user [label="user_id"]; | ||
opportunity_participation -> opportunity_permission_enum [label="permission"]; | ||
review -> opportunity [label="opportunity_id"]; | ||
review -> user [label="assignee_id"]; | ||
department_membership -> department [label="department_id"]; | ||
department_membership -> user [label="user_id"]; | ||
department_membership -> department_position_enum [label="position"]; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import db from "../../../database/db"; | ||
import { NextResponse } from "next/server"; | ||
import { hash } from 'bcrypt'; | ||
import * as z from 'zod'; | ||
|
||
const userSchema = z | ||
.object({ | ||
email: z.string().min(1, 'Email is required').email('Invalid email'), | ||
password: z | ||
.string() | ||
.min(1, 'Password is required') | ||
.min(8, 'Password must have than 8 characters') | ||
}) | ||
|
||
export async function POST(req: Request) { | ||
try { | ||
const body = await req.json(); | ||
const { email, password } = userSchema.parse(body); | ||
|
||
//check email format | ||
const existingEmail = await db.user.findUnique({ | ||
where: { email: email } | ||
}); | ||
if(existingEmail) { | ||
return NextResponse.json({ user: null, message: "User with this email already exists!"}, | ||
{ status : 409}) | ||
} | ||
|
||
const hashedPass = await hash(password, 10); | ||
const newUser = await db.user.create({ | ||
data: { | ||
email, | ||
password : hashedPass | ||
} | ||
}); | ||
const { password: newUserPassword, ...rest} = newUser; | ||
|
||
|
||
return NextResponse.json({ user: rest, message: "Great success!"}, { status : 201 }); | ||
} catch(error) { | ||
return NextResponse.json({message: "Error!"}, { status : 500}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think just throwing an error should lead to a response with error code 500. Additionaly you get the error wrapped in the response so you can see the error code in the client - The only thing you have to be careful about is that you don't expose sensitive information |
||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -9,6 +9,7 @@ import { useRouter } from "next/navigation"; | |||||
import toast from "react-hot-toast"; | ||||||
import { Field, Form, Formik } from "formik"; | ||||||
import ErrorMessage from "@components/ErrorMessage"; | ||||||
import { signIn } from 'next-auth/react'; | ||||||
|
||||||
export const LoginForm = ({ setResetPassword }) => { | ||||||
const router = useRouter(); | ||||||
|
@@ -23,16 +24,17 @@ export const LoginForm = ({ setResetPassword }) => { | |||||
email: "", | ||||||
password: "", | ||||||
}} | ||||||
onSubmit={async (values) => { | ||||||
await toast.promise( | ||||||
signInWithEmailAndPassword(auth, values.email, values.password), | ||||||
{ | ||||||
loading: "Signing in", | ||||||
success: "Welcome!", | ||||||
error: "Failed to sign in", | ||||||
}, | ||||||
); | ||||||
return router.push("/"); | ||||||
onSubmit = {async (values) => { | ||||||
const signInData = await signIn('credentials', { | ||||||
email: values.email, | ||||||
password: values.password, | ||||||
redirect: false | ||||||
}); | ||||||
if(signInData?.error) { | ||||||
console.log(signInData.error); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} else { | ||||||
return router.push("/"); | ||||||
} | ||||||
}} | ||||||
> | ||||||
{({ errors, touched }) => ( | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { PrismaClient } from '@prisma/client' | ||
|
||
const prismaClientSingleton = () => { | ||
return new PrismaClient() | ||
} | ||
|
||
type PrismaClientSingleton = ReturnType<typeof prismaClientSingleton> | ||
|
||
const globalForPrisma = globalThis as unknown as { | ||
prisma: PrismaClientSingleton | undefined | ||
} | ||
|
||
const prisma = globalForPrisma.prisma ?? prismaClientSingleton() | ||
|
||
const db = prisma | ||
|
||
export default db | ||
|
||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add this var in
/app/env.mjs
, so it is checked at build time?https://github.com/t3-oss/t3-env
You can then use it with typesafety by using
import { env } from "env.mjs";