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/README.md b/README.md index ac40573d..b74262f3 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,119 @@ # Authentication Challenge -## Learning Objectives +This project demonstrates a backend application where users can register, log in, and add movies to a database. It showcases my backend development skills. -- Use a token-based approach to authorise access to API resources -- Use a hashing library to encrypt sensitive information -- Build a front-end application that interacts with a bearer-auth protected API +****Please be aware that the frontend is close to nonexistent, since this project's purpose is to demonstrate my backend capabilities**** -## Introduction +## Features -You are tasked with building a small frontend application containing 3 forms and a list. There is a screenshot at the -bottom of this document that gives you an idea of what to aim for. As you'll be able to see, it doesn't have to *look* -good so don't spend time on styling! +- User registration +- User authentication +- Adding movies to the database -The flow of the application you build looks like this: +## Getting Started -1. A user fills in the register form to create their account with a hashed password -2. The user fills in the login form to get a bearer token -3. The user can then create movies once they have a valid token -4. The list of displayed movies will update as a user creates them +The included `.env` file allows for seamless testing. **This is temporary and will be removed.** -## Setting up +### Prerequisites -Take a little bit of time to familiarise yourself with the project structure - this exercise has both a front-end React -app (`src/client/`) *and* a back-end express API (`src/server/`) in it. +- Node.js installed +- npm installed -1. Fork this repository and clone the fork. - 1. If you want to use the more challenging `freedom` branch, or the completely empty `empty` branch, remember to UNCHECK the checkbox for "Copy the - `main` branch only" +### Installation - forking screenshot - 2. Then, once you have cloned your fork of the repo, you can run `git checkout freedom` or `git checkout empty` in your terminal. - 3. If you do not uncheck this box, then you will only be able to access the `main` branch. -2. Rename `.env.example` to `.env` -3. Edit the `DATABASE_URL` variable in `.env`, swapping `YOUR_DATABASE_URL` for the URL of your database -4. Run `npm ci` to install the project dependencies. -5. If you're using the main branch, we have already created the Prisma schema and migrations for you. Run `npx prisma migrate reset` to execute the - database migrations. Press `y` when it asks if you're sure. +1. **Clone the repository**: -## Instructions +```bash +git clone https://github.com/AtikoSpeed/auth-challenge.git +``` -- Run the app with `npm run dev` - this will open a React app in your browser *and* run the express server at the - same time. This is thanks to the `concurrently` package that we have included in this project. -- The frontend React app will run on Vite's default port, which is usually `5173` - check the terminal once you have - started the app to see what port it is running on. - - If you have other Vite apps running, then this port could be different. -- The server will run on port `4000` (as set in `.env.example` - remember to create your own `.env` file). If you need - to, you can change this by updating the `VITE_PORT` environment variable in your `.env`. - - Note: This environment variable is used in both the server and the client. Vite requires environment variables - to be prefixed with `VITE_`, so do not change the name! Read more about environment variables in Vite - [here](https://vitejs.dev/guide/env-and-mode.html#env-files). -- Work through each file in the `requirements` directory in numerical order. You can choose whether to work on the - Client or Server version of each requirement first, but you may find it easier to do Server first. +2. **Navigate to the project directory**: -## Example solution +```bash +cd auth-challenge +``` -![](./assets/example_solution.png) +3. **Install dependencies**: + +```bash +npm ci +``` + +### Running the Application + +Start the front and back-end servers with a single command: + +```bash +npm run dev +``` + +The server will run on the port specified in the `.env` file. + +## Testing the Application + +Use an API testing tool like Postman to interact with the endpoints. + +### Endpoints + +- **Register** + + - **Endpoint**: `POST /register` + - **Description**: Register a new user. + - **Request Body**: + ```json + { + "username": "your_username", + "password": "your_password" + } + ``` + - **Responses**: + - `201 Created`: "User created successfully" + - `409 Conflict`: "User already exists" + +- **Login** + + - **Endpoint**: `POST /login` + - **Description**: Authenticate a user and receive a JWT token. + - **Request Body**: + ```json + { + "username": "your_username", + "password": "your_password" + } + ``` + - **Responses**: + - `200 OK`: `{ "data": "" }` + - `401 Unauthorized`: + - `{ "error": "Invalid username." }` + - `{ "error": "Invalid password." }` + +- **Get All Movies** + + - **Endpoint**: `GET /movies` + - **Description**: Retrieve all movies from the database. + - **Responses**: + - `200 OK`: `{ "data": [...] }` + +- **Add Movie** + - **Endpoint**: `POST /movies` + - **Description**: Add a new movie to the database. Requires authentication. + - **Headers**: + ``` + Authentication: Bearer + ``` + - **Request Body**: + ```json + { + "title": "Movie Title", + "description": "Movie Description", + "runtimeMins": 120 + } + ``` + - **Responses**: + - `200 OK`: "Movie created successfully" + - `401 Unauthorized`: `{ "error": "Invalid token provided." }` + - `409 Conflict`: "Movie already exists" + +## Purpose + +This project is intended to demonstrate my backend capabilities as a full-stack developer. diff --git a/src/client/App.jsx b/src/client/App.jsx index bae3b635..693b816d 100644 --- a/src/client/App.jsx +++ b/src/client/App.jsx @@ -1,19 +1,19 @@ -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 [newMovie, setNewMovie] = useState(false); useEffect(() => { fetch(`${apiUrl}/movie`) - .then(res => res.json()) - .then(res => setMovies(res.data)); - }, []); + .then((res) => res.json()) + .then((res) => setMovies(res.data)); + }, [newMovie]); /** * HINTS! @@ -34,16 +34,41 @@ function App() { * */ const handleRegister = async ({ username, password }) => { - + const response = await fetch(`${apiUrl}/user/register`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username: username, password: password }), + }); + alert(await response.json()); }; const handleLogin = async ({ username, password }) => { - + const response = await fetch(`${apiUrl}/user/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username: username, password: password }), + }); + const token = await response.json(); + localStorage.setItem("token", token.data); + token.errer ? alert(token.error) : alert("Logged in successfully"); }; const handleCreateMovie = async ({ title, description, runtimeMins }) => { - - } + const response = await fetch(`${apiUrl}/movie`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authentication: localStorage.getItem("token"), + }, + body: JSON.stringify({ + title: title, + description: description, + runtimeMins: runtimeMins, + }), + }); + setNewMovie(!newMovie); + alert(await response.json()); + }; return (
@@ -58,7 +83,7 @@ function App() {

Movie list

    - {movies.map(movie => { + {movies.map((movie) => { return (
  • {movie.title}

    diff --git a/src/server/controllers/movie.js b/src/server/controllers/movie.js index d4733b61..8a21cb07 100644 --- a/src/server/controllers/movie.js +++ b/src/server/controllers/movie.js @@ -1,31 +1,38 @@ -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) => { - const movies = await prisma.movie.findMany(); + const movies = await prisma.movie.findMany(); - res.json({ data: movies }); + res.json({ data: movies }); }; const createMovie = async (req, res) => { - 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.' }) - } - - const createdMovie = null; - - res.json({ data: createdMovie }); + const { title, description, runtimeMins } = req.body; + try { + const token = req.headers.authentication; + // todo verify the token + jwt.verify(token, jwtSecret); + } catch (e) { + return res.status(401).json({ error: "Invalid token provided." }); + } + try { + const createdMovie = await prisma.movie.create({ + data: { + title: title, + description: description, + runtimeMins: runtimeMins, + }, + }); + console.log(createdMovie); + res.json("Movie created successfully"); + } catch (e) { + console.log("Movie already exists"); + res.json("Movie already exists"); + } }; -export { - getAllMovies, - createMovie -}; +export { getAllMovies, createMovie }; diff --git a/src/server/controllers/user.js b/src/server/controllers/user.js index 05db4183..507666d7 100644 --- a/src/server/controllers/user.js +++ b/src/server/controllers/user.js @@ -1,39 +1,49 @@ -import bcrypt from 'bcrypt'; -import jwt from 'jsonwebtoken'; -import { PrismaClient } from '@prisma/client' +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; +import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); -const jwtSecret = 'mysecret'; +const jwtSecret = "mysecret"; const register = async (req, res) => { + try { const { username, password } = req.body; - const createdUser = null; + const cryptHash = await bcrypt.hash(password, 10); - res.json({ data: createdUser }); + const createdUser = await prisma.user.create({ + data: { + username: username, + password: cryptHash, + }, + }); + + res.status(201).json("User created successfully"); + } catch { + res.status(409).json("User already exists"); + } }; const login = async (req, res) => { - const { username, password } = req.body; + const { username, password } = req.body; - const foundUser = null; + const foundUser = await prisma.user.findUnique({ + where: { username: username }, + }); - if (!foundUser) { - return res.status(401).json({ error: 'Invalid username or password.' }); - } + if (!foundUser) { + return res.status(401).json({ error: "Invalid username." }); + } - const passwordsMatch = false; + const passwordsMatch = await bcrypt.compare(password, foundUser.password); - if (!passwordsMatch) { - return res.status(401).json({ error: 'Invalid username or password.' }); - } + if (!passwordsMatch) { + return res.status(401).json({ error: "Invalid password." }); + } - const token = null; - - res.json({ data: token }); + const token = jwt.sign(username, jwtSecret); + console.log(token); + res.json({ data: token }); }; -export { - register, - login -}; +export { register, login };