commit da746fc04f7a41cc2086f7c56716925700b6cba6 Author: SowinskiBraeden Date: Mon May 5 10:17:17 2025 -0700 initial commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5989274 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +MONGODB_HOST=YourMongoDBClusterHere +MONGODB_USER=YourUsernameHere +MONGODB_PASSWORD=YourPasswordHere +MONGODB_DATABASE=YourMongoDBDatabaseHere +MONGODB_SESSION_SECRET=Some_Session_Key_Here1 +NODE_SESSION_SECRET=Some_Session_Key_Here2 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..ecff155 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +package-lock.json +.env +*.sync-confcict-* \ No newline at end of file diff --git a/config/config.js b/config/config.js new file mode 100644 index 0000000..0d7c600 --- /dev/null +++ b/config/config.js @@ -0,0 +1,10 @@ +require('dotenv').config(); + +module.exports = { + mongo_user: process.env.MONGODB_USER, + mongo_password: process.env.MONGODB_PASSWORD, + mongo_host: process.env.MONGODB_HOST, + mongo_database: process.env.MONGODB_DATABASE, + mongo_secret: process.env.MONGODB_SESSION_SECRET, + express_secret: process.env.NODE_SESSION_SECRET, +}; diff --git a/index.js b/index.js new file mode 100755 index 0000000..c80e2dd --- /dev/null +++ b/index.js @@ -0,0 +1,154 @@ +const express = require("express"); +const path = require("path"); +const session = require("express-session"); +const MongoStore = require("connect-mongo"); +const MongoClient = require("mongodb").MongoClient; +const joi = require("joi"); +const bcrypt = require("bcrypt"); + +// Initialize app +const app = express(); +const port = 8000; + +// Connect MongoDB +const config = require("./config/config"); + +const mongoURI = `mongodb+srv://${config.mongo_user}:${config.mongo_password}@${config.mongo_host}`; +let users; +async function connectMongo() { + try { + const connection = await MongoClient.connect(mongoURI, { connectTimeoutMS: 1000 }); + users = connection.db(config.mongo_database).collection("users"); + } catch (err) { + console.error(`Failed to connect to mongodb at (mongodb+srv://${config.mongo_host})`); + } +} +connectMongo(); + +app.set("view engine", "ejs"); +app.use(express.urlencoded({ extended: false })); +app.use('/static', express.static(path.join(__dirname, "./public"))); + +// Sessions +app.use(session({ + secret: config.express_secret, + store: MongoStore.create({ mongoUrl: `${mongoURI}/${config.mongo_database}`, crypto: { secret: config.mongo_secret } }), + resave: true, + saveUninitialized: false, + cookie: { maxAge: 60000 }, +})); + +/**** Page routes ****/ + +app.get("/", async (req, res) => { + res.set('Content-Type', 'text/html'); + + res.render("index", { authenticated: req.session.authenticated, username: req.session.username }); + return res.status(200); +}); + +app.get("/login", (req, res) => { + res.set('Content-Type', 'text/html'); + res.render("login", { message: req.session.message }); + return res.status(200); +}) + +app.get("/signup", (req, res) => { + res.set('Content-Type', 'text/html'); + res.render("signup", { message: req.session.message }); + return res.status(200); +}); + +app.get("/members", (req, res) => { + if (!req.session.authenticated) { + req.session.message = "Please login"; + res.status(401); + return res.redirect("/login"); + } + + let names = ["carl", "gary", "jebediah"]; + let name = names[Math.floor(Math.random() * names.length)]; + + res.set('Content-Type', 'text/html'); + res.render("members", { name: name, username: req.session.username }); + return res.status(200); +}); + +/*** Authentication routes ***/ + +app.post("/login", async (req, res) => { + const schema = joi.string().email().required(); + let valid = schema.validate(req.body.email); + + if (valid.error) { + req.session.message = "Invalid input"; + res.status(400); + return res.redirect("/login"); + } + + let user = await users.findOne({ email: req.body.email }); + + if (!user) { + req.session.message = "User not found"; + res.status(404); + return res.redirect("/login"); + } + + if (await bcrypt.compare(req.body.password, user.password)) { + req.session.authenticated = true; + req.session.username = user.name; + req.session.message = ""; + + res.status(200); + return res.redirect('/members'); + } else { + req.session.message = "Incorrect password"; + res.status(401); + return res.redirect("/login"); + } +}); + +app.post("/signup", async (req, res) => { + const schema = joi.object({ + name: joi.string().alphanum().max(20).required(), + email: joi.string().email().required(), + password: joi.string().max(20).required() + }); + + let valid = schema.validate(req.body); + + if (valid.error) { + req.session.message = "Invalid input"; + res.status(400); + return res.redirect('/signup'); + } + + let password = await bcrypt.hash(req.body.password, 12); + await users.insertOne({ + name: req.body.name, + email: req.body.email, + password: password, + }); + + req.session.message = "Please login"; + res.status(200); + return res.redirect("/login"); +}); + +app.get("/logout", (req, res) => { + req.session.destroy(); + res.status(200); + return res.redirect('/'); +}); + +/*** 404 Not found ***/ + +app.get("/*splat", (req, res) => { + res.set('Content-Type', 'text/html'); + res.render("notFound"); + return res.status(404); +}); + +app.listen(port, () => { + console.log(`Server listing on port ${port}`); +}); diff --git a/package.json b/package.json new file mode 100755 index 0000000..dacd9a2 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "assignment1", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "nodemon index.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/SowinskiBraeden/comp2537-assignment1.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/SowinskiBraeden/comp2537-assignment1/issues" + }, + "homepage": "https://github.com/SowinskiBraeden/comp2537-assignment1#readme", + "description": "", + "dependencies": { + "bcrypt": "^5.1.1", + "connect-mongo": "^5.1.0", + "dotenv": "^16.5.0", + "ejs": "^3.1.10", + "express": "^5.1.0", + "express-session": "^1.18.1", + "joi": "^17.13.3", + "mongodb": "^6.16.0" + }, + "devDependencies": { + "nodemon": "^3.1.10" + } +} diff --git a/public/css/index.css b/public/css/index.css new file mode 100644 index 0000000..529137a --- /dev/null +++ b/public/css/index.css @@ -0,0 +1,47 @@ +@import url('https://fonts.googleapis.com/css2?family=Oswald&display=swap'); + +* { + margin: 0pt; + padding: 0pt; + box-sizing: border-box; + font-family: 'Oswald', sans-serif; + overflow-x: hidden; + overflow-y: hidden; +} + +body { + width: 100vw; + height: 100vh; +} + +.center { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; +} + +.center p { + margin: 12pt; +} + +.center a { + padding-left: 6pt; + padding-right: 6pt; + margin-top: 2pt; + margin-bottom: 2pt; + font-size: 24px; + background-color: lightsteelblue; + border-radius: 8px; +} + +.center form { + display: flex; + flex-direction: column; + justify-content: center; +} + +.flash-msg { + color: red; +} \ No newline at end of file diff --git a/public/images/carl.png b/public/images/carl.png new file mode 100644 index 0000000..a0058c6 Binary files /dev/null and b/public/images/carl.png differ diff --git a/public/images/gary.png b/public/images/gary.png new file mode 100644 index 0000000..c6cabc9 Binary files /dev/null and b/public/images/gary.png differ diff --git a/public/images/jebediah.png b/public/images/jebediah.png new file mode 100644 index 0000000..db387eb Binary files /dev/null and b/public/images/jebediah.png differ diff --git a/selfgrade.txt b/selfgrade.txt new file mode 100644 index 0000000..28298ff --- /dev/null +++ b/selfgrade.txt @@ -0,0 +1,22 @@ +Self-graded Assignment 1 Checklist + + Criteria + ======== +Student Name: Braeden Sowinski +Student Set: 1D + +[x] A home page links to signup and login, if not logged in; and links to members and signout, if logged in. +[x] A members page that displays 1 of 3 random images stored on the server. +[x] The members page will redirect to the home page if no valid session is found. +[x] The signout buttons end the session. +[x] All secrets, encryption keys, database passwords are stored in a .env file. + +[x] The .env file is NOT in your git repo. +[x] Password is BCrypted in the MongoDB database. +[x] Your site is hosted in a hosting service like Qoddi. +[x] A 404 page that "catches" all invalid page hits and that sets the status code to 404. +[x] Session information is stored in an encrypted MongoDB session database. Sessions expire after 1 hour. + +50/50 (Total grade out of 50, 5 marks each x 10 items) + +*Note items are considered *fully* complete (marked with an x inside the box: [x]), OR incomplete (box is left empty: [ ]) \ No newline at end of file diff --git a/views/index.ejs b/views/index.ejs new file mode 100644 index 0000000..e99408b --- /dev/null +++ b/views/index.ejs @@ -0,0 +1,23 @@ + + + + 2537 Assignment 1 + + + + + + +
+ <% if (authenticated) { %> +

Welcome <%= username %>

+ Go to members page + Logout + <% } else { %> + Login +

or

+ Signup + <% } %> +
+ + diff --git a/views/login.ejs b/views/login.ejs new file mode 100755 index 0000000..7f7cc33 --- /dev/null +++ b/views/login.ejs @@ -0,0 +1,31 @@ + + + + 2537 Assignment 1 + + + + + + + +
+
+ + + + + + + +

<%= message %>

+
+
+ + + + diff --git a/views/members.ejs b/views/members.ejs new file mode 100644 index 0000000..2a2e7e0 --- /dev/null +++ b/views/members.ejs @@ -0,0 +1,18 @@ + + + + 2537 Assignment 1 + + + + + +

Hello <%= username %>

+
+ +
<%= name %>
+
+
+ Logout + + diff --git a/views/notFound.ejs b/views/notFound.ejs new file mode 100755 index 0000000..8bfd493 --- /dev/null +++ b/views/notFound.ejs @@ -0,0 +1,13 @@ + + + + 2537 Assignment 1 + + + + + +

404 - Not found

+ Home + + diff --git a/views/signup.ejs b/views/signup.ejs new file mode 100755 index 0000000..601fa41 --- /dev/null +++ b/views/signup.ejs @@ -0,0 +1,40 @@ + + + + 2537 Assignment 1 + + + + + + + + + +
+ +
+
+ + + + + + + + + + +

<%= message %>

+
+
+ + + + \ No newline at end of file