diff --git a/.env.example b/.env.example deleted file mode 100644 index 73eec69b..00000000 --- a/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -DATABASE_URL="YOUR_DATABASE_URL" - -# This env var must be prefixed with `VITE_` in order to work in the client / Vite React app. -VITE_PORT=4000 diff --git a/package-lock.json b/package-lock.json index 620aae66..e6e861c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,11 @@ "name": "auth-challenge", "version": "1.0.0", "dependencies": { - "@prisma/client": "^5.16.2", + "@prisma/client": "^5.18.0", "bcrypt": "^5.1.1", "concurrently": "^7.6.0", "cors": "^2.8.5", - "dotenv": "^16.4.0", + "dotenv": "^16.4.5", "express": "^4.18.2", "jsonwebtoken": "^9.0.2", "react": "^18.2.0", @@ -27,7 +27,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", "nodemon": "^3.0.2", - "prisma": "^5.16.2", + "prisma": "^5.18.0", "vite": "^5.0.8" } }, @@ -996,9 +996,9 @@ } }, "node_modules/@prisma/client": { - "version": "5.16.2", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.16.2.tgz", - "integrity": "sha512-+1lmkhR9gHWcTC5oghm2ZKpWljyWdzfazCVlLKUWXVmwHSf52g81aZ8qb6Km5Bs025yBi7puLp3qSLEvktoUtw==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.18.0.tgz", + "integrity": "sha512-BWivkLh+af1kqC89zCJYkHsRcyWsM8/JHpsDMM76DjP3ZdEquJhXa4IeX+HkWPnwJ5FanxEJFZZDTWiDs/Kvyw==", "hasInstallScript": true, "engines": { "node": ">=16.13" @@ -1013,48 +1013,48 @@ } }, "node_modules/@prisma/debug": { - "version": "5.16.2", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.16.2.tgz", - "integrity": "sha512-ItzB4nR4O8eLzuJiuP3WwUJfoIvewMHqpGCad+64gvThcKEVOtaUza9AEJo2DPqAOa/AWkFyK54oM4WwHeew+A==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.18.0.tgz", + "integrity": "sha512-f+ZvpTLidSo3LMJxQPVgAxdAjzv5OpzAo/eF8qZqbwvgi2F5cTOI9XCpdRzJYA0iGfajjwjOKKrVq64vkxEfUw==", "devOptional": true }, "node_modules/@prisma/engines": { - "version": "5.16.2", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.16.2.tgz", - "integrity": "sha512-qUxwMtrwoG3byd4PbX6T7EjHJ8AUhzTuwniOGkh/hIznBfcE2QQnGakyEq4VnwNuttMqvh/GgPFapHQ3lCuRHg==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.18.0.tgz", + "integrity": "sha512-ofmpGLeJ2q2P0wa/XaEgTnX/IsLnvSp/gZts0zjgLNdBhfuj2lowOOPmDcfKljLQUXMvAek3lw5T01kHmCG8rg==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/debug": "5.16.2", - "@prisma/engines-version": "5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303", - "@prisma/fetch-engine": "5.16.2", - "@prisma/get-platform": "5.16.2" + "@prisma/debug": "5.18.0", + "@prisma/engines-version": "5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169", + "@prisma/fetch-engine": "5.18.0", + "@prisma/get-platform": "5.18.0" } }, "node_modules/@prisma/engines-version": { - "version": "5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303.tgz", - "integrity": "sha512-HkT2WbfmFZ9WUPyuJHhkiADxazHg8Y4gByrTSVeb3OikP6tjQ7txtSUGu9OBOBH0C13dPKN2qqH12xKtHu/Hiw==", + "version": "5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169.tgz", + "integrity": "sha512-a/+LpJj8vYU3nmtkg+N3X51ddbt35yYrRe8wqHTJtYQt7l1f8kjIBcCs6sHJvodW/EK5XGvboOiwm47fmNrbgg==", "devOptional": true }, "node_modules/@prisma/fetch-engine": { - "version": "5.16.2", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.16.2.tgz", - "integrity": "sha512-sq51lfHKfH2jjYSjBtMjP+AznFqOJzXpqmq6B9auWrlTJrMgZ7lPyhWUW7VU7LsQU48/TJ+DZeIz8s9bMYvcHg==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.18.0.tgz", + "integrity": "sha512-I/3u0x2n31rGaAuBRx2YK4eB7R/1zCuayo2DGwSpGyrJWsZesrV7QVw7ND0/Suxeo/vLkJ5OwuBqHoCxvTHpOg==", "devOptional": true, "dependencies": { - "@prisma/debug": "5.16.2", - "@prisma/engines-version": "5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303", - "@prisma/get-platform": "5.16.2" + "@prisma/debug": "5.18.0", + "@prisma/engines-version": "5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169", + "@prisma/get-platform": "5.18.0" } }, "node_modules/@prisma/get-platform": { - "version": "5.16.2", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.16.2.tgz", - "integrity": "sha512-cXiHPgNLNyj22vLouPVNegklpRL/iX2jxTeap5GRO3DmCoVyIHmJAV1CgUMUJhHlcol9yYy7EHvsnXTDJ/PKEA==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.18.0.tgz", + "integrity": "sha512-Tk+m7+uhqcKDgnMnFN0lRiH7Ewea0OEsZZs9pqXa7i3+7svS3FSCqDBCaM9x5fmhhkufiG0BtunJVDka+46DlA==", "devOptional": true, "dependencies": { - "@prisma/debug": "5.16.2" + "@prisma/debug": "5.18.0" } }, "node_modules/@rollup/rollup-android-arm-eabi": { @@ -2180,14 +2180,14 @@ } }, "node_modules/dotenv": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.0.tgz", - "integrity": "sha512-WvImr5kpN5NGNn7KaDjJnLTh5rDVLZiDf/YLA8T1ZEZEBZNEDOE+mnkS0PVjPax8ZxBP5zC5SLMB3/9VV5de9g==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "url": "https://dotenvx.com" } }, "node_modules/ecdsa-sig-formatter": { @@ -4492,13 +4492,13 @@ } }, "node_modules/prisma": { - "version": "5.16.2", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.16.2.tgz", - "integrity": "sha512-rFV/xoBR2hBGGlu4LPLQd4U8WVA+tSAmYyFWGPRVfj+xg7N4kiZV4lSk38htSpF+/IuHKzlrbh4SFk8Z18cI8A==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.18.0.tgz", + "integrity": "sha512-+TrSIxZsh64OPOmaSgVPH7ALL9dfU0jceYaMJXsNrTkFHO7/3RANi5K2ZiPB1De9+KDxCWn7jvRq8y8pvk+o9g==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/engines": "5.16.2" + "@prisma/engines": "5.18.0" }, "bin": { "prisma": "build/index.js" diff --git a/package.json b/package.json index e0b7aaca..2c618089 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,11 @@ "preview": "vite preview" }, "dependencies": { - "@prisma/client": "^5.16.2", + "@prisma/client": "^5.18.0", "bcrypt": "^5.1.1", "concurrently": "^7.6.0", "cors": "^2.8.5", - "dotenv": "^16.4.0", + "dotenv": "^16.4.5", "express": "^4.18.2", "jsonwebtoken": "^9.0.2", "react": "^18.2.0", @@ -29,7 +29,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", "nodemon": "^3.0.2", - "prisma": "^5.16.2", + "prisma": "^5.18.0", "vite": "^5.0.8" } } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 68676dd3..3aaa3ae8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -7,8 +7,8 @@ generator client { datasource db { provider = "postgresql" - url = env("DATABASE_URL") - shadowDatabaseUrl = env("SHADOW_DATABASE_URL") + url = "postgresql://neondb_owner:0oLU3FieVmED@ep-patient-sun-a53sxtxp-pooler.us-east-2.aws.neon.tech/auth%20challange?sslmode=require" + shadowDatabaseUrl = env("postgresql://neondb_owner:0oLU3FieVmED@ep-sparkling-dream-a53gmr91-pooler.us-east-2.aws.neon.tech/neondb?sslmode=require") } model User { diff --git a/src/client/App.jsx b/src/client/App.jsx index bae3b635..83d0016b 100644 --- a/src/client/App.jsx +++ b/src/client/App.jsx @@ -1,73 +1,116 @@ -import { useEffect, useState } from 'react'; -import './App.css'; -import MovieForm from './components/MovieForm'; -import UserForm from './components/UserForm'; - -const port = import.meta.env.VITE_PORT; -const apiUrl = `http://localhost:${port}`; +import React, { useState, useEffect } from 'react'; function App() { const [movies, setMovies] = useState([]); + const fetchMovies = async () => { + const response = await fetch('http://localhost:4000/movie'); + const data = await response.json(); + setMovies(data.data); + }; + useEffect(() => { - fetch(`${apiUrl}/movie`) - .then(res => res.json()) - .then(res => setMovies(res.data)); + fetchMovies(); }, []); - /** - * HINTS! - * 1. This handle___ functions below use async/await to handle promises, but the - * useEffect above is using .then to handle them. Both are valid approaches, but - * we should ideally use one or the other. Pick whichever you prefer. - * - * 2. The default method for the `fetch` API is to make a GET request. To make other - * types of requests, we must provide an object as the second argument of `fetch`. - * The values that you must provide are: - * - method - * - headers - * - body (if needed) - * For the "headers" property, you must state the content type of the body, i.e.: - * headers: { - * 'Content-Type': 'application/json' - * } - * */ - - const handleRegister = async ({ username, password }) => { + const handleRegister = async (event) => { + event.preventDefault(); + const username = event.target.elements.username.value; + const password = event.target.elements.password.value; + const response = await fetch('http://localhost:4000/user/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }), + }); + + const data = await response.json(); + console.log('Registered user:', data); }; - const handleLogin = async ({ username, password }) => { + const handleLogin = async (event) => { + event.preventDefault(); + const username = event.target.elements.username.value; + const password = event.target.elements.password.value; + + const response = await fetch('http://localhost:4000/user/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }), + }); + const data = await response.json(); + localStorage.setItem('token', data.token); + console.log('Logged in user:', data); }; - const handleCreateMovie = async ({ title, description, runtimeMins }) => { + const handleCreateMovie = async (event) => { + event.preventDefault(); + const title = event.target.elements.title.value; + const description = event.target.elements.description.value; + const runtimeMins = parseInt(event.target.elements.runtimeMins.value, 10); - } + const response = await fetch('http://localhost:4000/movie', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('token')}`, + }, + body: JSON.stringify({ title, description, runtimeMins }), + }); + + const data = await response.json(); + console.log('Created movie:', data); + fetchMovies(); + }; return ( -
-

Register

- - -

Login

- - -

Create a movie

- - -

Movie list

-
    - {movies.map(movie => { - return ( -
  • -

    {movie.title}

    -

    Description: {movie.description}

    -

    Runtime: {movie.runtimeMins}

    -
  • - ); - })} -
+
+
+

Register

+
+ + + + + +
+
+ +
+

Login

+
+ + + + + +
+
+ +
+

Create a Movie

+
+ + + + + + + +
+
+ +
+

Movie List

+ {movies.map((movie, index) => ( +
+

{movie.title}

+

Description: {movie.description}

+

Runtime: {movie.runtimeMins}

+
+ ))} +
); } diff --git a/src/server/controllers/movie.js b/src/server/controllers/movie.js index d4733b61..a24dc1c4 100644 --- a/src/server/controllers/movie.js +++ b/src/server/controllers/movie.js @@ -1,31 +1,32 @@ import jwt from 'jsonwebtoken'; -import { PrismaClient } from '@prisma/client' -const prisma = new PrismaClient(); +import { PrismaClient } from '@prisma/client'; +const prisma = new PrismaClient(); const jwtSecret = 'mysecret'; -const getAllMovies = async (req, res) => { +export const getAllMovies = async (req, res) => { const movies = await prisma.movie.findMany(); - res.json({ data: movies }); }; -const createMovie = async (req, res) => { +export const createMovie = async (req, res) => { const { title, description, runtimeMins } = req.body; + const token = req.headers.authorization?.split(' ')[1]; try { - const token = null; - // todo verify the token + const decoded = jwt.verify(token, jwtSecret); + + const createdMovie = await prisma.movie.create({ + data: { + title, + description, + runtimeMins, + userId: decoded.userId, // Link the movie to the authenticated user + }, + }); + + res.json({ data: createdMovie }); } catch (e) { - return res.status(401).json({ error: 'Invalid token provided.' }) + return res.status(401).json({ error: 'Invalid token provided.' }); } - - const createdMovie = null; - - res.json({ data: createdMovie }); -}; - -export { - getAllMovies, - createMovie }; diff --git a/src/server/controllers/user.js b/src/server/controllers/user.js index 05db4183..94b9ad4c 100644 --- a/src/server/controllers/user.js +++ b/src/server/controllers/user.js @@ -1,39 +1,34 @@ import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; -import { PrismaClient } from '@prisma/client' -const prisma = new PrismaClient(); +import { PrismaClient } from '@prisma/client'; +const prisma = new PrismaClient(); const jwtSecret = 'mysecret'; -const register = async (req, res) => { +export const register = async (req, res) => { const { username, password } = req.body; - const createdUser = null; + const existingUser = await prisma.user.findUnique({ where: { username } }); + if (existingUser) { + return res.status(400).json({ error: 'User already exists' }); + } + + const hashedPassword = await bcrypt.hash(password, 10); + const createdUser = await prisma.user.create({ + data: { username, password: hashedPassword }, + }); res.json({ data: createdUser }); }; -const login = async (req, res) => { +export const login = async (req, res) => { const { username, password } = req.body; + const foundUser = await prisma.user.findUnique({ where: { username } }); - const foundUser = null; - - if (!foundUser) { - return res.status(401).json({ error: 'Invalid username or password.' }); - } - - const passwordsMatch = false; - - if (!passwordsMatch) { + if (!foundUser || !await bcrypt.compare(password, foundUser.password)) { return res.status(401).json({ error: 'Invalid username or password.' }); } - const token = null; - - res.json({ data: token }); -}; - -export { - register, - login + const token = jwt.sign({ userId: foundUser.id }, jwtSecret, { expiresIn: '1h' }); + res.json({ token }); };