Ability to post in Forum

This commit is contained in:
metacryst
2025-11-25 10:17:01 -06:00
parent 7f4a9a8b18
commit 89702efa3a
27 changed files with 494 additions and 254 deletions

View File

@@ -28,6 +28,21 @@ export default class AuthHandler {
}
}
getProfile(req, res) {
const token = req.cookies.auth_token;
if (!token) return res.status(401).send({ error: "No auth token" });
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
const email = payload.email;
const user = db.members.getByEmail(email);
res.send({ email: user.email, name: user.firstName + " " + user.lastName });
} catch (err) {
res.status(401).send({ error: "Invalid token" });
}
}
async login(req, res) {
const { email, password } = req.body;
let foundUser = global.db.members.getByEmail(email)
@@ -40,7 +55,8 @@ export default class AuthHandler {
if (!valid) {
res.status(400).json({ error: 'Incorrect password.' });
} else {
const payload = { id: foundUser.id };
const payload = { email: foundUser.email };
console.log(payload)
const secret = process.env.JWT_SECRET;
const options = { expiresIn: "2h" };
const token = jwt.sign(payload, secret, options);

View File

@@ -5,16 +5,19 @@ import QuillDB from "../_/quilldb.js"
import Titles from "./model/Titles.js"
import Members from './model/Members.js'
import Tokens from './model/Tokens.js'
import Posts from "./model/Posts.js"
export default class Database {
titles = new Titles()
members = new Members()
tokens = new Tokens()
posts = new Posts()
fromID = {
"HY": this.titles,
"MEMBER": this.members,
"TOKEN": this.tokens
"TOKEN": this.tokens,
"POST": this.posts
}
constructor() {
@@ -39,7 +42,7 @@ export default class Database {
try {
let collection = this.fromID[type]
if(collection) {
collection.save(node)
collection.save(node, id)
} else {
throw new Error("Type does not exist for node: ", id)
}
@@ -63,12 +66,14 @@ export default class Database {
let arrs = [
this.titles.entries,
this.members.entries,
this.tokens.entries
this.tokens.entries,
this.posts.entries
]
let ids = [
Object.entries(this.titles.ids),
Object.entries(this.members.ids),
Object.entries(this.tokens.ids),
Object.entries(this.posts.ids),
]
for(let i=0; i<arrs.length; i++) {
let arr = arrs[i]

View File

@@ -26,7 +26,7 @@ export default class Members extends OrderedObject {
isHashed = (s) => {return s.startsWith("$argon2")}
save(member) {
let id = `MEMBER-${member.email}`
let id = `MEMBER-${this.entries.length+1}`
let result = this.schema.safeParse(member)
if(result.success) {
try {
@@ -45,29 +45,31 @@ export default class Members extends OrderedObject {
newMember.tokenUsed = tokenID
const hash = await argon2.hash(newMember.password);
newMember.password = hash
newMember.joined = this.currentTime()
newMember.joined = global.currentTime()
this.save(newMember)
}
getByEmail(email) {
return super.get(`MEMBER-${email}`)
get(id) {
return this.entries[this.ids[id]]
}
currentTime() {
const now = new Date();
getByEmail(email) {
for(let i=0; i<this.entries.length; i++) {
if(this.entries[i].email === email) {
return this.entries[i]
}
}
return null
}
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
const year = now.getFullYear();
let hours = now.getHours();
const ampm = hours >= 12 ? "pm" : "am";
hours = hours % 12 || 12; // convert to 12-hour format
const minutes = String(now.getMinutes()).padStart(2, "0");
const seconds = String(now.getSeconds()).padStart(2, "0");
const ms = String(now.getMilliseconds()).padStart(4, "0"); // 4-digit like "5838"
return `${month}.${day}.${year}-${hours}:${minutes}:${seconds}${ms}${ampm}`;
getIDFromEmail(email) {
let index = 0
for(let i=0; i<this.entries.length; i++) {
if(this.entries[i].email === email) {
index = i
break
}
}
return Object.entries(this.ids)[index][0]
}
}

View File

@@ -1,9 +1,10 @@
export default class OrderedObject {
entries = []
ids = {}
indexes = []
add(id, data) {
if(this.get(id)) {
if(this.ids[id]) {
console.error(`Can't add item ${id}: id already exists`)
throw new global.ServerError(400, `Member with this email already exists`)
}
@@ -23,12 +24,4 @@ export default class OrderedObject {
return this.entries[this.ids[key]]
}
}
get(key) {
if (typeof key === "number") {
return this.entries[key]
} else {
return this.entries[this.ids[key]]
}
}
}

56
server/db/model/Posts.js Normal file
View File

@@ -0,0 +1,56 @@
import OrderedObject from "./OrderedObject.js"
const { z } = require("zod")
export default class Posts extends OrderedObject {
schema = z.object({
text: z.string(),
time: z.string(),
sentBy: z.string()
})
makeID(forum, number) {
return `POST-${forum}-${number}`
}
save(post, id) {
let result = this.schema.safeParse(post)
if(result.success) {
try {
super.add(id, post)
} catch(e) {
console.error(e)
throw e
}
} else {
console.error("Failed parsing member: ", result.error)
throw new global.ServerError(400, "Invalid Member Data!: ");
}
}
get(forum, number) {
let result = []
let limit = Math.min(number, this.entries.length)
for(let i=1; i<=limit; i++) {
let id = this.makeID(forum, i)
let post = this.entries[this.ids[id]]
let {firstName, lastName} = global.db.members.get(post.sentBy)
let seededObj = {
...post
}
seededObj.sentByID = post.sentBy
seededObj.sentBy = firstName + " " + lastName
result.push(seededObj)
}
return result
}
async add(text, forum, userEmail) {
let newPost = {}
newPost.text = text
newPost.sentBy = db.members.getIDFromEmail(userEmail)
newPost.time = global.currentTime()
let idNumber = this.entries.length+1
super.add(this.makeID(forum, idNumber), newPost)
}
}

View File

@@ -4,7 +4,6 @@ export default class Titles extends OrderedObject {
save(newTitle) {
let id = `HY-${this.entries.length+1}`
console.log(id)
if(this.validate(id, newTitle)) {
try {
super.add(id, newTitle)

View File

@@ -36,6 +36,6 @@ export default class Tokens extends OrderedObject {
}
get(uuid) {
return super.get(`TOKEN-${uuid}`)
return this.entries[this.ids[`TOKEN-${uuid}`]]
}
}

View File

@@ -1,12 +0,0 @@
import { broadcast } from './ws.js';
const handlers = {
updateLocation(req, res) {
const { name, latitude, longitude, timestamp } = req.body;
console.log(`Received location: (${name}, ${latitude}, ${longitude}) at ${timestamp}`);
broadcast("update-location", { name, latitude, longitude, timestamp });
res.json({ success: true });
}
}
export default handlers;

View File

@@ -7,10 +7,10 @@ const chalk = require('chalk');
const moment = require('moment');
const path = require('path');
import { initWebSocket } from './ws.js'
import "./util.js"
import Socket from './ws/ws.js'
import Database from "./db/db.js"
import AuthHandler from './auth.js';
import handlers from "./handlers.js";
class Server {
db;
@@ -21,6 +21,7 @@ class Server {
registerRoutes(router) {
// router.post('/api/location', handlers.updateLocation)
router.post('/login', this.auth.login)
router.get('/profile', this.auth.getProfile)
router.get('/signout', this.auth.logout)
router.get('/signup', this.verifyToken, this.get)
router.post('/signup', this.verifyToken, this.newUserSubmission)
@@ -171,8 +172,13 @@ class Server {
this.registerRoutes(router)
app.use('/', router);
setInterval(() => {
console.log("saving db")
global.db.saveData()
}, 5000)
const server = http.createServer(app);
initWebSocket(server);
global.Socket = new Socket(server);
const PORT = 3003;
server.listen(PORT, () => {
console.logclean("\n")
@@ -219,11 +225,4 @@ console.logclean = function (...args) {
// _log.call(console, `[${location}]`, ...args);
// };
global.ServerError = class extends Error {
constructor(status, msg) {
super(msg);
this.status = status;
}
}
const server = new Server()

24
server/util.js Normal file
View File

@@ -0,0 +1,24 @@
global.ServerError = class extends Error {
constructor(status, msg) {
super(msg);
this.status = status;
}
}
global.currentTime = function () {
const now = new Date();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
const year = now.getFullYear();
let hours = now.getHours();
const ampm = hours >= 12 ? "pm" : "am";
hours = hours % 12 || 12; // convert to 12-hour format
const minutes = String(now.getMinutes()).padStart(2, "0");
const seconds = String(now.getSeconds()).padStart(2, "0");
const ms = String(now.getMilliseconds()).padStart(4, "0"); // 4-digit like "5838"
return `${month}.${day}.${year}-${hours}:${minutes}:${seconds}${ms}${ampm}`;
}

View File

@@ -1,31 +0,0 @@
const { WebSocket, WebSocketServer } = require('ws');
let wss;
export function initWebSocket(server) {
wss = new WebSocketServer({ server });
wss.on('connection', (ws, req) => {
console.log('✅ New WebSocket client connected');
ws.on('close', () => {
console.log('Client disconnected');
});
});
console.log('WebSocket server initialized');
}
// Broadcast a message to all connected clients
export function broadcast(reqType, data) {
if (!wss) return;
const payload = typeof data === 'string' ? data : JSON.stringify(data);
const message = `${reqType}|${payload}`;
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}

View File

@@ -0,0 +1,43 @@
const { z } = require("zod")
const sendSchema = z.object({
forum: z.string(),
text: z.string(),
})
.strict()
const getSchema = z.object({
forum: z.string(),
number: z.number()
})
.strict()
export default class ForumHandler {
static handleSend(msg, ws) {
try {
global.db.posts.add(msg.text, msg.forum, ws.userEmail)
global.Socket.broadcast({event: "new-message", app: "FORUM", forum: msg.forum, msg: this.handleGet({forum: msg.forum, number: 100})})
return {success: true}
} catch(e) {
console.error(e)
}
}
static handleGet(msg) {
let data = global.db.posts.get(msg.forum, msg.number)
return data
}
static handle(operation, msg, ws) {
switch(operation) {
case "SEND":
if(!sendSchema.safeParse(msg).success) throw new Error("Incorrectly formatted Forum ws message!")
return this.handleSend(msg, ws)
case "GET":
if(!getSchema.safeParse(msg).success) throw new Error("Incorrectly formatted Forum ws message!")
return this.handleGet(msg)
}
}
}

93
server/ws/ws.js Normal file
View File

@@ -0,0 +1,93 @@
const { WebSocket, WebSocketServer } = require('ws');
const { z } = require("zod")
const jwt = require('jsonwebtoken');
import ForumHandler from "./handlers/ForumHandler.js"
export default class Socket {
wss;
messageSchema = z.object({
id: z.string(),
app: z.string(),
operation: z.string().optional(),
msg: z.union([
z.object({}).passthrough(), // allows any object
z.array(z.any()) // allows any array
])
}).strict()
constructor(server) {
this.wss = new WebSocketServer({ server });
this.wss.on('connection', (ws, req) => {
console.log('✅ New WebSocket client connected');
function parseCookies(cookieHeader = "") {
return Object.fromEntries(
cookieHeader.split(";").map(c => {
const [key, ...v] = c.trim().split("=");
return [key, decodeURIComponent(v.join("="))];
})
);
}
const cookies = parseCookies(req.headers.cookie);
const token = cookies.auth_token;
if (!token) throw new Error("No auth token");
const payload = jwt.verify(token, process.env.JWT_SECRET);
ws.userEmail = payload.email;
ws.on('message', (msg) => {
this.handleMessage(msg, ws);
});
ws.on('close', () => {
console.log('Client disconnected');
});
});
console.log('WebSocket server initialized');
}
// Build a system where the ws obj is updated every time on navigate, so it already has context
// this way, we can only send broadcast messages to clients that actually have that app / subapp open
handleMessage = (msg, ws) => {
try {
const text = msg.toString();
const req = JSON.parse(text);
if(!this.messageSchema.safeParse(req).success) throw new Error("Socket.handleMessage: Incorrectly formatted incoming ws message!")
let responseData;
switch (req.app) {
case "FORUM":
responseData = ForumHandler.handle(req.operation, req.msg, ws)
break;
default:
console.error("unknown ws message")
}
let response = {
...req
}
response.msg = responseData
if(!this.messageSchema.safeParse(response).success) throw new Error("Socket.handleMessage: Incorrectly formatted outgoing ws message!")
ws.send(JSON.stringify(response))
} catch (e) {
console.error("Invalid WS message:", e);
}
}
broadcast(event) {
if (!this.wss) return;
let message = JSON.stringify(event)
this.wss.clients.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(message);
}
});
}
}