diff --git a/README.md b/README.md index dfa05e177..cdb876cc6 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,11 @@ # Project Auth API -Replace this readme with your own information about your project. - -Start by briefly describing the assignment in a sentence or two. Keep it short and to the point. +This assignment was to build an API with authentication to implement a registration flow and a frontend with forms to register, sign in, and view some content once logged in. ## The problem -Describe how you approached to problem, and what tools and techniques you used to solve it. How did you plan? What technologies did you use? If you had more time, what would be next? +Struggled a lot with where to salt the password, then found Technigo's example of how to solve it. This was a hard project and it helped a lot to break it down into lots of little steps. So first I just created the React components with no back end. Then I set up the API endpoints but didn't put any code in them yet. After that I made the login form send a POST request to the API but didn't try to code the login check yet. ## View it live -Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about. +https://project-auth-jcs.netlify.app/ diff --git a/backend/package.json b/backend/package.json index 8de5c4ce0..7499fa261 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,6 +12,7 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "bcrypt": "^5.1.1", "cors": "^2.8.5", "express": "^4.17.3", "mongoose": "^8.0.0", diff --git a/backend/server.js b/backend/server.js index dfe86fb8e..0133f6385 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,11 +1,30 @@ import cors from "cors"; import express from "express"; import mongoose from "mongoose"; +// install bcrypt with npm install. https://nordvpn.com/sv/blog/what-is-bcrypt/ +import bcrypt from "bcrypt"; +import crypto from "crypto"; const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/project-mongo"; mongoose.connect(mongoUrl); mongoose.Promise = Promise; +const User = mongoose.model("User", { + username: { + type: String, + unique: true, + required: true, + }, + password: { + type: String, + required: true, + }, + token: { + type: String, + default: () => crypto.randomBytes(128).toString("hex"), + }, +}); + // Defines the port the app will run on. Defaults to 8080, but can be overridden // when starting the server. Example command to overwrite PORT env variable value: // PORT=9000 npm start @@ -21,6 +40,54 @@ app.get("/", (req, res) => { res.send("Hello Technigo!"); }); +// here is the route for post sign up. +app.post("/signup", async (req, res) => { + // https://heynode.com/blog/2020-04/salt-and-hash-passwords-bcrypt/ + const salt = bcrypt.genSaltSync(10); + + // here we are defining what kind of data we are going to get. + User.create({ + username: req.body.username, + password: bcrypt.hashSync(req.body.password, salt), + }) + .then((user) => { + res.status(201).json({ token: user.token }); + }) + .catch((error) => { + res.status(400).json({ message: "Could not create user", error }); + }); +}); + +// created a login endpoint here. POST/login is the endpoint. POST/sign up is also an endpoint. GET is also an endpoint. The endpoints like log in and user data gets stored in the database (mongoDB) +app.post("/login", async (req, res) => { + const { username, password } = req.body; + // here we are creating a token ... like you get a ticket to a festival, and the wristband is the token + return User.findOne({ username: req.body.username }).then((user) => { + if (user && bcrypt.compareSync(req.body.password, user.password)) { + res.json({ token: user.token }); + } else { + res.status(401).json({ message: "Could not log in" }); + return; + } + }); +}); + +// here we are checking the token, if it is correct we get the message, if not we get a 401 error +app.get("/private", async (req, res) => { + if (!req.headers.authorization) { + res.status(401).json({ message: "Not authorized" }); + } + + const user = await User.findOne({ + token: req.headers.authorization.split(" ")[1], + }); + if (!user) { + res.status(401).json({ message: "Not authorized" }); + return; + } + res.json({ message: "This is a secret message" }); +}); + // Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); diff --git a/frontend/package.json b/frontend/package.json index e9c95b79f..db1a62a6e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,8 @@ }, "dependencies": { "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.23.1" }, "devDependencies": { "@types/react": "^18.2.15", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 1091d4310..5a3aa624f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,3 +1,19 @@ -export const App = () => { - return
Find me in src/app.jsx!
; -}; +// Import code from movie-project +// here i import the componets +import React from "react"; +import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +import { LoginPage } from "./LoginPage"; +import { SignupPage } from "./SignupPage"; +import { PrivatePage } from "./PrivatePage"; + +// https://reactrouter.com/ +// here setting up routes for the app +export const App = () => ( + + + } /> + } /> + } /> + + +); diff --git a/frontend/src/LoginPage.jsx b/frontend/src/LoginPage.jsx new file mode 100644 index 000000000..c48d8a6f4 --- /dev/null +++ b/frontend/src/LoginPage.jsx @@ -0,0 +1,52 @@ +// here we import the useNavigate hook from react-router-dom and use it to navigate to the /private route when the form is submitted. +import { Link, useNavigate } from "react-router-dom"; + +export const LoginPage = () => { + // getting the function out + const navigate = useNavigate(); + const handleSubmit = (event) => { + // prevent dedefault stops the page from reloading + event.preventDefault(); + + // fetch is for sending requests to the server. Post is for sending data to the server. + fetch("https://project-auth-jcs.onrender.com/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + // here we are defining what data we are going to send to the server + body: JSON.stringify({ + username: event.target.username.value, + password: event.target.password.value, + }), + }) + // here converting the response from json, which is a string, to an object that we can work with + .then((res) => res.json()) + .then((res) => { + console.log(res); + // here we are saving the token in the local storage, so the user can stay logged in + localStorage.setItem("token", res.token); + navigate("/private"); + }); + }; + return ( + <> + {/* here we have to first register an event handler for the form submission */} +
+

Login

+ + + +
+

+ Sign up +

+ + ); +}; diff --git a/frontend/src/PrivatePage.jsx b/frontend/src/PrivatePage.jsx new file mode 100644 index 000000000..434b03538 --- /dev/null +++ b/frontend/src/PrivatePage.jsx @@ -0,0 +1,42 @@ +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; + +export const PrivatePage = () => { + const navigate = useNavigate(); + + const [data, setData] = useState(null); + const [status, setStatus] = useState(null); + + useEffect(() => { + fetch("https://project-auth-jcs.onrender.com/private", { + headers: { + // this is how we get the token back from the local storage, to prove that the user is still logged in. + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + }) + .then((res) => { + setStatus(res.status); + return res.json(); + }) + .then((data) => { + setData(data.message); + }); + }, []); + + // here we create the log out function that removes the token from the local storage + const logout = (event) => { + event.preventDefault(); + localStorage.removeItem("token"); + navigate("/"); + }; + + return ( +
+ {/* data is the msg from the server */} + {data} + {/* the && is how to write if in jsx, in js we would typ "if (status === 200) } +
+ ); +}; diff --git a/frontend/src/SignupPage.jsx b/frontend/src/SignupPage.jsx new file mode 100644 index 000000000..5a3657907 --- /dev/null +++ b/frontend/src/SignupPage.jsx @@ -0,0 +1,57 @@ +import { useNavigate } from "react-router-dom"; +import { useState } from "react"; +import { Link } from "react-router-dom"; + +export const SignupPage = () => { + const [error, setError] = useState(false); + const navigate = useNavigate(); + const handleSubmit = (event) => { + // prevent dedefault stops the page from reloading + event.preventDefault(); + + // fetch is for sending requests to the server. Post is for sending data to the server. + fetch("https://project-auth-jcs.onrender.com/signup", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + // here we are defining what data we are going to send to the server + body: JSON.stringify({ + username: event.target.username.value, + password: event.target.password.value, + }), + }) + // here converting the response from json, which is a string, to an object that we can work with + .then((res) => res.json()) + .then((res) => { + if (res.error) { + setError(true); + return; + } + console.log(res); + // here we are saving the token in the local storage, so the user can stay logged in + localStorage.setItem("token", res.token); + navigate("/private"); + }); + }; + return ( + <> +
+

Sign up

+ {error &&

Something went wrong

} + + + +
+

+ Log in +

+ + ); +}; diff --git a/netlify.toml b/netlify.toml deleted file mode 100644 index ed9e83391..000000000 --- a/netlify.toml +++ /dev/null @@ -1,6 +0,0 @@ -# This file tells netlify where the code for this project is and -# how it should build the JavaScript assets to deploy from. -[build] - base = "frontend/" - publish = "dist" - command = "npm run build"