From 7cfcc01c99f039b3cb928a3652b08d168cf77069 Mon Sep 17 00:00:00 2001 From: metacryst Date: Tue, 10 Mar 2026 19:09:47 -0500 Subject: [PATCH] working with db, docker working, small error with personal data --- Dockerfile | 6 + db/01_init.sql | 58 +++++++ db/02_seed.sql | 31 ++++ docker-compose.yml | 36 ++++ package.json | 7 +- server/auth.js | 24 +-- server/db/db.js | 186 --------------------- server/db/model/dms/conversation.js | 1 + server/db/model/dms/dm.js | 1 + server/db/model/edges/MEMBER_IN_NETWORK.js | 47 ++---- server/db/model/edges/POST_FROM_NETWORK.js | 1 + server/db/model/forum/post.js | 6 + server/db/model/import.js | 6 - server/db/model/member.js | 130 +++++++------- server/db/model/network.js | 97 +++++------ server/db/model/payment.js | 47 ------ server/db/model/title.js | 29 ---- server/db/sql.js | 8 + server/index.js | 56 +++++-- server/payments.js | 16 +- server/ws/handlers/MessagesHandler.js | 2 + ui/desktop/apps/Dashboard/Dashboard.js | 3 +- ui/desktop/apps/Dashboard/alt.js | 2 +- ui/desktop/apps/People/People.js | 2 +- ui/desktop/index.js | 4 +- 25 files changed, 337 insertions(+), 469 deletions(-) create mode 100644 Dockerfile create mode 100644 db/01_init.sql create mode 100644 db/02_seed.sql create mode 100644 docker-compose.yml delete mode 100644 server/db/model/payment.js delete mode 100644 server/db/model/title.js create mode 100644 server/db/sql.js diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f00719e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM node:18-alpine +WORKDIR /usr/src/app +COPY package*.json ./ +RUN npm install +COPY . . +CMD ["node", "server/index.js"] \ No newline at end of file diff --git a/db/01_init.sql b/db/01_init.sql new file mode 100644 index 0000000..da102ea --- /dev/null +++ b/db/01_init.sql @@ -0,0 +1,58 @@ +-- members +CREATE TABLE IF NOT EXISTS members ( + id SERIAL PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + password TEXT NOT NULL, + apps TEXT[] DEFAULT '{}', + created TIMESTAMPTZ DEFAULT NOW() +); + +-- networks +CREATE TABLE IF NOT EXISTS networks ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + logo TEXT, + abbreviation TEXT, + apps TEXT[] DEFAULT '{}', + stripe_account_id TEXT, + stripe_access_token TEXT, + created TIMESTAMPTZ DEFAULT NOW() +); + +-- edges (member in network) +CREATE TABLE IF NOT EXISTS member_network ( + id SERIAL PRIMARY KEY, + member_id INTEGER REFERENCES members(id), + network_id INTEGER REFERENCES networks(id), + type TEXT NOT NULL DEFAULT 'IN', + created TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(member_id, network_id) +); + +-- create schema +CREATE SCHEMA IF NOT EXISTS org_1; + +-- join form table +CREATE TABLE IF NOT EXISTS org_1.join_form ( + id SERIAL PRIMARY KEY, + fname TEXT NOT NULL, + lname TEXT NOT NULL, + email TEXT NOT NULL, + phone TEXT, + county TEXT, + time TIMESTAMPTZ DEFAULT NOW() +); + +-- contact form table +CREATE TABLE IF NOT EXISTS org_1.contact_form ( + id SERIAL PRIMARY KEY, + fname TEXT NOT NULL, + lname TEXT NOT NULL, + email TEXT NOT NULL, + phone TEXT, + message TEXT, + county TEXT, + time TIMESTAMPTZ DEFAULT NOW() +); \ No newline at end of file diff --git a/db/02_seed.sql b/db/02_seed.sql new file mode 100644 index 0000000..f195ec3 --- /dev/null +++ b/db/02_seed.sql @@ -0,0 +1,31 @@ +-- members +INSERT INTO members (email, first_name, last_name, password, apps, created) VALUES +('samrussell99@pm.me', 'Sam', 'Russell', '$argon2id$v=19$m=65536,t=3,p=4$n/8BaBisEnBaQNbkxzs1VA$dvvnupWNtB5w5qTBgEciDsNA6rOgXaEypcEK1A0ndLM', '{"Dashboard"}', '2026-01-15 09:58:01.0072'), +('freddyjkrueger@gmail.com', 'Freddy','Krueger', '$argon2id$v=19$m=65536,t=3,p=4$ioAYDPtyUulykMrH9W7q9A$lG43cq6Dj3/n1+bJrkupWpB5Xro3UIQaVd9rjuJJ6nM', '{"Dashboard"}', '2026-01-13 13:38:46.0810'), +('harmysmarmy@gmail.com', 'Harmy', 'Smarmy', '$argon2id$v=19$m=65536,t=3,p=4$FAhGtCtqNAQ19tBYD73wXQ$0AM/khyBFFuX2mv0ieqtGfsXRgtEldWKFwyeV3BA3Xk', '{"Dashboard"}', '2026-01-13 13:41:41.0722'); + +-- networks +INSERT INTO networks (name, logo, abbreviation, apps, stripe_account_id, stripe_access_token, created) VALUES +('Comal County Young Republicans', 'comalyr.svg', 'comalyr', '{"Dashboard","People","Settings"}', 'acct_1Sn6DwLpyskwAml9', 'sk_test_51Sn6DwLpyskwAml9JzWYlSee2we70EZ2nCwgrEVFGkaPdzXLUBXEDeiMSaVUZtvUowEl5nh07N3CEFYymjDvMXok00vaBYPNgn', '2026-01-10 09:58:01.0074'), +('Hyperia', 'hyperia.svg', 'hyperia', '{"Dashboard","People","Settings"}', 'acct_1S4w0GHZemeF9CKR', 'sk_test_51S4w0GHZemeF9CKR2P1B4N7eO51aUcgTuOY5KRiKYXAQXuGaKblXitmBp58fYA200E0N7nHzgZxin1eNkhp2K1xB00q9hFNuQM', '2026-01-10 09:58:01.0074'); + +-- member_network edges +INSERT INTO member_network (member_id, network_id, type, created) VALUES +(1, 1, 'IN', '2025-11-24 00:54:36.0784'), +(2, 1, 'IN', '2026-01-13 13:14:28.0178'), +(3, 1, 'IN', '2026-01-13 13:28:35.0701'), +(1, 2, 'IN', '2026-01-13 13:28:35.0701'); + +-- join form seed data +INSERT INTO org_1.join_form (fname, lname, email, phone, county, time) VALUES +('James', 'Mitchell', 'james.mitchell@gmail.com', '512-555-0101', 'Comal', '2025-12-16 23:11:31.0011'), +('Rachel', 'Torres', 'rachel.torres@yahoo.com', '512-555-0102', 'Bexar', '2025-12-19 19:23:12.0717'), +('David', 'Nguyen', 'david.nguyen@gmail.com', '830-555-0103', 'Comal', '2026-01-06 16:55:29.0288'), +('Emily', 'Sanders', 'emily.sanders@outlook.com', '210-555-0104', 'Hays', '2026-01-07 17:14:01.0711'); + +-- contact form seed data +INSERT INTO org_1.contact_form (fname, lname, email, phone, county, message, time) VALUES +('Marcus', 'Webb', 'marcus.webb@gmail.com', '512-555-0201', 'Comal', 'Interested in volunteering at upcoming events.', '2025-12-29 13:20:28.0157'), +('Sandra', 'Holloway', 'sandra.holloway@gmail.com', '830-555-0202', 'Comal', 'Would love to connect with your organization.', '2025-12-30 22:10:24.0971'), +('Robert', 'Finley', 'robert.finley@gmail.com', '210-555-0203', 'Comal', 'Looking forward to getting more involved locally.', '2026-01-10 21:23:51.0073'), +('Barbara', 'Crane', 'barbara.crane@outlook.com', '512-555-0204', 'Comal', 'Please reach out regarding the next meeting schedule.', '2026-01-10 21:23:54.0841'); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0bc0814 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,36 @@ +version: "3.9" + +services: + server: + container_name: server + build: + context: . + dockerfile: Dockerfile + ports: + - "10002:10002" + volumes: + - .:/usr/src/app + - node_modules:/usr/src/app/node_modules + - ./db:/usr/src/app/db + depends_on: + - db + env_file: + - .env + + db: + image: postgres:16-alpine + restart: always + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=postgres + ports: + - "5432:5432" + volumes: + - db-data:/var/lib/postgresql/data + - ./db/01_init.sql:/docker-entrypoint-initdb.d/01_init.sql + - ./db/02_seed.sql:/docker-entrypoint-initdb.d/02_seed.sql + +volumes: + db-data: + node_modules: \ No newline at end of file diff --git a/package.json b/package.json index 3a9e482..9b84b18 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "start": "node server/index.js" }, "workspaces": [ - "apps/*" - ], + "apps/*" + ], "dependencies": { "argon2": "^0.44.0", "chalk": "^4.1.2", @@ -18,8 +18,9 @@ "express-useragent": "^2.0.2", "jsonwebtoken": "^9.0.2", "moment": "^2.30.1", + "postgres": "^3.4.8", "stripe": "^20.0.0", "ws": "^8.18.3", "zod": "^4.1.12" } -} \ No newline at end of file +} diff --git a/server/auth.js b/server/auth.js index 10de8ae..ed8b693 100644 --- a/server/auth.js +++ b/server/auth.js @@ -42,23 +42,25 @@ export default class AuthHandler { } } - getProfile(req, res) { + async getProfile(req, res) { try { let email = global.auth.getRequestEmail(req, res) - const user = db.members.getByEmail(email); - let connections = db.MEMBER_IN_NETWORK.getByMember(db.members.prefix + "-" + user.id) - let userOrgs = connections.map((c) => { - let network = db.networks.get(c.to) - delete network.stripeAccountId - delete network.stripeAccessToken - return network - }) + const user = await db.members.getByEmail(email); + const connections = await db.MEMBER_IN_NETWORK.getByMember(user.id); + + const userOrgs = await Promise.all(connections.map(async (c) => { + const network = await db.networks.get(c.network_id); + delete network.stripe_account_id; + delete network.stripe_access_token; + return network; + })); res.send({ id: user.id, email: user.email, - name: user.firstName + " " + user.lastName, + first_name: user.first_name, + last_name: user.last_name, networks: userOrgs, apps: user.apps }); @@ -70,7 +72,7 @@ export default class AuthHandler { async login(req, res) { const { email, password } = req.body; - let foundUser = global.db.members.getByEmail(email) + let foundUser = await global.db.members.getByEmail(email) if(!foundUser) { res.status(400).json({ error: 'Incorrect email.' }); return; diff --git a/server/db/db.js b/server/db/db.js index d8092c5..197de1e 100644 --- a/server/db/db.js +++ b/server/db/db.js @@ -3,16 +3,8 @@ import chalk from 'chalk'; import path from 'path'; import { nodeModels, edgeModels } from './model/import.js' import Edge from "./model/edge.js" -import { fileURLToPath } from "url" -const __dirname = path.dirname(fileURLToPath(import.meta.url)) export default class Database { - - PERSONAL_DATA_PATH = path.join(__dirname, '../../db/personal') - - nodes = new Array(10000).fill(0); - edges = new Array(10000).fill(0); - constructor() { let values = Object.values(nodeModels) for(let i = 0; i < values.length; i++) { @@ -26,183 +18,5 @@ export default class Database { this[key] = eValues[i] } this.edge = new Edge() - this.loadData() - } - - getCurrentIndex(model) { - return model.indices[1] - model.indices[0] - } - - getNextIndex(model) { - return model.indices[1] - model.indices[0] + 1 - } - - // add a get in here that returns a safe copy, not the actual reference - - addNode(prefix, node) { - try { - let model = nodeModels[prefix] - if(model) { - let toAdd = { - id: this.getNextIndex(model), - ...node, - } - if(!toAdd.created) toAdd.created = global.currentTime() - let schema = model.schema - let result = schema.safeParse(toAdd) - if(result.success) { - this.nodes[model.indices[1]] = toAdd - model.indices[1]++; - } else { - console.error(result.error) - throw new global.ServerError(400, "Invalid Data!"); - } - } else { - throw new Error("Type does not exist for node: " + id) - } - } catch(e) { - throw e - } - } - - deleteNode(prefix, id) { - try { - let model = nodeModels[prefix] - if(model) { - this.nodes[model.indices[0] + (id - 1)] = 0 - } else { - throw new Error("Type does not exist for node: " + id) - } - } catch(e) { - throw e - } - } - - updateNode(prefix, id, toEdit) { - try { - let model = nodeModels[prefix] - if(model) { - let schema = model.schema - let result = schema.safeParse(toEdit) - if(result.success) { - this.nodes[model.indices[0] + (id - 1)] = toEdit - } else { - console.error(result.error) - throw new global.ServerError(400, "Invalid Data!"); - } - } else { - throw new Error("Type does not exist for node: " + prefix) - } - } catch(e) { - throw e - } - } - - addEdge(prefix, edge) { - try { - let type = prefix - let model = edgeModels[type] - if(model) { - let toAdd = { - id: model.indices[1] - model.indices[0] + 1, - ...edge - } - if(!toAdd.created) toAdd.created = global.currentTime() - let schema = model.schema - let result = schema.safeParse(toAdd) - if(result.success) { - this.edges[model.indices[1]] = toAdd - model.indices[1]++; - } else { - console.error(result.error) - throw new global.ServerError(400, "Invalid Data!"); - } - } else { - throw new Error("Type does not exist for edge: " + prefix) - } - } catch(e) { - throw e - } - } - - deleteEdge(prefix, id) { - try { - let model = edgeModels[prefix] - if(model) { - this.edges[model.indices[0] + (id - 1)] = 0 - } else { - throw new Error("Type does not exist for edge: " + prefix) - } - } catch(e) { - throw e - } - } - - async loadData() { - const dbData = await fs.readFile(path.join(process.cwd(), 'db/db.json'), 'utf8'); - let dbJson; - try { - dbJson = JSON.parse(dbData); - } catch { - dbJson = [] - } - - let nodes = dbJson["nodes"]; - let entries = Object.entries(nodes) - for(let i=0; i { - global.db.saveData() - }, 5000) - } - - async saveData() { - let data = { - "nodes": {}, - "edges": {} - } - - let nModels = Object.values(nodeModels) - this.nodes.forEach((entry, i) => { - if(entry) { - for(let j = 0; j < nModels.length; j++) { - let model = nModels[j] - let indices = model.indices - if(i >= indices[0] && i < indices[1]) { - let prefix = model.prefix - data.nodes[prefix + "-" + entry.id] = entry - } - } - } - }) - - let eModels = Object.values(edgeModels) - this.edges.forEach((entry, i) => { - if(entry) { - for(let j = 0; j < eModels.length; j++) { - let model = eModels[j] - let indices = model.indices - if(i >= indices[0] && i < indices[1]) { - let prefix = model.prefix - data.edges[prefix + "-" + entry.id] = entry - } - } - } - }) - - let string = JSON.stringify(data, null, 4) - await fs.writeFile(path.join(process.cwd(), 'db/db.json'), string, "utf8"); } } \ No newline at end of file diff --git a/server/db/model/dms/conversation.js b/server/db/model/dms/conversation.js index 92f71a1..1c81d20 100644 --- a/server/db/model/dms/conversation.js +++ b/server/db/model/dms/conversation.js @@ -40,6 +40,7 @@ export default class Conversation { function populateMemberProfilesFromIDs(ids) { let result = [] for(let i=0; i s.startsWith("$argon2"); + + async add(newMember, networkId = null) { + if (!this.isHashed(newMember.password)) { + newMember.password = await argon2.hash(newMember.password); } - schema = z.object({ - id: z.number(), - email: z.string().email(), - firstName: z.string(), - lastName: z.string(), - password: z.string(), - apps: z.array(z.string()), - created: z.string() - }) - - isHashed = (s) => {return s.startsWith("$argon2")} - - async add(newMember, network = null) { - const hash = await argon2.hash(newMember.password); - newMember.password = hash - newMember.apps = ["Dashboard"] - - try { - global.db.addNode(this.prefix, newMember) - if(network) { - global.db.edge.add({ - type: "IN", - from: `${this.prefix}-${global.db.getCurrentIndex(this)}`, - to: "NETWORK-1" - }) - } - } catch(e) { - console.error(e) - throw new global.ServerError(400, "Failed to add member!"); - } + const result = this.schema.safeParse(newMember); + if (!result.success) { + console.error(result.error); + throw new global.ServerError(400, "Invalid Member Data!"); } - async getPersonalData(id) { - const filePath = path.join(global.db.PERSONAL_DATA_PATH, id, "db.json"); + try { + const [member] = await sql` + INSERT INTO members ${sql({ + email: newMember.email, + first_name: newMember.firstName, + last_name: newMember.lastName, + password: newMember.password, + apps: newMember.apps ?? ['Dashboard'] + })} + RETURNING * + `; - const [raw] = await Promise.all([ - fs.promises.readFile(filePath, "utf8"), - ]); + if (networkId) { + await sql` + INSERT INTO member_network ${sql({ + member_id: member.id, + network_id: networkId, + type: 'IN' + })} + `; + } - const result = raw.trim() ? JSON.parse(raw) : []; - return result + return member; + } catch (e) { + console.error(e); + throw new global.ServerError(400, "Failed to add member!"); } + } - getByNetwork(id) { - let connections = db.MEMBER_IN_NETWORK.getByNetwork(id) - let members = [] - connections.forEach((conn) => { - members.push(this.getByID(conn.from)) - }) - return members - } + async getByID(id) { + const [member] = await sql` + SELECT * FROM members WHERE id = ${id} + `; + return member ?? null; + } - getByID(id) { - if(typeof id === 'string') { - id = id.split("-")[1] - } - return global.db.nodes[this.indices[0] + id - 1] - } + async getByEmail(email) { + const [member] = await sql` + SELECT * FROM members WHERE email = ${email} + `; + return member ?? null; + } - getByEmail(email) { - for(let i=this.indices[0]; i 1) { - id = id.split("-")[1] - } - let index = this.indices[0] + (id - 1) - return structuredClone(global.db.nodes[index]) + async get(id) { + const [network] = await sql` + SELECT * FROM networks WHERE id = ${id} + `; + return network ?? null; } - getByAbbreviation(abbreviation) { - for(let i=this.indices[0]; i ({ diff --git a/server/ws/handlers/MessagesHandler.js b/server/ws/handlers/MessagesHandler.js index 1abdeaa..16af275 100644 --- a/server/ws/handlers/MessagesHandler.js +++ b/server/ws/handlers/MessagesHandler.js @@ -9,6 +9,7 @@ const sendSchema = z.object({ export default class MessagesHandler { static handleSend(msg, ws) { + console.warn("MessagesHandler.js 12: These calls to global.db need to be awaited") let user = global.db.members.getByEmail(ws.userEmail) let convo = global.db.conversations.get(msg.conversation) if(convo.between.includes(`MEMBER-${user.id}`)) { @@ -22,6 +23,7 @@ export default class MessagesHandler { } static handleGet(ws) { + console.warn("MessagesHandler.js 26: These calls to global.db need to be awaited") let user = global.db.members.getByEmail(ws.userEmail) let data = global.db.conversations.getByMember(`MEMBER-${user.id}`) return data diff --git a/ui/desktop/apps/Dashboard/Dashboard.js b/ui/desktop/apps/Dashboard/Dashboard.js index 6bae519..877c7e5 100644 --- a/ui/desktop/apps/Dashboard/Dashboard.js +++ b/ui/desktop/apps/Dashboard/Dashboard.js @@ -51,7 +51,7 @@ class Dashboard extends Shadow { VStack(() => { if(window.location.pathname.startsWith("/my")) { - h1(global.profile.name); + h1(global.profile.first_name + " " + global.profile.last_name); return } else if(!window.location.pathname.includes("comalyr")) { @@ -150,7 +150,6 @@ class Dashboard extends Shadow { .gap(8); }); }) - .gap(0.5, em) .paddingTop(4, pct) .paddingLeft(5, pct) .width(100, pct) diff --git a/ui/desktop/apps/Dashboard/alt.js b/ui/desktop/apps/Dashboard/alt.js index eb93f07..31222c4 100644 --- a/ui/desktop/apps/Dashboard/alt.js +++ b/ui/desktop/apps/Dashboard/alt.js @@ -48,7 +48,7 @@ class Dashboard extends Shadow { VStack(() => { if(window.location.pathname.startsWith("/my")) { - h1(global.profile.name); + h1(global.profile.first_name + " " + global.profile.last_name); return } else if(!window.location.pathname.includes("comalyr")) { diff --git a/ui/desktop/apps/People/People.js b/ui/desktop/apps/People/People.js index a428acc..88788e5 100644 --- a/ui/desktop/apps/People/People.js +++ b/ui/desktop/apps/People/People.js @@ -17,7 +17,7 @@ class People extends Shadow { new gridjs.Grid({ columns: ["Name", "Email"], data: global.currentNetwork.data.members.map(m => [ - m.firstName + " " + m.lastName, + m.first_name + " " + m.last_name, m.email ]), sort: true, diff --git a/ui/desktop/index.js b/ui/desktop/index.js index 01d0851..e106d45 100644 --- a/ui/desktop/index.js +++ b/ui/desktop/index.js @@ -20,7 +20,7 @@ let Global = class { window.dispatchEvent(event) } - async fetchAppData() { + async fetchOrgData() { let personalSpace = this.currentNetwork === this.profile let appData = await fetch(`/api/${personalSpace ? "my" : "org"}data/` + this.currentNetwork.id, {method: "GET"}) let json = await appData.json() @@ -84,7 +84,7 @@ let Global = class { } if(!this.currentNetwork.data) { - this.currentNetwork.data = await this.fetchAppData() + this.currentNetwork.data = await this.fetchOrgData() } if(appChanged && !networkChanged) {