summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMhykol <mchaeldonald62@pm.me>2026-01-08 07:23:16 -0500
committerMhykol <mchaeldonald62@pm.me>2026-01-08 07:23:16 -0500
commitf4f49cd244d2f840cbb79a9286973b5c98d4f534 (patch)
tree2d7b7111d0f998ee72a8cd0106d4609a254a125c
Initial Commit
-rw-r--r--.gitignore12
-rw-r--r--README.md0
-rw-r--r--app.js234
-rw-r--r--assets/js/page-init.js102
-rw-r--r--package.json37
-rw-r--r--src/buildpages.js178
-rw-r--r--src/controller.js70
-rw-r--r--src/logger.js37
-rw-r--r--src/routes.js47
-rw-r--r--src/session.js31
-rw-r--r--src/sitemap.js32
11 files changed, 780 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a36a6f8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+.env
+node_modules
+config/*
+instances/*
+routes/*
+pages/*
+template/*
+assets/*
+backup/*
+package-lock.json
+notes.md
+*.log
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/README.md
diff --git a/app.js b/app.js
new file mode 100644
index 0000000..f6a6122
--- /dev/null
+++ b/app.js
@@ -0,0 +1,234 @@
+const express = require('express')
+const app = express()
+const rateLimit = require('express-rate-limit')
+const helmet = require('helmet')
+const bp = require('body-parser')
+const cookieParser = require('cookie-parser')
+const fs = require('fs')
+const path = require('path')
+const { readdirSync } = require('fs')
+const session = require('./src/session')()
+const routesConfig = require('./config/routes')
+const mime = {
+ html: 'text/html',
+ txt: 'text/plain',
+ css: 'text/css',
+ xml: 'text/xml',
+ gif: 'image/gif',
+ jpg: 'image/jpeg',
+ png: 'image/png',
+ webp: 'image/webp',
+ avif: 'image/avif',
+ ico: 'image/ico',
+ svg: 'image/svg+xml',
+ js: 'application/javascript',
+ mp4: 'video/mp4',
+ webm: 'video/webm',
+ ttf: 'application/x-font-truetype',
+ ttc: 'application/x-font-truetype',
+ otf: 'application/x-font-otf',
+ ots: 'application/x-font-otf',
+ woff: 'application/font-woff',
+ woff2: 'application/font-woff',
+ pfb: 'application/vnd.ms-fontbook',
+ pfm: 'application/vnd.ms-fontbook',
+ gsf: 'application/vnd.ms-fontbook',
+ swf: 'application/vnd.ms-fontbook'
+}
+const assetDir = () => {
+ return readdirSync(`${__dirname}/assets`, { withFileTypes: true })
+ .filter(dir => dir.isDirectory())
+ .map(dir => dir.name)
+}
+
+/*
+const limiter = rateLimit({
+ windowMS: 30 * 1000,
+ max: 100,
+ message: '<h1>Rate Limit Exceeded</h1><p>You have exceeded the allowed number of requests. Please try again later.</p>'
+})
+*/
+
+require('dotenv').config()
+const sitemap = require('./src/sitemap')
+const logger = require('./src/logger')({
+ baseURL: process.env.baseURL
+})
+const controller = require('./src/controller')({
+ baseURL: process.env.baseURL
+})
+
+const createLog = (req, res, next) => {
+ const { cookies } = req
+ const object = {
+ queryString: '',
+ exists: false
+ }
+
+ Object.entries(req.query).forEach(([key, value]) => {
+ if (object.exists === false) {
+ object.queryString += '?'
+ }
+ object.queryString += `${key}=${value}&`
+ object.exists = true
+ })
+
+ if (object.exists) object.queryString = object.queryString.slice(0, -1)
+
+ logger.Info(`${req.method} ${req.path}${object.queryString} from ${req.ip}:${cookies.session_id ? cookies.session_id.replace('session_id', '') : 'n/a'}`)
+ next()
+}
+const validateCookie = (req, res, next) => {
+ const { cookies } = req
+ if (cookies === undefined) {
+ res.cookie('session_id', session.Create())
+ } else {
+ if ('session_id' in cookies) {
+ if (session.Exists(cookies.session_id.replace('session_id', ''))) {
+ session.Expired()
+ } else {
+ res.clearCookie('session_id')
+ res.cookie('session_id', session.Create())
+ }
+ } else res.cookie('session_id', session.Create())
+ }
+ next()
+}
+
+const openFile = x => {
+ const data = {
+ path: x.filePath.split('/'),
+ exists: false,
+ data: {}
+ }
+
+ if (assetDir().includes(data.path[1])) {
+ this.path = `${__dirname}/assets/${data.path[1]}/${data.path[2]}`
+ data.exists = fs.existsSync(this.path)
+ data.data.mime = mime[path.extname(this.path).slice(1)] || 'text/plain'
+ data.data.file = fs.createReadStream(this.path)
+ .on('error', (err) => {
+ return
+ })
+ } else if (data.path[1] === 'favicon.ico' && (data.path.length === 2 || (data.path.length === 3 && data.path[2] === ''))) {
+ data.data.file = fs.createReadStream(`${__dirname}/config/favicon.ico`)
+ .on('error', (err) => {
+ logger.Error(`'favicon.ico' does not exist. Please add it to the config.`)
+ })
+ }
+
+ if (data.exists) {
+ x.res.set('Content-Type', data.data.mime)
+ data.data.file
+ .on('open', () => {
+ data.data.file.pipe(x.res)
+ })
+ .on('error', err => {
+ logger.Error(err)
+ })
+ } else {
+ x.res.set('Content-Type', mime.html)
+ x.res.status(404).send('File not found.')
+ }
+}
+
+app.use(bp.json())
+app.use(cookieParser())
+
+app.use((req, res, next) => {
+ let err = null
+ try {
+ decodeURIComponent(req.path)
+ } catch (e) {
+ err = e
+ }
+
+ if (err) {
+ return res.redirect(['http://', req.get('Host'), '/404'].join(''))
+ }
+ next()
+})
+
+const handleRoutes = (req, res, next) => {
+ this.path = {
+ split: req.path.split('/'),
+ string: req.path
+ }
+ if(routes.has(this.path.string)) {
+ const route = routes.get(this.path.string)
+ if (route.req === req.method) {
+ res.send(route.method())
+ } else {
+ next()
+ }
+ } else {
+ next()
+ }
+}
+
+const routes = routesConfig({
+ app: app,
+ validateCookie: validateCookie,
+ log: createLog,
+ mime: mime,
+ session: session
+})
+app.route(/(.*)/)
+ .get(validateCookie, createLog, (req, res) => {
+ this.path = {
+ split: req.path.split('/'),
+ string: req.path
+ }
+
+ if (assetDir().includes(this.path.split[1])) {
+ openFile({
+ filePath: this.path.string,
+ res: res
+ })
+ } else {
+ switch (this.path.split[1]) {
+ case 'favicon.ico':
+ openFile({
+ filePath: this.path.string,
+ res: res
+ })
+ break
+ case 'sitemap.xml':
+ const data = sitemap({baseURL: process.env.baseURL})
+ data.Sitemap().then(output => {
+ res.send(output)
+ })
+ break
+ case 'rss':
+ rss({
+ baseURL: process.env.baseURL
+ })
+ break
+ case 'deverror':
+ break
+ default:
+ const response = controller.Init({
+ req: req,
+ res: res,
+ path: this.path.string,
+ baseURL: process.env.baseURL
+ })
+
+ res.send(response)
+ break
+ }
+ }
+ })
+ .post(createLog, (req, res) => {})
+ .put(createLog, (req, res) => {})
+ .delete(createLog, (req, res) => {})
+ .patch(createLog, (req, res) => {})
+ .all(createLog, (req, res) => {
+ res.set('Content-Type', mime.html)
+ res.status(404).send('You are not allowed to access this resource')
+ })
+
+app.listen(process.env.PORT, () => {
+ logger.Info(`Listening on: ${process.env.baseURL}`)
+})
+
diff --git a/assets/js/page-init.js b/assets/js/page-init.js
new file mode 100644
index 0000000..207393a
--- /dev/null
+++ b/assets/js/page-init.js
@@ -0,0 +1,102 @@
+class Init {
+ constructor() {
+ this.object = {}
+ this.settings = {
+ scroll: 0
+ }
+ this.html = {}
+ }
+
+ GetSettings() {return this.settings}
+ async Start() {
+ this.fetch = `${window.location.pathname}?page=index`
+
+ await fetch(this.fetch, {
+ method: 'GET',
+ headers: new Headers({
+ 'Content-Type': 'application/json'
+ })
+ })
+ .then(res => res.json())
+ .then(output => {
+ this.output = output
+ this.object.head = document.getElementsByTagName('head')[0]
+ this.object.body = document.getElementsByTagName('body')[0]
+ this.object.header = document.getElementsByTagName('header')[0]
+ this.object.main = document.getElementsByTagName('main')[0]
+ this.object.footer = document.getElementsByTagName('footer')[0]
+
+ this.object.header.innerHTML += output.html.header
+ this.object.main.innerHTML = output.html.main
+ this.object.footer.innerHTML += output.html.footer
+ this.object.nav = document.getElementById('header')
+ this.object.nav.style.opacity = 0
+
+ this.#LoadCSS(output.css).then(() => {
+ this.output.js.map(file => {
+ const script = document.createElement('script')
+ script.src = `${window.location.origin}/js/${file}`
+ this.object.body.appendChild(script)
+ })
+ })
+ this.#Scroll()
+ })
+ }
+
+ #LoadElements() {
+ setTimeout(() => {
+ this.object.nav.style.opacity = 1
+ }, 750)
+ }
+
+ async Startup(data) {
+ const promise = new Promise((res, rej) => {
+ const map = new Map()
+
+ try {
+ data.Stop()
+ map.set('status', 'success')
+ map.set('msg', 'success')
+ this.#LoadElements()
+ res(map)
+ } catch (err) {
+ map.set('status', 'error')
+ map.set('msg', err)
+ // ! Dev mode
+ rej(map)
+ }
+ })
+ return await promise
+ }
+
+
+
+ #Scroll() {window.addEventListener('scroll', () => this.settings.scroll = window.scrollY)}
+ async #Error() {
+ this.fetch = `${window.location.pathname}?page=index`;
+ await fetch(this.fetch, {
+ method: 'POST',
+ headers: new Headers({
+ 'Content-Type': 'application/json'
+ })
+ })
+ .then(res => console.log(res))
+ }
+ async #LoadCSS(files) {
+ const promises = files.map(file => {
+ return new Promise(res => {
+ const link = document.createElement('link')
+ link.rel = 'stylesheet'
+ link.type = 'text/css'
+ link.href = `${window.location.origin}/css/${file}`
+ this.object.head.appendChild(link)
+ link.onload = res
+ })
+ })
+ return Promise.all(promises)
+ }
+}
+
+const app = new Init()
+app.Start()
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..51c3487
--- /dev/null
+++ b/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "website",
+ "version": "1.0.0",
+ "description": "",
+ "main": "app.js",
+ "scripts": {
+ "web:dev": "nodemon app.js",
+ "web:prod": "node app.js",
+ "assets:dev": "nodemon ./instances/assets.js",
+ "assets:prod": "node instances/assets.js",
+ "start": "npm-run-all --parallel web:dev assets:dev",
+ "prod": "npm-run-all --parallel web:prod",
+ "update": "git pull; npm i; npm audit fix"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "type": "commonjs",
+ "dependencies": {
+ "body-parser": "^2.2.1",
+ "chalk": "^5.6.2",
+ "cookie-parser": "^1.4.7",
+ "dompurify": "^3.3.1",
+ "dotenv": "^17.2.3",
+ "express": "^5.2.1",
+ "express-rate-limit": "^8.2.1",
+ "helmet": "^8.1.0",
+ "markdown-it": "^14.1.0",
+ "mysql2": "^3.16.0",
+ "nodemailer": "^7.0.11",
+ "nodemon": "^3.1.11",
+ "npm-run-all": "^4.1.5",
+ "rss": "^1.2.2",
+ "sitemap": "^9.0.0",
+ "winston": "^3.19.0"
+ }
+}
diff --git a/src/buildpages.js b/src/buildpages.js
new file mode 100644
index 0000000..89409ec
--- /dev/null
+++ b/src/buildpages.js
@@ -0,0 +1,178 @@
+const fs = require('fs')
+const path = require('path')
+const logger = require('./logger')()
+const template = require('../template/template')
+
+class BuildPages {
+ constructor(x) {
+ this.newTemplate = new Map()
+ this.template = template({baseURL: x.baseURL})
+ this.pages = this.#GetAllPages()
+
+ this.path = []
+ this.pages.forEach((value, key) => {
+ this.path.push(key)
+ })
+ this.routes = x.routes
+
+ this.#AddTemplate({
+ name: 'start',
+ template: `
+ <!DOCTYPE html>
+ <html>
+ `
+ })
+ this.html = {
+ start: `
+ <!DOCTYPE html>
+ <html>
+ `,
+ end: `
+ </html>
+ `
+ }
+ }
+
+ GetSitemap() {return this.path}
+
+ Build(pageData) {
+ if (this.routes.has(pageData.path)) {
+ } else if (this.pages.has(pageData.path)) {
+ const pages = this.pages.get(pageData.path)
+ const data = pages({
+ req: '',
+ res: '',
+ next: '',
+ baseURL: pageData.baseURL
+ })
+
+ this.map = new Map()
+ for (let i = 0; i < data.page.length; i++) {
+ this.object = {}
+ this.object.data = data.page[i]
+ this.object.name = this.object.data.name
+
+ if (pageData.path === '/') {
+ if (this.object.name === 'index') {
+ this.data = this.object.data()
+ this.output = {
+ html: this.data.html,
+ css: this.data.css,
+ js: this.data.js
+ }
+ this.output.js.push('loading.js')
+
+ return this.output
+ }
+ } else {
+ this.data = this.object.data()
+ this.output = {
+ html: this.data.html,
+ css: this.data.css,
+ js: this.data.js
+ }
+ this.output.js.push('loading.js')
+ return this.output
+ }
+ }
+ } else {
+ // ! 404 Not Found
+ const notFound = this.template.NotFound()
+ //const css = notFound.css
+
+ this.output = {
+ html: {
+ header: this.template.Header(),
+ main: notFound.html,
+ footer: this.template.Footer()
+ },
+ css: [],
+ js: ['loading.js']
+ }
+
+ notFound.css.forEach(file => this.output.css.push(file))
+ notFound.js.forEach(file => this.output.js.push(file))
+
+ return this.output
+ }
+ return false
+ }
+
+ Template(pageData) {
+ const html = {
+ start: `
+ <!DOCTYPE html>
+ <html>
+ `,
+ head: `
+ <head>
+ <title>website</title>
+ <link rel="stylesheet" type="text/css" href="/css/loading.css">
+ </head>
+ `,
+ headerStart: `
+ <body>
+ <header>
+ `,
+ headerEnd: `
+ </header>
+ `,
+ mainStart: `
+ <main>
+ `,
+ mainEnd: `
+ </main>
+ `,
+ footerStart: `
+ <footer>
+ `,
+ footerEnd: `
+ </footer>
+ <script src='/js/page-init.js'></script>
+ <script src='/js/forge.min.js'></script>
+ </body>
+ `,
+ end: `
+ </html>
+ `
+ }
+
+ html.headerEnd = this.template.Loading() + html.headerEnd
+
+ let fullHTML = ''
+ Object.entries(html).forEach(([key, value]) => {
+ fullHTML += value
+ })
+
+ return this.#RemoveSpaces(fullHTML)
+ }
+
+ #AddTemplate() {
+ this.newTemplate.set()
+ }
+ #RemoveSpaces(html) {return html.replace(/(\n)\s+/g, '$1')}
+ #GetAllPages() {
+ const object = {
+ modulesDir: path.resolve(`${__dirname}/../pages`),
+ map: new Map()
+ }
+ object.files = fs.readdirSync(object.modulesDir).filter(file => file.endsWith('.js'))
+
+ object.files.forEach(file => {
+ try {
+ const name = file.replace('.js', '')
+ const modulePath = path.join(object.modulesDir, file)
+ const moduleObject = require(modulePath)
+ this.path = '/'
+ if (name !== 'home') this.path = `/${name}`
+ if (!object.map.has(this.path)) object.map.set(this.path, moduleObject)
+ } catch (err) {
+ logger.Error(err)
+ }
+ })
+ return object.map
+ }
+}
+
+module.exports = (x) => {return new BuildPages(x)}
+
diff --git a/src/controller.js b/src/controller.js
new file mode 100644
index 0000000..9d81baf
--- /dev/null
+++ b/src/controller.js
@@ -0,0 +1,70 @@
+const buildPages = require('./buildpages')
+// ! Maybe write a library to create html elements
+
+class Controller {
+ constructor(data) {
+ this.baseURL = data.baseURL
+ this.data = {
+ req: data.req,
+ res: data.res
+ }
+ this.buildPages = buildPages({
+ baseURL: data.baseURL,
+ routes: new Map()
+ })
+ }
+
+ Init(data) {
+ const template = this.#GetTemplate(data)
+ return template
+ }
+
+ #GetTemplate(data) {
+ const query = new Map()
+ Object.entries(data.req.query).forEach(([key, value]) => {
+ query.set(key, value)
+ })
+
+ const object = {}
+
+ if (query.size > 0) {
+ object.req = data.req
+ object.res = data.res
+ object.path = data.path
+ object.query = query
+ object.baseURL = data.baseURL
+
+ const output = JSON.stringify(this.buildPages.Build(object))
+ return output
+ } else {
+ object.req = data.req
+ object.res = data.res
+ object.path = data.path
+ object.query = null
+ object.baseURL = data.baseURL
+
+ return this.buildPages.Template(object)
+ }
+ }
+ #GetRoutes() {
+ // ! Check if output is a `Map()`
+ const moduleObject = require(path.resolve(`${__dirname}/../config/routes`))
+ return moduleObject
+ /*
+ fs.readdirSync(path.resolve(`${__dirname}`))
+
+ const object = {
+ modulesDir: path.resolve(`${__dirname}/../routes`),
+ map: new Map()
+ }
+ object.files = fs.readdirSync(object.modulesDir).filter(file => file.endsWith('.js'))
+ const routes = new Map()
+
+
+ return
+ */
+ }
+}
+
+module.exports = (x) => {return new Controller(x)}
+
diff --git a/src/logger.js b/src/logger.js
new file mode 100644
index 0000000..c636681
--- /dev/null
+++ b/src/logger.js
@@ -0,0 +1,37 @@
+const winston = require('winston')
+const chalk = require('chalk')
+
+class Logger {
+ constructor(x) {
+ this.logger = winston.createLogger({
+ level: 'info',
+ format: winston.format.json(),
+ defaultMeta: {service: 'user-service'},
+ transports: [
+ new winston.transports.File({filename: 'error.log', level: 'error'}),
+ new winston.transports.File({filename: 'combined.log'})
+ ]
+ })
+ //if (process.env.NODE_ENV !== 'prod') {this.logger.add(new winston.transports.Console({format: winston.format.simple()}))}
+ }
+
+ Error(x) {
+ if (process.env.NODE_ENV !== 'prod') {
+ //console.error(chalk.red(x))
+ console.error(x)
+ } else {
+ this.logger.error(x)
+ }
+ }
+ Info(x) {
+ if (process.env.NODE_ENV !== 'prod') {
+ //console.log(chalk.yellow(x))
+ console.log(x)
+ } else {
+ this.logger.info(x)
+ }
+ }
+}
+
+module.exports = (x) => {return new Logger(x)}
+
diff --git a/src/routes.js b/src/routes.js
new file mode 100644
index 0000000..42d26c1
--- /dev/null
+++ b/src/routes.js
@@ -0,0 +1,47 @@
+const fs = require('fs')
+const path = require('path')
+const logger = require('./logger')()
+
+class Routes {
+ constructor(x) {
+ const object = {
+ routesDir: path.resolve(`${__dirname}/../routes`),
+ }
+ object.files = fs.readdirSync(object.routesDir).filter(file => file.endsWith('.js'))
+
+ object.files.forEach(file => {
+ try {
+ console.log(file)
+ } catch (err) {
+ logger.Error(err)
+ }
+ })
+
+ this.app = x.app
+ this.routes = new Map()
+
+
+ }
+
+ Route() {return new Map()}
+}
+
+module.exports = (x) => {
+ const routes = new Routes({
+ app: x.app
+ })
+
+ const createRoute = () => {
+ const temp = routes.CreateRoute({
+ path: x.path,
+ req: req,
+ methods: x.methods
+ })
+
+ console.log(temp)
+ }
+
+ return {
+ createRoute: createRoute
+ }
+}
diff --git a/src/session.js b/src/session.js
new file mode 100644
index 0000000..7fec352
--- /dev/null
+++ b/src/session.js
@@ -0,0 +1,31 @@
+const sessions = new Map()
+
+class Session {
+ constructor() {}
+
+ Create() {
+ const object = {
+ result: '',
+ characters: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
+ }
+
+ for (let i = 0; i < object.characters.length; i++) object.result += object.characters.charAt(Math.floor(Math.random() * object.characters.length))
+ sessions.set(object.result, Date.now() + (1000 * 60 * 60))
+ return object.result
+ }
+ Exists(x) {
+ if (sessions.has(x)) {
+ sessions.set(x, Date.now() + (1000 * 60 * 60))
+ return true
+ } else {
+ return false
+ }
+ }
+ Expired() {sessions.forEach((value, key) => {if (Date.now() > value) sessions.delete(key)})}
+ GetSessions() {
+ return sessions
+ }
+}
+
+module.exports = () => {return new Session()}
+
diff --git a/src/sitemap.js b/src/sitemap.js
new file mode 100644
index 0000000..1c07f99
--- /dev/null
+++ b/src/sitemap.js
@@ -0,0 +1,32 @@
+const buildPages = require('./buildpages')
+const { SitemapStream, streamToPromise } = require('sitemap')
+const { Readable } = require('stream')
+
+class Sitemap {
+ constructor(data) {
+ this.baseURL = data.baseURL
+ this.buildPages = buildPages({baseURL: this.baseURL})
+ this.pages = this.#GetSitemap()
+ }
+
+ Sitemap() {return this.pages}
+
+ #GetSitemap() {
+ const pages = this.buildPages.GetSitemap()
+ const object = {
+ links: [],
+ remove: ['/', '/blog'],
+ stream: new SitemapStream({hostname: this.baseURL})
+ }
+
+ for (let i = 0; i < pages.length; i++) {
+ // ! Default priority
+ object.links.push({url: this.baseURL + pages[i].replace('/', ''), changefreq: 'weekly', priority: 0.8})
+ }
+
+ return streamToPromise(Readable.from(object.links).pipe(object.stream)).then(data => {return data.toString()})
+ //return output
+ }
+}
+
+module.exports = (x) => {return new Sitemap(x)}