Skip to content
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

Merged
merged 9 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions app/app/api/auth/[...nextauth].ts
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,
Copy link
Collaborator

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

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
}
}
}
}
}
6 changes: 6 additions & 0 deletions app/app/api/auth/[...nextauth]/route.ts
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 }
34 changes: 34 additions & 0 deletions app/app/api/schema.dot
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"];
}
43 changes: 43 additions & 0 deletions app/app/api/user/route.ts
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});
Copy link
Collaborator

Choose a reason for hiding this comment

The 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

}
}
22 changes: 12 additions & 10 deletions app/app/auth/components/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
console.log(signInData.error);
console.error(signInData.error);

} else {
return router.push("/");
}
}}
>
{({ errors, touched }) => (
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion app/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { createContext } from "react";
import { Toaster } from "react-hot-toast";
import { useStores } from "../providers/StoreProvider";
import { ThemeProvider } from "@components/theme-provider";
import { env } from "env.mjs";
import { env } from "app/env.mjs";

const StoresContext = createContext(null);
const queryClient = new QueryClient();
Expand Down
2 changes: 1 addition & 1 deletion app/config/firebase.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { env } from "env.mjs";
import { env } from "app/env.mjs";
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";

Expand Down
19 changes: 19 additions & 0 deletions app/database/db.ts
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
7 changes: 7 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
"format": "prettier --write ."
},
"dependencies": {
"@auth/prisma-adapter": "^1.0.6",
"@heroicons/react": "^2.0.18",
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^5.6.0",
"@radix-ui/colors": "^2.1.0",
"@radix-ui/react-avatar": "^1.0.3",
"@radix-ui/react-checkbox": "^1.0.4",
Expand All @@ -29,6 +32,7 @@
"@types/node": "^20.8.2",
"@types/react-dom": "18.2.7",
"axios": "^1.4.0",
"bcrypt": "^5.1.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"downloadjs": "^1.4.7",
Expand All @@ -41,6 +45,7 @@
"mobx-react": "^7.6.0",
"mobx-react-lite": "^4.0.3",
"next": "13.4.19",
"next-auth": "^4.24.5",
"next-themes": "^0.2.1",
"react": "18.2.0",
"react-dom": "18.2.0",
Expand All @@ -54,13 +59,15 @@
"zod": "^3.22.4"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/downloadjs": "^1.4.3",
"@types/react": "^18.2.25",
"autoprefixer": "10.4.15",
"env-cmd": "^10.1.0",
"postcss": "8.4.28",
"prettier": "^3.0.2",
"prettier-plugin-tailwindcss": "^0.5.3",
"prisma": "^5.6.0",
"tailwindcss": "3.3.3",
"webpack": "^5.88.2"
}
Expand Down
Loading
Loading