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"