commit f219a2fe41cce1262085d40e6a7913eb8943a261 Author: Alexander Bass Date: Thu Apr 13 15:21:17 2023 -0400 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8af97fe --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +data.db +.sass-cache +node_modules +package-lock.json +sessions +build \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f1dad7a --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# Web Forum Experiment + +This is a weekend project I made to gain a better understanding of databases. +It's a simple message board forum where users can create threads and messages within those threads. +It is written in JavaScript using the [Express framework](https://expressjs.com/), with SQLite for a database. +I don't intend to develop this any further. + +## Installing Dependencies +``` +npm install +``` +## Running +If you have GNU make installed: +``` +make dev +``` +or if not: +``` +npx nodemon --ignore ./sessions/ -e js,mjs,json,scss index.js +``` +Then you can go to `localhost:3333` in your browser to view the forum. + + +![Screenshot](screenshot.png) \ No newline at end of file diff --git a/buildstyle.js b/buildstyle.js new file mode 100644 index 0000000..7cf6572 --- /dev/null +++ b/buildstyle.js @@ -0,0 +1,8 @@ +import sass from "sass"; +import { writeFileSync, } from "fs"; + +const style = sass.compile("./css/main.scss").css; + +writeFileSync("static/out.css", style, err => { + console.log(err); +}); \ No newline at end of file diff --git a/css/main.scss b/css/main.scss new file mode 100644 index 0000000..84c9b83 --- /dev/null +++ b/css/main.scss @@ -0,0 +1,160 @@ +@import 'variables'; + +$asideWidth: 150px; +$mainWidth: 1200px; +$borderSize: 5px; +$totalWidth: $asideWidth+$mainWidth+(4*$borderSize); + +body { + margin: 0; + background-color: var(--background-color); + background-image: url("/background.png"); + background-repeat: repeat; + background-size: 100px 100px; + font-family: $content-font; +} + + +div#actionBarRight { + margin-left: auto; +} + +* { + box-sizing: border-box; +} + +div#actionBar { + margin: 0px; + padding: 0px; + display: flex; + border: 5px ridge var(--fourth-color); + border-bottom: none; + background-color: lightgray; + font-family: sans-serif; + align-items: flex-start; + + div { + a { + font-size: large; + text-decoration: none; + color: black; + display: block; + padding-inline: 20px; + padding-block: 8px; + + } + + &:hover { + background-color: var(--fourth-color); + } + } +} + +div#content { + margin-inline: auto; + max-width: $mainWidth; +} + +a { + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +header { + user-select: none; + font-weight: 600; + text-align: center; + font-size: 4rem; + background: linear-gradient(red, orange); + background-clip: text; + -webkit-text-fill-color: transparent; + -webkit-background-clip: text; + -webkit-text-stroke: 1px red; + margin: 10px; +} + +header a:hover { + text-decoration: none; +} + +#messageContainer:first-child .messageMeta { + // background: linear-gradient(rgba(255, 0, 0, 1), rgba(255, 166, 0, 1)); + padding-block: 10px; + font-size: 15px; + text-decoration: none; + text-decoration-style: none; +} + +; + +.message { + background-color: var(--tertiary-color); + // padding: 10px; + margin-bottom: 5px; + padding: 0 0 10px 0; + margin-top: 15px; + + .messageMeta { + font-family: $clean-font; + user-select: none; + background: rgb(190, 190, 190); + text-decoration: underline; + text-decoration-style: dotted; + font-size: 13px; + padding-block: 5px; + padding-left: 5px; + + } + + .messageContent { + display: flex; + padding-block: 10px; + + + .userInfo { + font-family: $clean-font; + + display: inline-block; + margin-right: 16px; + max-width: 170px; + font-size: 15px; + border-right: 2px ridge gainsboro; + text-align: center; + + #userName { + font-weight: bold; + font-size: 20px; + margin-bottom: 5px; + } + + } + + .messageBody { + width: 100%; + + h3 { + margin-top: 0px; + margin-bottom: 2px; + font-size: 20px; + } + + p { + margin: 0px 0px 0px 0px; + word-wrap: break-word; + } + } + } + + + +} + +main { + background-color: var(--main-color); + border: 5px ridge var(--border-color); + padding-block: 10px; + padding-inline: 20px; +} \ No newline at end of file diff --git a/css/variables.scss b/css/variables.scss new file mode 100644 index 0000000..e1a0642 --- /dev/null +++ b/css/variables.scss @@ -0,0 +1,17 @@ +// Light mode colors +:root { + --faded-text-color: #303030; + --background-color: #6d695c; + --main-color: #E0E0E0; + --border-color: white; + --tertiary-color: lightgray; + --fourth-color: darkgray; + --fun-color: #4CAF50; + --angry-color: red; + --text-color: #242424; + --darker-color: #ebebeb; +} + + +$clean-font: sans-serif; +$content-font: Charter, 'Bitstream Charter', 'Sitka Text', Cambria, serif; \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..c8e2383 --- /dev/null +++ b/index.js @@ -0,0 +1,155 @@ +import bcrypt from "bcrypt"; +import sqlite3 from "sqlite3"; +import { open } from "sqlite"; +import express from 'express'; +import session from "express-session"; + +import sfs from "session-file-store"; +let FileStore = sfs(session); +const app = express(); +const PORT = 3333; +const SALT_COUNT = 12; + + + +const dbPromise = open({ + filename: "data.db", + driver: sqlite3.Database, +}); + +var fileStoreOptions = {}; + +app.set('view engine', 'ejs'); +app.use( + express.urlencoded({ extended: true }), + express.static('static'), + session({ + store: new FileStore(fileStoreOptions), + secret: "process.env.SESSION_SECRET", cookie: { maxAge: 60000 }, + resave: true, + saveUninitialized: true + }), + +); + +app.get('/thread/:threadId/', async (req, res) => { + try { + const db = await dbPromise; + const threadId = req.params.threadId; + const thread = await db.get('SELECT * FROM Thread WHERE Thread.id=?', threadId); + if (!thread) throw ""; + + const messages = await db.all('SELECT Message.*, User.creationdate as accountCreated, User.username FROM Message LEFT JOIN User WHERE Message.author=User.id AND Message.thread=?', threadId); + res.render('pages/thread', { threadinfo: thread, messages: messages, session: req.session }); + } catch { + res.redirect('back'); + } +}); + + +app.get('/', async (req, res) => { + const db = await dbPromise; + const threads = await db.all('SELECT * FROM Thread'); + res.render('pages/index', { + threads: threads, + session: req.session, + }); +}); + +app.get("/login", async (req, res) => { + if (req.session.logged_in) { + return res.redirect('/'); + } + res.render('pages/login'); +}); + +app.get("/logout", async (req, res) => { + req.session.logged_in = false; + req.session.user_id = null; + res.redirect("/"); +}); + +app.post("/login", async (req, res) => { + try { + const db = await dbPromise; + const { username, password } = req.body; + if (!username || !password) { return res.render('pages/login', { error: "Username or password not provided" }); } + const user = await db.get(`SELECT password,id FROM User WHERE username = ?`, username); + if (!user) return res.render('pages/login', { error: "Incorrect username or password" }); + const compared = await bcrypt.compare(password, user.password); + if (compared) { + req.session.logged_in = true; + req.session.user_id = user.id; + res.redirect("/"); + } else { + return res.render('pages/login', { error: "Incorrect username or password" }); + } + } catch (err) { + console.log(err); + return res.render('pages/login', { error: "An error ocurred." }); + } +}); + +app.post("/register", async (req, res) => { + try { + const db = await dbPromise; + const { username, password } = req.body; + if (!username || !password) { return res.render('pages/login', { error: "Username or password not provided" }); } + if (username.length < 4) { return res.render('pages/login', { error: "Username must be at least 4 characters long" }); } + if (password.length < 6) { return res.render('pages/login', { error: "Password must be at least 6 characters long" }); } + const passwordHash = await bcrypt.hash(password, SALT_COUNT); + const result = await db.run('INSERT INTO User (username,password,creationdate) VALUES (?,?,unixepoch())', username, passwordHash); + const userID = result.lastID; + req.session.user_id = userID; + req.session.logged_in = true; + res.redirect("/"); + } catch (err) { + console.log(err); + if (err.errno) { + switch (err.errno) { + case 19: + return res.render('pages/login', { error: "Username already exists" }); + break; + + } + } + return res.render('pages/login', { error: "An error ocurred." }); + } +}); + +app.post("/message/:threadId/", async (req, res) => { + try { + const threadId = req.params.threadId; + const db = await dbPromise; + const thread = await db.get('SELECT * FROM Thread WHERE Thread.id=?', threadId); + const messageText = req.body.messageText; + const subjectText = req.body.subjectText; + const userID = req.session.user_id; + await db.run("INSERT INTO Message (text,author,subject,creationdate,thread) VALUES (?,?,?,unixepoch(),?);", messageText, userID, subjectText, threadId); + } catch (err) { + console.error(err); + } + res.redirect('back'); +}); + +app.post("/newthread", async (req, res) => { + try { + const db = await dbPromise; + const threadName = req.body.threadName; + const threadDesc = req.body.threadDesc; + const userID = req.session.user_id; + await db.run("INSERT INTO Thread (name,author,description,creationdate) VALUES (?,?,?,unixepoch());", threadName, userID, threadDesc); + } catch (err) { + console.error(err); + } + res.redirect("/"); +}); + +const setup = async () => { + const db = await dbPromise; + await db.migrate(); + app.listen(PORT, () => { + console.log("listening on http://localhost:" + PORT); + }); +}; +setup(); \ No newline at end of file diff --git a/makefile b/makefile new file mode 100644 index 0000000..57a44e4 --- /dev/null +++ b/makefile @@ -0,0 +1,17 @@ +dev: + npx nodemon --ignore ./sessions/ -e js,mjs,json,scss index.js + +build: style + - mkdir build + cp -r static build/ + cp -r views build/ + cp index.js build/index.js + +clean: + -rm -r .sass-cache + -rm -r sessions + -rm -r build + -rm data.db + +style: + node buildstyle.js \ No newline at end of file diff --git a/migrations/001-initial-schema.sql b/migrations/001-initial-schema.sql new file mode 100644 index 0000000..6c0d8d6 --- /dev/null +++ b/migrations/001-initial-schema.sql @@ -0,0 +1,32 @@ +-- Up + +CREATE TABLE Thread( + id INTEGER PRIMARY KEY, + author INTEGER NOT NULL, + name STRING NOT NULL, + creationdate INTEGER, + description STRING +); + +CREATE TABLE Message ( + id INTEGER PRIMARY KEY, + text STRING NOT NULL, + subject STRING, + creationdate INTEGER NOT NULL, + thread INTEGER NOT NULL, + author INTEGER NOT NULL +); + +CREATE TABLE User ( + id INTEGER PRIMARY KEY, + username STRING UNIQUE NOT NULL, + password STRING NOT NULL, + creationdate INTEGER NOT NULL +); + + +-- Down + +DROP TABLE Message; +DROP TABLE User; +DROP TABLE Thread; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..dff54cf --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "forum", + "version": "0.0.1", + "type": "module", + "main": "index.js", + "author": "Alexander Bass", + "license": "GPL-3.0", + "dependencies": { + "bcrypt": "^5.1.0", + "cookie-parser": "^1.4.6", + "dotenv": "^16.0.3", + "ejs": "^3.1.9", + "esm": "^3.2.25", + "express": "^4.18.2", + "express-session": "^1.17.3", + "session-file-store": "^1.5.0", + "sqlite": "^4.1.2", + "sqlite3": "^5.1.6" + }, + "devDependencies": { + "nodemon": "^2.0.22", + "sass": "^1.61.0" + } +} \ No newline at end of file diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..ddb7904 Binary files /dev/null and b/screenshot.png differ diff --git a/static/background.png b/static/background.png new file mode 100644 index 0000000..8ca956b Binary files /dev/null and b/static/background.png differ diff --git a/static/out.css b/static/out.css new file mode 100644 index 0000000..8e7ed8c --- /dev/null +++ b/static/out.css @@ -0,0 +1,158 @@ +:root { + --faded-text-color: #303030; + --background-color: #6d695c; + --main-color: #E0E0E0; + --border-color: white; + --tertiary-color: lightgray; + --fourth-color: darkgray; + --fun-color: #4CAF50; + --angry-color: red; + --text-color: #242424; + --link-color: #2980b9; + --link-visited-color: #9b59b6; + --darker-color: #ebebeb; +} + +body { + margin: 0; + background-color: var(--background-color); + background-image: url("/background.png"); + background-repeat: repeat; + background-size: 100px 100px; + font-family: Charter, "Bitstream Charter", "Sitka Text", Cambria, serif; +} + +aside { + width: 150px; + margin-right: 10px; +} + +footer { + text-align: center; + background: linear-gradient(red, orange); + background-clip: text; + -webkit-text-fill-color: transparent; + -webkit-background-clip: text; + font-size: large; +} + +div#actionBarRight { + margin-left: auto; +} + +* { + box-sizing: border-box; +} + +div#actionBar { + margin: 0px; + padding: 0px; + display: flex; + border: 5px ridge var(--fourth-color); + border-bottom: none; + background-color: lightgray; + font-family: sans-serif; + align-items: flex-start; +} +div#actionBar div a { + font-size: large; + text-decoration: none; + color: black; + display: block; + padding-inline: 20px; + padding-block: 8px; +} +div#actionBar div:hover { + background-color: var(--fourth-color); +} + +div#content { + margin-inline: auto; + max-width: 1200px; +} + +a { + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +header { + user-select: none; + font-weight: 600; + text-align: center; + font-size: 4rem; + background: linear-gradient(red, orange); + background-clip: text; + -webkit-text-fill-color: transparent; + -webkit-background-clip: text; + -webkit-text-stroke: 1px red; + margin: 10px; +} + +header a:hover { + text-decoration: none; +} + +#messageContainer:first-child .messageMeta { + padding-block: 10px; + font-size: 15px; + text-decoration: none; + text-decoration-style: none; +} + +.message { + background-color: var(--tertiary-color); + margin-bottom: 5px; + padding: 0 0 10px 0; + margin-top: 15px; +} +.message .messageMeta { + font-family: sans-serif; + user-select: none; + background: rgb(190, 190, 190); + text-decoration: underline; + text-decoration-style: dotted; + font-size: 13px; + padding-block: 5px; + padding-left: 5px; +} +.message .messageContent { + display: flex; + padding-block: 10px; +} +.message .messageContent .userInfo { + font-family: sans-serif; + display: inline-block; + margin-right: 16px; + max-width: 170px; + font-size: 15px; + border-right: 2px ridge gainsboro; + text-align: center; +} +.message .messageContent .userInfo #userName { + font-weight: bold; + font-size: 20px; + margin-bottom: 5px; +} +.message .messageContent .messageBody { + width: 100%; +} +.message .messageContent .messageBody h3 { + margin-top: 0px; + margin-bottom: 2px; + font-size: 20px; +} +.message .messageContent .messageBody p { + margin: 0px 0px 0px 0px; + word-wrap: break-word; +} + +main { + background-color: var(--main-color); + border: 5px ridge var(--border-color); + padding-block: 10px; + padding-inline: 20px; +} \ No newline at end of file diff --git a/views/pages/board.ejs b/views/pages/board.ejs new file mode 100644 index 0000000..5ca292b --- /dev/null +++ b/views/pages/board.ejs @@ -0,0 +1,46 @@ + + + + <%- include('../partials/head'); %> + + + +<%- include('../partials/header') -%> +
+ <%- include('../partials/top'); %> +
+ + <% + if(locals.login){ + %> +
You are logged in
+ <% } else {; %> +
You are not logged in
+ <% } %> + <% messages.forEach((m) => { %> + + <%= m.username %>: + <%= m.text %> +
+ <% }); %> + + <% + if(locals.login){ + %> +
+ + + + + +
+ <% }; %> + +
+
+ + + + diff --git a/views/pages/index.ejs b/views/pages/index.ejs new file mode 100644 index 0000000..037fb5d --- /dev/null +++ b/views/pages/index.ejs @@ -0,0 +1,34 @@ + + + + <%- include('../partials/head'); %> + + + +<%- include('../partials/header') -%> +
+ + <%- include('../partials/top'); %> + +
+ + <% threads.forEach((t) => { %> +

<%= t.name %>

+ <%= t.description %> +
+ <%- include('../partials/format_date',{format_date:t.creationdate}); %> +
+ <% }); %> + + + + <%- include('../partials/create_thread'); %> + +
+
+ + + + diff --git a/views/pages/login.ejs b/views/pages/login.ejs new file mode 100644 index 0000000..08ba686 --- /dev/null +++ b/views/pages/login.ejs @@ -0,0 +1,53 @@ + + + + <%- include('../partials/head'); %> + + + +
+ <%- include('../partials/header'); %> +
+
+ <%- include('../partials/top'); %> +
+ <% + if(locals.error){ + %> +
Could not login: <%= error %>
+ <% }; %> + <% + if(locals.success){ + %> +
<%= success %>
+ <% }; %> +

Login

+
+ + + + + + +
+ +
+
+

Register

+
+ + + + + + +
+
+
+ + + + + \ No newline at end of file diff --git a/views/pages/thread.ejs b/views/pages/thread.ejs new file mode 100644 index 0000000..12524eb --- /dev/null +++ b/views/pages/thread.ejs @@ -0,0 +1,42 @@ + + + + <%- include('../partials/head'); %> + + + +<%- include('../partials/header') -%> +
+ + <%- include('../partials/top'); %> + +
+

<%= threadinfo.name %>

+

<%= threadinfo.description %> <%- include('../partials/format_date',{format_date:threadinfo.creationdate}); %>

+ <% messages.forEach((m) => { %> +
+ <%- include('../partials/message',{m:m}); %> +
+ <% }); %> + <% + if(locals.session.logged_in){ + %> +
+ + + + + +
+ <% } else {; %> +

You must login to post a message.

+ <% } %> + +
+
+ + + + diff --git a/views/partials/create_thread.ejs b/views/partials/create_thread.ejs new file mode 100644 index 0000000..b9963af --- /dev/null +++ b/views/partials/create_thread.ejs @@ -0,0 +1,13 @@ +<% +if(locals.session.logged_in){ +%> +
+ + + + + +
+<% } else {; %> +

You must login to create a thread.

+<% } %> \ No newline at end of file diff --git a/views/partials/footer.ejs b/views/partials/footer.ejs new file mode 100644 index 0000000..e69de29 diff --git a/views/partials/format_date.ejs b/views/partials/format_date.ejs new file mode 100644 index 0000000..e12c22d --- /dev/null +++ b/views/partials/format_date.ejs @@ -0,0 +1,7 @@ +<% const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; %> +<% +const date = new Date(format_date*1000); +const year = date.getFullYear(); +const month = date.getMonth(); +const day = date.getDate(); +%><%= months[month] %> <%= `${day}` %> <%= year %> \ No newline at end of file diff --git a/views/partials/head.ejs b/views/partials/head.ejs new file mode 100644 index 0000000..3383fac --- /dev/null +++ b/views/partials/head.ejs @@ -0,0 +1,3 @@ + +ForumExperiment.Example + diff --git a/views/partials/header.ejs b/views/partials/header.ejs new file mode 100644 index 0000000..060e7a4 --- /dev/null +++ b/views/partials/header.ejs @@ -0,0 +1,3 @@ +
+ Forum Experiment +
\ No newline at end of file diff --git a/views/partials/message.ejs b/views/partials/message.ejs new file mode 100644 index 0000000..8905ce4 --- /dev/null +++ b/views/partials/message.ejs @@ -0,0 +1,23 @@ +
+
+ +
<%- include('./format_date',{format_date:m.creationdate}); %>
+
+
+
+
+ <%= m.username %> +
+ + Member since <%- include('./format_date',{format_date:m.accountCreated}); %> +
+
+ <% if (m.subject){ %> +

<%= m.subject %>

+ <% }; %> +

+ <%= m.text %> +

+
+
+
\ No newline at end of file diff --git a/views/partials/top.ejs b/views/partials/top.ejs new file mode 100644 index 0000000..c02c2c9 --- /dev/null +++ b/views/partials/top.ejs @@ -0,0 +1,9 @@ +
+
Home
+ <% if(locals.session?.logged_in){ %> + + <% } else { %> + + <% }; %> + +
\ No newline at end of file