diff --git a/README.md b/README.md
index dfa05e177..3815c7313 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,18 @@
# Project Auth API
-Replace this readme with your own information about your project.
+Developed a backend API and frontend registration & sign-in form.
+
+API uses mongoose to include endpoints for user registration & sign-in, authenticated endpoint accessible only to logged-in users, storing and removing access tokens.
-Start by briefly describing the assignment in a sentence or two. Keep it short and to the point.
## 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?
+We had issues storing the access token, but figured it out by parsing the token to JSON.
## 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.
+#### Backend:
+https://project-auth-kh49.onrender.com/
+
+#### Frontend:
+https://heroic-beignet-77e068.netlify.app/
diff --git a/backend/package.json b/backend/package.json
index 8de5c4ce0..a00d732a1 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -12,9 +12,13 @@
"@babel/core": "^7.17.9",
"@babel/node": "^7.16.8",
"@babel/preset-env": "^7.16.11",
+ "bcrypt": "^5.1.1",
"cors": "^2.8.5",
+ "dotenv": "^16.4.5",
"express": "^4.17.3",
- "mongoose": "^8.0.0",
- "nodemon": "^3.0.1"
+ "express-list-endpoints": "^7.1.0",
+ "mongodb": "^6.6.2",
+ "mongoose": "^8.4.0",
+ "nodemon": "^3.1.1"
}
}
diff --git a/backend/server.js b/backend/server.js
index dfe86fb8e..d3ebfa70f 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -1,14 +1,59 @@
import cors from "cors";
import express from "express";
import mongoose from "mongoose";
+import bcrypt from "bcrypt";
+import expressListEndpoints from "express-list-endpoints";
-const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/project-mongo";
+const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/project-auth";
mongoose.connect(mongoUrl);
mongoose.Promise = Promise;
-// 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
+const { Schema } = mongoose;
+
+// Schema
+const userSchema = new Schema({
+ name: {
+ type: String,
+ required: true,
+ },
+ username: {
+ type: String,
+ unique: true,
+ required: true,
+ },
+ email: {
+ type: String,
+ unique: true,
+ required: true,
+ },
+ password: {
+ type: String,
+ required: true,
+ },
+ address: {
+ type: String,
+ required: true,
+ },
+ accessToken: {
+ type: String,
+ default: () => bcrypt.genSaltSync(),
+ },
+});
+
+// Model
+const User = mongoose.model("User", userSchema);
+
+const authenticateUser = async (req, res, next) => {
+ const user = await User.findOne({ accessToken: req.header("Authorization") });
+ if (user) {
+ req.user = user;
+ next();
+ } else {
+ res.status(401).json({ loggedOut: true });
+ }
+};
+
+// Defines the port the app will run on. Defaults to 8080
const port = process.env.PORT || 8080;
const app = express();
@@ -18,7 +63,51 @@ app.use(express.json());
// Start defining your routes here
app.get("/", (req, res) => {
- res.send("Hello Technigo!");
+const endpoints = expressListEndpoints(app)
+res.json(endpoints)
+});
+
+app.get("/users", async (req, res) => {
+ const allUsers = await User.find().exec();
+ if (allUsers.length > 0) {
+ res.json(allUsers);
+ } else {
+ res.status(404).send("No users found");
+ }
+});
+
+app.post("/users", async (req, res) => {
+ try {
+ const { name, username, email, password, address } = req.body;
+ const salt = bcrypt.genSaltSync();
+ const user = new User({
+ name,
+ username,
+ email,
+ password: bcrypt.hashSync(password, salt),
+ address,
+ });
+ await user.save();
+ res.status(201).json({ userId: user._id, accessToken: user.accessToken });
+ } catch (error) {
+ res
+ .status(400)
+ .json({ message: "Could not create user", errors: error.errors });
+ }
+});
+
+app.get("/logged-in", authenticateUser);
+app.get("/logged-in", (req, res) => {
+ res.json({ message: "You are signed in." });
+});
+
+app.post("/sessions", async (req, res) => {
+ const user = await User.findOne({ username: req.body.username });
+ if (user && bcrypt.compareSync(req.body.password, user.password)) {
+ res.json({ userId: user._id, accessToken: user.accessToken });
+ } else {
+ res.json({ notFound: true });
+ }
});
// Start the server
diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs
index 4dcb43901..94e01b901 100644
--- a/frontend/.eslintrc.cjs
+++ b/frontend/.eslintrc.cjs
@@ -17,4 +17,6 @@ module.exports = {
{ allowConstantExport: true },
],
},
+ 'react/prop-types': "off"
}
+
diff --git a/frontend/_redirects b/frontend/_redirects
new file mode 100644
index 000000000..50a463356
--- /dev/null
+++ b/frontend/_redirects
@@ -0,0 +1 @@
+/* /index.html 200
\ No newline at end of file
diff --git a/frontend/index.html b/frontend/index.html
index 0c589eccd..f66f7ef81 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1,10 +1,11 @@
-
+
-
- Vite + React
+
+
+ Log in
diff --git a/frontend/package.json b/frontend/package.json
index e9c95b79f..23e2448e4 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -11,7 +11,9 @@
},
"dependencies": {
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.23.1",
+ "zustand": "^4.5.2"
},
"devDependencies": {
"@types/react": "^18.2.15",
diff --git a/frontend/public/_redirects b/frontend/public/_redirects
new file mode 100644
index 000000000..50a463356
--- /dev/null
+++ b/frontend/public/_redirects
@@ -0,0 +1 @@
+/* /index.html 200
\ No newline at end of file
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 1091d4310..1c7be68f8 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,3 +1,11 @@
+import { BrowserRouter } from "react-router-dom";
+import { Home } from "./components/Home";
+import { RouteList } from "./components/RouteList";
+
export const App = () => {
- return Find me in src/app.jsx!
;
+ return (
+
+
+
+ );
};
diff --git a/frontend/src/components/Button.jsx b/frontend/src/components/Button.jsx
new file mode 100644
index 000000000..9db3ce541
--- /dev/null
+++ b/frontend/src/components/Button.jsx
@@ -0,0 +1,7 @@
+export const Button = ({ btnText, type, onClick }) => {
+ return (
+
+ );
+};
diff --git a/frontend/src/components/Headline.jsx b/frontend/src/components/Headline.jsx
new file mode 100644
index 000000000..917d7025c
--- /dev/null
+++ b/frontend/src/components/Headline.jsx
@@ -0,0 +1,3 @@
+export const Headline = ({ titleText }) => {
+ return {titleText}
;
+};
diff --git a/frontend/src/components/Home.jsx b/frontend/src/components/Home.jsx
new file mode 100644
index 000000000..a18d67df1
--- /dev/null
+++ b/frontend/src/components/Home.jsx
@@ -0,0 +1,11 @@
+import { LogIn } from "./LogIn";
+import { Register } from "./Register";
+
+export const Home = () => {
+ return (
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/LogIn.jsx b/frontend/src/components/LogIn.jsx
new file mode 100644
index 000000000..4a81b5bda
--- /dev/null
+++ b/frontend/src/components/LogIn.jsx
@@ -0,0 +1,61 @@
+import { Button } from "./Button";
+import { Headline } from "./Headline";
+import { TextInput } from "./TextInput";
+import { useStore } from "../store/useStore";
+import { useState } from "react";
+
+export const LogIn = () => {
+ const { loginData, handleSubmitLogin, handleLoginChange } = useStore();
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleFormSubmit = async (event) => {
+ event.preventDefault();
+ setIsLoading(true);
+
+ try {
+ await handleSubmitLogin(event);
+ // Redirect to /logged-in after successful login
+ window.location.href = "/logged-in";
+ } catch (error) {
+ console.error("Error logging in", error);
+ setIsLoading(false);
+ }
+ };
+
+ return (
+ <>
+
+ >
+ );
+};
diff --git a/frontend/src/components/Register.jsx b/frontend/src/components/Register.jsx
new file mode 100644
index 000000000..ddee1be51
--- /dev/null
+++ b/frontend/src/components/Register.jsx
@@ -0,0 +1,123 @@
+import { useStore } from "../store/useStore";
+import { Button } from "./Button";
+import { Headline } from "./Headline";
+import { TextInput } from "./TextInput";
+import { useState } from "react";
+
+export const Register = () => {
+ const { signUpData, handleSubmitForm, handleSignUpChange } = useStore();
+ const [isLoading, setIsLoading] = useState(false);
+ const [passwordError, setPasswordError] = useState(false);
+
+ const handleFormSubmit = async (event) => {
+ event.preventDefault();
+ setIsLoading(true);
+
+ const success = await handleSubmitForm(event);
+ if (success) {
+ window.location.href = "/logged-in";
+ } else {
+ console.error("Error logging in");
+ setPasswordError(true);
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/frontend/src/components/RouteList.jsx b/frontend/src/components/RouteList.jsx
new file mode 100644
index 000000000..47d61cb81
--- /dev/null
+++ b/frontend/src/components/RouteList.jsx
@@ -0,0 +1,14 @@
+import { Routes, Route } from "react-router-dom";
+import { Home } from "./Home";
+import { Session } from "./Session";
+
+export const RouteList = () => {
+ return (
+ <>
+
+ } />
+ } />
+
+ >
+ );
+};
diff --git a/frontend/src/components/Session.jsx b/frontend/src/components/Session.jsx
new file mode 100644
index 000000000..8550c6c44
--- /dev/null
+++ b/frontend/src/components/Session.jsx
@@ -0,0 +1,75 @@
+import { useEffect, useState } from "react";
+import { useStore } from "../store/useStore";
+import { Navigate } from "react-router-dom";
+import { Button } from "./Button";
+import { Link } from "react-router-dom";
+import "../styling/Session.css";
+
+export const Session = () => {
+ const { message, fetchLoggedInData, resetSignUpData, resetLoginData } =
+ useStore();
+ const [shouldRedirect, setShouldRedirect] = useState(false);
+ const [text, setText] = useState("");
+ const [error, setError] = useState(false);
+ let user = localStorage.getItem("username");
+ if (user) {
+ user = user.replace(/^"(.*)"$/, "$1");
+ }
+
+ useEffect(() => {
+ const storedAccessToken = localStorage.getItem("token");
+ try {
+ const parsedToken = JSON.parse(storedAccessToken);
+ fetchLoggedInData(parsedToken);
+ } catch (err) {
+ setError(true);
+ console.error("Error parsing token:", err);
+ }
+ }, [fetchLoggedInData]);
+
+ useEffect(() => {
+ if (message) {
+ setText(`Welcome, ${user}! We're so happy to see you!`);
+ setError(false);
+ } else if (error) {
+ setText(
+ "The username or password is incorrect. Please try signing in again."
+ );
+ localStorage.clear();
+ resetSignUpData();
+ resetLoginData();
+ } else {
+ setText("You are not signed in.");
+ }
+ }, [message, error]);
+
+ const signOut = () => {
+ localStorage.clear();
+ resetSignUpData();
+ resetLoginData();
+ setShouldRedirect(true);
+ };
+
+ if (shouldRedirect) {
+ return ;
+ }
+
+ return (
+
+ {message &&
{message}
}
+
{text}
+ {message && (
+
+ )}
+ {!message && (
+
+
+
+ )}
+
+ );
+};
diff --git a/frontend/src/components/TextInput.jsx b/frontend/src/components/TextInput.jsx
new file mode 100644
index 000000000..2895a99c7
--- /dev/null
+++ b/frontend/src/components/TextInput.jsx
@@ -0,0 +1,23 @@
+import { HandleFocus } from "../helpers/HandleFocus";
+
+export const TextInput = ({ inputType, inputName, placeholder, label, value, onChange }) => {
+ const { placeholder: initialPlaceholder, handleFocus, handleOnBlur } = HandleFocus(placeholder, inputName);
+
+ return (
+ <>
+
+ >
+ );
+};
diff --git a/frontend/src/helpers/HandleFocus.jsx b/frontend/src/helpers/HandleFocus.jsx
new file mode 100644
index 000000000..2b63b4363
--- /dev/null
+++ b/frontend/src/helpers/HandleFocus.jsx
@@ -0,0 +1,20 @@
+import { useState } from "react";
+
+// handles the placeholder visibility depending on, if the input field is active or not
+export const HandleFocus = (initialPlaceholder, inputName) => {
+ const [placeholder, setPlaceholder] = useState(initialPlaceholder);
+
+ // when the input field is active the placeholder disappear
+ const handleFocus = () => {
+ setPlaceholder("");
+ };
+
+ // if the input field is empty when focusing on another input, the placeholder will appear again.
+ const handleOnBlur = (value) => {
+ if (value === "") {
+ setPlaceholder(initialPlaceholder);
+ }
+ };
+
+ return { placeholder, handleFocus, handleOnBlur, inputName };
+};
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 3e560a674..670c19746 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -1,13 +1,135 @@
:root {
+ --title-font: "Playfair Display";
+ --text-font: "Work Sans";
+ --title-color: #683651;
+ --dark-purple: #683651;
+}
+
+* {
+ box-sizing: border-box;
margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
- "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
- sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
+ padding: 0;
+ background-color: #fffcf5;
+ font-family: var(--text-font);
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.outer-container {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+h2 {
+ font-family: var(--title-font);
+ font-size: 35px;
+ color: var(--title-color);
+}
+
+.title-box {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-bottom: 10px;
+ gap: 15px;
+}
+
+.text-box {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ text-align: center;
+}
+
+form {
+ display: flex;
+ flex-direction: column;
+ margin: 10px;
+ gap: 20px;
+}
+
+label {
+ display: flex;
+ flex-direction: column;
+ /* margin-bottom: 20px; */
+ font-weight: 400;
+}
+
+legend {
+ font-size: 18px;
+}
+
+fieldset {
+ display: flex;
+ flex-direction: column;
+ border: none;
+ padding-top: 5px;
+ gap: 15px;
+}
+
+input[type="text"],
+input[type="password"],
+input[type="email"] {
+ padding: 10px;
+ border-radius: 3px;
+ border: 1px solid black;
+}
+
+button {
+ background-color: var(--dark-purple);
+ color: #fffcf5;
+ font-family: var(--title-font);
+ font-size: 16px;
+ border-radius: 5px;
+ border: none;
+ padding: 8px;
+ text-transform: uppercase;
+ width: 100%;
+}
+
+@media (min-width: 480px) {
+ form {
+ width: 440px;
+ }
+
+ h2 {
+ font-size: 45px;
+ }
+}
+
+@media (min-width: 768px) {
+ form {
+ width: 580px;
+ }
+
+ .input-tablet-desktop {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ .input-tablet-desktop label {
+ width: 280px;
+ }
+
+ .postcode-box label {
+ width: 200px;
+ }
+
+ .city-box label {
+ width: 350px;
+ }
+
+ button {
+ width: 150px;
+ align-self: center;
+ }
}
-code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
- monospace;
-}
\ No newline at end of file
+/* 3D6E30 */
+/* 725982 */
+/* 683651 */
diff --git a/frontend/src/store/useStore.jsx b/frontend/src/store/useStore.jsx
new file mode 100644
index 000000000..9488f70b0
--- /dev/null
+++ b/frontend/src/store/useStore.jsx
@@ -0,0 +1,157 @@
+import { create } from "zustand";
+
+export const useStore = create((set, get) => ({
+ signUpData: {
+ name: "",
+ email: "",
+ street: "",
+ postCode: "",
+ city: "",
+ username: "",
+ password: "",
+ verifyingPassword: "",
+ },
+
+ loginData: {
+ username: "",
+ password: "",
+ },
+
+ accessToken: "",
+ message: "",
+
+ resetSignUpData: () =>
+ set({
+ signUpData: {
+ name: "",
+ email: "",
+ street: "",
+ postCode: "",
+ city: "",
+ username: "",
+ password: "",
+ verifyingPassword: "",
+ },
+ }),
+
+ resetLoginData: () =>
+ set({
+ loginData: {
+ username: "",
+ password: "",
+ },
+ }),
+
+ handleSubmitForm: async (event) => {
+ event.preventDefault();
+ const { signUpData } = get();
+ const constructedAddress =
+ signUpData.street + signUpData.postCode + signUpData.city;
+
+ if (signUpData.password !== signUpData.verifyingPassword) {
+ console.error("Passwords do not match");
+ return false;
+ }
+ try {
+ const response = await fetch(
+ "https://project-auth-kh49.onrender.com/users",
+ {
+ method: "POST",
+ body: JSON.stringify({
+ name: signUpData.name,
+ username: signUpData.username,
+ email: signUpData.email,
+ password: signUpData.password,
+ address: constructedAddress,
+ }),
+ headers: { "Content-Type": "application/json" },
+ }
+ );
+ if (!response.ok) {
+ throw new Error("Network response was not ok");
+ }
+ const result = await response.json();
+ set((state) => ({ ...state, accessToken: result.accessToken }));
+ const updatedAccessToken = get().accessToken;
+ const updatedUsername = get().signUpData.username;
+ localStorage.setItem("token", JSON.stringify(updatedAccessToken));
+ localStorage.setItem("username", JSON.stringify(updatedUsername));
+ return true;
+ } catch (error) {
+ console.error("Error adding new user:", error);
+ return false;
+ }
+ },
+
+ handleSignUpChange: (fieldName, value) => {
+ set((state) => ({
+ signUpData: {
+ ...state.signUpData,
+ [fieldName]: value,
+ },
+ }));
+ },
+
+ handleLoginChange: (fieldName, value) => {
+ set((state) => ({
+ loginData: {
+ ...state.loginData,
+ [fieldName]: value,
+ },
+ }));
+ },
+
+ handleSubmitLogin: async (event) => {
+ event.preventDefault();
+ const { loginData } = get();
+ try {
+ const response = await fetch(
+ "https://project-auth-kh49.onrender.com/sessions",
+ {
+ method: "POST",
+ body: JSON.stringify({
+ username: loginData.username,
+ password: loginData.password,
+ }),
+ headers: { "Content-Type": "application/json" },
+ }
+ );
+ if (!response.ok) {
+ throw new Error("Network response was not ok");
+ }
+ const result = await response.json();
+ set((state) => ({
+ ...state,
+ accessToken: result.accessToken,
+ }));
+ const updatedAccessToken = get().accessToken;
+ const updatedUsername = get().loginData.username;
+ localStorage.setItem("token", JSON.stringify(updatedAccessToken));
+ localStorage.setItem("username", JSON.stringify(updatedUsername));
+ } catch (error) {
+ console.error("Error logging in", error);
+ }
+ },
+
+ fetchLoggedInData: async (accessToken) => {
+ try {
+ const response = await fetch(
+ "https://project-auth-kh49.onrender.com/logged-in",
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: accessToken,
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error("Network response was not ok");
+ }
+ const result = await response.json();
+ set((state) => ({ ...state, message: result.message }));
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ }
+ },
+}));
diff --git a/frontend/src/styling/Session.css b/frontend/src/styling/Session.css
new file mode 100644
index 000000000..cbe30b198
--- /dev/null
+++ b/frontend/src/styling/Session.css
@@ -0,0 +1,18 @@
+.session-container {
+display: flex;
+flex-direction: column;
+align-items: center;
+row-gap: 10px;
+margin-top: 50px;
+}
+
+.session-in-text {
+font-size: 20px;
+text-align: center;
+}
+
+@media (min-width: 768px) {
+ .session-in-button {
+ text-align: center;
+ }
+}
\ No newline at end of file
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"
diff --git a/package.json b/package.json
index d774b8cc3..fea545062 100644
--- a/package.json
+++ b/package.json
@@ -3,5 +3,10 @@
"version": "1.0.0",
"scripts": {
"postinstall": "npm install --prefix backend"
+ },
+ "dependencies": {
+ "bcrypt": "^5.1.1",
+ "bcrypt-nodejs": "^0.0.3",
+ "mongodb": "^6.6.2"
}
}