diff --git a/_test/OrderedObject.test.js b/_test/OrderedObject.test.js new file mode 100644 index 0000000..c4e6033 --- /dev/null +++ b/_test/OrderedObject.test.js @@ -0,0 +1,27 @@ +import OrderedObject from "../server/db/model/OrderedObject.js" + +window.testSuites.push( + + class testOrderedObject { + + async addShouldFailIfKeyIsDuplicate() { + class Test extends OrderedObject { + + } + + let test = new Test() + + test.add("1", {name: "hello"}) + + try { + test.add("1", {name: "bye"}) + } catch(e){ + return + } + + return "Received no error!" + } + + } + +) \ No newline at end of file diff --git a/_test/test.js b/_test/test.js new file mode 100644 index 0000000..9f7f5cf --- /dev/null +++ b/_test/test.js @@ -0,0 +1,57 @@ +let scriptToPaste = ` + +`; +console.log("Tests initializing.") +window.testSuites = []; + +/* Server - DB */ +import ("./OrderedObject.test.js") + +window.test = async function() { + // window.testSuites.sort(); + window.alert = () => true; + window.confirm = () => true; + + console.clear(); + + let failed = 0; + let success = 0; + + var start = new Date(); + for(let j=0; j new Promise(res => setTimeout(res, ms)); + +window.__defineGetter__("test", test); \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..e0b254b --- /dev/null +++ b/index.html @@ -0,0 +1,11 @@ + + + + + Node Terminal + + + + + + diff --git a/main.js b/main.js new file mode 100644 index 0000000..ceadd76 --- /dev/null +++ b/main.js @@ -0,0 +1,30 @@ +// main.js +const { app, BrowserWindow, nativeImage } = require('electron'); +const path = require('path'); + +if (process.platform === 'darwin') { + setTimeout(() => { + app.dock.bounce('informational'); // or 'critical' + }, 1000); +} + +function createWindow() { + const win = new BrowserWindow({ + show: false, // window is hidden + width: 1200, + height: 800, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + icon: path.join(__dirname, '_', 'fabric.png') + } + }); + win.loadFile(path.join(__dirname, 'index.html')); + win.webContents.openDevTools({ mode: 'undocked' }); +} + +app.whenReady().then(createWindow); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') app.quit(); +}); \ No newline at end of file diff --git a/package.json b/package.json index 2292054..a1c51f2 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,22 @@ { "name": "Hyperia", - "type": "module", "version": "1.0.0", - "description": "", - "main": "index.js", + "main": "main.js", "scripts": { - "start": "node server/index.js", - "test": "echo \"Error: no test specified\" && exit 1" + "start": "electron ." }, - "author": "", - "license": "ISC", "dependencies": { "argon2": "^0.44.0", - "chalk": "^5.6.2", + "chalk": "^4.1.2", "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^4.18.2", - "http-proxy": "^1.18.1", "jsonwebtoken": "^9.0.2", "moment": "^2.30.1", "ws": "^8.18.3" + }, + "devDependencies": { + "electron": "^25.0.0" } -} +} \ No newline at end of file diff --git a/server/_/quilldb.js b/server/_/quilldb.js index 09df8ee..51aa1d3 100644 --- a/server/_/quilldb.js +++ b/server/_/quilldb.js @@ -1,7 +1,7 @@ -import chalk from 'chalk' -import path from 'path'; -import fs from 'fs/promises'; -import { pathToFileURL } from 'url'; +const chalk = require('chalk'); +const path = require('path'); +const fs = require('fs/promises'); +const { pathToFileURL } = require('url'); function Node(node) { let traits = [ diff --git a/server/auth.js b/server/auth.js index 0a53bb5..de4ce97 100644 --- a/server/auth.js +++ b/server/auth.js @@ -1,8 +1,7 @@ -import dotenv from 'dotenv'; -import chalk from 'chalk'; -import jwt from 'jsonwebtoken' -import argon2 from 'argon2' -import { randomUUID } from 'node:crypto' +const dotenv = require("dotenv") +const jwt = require('jsonwebtoken'); +const argon2 = require('argon2'); + dotenv.config(); export default class AuthHandler { diff --git a/server/db/db.js b/server/db/db.js index b04c2e5..c78ab58 100644 --- a/server/db/db.js +++ b/server/db/db.js @@ -1,29 +1,21 @@ +const fs = require('fs/promises'); +const chalk = require('chalk'); +const path = require('path'); import QuillDB from "../_/quilldb.js" -import fs from 'fs/promises' -import path from 'path' -import Title from "./model/Title.js" -import Member from './model/Member.js' -import Token from './model/Token.js' +import Titles from "./model/Titles.js" +import Members from './model/Members.js' +import Tokens from './model/Tokens.js' export default class Database { - nodes = []; - types = [ - { - validate: Title, - start: 0, - end: null, - }, - { - validate: Member, - start: null, - end: null, - }, - { - validate: Token, - start: null, - end: null, - }, - ] + titles = new Titles() + members = new Members() + tokens = new Tokens() + + fromID = { + "HY": this.titles, + "MEMBER": this.members, + "TOKEN": this.tokens + } constructor() { this.loadData() @@ -38,82 +30,25 @@ export default class Database { dbJson = [] } let nodes = dbJson["nodes"]; - this.validateNodes(nodes) - } - - validateNodes(nodes) { - nodes = Object.entries(nodes) - - let t = 0 - - let currentType = () => {return this.types[t]} - let nextType = () => {return this.types[t+1]} - let selectNextType = () => { - currentType().end = t - t += 1; - currentType().start = t - } - let lastNode = (i=null) => { - if(i == null) throw new Error("must pass a param to lastNode()") - return i+1 === nodes.length - } - - for(let i=0; i { - return this.nodes[id] - }, - userByEmail: (email) => { - for (const id of this.labels["User"]) { - const user = this.get.user(id); - if (user.email === email) { - return { id, ...user } - } - } - return null; - }, - token: (id) => { - return this.nodes[`TOKEN-${id}`] - } - } - - add = { - user: (node) => { - let lastUser = {} - - } - } - - submitNewUser(qrCodeID, userInfo) { - let newUser = { - labels: ["User"], - ...userInfo - } - if(User(newUser)) - this.add.user(newUser) - } - generateUserID() { let id = this.labels["User"].length + 1; while (this.get.user(`user-${id}`)) { diff --git a/server/db/model/Member.js b/server/db/model/Member.js deleted file mode 100644 index 4fd501e..0000000 --- a/server/db/model/Member.js +++ /dev/null @@ -1,43 +0,0 @@ -export default function Member(id, node) { - let idTraits = { - firstWord: "MEMBER", - length: 2 - } - - let fields = [ - "firstName", - "lastName", - "email", - "password" - ] - - let checkID = () => { - let split = id.split("-") - return ( - split.length === idTraits.length - && split[0] === idTraits.firstWord - && !isNaN(Number(split[1])) - ) - } - let idres = checkID() - if(!idres) { - return false - } - - let checkFields = () => { - for(let i = 0; i < fields.length; i++) { - if(!node[fields[i]]) { - throw new Error(`Member ${node.email} is missing trait ${fields[i]}`) - return false - } else { - return true - } - } - } - let fieldres = checkFields() - if(!fieldres) { - return false - } - - return true -} \ No newline at end of file diff --git a/server/db/model/Members.js b/server/db/model/Members.js new file mode 100644 index 0000000..42271bc --- /dev/null +++ b/server/db/model/Members.js @@ -0,0 +1,64 @@ +import OrderedObject from "./OrderedObject.js" + +export default class Members extends OrderedObject { + + add(newMember) { + console.log("adding ", newMember) + let id = `MEMBER-${newMember.email}` + if(this.validate(id, newMember)) { + try { + super.add(id, newMember) + } catch(e) { + console.error(e) + throw e + } + } else { + throw new global.ServerError(400, "Invalid Member Data!"); + } + } + + validate(id, node) { + let idTraits = { + firstWord: "MEMBER" + } + + let fields = [ + "firstName", + "lastName", + "email", + "password" + ] + + let checkID = () => { + let split = id.split("-") + return ( + split[0] === idTraits.firstWord + && split[1].includes("@") + && split[1].includes(".") + ) + } + let idres = checkID() + if(!idres) { + console.log("id failed: ", id) + return false + } + + let checkFields = () => { + for(let i = 0; i < fields.length; i++) { + if(!node[fields[i]]) { + throw new Error(`Member ${node.email} is missing trait ${fields[i]}`) + return false + } else { + return true + } + } + } + let fieldres = checkFields() + if(!fieldres) { + console.log("fields failed") + return false + } + + return true + } +} \ No newline at end of file diff --git a/server/db/model/OrderedObject.js b/server/db/model/OrderedObject.js new file mode 100644 index 0000000..5d91869 --- /dev/null +++ b/server/db/model/OrderedObject.js @@ -0,0 +1,29 @@ +export default class OrderedObject { + entries = [] + ids = {} + + add(id, data) { + if(this.get(id)) { + console.error(`Can't add item ${id}: id already exists`) + throw new global.ServerError(400, `Member with this email already exists`) + } + this.entries.push(data) + this.ids[id] = this.entries.length - 1 + } + + delete(key) { + if (typeof key === "number") { + return this.entries[key] + } else { + return this.entries[this.ids[key]] + } + } + + get(key) { + if (typeof key === "number") { + return this.entries[key] + } else { + return this.entries[this.ids[key]] + } + } +} \ No newline at end of file diff --git a/server/db/model/Title.js b/server/db/model/Title.js deleted file mode 100644 index 1a5ffdb..0000000 --- a/server/db/model/Title.js +++ /dev/null @@ -1,33 +0,0 @@ -export default function Title(id, node) { - let checkID = () => { - let split = id.split("-") - return ( - split.length === 2 - && split[0] === "HY" - && !isNaN(Number(split[1])) - ) - } - let idres = checkID() - if(!idres) { - return false - } - - let checkFields = () => { - let fields = [ - "fullName", - ] - for(let i = 0; i < fields.length; i++) { - if(!node[fields[i]]) { - throw new Error(`Title ${id} is missing trait ${fields[i]}`) - return false - } - } - return true - } - let fieldres = checkFields() - if(!fieldres) { - return false - } - - return true -} \ No newline at end of file diff --git a/server/db/model/Titles.js b/server/db/model/Titles.js new file mode 100644 index 0000000..5e37cfc --- /dev/null +++ b/server/db/model/Titles.js @@ -0,0 +1,53 @@ +import OrderedObject from "./OrderedObject.js" + +export default class Titles extends OrderedObject { + + add(newTitle) { + let id = `HY-${this.entries.length+1}` + console.log(id) + if(this.validate(id, newTitle)) { + try { + super.add(id, newTitle) + } catch(e) { + console.error(e) + throw e + } + } else { + throw new global.ServerError(400, "Invalid Member Data!"); + } + } + + validate(id, node) { + let checkID = () => { + let split = id.split("-") + return ( + split.length === 2 + && split[0] === "HY" + && !isNaN(Number(split[1])) + ) + } + let idres = checkID() + if(!idres) { + return false + } + + let checkFields = () => { + let fields = [ + "fullName", + ] + for(let i = 0; i < fields.length; i++) { + if(!node[fields[i]]) { + throw new Error(`Title ${id} is missing trait ${fields[i]}`) + return false + } + } + return true + } + let fieldres = checkFields() + if(!fieldres) { + return false + } + + return true + } +} \ No newline at end of file diff --git a/server/db/model/Token.js b/server/db/model/Token.js deleted file mode 100644 index 06f0c12..0000000 --- a/server/db/model/Token.js +++ /dev/null @@ -1,39 +0,0 @@ -export default function Token(id, node) { - let idTraits = { - firstWord: "TOKEN" - } - - let fields = [ - "index", - "url", - "used" - ] - - let checkID = () => { - let split = id.split("-") - return ( - split[0] === idTraits.firstWord - ) - } - let idres = checkID() - if(!idres) { - return false - } - - let checkFields = () => { - for(let i = 0; i < fields.length; i++) { - if(!node[fields[i]]) { - throw new Error(`Token ${node.email} is missing trait ${fields[i]}`) - return false - } else { - return true - } - } - } - let fieldres = checkFields() - if(!fieldres) { - return false - } - - return true -} \ No newline at end of file diff --git a/server/db/model/Tokens.js b/server/db/model/Tokens.js new file mode 100644 index 0000000..8d74b5d --- /dev/null +++ b/server/db/model/Tokens.js @@ -0,0 +1,62 @@ +import OrderedObject from "./OrderedObject.js" + +export default class Tokens extends OrderedObject { + + add(token) { + let id = `TOKEN-${token.uuid}` + if(this.validate(id, token)) { + try { + super.add(id, token) + } catch(e) { + console.error(e) + throw e + } + } else { + throw new global.ServerError(400, "Invalid Member Data!"); + } + } + + get(uuid) { + return super.get(`TOKEN-${uuid}`) + } + + validate(id, node) { + let idTraits = { + firstWord: "TOKEN" + } + + let fields = [ + "index", + "url", + "used" + ] + + let checkID = () => { + let split = id.split("-") + return ( + split[0] === idTraits.firstWord + ) + } + let idres = checkID() + if(!idres) { + return false + } + + let checkFields = () => { + for(let i = 0; i < fields.length; i++) { + if(!node[fields[i]]) { + throw new Error(`Token ${node.email} is missing trait ${fields[i]}`) + return false + } else { + return true + } + } + } + let fieldres = checkFields() + if(!fieldres) { + return false + } + + return true + } +} \ No newline at end of file diff --git a/server/index.js b/server/index.js index e4f08d7..31739c5 100644 --- a/server/index.js +++ b/server/index.js @@ -1,27 +1,22 @@ -import express from 'express'; -import cors from 'cors' -import cookieParser from 'cookie-parser' -import http from 'http' -import fs from 'fs' -import chalk from 'chalk' -import moment from 'moment' -import path from 'path'; -import { initWebSocket } from './ws.js' +const express = require('express'); +const cors = require('cors'); +const cookieParser = require('cookie-parser'); +const http = require('http'); +const fs = require('fs'); +const chalk = require('chalk'); +const moment = require('moment'); +const path = require('path'); +import { initWebSocket } from './ws.js' import Database from "./db/db.js" import AuthHandler from './auth.js'; import handlers from "./handlers.js"; -// Get __dirname in ES6 environment -import { fileURLToPath } from 'url'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - class Server { db; auth; - UIPath = path.join(__dirname, '../ui') - DBPath = path.join(__dirname, '../db') + UIPath = path.join(__dirname, './ui') + DBPath = path.join(__dirname, './db') registerRoutes(router) { // router.post('/api/location', handlers.updateLocation) @@ -39,7 +34,7 @@ class Server { if (!token) { return res.status(400).json({ error: 'Token is required' }); } - let fromDB = this.db.get.token(token) + let fromDB = this.db.tokens.get(token) if (!fromDB) { return res.status(403).json({ error: 'Invalid or expired token' }); } else if(fromDB.used) { @@ -50,8 +45,12 @@ class Server { newUserSubmission = (req, res) => { const { token } = req.query; - db.submitNewUser(token, req.body) - return res.status(400).json({ error: 'Haven\t finished this bruh' }); + try { + db.members.add(req.body) + return res.redirect(`/signin/?new=true`); + } catch(e) { + return res.status(e.status).json({ error: 'Error adding new member' }); + } } authMiddleware = (req, res, next) => { @@ -92,23 +91,14 @@ class Server { let url = req.url - let publicPage = () => { - url = "/index.html" - let filePath = path.join(this.UIPath, "public", url); - res.sendFile(filePath, (err) => { - if (err) { - console.log("File not found, sending fallback:", filePath); - res.redirect("/"); - } - }); - } - - let publicFile = () => { + let publicSite = () => { let filePath; if(url.startsWith("/_")) { filePath = path.join(this.UIPath, url); + } else if(url.includes("75820185")) { + filePath = path.join(this.UIPath, "public", url.split("75820185")[1]); } else { - filePath = path.join(this.UIPath, "public", url); + filePath = path.join(this.UIPath, "public", "index.html"); } res.sendFile(filePath); @@ -128,11 +118,7 @@ class Server { } if(!this.auth.isLoggedInUser(req, res)) { - if(!url.includes(".")) { - publicPage() - } else { - publicFile() - } + publicSite() } else { privateSite() } @@ -142,10 +128,10 @@ class Server { const formattedDate = moment().format('M.D'); const formattedTime = moment().format('h:mma'); if(req.url.includes("/api/")) { - console.log(chalk.blue(` ${req.method} ${req.url} | ${formattedDate} ${formattedTime}`)); + console.logclean(chalk.blue(` ${req.method} ${req.url} | ${formattedDate} ${formattedTime}`)); } else { if(req.url === "/") - console.log(chalk.gray(` ${req.method} ${req.url} | ${formattedDate} ${formattedTime}`)); + console.logclean(chalk.gray(` ${req.method} ${req.url} | ${formattedDate} ${formattedTime}`)); } next(); } @@ -154,9 +140,9 @@ class Server { const originalSend = res.send; res.send = function (body) { if(res.statusCode >= 400) { - console.log(chalk.blue( `<-${chalk.red(res.statusCode)}- ${req.method} ${req.url} | ${chalk.red(body)}`)); + console.logclean(chalk.blue( `<-${chalk.red(res.statusCode)}- ${req.method} ${req.url} | ${chalk.red(body)}`)); } else { - console.log(chalk.blue(`<-${res.statusCode}- ${req.method} ${req.url}`)); + console.logclean(chalk.blue(`<-${res.statusCode}- ${req.method} ${req.url}`)); } originalSend.call(this, body); }; @@ -184,16 +170,16 @@ class Server { initWebSocket(server); const PORT = 3003; server.listen(PORT, () => { - console.log("\n") - console.log(chalk.yellow("*************** Hyperia ***************")) - console.log(chalk.yellowBright(`Server is running on port ${PORT}: http://localhost`)); - console.log(chalk.yellow("***************************************")) - console.log("\n") + console.logclean("\n") + console.logclean(chalk.yellow("*************** Hyperia ***************")) + console.logclean(chalk.yellowBright(`Server is running on port ${PORT}: http://localhost`)); + console.logclean(chalk.yellow("***************************************")) + console.logclean("\n") }); process.on('SIGINT', async () => { - console.log(chalk.red('Closing server...')); - console.log(chalk.green('Database connection closed.')); + console.logclean(chalk.red('Closing server...')); + console.logclean(chalk.green('Database connection closed.')); process.exit(0); }); @@ -201,4 +187,38 @@ class Server { } } +const _log = console.log; + +console.logclean = function (...args) { + _log.call(console, ...args); +} + +// console.log = function (...args) { +// // Get the caller location +// const stack = new Error().stack.split("\n")[2]; +// const match = stack.match(/(\/.*:\d+:\d+)/); +// let location = match ? match[1] : "unknown"; + +// // Remove CWD prefix +// while (location.startsWith("/")) { +// location = location.slice(1); +// } +// location = "/" + location + +// let cwd = process.cwd(); +// if (location.startsWith(cwd)) { +// location = location.slice(cwd.length); +// if (location.startsWith("/")) location = location.slice(1); +// } + +// _log.call(console, `[${location}]`, ...args); +// }; + +global.ServerError = class extends Error { + constructor(status, msg) { + super(msg); + this.status = status; + } +} + const server = new Server() \ No newline at end of file diff --git a/server/ws.js b/server/ws.js index 2cf8142..8dd88e9 100644 --- a/server/ws.js +++ b/server/ws.js @@ -1,4 +1,4 @@ -import { WebSocket, WebSocketServer } from 'ws'; +const { WebSocket, WebSocketServer } = require('ws'); let wss; diff --git a/ui/public/components/SignupForm.js b/ui/public/components/SignupForm.js index 2066b6a..73904bf 100644 --- a/ui/public/components/SignupForm.js +++ b/ui/public/components/SignupForm.js @@ -15,11 +15,11 @@ class SignupForm extends Shadow { VStack(() => { input("First Name") - .attr({name: "firstname", type: "name"}) + .attr({name: "firstName", type: "name"}) .styles(this.inputStyles) input("Last Name") - .attr({name: "lastname", type: "name"}) + .attr({name: "lastName", type: "name"}) .styles(this.inputStyles) input("Email") diff --git a/ui/public/index.html b/ui/public/index.html index 28a9064..a06b9c2 100644 --- a/ui/public/index.html +++ b/ui/public/index.html @@ -4,17 +4,17 @@ Hyperia - + - - + + diff --git a/ui/public/pages/Home.js b/ui/public/pages/Home.js index 6456b23..bdff080 100644 --- a/ui/public/pages/Home.js +++ b/ui/public/pages/Home.js @@ -12,7 +12,7 @@ class Home extends Shadow { NavBar() - img("_/icons/logo.svg", "2.5em") + img("/_/icons/logo.svg", "2.5em") .onClick((done) => { if(!done) return window.navigateTo("/") @@ -24,7 +24,7 @@ class Home extends Shadow { switch(window.location.pathname) { case "/": - img("_/images/knight.png", "29vmax") + img("/_/images/knight.png", "29vmax") .position("absolute") .left(50, vw).top(50, vh) .center() @@ -51,13 +51,12 @@ class Home extends Shadow { case "/join": Join() break; - case "/signin": - SignIn() - break; default: if(window.location.pathname.startsWith("/signup")) { SignupForm() + } else if(window.location.pathname.startsWith("/signin")) { + SignIn() } } diff --git a/ui/public/pages/SIgnIn.js b/ui/public/pages/SIgnIn.js index cf93d85..b936589 100644 --- a/ui/public/pages/SIgnIn.js +++ b/ui/public/pages/SIgnIn.js @@ -8,6 +8,12 @@ class SignIn extends Shadow { render() { ZStack(() => { + if(window.location.search.includes("new")) { + p("Welcome to Hyperia! You may now log in.") + .x(50, vw).y(40, vh) + .center() + } + form(() => { input("Email") .attr({name: "email", type: "email"})