Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 0 additions & 4 deletions .env.example

This file was deleted.

11 changes: 11 additions & 0 deletions prisma/migrations/20241225132850_add_userid_to_movie/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
Warnings:

- Added the required column `userId` to the `Movie` table without a default value. This is not possible if the table is not empty.

*/
-- AlterTable
ALTER TABLE "Movie" ADD COLUMN "userId" INTEGER NOT NULL;

-- AddForeignKey
ALTER TABLE "Movie" ADD CONSTRAINT "Movie_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
4 changes: 2 additions & 2 deletions prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
3 changes: 3 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ model User {
id Int @id @default(autoincrement())
username String @unique
password String
movies Movie[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Expand All @@ -24,6 +25,8 @@ model Movie {
title String @unique
description String
runtimeMins Int
userId Int
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
104 changes: 95 additions & 9 deletions src/client/App.jsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,39 @@
import { useEffect, useState } from 'react';
import './App.css';
import MovieForm from './components/MovieForm';
import UserForm from './components/UserForm';
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}`;

function App() {
const [movies, setMovies] = useState([]);
const [errorMessage, setErrorMessage] = useState("");
const [successMessage, setSuccessMessage] = useState("");

useEffect(() => {
fetch(`${apiUrl}/movie`)
.then(res => res.json())
.then(res => setMovies(res.data));
const fetchMovies = async () => {
try {
const response = await fetch(`${apiUrl}/movie`);
const data = await response.json();

if (!response.ok) {
throw new Error(data.message || "Failed to fetch movies");
}
setMovies(data.data);
} catch (error) {
console.error("Error fetching movies:", error.message);
setErrorMessage("Failed to fetch movies.");
}
};
fetchMovies();
}, []);

const clearMessages = () => {
setErrorMessage("");
setSuccessMessage("");
};

/**
* HINTS!
* 1. This handle___ functions below use async/await to handle promises, but the
Expand All @@ -34,16 +53,80 @@ function App() {
* */

const handleRegister = async ({ username, password }) => {
clearMessages();
try {
const response = await fetch(`${apiUrl}/user/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});

const data = await response.json();

if (!response.ok) {
throw new Error(data.error || "Failed to register");
}
setSuccessMessage("User registered successfully!");
} catch (error) {
setErrorMessage("Registration failed: " + error.message);
}
};

const handleLogin = async ({ username, password }) => {
clearMessages();
try {
const response = await fetch(`${apiUrl}/user/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});

const data = await response.json();

if (!response.ok) {
throw new Error(data.error || "Failed to login");
}
localStorage.setItem("token", data.token)
setSuccessMessage("User logged in successfully !")
} catch (error) {
setErrorMessage("Login failed: " + error.message)
}
};

const handleCreateMovie = async ({ title, description, runtimeMins }) => {
clearMessages();

const token = localStorage.getItem("token");

if(!token){
setErrorMessage("You must logged in to create a movie")
return;
}

}
try {
const response = await fetch(`${apiUrl}/movie`, {
method: "POST",
headers: { "Content-Type": "application/json",
Authorization: `Bearer ${token}`
},
body: JSON.stringify({ title, description, runtimeMins }),
});

const data = await response.json();

if (!response.ok) {
throw new Error(data.error || "Failed to create movie");
}

// Update the movie list
setMovies((prevMovies) => [...prevMovies, data.data]);
setSuccessMessage("Movie created successfully!");

} catch (error) {
// console.error("Create movie error:", error.message);
setErrorMessage("Failed to create movie: " + error.message)
}
};

return (
<div className="App">
Expand All @@ -56,9 +139,12 @@ function App() {
<h1>Create a movie</h1>
<MovieForm handleSubmit={handleCreateMovie} />

{successMessage && <p className="success">{successMessage}</p>}
{errorMessage && <p className="error">{errorMessage}</p>}

<h1>Movie list</h1>
<ul>
{movies.map(movie => {
{movies.map((movie) => {
return (
<li key={movie.id}>
<h3>{movie.title}</h3>
Expand Down
58 changes: 41 additions & 17 deletions src/server/controllers/movie.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,55 @@
import jwt from 'jsonwebtoken';
import { PrismaClient } from '@prisma/client'
import jwt from "jsonwebtoken";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();

const jwtSecret = 'mysecret';
const jwtSecret = "mysecret";

const getAllMovies = async (req, res) => {
try {
const movies = await prisma.movie.findMany();

res.json({ data: movies });
} catch (error) {
console.error("Error fetching movies:", error.message);
res.status(500).json({ error: "Internal server error." });
}
};

const createMovie = async (req, res) => {
const { title, description, runtimeMins } = req.body;
const { title, description, runtimeMins } = req.body;

try {
const token = null;
// todo verify the token
} catch (e) {
return res.status(401).json({ error: 'Invalid token provided.' })
}
try {
// todo verify the token
const authHeader = req.headers.authorization;

const createdMovie = null;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res
.status(401)
.json({ error: "Authorization token missing or invalid." });
}
const token = authHeader.split(" ")[1];

const decoded = jwt.verify(token, jwtSecret);
console.log("Token decoded:", decoded);

// Create the movie
const createdMovie = await prisma.movie.create({
data: {
title,
description,
runtimeMins,
userId: decoded.userId,
},
});
res.status(201).json({ data: createdMovie });
} catch (error) {
console.log("error creating movie", error.message);

if (error.name === "JsonWebTokenError") {
return res.status(401).json({ error: "Invalid token provided." });
}

res.json({ data: createdMovie });
return res.status(501).json({ error: "Internal error." });
}
};

export {
getAllMovies,
createMovie
};
export { getAllMovies, createMovie };
71 changes: 53 additions & 18 deletions src/server/controllers/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,66 @@ const prisma = new PrismaClient();
const jwtSecret = 'mysecret';

const register = async (req, res) => {
const { username, password } = req.body;
try {

const createdUser = null;
const { username, password } = req.body;

res.json({ data: createdUser });
};

const login = async (req, res) => {
const { username, password } = req.body;
// check if username alreaady exists
const existingUser = await prisma.user.findUnique({
where: { username}
})
if(existingUser) {
return res.status(400).json({error: 'username already taken'})
}
// const createdUser = null;

// hash the password
const hashedPassword = await bcrypt.hash(password, 10);
console.log("hashed password", hashedPassword)

const foundUser = null;

if (!foundUser) {
return res.status(401).json({ error: 'Invalid username or password.' });
const createdUser = await prisma.user.create({
data: {
username: username,
password: hashedPassword
}
})
res.status(201).json({ message: ` ${createdUser.username} was successfully created !`
});
} catch (error){
console.error('Registration error:', error.message);
res.status(500).json({ error: 'Internal server error!'})
}
};

const passwordsMatch = false;

if (!passwordsMatch) {
return res.status(401).json({ error: 'Invalid username or password.' });
const login = async (req, res) => {
try {

const { username, password } = req.body;

const foundUser = await prisma.user.findUnique({
where: { username}
})

if (!foundUser) {
return res.status(401).json({ error: 'Invalid username or password.' });
}

const passwordsMatch = await bcrypt.compare(password, foundUser.password)

if (!passwordsMatch) {
return res.status(401).json({ error: 'Invalid username or password.' });
}

const token = jwt.sign({
userId: foundUser.id, username: foundUser.username
}, jwtSecret, { expiresIn: '1h' })

res.json({ message: 'Login successfully!', token });
} catch (error) {
console.error('Login error:', error.message);
res.status(500).json({ error: 'Internal server error!'})
}

const token = null;

res.json({ data: token });
};

export {
Expand Down