notherbase-fs 4.4.7 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/notherbase-fs.js CHANGED
@@ -1,30 +1,35 @@
1
1
  import dotenv from "dotenv";
2
2
  dotenv.config();
3
- import Models from "./models/index.js";
3
+ import Spirit from "./server/spirit.js";
4
+ import SendMail from "./server/send-mail.js";
4
5
  import { Server } from "socket.io";
5
6
  import express from "express";
6
7
  import http from 'http';
7
8
  import { fileURLToPath } from 'node:url';
8
9
  const __dirname = fileURLToPath(new URL('./', import.meta.url));
9
- import Creation from "./controllers/creation.js";
10
- import SpiritWorld from "./controllers/spirit-world.js";
10
+ import Creation from "./server/creation.js";
11
+ import SpiritWorld from "./server/spirit-world.js";
11
12
  import favicon from 'serve-favicon';
12
13
  import session from 'express-session';
13
14
  import MongoStore from 'connect-mongo';
14
15
 
15
16
 
16
17
  /**
17
- * The engine that runs a nother base.
18
+ * The engine that runs nother bases.
18
19
  */
19
20
  class NotherBaseFS {
20
21
  constructor(globals = {}, bases = {}) {
21
22
  this.bases = bases;
23
+
24
+ this.initServer();
25
+ }
22
26
 
27
+ initServer() {
23
28
  this.app = express();
24
29
  this.server = http.createServer(this.app);
25
30
  this.io = new Server(this.server);
26
31
  this.spiritWorld = new SpiritWorld(this.io);
27
- this.creation = new Creation(bases);
32
+ this.creation = new Creation(this.bases);
28
33
 
29
34
  //set view engine
30
35
  this.app.set("view engine", "ejs");
@@ -43,11 +48,12 @@ class NotherBaseFS {
43
48
  //be safe, needs to be before session
44
49
  if (process.env.PRODUCTION == "true") this.app.set('trust proxy', 2);
45
50
 
46
- let baseKeys = Object.keys(this.bases);
51
+ //provide multiple base support
47
52
  let store = MongoStore.create({ mongoUrl: process.env.MONGODB_URI });
53
+ let baseKeys = Object.keys(this.bases);
48
54
  for (let i = 0; i < baseKeys.length; i++) {
49
55
  this.bases[baseKeys[i]].static = express.static(this.bases[baseKeys[i]].directory + "/public");
50
- this.bases[baseKeys[i]].favicon = favicon(this.bases[baseKeys[i]].directory + this.bases[baseKeys[i]].icon);
56
+ if (this.bases[baseKeys[i]].icon) this.bases[baseKeys[i]].favicon = favicon(this.bases[baseKeys[i]].directory + this.bases[baseKeys[i]].icon);
51
57
  this.bases[baseKeys[i]].session = session({
52
58
  store: store,
53
59
  secret: process.env.SECRET,
@@ -61,39 +67,40 @@ class NotherBaseFS {
61
67
  });
62
68
  }
63
69
 
64
- //provide database access and etc to use in routes
70
+
65
71
  this.app.use((req, res, next) => {
66
72
  let split = req.hostname.split(".");
67
73
  if (split.length > 2) {
68
74
  if (split[split.length - 2].length < 3) req.hosting = split[split.length - 3] + split[split.length - 2];
69
75
  else req.hosting = split[split.length - 2];
70
76
  }
71
- else req.hosting = split[0];
77
+ else req.hosting = split[0];
72
78
 
73
79
  req.contentPath = this.bases[req.hosting].directory;
74
80
  next();
75
81
  });
76
82
 
83
+ // allows us to get static files like css
84
+ this.app.use((req, res, next) => {
85
+ this.bases[req.hosting].static(req, res, next);
86
+ });
87
+ this.app.use(express.static(`${__dirname}/public`));
88
+
77
89
  this.app.use((req, res, next) => {
78
- this.bases[req.hosting].favicon(req, res, next);
90
+ if (this.bases[req.hosting].favicon) this.bases[req.hosting].favicon(req, res, next);
91
+ else next();
79
92
  });
80
93
 
81
94
  //enable cookies
82
95
  this.app.use((req, res, next) => {
83
96
  this.bases[req.hosting].session(req, res, next);
84
97
  });
85
-
86
- // allows us to get static files like css
87
- this.app.use((req, res, next) => {
88
- this.bases[req.hosting].static(req, res, next);
89
- });
90
- this.app.use(express.static(`${__dirname}/public`));
91
98
 
92
99
  //provide database access and etc to use in routes
93
100
  this.app.use(async (req, res, next) => {
94
- req.globals = globals;
95
- req.db = Models;
96
- req.user = req.session?.currentUser ? await req.db.Spirit.recallOne("user", null, { username: req.session.currentUser }) : null;
101
+ req.Spirit = Spirit;
102
+ req.SendMail = SendMail;
103
+ req.user = req.session?.currentUser ? await req.Spirit.findOne({ service: "user", username: req.session.currentUser }) : null;
97
104
  req.lock = false;
98
105
  // enables sessions only if the protocol is https and we are in production, or if we are in development
99
106
  req.sessionsEnabled = (process.env.PRODUCTION == "true" && req.secure) || process.env.PRODUCTION == "false";
@@ -101,20 +108,20 @@ class NotherBaseFS {
101
108
  });
102
109
 
103
110
  //destroy session if it is not authorized
104
- this.app.use(async (req, res, next) => {
105
- if (req.session.currentUser) {
106
- if (req.user?.memory?.data?.sessions?.[req.session.id]) {
107
- if (req.user.memory.data.sessions[req.session.id] < Date.now()) {
108
- req.session.regenerate(() => {});
109
- delete req.user.memory.data.sessions[req.session.id];
110
- await req.user.commit();
111
- }
112
- }
113
- else req.session.regenerate(() => {});
114
- }
111
+ // this.app.use(async (req, res, next) => {
112
+ // if (req.session.currentUser) {
113
+ // if (req.user?.memory?.data?.sessions?.[req.session.id]) {
114
+ // if (req.user.memory.data.sessions[req.session.id] < Date.now()) {
115
+ // req.session.regenerate(() => {});
116
+ // delete req.user.memory.data.sessions[req.session.id];
117
+ // await req.user.commit();
118
+ // }
119
+ // }
120
+ // else req.session.regenerate(() => {});
121
+ // }
115
122
 
116
- next();
117
- });
123
+ // next();
124
+ // });
118
125
 
119
126
  //spirit world routes
120
127
  this.app.use("/s", this.spiritWorld.router);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "notherbase-fs",
3
- "version": "4.4.7",
3
+ "version": "4.5.0",
4
4
  "description": "Functions to help make developing for NotherBase easier.",
5
5
  "exports": "./notherbase-fs.js",
6
6
  "scripts": {
package/public/js/base.js CHANGED
@@ -108,52 +108,12 @@ class Base {
108
108
  * @param {ObjectID} id The id of the spirit to load.
109
109
  * @returns Spirit world response.
110
110
  */
111
- loadAll = async (service, scope = "local", data = {}, id = null) => {
112
- let response = await $.post("/s/loadAll", JSON.stringify({ service, scope, data, id }));
113
-
114
- return response;
115
- }
116
-
117
- /**
118
- * Loads a single spirit of a certain service.
119
- * @param {String} service The name of the spirit to load.
120
- * @param {String} scope Defaults to local, else global.
121
- * @param {Object} data Data to filter the spirits by.
122
- * @param {ObjectID} id The id of the spirit to load.
123
- * @returns Spirit world response.
124
- */
125
- load = async (service, scope = "local", data = {}, id = null) => {
111
+ load = async (service, scope = "local", data = null, id = null) => {
126
112
  let response = await $.post("/s/load", JSON.stringify({ service, scope, data, id }));
127
113
 
128
114
  return response;
129
115
  }
130
116
 
131
- /**
132
- * Saves a spirit of a certain service.
133
- * @param {String} service The name of the spirit to save.
134
- * @param {String} scope Defaults to local, else global.
135
- * @param {Object} data Data to save the spirit with.
136
- * @param {ObjectID} id The id of the spirit to save.
137
- * @returns Spirit world response.
138
- */
139
- save = async (service, scope = "local", data = {}, id = null) => {
140
- let response = await $.post("/s/save", JSON.stringify({ service, scope, data, id }));
141
- return response;
142
- }
143
-
144
- /**
145
- * Deletes a spirit of a certain service.
146
- * @param {String} service The name of the spirit to delete.
147
- * @param {String} scope Defaults to local, else global.
148
- * @param {Object} data Data to filter the spirits by.
149
- * @param {ObjectID} id The id of the spirit to delete.
150
- * @returns Spirit world response.
151
- */
152
- delete = async (service, scope = "local", data = {}, id = null) => {
153
- let response = await $.post("/s/delete", JSON.stringify({ service, scope, data, id }));
154
- return response;
155
- }
156
-
157
117
  /**
158
118
  * Appends html to the head.
159
119
  * @param {String} html The html to append.
@@ -0,0 +1,117 @@
1
+ import express from "express";
2
+ import fs from 'fs';
3
+
4
+ /**
5
+ * Creation is all the renedered pages in a base.
6
+ */
7
+ export default class Creation {
8
+ constructor(bases = {}) {
9
+ this.bases = bases;
10
+ this.router = express.Router();
11
+
12
+ this.router.get("/", this.explore);
13
+ this.router.get("/the-front", this.explore);
14
+ this.router.get(`/:region`, this.lock, this.explore);
15
+ this.router.get(`/:region/:area`, this.lock, this.explore);
16
+ this.router.get(`/:region/:area/:poi`, this.lock, this.explore);
17
+ this.router.get(`/:region/:area/:poi/:detail`, this.lock, this.explore);
18
+
19
+ //void
20
+ this.router.use(function(req, res) {
21
+ res.redirect("/void");
22
+ });
23
+ }
24
+
25
+ /**
26
+ * This middleware requires a user to login to access affected routes.
27
+ * @param {Object} req An Express.js request.
28
+ * @param {Object} res An Express.js response.
29
+ * @param {Function} next next()
30
+ */
31
+ lock = (req, res, next) => {
32
+ req.lock = true;
33
+ next();
34
+ }
35
+
36
+ /**
37
+ * This route renders a page and sends it to the client.
38
+ * @param {Object} req An Express.js request.
39
+ * @param {Object} res An Express.js response.
40
+ * @param {Function} next next()
41
+ */
42
+ explore = async (req, res, next) => {
43
+ try {
44
+ req.main = `index`;
45
+ if (req.params.detail) req.main = req.params.detail + "\\" + req.main;
46
+ if (req.params.poi) req.main = req.params.poi + "\\" + req.main;
47
+ if (req.params.area) req.main = req.params.area + "\\" + req.main;
48
+ if (req.params.region) req.main = req.params.region + "\\" + req.main;
49
+ else req.main = "the-front\\" + req.main;
50
+ req.main = req.contentPath + "\\" + req.main;
51
+ req.preprocess = "_preprocess";
52
+ if (req.params.detail) req.preprocess = req.params.detail + "\\" + req.preprocess;
53
+ if (req.params.poi) req.preprocess = req.params.poi + "\\" + req.preprocess;
54
+ if (req.params.area) req.preprocess = req.params.area + "\\" + req.preprocess;
55
+ if (req.params.region) req.preprocess = req.params.region + "\\" + req.preprocess;
56
+ else req.preprocess = "the-front\\" + req.preprocess;
57
+ req.preprocess = req.contentPath + "\\" + req.preprocess;
58
+ req.siteTitle = `${this.bases[req.hosting].title} - ${req.params.detail || req.params.poi || req.params.area || req.params.region || "Home"}`;
59
+ req.toRender = "explorer";
60
+
61
+ if (fs.existsSync(req.main + ".ejs")) {
62
+ let stats = await req.Spirit.findOne({ service: "stats", data: { route: req.path } });
63
+ if (stats == null) stats = new req.Spirit({ service: "stats", data: { route: req.path, visits: 0 } });
64
+ if (stats.data.visits) stats.data.visits++;
65
+ else stats.data.visits = 1;
66
+ await stats.commit();
67
+
68
+ let context = {
69
+ user: null,
70
+ siteTitle: req.siteTitle,
71
+ main: req.main,
72
+ query: req.query,
73
+ route: req.path,
74
+ requireUser: req.lock,
75
+ preprocessed: {}
76
+ }
77
+
78
+ if (req.session.currentUser) {
79
+ let user = req.user;
80
+ context.user = {
81
+ data: user.data,
82
+ backups: user.backups,
83
+ _id: user._id,
84
+ parent: user.parent,
85
+ service: user.service,
86
+ _lastUpdate: user._lastUpdate
87
+ }
88
+ }
89
+
90
+ //preprocess
91
+ let preprocessScripts = fs.existsSync(req.preprocess) ? fs.readdirSync(req.preprocess) : [];
92
+ for (let preprocessScript of preprocessScripts) {
93
+ try {
94
+ let scriptPath = `${req.preprocess}/${preprocessScript}`;
95
+
96
+ if (fs.existsSync(scriptPath)) {
97
+ let script = await import(process.env.WINDOWS == "true" ? `file://${scriptPath}` : scriptPath);
98
+ let result = await script.default(req, context.user, this.io);
99
+ context.preprocessed[preprocessScript.split(".")[0]] = result;
100
+ }
101
+ else context.preprocessed[preprocessScript.split(".")[0]] = `Error: Script Not Found`;
102
+ } catch (error) {
103
+ console.log(error);
104
+ context.preprocessed[preprocessScript.split(".")[0]] = `Error: Server Error`;
105
+ }
106
+ }
107
+
108
+ res.render(req.toRender, context);
109
+ }
110
+ else next();
111
+ }
112
+ catch(err) {
113
+ console.log(err);
114
+ res.status(500).send("Server Error");
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,147 @@
1
+ import express from "express";
2
+ import { stripHtml } from "string-strip-html";
3
+ import { success, fail } from "./util.js";
4
+ import User from "./user.js";
5
+ import fs from 'fs';
6
+
7
+ /**
8
+ * The spirit world is the API of a base.
9
+ */
10
+ export default class SpiritWorld {
11
+ /**
12
+ * Sets up the spirit world.
13
+ * @param {Server} io
14
+ */
15
+ constructor(io) {
16
+ this.io = io;
17
+ this.rooms = {};
18
+ this.io.on('connection', this.setupChat);
19
+
20
+ this.user = new User();
21
+ this.router = express.Router();
22
+ this.router.post("/load", this.load);
23
+ this.router.post("/serve", this.serve);
24
+ this.router.use("/user", this.user.router);
25
+ }
26
+
27
+ /**
28
+ * Sets up socket.io for instant messaging and etc.
29
+ * @param {*} socket
30
+ */
31
+ setupChat = (socket) => {
32
+ try {
33
+ let roomName = socket.handshake.query.room;
34
+ socket.join(roomName);
35
+ let room = this.rooms[roomName];
36
+ if (room) room.users.push(socket.handshake.query.name);
37
+ else {
38
+ this.rooms[roomName] = {
39
+ users: [ socket.handshake.query.name ]
40
+ }
41
+ room = this.rooms[roomName];
42
+ }
43
+
44
+ this.io.to(roomName).emit("chat message", {
45
+ name: "Server",
46
+ time: Date.now(),
47
+ text: `${socket.handshake.query.name} has joined the room.`
48
+ });
49
+
50
+ this.io.to(roomName).emit("chat info", {
51
+ name: socket.handshake.query.room,
52
+ time: Date.now(),
53
+ data: {
54
+ users: room.users
55
+ }
56
+ });
57
+
58
+ socket.on("chat message", (msg) => {
59
+ this.io.to(roomName).emit("chat message", {
60
+ name: msg.name,
61
+ time: msg.time,
62
+ text: stripHtml(msg.text).result
63
+ });
64
+ });
65
+
66
+ socket.on('disconnect', () => {
67
+ room.users.splice(room.users.indexOf(socket.handshake.query.name));
68
+
69
+ this.io.to(roomName).emit("chat message", {
70
+ name: "Server",
71
+ time: Date.now(),
72
+ text: `${socket.handshake.query.name} has left the room.`
73
+ });
74
+
75
+ this.io.to(roomName).emit("chat info", {
76
+ name: socket.handshake.query.room,
77
+ time: Date.now(),
78
+ data: {
79
+ users: room.users
80
+ }
81
+ });
82
+
83
+ if (room.users.length < 1) delete this.rooms[roomName];
84
+ });
85
+ } catch (error) {
86
+ console.log(error);
87
+ fail(res, "Server error: Sockets");
88
+ }
89
+ }
90
+
91
+ /**
92
+ * This API route requests all spirits of a kind from the database.
93
+ * @param {Object} req
94
+ * @param {Object} res
95
+ * @returns {Object} The requested spirits.
96
+ */
97
+ load = async (req, res) => {
98
+ try {
99
+ let query = {
100
+ service: req.body.service,
101
+ parent: null
102
+ };
103
+ if (req.body.data) query.data = req.body.data;
104
+ if (req.body.id) query._id = req.body.id;
105
+
106
+ // if the scope is local, the parent is the user's id
107
+ if (req.body.scope === "local") {
108
+ if (req.user?._id) query.parent = req.user._id;
109
+ else {
110
+ fail(res, "User had no id on load()");
111
+ return;
112
+ }
113
+ }
114
+
115
+ // recall all spirits with the given service name and parent
116
+ let spirits = await req.Spirit.find(query);
117
+
118
+ res.send(spirits);
119
+ } catch (error) {
120
+ console.log(error);
121
+ fail(res, "Server error");
122
+ }
123
+ }
124
+
125
+ /**
126
+ * This API route runs a script on the server. Responds with the result.
127
+ * @param {Object} req
128
+ * @param {Object} res
129
+ */
130
+ serve = async (req, res) => {
131
+ try {
132
+ let scriptPath = `${req.contentPath}${req.body.route}/${req.body.script}.js`;
133
+
134
+ let script, result = null;
135
+
136
+ if (fs.existsSync(scriptPath)) {
137
+ script = await import(process.env.WINDOWS == "true" ? `file://${scriptPath}` : scriptPath);
138
+ result = await script.default(req, req.user, this.io);
139
+ success(res, "Served.", result);
140
+ }
141
+ else fail(res, `Script not found: ${req.body.script} at ${scriptPath}`);
142
+ } catch (error) {
143
+ console.log(error);
144
+ fail(res, "Server error");
145
+ }
146
+ }
147
+ }
@@ -0,0 +1,83 @@
1
+ import mongoose from "mongoose";
2
+ import dotenv from "dotenv";
3
+ dotenv.config();
4
+
5
+
6
+ // DB setup
7
+ mongoose.set('strictQuery', true);
8
+ mongoose.connection.on('connected', (err) => {
9
+ console.log(`Mongoose connected to db`);
10
+ });
11
+ mongoose.connection.on('error', (err) => {
12
+ console.log(`Mongoose ${err}`);
13
+ });
14
+ mongoose.connection.on('disconnected', () => {
15
+ console.log('Mongoose disconnected');
16
+ });
17
+ try {
18
+ mongoose.connect(process.env.MONGODB_URI);
19
+ }
20
+ catch (err) {
21
+ console.log(`Mongoose on connect: ${err}`);
22
+ }
23
+
24
+ /**
25
+ * Curated Mongoose.js documents for use in bases.
26
+ */
27
+ let spiritSchema = new mongoose.Schema({
28
+ _lastUpdate: Number,
29
+ service: String,
30
+ parent: {
31
+ type: mongoose.Schema.Types.ObjectId,
32
+ ref: "spirits",
33
+ required: false
34
+ },
35
+ data: {},
36
+ backups: []
37
+ });
38
+
39
+ /**
40
+ * Commits new data to a spirit.
41
+ * @param {Object} data Data to save.
42
+ * @returns Updated
43
+ */
44
+ spiritSchema.method("commit", async function(data = this.data, maxBackups = 5) {
45
+ this._lastUpdate = Date.now();
46
+
47
+ this.backups.unshift({
48
+ _lastUpdate: Date.now(),
49
+ data: this.data
50
+ });
51
+ if (maxBackups > 1) {
52
+ while (this.backups.length > maxBackups) this.backups.pop();
53
+ }
54
+ this.data = data;
55
+ this.markModified("backups");
56
+ this.markModified("data");
57
+ await this.save();
58
+
59
+ return "Updated";
60
+ });
61
+
62
+ /**
63
+ * Restores a backup by its index.
64
+ * @param {Number} backupIndex The index of the backup to restore.
65
+ * @returns Status message.
66
+ */
67
+ spiritSchema.method("restoreBackup", async function(backupIndex) {
68
+ if (backupIndex < 0 || backupIndex >= this.backups.length) return "Invalid backup index";
69
+ let backup = this.backups[backupIndex];
70
+ this.backups.unshift({
71
+ _lastUpdate: Date.now(),
72
+ data: this.data
73
+ });
74
+ this.data = backup.data;
75
+ this.markModified("backups");
76
+ this.markModified("data");
77
+ await this.save();
78
+ return "Restored";
79
+ });
80
+
81
+ let Spirit = mongoose.model('spirits', spiritSchema);
82
+
83
+ export default Spirit;