diff --git a/Goobieverse/.editorconfig b/Goobieverse/.editorconfig
new file mode 100644
index 00000000..e717f5eb
--- /dev/null
+++ b/Goobieverse/.editorconfig
@@ -0,0 +1,13 @@
+# http://editorconfig.org
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/Goobieverse/.env.local.default b/Goobieverse/.env.local.default
new file mode 100644
index 00000000..0acb07ea
--- /dev/null
+++ b/Goobieverse/.env.local.default
@@ -0,0 +1,31 @@
+
+# DB variables -------------------
+DB_HOST=localhost
+DB_PORT=27017
+DB_NAME=tester
+DB_USER=metaverse
+DB_PASSWORD=nooneknowsit
+DB_AUTHDB=admin
+DATABASE_URL=
+
+# Authentication variables -------------------
+AUTH_SECRET=M7iszvSxttilkptn22GT4/NbFGY=
+
+# Server variables ---------------
+SERVER_HOST=localhost
+SERVER_PORT=3030
+SERVER_VERSION=1.1.1-20200101-abcdefg
+
+# General ------------------------
+LOCAL=true
+APP_ENV=development
+PUBLIC_PATH=./public
+
+# Metaverse ------------------------
+METAVERSE_NAME=Vircadia noobie
+METAVERSE_NICK_NAME=Noobie
+METAVERSE_SERVER_URL=
+DEFAULT_ICE_SERVER_URL=
+DASHBOARD_URL=https://dashboard.vircadia.com
+LISTEN_HOST=0.0.0.0
+LISTEN_PORT=9400
\ No newline at end of file
diff --git a/Goobieverse/.eslintrc.json b/Goobieverse/.eslintrc.json
new file mode 100644
index 00000000..d0fbe201
--- /dev/null
+++ b/Goobieverse/.eslintrc.json
@@ -0,0 +1,38 @@
+{
+ "env": {
+ "es6": true,
+ "node": true,
+ "jest": true
+ },
+ "parserOptions": {
+ "parser": "@typescript-eslint/parser",
+ "ecmaVersion": 2018,
+ "sourceType": "module"
+ },
+ "plugins": [
+ "@typescript-eslint"
+ ],
+ "extends": [
+ "plugin:@typescript-eslint/recommended"
+ ],
+ "rules": {
+ "indent": [
+ "error",
+ 2
+ ],
+ "linebreak-style": [
+ "error",
+ "unix"
+ ],
+ "quotes": [
+ "error",
+ "single"
+ ],
+ "semi": [
+ "error",
+ "always"
+ ],
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-empty-interface": "off"
+ }
+}
diff --git a/Goobieverse/.gitignore b/Goobieverse/.gitignore
new file mode 100644
index 00000000..25d7c18a
--- /dev/null
+++ b/Goobieverse/.gitignore
@@ -0,0 +1,115 @@
+# Logs
+logs
+*.log
+
+# Runtime data
+pids
+*.pid
+*.seed
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directory
+# Commenting this out is preferred by some people, see
+# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
+node_modules
+
+# Users Environment Variables
+.lock-wscript
+
+# IDEs and editors (shamelessly copied from @angular/cli's .gitignore)
+/.idea
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# IDE - VSCode
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### OSX ###
+*.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Folder config file
+Desktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# Others
+lib/
+data/
+
+.env.local
+package-lock.json
\ No newline at end of file
diff --git a/Goobieverse/README.md b/Goobieverse/README.md
new file mode 100644
index 00000000..6a184037
--- /dev/null
+++ b/Goobieverse/README.md
@@ -0,0 +1,45 @@
+# Goobieverse
+
+>
+
+## About
+
+This project uses [Feathers](http://feathersjs.com). An open source web framework for building modern real-time applications.
+
+## Getting Started
+
+Getting up and running is as easy as 1, 2, 3.
+
+1. Make sure you have [NodeJS](https://nodejs.org/) and [npm](https://www.npmjs.com/) installed.
+2. Install your dependencies
+
+ ```
+ cd path/to/Goobieverse
+ npm install
+ ```
+
+3. Start your app
+
+ ```
+ npm start
+ ```
+
+## Testing
+
+Simply run `npm test` and all your tests in the `test/` directory will be run.
+
+## Scaffolding
+
+Feathers has a powerful command line interface. Here are a few things it can do:
+
+```
+$ npm install -g @feathersjs/cli # Install Feathers CLI
+
+$ feathers generate service # Generate a new Service
+$ feathers generate hook # Generate a new Hook
+$ feathers help # Show all commands
+```
+
+## Help
+
+For more information on all the things you can do with Feathers visit [docs.feathersjs.com](http://docs.feathersjs.com).
diff --git a/Goobieverse/build_scripts/createVersion.js b/Goobieverse/build_scripts/createVersion.js
new file mode 100644
index 00000000..7943714a
--- /dev/null
+++ b/Goobieverse/build_scripts/createVersion.js
@@ -0,0 +1,32 @@
+
+const fse = require('fs-extra');
+
+var gitVer = require('child_process').execSync('git rev-parse --short HEAD').toString().trim();
+var gitVerFull = require('child_process').execSync('git rev-parse HEAD').toString().trim();
+var packageVersion = process.env.npm_package_version;
+const filePath = './lib/metaverse_info.json';
+
+console.log('Found package version', packageVersion);
+console.log('Found Git commit short hash', gitVer);
+console.log('Found Git commit long hash', gitVerFull);
+
+function yyyymmdd() {
+ var x = new Date();
+ var y = x.getFullYear().toString();
+ var m = (x.getMonth() + 1).toString();
+ var d = x.getDate().toString();
+ (d.length == 1) && (d = '0' + d);
+ (m.length == 1) && (m = '0' + m);
+ var yyyymmdd = y + m + d;
+ return yyyymmdd;
+}
+
+var jsonToWrite = {
+ npm_package_version: packageVersion,
+ git_commit: gitVerFull,
+ version_tag: (packageVersion + '-' + yyyymmdd() + '-' + gitVer)
+};
+
+jsonToWrite = JSON.stringify(jsonToWrite);
+
+var attemptFileWrite = fse.outputFileSync(filePath, jsonToWrite);
diff --git a/Goobieverse/jest.config.js b/Goobieverse/jest.config.js
new file mode 100644
index 00000000..57070898
--- /dev/null
+++ b/Goobieverse/jest.config.js
@@ -0,0 +1,9 @@
+module.exports = {
+ preset: 'ts-jest',
+ testEnvironment: 'node',
+ globals: {
+ 'ts-jest': {
+ diagnostics: false
+ }
+ }
+};
diff --git a/Goobieverse/package.json b/Goobieverse/package.json
new file mode 100644
index 00000000..2ae24693
--- /dev/null
+++ b/Goobieverse/package.json
@@ -0,0 +1,94 @@
+{
+ "name": "goobie-verse",
+ "description": "Goobieverse",
+ "version": "0.0.1",
+ "homepage": "",
+ "private": true,
+ "main": "src",
+ "keywords": [
+ "feathers"
+ ],
+ "author": {
+ "name": "",
+ "email": ""
+ },
+ "contributors": [],
+ "bugs": {},
+ "directories": {
+ "lib": "src",
+ "test": "test/"
+ },
+ "engines": {
+ "node": "^16.0.0",
+ "npm": ">= 3.0.0"
+ },
+ "scripts": {
+ "test": "npm run lint && npm run compile && npm run jest",
+ "lint": "eslint src/. test/. --config .eslintrc.json --ext .ts --fix",
+ "dev": "ts-eager src/index.ts cross-env APP_ENV=development",
+ "start": "cross-env APP_ENV=prod npm run compile && node lib/",
+ "jest": "jest --forceExit",
+ "compile": "shx rm -rf lib/ && tsc",
+ "create-version": "node build_scripts/createVersion.js"
+ },
+ "standard": {
+ "env": [
+ "jest"
+ ],
+ "ignore": []
+ },
+ "types": "lib/",
+ "dependencies": {
+ "@feathersjs/authentication": "^4.5.11",
+ "@feathersjs/authentication-local": "^4.5.11",
+ "@feathersjs/authentication-oauth": "^4.5.11",
+ "@feathersjs/configuration": "^4.5.11",
+ "@feathersjs/errors": "^4.5.11",
+ "@feathersjs/express": "^4.5.11",
+ "@feathersjs/feathers": "^4.5.11",
+ "@feathersjs/socketio": "^4.5.11",
+ "@feathersjs/transport-commons": "^4.5.11",
+ "@types/dotenv-flow": "^3.2.0",
+ "app-root-path": "^3.0.0",
+ "bcrypt": "^5.0.1",
+ "compression": "^1.7.4",
+ "cors": "^2.8.5",
+ "cross-env": "^7.0.3",
+ "dotenv-flow": "^3.2.0",
+ "feathers-hooks-common": "^5.0.6",
+ "feathers-mailer": "^3.1.0",
+ "feathers-mongodb": "^6.4.1",
+ "helmet": "^4.6.0",
+ "mongodb": "^4.2.2",
+ "mongodb-core": "^3.2.7",
+ "nodemailer-smtp-transport": "^2.7.4",
+ "serve-favicon": "^2.5.0",
+ "trim": "^1.0.1",
+ "ts-eager": "^2.0.2",
+ "uuidv4": "^6.2.12",
+ "winston": "^3.3.3"
+ },
+ "devDependencies": {
+ "@types/app-root-path": "^1.2.4",
+ "@types/bcrypt": "^5.0.0",
+ "@types/compression": "^1.7.2",
+ "@types/cors": "^2.8.12",
+ "@types/dotenv-flow": "^3.2.0",
+ "@types/jest": "^27.0.3",
+ "@types/jsonwebtoken": "^8.5.6",
+ "@types/mongodb": "^4.0.7",
+ "@types/morgan": "^1.9.3",
+ "@types/nodemailer-smtp-transport": "^2.7.5",
+ "@types/serve-favicon": "^2.5.3",
+ "@types/trim": "^0.1.1",
+ "@typescript-eslint/eslint-plugin": "^5.8.0",
+ "@typescript-eslint/parser": "^5.8.0",
+ "axios": "^0.24.0",
+ "eslint": "^8.5.0",
+ "jest": "^27.4.5",
+ "shx": "^0.3.3",
+ "ts-jest": "^27.0.7",
+ "ts-node-dev": "^1.1.8",
+ "typescript": "^4.5.4"
+ }
+}
diff --git a/Goobieverse/public/favicon.ico b/Goobieverse/public/favicon.ico
new file mode 100644
index 00000000..7ed25a60
Binary files /dev/null and b/Goobieverse/public/favicon.ico differ
diff --git a/Goobieverse/public/index.html b/Goobieverse/public/index.html
new file mode 100644
index 00000000..c5879ef8
--- /dev/null
+++ b/Goobieverse/public/index.html
@@ -0,0 +1,75 @@
+
+
+
+ Goobieverse
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Goobieverse/src/app.hooks.ts b/Goobieverse/src/app.hooks.ts
new file mode 100644
index 00000000..1be53383
--- /dev/null
+++ b/Goobieverse/src/app.hooks.ts
@@ -0,0 +1,34 @@
+// Application hooks that run for every service
+// Don't remove this comment. It's needed to format import lines nicely.
+
+export default {
+ before: {
+ all: [],
+ find: [],
+ get: [],
+ create: [],
+ update: [],
+ patch: [],
+ remove: []
+ },
+
+ after: {
+ all: [],
+ find: [],
+ get: [],
+ create: [],
+ update: [],
+ patch: [],
+ remove: []
+ },
+
+ error: {
+ all: [],
+ find: [],
+ get: [],
+ create: [],
+ update: [],
+ patch: [],
+ remove: []
+ }
+};
diff --git a/Goobieverse/src/app.ts b/Goobieverse/src/app.ts
new file mode 100644
index 00000000..5f08aa78
--- /dev/null
+++ b/Goobieverse/src/app.ts
@@ -0,0 +1,74 @@
+import path from 'path';
+import favicon from 'serve-favicon';
+import compress from 'compression';
+import helmet from 'helmet';
+import cors from 'cors';
+
+import feathers from '@feathersjs/feathers';
+import configuration from '@feathersjs/configuration';
+import express from '@feathersjs/express';
+import socketio from '@feathersjs/socketio';
+import { publicRoutes } from './routes/publicRoutes';
+import config from './appconfig';
+import { Application } from './declarations';
+import logger from './logger';
+import middleware from './middleware';
+import services from './services';
+import appHooks from './app.hooks';
+import channels from './channels';
+import { HookContext as FeathersHookContext } from '@feathersjs/feathers';
+import authentication from './authentication';
+import mongodb from './mongodb';
+// Don't remove this comment. It's needed to format import lines nicely.
+
+const app: Application = express(feathers());
+export type HookContext = { app: Application } & FeathersHookContext;
+
+// Enable security, CORS, compression, favicon and body parsing
+app.use(helmet({
+ contentSecurityPolicy: false
+}));
+app.use(cors());
+app.use(compress());
+app.use(express.json());
+app.use(express.urlencoded({ extended: true }));
+
+//set Public folder path
+app.set('public',config.server.publicPath);
+
+//page favicon
+app.use(favicon(path.join(app.settings.public, 'favicon.ico')));
+
+
+// routes
+app.use('/', publicRoutes);
+// Host the public folder
+app.use('/', express.static(app.settings.public));
+
+
+// Set up Plugins and providers
+app.configure(express.rest());
+app.configure(socketio());
+
+app.set('host', config.server.local ? config.server.hostName + ':' + config.server.port : config.server.hostName);
+app.set('port',config.server.port);
+app.set('paginate',config.server.paginate);
+app.set('authentication', config.authentication);
+
+app.configure(mongodb);
+
+// Configure other middleware (see `middleware/index.ts`)
+app.configure(middleware);
+app.configure(authentication);
+// Set up our services (see `services/index.ts`)
+app.configure(services);
+// Set up event channels (see channels.ts)
+app.configure(channels);
+
+// Configure a middleware for 404s and the error handler
+app.use(express.notFound());
+app.use(express.errorHandler({ logger } as any));
+
+app.hooks(appHooks);
+
+export default app;
diff --git a/Goobieverse/src/appconfig.ts b/Goobieverse/src/appconfig.ts
new file mode 100644
index 00000000..8df343f7
--- /dev/null
+++ b/Goobieverse/src/appconfig.ts
@@ -0,0 +1,153 @@
+import dotenv from 'dotenv-flow';
+import appRootPath from 'app-root-path';
+import { IsNullOrEmpty, getMyExternalIPAddress } from './utils/Misc';
+import fs from 'fs';
+
+if (globalThis.process?.env.APP_ENV === 'development') {
+ // const fs = require('fs');
+ if (
+ !fs.existsSync(appRootPath.path + '/.env') &&
+ !fs.existsSync(appRootPath.path + '/.env.local')
+ ) {
+ const fromEnvPath = appRootPath.path + '/.env.local.default';
+ const toEnvPath = appRootPath.path + '/.env.local';
+ fs.copyFileSync(fromEnvPath, toEnvPath, fs.constants.COPYFILE_EXCL);
+ }
+}
+
+dotenv.config({
+ path: appRootPath.path,
+ silent: true,
+});
+
+/**
+ * Server
+ */
+
+const server = {
+ local: process.env.LOCAL === 'true',
+ hostName: process.env.SERVER_HOST,
+ port: process.env.PORT ?? 3030,
+ paginate: {
+ default: 10,
+ max: 100,
+ },
+ publicPath: process.env.PUBLIC_PATH,
+ version: process.env.SERVER_VERSION ?? '',
+};
+
+const email = {
+ host: process.env.SMTP_HOST ?? 'smtp.gmail.com',
+ port: process.env.SMTP_PORT ?? '465',
+ secure: process.env.SMTP_SECURE ?? true,
+ auth: {
+ user: process.env.SMTP_USER ?? 'khilan.odan@gmail.com',
+ pass: process.env.SMTP_PASS ?? 'blackhawk143',
+ }
+};
+
+/**
+ * Metaverse Server
+ */
+
+const metaverseServer = {
+ listen_host: process.env.LISTEN_HOST ?? '0.0.0.0',
+ listen_port: process.env.LISTEN_PORT ?? 9400,
+ metaverseInfoAdditionFile: process.env.METAVERSE_INFO_File ?? '',
+ session_timeout_minutes: 5,
+ heartbeat_seconds_until_offline: 5 * 60, // seconds until non-heartbeating user is offline
+ domain_seconds_until_offline: 10 * 60, // seconds until non-heartbeating domain is offline
+ domain_seconds_check_if_online: 2 * 60, // how often to check if a domain is online
+ handshake_request_expiration_minutes: 1, // minutes that a handshake friend request is active
+ connection_request_expiration_minutes: 60 * 24 * 4, // 4 days
+ friend_request_expiration_minutes: 60 * 24 * 4, // 4 days
+ base_admin_account:process.env.ADMIN_ACCOUNT ?? 'Goobieverse',
+ place_current_timeout_minutes: 5, // minutes until current place info is stale
+ place_inactive_timeout_minutes: 60, // minutes until place is considered inactive
+ place_check_last_activity_seconds: (3 * 60) - 5, // seconds between checks for Place lastActivity updates
+ email_verification_timeout_minutes: process.env.EMAIL_VERIFICATION_TIME,
+ enable_account_email_verification: process.env.ENABLE_ACCOUNT_VERIFICATION ?? 'true',
+ email_verification_email_body: '../verificationEmail.html',
+};
+
+/**
+ * Authentication
+ */
+const authentication = {
+ entity: 'user',
+ service: 'auth',
+ secret: process.env.AUTH_SECRET ?? 'testing',
+ authStrategies: ['jwt', 'local'],
+ jwtOptions: {
+ expiresIn: '60 days',
+ },
+ local: {
+ usernameField: 'username',
+ passwordField: 'password',
+ },
+ bearerToken: {
+ numBytes: 16,
+ },
+ oauth: {
+ redirect: '/',
+ auth0: {
+ key: '',
+ secret: '',
+ subdomain: '',
+ scope: ['profile', 'openid', 'email'],
+ },
+ },
+};
+
+/**
+ * Metaverse
+ */
+
+const metaverse = {
+ metaverseName: process.env.METAVERSE_NAME ?? '',
+ metaverseNickName: process.env.METAVERSE_NICK_NAME ?? '',
+ metaverseServerUrl: process.env.METAVERSE_SERVER_URL ?? '', // if empty, set to self
+ defaultIceServerUrl: process.env.DEFAULT_ICE_SERVER_URL ?? '', // if empty, set to self
+ dashboardUrl: process.env.DASHBOARD_URL
+
+};
+
+if (
+ IsNullOrEmpty(metaverse.metaverseServerUrl) ||
+ IsNullOrEmpty(metaverse.defaultIceServerUrl)
+) {
+ getMyExternalIPAddress().then((ipAddress) => {
+ if (IsNullOrEmpty(metaverse.metaverseServerUrl)) {
+ const newUrl = `http://${ipAddress}:${metaverseServer.listen_port.toString()}/`;
+ metaverse.metaverseServerUrl = newUrl;
+ }
+ if (IsNullOrEmpty(metaverse.defaultIceServerUrl)) {
+ metaverse.defaultIceServerUrl = ipAddress;
+ }
+ });
+}
+
+
+const dbCollections = {
+ domains : 'domains',
+ accounts : 'accounts',
+ places : 'places',
+ tokens : 'tokens'
+};
+
+
+/**
+ * Full config
+ */
+
+const config = {
+ deployStage: process.env.DEPLOY_STAGE,
+ authentication,
+ server,
+ metaverse,
+ metaverseServer,
+ dbCollections,
+ email
+};
+
+export default config;
diff --git a/Goobieverse/src/authentication.ts b/Goobieverse/src/authentication.ts
new file mode 100644
index 00000000..76809616
--- /dev/null
+++ b/Goobieverse/src/authentication.ts
@@ -0,0 +1,21 @@
+import { ServiceAddons } from '@feathersjs/feathers';
+import { AuthenticationService, JWTStrategy } from '@feathersjs/authentication';
+import { LocalStrategy } from '@feathersjs/authentication-local';
+import { expressOauth } from '@feathersjs/authentication-oauth';
+
+import { Application } from './declarations';
+
+declare module './declarations' {
+ interface ServiceTypes {
+ 'authentication': AuthenticationService & ServiceAddons;
+ }
+}
+export default function(app: Application): void {
+ const authentication = new AuthenticationService(app);
+
+ authentication.register('jwt', new JWTStrategy());
+ authentication.register('local', new LocalStrategy());
+
+ app.use('/authentication', authentication);
+ app.configure(expressOauth());
+}
diff --git a/Goobieverse/src/channels.ts b/Goobieverse/src/channels.ts
new file mode 100644
index 00000000..687c756e
--- /dev/null
+++ b/Goobieverse/src/channels.ts
@@ -0,0 +1,65 @@
+import '@feathersjs/transport-commons';
+import { HookContext } from '@feathersjs/feathers';
+import { Application } from './declarations';
+
+export default function(app: Application): void {
+ if(typeof app.channel !== 'function') {
+ // If no real-time functionality has been configured just return
+ return;
+ }
+
+ app.on('connection', (connection: any): void => {
+ // On a new real-time connection, add it to the anonymous channel
+ app.channel('anonymous').join(connection);
+ });
+
+ app.on('login', (authResult: any, { connection }: any): void => {
+ // connection can be undefined if there is no
+ // real-time connection, e.g. when logging in via REST
+ if(connection) {
+ // Obtain the logged in user from the connection
+ // const user = connection.user;
+
+ // The connection is no longer anonymous, remove it
+ app.channel('anonymous').leave(connection);
+
+ // Add it to the authenticated user channel
+ app.channel('authenticated').join(connection);
+
+ // Channels can be named anything and joined on any condition
+
+ // E.g. to send real-time events only to admins use
+ // if(user.isAdmin) { app.channel('admins').join(connection); }
+
+ // If the user has joined e.g. chat rooms
+ // if(Array.isArray(user.rooms)) user.rooms.forEach(room => app.channel(`rooms/${room.id}`).join(connection));
+
+ // Easily organize users by email and userid for things like messaging
+ // app.channel(`emails/${user.email}`).join(connection);
+ // app.channel(`userIds/${user.id}`).join(connection);
+ }
+ });
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ app.publish((data: any, hook: HookContext) => {
+ // Here you can add event publishers to channels set up in `channels.ts`
+ // To publish only for a specific event use `app.publish(eventname, () => {})`
+
+ console.log('Publishing all events to all authenticated users. See `channels.ts` and https://docs.feathersjs.com/api/channels.html for more information.'); // eslint-disable-line
+
+ // e.g. to publish all service events to all authenticated users use
+ return app.channel('authenticated');
+ });
+
+ // Here you can also add service specific event publishers
+ // e.g. the publish the `users` service `created` event to the `admins` channel
+ // app.service('users').publish('created', () => app.channel('admins'));
+
+ // With the userid and email organization from above you can easily select involved users
+ // app.service('messages').publish(() => {
+ // return [
+ // app.channel(`userIds/${data.createdBy}`),
+ // app.channel(`emails/${data.recipientEmail}`)
+ // ];
+ // });
+}
diff --git a/Goobieverse/src/controllers/PublicRoutesController.ts b/Goobieverse/src/controllers/PublicRoutesController.ts
new file mode 100644
index 00000000..8f88c306
--- /dev/null
+++ b/Goobieverse/src/controllers/PublicRoutesController.ts
@@ -0,0 +1,31 @@
+import { MetaverseInfoModel } from './../interfaces/MetaverseInfo';
+import config from '../appconfig';
+import {IsNotNullOrEmpty, readInJSON } from '../utils/Misc';
+
+export const PublicRoutesController = ()=>{
+ const metaverseInfo = async(req:any,res:any,next:any) => {
+ const response:MetaverseInfoModel = {
+ metaverse_name:config.metaverse.metaverseName,
+ metaverse_nick_name: config.metaverse.metaverseNickName,
+ ice_server_url: config.metaverse.defaultIceServerUrl ,
+ metaverse_url: config.metaverse.metaverseServerUrl,
+ metaverse_server_version:{
+ version_tag:config.server.version
+ }
+ };
+ try {
+ const additionUrl: string = config.metaverseServer.metaverseInfoAdditionFile;
+ if (IsNotNullOrEmpty(additionUrl)) {
+ const additional = await readInJSON(additionUrl);
+ if (IsNotNullOrEmpty(additional)) {
+ response.metaverse_server_version = additional;
+ }
+ }
+ }
+ catch (err) {
+ //console.error(`procMetaverseInfo: exception reading additional info file: ${err}`);
+ }
+ res.status(200).json(response);
+ };
+ return {metaverseInfo};
+};
\ No newline at end of file
diff --git a/Goobieverse/src/dbservice/DatabaseService.ts b/Goobieverse/src/dbservice/DatabaseService.ts
new file mode 100644
index 00000000..186e44bb
--- /dev/null
+++ b/Goobieverse/src/dbservice/DatabaseService.ts
@@ -0,0 +1,72 @@
+import { Service } from 'feathers-mongodb';
+import { Application } from '../declarations';
+import { HookContext, Paginated, } from '@feathersjs/feathers';
+import { DatabaseServiceOptions } from './DatabaseServiceOptions';
+import { Db, Collection, Document, FindCursor, WithId,Filter } from 'mongodb';
+import { IsNotNullOrEmpty, IsNullOrEmpty } from '../utils/Misc';
+import { AccountModel } from '../interfaces/AccountModel';
+
+
+export class DatabaseService extends Service {
+ app?: Application;
+ db?:Db;
+ context?:HookContext;
+ constructor(options: Partial,app?: Application,context?: HookContext) {
+ super(options);
+ this.app = app;
+ this.context = context;
+ this.loadDatabase();
+ }
+
+
+ async loadDatabase() {
+ if(IsNotNullOrEmpty(this.app) && this.app){
+ this.db = await this.app.get('mongoClient');
+ }else if(IsNotNullOrEmpty(this.context) && this.context){
+ this.db = await this.context.app.get('mongoClient');
+ }
+ }
+
+ async getDatabase(): Promise {
+ if(IsNullOrEmpty(this.db)){
+ await this.loadDatabase();
+ }
+ return this.db!;
+ }
+
+ async getService(tableName:string):Promise>{
+ this.Model = await (await this.getDatabase()).collection(tableName);
+ return this.Model;
+ }
+
+ async findData(tableName: string, filter?:Filter ): Promise | any[]>{
+ await (this.getService(tableName));
+ if(IsNotNullOrEmpty(filter)){
+ console.log(filter);
+ return await super.find(filter!);
+ } else {
+ return await super.find();
+ }
+ }
+
+ async findDataToArray(tableName: string, filter?:Filter ): Promise{
+ await (this.getService(tableName));
+ const data = await this.findData(tableName,filter);
+ if(data instanceof Array){
+ return data;
+ }else{
+ return data.data;
+ }
+ }
+
+ async CreateData(tableName: string, data:any): Promise {
+ await (this.getService(tableName));
+ return await super.create(data);
+ }
+
+ async UpdateDataById(tableName: string, data:any,id:any): Promise {
+ await (this.getService(tableName));
+ return await super.update(id, data);
+ }
+
+}
\ No newline at end of file
diff --git a/Goobieverse/src/dbservice/DatabaseServiceOptions.ts b/Goobieverse/src/dbservice/DatabaseServiceOptions.ts
new file mode 100644
index 00000000..99a05d9b
--- /dev/null
+++ b/Goobieverse/src/dbservice/DatabaseServiceOptions.ts
@@ -0,0 +1,4 @@
+import { MongoDBServiceOptions } from 'feathers-mongodb';
+import { Collection } from 'mongodb';
+
+export interface DatabaseServiceOptions extends MongoDBServiceOptions {}
\ No newline at end of file
diff --git a/Goobieverse/src/declarations.d.ts b/Goobieverse/src/declarations.d.ts
new file mode 100644
index 00000000..56550834
--- /dev/null
+++ b/Goobieverse/src/declarations.d.ts
@@ -0,0 +1,6 @@
+import { Application as ExpressFeathers } from '@feathersjs/express';
+
+// A mapping of service names to types. Will be extended in service files.
+export interface ServiceTypes {}
+// The application instance type that will be used everywhere else
+export type Application = ExpressFeathers;
diff --git a/Goobieverse/src/hooks/checkAccessToAccount.ts b/Goobieverse/src/hooks/checkAccessToAccount.ts
new file mode 100644
index 00000000..399a7966
--- /dev/null
+++ b/Goobieverse/src/hooks/checkAccessToAccount.ts
@@ -0,0 +1,153 @@
+import { AccountModel } from './../interfaces/AccountModel';
+import { Application, Paginated } from '@feathersjs/feathers';
+import { HookContext } from '@feathersjs/feathers';
+import { IsNotNullOrEmpty } from '../utils/Misc';
+import { Perm } from '../utils/Perm';
+import config from '../appconfig';
+import {HTTPStatusCode} from '../utils/response';
+import {Availability} from '../utils/sets/Availability';
+import { SArray } from '../utils/vTypes';
+import { DatabaseService } from '../dbservice/DatabaseService';
+
+
+export default (pRequiredAccess: Perm[]) => {
+ return async (context: HookContext): Promise => {
+ const dbService = new DatabaseService({},undefined,context);
+
+ const accounts = await dbService.findData(config.dbCollections.accounts,{query:{id:context.id}});
+ console.log(accounts);
+ let canAccess = false;
+
+ let pTargetEntity:AccountModel | undefined;
+ if(accounts instanceof Array){
+ pTargetEntity = (accounts as Array)[0];
+ }else if(IsNotNullOrEmpty(accounts.data[0])){
+ pTargetEntity = accounts.data[0];
+ }
+
+ if (IsNotNullOrEmpty(pTargetEntity)) {
+ for (const perm of pRequiredAccess) {
+ switch (perm) {
+ case Perm.ALL:
+ canAccess = true;
+ break;
+ case Perm.PUBLIC:
+ // The target entity is publicly visible
+ // Mostly AccountEntities that must have an 'availability' field
+ if (pTargetEntity?.hasOwnProperty('availability')) {
+ if ((pTargetEntity).availability.includes(Availability.ALL)) {
+ canAccess = true;
+ }
+ }
+ break;/* case Perm.DOMAIN:
+ // requestor is a domain and it's account is the domain's sponsoring account
+ if (pAuthToken && SArray.has(pAuthToken.scope, TokenScope.DOMAIN)) {
+ if (pTargetEntity.hasOwnProperty('sponsorAccountId')) {
+ canAccess = pAuthToken.accountId === (pTargetEntity as any).sponsorAccountId;
+ }
+ else {
+ // Super special case where domain doesn't have a sponsor but has an api_key.
+ // In this case, the API_KEY is put in the accountId field of the DOMAIN scoped AuthToken
+ if (pTargetEntity.hasOwnProperty('apiKey')) {
+ canAccess = pAuthToken.accountId === (pTargetEntity as any).apiKey;
+ }
+ }
+ }
+ break;
+
+ case Perm.OWNER:
+ // The requestor wants to be the same account as the target entity
+ if (pAuthToken && pTargetEntity.hasOwnProperty('id')) {
+ canAccess = pAuthToken.accountId === (pTargetEntity as AccountEntity).id;
+ }
+ if (!canAccess && pTargetEntity.hasOwnProperty('accountId')) {
+ canAccess = pAuthToken.accountId === (pTargetEntity as any).accountId;
+ }
+ break;
+ case Perm.FRIEND:
+ // The requestor is a 'friend' of the target entity
+ if (pAuthToken && pTargetEntity.hasOwnProperty('friends')) {
+ const targetFriends: string[] = (pTargetEntity as AccountEntity).friends;
+ if (targetFriends) {
+ requestingAccount = requestingAccount ?? await Accounts.getAccountWithId(pAuthToken.accountId);
+ canAccess = SArray.hasNoCase(targetFriends, requestingAccount.username);
+ }
+ }
+ break;
+ case Perm.CONNECTION:
+ // The requestor is a 'connection' of the target entity
+ if (pAuthToken && pTargetEntity.hasOwnProperty('connections')) {
+ const targetConnections: string[] = (pTargetEntity as AccountEntity).connections;
+ if (targetConnections) {
+ requestingAccount = requestingAccount ?? await Accounts.getAccountWithId(pAuthToken.accountId);
+ canAccess = SArray.hasNoCase(targetConnections, requestingAccount.username);
+ }
+ }
+ break;
+ case Perm.ADMIN:
+ if (pAuthToken && Tokens.isSpecialAdminToken(pAuthToken)) {
+ Logger.cdebug('field-setting', `checkAccessToEntity: isSpecialAdminToken`);
+ canAccess = true;
+ }
+ else {
+ // If the authToken is an account, has access if admin
+ if (pAuthToken && SArray.has(pAuthToken.scope, TokenScope.OWNER)) {
+ Logger.cdebug('field-setting', `checkAccessToEntity: admin. auth.AccountId=${pAuthToken.accountId}`);
+ requestingAccount = requestingAccount ?? await Accounts.getAccountWithId(pAuthToken.accountId);
+ canAccess = Accounts.isAdmin(requestingAccount);
+ }
+ }
+ break;
+ case Perm.SPONSOR:
+ // Requestor is a regular account and is the sponsor of the domain
+ if (pAuthToken && SArray.has(pAuthToken.scope, TokenScope.OWNER)) {
+ if (pTargetEntity.hasOwnProperty('sponsorAccountId')) {
+ Logger.cdebug('field-setting', `checkAccessToEntity: authToken is domain. auth.AccountId=${pAuthToken.accountId}, sponsor=${(pTargetEntity as any).sponsorAccountId}`);
+ canAccess = pAuthToken.accountId === (pTargetEntity as DomainEntity).sponsorAccountId;
+ }
+ }
+ break;
+ case Perm.MANAGER:
+ // See if requesting account is in the list of managers of this entity
+ if (pAuthToken && SArray.has(pAuthToken.scope, TokenScope.OWNER)) {
+ if (pTargetEntity.hasOwnProperty('managers')) {
+ requestingAccount = requestingAccount ?? await Accounts.getAccountWithId(pAuthToken.accountId);
+ if (requestingAccount) {
+ const managers: string[] = (pTargetEntity as DomainEntity).managers;
+ // Logger.debug(`Perm.MANAGER: managers=${JSON.stringify(managers)}, target=${requestingAccount.username}`);
+ if (managers && managers.includes(requestingAccount.username.toLowerCase())) {
+ canAccess = true;
+ }
+ }
+ }
+ }
+ break;
+ case Perm.DOMAINACCESS:
+ // Target entity has a domain reference and verify the requestor is able to reference that domain
+ if (pAuthToken && pTargetEntity.hasOwnProperty('domainId')) {
+ const aDomain = await Domains.getDomainWithId((pTargetEntity as any).domainId);
+ if (aDomain) {
+ canAccess = aDomain.sponsorAccountId === pAuthToken.accountId;
+ }
+ }
+ break;*/
+ default:
+ canAccess = false;
+ break;
+ }
+ // If some permission allows access, we are done
+
+ }
+ }else{
+ context.statusCode = HTTPStatusCode.NotFound;
+ throw new Error('Target account not found');
+ }
+
+ if (!canAccess){
+ context.statusCode = HTTPStatusCode.Unauthorized;
+ throw new Error('Unauthorized');
+ }
+
+ return context;
+ };
+};
\ No newline at end of file
diff --git a/Goobieverse/src/hooks/requestFail.ts b/Goobieverse/src/hooks/requestFail.ts
new file mode 100644
index 00000000..258c34d0
--- /dev/null
+++ b/Goobieverse/src/hooks/requestFail.ts
@@ -0,0 +1,12 @@
+import { HookContext } from '@feathersjs/feathers';
+import { IsNotNullOrEmpty } from '../utils/Misc';
+import { Perm } from '../utils/Perm';
+import { Response } from '../utils/response';
+
+export default () => {
+ return async (context: HookContext): Promise => {
+
+ context.result = Response.error(context?.error?.message);
+ return context;
+ };
+};
\ No newline at end of file
diff --git a/Goobieverse/src/hooks/requestSuccess.ts b/Goobieverse/src/hooks/requestSuccess.ts
new file mode 100644
index 00000000..0cc54059
--- /dev/null
+++ b/Goobieverse/src/hooks/requestSuccess.ts
@@ -0,0 +1,11 @@
+import { HookContext } from '@feathersjs/feathers';
+import { IsNotNullOrEmpty } from '../utils/Misc';
+import { Perm } from '../utils/Perm';
+import { Response } from '../utils/response';
+
+export default () => {
+ return async (context: HookContext): Promise => {
+ context.result = Response.success(context.result);
+ return context;
+ };
+};
\ No newline at end of file
diff --git a/Goobieverse/src/hooks/userData.ts b/Goobieverse/src/hooks/userData.ts
new file mode 100644
index 00000000..96348da2
--- /dev/null
+++ b/Goobieverse/src/hooks/userData.ts
@@ -0,0 +1,7 @@
+import { Hook, HookContext } from '@feathersjs/feathers';
+
+export async function myHook(context: any): Promise {
+ // console.log(context, "myHook");
+
+ return context;
+}
diff --git a/Goobieverse/src/index.ts b/Goobieverse/src/index.ts
new file mode 100644
index 00000000..7c8a791c
--- /dev/null
+++ b/Goobieverse/src/index.ts
@@ -0,0 +1,13 @@
+import logger from './logger';
+import app from './app';
+
+const port = app.get('port');
+const server = app.listen(port);
+
+process.on('unhandledRejection', (reason, p) =>
+ logger.error('Unhandled Rejection at: Promise ', p, reason)
+);
+
+server.on('listening', () =>
+ logger.info('Feathers application started on http://%s:%d', app.get('host'), port)
+);
diff --git a/Goobieverse/src/interfaces/AccountModel.ts b/Goobieverse/src/interfaces/AccountModel.ts
new file mode 100755
index 00000000..ae4bff71
--- /dev/null
+++ b/Goobieverse/src/interfaces/AccountModel.ts
@@ -0,0 +1,42 @@
+export interface AccountModel {
+ id: string;
+ username: string;
+ email: string;
+ accountSettings: string; // JSON of client settings
+ imagesHero: string;
+ imagesThumbnail: string;
+ imagesTiny: string;
+ password: string;
+
+ locationConnected: boolean;
+ locationPath: string; // "/floatX,floatY,floatZ/floatX,floatY,floatZ,floatW"
+ locationPlaceId: string; // uuid of place
+ locationDomainId: string; // uuid of domain located in
+ locationNetworkAddress: string;
+ locationNetworkPort: number;
+ locationNodeId: string; // sessionId
+ availability: string[]; // contains 'none', 'friends', 'connections', 'all'
+
+ connections: string[];
+ friends: string[];
+ locker: any; // JSON blob stored for user from server
+ profileDetail: any; // JSON blob stored for user from server
+
+ // User authentication
+ // passwordHash: string;
+ // passwordSalt: string;
+ sessionPublicKey: string; // PEM public key generated for this session
+ accountEmailVerified: boolean; // default true if not present
+
+ // Old stuff
+ xmppPassword: string;
+ discourseApiKey: string;
+ walletId: string;
+
+ // Admin stuff
+ // ALWAYS USE functions in Roles class to manipulate this list of roles
+ roles: string[]; // account roles (like 'admin')
+ IPAddrOfCreator: string; // IP address that created this account
+ whenCreated: Date; // date of account creation
+ timeOfLastHeartbeat: Date; // when we last heard from this user
+}
diff --git a/Goobieverse/src/interfaces/DomainModel.ts b/Goobieverse/src/interfaces/DomainModel.ts
new file mode 100755
index 00000000..8720af5b
--- /dev/null
+++ b/Goobieverse/src/interfaces/DomainModel.ts
@@ -0,0 +1,38 @@
+export interface DomainModel{
+ id: string, // globally unique domain identifier
+ name: string, // domain name/label
+ visibility: string, // visibility of this entry in general domain lists
+ publicKey: string, // DomainServers's public key in multi-line PEM format
+ apiKey: string, // Access key if a temp domain
+ sponsorAccountId: string, // The account that gave this domain an access key
+ iceServerAddr: string,// IP address of ICE server being used by this domain
+
+ // Information that comes in via heartbeat
+ version: string, // DomainServer's build version (like "K3")
+ protocol: string, // Protocol version
+ networkAddr: string, // reported network address
+ networkPort: string, // reported network address
+ networkingMode: string, // one of "full", "ip", "disabled"
+ restricted: boolean, // 'true' if restricted to users with accounts
+ numUsers: number, // total number of logged in users
+ anonUsers: number, // number of anonymous users
+ hostnames: string[], // User segmentation
+
+ // More information that's metadata that's passed in PUT domain
+ capacity: number, // Total possible users
+ description: string, // Short description of domain
+ contactInfo: string, // domain contact information
+ thumbnail: string, // thumbnail image of domain
+ images: string[], // collection of images for the domain
+ maturity: string, // Maturity rating
+ restriction: string, // Access restrictions ("open")
+ managers: string[], // Usernames of people who are domain admins
+ tags: string[], // Categories for describing the domain
+
+ // admin stuff
+ iPAddrOfFirstContact: string, // IP address that registered this domain
+ whenCreated: Date, // What the variable name says
+ active: boolean, // domain is heartbeating
+ timeOfLastHeartbeat: Date, // time of last heartbeat
+ lastSenderKey: string, // a key identifying the sender
+}
\ No newline at end of file
diff --git a/Goobieverse/src/interfaces/MetaverseInfo.ts b/Goobieverse/src/interfaces/MetaverseInfo.ts
new file mode 100644
index 00000000..4552ccb1
--- /dev/null
+++ b/Goobieverse/src/interfaces/MetaverseInfo.ts
@@ -0,0 +1,7 @@
+export interface MetaverseInfoModel{
+ metaverse_name:string,
+ metaverse_nick_name:string,
+ metaverse_url:string,
+ ice_server_url:string,
+ metaverse_server_version:any
+}
\ No newline at end of file
diff --git a/Goobieverse/src/interfaces/PlaceModel.ts b/Goobieverse/src/interfaces/PlaceModel.ts
new file mode 100755
index 00000000..2793916e
--- /dev/null
+++ b/Goobieverse/src/interfaces/PlaceModel.ts
@@ -0,0 +1,29 @@
+export interface PlaceModel{
+ id: string, // globally unique place identifier
+ name: string, // Human friendly name of the place
+ displayName: string, // Human friendly name of the place
+ description: string, // Human friendly description of the place
+ visibility: string, // visibility of this Place in general Place lists
+ maturity: string, // maturity level of the place (see Sets/Maturity.ts)
+ tags: string[], // tags defining the string content
+ domainId: string, // domain the place is in
+ managers: string[], // Usernames of people who are domain admins
+ path: string, // address within the domain: "optional-domain/x,y,z/x,y,z,x"
+ thumbnail: string, // thumbnail for place
+ images: string[], // images for the place
+
+ // A Place can have a beacon that updates current state and information
+ // If current information is not supplied, attendance defaults to domain's
+ currentAttendance: number // current attendance at the Place
+ currentImages: string[] // images at the session
+ currentInfo: string // JSON information about the session
+ currentLastUpdateTime: Date // time that the last session information was updated
+ currentAPIKeyTokenId: string // API key for updating the session information
+
+ // admin stuff
+ iPAddrOfFirstContact: string, // IP address that registered this place
+ whenCreated: Date, // What the variable name says
+ // 'lastActivity' is computed by Places.initPlaces and used for aliveness checks
+ lastActivity: Date, // newest of currentLastUpdateTime and Domain.timeOfLastHeartbeat
+}
+
diff --git a/Goobieverse/src/interfaces/RequestModal.ts b/Goobieverse/src/interfaces/RequestModal.ts
new file mode 100644
index 00000000..441b08aa
--- /dev/null
+++ b/Goobieverse/src/interfaces/RequestModal.ts
@@ -0,0 +1,22 @@
+export interface RequestEntity {
+ id: string;
+ requestType: string;
+
+ // requestor and target
+ requestingAccountId: string;
+ targetAccountId: string;
+
+ // administration
+ expirationTime: Date;
+ whenCreated: Date;
+
+ // requestType == HANDSHAKE
+ requesterNodeId: string;
+ targetNodeId: string;
+ requesterAccepted: boolean;
+ targetAccepted: boolean;
+
+ // requestType == VERIFYEMAIL
+ // 'requestingAccountId' is the account being verified
+ verificationCode: string; // the code we're waiting for
+}
\ No newline at end of file
diff --git a/Goobieverse/src/logger.ts b/Goobieverse/src/logger.ts
new file mode 100644
index 00000000..739c222b
--- /dev/null
+++ b/Goobieverse/src/logger.ts
@@ -0,0 +1,16 @@
+import { createLogger, format, transports } from 'winston';
+
+// Configure the Winston logger. For the complete documentation see https://github.com/winstonjs/winston
+const logger = createLogger({
+ // To see more detailed errors, change this to 'debug'
+ level: 'info',
+ format: format.combine(
+ format.splat(),
+ format.simple()
+ ),
+ transports: [
+ new transports.Console()
+ ],
+});
+
+export default logger;
diff --git a/Goobieverse/src/middleware/index.ts b/Goobieverse/src/middleware/index.ts
new file mode 100644
index 00000000..e7826837
--- /dev/null
+++ b/Goobieverse/src/middleware/index.ts
@@ -0,0 +1,6 @@
+import { Application } from '../declarations';
+// Don't remove this comment. It's needed to format import lines nicely.
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
+export default function (app: Application): void {
+}
diff --git a/Goobieverse/src/mongodb.ts b/Goobieverse/src/mongodb.ts
new file mode 100644
index 00000000..67703836
--- /dev/null
+++ b/Goobieverse/src/mongodb.ts
@@ -0,0 +1,20 @@
+import { MongoClient } from 'mongodb';
+import { Application } from './declarations';
+import { IsNotNullOrEmpty } from './utils/Misc';
+export default function (app: Application): void {
+ let connection = '';
+ if(IsNotNullOrEmpty(process.env.DATABASE_URL)){
+ connection = process.env.DATABASE_URL || '';
+ }else{
+ const userSpec = `${process.env.DB_USER}:${process.env.DB_PASSWORD}`;
+ const hostSpec = `${process.env.DB_HOST}:${process.env.DB_PORT}`;
+ let optionsSpec = '';
+ if (process.env.DB_AUTHDB !== 'admin') {
+ optionsSpec += `?authSource=${process.env.DB_AUTHDB}`;
+ }
+ connection = `mongodb://${userSpec}@${hostSpec}/${optionsSpec}`;
+ }
+ const database = process.env.DB_NAME;
+ const mongoClient = MongoClient.connect(connection).then(client => client.db(database));
+ app.set('mongoClient', mongoClient);
+}
diff --git a/Goobieverse/src/responsebuilder/accountsBuilder.ts b/Goobieverse/src/responsebuilder/accountsBuilder.ts
new file mode 100644
index 00000000..d88ebf29
--- /dev/null
+++ b/Goobieverse/src/responsebuilder/accountsBuilder.ts
@@ -0,0 +1,79 @@
+import { DomainModel } from '../interfaces/DomainModel';
+import { AccountModel } from '../interfaces/AccountModel';
+import { buildLocationInfo } from './placesBuilder';
+import { VKeyedCollection } from '../utils/vTypes';
+import { isAdmin, isEnabled, createSimplifiedPublicKey } from '../utils/Utils';
+// Return the limited "user" info.. used by /api/v1/users
+export async function buildUserInfo(pAccount: AccountModel): Promise {
+ return {
+ accountId: pAccount.id,
+ id: pAccount.id,
+ username: pAccount.username,
+ images: await buildImageInfo(pAccount),
+ location: await buildLocationInfo(pAccount),
+ };
+}
+
+export async function buildImageInfo(pAccount: AccountModel): Promise {
+ const ret: VKeyedCollection = {};
+
+ if (pAccount.imagesTiny) ret.tiny = pAccount.imagesTiny;
+ if (pAccount.imagesHero) ret.hero = pAccount.imagesHero;
+ if (pAccount.imagesThumbnail) ret.thumbnail = pAccount.imagesThumbnail;
+ return ret;
+}
+
+// Return the block of account information.
+// Used by several of the requests to return the complete account information.
+export async function buildAccountInfo(
+ pAccount: AccountModel
+): Promise {
+ return {
+ accountId: pAccount.id,
+ id: pAccount.id,
+ username: pAccount.username,
+ email: pAccount.email,
+ administrator: isAdmin(pAccount),
+ enabled: isEnabled(pAccount),
+ roles: pAccount.roles,
+ availability: pAccount.availability,
+ public_key: createSimplifiedPublicKey(pAccount.sessionPublicKey),
+ images: {
+ hero: pAccount.imagesHero,
+ tiny: pAccount.imagesTiny,
+ thumbnail: pAccount.imagesThumbnail,
+ },
+ profile_detail: pAccount.profileDetail,
+ location: await buildLocationInfo(pAccount),
+ friends: pAccount.friends,
+ connections: pAccount.connections,
+ when_account_created: pAccount.whenCreated?.toISOString(),
+ when_account_created_s: pAccount.whenCreated?.getTime().toString(),
+ time_of_last_heartbeat: pAccount.timeOfLastHeartbeat?.toISOString(),
+ time_of_last_heartbeat_s: pAccount.timeOfLastHeartbeat?.getTime().toString(),
+ };
+}
+
+// Return the block of account information used as the account 'profile'.
+// Anyone can fetch a profile (if 'availability' is 'any') so not all info is returned
+export async function buildAccountProfile(
+ pAccount: AccountModel,
+ aDomain?: DomainModel
+): Promise {
+ return {
+ accountId: pAccount.id,
+ id: pAccount.id,
+ username: pAccount.username,
+ images: {
+ hero: pAccount.imagesHero,
+ tiny: pAccount.imagesTiny,
+ thumbnail: pAccount.imagesThumbnail,
+ },
+ profile_detail: pAccount.profileDetail,
+ location: await buildLocationInfo(pAccount, aDomain),
+ when_account_created: pAccount.whenCreated?.toISOString(),
+ when_account_created_s: pAccount.whenCreated?.getTime().toString(),
+ time_of_last_heartbeat: pAccount.timeOfLastHeartbeat?.toISOString(),
+ time_of_last_heartbeat_s: pAccount.timeOfLastHeartbeat?.getTime().toString(),
+ };
+}
\ No newline at end of file
diff --git a/Goobieverse/src/responsebuilder/domainsBuilder.ts b/Goobieverse/src/responsebuilder/domainsBuilder.ts
new file mode 100644
index 00000000..a4f89b90
--- /dev/null
+++ b/Goobieverse/src/responsebuilder/domainsBuilder.ts
@@ -0,0 +1,80 @@
+import { Visibility } from '../utils/sets/Visibility';
+import { DomainModel } from '../interfaces/DomainModel';
+import { createSimplifiedPublicKey } from '../utils/Utils';
+import { buildPlacesForDomain } from './placesBuilder';
+import { Maturity } from '../utils/sets/Maturity';
+// A smaller, top-level domain info block
+export async function buildDomainInfo(pDomain: DomainModel): Promise {
+ return {
+ id: pDomain.id,
+ domainId: pDomain.id,
+ name: pDomain.name,
+ visibility: pDomain.visibility ?? Visibility.OPEN,
+ capacity: pDomain.capacity,
+ sponsorAccountId: pDomain.sponsorAccountId,
+ label: pDomain.name,
+ network_address: pDomain.networkAddr,
+ network_port: pDomain.networkPort,
+ ice_server_address: pDomain.iceServerAddr,
+ version: pDomain.version,
+ protocol_version: pDomain.protocol,
+ active: pDomain.active ?? false,
+ time_of_last_heartbeat: pDomain.timeOfLastHeartbeat?.toISOString(),
+ time_of_last_heartbeat_s: pDomain.timeOfLastHeartbeat?.getTime().toString(),
+ num_users: pDomain.numUsers,
+ };
+}
+
+// Return a structure with the usual domain information.
+export async function buildDomainInfoV1(pDomain: DomainModel): Promise {
+ return {
+ domainId: pDomain.id,
+ id: pDomain.id, // legacy
+ name: pDomain.name,
+ visibility: pDomain.visibility ?? Visibility.OPEN,
+ world_name: pDomain.name, // legacy
+ label: pDomain.name, // legacy
+ public_key: pDomain.publicKey? createSimplifiedPublicKey(pDomain.publicKey): undefined,
+ owner_places: await buildPlacesForDomain(pDomain),
+ sponsor_account_id: pDomain.sponsorAccountId,
+ ice_server_address: pDomain.iceServerAddr,
+ version: pDomain.version,
+ protocol_version: pDomain.protocol,
+ network_address: pDomain.networkAddr,
+ network_port: pDomain.networkPort,
+ automatic_networking: pDomain.networkingMode,
+ restricted: pDomain.restricted,
+ num_users: pDomain.numUsers,
+ anon_users: pDomain.anonUsers,
+ total_users: pDomain.numUsers,
+ capacity: pDomain.capacity,
+ description: pDomain.description,
+ maturity: pDomain.maturity ?? Maturity.UNRATED,
+ restriction: pDomain.restriction,
+ managers: pDomain.managers,
+ tags: pDomain.tags,
+ meta: {
+ capacity: pDomain.capacity,
+ contact_info: pDomain.contactInfo,
+ description: pDomain.description,
+ images: pDomain.images,
+ managers: pDomain.managers,
+ restriction: pDomain.restriction,
+ tags: pDomain.tags,
+ thumbnail: pDomain.thumbnail,
+ world_name: pDomain.name,
+ },
+ users: {
+ num_anon_users: pDomain.anonUsers,
+ num_users: pDomain.numUsers,
+ user_hostnames: pDomain.hostnames,
+ },
+ time_of_last_heartbeat: pDomain.timeOfLastHeartbeat?.toISOString(),
+ time_of_last_heartbeat_s: pDomain.timeOfLastHeartbeat?.getTime().toString(),
+ last_sender_key: pDomain.lastSenderKey,
+ addr_of_first_contact: pDomain.iPAddrOfFirstContact,
+ when_domain_entry_created: pDomain.whenCreated?.toISOString(),
+ when_domain_entry_created_s: pDomain.whenCreated?.getTime().toString(),
+ };
+}
+
\ No newline at end of file
diff --git a/Goobieverse/src/responsebuilder/placesBuilder.ts b/Goobieverse/src/responsebuilder/placesBuilder.ts
new file mode 100644
index 00000000..9fc18e58
--- /dev/null
+++ b/Goobieverse/src/responsebuilder/placesBuilder.ts
@@ -0,0 +1,130 @@
+import { AccountModel } from './../interfaces/AccountModel';
+import { IsNotNullOrEmpty,IsNullOrEmpty } from '../utils/Misc';
+import { buildDomainInfo } from './domainsBuilder';
+import { DomainModel } from '../interfaces/DomainModel';
+import { isOnline } from '../utils/Utils';
+import { PlaceModel } from '../interfaces/PlaceModel';
+import { Visibility } from '../utils/sets/Visibility';
+import { Maturity } from '../utils/sets/Maturity';
+// The returned location info has many options depending on whether
+// the account has set location and/or has an associated domain.
+// Return a structure that represents the target account's domain
+
+export async function buildLocationInfo(pAcct: AccountModel,aDomain?: DomainModel): Promise {
+ let ret: any = {};
+ if (pAcct.locationDomainId) {
+ if (IsNotNullOrEmpty(aDomain) && aDomain) {
+ ret = {
+ root: {
+ domain: await buildDomainInfo(aDomain),
+ },
+ path: pAcct.locationPath,
+ };
+ } else {
+ // The domain doesn't have an ID
+ ret = {
+ root: {
+ domain: {
+ network_address: pAcct.locationNetworkAddress,
+ network_port: pAcct.locationNetworkPort,
+ },
+ },
+ };
+ }
+ }
+ ret.node_id = pAcct.locationNodeId;
+ ret.online = isOnline(pAcct);
+ return ret;
+}
+
+// Return an object with the formatted place information
+// Pass the PlaceModel and the place's domain if known.
+export async function buildPlaceInfo(pPlace: PlaceModel,pDomain?: DomainModel): Promise {
+ const ret = await buildPlaceInfoSmall(pPlace, pDomain);
+
+ // if the place points to a domain, add that information also
+ if (IsNotNullOrEmpty(pDomain) && pDomain) {
+ ret.domain = await buildDomainInfo(pDomain);
+ }
+ return ret;
+}
+
+
+function getAddressString(pPlace: PlaceModel,aDomain?: DomainModel): string {
+// Compute and return the string for the Places's address.
+// The address is of the form "optional-domain/x,y,z/x,y,z,w".
+// If the domain is missing, the domain-server's network address is added
+ let addr = pPlace.path ?? '/0,0,0/0,0,0,1';
+
+ // If no domain/address specified in path, build addr using reported domain IP/port
+ const pieces = addr.split('/');
+ if (pieces[0].length === 0) {
+ if (IsNotNullOrEmpty(aDomain) && aDomain) {
+ if (IsNotNullOrEmpty(aDomain.networkAddr)) {
+ let domainAddr = aDomain.networkAddr;
+ if (IsNotNullOrEmpty(aDomain.networkPort)) {
+ domainAddr = aDomain.networkAddr + ':' + aDomain.networkPort;
+ }
+ addr = domainAddr + addr;
+ }
+ }
+ }
+ return addr;
+}
+
+
+// Return the basic information block for a Place
+export async function buildPlaceInfoSmall(pPlace: PlaceModel,pDomain?: DomainModel): Promise {
+ const ret = {
+ placeId: pPlace.id,
+ id: pPlace.id,
+ name: pPlace.name,
+ displayName: pPlace.displayName,
+ visibility: pPlace.visibility ?? Visibility.OPEN,
+ address: getAddressString(pPlace,pDomain),
+ path: pPlace.path,
+ description: pPlace.description,
+ maturity: pPlace.maturity ?? Maturity.UNRATED,
+ tags: pPlace.tags,
+ managers: await getManagers(pPlace,pDomain),
+ thumbnail: pPlace.thumbnail,
+ images: pPlace.images,
+ current_attendance: pPlace.currentAttendance ?? 0,
+ current_images: pPlace.currentImages,
+ current_info: pPlace.currentInfo,
+ current_last_update_time: pPlace.currentLastUpdateTime?.toISOString(),
+ current_last_update_time_s: pPlace.currentLastUpdateTime?.getTime().toString(),
+ last_activity_update: pPlace.lastActivity?.toISOString(),
+ last_activity_update_s: pPlace.lastActivity?.getTime().toString(),
+ };
+ return ret;
+}
+
+
+async function getManagers(pPlace: PlaceModel,aDomain?: DomainModel): Promise {
+ if(IsNullOrEmpty(pPlace.managers)) {
+ pPlace.managers = [];
+ //uncomment after complete Accounts Places api
+ /*
+ if (aDomain) {
+ const aAccount = await Accounts.getAccountWithId(aDomain.sponsorAccountId);
+ if (aAccount) {
+ pPlace.managers = [ aAccount.username ];
+ }
+ }
+ await Places.updateEntityFields(pPlace, { 'managers': pPlace.managers })
+ */
+ }
+ return pPlace.managers;
+}
+
+
+// Return an array of Places names that are associated with the passed domain
+export async function buildPlacesForDomain(pDomain: DomainModel): Promise {
+ const ret: any[] = [];
+ //uncomment after complete Places api
+ /* for await (const aPlace of Places.enumerateAsync(new GenericFilter({ domainId: pDomain.id }))) {
+ ret.push(await buildPlaceInfoSmall(aPlace, pDomain));
+ }*/
+ return ret;
+}
\ No newline at end of file
diff --git a/Goobieverse/src/responsebuilder/tokensBuilder.ts b/Goobieverse/src/responsebuilder/tokensBuilder.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/Goobieverse/src/routes/publicRoutes.ts b/Goobieverse/src/routes/publicRoutes.ts
new file mode 100644
index 00000000..6d2f24f5
--- /dev/null
+++ b/Goobieverse/src/routes/publicRoutes.ts
@@ -0,0 +1,9 @@
+import express from 'express';
+import { PublicRoutesController } from '../controllers/PublicRoutesController';
+
+const publicRoutes = express.Router();
+
+publicRoutes.get('/metaverse_info',PublicRoutesController().metaverseInfo);
+
+export { publicRoutes};
+
\ No newline at end of file
diff --git a/Goobieverse/src/services/auth/auth.class.ts b/Goobieverse/src/services/auth/auth.class.ts
new file mode 100644
index 00000000..d78dfc67
--- /dev/null
+++ b/Goobieverse/src/services/auth/auth.class.ts
@@ -0,0 +1,13 @@
+import { Db } from 'mongodb';
+import { Service, MongoDBServiceOptions } from 'feathers-mongodb';
+import { Application } from '../../declarations';
+
+export class Auth extends Service {
+ constructor(options: Partial, app: Application) {
+ super(options);
+ const client: Promise = app.get('mongoClient');
+ client.then((db) => {
+ this.Model = db.collection('accounts');
+ });
+ }
+}
diff --git a/Goobieverse/src/services/auth/auth.hooks.ts b/Goobieverse/src/services/auth/auth.hooks.ts
new file mode 100644
index 00000000..661db2a9
--- /dev/null
+++ b/Goobieverse/src/services/auth/auth.hooks.ts
@@ -0,0 +1,38 @@
+import { HooksObject } from '@feathersjs/feathers';
+import * as feathersAuthentication from '@feathersjs/authentication';
+import * as local from '@feathersjs/authentication-local';
+import { disallow } from 'feathers-hooks-common';
+const { authenticate } = feathersAuthentication.hooks;
+const { hashPassword, protect } = local.hooks;
+
+export default {
+ before: {
+ all: [],
+ find: [ authenticate('jwt') ],
+ get: [ authenticate('jwt') ],
+ create: [disallow('external')],
+ update: [disallow('external')],
+ patch: [disallow('external')],
+ remove: [ disallow('external')]
+ },
+
+ after: {
+ all: [protect('password')],
+ find: [],
+ get: [],
+ create: [],
+ update: [],
+ patch: [],
+ remove: [],
+ },
+
+ error: {
+ all: [],
+ find: [],
+ get: [],
+ create: [],
+ update: [],
+ patch: [],
+ remove: [],
+ },
+} as HooksObject;
diff --git a/Goobieverse/src/services/auth/auth.service.ts b/Goobieverse/src/services/auth/auth.service.ts
new file mode 100644
index 00000000..3c8618a1
--- /dev/null
+++ b/Goobieverse/src/services/auth/auth.service.ts
@@ -0,0 +1,26 @@
+// Initializes the `users` service on path `/users`
+import { ServiceAddons } from '@feathersjs/feathers';
+import { Application } from '../../declarations';
+import { Auth } from './auth.class';
+import hooks from './auth.hooks';
+
+// Add this service to the service type index
+declare module '../../declarations' {
+ interface ServiceTypes {
+ auth: Auth & ServiceAddons;
+ }
+}
+
+export default function (app: Application): void {
+ const options = {
+ paginate: app.get('paginate'),
+ };
+
+ // Initialize our service with any options it requires
+ app.use('/auth', new Auth(options, app));
+
+ // Get our initialized service so that we can register hooks
+ const service = app.service('auth');
+
+ service.hooks(hooks);
+}
diff --git a/Goobieverse/src/services/connections/connections.class.ts b/Goobieverse/src/services/connections/connections.class.ts
new file mode 100644
index 00000000..6aaf99bc
--- /dev/null
+++ b/Goobieverse/src/services/connections/connections.class.ts
@@ -0,0 +1,32 @@
+import { MongoDBServiceOptions } from 'feathers-mongodb';
+import { DatabaseService } from './../../dbservice/DatabaseService';
+import { Application } from '../../declarations';
+import config from '../../appconfig';
+import { Response } from '../../utils/response';
+import { isValidObject } from '../../utils/Misc';
+
+export class Connections extends DatabaseService {
+ //eslint-disable-next-line @typescript-eslint/no-unused-vars
+ constructor(options: Partial, app: Application) {
+ super(options, app);
+ this.app = app;
+ }
+
+ async create(data: any, params?: any): Promise {
+ if (data && data.username) {
+ const ParticularUserData: any = await this.findData(config.dbCollections.accounts, { query: { id: params.user.id } });
+ const newParticularUserData = ParticularUserData.data[0];
+ newParticularUserData.connections.push(data.username);
+ const addUserData = await this.UpdateDataById(config.dbCollections.accounts,newParticularUserData, params.user.id);
+ if (isValidObject(addUserData)) {
+ return Promise.resolve({});
+ } else {
+ return Response.error('cannot add connections this way');
+ }
+ } else {
+ return Response.error('Badly formed request');
+ }
+ }
+
+
+}
diff --git a/Goobieverse/src/services/connections/connections.hooks.ts b/Goobieverse/src/services/connections/connections.hooks.ts
new file mode 100644
index 00000000..4e5135ae
--- /dev/null
+++ b/Goobieverse/src/services/connections/connections.hooks.ts
@@ -0,0 +1,38 @@
+import { HooksObject } from '@feathersjs/feathers';
+import * as authentication from '@feathersjs/authentication';
+import requestFail from '../../hooks/requestFail';
+import requestSuccess from '../../hooks/requestSuccess';
+
+const { authenticate } = authentication.hooks;
+
+export default {
+ before: {
+ all: [authenticate('jwt')],
+ find: [],
+ get: [],
+ create: [],
+ update: [],
+ patch: [],
+ remove: []
+ },
+
+ after: {
+ all: [requestSuccess()],
+ find: [],
+ get: [],
+ create: [],
+ update: [],
+ patch: [],
+ remove: []
+ },
+
+ error: {
+ all: [requestFail()],
+ find: [],
+ get: [],
+ create: [],
+ update: [],
+ patch: [],
+ remove: []
+ }
+} as HooksObject;
diff --git a/Goobieverse/src/services/connections/connections.service.ts b/Goobieverse/src/services/connections/connections.service.ts
new file mode 100644
index 00000000..486e7e82
--- /dev/null
+++ b/Goobieverse/src/services/connections/connections.service.ts
@@ -0,0 +1,27 @@
+// Initializes the `connections` service on path `/connections`
+import { ServiceAddons } from '@feathersjs/feathers';
+import { Application } from '../../declarations';
+import { Connections } from './connections.class';
+import hooks from './connections.hooks';
+
+// Add this service to the service type index
+declare module '../../declarations' {
+ interface ServiceTypes {
+ 'connections': Connections & ServiceAddons;
+ }
+}
+
+export default function (app: Application): void {
+ const options = {
+ paginate: app.get('paginate'),
+ id:'id'
+ };
+
+ // Initialize our service with any options it requires
+ app.use('/connections', new Connections(options, app));
+
+ // Get our initialized service so that we can register hooks
+ const service = app.service('connections');
+
+ service.hooks(hooks);
+}
diff --git a/Goobieverse/src/services/email/email.class.ts b/Goobieverse/src/services/email/email.class.ts
new file mode 100644
index 00000000..174de40d
--- /dev/null
+++ b/Goobieverse/src/services/email/email.class.ts
@@ -0,0 +1,16 @@
+import { Db } from 'mongodb';
+import { Service, MongoDBServiceOptions } from 'feathers-mongodb';
+import { Application } from '../../declarations';
+
+export class Email extends Service {
+ //eslint-disable-next-line @typescript-eslint/no-unused-vars
+ constructor(options: Partial, app: Application) {
+ super(options);
+
+ const client: Promise = app.get('mongoClient');
+
+ client.then(db => {
+ this.Model = db.collection('email');
+ });
+ }
+}
diff --git a/Goobieverse/src/services/email/email.hooks.ts b/Goobieverse/src/services/email/email.hooks.ts
new file mode 100644
index 00000000..cfbd4fde
--- /dev/null
+++ b/Goobieverse/src/services/email/email.hooks.ts
@@ -0,0 +1,33 @@
+import { HooksObject } from '@feathersjs/feathers';
+
+export default {
+ before: {
+ all: [],
+ find: [],
+ get: [],
+ create: [],
+ update: [],
+ patch: [],
+ remove: []
+ },
+
+ after: {
+ all: [],
+ find: [],
+ get: [],
+ create: [],
+ update: [],
+ patch: [],
+ remove: []
+ },
+
+ error: {
+ all: [],
+ find: [],
+ get: [],
+ create: [],
+ update: [],
+ patch: [],
+ remove: []
+ }
+} as HooksObject;
diff --git a/Goobieverse/src/services/email/email.service.ts b/Goobieverse/src/services/email/email.service.ts
new file mode 100644
index 00000000..cb018f96
--- /dev/null
+++ b/Goobieverse/src/services/email/email.service.ts
@@ -0,0 +1,25 @@
+// Initializes the `email` service on path `/email`
+import { ServiceAddons } from '@feathersjs/feathers';
+import { Application } from '../../declarations';
+import { Email } from './email.class';
+import hooks from './email.hooks';
+import config from '../../appconfig';
+import smtpTransport from 'nodemailer-smtp-transport';
+import Mailer from 'feathers-mailer';
+
+
+// Add this service to the service type index
+declare module '../../declarations' {
+ interface ServiceTypes {
+ 'email': Email & ServiceAddons;
+ }
+}
+
+export default function (app: Application): void {
+ const event = Mailer(smtpTransport(config.email));
+ app.use('email', event);
+
+ const service = app.service('email');
+
+ service.hooks(hooks);
+}
diff --git a/Goobieverse/src/services/friends/friends.class.ts b/Goobieverse/src/services/friends/friends.class.ts
new file mode 100644
index 00000000..39f8246e
--- /dev/null
+++ b/Goobieverse/src/services/friends/friends.class.ts
@@ -0,0 +1,54 @@
+import { MongoDBServiceOptions } from 'feathers-mongodb';
+import { DatabaseService } from './../../dbservice/DatabaseService';
+import { Application } from '../../declarations';
+import config from '../../appconfig';
+import { Response } from '../../utils/response';
+import { Params } from '@feathersjs/feathers';
+
+
+export class Friends extends DatabaseService {
+ //eslint-disable-next-line @typescript-eslint/no-unused-vars
+ constructor(options: Partial, app: Application) {
+ super(options, app);
+ this.app = app;
+ }
+
+ async create(data: any, params?: any): Promise {
+ if (data && data.username) {
+ const ParticularUserData: any = await this.findData(config.dbCollections.accounts, { query: { id: params.user.id } });
+ if (ParticularUserData.data[0].connections.includes(data.username)) {
+ const newParticularUserData = ParticularUserData.data[0];
+ newParticularUserData.friends.push(data.username);
+ await this.UpdateDataById(config.dbCollections.accounts,newParticularUserData, params.user.id);
+ } else {
+ return Response.error('cannot add friend who is not a connection');
+ }
+ } else {
+ return Response.error('Badly formed request');
+ }
+ }
+
+ async find(params?: any): Promise {
+ if (params.user.friends) {
+ const friends = params.user.friends;
+ return Promise.resolve({ friends });
+ } else {
+ throw new Error('No friend found');
+ }
+ }
+
+ async remove(id: string, params?: any): Promise {
+ if (params.user.friends) {
+ const ParticularUserData: any = await this.findData(config.dbCollections.accounts, { query: { id: params.user.id } });
+ const friends = ParticularUserData.data[0].friends.filter(function (value:string) {
+ return value !== id;
+ });
+ ParticularUserData.data[0].friends = friends;
+ const newParticularUserData = ParticularUserData.data[0];
+ await this.UpdateDataById(config.dbCollections.accounts,newParticularUserData, params.user.id);
+ } else {
+ throw new Error('Not logged in');
+ }
+ }
+
+}
diff --git a/Goobieverse/src/services/friends/friends.hooks.ts b/Goobieverse/src/services/friends/friends.hooks.ts
new file mode 100644
index 00000000..e2333ff5
--- /dev/null
+++ b/Goobieverse/src/services/friends/friends.hooks.ts
@@ -0,0 +1,38 @@
+import { HooksObject } from '@feathersjs/feathers';
+import * as authentication from '@feathersjs/authentication';
+import requestFail from '../../hooks/requestFail';
+import requestSuccess from '../../hooks/requestSuccess';
+
+const { authenticate } = authentication.hooks;
+
+export default {
+ before: {
+ all: [authenticate('jwt')],
+ find: [],
+ get: [],
+ create: [],
+ update: [],
+ patch: [],
+ remove: [],
+ },
+
+ after: {
+ all: [requestSuccess()],
+ find: [],
+ get: [],
+ create: [],
+ update: [],
+ patch: [],
+ remove: [],
+ },
+
+ error: {
+ all: [requestFail()],
+ find: [],
+ get: [],
+ create: [],
+ update: [],
+ patch: [],
+ remove: [],
+ },
+} as HooksObject;
diff --git a/Goobieverse/src/services/friends/friends.service.ts b/Goobieverse/src/services/friends/friends.service.ts
new file mode 100644
index 00000000..17e3180b
--- /dev/null
+++ b/Goobieverse/src/services/friends/friends.service.ts
@@ -0,0 +1,27 @@
+// Initializes the `friends` service on path `/friends`
+import { ServiceAddons } from '@feathersjs/feathers';
+import { Application } from '../../declarations';
+import { Friends } from './friends.class';
+import hooks from './friends.hooks';
+
+// Add this service to the service type index
+declare module '../../declarations' {
+ interface ServiceTypes {
+ friends: Friends & ServiceAddons;
+ }
+}
+
+export default function (app: Application): void {
+ const options = {
+ paginate: app.get('paginate'),
+ id:'id'
+ };
+
+ // Initialize our service with any options it requires
+ app.use('/friends', new Friends(options, app));
+
+ // Get our initialized service so that we can register hooks
+ const service = app.service('friends');
+
+ service.hooks(hooks);
+}
diff --git a/Goobieverse/src/services/index.ts b/Goobieverse/src/services/index.ts
new file mode 100644
index 00000000..eb2ef53e
--- /dev/null
+++ b/Goobieverse/src/services/index.ts
@@ -0,0 +1,17 @@
+import { Application } from '../declarations';
+import profiles from './profiles/profiles.service';
+// Don't remove this comment. It's needed to format import lines nicely.
+import users from './users/users.service';
+import friends from './friends/friends.service';
+import auth from './auth/auth.service';
+import email from './email/email.service';
+import connections from './connections/connections.service';
+
+export default function (app: Application): void {
+ app.configure(auth);
+ app.configure(users);
+ app.configure(friends);
+ app.configure(profiles);
+ app.configure(email);
+ app.configure(connections);
+}
diff --git a/Goobieverse/src/services/profiles/profiles.class.ts b/Goobieverse/src/services/profiles/profiles.class.ts
new file mode 100644
index 00000000..6a36727d
--- /dev/null
+++ b/Goobieverse/src/services/profiles/profiles.class.ts
@@ -0,0 +1,104 @@
+import { DatabaseService } from './../../dbservice/DatabaseService';
+import { DomainModel } from './../../interfaces/DomainModel';
+import { AccountModel } from '../../interfaces/AccountModel';
+import config from '../../appconfig';
+import { Availability } from '../../utils/sets/Availability';
+import { Params, Id } from '@feathersjs/feathers';
+import { MongoDBServiceOptions } from 'feathers-mongodb';
+import { Application } from '../../declarations';
+import { buildAccountProfile } from '../../responsebuilder/accountsBuilder';
+import { IsNotNullOrEmpty } from '../../utils/Misc';
+
+export class Profiles extends DatabaseService {
+
+ constructor(options: Partial, app: Application) {
+ super(options,app);
+ }
+
+ async find(params?: Params): Promise {
+
+ const perPage = parseInt(params?.query?.per_page) || 10;
+ const skip = ((parseInt(params?.query?.page) || 1) - 1) * perPage;
+
+ const accountData = await this.findData(config.dbCollections.accounts,{
+ query: {
+ $or: [{availability:undefined},{availability: Availability.ALL }],
+ $skip: skip,
+ $limit: perPage,
+ },
+ });
+
+ let accounts:AccountModel[] = [];
+
+ if(accountData instanceof Array){
+ accounts = accountData as Array;
+ }else{
+ accounts = accountData.data as Array;
+ }
+
+
+ const domainIds = (accounts as Array)
+ ?.map((item) => item.locationDomainId)
+ .filter(
+ (value, index, self) =>
+ self.indexOf(value) === index && value !== undefined
+ );
+
+ const domainData= await this.findData(config.dbCollections.domains,{ query:{id: { $in: domainIds }}});
+
+ let domains:DomainModel[] = [];
+
+ if(domainData instanceof Array){
+ domains = domainData as Array;
+ }else{
+ domains = domainData.data as Array;
+ }
+
+ const profiles: Array = [];
+
+ (accounts as Array)?.forEach(async (element) => {
+ let domainModel: DomainModel | undefined;
+ for (const domain of domains) {
+ if (domain && domain.id === element.locationDomainId) {
+ domainModel = domain;
+ break;
+ }
+ }
+ profiles.push(await buildAccountProfile(element, domainModel));
+ });
+ return Promise.resolve({ profiles });
+ }
+
+ async get(id: Id, params: Params): Promise {
+ const accountData = await this.findData(config.dbCollections.accounts,{query:{id:id}});
+
+ let accounts:AccountModel[] = [];
+
+ if(accountData instanceof Array){
+ accounts = accountData as Array;
+ }else{
+ accounts = accountData.data as Array;
+ }
+
+ if(IsNotNullOrEmpty(accounts)){
+ const account = (accounts as Array)[0];
+
+ const domainData = await this.findData(config.dbCollections.domains,{ id: { $eq: account.locationDomainId } });
+
+ let domains:DomainModel[] = [];
+
+ if(domainData instanceof Array){
+ domains = domainData as Array;
+ }else{
+ domains = domainData.data as Array;
+ }
+
+ let domainModel: any;
+ if(IsNotNullOrEmpty(domains)){domainModel = domains[0];}
+ const profile = await buildAccountProfile(account, domainModel);
+ return Promise.resolve({ profile });
+ }else{
+ return Promise.resolve({});
+ }
+ }
+}
diff --git a/Goobieverse/src/services/profiles/profiles.hooks.ts b/Goobieverse/src/services/profiles/profiles.hooks.ts
new file mode 100644
index 00000000..fd8a4416
--- /dev/null
+++ b/Goobieverse/src/services/profiles/profiles.hooks.ts
@@ -0,0 +1,36 @@
+import { disallow } from 'feathers-hooks-common';
+import checkAccessToAccount from '../../hooks/checkAccessToAccount';
+import requestFail from '../../hooks/requestFail';
+import requestSuccess from '../../hooks/requestSuccess';
+import {Perm} from '../../utils/Perm';
+export default {
+ before: {
+ all: [],
+ find: [],
+ get: [checkAccessToAccount([Perm.PUBLIC,Perm.OWNER,Perm.ADMIN])],
+ create: [disallow()],
+ update: [disallow()],
+ patch: [disallow()],
+ remove: [disallow()]
+ },
+
+ after: {
+ all: [requestSuccess()],
+ find: [],
+ get: [],
+ create: [],
+ update: [],
+ patch: [],
+ remove: []
+ },
+
+ error: {
+ all: [requestFail()],
+ find: [],
+ get: [],
+ create: [],
+ update: [],
+ patch: [],
+ remove: []
+ }
+};
diff --git a/Goobieverse/src/services/profiles/profiles.service.ts b/Goobieverse/src/services/profiles/profiles.service.ts
new file mode 100644
index 00000000..c1a4c30b
--- /dev/null
+++ b/Goobieverse/src/services/profiles/profiles.service.ts
@@ -0,0 +1,26 @@
+// Initializes the `profiles` service on path `/profiles`
+import { ServiceAddons } from '@feathersjs/feathers';
+import { Application } from '../../declarations';
+import { Profiles } from './profiles.class';
+import hooks from './profiles.hooks';
+
+// Add this service to the service type index
+declare module '../../declarations' {
+ interface ServiceTypes {
+ 'profiles': Profiles & ServiceAddons;
+ }
+}
+
+export default function (app: Application): void {
+ const options = {
+ paginate: app.get('paginate')
+ };
+
+ // Initialize our service with any options it requires
+ app.use('/profiles', new Profiles(options, app));
+
+ // Get our initialized service so that we can register hooks
+ const service = app.service('profiles');
+
+ service.hooks(hooks);
+}
diff --git a/Goobieverse/src/services/users/users.class.ts b/Goobieverse/src/services/users/users.class.ts
new file mode 100644
index 00000000..6aa8490a
--- /dev/null
+++ b/Goobieverse/src/services/users/users.class.ts
@@ -0,0 +1,139 @@
+import { DatabaseService } from './../../dbservice/DatabaseService';
+import { MongoDBServiceOptions } from 'feathers-mongodb';
+import { Application } from '../../declarations';
+import config from '../../appconfig';
+import { Params } from '@feathersjs/feathers';
+import { AccountModel } from '../../interfaces/AccountModel';
+import { GenUUID } from '../../utils/Misc';
+import { Roles } from '../../utils/sets/Roles';
+import { IsNullOrEmpty, isValidObject } from '../../utils/Misc';
+import { SArray } from '../../utils/vTypes';
+import { sendEmail } from '../../utils/mail';
+import path from 'path';
+import fsPromises from 'fs/promises';
+
+export class Users extends DatabaseService {
+ app: Application;
+ //eslint-disable-next-line @typescript-eslint/no-unused-vars
+ constructor(options: Partial, app: Application) {
+ super(options, app);
+ this.app = app;
+ }
+
+ async create(data: AccountModel, params?: Params): Promise {
+ if (data.username && data.email && data.password) {
+ const username : string = data.username;
+ const email : string = data.email;
+ const password : string = data.password;
+ if (username) {
+ const accountsName: AccountModel[] = await this.findDataToArray(config.dbCollections.accounts, { query: { username: username } });
+ const name = (accountsName as Array)
+ ?.map((item) => item.username);
+ if (!name.includes(username)) {
+
+ const accountsEmail: AccountModel[] = await this.findDataToArray(config.dbCollections.accounts, { query:{email: email }});
+ const emailAddress = (accountsEmail as Array)
+ ?.map((item) => item.email);
+ if (!emailAddress.includes(email)) {
+
+ const id = GenUUID();
+ const roles = [Roles.USER];
+ const friends : string[] = [];
+ const connections : string[] = [];
+ const whenCreated = new Date();
+ const accountIsActive = true;
+ const accountWaitingVerification = false;
+ const accounts = await this.CreateData(config.dbCollections.accounts, {
+ ...data,
+ id: id,
+ roles: roles,
+ whenCreated: whenCreated,
+ friends: friends,
+ connections: connections,
+ accountIsActive: accountIsActive,
+ accountWaitingVerification :accountWaitingVerification
+ });
+ if (isValidObject(accounts)) {
+ const emailToValidate = data.email;
+ const emailRegexp = /^[a-zA-Z0-9.!#$%&'+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)$/;
+ if (emailRegexp.test(emailToValidate)) {
+ try {
+ const adminAccountName = config.metaverseServer['base_admin_account'];
+ if (accounts.username === adminAccountName) {
+ if (IsNullOrEmpty(accounts.roles)) accounts.roles = [];
+ SArray.add(accounts.roles, Roles.ADMIN);
+ }
+
+ const verificationURL = config.metaverse['metaverseServerUrl']
+ + `/api/v1/account/verify/email?a=${accounts.id}&v=${accounts.id}`;
+ const metaverseName = config.metaverse['metaverseName'];
+ const shortMetaverseName = config.metaverse['metaverseNickName'];
+ const verificationFile = path.join(__dirname, '../..', config.metaverseServer['email_verification_email_body']);
+
+ let emailBody = await fsPromises.readFile(verificationFile, 'utf-8');
+ emailBody = emailBody.replace('VERIFICATION_URL', verificationURL)
+ .replace('METAVERSE_NAME', metaverseName)
+ .replace('SHORT_METAVERSE_NAME', shortMetaverseName);
+
+ const email = {
+ from: 'khilan.odan@gmail.com',
+ to: accounts.email,
+ subject: `${shortMetaverseName} account verification`,
+ html: emailBody,
+ };
+ const sendEmailVerificationLink = await sendEmail(this.app, email).then(
+ function (result:any) {
+ let sendEmailVerificationStatus = {};
+ sendEmailVerificationStatus = result == null ? {} : result;
+ return sendEmailVerificationStatus;
+ }
+ );
+ return Promise.resolve({
+ accountId: accounts.id,
+ username: accounts.username,
+ accountIsActive: accounts.accountIsActive,
+ accountWaitingVerification:accounts.accountWaitingVerification
+ });
+ } catch (error: any) {
+ throw new Error('Exception adding user: ' + error);
+ }
+ }
+ else {
+ throw new Error('Send valid Email address');
+ }
+ } else {
+ throw new Error('Could not create account');
+ }
+ } else {
+ throw new Error('Email already exists');
+ }
+ } else {
+ throw new Error('Account already exists');
+ }
+ } else {
+ throw new Error('Badly formatted username');
+ }
+ } else {
+ throw new Error('Badly formatted request');
+ }
+ }
+
+ async find(params?: Params): Promise {
+
+ const perPage = parseInt(params?.query?.per_page) || 10;
+ const skip = ((parseInt(params?.query?.page) || 1) - 1) * perPage;
+
+
+ const user = await this.findDataToArray(config.dbCollections.accounts, {
+ query: {
+ accountIsActive: true ,
+ $select: [ 'username', 'accountId' ],
+ $skip: skip,
+ $limit: perPage
+ }
+ });
+
+ return Promise.resolve({ user });
+ }
+
+}
diff --git a/Goobieverse/src/services/users/users.hooks.ts b/Goobieverse/src/services/users/users.hooks.ts
new file mode 100644
index 00000000..34a59886
--- /dev/null
+++ b/Goobieverse/src/services/users/users.hooks.ts
@@ -0,0 +1,42 @@
+import { HooksObject } from '@feathersjs/feathers';
+import * as local from '@feathersjs/authentication-local';
+import requestFail from '../../hooks/requestFail';
+import requestSuccess from '../../hooks/requestSuccess';
+// import { Perm } from '../../utils/Perm';
+// import checkAccessToAccount from '../../hooks/checkAccessToAccount';
+import * as authentication from '@feathersjs/authentication';
+
+const { authenticate } = authentication.hooks;
+const { hashPassword, protect } = local.hooks;
+
+export default {
+ before: {
+ all: [],
+ find: [authenticate('jwt')],
+ get: [],
+ create: [hashPassword('password')],
+ update: [hashPassword('password')],
+ patch: [],
+ remove: [],
+ },
+
+ after: {
+ all: [requestSuccess()],
+ find: [],
+ get: [],
+ create: [],
+ update: [],
+ patch: [],
+ remove: [],
+ },
+
+ error: {
+ all: [requestFail()],
+ find: [],
+ get: [],
+ create: [],
+ update: [],
+ patch: [],
+ remove: [],
+ },
+} as HooksObject;
diff --git a/Goobieverse/src/services/users/users.service.ts b/Goobieverse/src/services/users/users.service.ts
new file mode 100644
index 00000000..820552c4
--- /dev/null
+++ b/Goobieverse/src/services/users/users.service.ts
@@ -0,0 +1,27 @@
+// Initializes the `users` service on path `/users`
+import { ServiceAddons } from '@feathersjs/feathers';
+import { Application } from '../../declarations';
+import { Users } from './users.class';
+import hooks from './users.hooks';
+
+// Add this service to the service type index
+declare module '../../declarations' {
+ interface ServiceTypes {
+ users: Users & ServiceAddons;
+ }
+}
+
+export default function (app: Application): void {
+ const options = {
+ paginate: app.get('paginate'),
+ id:'id'
+ };
+
+ // Initialize our service with any options it requires
+ app.use('/users', new Users(options, app));
+
+ // Get our initialized service so that we can register hooks
+ const service = app.service('users');
+
+ service.hooks(hooks);
+}
diff --git a/Goobieverse/src/utils/Misc.ts b/Goobieverse/src/utils/Misc.ts
new file mode 100644
index 00000000..3edc7fd2
--- /dev/null
+++ b/Goobieverse/src/utils/Misc.ts
@@ -0,0 +1,142 @@
+import fs from 'fs';
+import http from 'http';
+import https from 'https';
+import os from 'os';
+import { v4 as uuidv4 } from 'uuid';
+
+// Return 'true' if the passed value is null or empty
+export function IsNullOrEmpty(pVal: any): boolean {
+ return (
+ typeof pVal === 'undefined' ||
+ pVal === null ||
+ (typeof pVal === 'string' && String(pVal).length === 0)
+ );
+}
+
+export function GenUUID(): string {
+ return uuidv4();
+}
+
+// Return 'true' if the passed value is not null or empty
+export function IsNotNullOrEmpty(pVal: any): boolean {
+ return !IsNullOrEmpty(pVal);
+}
+
+// Utility routine that reads in JSON content from either an URL or a filename.
+// Returns the parsed JSON object or 'undefined' if any errors.
+export async function readInJSON(pFilenameOrURL: string): Promise {
+ let configBody: string;
+ if (pFilenameOrURL.startsWith('http://')) {
+ configBody = await httpRequest(pFilenameOrURL);
+ } else {
+ if (pFilenameOrURL.startsWith('https://')) {
+ configBody = await httpsRequest(pFilenameOrURL);
+ } else {
+ try {
+ // We should technically sanitize this filename but if one can change the environment
+ // or config file variables, the app is already poned.
+ configBody = fs.readFileSync(pFilenameOrURL, 'utf-8');
+ } catch (err) {
+ configBody = '';
+ console.debug(
+ `readInJSON: failed read of user config file ${pFilenameOrURL}: ${err}`
+ );
+ }
+ }
+ }
+ if (IsNotNullOrEmpty(configBody)) {
+ return JSON.parse(configBody);
+ }
+ return undefined;
+}
+
+// Do a simple https GET and return the response as a string
+export async function httpsRequest(pUrl: string): Promise {
+ return new Promise((resolve, reject) => {
+ https
+ .get(pUrl, (resp: any) => {
+ let data = '';
+ resp.on('data', (chunk: string) => {
+ data += chunk;
+ });
+ resp.on('end', () => {
+ resolve(data);
+ });
+ })
+ .on('error', (err: any) => {
+ reject(err);
+ });
+ });
+}
+
+// Do a simple http GET and return the response as a string
+export async function httpRequest(pUrl: string): Promise {
+ return new Promise((resolve, reject) => {
+ http
+ .get(pUrl, (resp: any) => {
+ let data = '';
+ resp.on('data', (chunk: string) => {
+ data += chunk;
+ });
+ resp.on('end', () => {
+ resolve(data);
+ });
+ })
+ .on('error', (err: any) => {
+ reject(err);
+ });
+ });
+}
+
+let myExternalAddr: string;
+export async function getMyExternalIPAddress(): Promise {
+ if (IsNotNullOrEmpty(myExternalAddr)) {
+ return Promise.resolve(myExternalAddr);
+ }
+ return new Promise((resolve, reject) => {
+ httpsRequest('https://api.ipify.org')
+ .then((resp) => {
+ myExternalAddr = resp;
+ resolve(myExternalAddr);
+ })
+ .catch((err) => {
+ // Can't get it that way for some reason. Ask our interface
+ const networkInterfaces = os.networkInterfaces();
+ // { 'lo1': [ info, info ], 'eth0': [ info, info ]} where 'info' could be v4 and v6 addr infos
+
+ let addressv4 = '';
+ let addressv6 = '';
+
+ Object.keys(networkInterfaces).forEach((dev) => {
+ networkInterfaces[dev]?.filter((details) => {
+ if (details.family === 'IPv4' && details.internal === false) {
+ addressv4 = details.address;
+ }
+ if (details.family === 'IPv6' && details.internal === false) {
+ addressv6 = details.address;
+ }
+ });
+ });
+ let address = '';
+ if (IsNullOrEmpty(addressv4)) {
+ address = addressv6;
+ } else {
+ address = addressv6;
+ }
+
+ if (IsNullOrEmpty(address)) {
+ reject('No address found');
+ }
+ myExternalAddr = address.toString();
+ resolve(myExternalAddr);
+ });
+ });
+}
+
+export const isValidArray = (arr: []) => {
+ return arr && Array.isArray(arr) && arr.length > 0;
+};
+
+export const isValidObject = (obj: object) => {
+ return obj && Object.keys(obj).length > 0;
+};
diff --git a/Goobieverse/src/utils/Perm.ts b/Goobieverse/src/utils/Perm.ts
new file mode 100644
index 00000000..bff36d70
--- /dev/null
+++ b/Goobieverse/src/utils/Perm.ts
@@ -0,0 +1,39 @@
+// Copyright 2020 Vircadia Contributors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+'use strict';
+
+// Permission codes:
+// 'all': any one
+// 'domain': the requesting authToken is for a domain and the sponsor account matches
+// 'owner': the requesting account is the owner of the target account
+// 'friend': the requesting account is a friend of the target account
+// 'connection': the requesting account is a connection of the target account
+// 'admin': the requesting account has 'admin' privilages
+// 'sponsor': the requesting account is the sponsor of the traget domain
+// 'domainaccess': the target entity has a domain and requesting account must be sponsor
+export class Perm {
+ public static NONE = 'none';
+ public static ALL = 'all';
+ public static PUBLIC = 'public'; // target account is publicly visible
+ public static DOMAIN = 'domain'; // check against .sponsorId
+ public static OWNER = 'owner'; // check against .id or .accountId
+ public static FRIEND = 'friend'; // check member of .friends
+ public static CONNECTION = 'connection';// check member of .connections
+ public static ADMIN = 'admin'; // check if isAdmin
+ public static SPONSOR = 'sponsor'; // check against .sponsorAccountId
+ public static MANAGER = 'manager'; // check against .managers
+ public static DOMAINACCESS = 'domainaccess'; // check that entity's domain has access
+}
+
diff --git a/Goobieverse/src/utils/Utils.ts b/Goobieverse/src/utils/Utils.ts
new file mode 100644
index 00000000..95a35e5f
--- /dev/null
+++ b/Goobieverse/src/utils/Utils.ts
@@ -0,0 +1,43 @@
+import { SArray } from './vTypes';
+import { Roles } from './sets/Roles';
+import { AccountModel } from '../interfaces/AccountModel';
+import config from '../appconfig';
+
+
+// The legacy interface returns public keys as a stripped PEM key.
+// "stripped" in that the bounding "BEGIN" and "END" lines have been removed.
+// This routine returns a stripped key string from a properly PEM formatted public key string.
+export function createSimplifiedPublicKey(pPubKey: string): string {
+ let keyLines: string[] = [];
+ if (pPubKey) {
+ keyLines = pPubKey.split('\n');
+ keyLines.shift(); // Remove the "BEGIN" first line
+ while (keyLines.length > 1
+ && ( keyLines[keyLines.length-1].length < 1 || keyLines[keyLines.length-1].includes('END PUBLIC KEY') ) ) {
+ keyLines.pop(); // Remove the "END" last line
+ }
+ }
+ return keyLines.join(''); // Combine all lines into one long string
+}
+
+// getter property that is 'true' if the user is a grid administrator
+export function isAdmin(pAcct: AccountModel): boolean {
+ return SArray.has(pAcct.roles, Roles.ADMIN);
+}
+// Any logic to test of account is active
+// Currently checks if account email is verified or is legacy
+// account (no 'accountEmailVerified' variable)
+export function isEnabled(pAcct: AccountModel): boolean {
+ return pAcct.accountEmailVerified ?? true;
+}
+
+
+export function isOnline(pAcct: AccountModel): boolean {
+ if (pAcct && pAcct.timeOfLastHeartbeat) {
+ return (
+ Date.now().valueOf() - pAcct.timeOfLastHeartbeat.valueOf() <
+ config.metaverseServer.heartbeat_seconds_until_offline * 1000
+ );
+ }
+ return false;
+}
diff --git a/Goobieverse/src/utils/mail.ts b/Goobieverse/src/utils/mail.ts
new file mode 100644
index 00000000..8c2cbda9
--- /dev/null
+++ b/Goobieverse/src/utils/mail.ts
@@ -0,0 +1,19 @@
+import { Application } from '../declarations';
+import { BadRequest } from '@feathersjs/errors';
+
+export async function sendEmail(app: Application, email: any): Promise {
+ if (email.to) {
+ email.html = email.html.replace(/&/g, '&');
+ try {
+ const abc = await app
+ .service('email')
+ .create(email)
+ .then(function (result) {
+ return result;
+ });
+ return abc;
+ } catch (error: any) {
+ return Promise.reject(new BadRequest(error));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Goobieverse/src/utils/messages.ts b/Goobieverse/src/utils/messages.ts
new file mode 100644
index 00000000..2fac7658
--- /dev/null
+++ b/Goobieverse/src/utils/messages.ts
@@ -0,0 +1,9 @@
+export const messages = {
+ common_messages_error: 'Something went wrong please try again later.',
+ common_messages_record_available: 'Record is available.',
+ common_messages_record_not_available: 'Record is not available.',
+ common_messages_records_available: 'Records are available.',
+ common_messages_records_not_available: 'Records are not available.',
+ common_messages_record_added_failed: 'Failed to add record!',
+
+};
diff --git a/Goobieverse/src/utils/response.ts b/Goobieverse/src/utils/response.ts
new file mode 100644
index 00000000..3a45a86c
--- /dev/null
+++ b/Goobieverse/src/utils/response.ts
@@ -0,0 +1,17 @@
+export const Response = {
+ success : (data: any,additionalFields?:any) => {
+ return {status: 'success',data: data,...additionalFields};
+ },
+ error:(message: string,additionalFields?:any) => {
+ return { status: 'failure', message: message,...additionalFields};
+ }
+};
+
+export enum HTTPStatusCode {
+ OK = 200,
+ Found = 302,
+ BadRequest = 400,
+ Unauthorized = 401,
+ Forbidden = 403,
+ NotFound = 404,
+}
diff --git a/Goobieverse/src/utils/sets/Availability.ts b/Goobieverse/src/utils/sets/Availability.ts
new file mode 100644
index 00000000..94ede550
--- /dev/null
+++ b/Goobieverse/src/utils/sets/Availability.ts
@@ -0,0 +1,28 @@
+// Copyright 2020 Vircadia Contributors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+'use strict';
+
+export class Availability {
+ public static NONE = 'none'; // no one can see me
+ public static FRIENDS = 'friends'; // available to friends
+ public static CONNECTIONS= 'connections'; // available to connections
+ public static ALL = 'all'; // available to all
+
+ // See if the passed availability code is a known availability token
+ static async KnownAvailability(pAvailability: string): Promise {
+ return [ Availability.NONE,Availability.FRIENDS,Availability.CONNECTIONS,Availability.ALL].includes(pAvailability);
+ }
+}
+
diff --git a/Goobieverse/src/utils/sets/Maturity.ts b/Goobieverse/src/utils/sets/Maturity.ts
new file mode 100644
index 00000000..8668f40e
--- /dev/null
+++ b/Goobieverse/src/utils/sets/Maturity.ts
@@ -0,0 +1,35 @@
+// Copyright 2020 Vircadia Contributors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+'use strict';
+
+export class Maturity {
+ public static UNRATED = 'unrated';
+ public static EVERYONE = 'everyone';
+ public static TEEN = 'teen';
+ public static MATURE = 'mature';
+ public static ADULT = 'adult';
+
+ static MaturityCategories = [ Maturity.UNRATED,
+ Maturity.EVERYONE,
+ Maturity.TEEN,
+ Maturity.MATURE,
+ Maturity.ADULT
+ ];
+
+ static KnownMaturity(pMaturity: string): boolean {
+ return this.MaturityCategories.includes(pMaturity);
+ }
+}
+
diff --git a/Goobieverse/src/utils/sets/Roles.ts b/Goobieverse/src/utils/sets/Roles.ts
new file mode 100644
index 00000000..fb662496
--- /dev/null
+++ b/Goobieverse/src/utils/sets/Roles.ts
@@ -0,0 +1,27 @@
+// Copyright 2020 Vircadia Contributors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+'use strict';
+
+// Class to manage the manipulations on roles that accounts can have
+export class Roles {
+ // at the moment, the only role is 'admin'
+ public static ADMIN = 'admin'; // someone who has metaverse-server admin
+ public static USER = 'user'; // a 'user' or 'person'
+
+ // See if the passed role code is a known role token
+ static async KnownRole(pScope: string): Promise {
+ return [ Roles.ADMIN, Roles.USER ].includes(pScope);
+ }
+}
diff --git a/Goobieverse/src/utils/sets/Visibility.ts b/Goobieverse/src/utils/sets/Visibility.ts
new file mode 100644
index 00000000..9e8debea
--- /dev/null
+++ b/Goobieverse/src/utils/sets/Visibility.ts
@@ -0,0 +1,36 @@
+// Copyright 2021 Vircadia Contributors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+'use strict';
+
+export class Visibility {
+ public static OPEN = 'open';
+ public static FRIENDS = 'friends';
+ public static CONNECTIONS = 'connections';
+ public static GROUP = 'group';
+ public static PRIVATE = 'private';
+
+ static VisibilityCategories = [
+ Visibility.OPEN,
+ Visibility.FRIENDS,
+ Visibility.CONNECTIONS,
+ Visibility.GROUP,
+ Visibility.PRIVATE
+ ];
+
+ static KnownVisibility(pVisibility: string): boolean {
+ return this.VisibilityCategories.includes(pVisibility);
+ }
+}
+
diff --git a/Goobieverse/src/utils/vTypes.ts b/Goobieverse/src/utils/vTypes.ts
new file mode 100755
index 00000000..5e51a7d3
--- /dev/null
+++ b/Goobieverse/src/utils/vTypes.ts
@@ -0,0 +1,64 @@
+// Copyright 2020 Vircadia Contributors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+'use strict';
+
+import { IsNullOrEmpty, IsNotNullOrEmpty } from './Misc';
+
+// An object that is used as a keyed collection of objects.
+// The key is always a string
+export interface VKeyedCollection {
+ [ key: string]: any
+}
+
+export interface VKeyValue {
+ [ key: string]: string
+}
+
+// String array.
+// Several structures are an array of strings (TokenScope, AccountRoles, ...).
+export class SArray {
+ static has(pArray: string[], pCheck: string): boolean {
+ return IsNullOrEmpty(pArray) ? false : pArray.includes(pCheck);
+ }
+
+ static hasNoCase(pArray: string[], pCheck: string): boolean {
+ const pCheckLower = pCheck.toLowerCase();
+ if (IsNotNullOrEmpty(pArray)) {
+ for (const ent of pArray) {
+ if (ent.toLowerCase() === pCheckLower) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ static add(pArray: string[], pAdd: string): boolean {
+ let added = false;
+ if (typeof(pAdd) === 'string') {
+ if (! pArray.includes(pAdd)) {
+ pArray.push(pAdd);
+ added = true;
+ }
+ }
+ return added;
+ }
+
+ static remove(pArray: string[], pRemove: string): void {
+ const idx = pArray.indexOf(pRemove);
+ if (idx >= 0) {
+ pArray.splice(idx, 1);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Goobieverse/test/app.test.ts b/Goobieverse/test/app.test.ts
new file mode 100644
index 00000000..4f7d365e
--- /dev/null
+++ b/Goobieverse/test/app.test.ts
@@ -0,0 +1,69 @@
+import assert from 'assert';
+import { Server } from 'http';
+import url from 'url';
+import axios from 'axios';
+
+import app from '../src/app';
+
+const port = app.get('port') || 8998;
+const getUrl = (pathname?: string): string => url.format({
+ hostname: app.get('host') || 'localhost',
+ protocol: 'http',
+ port,
+ pathname
+});
+
+describe('Feathers application tests (with jest)', () => {
+ let server: Server;
+
+ beforeAll(done => {
+ server = app.listen(port);
+ server.once('listening', () => done());
+ });
+
+ afterAll(done => {
+ server.close(done);
+ });
+
+ it('starts and shows the index page', async () => {
+ expect.assertions(1);
+
+ const { data } = await axios.get(getUrl());
+
+ expect(data.indexOf('')).not.toBe(-1);
+ });
+
+ describe('404', () => {
+ it('shows a 404 HTML page', async () => {
+ expect.assertions(2);
+
+ try {
+ await axios.get(getUrl('path/to/nowhere'), {
+ headers: {
+ 'Accept': 'text/html'
+ }
+ });
+ } catch (error: any) {
+ const { response } = error;
+
+ expect(response.status).toBe(404);
+ expect(response.data.indexOf('')).not.toBe(-1);
+ }
+ });
+
+ it('shows a 404 JSON error without stack trace', async () => {
+ expect.assertions(4);
+
+ try {
+ await axios.get(getUrl('path/to/nowhere'));
+ } catch (error: any) {
+ const { response } = error;
+
+ expect(response.status).toBe(404);
+ expect(response.data.code).toBe(404);
+ expect(response.data.message).toBe('Page not found');
+ expect(response.data.name).toBe('NotFound');
+ }
+ });
+ });
+});
diff --git a/Goobieverse/test/authentication.test.ts b/Goobieverse/test/authentication.test.ts
new file mode 100644
index 00000000..c92c9bb9
--- /dev/null
+++ b/Goobieverse/test/authentication.test.ts
@@ -0,0 +1,32 @@
+import app from '../src/app';
+
+describe('authentication', () => {
+ it('registered the authentication service', () => {
+ expect(app.service('authentication')).toBeTruthy();
+ });
+
+ describe('local strategy', () => {
+ const userInfo = {
+ email: 'someone@example.com',
+ password: 'supersecret'
+ };
+
+ beforeAll(async () => {
+ try {
+ await app.service('users').create(userInfo);
+ } catch (error) {
+ // Do nothing, it just means the user already exists and can be tested
+ }
+ });
+
+ it('authenticates user and creates accessToken', async () => {
+ const { user, accessToken } = await app.service('authentication').create({
+ strategy: 'local',
+ ...userInfo
+ }, {});
+
+ expect(accessToken).toBeTruthy();
+ expect(user).toBeTruthy();
+ });
+ });
+});
diff --git a/Goobieverse/test/services/connections.test.ts b/Goobieverse/test/services/connections.test.ts
new file mode 100644
index 00000000..1d840d40
--- /dev/null
+++ b/Goobieverse/test/services/connections.test.ts
@@ -0,0 +1,8 @@
+import app from '../../src/app';
+
+describe('\'connections\' service', () => {
+ it('registered the service', () => {
+ const service = app.service('connections');
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/Goobieverse/test/services/email.test.ts b/Goobieverse/test/services/email.test.ts
new file mode 100644
index 00000000..ecaedcc4
--- /dev/null
+++ b/Goobieverse/test/services/email.test.ts
@@ -0,0 +1,8 @@
+import app from '../../src/app';
+
+describe('\'email\' service', () => {
+ it('registered the service', () => {
+ const service = app.service('email');
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/Goobieverse/test/services/friends.test.ts b/Goobieverse/test/services/friends.test.ts
new file mode 100644
index 00000000..fd36e7bd
--- /dev/null
+++ b/Goobieverse/test/services/friends.test.ts
@@ -0,0 +1,8 @@
+import app from '../../src/app';
+
+describe('\'friends\' service', () => {
+ it('registered the service', () => {
+ const service = app.service('friends');
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/Goobieverse/test/services/metaverse_info.test.ts b/Goobieverse/test/services/metaverse_info.test.ts
new file mode 100644
index 00000000..a4d9320f
--- /dev/null
+++ b/Goobieverse/test/services/metaverse_info.test.ts
@@ -0,0 +1,8 @@
+import app from '../../src/app';
+
+describe('\'metaverse_info\' service', () => {
+ it('registered the service', () => {
+ const service = app.service('metaverse_info');
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/Goobieverse/test/services/profiles.test.ts b/Goobieverse/test/services/profiles.test.ts
new file mode 100644
index 00000000..9525af9f
--- /dev/null
+++ b/Goobieverse/test/services/profiles.test.ts
@@ -0,0 +1,8 @@
+import app from '../../src/app';
+
+describe('\'profiles\' service', () => {
+ it('registered the service', () => {
+ const service = app.service('profiles');
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/Goobieverse/test/services/users.test.ts b/Goobieverse/test/services/users.test.ts
new file mode 100644
index 00000000..20d1ebb3
--- /dev/null
+++ b/Goobieverse/test/services/users.test.ts
@@ -0,0 +1,8 @@
+import app from '../../src/app';
+
+describe('\'users\' service', () => {
+ it('registered the service', () => {
+ const service = app.service('users');
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/Goobieverse/tsconfig.json b/Goobieverse/tsconfig.json
new file mode 100644
index 00000000..70dd6b8a
--- /dev/null
+++ b/Goobieverse/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "target": "es2018",
+ "module": "commonjs",
+ "outDir": "./lib",
+ "rootDir": "./src",
+ "strict": true,
+ "esModuleInterop": true
+ },
+ "exclude": [
+ "test"
+ ]
+}
diff --git a/Goobieverse/types.d.ts b/Goobieverse/types.d.ts
new file mode 100644
index 00000000..0a5888aa
--- /dev/null
+++ b/Goobieverse/types.d.ts
@@ -0,0 +1,5 @@
+
+
+declare module 'feathers-mailer' {
+ export default function Mailer(transport: any, defaults?: any): any;
+}
diff --git a/Goobieverse/verificationEmail.html b/Goobieverse/verificationEmail.html
new file mode 100644
index 00000000..0d98d273
--- /dev/null
+++ b/Goobieverse/verificationEmail.html
@@ -0,0 +1,18 @@
+
+
+ You have created an account in the METAVERSE_NAME metaverse.
+
+
+
+ Please verify the account email by following this link:
+
+
+
+ See you in the virtual world!
+
+
+ -- SHORT_METAVERSE_NAME admin
+
+
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 7b686b0f..57c1c0f0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "iamus-metaverse-server",
- "version": "2.4.9",
+ "version": "2.4.10",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -604,6 +604,11 @@
"is-symbol": "^1.0.2"
}
},
+ "esbuild": {
+ "version": "0.11.23",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.11.23.tgz",
+ "integrity": "sha512-iaiZZ9vUF5wJV8ob1tl+5aJTrwDczlvGP0JoMmnpC2B0ppiMCu8n8gmy5ZTGl5bcG081XBVn+U+jP+mPFm5T5Q=="
+ },
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -1379,6 +1384,20 @@
}
}
},
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ },
+ "source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
"sparse-bitfield": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
@@ -1504,6 +1523,15 @@
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
},
+ "ts-eager": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/ts-eager/-/ts-eager-2.0.2.tgz",
+ "integrity": "sha512-xzFPL2z7mgLs0brZXaIHTm91Pjl/Cuu9AMKprgSuK+kIS2LjiG8fqqg4eqz3tgBy9OIdupb9w55pr7ea3JBB+Q==",
+ "requires": {
+ "esbuild": "^0.11.20",
+ "source-map-support": "^0.5.19"
+ }
+ },
"tslib": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
diff --git a/package.json b/package.json
index 83d6da50..14be28bc 100755
--- a/package.json
+++ b/package.json
@@ -55,6 +55,7 @@
"morgan": "~1.9.1",
"multer": "^1.4.2",
"nodemailer": "^6.6.0",
+ "ts-eager": "^2.0.2",
"unique-names-generator": "^4.5.0",
"uuid": "^8.3.2",
"winston": "^3.3.3"