fully functioning

This commit is contained in:
ak 2023-09-12 20:33:23 -07:00
parent 6dc16493e5
commit fc87aaa0c8
20 changed files with 2624 additions and 0 deletions

65
.gitignore vendored Normal file
View file

@ -0,0 +1,65 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
# deta-space internal files
.space
Spacefile

51
app.js Normal file
View file

@ -0,0 +1,51 @@
const express = require("express");
const path = require("path");
const session = require("express-session");
const passport = require("passport");
const passportInit = require("./passportInit.js");
const mongoose = require("mongoose");
require("dotenv").config();
// make app
const app = express();
// get db
const mongoDB = `mongodb+srv://${process.env.USER}:${process.env.PASS}@odin.eftl02o.mongodb.net/membersOnly?retryWrites=true&w=majority`;
// or throw error
main().catch((err) => console.log(err));
async function main() {
await mongoose.connect(mongoDB);
}
// set paths
app.use(
"/styles/css",
express.static(path.join(__dirname, "node_modules/bootstrap/dist/css"))
);
app.use(express.static(path.join(__dirname, "public")));
// view engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "ejs");
// passport setup
passportInit();
app.use(
session({
secret: process.env.SECRET_KEY,
resave: false,
saveUninitialized: true,
})
);
app.use(passport.initialize());
app.use(passport.session());
// URL parsing
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// routing
const router = require("./routes.js");
app.use("/", router);
module.exports = app;

90
bin/www Executable file
View file

@ -0,0 +1,90 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('express-inventory-application:server');
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

12
controllers/index.js Normal file
View file

@ -0,0 +1,12 @@
const Message = require("../models/message.js");
const asyncHandler = require("express-async-handler");
exports.index = asyncHandler(async (req, res, next) => {
// gets messages from database
const messages = await Message.find().lean().exec();
res.render("index", {
messages: messages,
user: req.user,
});
});

69
controllers/message.js Normal file
View file

@ -0,0 +1,69 @@
const Message = require("../models/message.js");
const asyncHandler = require("express-async-handler");
const { body, validationResult } = require("express-validator");
const { default: mongoose } = require("mongoose");
exports.index = asyncHandler(async (req, res, next) => {
// gets message from database
const message = await Message.findOne({ _id: req.params.message })
.lean()
.exec();
res.render("message", {
message: message,
user: req.user,
});
});
exports.message_create_get = (req, res, next) => {
res.render("createmessage");
};
exports.message_create_post = [
// Validate and sanitize title
body("title", "Please enter Insight title!")
.trim()
.isLength({ min: 1 })
.escape(),
// Validate and sanitize text
body("message", "Please enter your Insight!")
.trim()
.isLength({ min: 1 })
.escape(),
// Process request after validation and sanitization.
asyncHandler(async (req, res, next) => {
// Extract the validation errors from a request.
const errors = validationResult(req);
// if there are validation errors
if (!errors.isEmpty()) {
// Render the creation form again with sanitized values/error messages.
res.render("createmessage", {
errors: errors.array(),
});
return;
}
// Data from form is valid.
else {
// create new user with temporary password
let message = new Message({
title: req.body.title,
date: new Date(),
text: req.body.message,
author: req.user.name,
_id: new mongoose.Types.ObjectId(),
});
await message.save();
// saved. Redirect to home page.
res.redirect(`/`);
}
}),
];
exports.message_delete_get = asyncHandler(async (req, res, next) => {
// delete message from database
await Message.findByIdAndDelete(req.params.message);
res.redirect("/");
});

165
controllers/user.js Normal file
View file

@ -0,0 +1,165 @@
const User = require("../models/user.js");
const asyncHandler = require("express-async-handler");
const { body, validationResult } = require("express-validator");
const { default: mongoose } = require("mongoose");
const bcrypt = require("bcryptjs");
exports.user_create_get = (req, res, next) => {
res.render("createuser");
};
exports.user_create_post = [
// Validate and sanitize name
body("name", "Please enter your name!").trim().isLength({ min: 1 }).escape(),
// Validate and sanitize username
body("username", "Please enter your username!")
.trim()
.isLength({ min: 1 })
.escape(),
// Validate and sanitize password
body("password", "Please enter your password!")
.trim()
.isLength({ min: 1 })
.escape(),
// Validate and sanitize password confirmation
body("password_confirmation", "Please confirm your password!")
.trim()
.isLength({ min: 1 })
.custom((value, { req }) => {
if (value !== req.body.password)
throw new Error("Passwords don't match!");
else return value;
})
.escape(),
// Process request after validation and sanitization.
asyncHandler(async (req, res, next) => {
// Extract the validation errors from a request.
const errors = validationResult(req);
// if there are validation errors
if (!errors.isEmpty()) {
// Render the creation form again with sanitized values/error messages.
res.render("createuser", {
errors: errors.array(),
});
return;
}
// else data is valid
// create new user with hashed password
const user = new User({
name: req.body.name,
username: req.body.username,
password: await bcrypt.hash(req.body.password, 10),
isMember: false,
isAdmin: false,
_id: new mongoose.Types.ObjectId(),
});
await user.save();
// saved. Redirect to home page.
res.redirect(`/`);
}),
];
exports.user_signin_get = (req, res, next) => {
res.render("signin");
};
exports.user_initiation_get = (req, res, next) => {
res.render("initiation");
};
exports.user_initiation_post = [
// Validate and sanitize Secret Code
body("code", "Secret Code not entered!")
.trim()
.isLength({ min: 1 })
.custom((value) => {
if (value !== process.env.INITIATION_CODE)
throw new Error("Incorrect Secret Code");
else return value;
})
.escape(),
// Process request after validation and sanitization.
asyncHandler(async (req, res, next) => {
// Extract the validation errors from a request.
const errors = validationResult(req);
// if there are validation errors
if (!errors.isEmpty()) {
// Render the Initiation form again with sanitized values/error messages.
res.render("initiation", {
errors: errors.array(),
});
return;
}
// Data from form is valid.
else {
// find user from DB
const dbUser = await User.findById(req.user._id).lean().exec();
// make a "new" user with same fields except member set to true
const user = new User({
name: dbUser.name,
username: dbUser.username,
password: dbUser.password,
isMember: true,
isAdmin: false, //always false if not a member
_id: dbUser._id,
});
await User.findByIdAndUpdate(dbUser._id, user, {});
// saved. Redirect to home page.
res.redirect(`/`);
}
}),
];
exports.user_sudo_get = (req, res, next) => {
res.render("sudo");
};
exports.user_sudo_post = [
// Validate and sanitize Great Secret
body("greatsecret", "Answer not entered!")
.trim()
.isLength({ min: 1 })
.custom((value) => {
if (value != process.env.GREAT_SECRET)
throw new Error("You are incorrect, Initiate");
else return value;
})
.escape(),
// Process request after validation and sanitization.
asyncHandler(async (req, res, next) => {
// Extract the validation errors from a request.
const errors = validationResult(req);
// if there are validation errors
if (!errors.isEmpty()) {
// Render the Initiation form again with sanitized values/error messages.
res.render("sudo", {
errors: errors.array(),
});
return;
}
// else data from form is valid.
// find user from DB
const dbUser = await User.findById(req.user._id).lean().exec();
// make a "new" user with same fields except admin set to true
const user = new User({
name: dbUser.name,
username: dbUser.username,
password: dbUser.password,
isMember: true, // always true if admin
isAdmin: true,
_id: dbUser._id,
});
await User.findByIdAndUpdate(dbUser._id, user, {});
// saved. Redirect to home page.
res.redirect(`/`);
}),
];

20
models/message.js Normal file
View file

@ -0,0 +1,20 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const MessageSchema = new Schema({
title: { type: String, required: true },
date: { type: Date, required: true },
text: { type: String, required: true },
author: { type: String, required: true },
_id: { type: mongoose.ObjectId, required: true },
});
// Virtual for message URL
MessageSchema.virtual("url").get(function () {
// We don't use an arrow function as we'll need the this object
return `/msg/${this._id}`;
});
// Export model
module.exports = mongoose.model("Message", MessageSchema);

16
models/user.js Normal file
View file

@ -0,0 +1,16 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const UserSchema = new Schema({
name: { type: String, required: true },
username: { type: String, required: true },
password: { type: String, required: true },
isMember: { type: Boolean, required: true },
isAdmin: { type: Boolean, required: false },
_id: { type: mongoose.ObjectId, required: true },
});
// we will NOT be exposing any URLs for users
// Export model
module.exports = mongoose.model("User", UserSchema);

1712
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

24
package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "express-members-only",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"bcryptjs": "^2.4.3",
"bootstrap": "^5.3.1",
"debug": "~2.6.9",
"dotenv": "^16.3.1",
"ejs": "^3.1.9",
"ejs-lint": "^2.0.0",
"express": "^4.18.2",
"express-async-handler": "^1.2.0",
"express-session": "^1.17.3",
"express-validator": "^7.0.1",
"mongoose": "^7.5.1",
"passport": "^0.6.0",
"passport-local": "^1.0.0",
"passport-local-mongoose": "^8.0.0"
}
}

35
passportInit.js Normal file
View file

@ -0,0 +1,35 @@
const passport = require("passport");
const LocalStrategy = require("passport-local").Strategy;
const bcrypt = require("bcryptjs");
const User = require("./models/user.js");
const passportInit = () => {
passport.use(
"local",
new LocalStrategy(async function verify(username, password, done) {
const user = await User.findOne({ username: username });
if (!user) {
return done(null, false, { message: "Incorrect username" });
}
const match = await bcrypt.compare(password, user.password);
if (!match) {
return done(null, false, { message: "Incorrect password" });
}
return done(null, user);
})
);
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser(async (user, done) => {
try {
done(null, user);
} catch (err) {
done(err);
}
});
};
module.exports = passportInit;

View file

@ -0,0 +1,8 @@
@import url("https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@900&display=swap");
.logotext {
color: gold;
text-shadow: 1px 1px 2px black;
font-family: "Cinzel Decorative", cursive;
font-weight: bold;
}

34
routes.js Normal file
View file

@ -0,0 +1,34 @@
const express = require("express");
const router = express.Router();
const passport = require("passport");
const index_controller = require("./controllers/index.js");
const user_controller = require("./controllers/user.js");
const message_controller = require("./controllers/message.js");
// home page
router.get("/", index_controller.index);
// user sign in page
router.get("/user/signin", user_controller.user_signin_get);
router.post("/user/signin", passport.authenticate("local"), (req, res) => {
res.redirect("/");
});
// user sign up page
router.get("/user/create", user_controller.user_create_get);
router.post("/user/create", user_controller.user_create_post);
// initiate a member with secret password
router.get("/user/initiation", user_controller.user_initiation_get);
router.post("/user/initiation", user_controller.user_initiation_post);
// member enters all the names of God to become 33rd degree Illumined Grandmaster
router.get("/user/sudo", user_controller.user_sudo_get);
router.post("/user/sudo", user_controller.user_sudo_post);
// message functions
// create URL has to go first before :matchers
router.get("/msg/create", message_controller.message_create_get);
router.post("/msg/create", message_controller.message_create_post);
//
router.get("/msg/:message", message_controller.index);
router.get("/msg/:message/delete", message_controller.message_delete_get);
module.exports = router;

43
views/createmessage.ejs Normal file
View file

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<title>CVLT - New Insight</title>
<link rel="stylesheet" href="/styles/css/bootstrap.min.css" />
</head>
<body class="bg-dark text-light">
<div class="d-flex flex-column py-4 px-4 align-items-center">
<h1 class="mb-4">Create Insight</h1>
<form class="d-flex flex-column align-items-center w-50" method="post">
<div class="input-group mb-4" data-bs-theme="dark">
<span class="input-group-text" id="title">Title:</span>
<input
type="text"
class="form-control"
aria-label="insight title"
aria-describedby="title"
name="title"
/>
</div>
<div class="input-group mb-4" data-bs-theme="dark">
<span class="input-group-text" id="message">Insight:</span>
<textarea
class="form-control"
aria-label="insight"
aria-describedby="message"
name="message"
></textarea>
</div>
<button type="submit" class="btn btn-outline-light px-4 py-2">
Submit Insight
</button>
</form>
</div>
<footer
class="position-fixed bottom-0 border-light border-top w-100 py-2 d-flex justify-content-center bg-dark"
>
<a href="/" type="button" class="btn btn-outline-light px-4 py-1">
Back to Home
</a>
</footer>
</body>
</html>

66
views/createuser.ejs Normal file
View file

@ -0,0 +1,66 @@
<!DOCTYPE html>
<html>
<head>
<title>CVLT - New User</title>
<link rel="stylesheet" href="/styles/css/bootstrap.min.css" />
</head>
<body class="bg-dark text-light">
<div class="d-flex flex-column py-4 px-4 align-items-center">
<h1 class="mb-4">New User</h1>
<form class="d-flex flex-column align-items-center w-50" method="post">
<div class="input-group mb-4" data-bs-theme="dark">
<span class="input-group-text" id="name">Full Name:</span>
<input
type="text"
class="form-control"
aria-label="name"
aria-describedby="name"
name="name"
/>
</div>
<div class="input-group mb-4" data-bs-theme="dark">
<span class="input-group-text" id="username">Username:</span>
<input
type="text"
class="form-control"
aria-label="username"
aria-describedby="username"
name="username"
/>
</div>
<div class="input-group mb-4" data-bs-theme="dark">
<span class="input-group-text" id="password">Password:</span>
<input
type="password"
class="form-control"
aria-label="password"
aria-describedby="password"
name="password"
/>
</div>
<div class="input-group mb-4" data-bs-theme="dark">
<span class="input-group-text" id="password_confirmation"
>Confirm Password:</span
>
<input
type="password"
class="form-control"
aria-label="confirm password"
aria-describedby="password_confirmation"
name="password_confirmation"
/>
</div>
<button type="submit" class="btn btn-outline-light px-4 py-2">
Submit
</button>
</form>
</div>
<footer
class="position-fixed bottom-0 border-light border-top w-100 py-2 d-flex justify-content-center bg-dark"
>
<a href="/" type="button" class="btn btn-outline-light px-4 py-1">
Back to Home
</a>
</footer>
</body>
</html>

61
views/index.ejs Normal file
View file

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html>
<head>
<title>CVLT</title>
<link rel="stylesheet" href="/styles/css/bootstrap.min.css" />
<link rel="stylesheet" href="/stylesheets/index.css" />
</head>
<body class="bg-dark text-light min-vh-100">
<div class="d-flex flex-column py-4 px-4 align-items-center">
<h1 class="logotext mb-4">CVLT</h1>
<p class="fs-3">Insights:</p>
<% messages.forEach(message => { %>
<a
href="/msg/<%= message._id %>"
type="button"
class="btn btn-outline-light px-4"
>
<h3 class="pt-2"><%= message.title %></h3>
<% if (user && user.isMember) { %>
<p>Penned: <%= message.date %> by <%= message.author %></p>
<% } %>
<p><%= message.text %></p>
</a>
<div class="pb-4"></div>
<% }) %> <% if (user && user.isMember) { %>
<a href="/msg/create" type="button" class="btn btn-success px-4">
+ Post Message
</a>
<% } %>
</div>
<% if (user && user.isMember === true && user.isAdmin === false) { %>
<footer
class="position-fixed bottom-0 border-light border-top w-100 py-2 d-flex justify-content-center bg-dark"
>
<a href="/user/sudo" type="button" class="btn btn-light px-4">
Join the Light
</a>
</footer>
<% } %> <% if (user && user.isMember === false) { %>
<footer
class="position-fixed bottom-0 border-light border-top w-100 py-2 d-flex justify-content-center bg-dark"
>
<a
href="/user/initiation"
type="button"
class="btn btn-outline-light px-4"
>
Become an Initiate
</a>
</footer>
<% } %> <% if (!user) { %>
<footer
class="position-fixed bottom-0 border-light border-top w-100 py-2 d-flex justify-content-center bg-dark"
>
<a href="/user/signin" type="button" class="btn btn-outline-light px-4">
Sign In
</a>
</footer>
<% } %>
</body>
</html>

34
views/initiation.ejs Normal file
View file

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<title>CVLT - Initiation</title>
<link rel="stylesheet" href="/styles/css/bootstrap.min.css" />
</head>
<body class="bg-dark text-light">
<div class="d-flex flex-column py-4 px-4 align-items-center">
<h1 class="mb-4">Initiation</h1>
<form class="d-flex flex-column align-items-center w-50" method="post">
<div class="input-group mb-4" data-bs-theme="dark">
<span class="input-group-text" id="code">Enter the Secret Code:</span>
<input
type="text"
class="form-control"
aria-label="secret code"
aria-describedby="code"
name="code"
/>
</div>
<button type="submit" class="btn btn-success px-4 py-2">
&check; I wish to be an Initiate
</button>
</form>
</div>
<footer
class="position-fixed bottom-0 border-light border-top w-100 py-2 d-flex justify-content-center bg-dark"
>
<a href="/" type="button" class="btn btn-outline-light px-4 py-1">
Back to Home
</a>
</footer>
</body>
</html>

34
views/message.ejs Normal file
View file

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<title>CVLT - <%= message.title %></title>
<link rel="stylesheet" href="/styles/css/bootstrap.min.css" />
</head>
<body class="bg-dark text-light">
<div class="d-flex flex-column py-4 px-4 align-items-center">
<h1 class="mb-0"><%= message.title %></h1>
<% if (user && user.isMember) { %>
<p>Penned: <%= message.date %> by <%= message.author %></p>
<% } %>
<div class="pb-4"></div>
<p><%= message.text %></p>
<div class="pb-4"></div>
<% if (user && user.isAdmin) { %>
<a
href="/msg/<%= message._id %>/delete"
type="button"
class="btn btn-danger px-4"
>
&#x1F5D9; Countermand this Insight
</a>
<% } %>
</div>
<footer
class="position-fixed bottom-0 border-light border-top w-100 py-2 d-flex justify-content-center"
>
<a href="/" type="button" class="btn btn-outline-light px-4 py-1">
Back to Home
</a>
</footer>
</body>
</html>

51
views/signin.ejs Normal file
View file

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<title>CVLT - Sign In</title>
<link rel="stylesheet" href="/styles/css/bootstrap.min.css" />
</head>
<body class="bg-dark text-light">
<div class="d-flex flex-column py-4 px-4 align-items-center">
<h1 class="mb-4">Sign In</h1>
<form class="d-flex flex-column align-items-center w-50" method="post">
<div class="input-group mb-4" data-bs-theme="dark">
<span class="input-group-text" id="username-aria">Username:</span>
<input
type="text"
class="form-control"
aria-label="username"
aria-describedby="username-aria"
name="username"
/>
</div>
<div class="input-group mb-4" data-bs-theme="dark">
<span class="input-group-text" id="password-aria">Password:</span>
<input
type="password"
class="form-control"
aria-label="password"
aria-describedby="password-aria"
name="password"
/>
</div>
<button type="submit" class="btn btn-outline-light px-4 py-2">
Submit
</button>
</form>
<div class="pb-4"></div>
<div class="d-flex flex-column align-items-center">
<p class="mb-2">Don't have an account?</p>
<a href="/user/create" type="button" class="btn btn-success px-4">
Join Us!
</a>
</div>
</div>
<footer
class="position-fixed bottom-0 border-light border-top w-100 py-2 d-flex justify-content-center bg-dark"
>
<a href="/" type="button" class="btn btn-outline-light px-4 py-1">
Back to Home
</a>
</footer>
</body>
</html>

34
views/sudo.ejs Normal file
View file

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<title>CVLT - Ascension</title>
<link rel="stylesheet" href="/styles/css/bootstrap.min.css" />
</head>
<body class="bg-dark text-light">
<div class="d-flex flex-column py-4 px-4 align-items-center">
<h1 class="mb-4">Ascension</h1>
<form class="d-flex flex-column align-items-center w-50" method="post">
<div class="input-group mb-4" data-bs-theme="dark">
<span class="input-group-text" id="greatsecret-label"
>Life, the Universe and Everything:</span
>
<input
type="text"
class="form-control"
aria-label="great secret"
aria-describedby="greatsecret-label"
name="greatsecret"
/>
</div>
<button type="submit" class="btn btn-light px-4 py-2">ASCEND</button>
</form>
</div>
<footer
class="position-fixed bottom-0 border-light border-top w-100 py-2 d-flex justify-content-center bg-dark"
>
<a href="/" type="button" class="btn btn-outline-light px-4 py-1">
Back to Home
</a>
</footer>
</body>
</html>