diff options
| -rw-r--r-- | .gitignore | 12 | ||||
| -rw-r--r-- | README.md | 0 | ||||
| -rw-r--r-- | app.js | 234 | ||||
| -rw-r--r-- | assets/js/page-init.js | 102 | ||||
| -rw-r--r-- | package.json | 37 | ||||
| -rw-r--r-- | src/buildpages.js | 178 | ||||
| -rw-r--r-- | src/controller.js | 70 | ||||
| -rw-r--r-- | src/logger.js | 37 | ||||
| -rw-r--r-- | src/routes.js | 47 | ||||
| -rw-r--r-- | src/session.js | 31 | ||||
| -rw-r--r-- | src/sitemap.js | 32 |
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 @@ -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)} |
