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.

146 changes: 104 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

<img src="./assets/forking_screenshot.png" alt="forking screenshot" width=600></img>
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": "<token>" }`
- `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 <token>
```
- **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.
51 changes: 38 additions & 13 deletions src/client/App.jsx
Original file line number Diff line number Diff line change
@@ -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!
Expand All @@ -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 (
<div className="App">
Expand All @@ -58,7 +83,7 @@ function App() {

<h1>Movie list</h1>
<ul>
{movies.map(movie => {
{movies.map((movie) => {
return (
<li key={movie.id}>
<h3>{movie.title}</h3>
Expand Down
49 changes: 28 additions & 21 deletions src/server/controllers/movie.js
Original file line number Diff line number Diff line change
@@ -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 };
54 changes: 32 additions & 22 deletions src/server/controllers/user.js
Original file line number Diff line number Diff line change
@@ -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 };