diff --git a/Dockerfile b/Dockerfile index 16297a87..f868945a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,7 +41,7 @@ COPY --from=bundle-builder /app/package.json /app/ RUN mkdir /app/db WORKDIR /app -ADD ./config.yml /app/config.yml +ADD config/local.yml /app/config.yml VOLUME /app/db diff --git a/config.yml b/config/local.yml similarity index 70% rename from config.yml rename to config/local.yml index 43574dbd..16a4b323 100644 --- a/config.yml +++ b/config/local.yml @@ -2,20 +2,26 @@ # Configuration for RPGKeeper #----------------------------------------------------------------------------------------------------------------------- -overrideAuth: false -secret: $SESSION_SECRET -key: 'rpgk_session' -auth: - google: - clientID: $CLIENT_ID - clientSecret: $CLIENT_SECRET +# HTTP Server configuration http: - secure: false, + host: '0.0.0.0' port: 5678 + secure: false + +# Database database: - client: 'better-sqlite3' + client: "better-sqlite3" connection: - filename: './db/rpgk.db' + filename: "db/rpgk.db" useNullAsDefault: true +# Authentication configuration +auth: + session: + key: 'rpgk_session' + secret: $SESSION_SECRET + google: + clientID: $CLIENT_ID + clientSecret: $CLIENT_SECRET + #----------------------------------------------------------------------------------------------------------------------- diff --git a/knexfile.ts b/knexfile.ts index 855722d1..25cd05e3 100644 --- a/knexfile.ts +++ b/knexfile.ts @@ -2,63 +2,27 @@ // Knex Migration configuration //---------------------------------------------------------------------------------------------------------------------- -require('ts-node/register'); - -//---------------------------------------------------------------------------------------------------------------------- - -// This has to be first, for reasons -import dotenv from 'dotenv'; - -import knex from 'knex'; +import 'dotenv/config'; import configUtil from '@strata-js/util-config'; +import type { Knex } from 'knex'; -// Managers -import { getConfig } from './src/server/managers/database'; +import { ServerConfig } from './src/common/interfaces/config'; -// --------------------------------------------------------------------------------------------------------------------- -// Configuration -// --------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- -dotenv.config(); const env = (process.env.ENVIRONMENT ?? 'local').toLowerCase(); configUtil.load(`./config/${ env }.yml`); //---------------------------------------------------------------------------------------------------------------------- -module.exports = async() => -{ - const db = knex(getConfig()); - - // When this file is run, it expects the migrations to end in .ts, so accommodate that. - await db('knex_migrations') - .select() - .limit(1) - .then(async() => - { - await db.update({ name: db.raw('replace(name, \'.js\', \'.ts\')') }) - .from('knex_migrations'); - }) - .catch(async(error) => - { - if(error.code !== 'SQLITE_ERROR') - { - throw error; - } // end if - }); - - return { - ...getConfig(), - migrations: { - directory: './src/server/knex/migrations', - extension: 'ts', - loadExtensions: [ '.ts' ] - }, - seeds: { - directory: './src/server/knex/seeds', - loadExtensions: [ '.ts' ] - } - }; -}; +module.exports = { + ...configUtil.get().database ?? {}, + migrations: { + directory: './src/server/knex/migrations' + }, + seeds: { + directory: './src/server/knex/seeds' + } +} satisfies Knex.Config; //---------------------------------------------------------------------------------------------------------------------- - diff --git a/package-lock.json b/package-lock.json index 2765dca4..ae55cf0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,8 @@ "rpgdicejs": "^2.0.3", "socket.io": "^4.7.5", "trivialperms": "^2.0.0-beta.0", - "ts-essentials": "^10.0.0" + "ts-essentials": "^10.0.0", + "zod": "^3.23.8" }, "devDependencies": { "@ckpack/vue-color": "^1.3.0", @@ -70,6 +71,7 @@ "rimraf": "^5.0.5", "sass": "^1.32.13", "socket.io-client": "^4.7.5", + "ts-node": "^10.9.2", "typescript": "^5.1.6", "unplugin-vue-components": "^0.27.0", "vite": "^5.2.11", @@ -258,6 +260,18 @@ "w3c-keyname": "^2.2.4" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@ctrl/tinycolor": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", @@ -998,12 +1012,31 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", "dev": true }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@lezer/common": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", @@ -1390,6 +1423,30 @@ "pino": "^8.11.0" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "node_modules/@tuplo/envsubst": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/@tuplo/envsubst/-/envsubst-1.15.2.tgz", @@ -2088,6 +2145,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2153,6 +2219,12 @@ "node": ">= 8" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2729,6 +2801,12 @@ "node": ">= 0.10" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "node_modules/crelt": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", @@ -2893,6 +2971,15 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -4729,6 +4816,12 @@ "resolved": "https://registry.npmjs.org/mailgun/-/mailgun-0.5.0.tgz", "integrity": "sha512-g0qrj4RP7l3S6+9Fb7x0nTmRoR+oB1rm68iEuSg3IKJir67b9RE5kfsNyK3ZenVgDCLRCdtaheDiybjkSYeZRA==" }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "node_modules/map-stream": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", @@ -6653,6 +6746,49 @@ } } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -6822,6 +6958,12 @@ "node": ">= 0.4.0" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -7163,6 +7305,15 @@ "node": ">= 14" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -7174,6 +7325,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index f8223b3b..0cf62141 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "start": "node ./dist/server/server.js", "dev": "tsc --build && DEBUG=true node ./dist/server/server.js --dev", "build": "tsc --build && vite build", + "db:migrate": "knex migrate:latest", + "db:setup": "knex migrate:latest && knex seed:run", "lint": "eslint --ext .ts,.js,.vue src/", "prepare": "if-env NODE_ENV=production && exit 0 || husky install" }, @@ -38,7 +40,8 @@ "rpgdicejs": "^2.0.3", "socket.io": "^4.7.5", "trivialperms": "^2.0.0-beta.0", - "ts-essentials": "^10.0.0" + "ts-essentials": "^10.0.0", + "zod": "^3.23.8" }, "devDependencies": { "@ckpack/vue-color": "^1.3.0", @@ -75,6 +78,7 @@ "rimraf": "^5.0.5", "sass": "^1.32.13", "socket.io-client": "^4.7.5", + "ts-node": "^10.9.2", "typescript": "^5.1.6", "unplugin-vue-components": "^0.27.0", "vite": "^5.2.11", diff --git a/src/client/components/systems/eote/charCard.vue b/src/client/components/systems/eote/charCard.vue index 1b4c4e6d..c271583b 100644 --- a/src/client/components/systems/eote/charCard.vue +++ b/src/client/components/systems/eote/charCard.vue @@ -72,7 +72,7 @@ import { useCharactersStore } from '../../../lib/stores/characters'; // Utils - import { startCase } from '../../../../common/utils/misc'; + import { startCase } from '../../../lib/utils/misc'; // Components import RpgkCard from '../../ui/rpgkCard.vue'; diff --git a/src/client/components/systems/eote/components/criticalCard.vue b/src/client/components/systems/eote/components/criticalCard.vue index 41ca0d87..79143eee 100644 --- a/src/client/components/systems/eote/components/criticalCard.vue +++ b/src/client/components/systems/eote/components/criticalCard.vue @@ -57,7 +57,7 @@ import { EoteCritical } from '../../../../../common/interfaces/systems/eote'; // Utils - import { shortID } from '../../../../../common/utils/misc'; + import { shortID } from '../../../../lib/utils/misc'; // Managers import eoteMan from '../../../../lib/managers/systems/eote'; diff --git a/src/client/components/systems/eote/components/forcePowerCard.vue b/src/client/components/systems/eote/components/forcePowerCard.vue index 8742ee70..f77905cd 100644 --- a/src/client/components/systems/eote/components/forcePowerCard.vue +++ b/src/client/components/systems/eote/components/forcePowerCard.vue @@ -72,7 +72,7 @@ import ReferenceBlock from '../../../character/referenceBlock.vue'; // Utils - import { startCase } from '../../../../../common/utils/misc'; + import { startCase } from '../../../../lib/utils/misc'; //------------------------------------------------------------------------------------------------------------------ // Component Definition diff --git a/src/client/components/systems/eote/components/motivationBlock.vue b/src/client/components/systems/eote/components/motivationBlock.vue index d2986603..5aa8bf9b 100644 --- a/src/client/components/systems/eote/components/motivationBlock.vue +++ b/src/client/components/systems/eote/components/motivationBlock.vue @@ -34,7 +34,7 @@ import { computed, ref } from 'vue'; // Utils - import { shortID } from '../../../../../common/utils/misc'; + import { shortID } from '../../../../lib/utils/misc'; // Managers import eoteMan from '../../../../lib/managers/systems/eote'; diff --git a/src/client/components/systems/eote/components/qualityEdit.vue b/src/client/components/systems/eote/components/qualityEdit.vue index fdecd189..08a35d6f 100644 --- a/src/client/components/systems/eote/components/qualityEdit.vue +++ b/src/client/components/systems/eote/components/qualityEdit.vue @@ -72,7 +72,7 @@ import Reference from '../../../character/referenceBlock.vue'; // Utils - import { uniqBy } from '../../../../../common/utils/misc'; + import { uniqBy } from '../../../../lib/utils/misc'; //------------------------------------------------------------------------------------------------------------------ // Component Definition diff --git a/src/client/components/systems/eote/components/qualityTag.vue b/src/client/components/systems/eote/components/qualityTag.vue index 4fe428ff..19a02ada 100644 --- a/src/client/components/systems/eote/components/qualityTag.vue +++ b/src/client/components/systems/eote/components/qualityTag.vue @@ -38,7 +38,7 @@ import { computed, ref } from 'vue'; // Utils - import { shortID } from '../../../../../common/utils/misc'; + import { shortID } from '../../../../lib/utils/misc'; // Managers import eoteMan from '../../../../lib/managers/systems/eote'; diff --git a/src/client/components/systems/eote/components/talentCard.vue b/src/client/components/systems/eote/components/talentCard.vue index 53ce1b8c..b2e6b717 100644 --- a/src/client/components/systems/eote/components/talentCard.vue +++ b/src/client/components/systems/eote/components/talentCard.vue @@ -62,7 +62,7 @@ import { EoteTalentInst, GenesysTalent } from '../../../../../common/interfaces/systems/eote'; // Utils - import { shortID } from '../../../../../common/utils/misc'; + import { shortID } from '../../../../lib/utils/misc'; // Managers import eoteMan from '../../../../lib/managers/systems/eote'; diff --git a/src/client/components/systems/eote/components/talentPlaceholder.vue b/src/client/components/systems/eote/components/talentPlaceholder.vue index ec4bab26..f33ec75e 100644 --- a/src/client/components/systems/eote/components/talentPlaceholder.vue +++ b/src/client/components/systems/eote/components/talentPlaceholder.vue @@ -31,7 +31,7 @@ import { computed, ref } from 'vue'; // Utils - import { shortID } from '../../../../../common/utils/misc'; + import { shortID } from '../../../../lib/utils/misc'; //------------------------------------------------------------------------------------------------------------------ // Refs diff --git a/src/client/components/systems/eote/modals/editCharacteristicsModal.vue b/src/client/components/systems/eote/modals/editCharacteristicsModal.vue index 6e23ac62..f40f63af 100644 --- a/src/client/components/systems/eote/modals/editCharacteristicsModal.vue +++ b/src/client/components/systems/eote/modals/editCharacteristicsModal.vue @@ -106,7 +106,7 @@ import { EoteCharacteristics, EoteOrGenCharacter } from '../../../../../common/interfaces/systems/eote'; // Utils - import { startCase } from '../../../../../common/utils/misc'; + import { startCase } from '../../../../lib/utils/misc'; // Components import { BModal } from 'bootstrap-vue-next'; diff --git a/src/client/components/systems/eote/modals/editForcePowersModal.vue b/src/client/components/systems/eote/modals/editForcePowersModal.vue index 5d17bcf8..5a740999 100644 --- a/src/client/components/systems/eote/modals/editForcePowersModal.vue +++ b/src/client/components/systems/eote/modals/editForcePowersModal.vue @@ -143,7 +143,7 @@ import { BModal } from 'bootstrap-vue-next'; // Utils - import { startCase, uniqBy } from '../../../../../common/utils/misc'; + import { startCase, uniqBy } from '../../../../lib/utils/misc'; import CloseButton from '../../../ui/closeButton.vue'; //------------------------------------------------------------------------------------------------------------------ diff --git a/src/client/components/systems/eote/modals/editSkillsModal.vue b/src/client/components/systems/eote/modals/editSkillsModal.vue index 7f2cf47f..5d8ee30e 100644 --- a/src/client/components/systems/eote/modals/editSkillsModal.vue +++ b/src/client/components/systems/eote/modals/editSkillsModal.vue @@ -233,7 +233,7 @@ // Components import { BModal } from 'bootstrap-vue-next'; - import { startCase } from '../../../../../common/utils/misc'; + import { startCase } from '../../../../lib/utils/misc'; import CloseButton from '../../../ui/closeButton.vue'; //------------------------------------------------------------------------------------------------------------------ diff --git a/src/client/components/systems/eote/modals/editTalentsModal.vue b/src/client/components/systems/eote/modals/editTalentsModal.vue index 296edaaf..5cfedf0b 100644 --- a/src/client/components/systems/eote/modals/editTalentsModal.vue +++ b/src/client/components/systems/eote/modals/editTalentsModal.vue @@ -151,7 +151,7 @@ import { BModal } from 'bootstrap-vue-next'; // Utils - import { uniqBy } from '../../../../../common/utils/misc'; + import { uniqBy } from '../../../../lib/utils/misc'; import CloseButton from '../../../ui/closeButton.vue'; //------------------------------------------------------------------------------------------------------------------ diff --git a/src/client/components/systems/eote/skillsCard.vue b/src/client/components/systems/eote/skillsCard.vue index 4c826440..9d88fcfa 100644 --- a/src/client/components/systems/eote/skillsCard.vue +++ b/src/client/components/systems/eote/skillsCard.vue @@ -228,7 +228,7 @@ import EditModal from './modals/editSkillsModal.vue'; // Utils - import { startCase } from '../../../../common/utils/misc'; + import { startCase } from '../../../lib/utils/misc'; //------------------------------------------------------------------------------------------------------------------ // Component Definition diff --git a/src/client/components/systems/eote/sub/eoteTalents.vue b/src/client/components/systems/eote/sub/eoteTalents.vue index ac8f7d7f..1775a433 100644 --- a/src/client/components/systems/eote/sub/eoteTalents.vue +++ b/src/client/components/systems/eote/sub/eoteTalents.vue @@ -39,7 +39,7 @@ import TalentCard from '../components/talentCard.vue'; // Utils - import { sortBy } from '../../../../../common/utils/misc'; + import { sortBy } from '../../../../lib/utils/misc'; //------------------------------------------------------------------------------------------------------------------ // Component Definition diff --git a/src/client/components/systems/eote/sub/tierRow.vue b/src/client/components/systems/eote/sub/tierRow.vue index 63ab0c45..baf51243 100644 --- a/src/client/components/systems/eote/sub/tierRow.vue +++ b/src/client/components/systems/eote/sub/tierRow.vue @@ -47,7 +47,7 @@ import TalentPlaceholder from '../components/talentPlaceholder.vue'; // Utils - import { sortBy } from '../../../../../common/utils/misc'; + import { sortBy } from '../../../../lib/utils/misc'; //------------------------------------------------------------------------------------------------------------------ // Component Definition diff --git a/src/client/lib/resource-access/character.ts b/src/client/lib/resource-access/character.ts index 0e45df9e..b99a72e4 100644 --- a/src/client/lib/resource-access/character.ts +++ b/src/client/lib/resource-access/character.ts @@ -13,7 +13,7 @@ import { useSystemsStore } from '../stores/systems'; // Utils import toastUtil from '../utils/toast'; -import { randomColor } from '../../../common/utils/misc'; +import { randomColor } from '../utils/colors'; // Errors import { CharacterSaveError, InvalidCharacterError } from '../error'; diff --git a/src/client/lib/utils/colors.ts b/src/client/lib/utils/colors.ts new file mode 100644 index 00000000..b90de348 --- /dev/null +++ b/src/client/lib/utils/colors.ts @@ -0,0 +1,48 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Color Utils +// --------------------------------------------------------------------------------------------------------------------- + +/** + * Generates a color based on a string. + * + * @param str - String to colorize. + * + * @returns Returns a color in hex code format. + */ +export function colorize(str : string) : string +{ + if(!str) + { + return '#aaaaaa'; + } + + let hash = 0; + for(let i = 0; i < str.length; i++) + { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + + let color = '#'; + for(let i = 0; i < 3; i++) + { + const value = (hash >> (i * 8)) & 0xFF; + color += (`00${ value.toString(16) }`).substr(-2); + } + + return color; +} + +/** + * Generate a random color in hex form. + */ +export function randomColor() : string +{ + function ChanelRand() : number + { + return Math.floor(Math.random() * (256 + 1)); + } + + const rgb = [ ChanelRand(), ChanelRand(), ChanelRand() ]; + return `#${ ((1 << 24) + (rgb[0] << 16) + (rgb[1] << 8) + rgb[2]).toString(16).slice(1) }`; +} +// --------------------------------------------------------------------------------------------------------------------- diff --git a/src/client/lib/utils/misc.ts b/src/client/lib/utils/misc.ts new file mode 100644 index 00000000..fe52c677 --- /dev/null +++ b/src/client/lib/utils/misc.ts @@ -0,0 +1,91 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Miscellaneous Utility Functions +// --------------------------------------------------------------------------------------------------------------------- + +import { customAlphabet } from 'nanoid'; +import { alphanumeric } from 'nanoid-dictionary'; + +//---------------------------------------------------------------------------------------------------------------------- + +const nanoID = customAlphabet(alphanumeric, 10); + +//---------------------------------------------------------------------------------------------------------------------- + +/** + * This generates nice, short ids (ex: 'HrILY', '2JjA9s') that are as unique as a uuid. + * + * @returns Returns a unique string id. + */ +export function shortID() : string +{ + return nanoID(); +} + +/** + * Converts a string to start case. + * + * @param str - The string to convert + * + * @returns Returns a string in start case format. + */ +export function startCase(str : string) : string +{ + const words = str.split(' '); + const capitalizedWords = words.map((word) => + { + const firstLetter = word.charAt(0).toUpperCase(); + const restOfWord = word.slice(1).toLowerCase(); + return firstLetter + restOfWord; + }); + + return capitalizedWords.join(' '); +} + +/** + * A comparator function for sorting by the key of an object. + * + * @param key - The key to sort by. + * + * @returns Returns`1`, `-1`, or `0`, depending on how the object sorts. + */ +export function sortBy(key : string) : (a : Record, b : Record) => number +{ + return (aObj : Record, bObj : Record) => + { + return (aObj[key] > bObj[key]) ? 1 : ((bObj[key] > aObj[key]) ? -1 : 0); + }; +} + +/** + * Creates a duplicate-free version of an array, using `iteratee` which is invoked for each element in `arr` to + * generate the criterion by which uniqueness is computed. The order of result values is determined by the order they + * occur in the array. The iteratee is invoked with one argument: `(value)`. + * + * **WARNING**: _This is not a drop in replacement solution, and it might not work for some edge cases._ + * + * _This is a simplified implementation of https://youmightnotneed.com/lodash#unionBy which would work with only one + * array._ + * + * @param arr - The array to inspect. + * @param iteratee - The iteratee invoked per element. + * + * @return Returns a copy of the array without duplicates. + */ +export function uniqBy(arr : T[], iteratee : string | ((item : T) => any)) : T[] +{ + const iter = (item : T) : any => + { + if(typeof iteratee === 'string') + { + return item[iteratee]; + } + else + { + iteratee(item); + } + }; + + return arr.filter((x, i, self) => i === self.findIndex((y) => iter(x) === iter(y))); +} + +// --------------------------------------------------------------------------------------------------------------------- diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json index a6f2976d..2f7576c4 100644 --- a/src/client/tsconfig.json +++ b/src/client/tsconfig.json @@ -5,16 +5,18 @@ "outDir": "../../dist", "skipLibCheck": true, "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "moduleResolution": "bundler", // Overrides - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "module": "Node16", - "moduleResolution": "node16", "allowSyntheticDefaultImports": true, + "resolveJsonModule": true }, "include": [ "../common/**/*.ts", "**/*.ts", "**/*.vue", + "components.d.ts" ] } diff --git a/src/common/interfaces/config.ts b/src/common/interfaces/config.ts index 389d5eb4..16c01aad 100644 --- a/src/common/interfaces/config.ts +++ b/src/common/interfaces/config.ts @@ -14,23 +14,32 @@ export interface GoogleAuthConfig export interface AuthConfig { - google : GoogleAuthConfig; + session : { + key : string; + secret : string; + }, + + // Auth Providers + google ?: GoogleAuthConfig; } export interface HTTPConfig { - secure : string; + host ?: string; port : number; + secure : string; +} + +export interface DatabaseConfig extends Knex.Config +{ + traceQueries ?: boolean; } -export interface RPGKeeperConfig +export interface ServerConfig { - overrideAuth : boolean; - secret : string; - key : string; auth : AuthConfig; http : HTTPConfig; - database : Knex.Config; + database : DatabaseConfig } // --------------------------------------------------------------------------------------------------------------------- diff --git a/src/server/auth/google.ts b/src/server/auth/google.ts index 79f01b06..89e7d5b8 100644 --- a/src/server/auth/google.ts +++ b/src/server/auth/google.ts @@ -2,20 +2,16 @@ // Google Authentication Support //---------------------------------------------------------------------------------------------------------------------- +import { Express } from 'express'; import passport from 'passport'; import GoogleStrategy from 'passport-google-oauth20'; -import { Express } from 'express'; -import configUtil from '@strata-js/util-config'; import logging from '@strata-js/util-logging'; -// Program Argument Parsing -import program from '../utils/args'; - // We just need to import this somewhere; here makes sense. import './serialization'; // Interfaces -import { RPGKeeperConfig } from '../../common/interfaces/config'; +import { ServerConfig } from '../../common/interfaces/config'; // Managers import * as accountMan from '../managers/account'; @@ -24,75 +20,74 @@ import * as accountMan from '../managers/account'; const logger = logging.getLogger('googleAuth'); -const callbackURL = `${ program.args.includes('--dev') ? 'http://localhost:5679' : process.env['DOMAIN'] }/auth/google/redirect`; - -const serverConfig = configUtil.get(); -const config = serverConfig.auth.google; - //---------------------------------------------------------------------------------------------------------------------- -passport.use(new GoogleStrategy( - { - clientID: config.clientID, - clientSecret: config.clientSecret, - callbackURL, - scope: [ 'profile', 'email' ], - state: true - }, - async (_accessToken, _refreshToken, profile, done) => +export default { + initialize(serverConfig : ServerConfig, app : Express, devMode = false) : void { - try - { - // TODO: Maybe support more than the first email? - const email = profile.emails[0].value; - const photo = profile.photos[0]?.value; - - let account; - try { account = await accountMan.getByEmail(email); } - catch (error) + const config = serverConfig.auth.google; + + const domain = devMode ? `http://localhost:${ serverConfig.http.port }` : process.env['DOMAIN']; + const callbackURL = `${ domain }/auth/google/redirect`; + + // Build Strategy + passport.use(new GoogleStrategy( { - if(error.code === 'ERR_NOT_FOUND') + clientID: config.clientID, + clientSecret: config.clientSecret, + callbackURL, + scope: [ 'profile', 'email' ], + state: true + }, + async (_accessToken, _refreshToken, profile, done) => + { + try { - account = null; + // TODO: Maybe support more than the first email? + const email = profile.emails[0].value; + const photo = profile.photos[0]?.value; + + let account; + try { account = await accountMan.getByEmail(email); } + catch (error) + { + if(error.code === 'ERR_NOT_FOUND') + { + account = null; + } + else + { + logger.error(`Encountered error during authentication:\n${ error.stack }`, error); + done(error); + } + } + + if(account) + { + account = await accountMan.update(account.id, { + name: account.name ?? profile.displayName ?? email.split('@')[0], + avatar: photo + }); + } + else + { + account = await accountMan.add({ + name: profile.displayName ?? email.split('@')[0], + avatar: photo, + email + }); + } + + done(null, account); } - else + catch (error) { logger.error(`Encountered error during authentication:\n${ error.stack }`, error); done(error); } } + )); - if(account) - { - account = await accountMan.update(account.id, { - name: account.name ?? profile.displayName ?? email.split('@')[0], - avatar: photo - }); - } - else - { - account = await accountMan.add({ - name: profile.displayName ?? email.split('@')[0], - avatar: photo, - email - }); - } - - done(null, account); - } - catch (error) - { - logger.error(`Encountered error during authentication:\n${ error.stack }`, error); - done(error); - } - } -)); - -//---------------------------------------------------------------------------------------------------------------------- - -export default { - initialize(app : Express) : void - { // Authenticate app.get('/auth/google', passport.authenticate('google')); @@ -101,21 +96,7 @@ export default { successReturnToOrRedirect: '/', failWithError: true })); - - // Get Current User - app.get('/auth/user', (req, resp) => - { - resp.json(req.user); - }); - - // Logout endpoint - app.post('/auth/logout', (req, res, done) => - { - req.logout(done); - res.end(); - }); } }; //---------------------------------------------------------------------------------------------------------------------- - diff --git a/src/server/knex/seeds/eote_abilities.ts b/src/server/knex/seeds/eote_abilities.ts index 3e976c0c..2f5bd935 100644 --- a/src/server/knex/seeds/eote_abilities.ts +++ b/src/server/knex/seeds/eote_abilities.ts @@ -4,7 +4,7 @@ import { Knex } from 'knex'; -import { sortBy } from '../../../common/utils/misc'; +import { sortBy } from '../../utils/misc'; //---------------------------------------------------------------------------------------------------------------------- diff --git a/src/server/knex/seeds/eote_attachments.ts b/src/server/knex/seeds/eote_attachments.ts index 063cfa92..5133ff2d 100644 --- a/src/server/knex/seeds/eote_attachments.ts +++ b/src/server/knex/seeds/eote_attachments.ts @@ -2,7 +2,7 @@ // Populate a default set of EotE/Genesys Attachments //---------------------------------------------------------------------------------------------------------------------- -import { sortBy } from '../../../common/utils/misc'; +import { sortBy } from '../../utils/misc'; //---------------------------------------------------------------------------------------------------------------------- diff --git a/src/server/knex/seeds/eote_qualities.ts b/src/server/knex/seeds/eote_qualities.ts index 5f4cdac4..011666e9 100644 --- a/src/server/knex/seeds/eote_qualities.ts +++ b/src/server/knex/seeds/eote_qualities.ts @@ -2,7 +2,7 @@ // Populate a default set of EotE/Genesys Qualities //---------------------------------------------------------------------------------------------------------------------- -import { sortBy } from '../../../common/utils/misc'; +import { sortBy } from '../../utils/misc'; //---------------------------------------------------------------------------------------------------------------------- diff --git a/src/server/knex/seeds/eote_talents.ts b/src/server/knex/seeds/eote_talents.ts index e6c18fc0..f1610551 100644 --- a/src/server/knex/seeds/eote_talents.ts +++ b/src/server/knex/seeds/eote_talents.ts @@ -2,7 +2,7 @@ // Populate a default set of EotE/Genesys Talents //---------------------------------------------------------------------------------------------------------------------- -import { sortBy } from '../../../common/utils/misc'; +import { sortBy } from '../../utils/misc'; //---------------------------------------------------------------------------------------------------------------------- diff --git a/src/server/managers/account.ts b/src/server/managers/account.ts index 717152aa..85bbe500 100644 --- a/src/server/managers/account.ts +++ b/src/server/managers/account.ts @@ -2,9 +2,6 @@ // Account Manager // --------------------------------------------------------------------------------------------------------------------- -// Managers -import { table } from './database'; - // Models import { Account } from '../models/account'; @@ -12,7 +9,8 @@ import { Account } from '../models/account'; import { MultipleResultsError, NotFoundError } from '../errors'; // Utils -import { shortID } from '../../common/utils/misc'; +import { getDB } from '../utils/database'; +import { shortID } from '../utils/misc'; // --------------------------------------------------------------------------------------------------------------------- @@ -26,7 +24,8 @@ export interface AccountFilters { export async function list(filters : AccountFilters) : Promise { - const query = table('account') + const db = await getDB(); + const query = db('account') .select( 'hash_id as id', 'email', @@ -56,7 +55,8 @@ export async function list(filters : AccountFilters) : Promise export async function getGroups(accountID : string) : Promise { - const roles = await table('account as ac') + const db = await getDB(); + const roles = await db('account as ac') .select('r.name as name', 'r.role_id as id') .join('account_role as ar', 'ac.account_id', '=', 'ar.account_id') .join('role as r', 'ar.role_id', '=', 'r.role_id') @@ -69,7 +69,8 @@ export async function getGroups(accountID : string) : Promise export async function getRaw(accountID : string) : Promise> { - const accounts = await table('account') + const db = await getDB(); + const accounts = await db('account') .select( 'account_id', 'hash_id as id', @@ -106,7 +107,8 @@ export async function get(accountID : string) : Promise export async function getByEmail(email : string) : Promise { - const accounts = await table('account') + const db = await getDB(); + const accounts = await db('account') .select( 'hash_id as id', 'email', @@ -135,7 +137,8 @@ export async function getByEmail(email : string) : Promise export async function add(newAccount : Record) : Promise { const account = Account.fromJSON({ ...newAccount, id: shortID(), created: Date.now() }); - await table('account') + const db = await getDB(); + await db('account') .insert(account.toDB()); return get(account.id); @@ -158,7 +161,8 @@ export async function update(accountID : string, accountUpdate : Record { - await table('account') + const db = await getDB(); + await db('account') .where({ hash_id: accountID }) .delete(); diff --git a/src/server/managers/character.ts b/src/server/managers/character.ts index 8e226a24..39cf812c 100644 --- a/src/server/managers/character.ts +++ b/src/server/managers/character.ts @@ -3,7 +3,6 @@ //---------------------------------------------------------------------------------------------------------------------- // Managers -import { table } from './database'; import * as accountMan from './account'; import * as notebookMan from './notebook'; import systemMan from './system'; @@ -12,17 +11,19 @@ import systemMan from './system'; import { Character } from '../models/character'; // Utils +import { getDB } from '../utils/database'; import { MultipleResultsError, NotFoundError } from '../errors'; -import { FilterToken } from '../routes/utils/query'; +import { FilterToken } from '../routes/utils'; import { applyFilters } from '../knex/utils'; -import { shortID } from '../../common/utils/misc'; +import { shortID } from '../utils/misc'; import { broadcast } from '../utils/sio'; //---------------------------------------------------------------------------------------------------------------------- export async function get(id : string) : Promise { - const characters = await table('character as char') + const db = await getDB(); + const characters = await db('character as char') .select( 'char.hash_id as id', 'char.system', @@ -57,7 +58,8 @@ export async function get(id : string) : Promise export async function list(filters : Record = {}) : Promise { - let query = table('character as char') + const db = await getDB(); + let query = db('character as char') .select( 'char.hash_id as id', 'char.system', @@ -92,7 +94,8 @@ export async function add(accountID : string, newCharacter : Record { - await table('character') + const db = await getDB(); + await db('character') .where({ hash_id: charID }) .delete(); diff --git a/src/server/managers/database.ts b/src/server/managers/database.ts deleted file mode 100644 index 6f06f389..00000000 --- a/src/server/managers/database.ts +++ /dev/null @@ -1,203 +0,0 @@ -// --------------------------------------------------------------------------------------------------------------------- -// Database Manager -// --------------------------------------------------------------------------------------------------------------------- - -import knex, { Knex } from 'knex'; -import configUtil from '@strata-js/util-config'; -import logging from '@strata-js/util-logging'; - -import { AppError } from '../errors'; - -// Interfaces -import { RPGKeeperConfig } from '../../common/interfaces/config'; - -//---------------------------------------------------------------------------------------------------------------------- - -interface DBConfig extends Knex.Config { - traceQueries ?: boolean -} - -// --------------------------------------------------------------------------------------------------------------------- - -// TODO: Make this configurable -const useTestDB = false; - -const serverConfig = configUtil.get(); - -const logger = logging.getLogger('dbMan'); - -// eslint-disable-next-line no-use-before-define -const dbConfig : DBConfig = _buildConfig(); -let db : Knex | undefined; - -// --------------------------------------------------------------------------------------------------------------------- - -function _buildConfig() : DBConfig -{ - const config : DBConfig = serverConfig.database ?? { - client: 'sqlite3', - connection: { - filename: './db/rpgk.db' - }, - useNullAsDefault: true - } as DBConfig; - - // The 'testDB' is an in-memory sqlite database. This makes testing easier. - if(useTestDB) - { - config.client = 'sqlite3'; - config.connection = { filename: ':memory:' }; - - // This is currently required by knex to prevent timeouts. - // Reference: https://github.com/tgriesser/knex/issues/1871 - config.pool = { - min: 1, - max: 1, - idleTimeoutMillis: 1000 * 60 * 60 * 100 // 100 hours - }; - } - - // We have some special sqlite configuration we need to do - if(config.client === 'sqlite3') - { - // eslint-disable-next-line @typescript-eslint/no-empty-function - const afterCreate = config?.pool?.afterCreate; - - // Create a new 'afterCreate' function that sets up sqlite. - const newAfterCreate = (dbConn, done) : void => - { - dbConn.run('PRAGMA foreign_keys = ON', (err) => - { - if(!err && config?.traceQueries) - { - // Turn on tracing - dbConn.on('trace', (queryString) => - { - logger.debug('QUERY TRACE:', queryString); - }); - - if(afterCreate) - { - return afterCreate(dbConn, done); - } - } - - done(err, dbConn); - }); - }; - - config.pool = config.pool ?? {}; - config.pool.afterCreate = newAfterCreate; - } - - return config; -} - -async function _setupDB() : Promise -{ - if(!db) - { - throw new AppError('Database not initialized!', 'DB_NOT_INITIALIZED'); - } - - await db('knex_migrations') - .select() - .limit(1) - .catch(async(error) => - { - if(error.code === 'SQLITE_ERROR') - { - if(!db) - { - throw new AppError('Database not initialized!', 'DB_NOT_INITIALIZED'); - } - - logger.warn('No existing database, creating one.'); - - await db.migrate.latest({ directory: './dist/server/knex/migrations' }); - await db.seed.run({ directory: './dist/server/knex/seeds' }); - - return db; - } - else - { - throw error; - } - }); - - // Cleanup the migration references; ensure we keep the extensions as `.js`; - await db.update({ name: db.raw('replace(name, \'.ts\', \'.js\')') }).from('knex_migrations'); - - // ----------------------------------------------------------------------------------------------------------------- - // Migrations - // ----------------------------------------------------------------------------------------------------------------- - - logger.info('Running any needed migrations...'); - - // Migrate to the latest migration - await db.migrate.latest({ directory: './dist/server/knex/migrations', loadExtensions: [ '.js' ] }); - - logger.info('Migrations complete.'); - - // ----------------------------------------------------------------------------------------------------------------- - // Seeds - // ----------------------------------------------------------------------------------------------------------------- - - logger.info('Running seeds...'); - - await db.seed.run({ directory: './dist/server/knex/seeds', loadExtensions: [ '.js' ] }); - - logger.info('Seeds complete.'); - - // ----------------------------------------------------------------------------------------------------------------- - - return db; -} - -// --------------------------------------------------------------------------------------------------------------------- - -export async function init() : Promise -{ - if(!db) - { - db = knex(dbConfig); - await _setupDB(); - } -} - -export function getConfig() : DBConfig -{ - return dbConfig; -} - -export function getDB() : Knex -{ - if(!db) - { - throw new AppError('Database not initialized!', 'DB_NOT_INITIALIZED'); - } - - return db; -} - -export function table(tableName : string) : Knex.QueryBuilder -{ - if(!db) - { - throw new AppError('Database not initialized!', 'DB_NOT_INITIALIZED'); - } - - return db(tableName); -} - -export function raw(sql : string, bindings : Knex.RawBinding) : Promise -{ - if(!db) - { - throw new AppError('Database not initialized!', 'DB_NOT_INITIALIZED'); - } - - return db.raw(sql, bindings); -} - -// --------------------------------------------------------------------------------------------------------------------- diff --git a/src/server/managers/notebook.ts b/src/server/managers/notebook.ts index afe24d12..d3134a78 100644 --- a/src/server/managers/notebook.ts +++ b/src/server/managers/notebook.ts @@ -2,14 +2,12 @@ // Notes Manager // --------------------------------------------------------------------------------------------------------------------- -// Managers -import { table } from './database'; - // Models import { Notebook, NotebookPage } from '../models/notebook'; // Utils -import { shortID } from '../../common/utils/misc'; +import { getDB } from '../utils/database'; +import { shortID } from '../utils/misc'; import { MultipleResultsError, NotFoundError } from '../errors'; // --------------------------------------------------------------------------------------------------------------------- @@ -24,7 +22,8 @@ export interface NoteFilters { export async function get(notebookID : string) : Promise { - const pages = (await table('note_page as np') + const db = await getDB(); + const pages = (await db('note_page as np') .select( 'page_id as id', 'n.hash_id as notebookID', @@ -40,7 +39,8 @@ export async function get(notebookID : string) : Promise export async function list(filters : NoteFilters) : Promise { - const query = table('note as n') + const db = await getDB(); + const query = db('note as n') .select('n.hash_id as notebookID') .distinct('n.hash_id') .leftJoin('note_page as np', 'np.note_id', '=', 'n.note_id') @@ -71,7 +71,8 @@ export async function list(filters : NoteFilters) : Promise export async function getRaw(notebookID : string) : Promise> { - const notebooks = await table('note') + const db = await getDB(); + const notebooks = await db('note') .select() .where({ hash_id: notebookID }); @@ -91,7 +92,8 @@ export async function getRaw(notebookID : string) : Promise { - const pages = await table('note_page as np') + const db = await getDB(); + const pages = await db('note_page as np') .select( 'page_id as id', 'n.hash_id as notebookID', @@ -117,9 +119,10 @@ export async function getPage(pageID : string | number) : Promise export async function addPage(notebookID : string, page : Record) : Promise { + const db = await getDB(); const notePage = NotebookPage.fromJSON({ ...page, notebookID }); - const [ notebook ] = await table('note') + const [ notebook ] = await db('note') .select('note_id as id') .where({ hash_id: notebookID }) .catch(() => @@ -127,7 +130,7 @@ export async function addPage(notebookID : string, page : Record[] = []) : Promise { + const db = await getDB(); const newNoteID = shortID(); - await table('note') + await db('note') .insert({ hash_id: newNoteID }); // Add any pages that were specified @@ -171,7 +175,8 @@ export async function updatePage(pageID : string | number, pageUpdate : Record { - await table('note_page') + const db = await getDB(); + await db('note_page') .where({ page_id: pageID }) .delete(); @@ -190,7 +196,8 @@ export async function removePage(pageID : string) : Promise<{ status : 'ok' }> export async function remove(notebookID : string) : Promise<{ status : 'ok' }> { - await table('note') + const db = await getDB(); + await db('note') .where({ hash_id: notebookID }) .delete(); diff --git a/src/server/managers/references.ts b/src/server/managers/references.ts index 740968c9..64bb73f0 100644 --- a/src/server/managers/references.ts +++ b/src/server/managers/references.ts @@ -4,12 +4,10 @@ import _ from 'lodash'; -// Managers -import * as dbMan from './database'; - // Utilities +import { getDB } from '../utils/database'; import { applyFilters } from '../knex/utils'; -import { FilterToken } from '../routes/utils/query'; +import { FilterToken } from '../routes/utils'; // Models import { Reference } from '../models/reference'; @@ -20,7 +18,7 @@ class ReferenceManager { async getFiltered(filters : Record, tableName : string) : Promise { - const db = await dbMan.getDB(); + const db = await getDB(); let query = db(tableName) .select(`${ tableName }.name`, `${ tableName }.abbr`, `${ tableName }.product_code as productCode`); diff --git a/src/server/managers/roles.ts b/src/server/managers/roles.ts index b6b0e637..48fd64f7 100644 --- a/src/server/managers/roles.ts +++ b/src/server/managers/roles.ts @@ -2,17 +2,18 @@ // Roles Manager // --------------------------------------------------------------------------------------------------------------------- -// Managers -import { table } from './database'; - // Models import { Role } from '../models/role'; +// Utils +import { getDB } from '../utils/database'; + // --------------------------------------------------------------------------------------------------------------------- export async function list() : Promise { - return (await table('role as r').select('r.role_id as id', 'r.name', 'r.permissions')) + const db = await getDB(); + return (await db('role as r').select('r.role_id as id', 'r.name', 'r.permissions')) .map(Role.fromDB); } diff --git a/src/server/managers/supplement.ts b/src/server/managers/supplement.ts index e4ccd846..244706aa 100644 --- a/src/server/managers/supplement.ts +++ b/src/server/managers/supplement.ts @@ -7,7 +7,6 @@ import { Knex } from 'knex'; import logging from '@strata-js/util-logging'; // Managers -import { table } from './database'; import * as accountMan from './account'; import * as permMan from './permissions'; @@ -16,9 +15,10 @@ import { Account } from '../models/account'; import { Supplement } from '../models/supplement'; // Utilities +import { getDB } from '../utils/database'; import { applyFilters } from '../knex/utils'; -import { FilterToken } from '../routes/utils/query'; -import { camelCaseKeys } from '../../common/utils/misc'; +import { FilterToken } from '../routes/utils'; +import { camelCaseKeys } from '../utils/misc'; // Errors import { MultipleResultsError, DuplicateSupplementError, NotFoundError, NotAuthorizedError } from '../errors'; @@ -125,7 +125,8 @@ async function $ensureCorrectOwner( export async function get(id : number, type : string, systemPrefix : string, account ?: Account) : Promise { const tableName = `${ systemPrefix }_${ type }`; - const query = table(`${ tableName } as t`) + const db = await getDB(); + const query = db(`${ tableName } as t`) .select('t.*', 'a.hash_id as ownerHash') .leftJoin('account as a', 'a.account_id', '=', 't.owner') .where({ id }); @@ -154,7 +155,8 @@ export async function list( ) : Promise { const tableName = `${ systemPrefix }_${ type }`; - let query = table(`${ tableName } as t`) + const db = await getDB(); + let query = db(`${ tableName } as t`) .select('t.*', 'a.hash_id as ownerHash') .leftJoin('account as a', 'a.account_id', '=', 't.owner'); @@ -188,6 +190,7 @@ export async function add( account ?: Account ) : Promise { + const db = await getDB(); const tableName = `${ systemPrefix }_${ type }`; const supplement = Supplement.fromJSON(systemPrefix, type, newSupplement); @@ -202,7 +205,7 @@ export async function add( // First, we check to see if we already have one that matches the unique constraint. We do this manually, because // it's very hard to catch specific sqlite errors reliably, so we do the check explicitly. - const suppExists = (await table(tableName) + const suppExists = (await db(tableName) .select() .where({ scope: supplement.scope, owner: supplement.owner ?? null, name: supplement.name })).length > 0; @@ -228,7 +231,7 @@ export async function add( // ===================================================================================== // Now, we insert the supplement - const [ id ] = await table(tableName).insert({ ...supplement.toDB(), owner }); + const [ id ] = await db(tableName).insert({ ...supplement.toDB(), owner }); // Return the inserted supplement return get(id, type, systemPrefix, account); @@ -241,6 +244,7 @@ export async function update( systemPrefix : string, account ?: Account ) : Promise { + const db = await getDB(); const supplement = await get(id, type, systemPrefix, account); const tableName = `${ systemPrefix }_${ type }`; @@ -278,7 +282,7 @@ export async function update( // ===================================================================================== // Now, we update the supplement - await table(tableName) + await db(tableName) .update({ ...newSupplement.toDB(), owner }) .where({ id }); @@ -293,6 +297,7 @@ export async function remove( account ?: Account ) : Promise<{ status : 'ok' }> { + const db = await getDB(); const supplement = await get(id, type, systemPrefix, account).catch(() => undefined); const tableName = `${ systemPrefix }_${ type }`; @@ -302,7 +307,7 @@ export async function remove( await $checkModAccess(supplement, systemPrefix, type, account); // Delete the supplement - await table(tableName) + await db(tableName) .delete() .where({ id }); } diff --git a/src/server/models/supplement.ts b/src/server/models/supplement.ts index bc4b8fa1..dd2a8534 100644 --- a/src/server/models/supplement.ts +++ b/src/server/models/supplement.ts @@ -11,7 +11,7 @@ import { getSupplementDecoder } from '../decoders/supplement'; import { SupplementOptions } from '../../common/interfaces/models/supplement'; // Utils -import { snakeCaseKeys } from '../../common/utils/misc'; +import { snakeCaseKeys } from '../utils/misc'; //---------------------------------------------------------------------------------------------------------------------- diff --git a/src/server/routes/auth.ts b/src/server/routes/auth.ts new file mode 100644 index 00000000..5493cdfb --- /dev/null +++ b/src/server/routes/auth.ts @@ -0,0 +1,29 @@ +//---------------------------------------------------------------------------------------------------------------------- +// Auth Router +//---------------------------------------------------------------------------------------------------------------------- + +import express from 'express'; + +//---------------------------------------------------------------------------------------------------------------------- + +const router = express.Router(); + +// Get Current User +router.get('/user', (req, resp) => +{ + resp.json(req.user); +}); + +// Logout endpoint +router.post('/logout', (req, res, done) => +{ + req.logout(done); + res.end(); +}); + + +//---------------------------------------------------------------------------------------------------------------------- + +export default router; + +//---------------------------------------------------------------------------------------------------------------------- diff --git a/src/server/routes/version.ts b/src/server/routes/version.ts new file mode 100644 index 00000000..4552744a --- /dev/null +++ b/src/server/routes/version.ts @@ -0,0 +1,28 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Version Route +// --------------------------------------------------------------------------------------------------------------------- + +import { Router } from 'express'; + +// Utils +import { getVersion } from '../utils/version'; + +// --------------------------------------------------------------------------------------------------------------------- + +const router = Router(); + +// --------------------------------------------------------------------------------------------------------------------- + +router.get('/', async (_req, resp) => +{ + const version = await getVersion(); + resp.json({ + version + }); +}); + +// --------------------------------------------------------------------------------------------------------------------- + +export default router; + +// --------------------------------------------------------------------------------------------------------------------- diff --git a/src/server/server.ts b/src/server/server.ts index e03a262e..5ceea12b 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -2,34 +2,22 @@ // Main server module for RPGKeeper. //---------------------------------------------------------------------------------------------------------------------- -// This has to be first, for reasons import 'dotenv/config'; -import configUtil from '@strata-js/util-config'; - -configUtil.load(`./config.yml`); -// --------------------------------------------------------------------------------------------------------------------- +import { resolve } from 'node:path'; +import http from 'node:http'; +import { AddressInfo } from 'node:net'; -import path from 'path'; -import { AddressInfo } from 'net'; -import express, { Express } from 'express'; -import bodyParser from 'body-parser'; +import express, { Request, Response } from 'express'; import cookieParser from 'cookie-parser'; import session from 'express-session'; import passport from 'passport'; import helmet from 'helmet'; - +import configUtil from '@strata-js/util-config'; import logging from '@strata-js/util-logging'; - -import http from 'http'; import { Server as SIOServer } from 'socket.io'; -// Interfaces -import { RPGKeeperConfig } from '../common/interfaces/config'; - // Managers -import * as dbMan from './managers/database'; -import * as accountMan from './managers/account'; import * as permsMan from './managers/permissions'; // Session Store @@ -39,25 +27,37 @@ const KnexSessionStore = connectSessionKnex(session); // Auth import GoogleAuth from './auth/google'; +// Interfaces +import { ServerConfig } from '../common/interfaces/config'; + // Routes -import { requestLogger, serveIndex, errorLogger } from './routes/utils'; +import authRouter from './routes/auth'; import noteRouter from './routes/notebook'; import charRouter from './routes/characters'; import sysRouter from './routes/systems'; import accountsRouter from './routes/accounts'; import rolesRouter from './routes/roles'; - -// Version information +import versionRouter from './routes/version'; // Utils +import { errorLogger, requestLogger, serveIndex } from './routes/utils'; import { setSIOInstance } from './utils/sio'; import program from './utils/args'; import { getVersion } from './utils/version'; +import { getDB } from './utils/database'; + +// --------------------------------------------------------------------------------------------------------------------- +// Server Configuration +// --------------------------------------------------------------------------------------------------------------------- + +const env = (process.env.ENVIRONMENT ?? 'local').toLowerCase(); +configUtil.load(`./config/${ env }.yml`); + +const config = configUtil.get(); // --------------------------------------------------------------------------------------------------------------------- const logger = logging.getLogger('server'); -const config = configUtil.get(); //---------------------------------------------------------------------------------------------------------------------- // Error Handler @@ -72,23 +72,25 @@ process.on('uncaughtException', (err) => // Main Function //---------------------------------------------------------------------------------------------------------------------- -/** - * Main function - */ -async function main() : Promise<{ app : Express, sio : any, server : any }> +async function main() : Promise { + let devMode = false; + if(program.args.includes('--dev')) + { + devMode = true; + } + //------------------------------------------------------------------------------------------------------------------ // Initialize managers //------------------------------------------------------------------------------------------------------------------ - await dbMan.init(); await permsMan.init(); //------------------------------------------------------------------------------------------------------------------ const store = new KnexSessionStore({ - sidfieldname: config.key as string | undefined, - knex: dbMan.getDB() as any, // This is because this library's typing is foobar'd. + sidfieldname: config.auth.session.key, + knex: await getDB() as any, createtable: true, // Clear expired sessions. (1 hour) @@ -97,27 +99,26 @@ async function main() : Promise<{ app : Express, sio : any, server : any }> //------------------------------------------------------------------------------------------------------------------ + // Get version + const version = await getVersion(); + // Build the express app const app = express(); - // Basic security fixes - app.use(helmet({ - contentSecurityPolicy: false, - crossOriginEmbedderPolicy: false // This might be useful to enable, but skip it for now - })); + // Middleware + app.use(helmet({ contentSecurityPolicy: false, crossOriginEmbedderPolicy: false })); + app.use(express.json()); + app.use(cookieParser()); // Basic request logging app.use(requestLogger(logger)); - // Auth support - app.use(cookieParser()); - app.use(bodyParser.json()); - - const httpSecureCookie = config.http.secure.toLowerCase() === 'true'; + // Session support + const httpSecureCookie = config.http.secure; app.use(session({ - secret: config.secret, - key: config.key, + secret: config.auth.session.secret, + name: config.auth.session.key, resave: false, store, @@ -131,40 +132,20 @@ async function main() : Promise<{ app : Express, sio : any, server : any }> app.use(passport.session()); // Set up our authentication support - GoogleAuth.initialize(app); - - // Auth override - if(config.overrideAuth) - { - // Middleware to skip authentication, for testing with postman, or unit tests. - app.use(async(req, _resp, next) => - { - let account = app.get('user'); - - // Check for an email header. Even if `app.user` is set, this overrides (this keeps the code simpler). - const email = req.get('auth-email'); - if(email) - { - account = await accountMan.getByEmail(email); - } - - if(account) - { - logger.warn(`Forcing auth to account: ${ account.email }`); - req.user = account; - } - next?.(); - }); - } + GoogleAuth.initialize(config, app, devMode); //------------------------------------------------------------------------------------------------------------------ // Routing //------------------------------------------------------------------------------------------------------------------ // Setup static serving - app.use(express.static(path.resolve(__dirname, '..', 'client'))); + app.use(express.static(resolve(__dirname, '..', 'client'))); - // Set up our application routes + // Core Application Routes + app.use('/auth', authRouter); + app.use('/version', versionRouter); + + // Api Routes app.use('/api/characters', charRouter); app.use('/api/systems', sysRouter); app.use('/api/accounts', accountsRouter); @@ -176,7 +157,7 @@ async function main() : Promise<{ app : Express, sio : any, server : any }> { response.format({ html: serveIndex, - json: (_req, resp) => + json: (_req : Request, resp : Response) => { resp.status(404).end(); } @@ -191,22 +172,28 @@ async function main() : Promise<{ app : Express, sio : any, server : any }> //------------------------------------------------------------------------------------------------------------------ const server = http.createServer(app); - const version = await getVersion(); + // Socket.IO const sio = new SIOServer(server); - - // Send the sio server to the sio utility setSIOInstance(sio); + let httpPort = config.http.port; + if(devMode) + { + httpPort -= 1; + logger.debug(`Starting real http server on port ${ httpPort }...`); + } + // Start the server - server.listen(config.http.port, () => + server.listen(httpPort, config.http.host, () => { const { address, port } = server.address() as AddressInfo; - const host = address === '::' ? 'localhost' : address; + const host = [ '::', '0.0.0.0' ].includes(address) ? 'localhost' : address; let actualPort = port; - if(program.args.includes('--dev')) + if(devMode) { + logger.debug('Launching vite...'); actualPort += 1; // Start Vite Dev Server @@ -218,20 +205,17 @@ async function main() : Promise<{ app : Express, sio : any, server : any }> })(); } - logger.info(`RPGKeeper v${ version } listening at http://${ host }:${ actualPort }.`); + const url = `http://${ host }:${ actualPort }`; + logger.info(`RPGKeeper v${ version } listening at ${ url }.`); }); - - // Return these, to make it easier for unit tests. - return { app, sio, server }; } //---------------------------------------------------------------------------------------------------------------------- -// Execute server -const loading = main(); - -//---------------------------------------------------------------------------------------------------------------------- - -module.exports = { loading }; +main() + .catch((error) => + { + logger.error('Unexpected error, exiting. Error was:', error.stack); + }); //---------------------------------------------------------------------------------------------------------------------- diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json index caa3bee0..9fdcd624 100644 --- a/src/server/tsconfig.json +++ b/src/server/tsconfig.json @@ -1,22 +1,22 @@ { "compilerOptions": { "composite": true, + "target": "ES2020", "rootDir": "..", "outDir": "../../dist", - "target": "ES2022", "skipLibCheck": true, - - // Overrides "module": "CommonJS", "moduleResolution": "node", + + // Overrides "allowSyntheticDefaultImports": true, "esModuleInterop": true, + "resolveJsonModule": true }, "files": [ "./server.ts" ], "include": [ - "../common/interfaces", "../common/**/*.ts", "**/*.ts", ] diff --git a/src/server/utils/database.ts b/src/server/utils/database.ts new file mode 100644 index 00000000..ffa21666 --- /dev/null +++ b/src/server/utils/database.ts @@ -0,0 +1,144 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Database Utility +// --------------------------------------------------------------------------------------------------------------------- + +import knex, { Knex } from 'knex'; +import configUtil from '@strata-js/util-config'; +import logging from '@strata-js/util-logging'; +import { DatabaseConfig, ServerConfig } from '../../common/interfaces/config'; + +// --------------------------------------------------------------------------------------------------------------------- + +const logger = logging.getLogger('dbUtil'); + +type AfterCreateCallback = (conn : any, done : any) => void; + +// --------------------------------------------------------------------------------------------------------------------- + +let dbInst : Knex | undefined; + +// --------------------------------------------------------------------------------------------------------------------- + +function _buildCustomAfterCreate(config : DatabaseConfig, afterCreate : AfterCreateCallback) : DatabaseConfig +{ + // Modify the config to enable query tracing or foreign key constraints + const _afterCreate = config?.pool?.afterCreate ?? ((_conn, done) => done()); + + // Create a new 'afterCreate' function that sets up sqlite. + const newAfterCreate = (dbConn, done) : void => + { + afterCreate(dbConn, (err) => + { + if(err) + { + done(err); + } + else + { + _afterCreate?.(dbConn, done); + } + }); + }; + + return { + ...config, + pool: { + ...config.pool, + afterCreate: newAfterCreate + } + }; +} + +function _buildSqliteDB(config : DatabaseConfig) : Knex +{ + const newConf = _buildCustomAfterCreate(config, (dbConn, done) => + { + if(config.traceQueries) + { + // Turn on tracing + dbConn.on('trace', (queryString) => + { + logger.trace('QUERY:', queryString); + }); + } + + // Turn on foreign key constraints and WAL mode + dbConn.exec('PRAGMA foreign_keys = ON', (err) => + { + if(err) + { + return done(err); + } + + dbConn.exec('PRAGMA journal_mode = WAL', (err1) => + { + done(err1, dbConn); + }); + }); + + done(); + }); + + return knex(newConf); +} + +function _buildPostgresDB(config : DatabaseConfig) : Knex +{ + const newConf = _buildCustomAfterCreate(config, (dbConn, done) => + { + if(config.traceQueries) + { + // Turn on tracing + dbConn.on('query', (queryString) => + { + logger.trace('QUERY:', queryString); + }); + } + + done(); + }); + + return knex(newConf); +} + +function _buildDB(config : Knex.Config) : Knex +{ + return knex(config); +} + +// --------------------------------------------------------------------------------------------------------------------- + +export function buildDB(config : DatabaseConfig) : Knex +{ + switch (config.client) + { + case 'sqlite3': + case 'better-sqlite3': + return _buildSqliteDB(config); + case 'pg': + return _buildPostgresDB(config); + default: + return _buildDB(config); + } +} + +export function getDBConfig() : DatabaseConfig +{ + const config = configUtil.get(); + return config.database; +} + +export async function getDB() : Promise +{ + if(dbInst) + { + return dbInst; + } + else + { + const dbConfig = getDBConfig(); + return buildDB(dbConfig); + } +} + +// --------------------------------------------------------------------------------------------------------------------- diff --git a/src/common/utils/misc.ts b/src/server/utils/misc.ts similarity index 59% rename from src/common/utils/misc.ts rename to src/server/utils/misc.ts index 19e1463c..48f911e1 100644 --- a/src/common/utils/misc.ts +++ b/src/server/utils/misc.ts @@ -23,50 +23,6 @@ export function shortID() : string return nanoID(); } -/** - * Generates a color based on a string. - * - * @param str - String to colorize. - * - * @returns Returns a color in hex code format. - */ -export function colorize(str : string) : string -{ - if(!str) - { - return '#aaaaaa'; - } - - let hash = 0; - for(let i = 0; i < str.length; i++) - { - hash = str.charCodeAt(i) + ((hash << 5) - hash); - } - - let color = '#'; - for(let i = 0; i < 3; i++) - { - const value = (hash >> (i * 8)) & 0xFF; - color += (`00${ value.toString(16) }`).substr(-2); - } - - return color; -} - -/** - * Generate a random color in hex form. - */ -export function randomColor() : string -{ - function ChanelRand() : number - { - return Math.floor(Math.random() * (256 + 1)); - } - - const rgb = [ ChanelRand(), ChanelRand(), ChanelRand() ]; - return `#${ ((1 << 24) + (rgb[0] << 16) + (rgb[1] << 8) + rgb[2]).toString(16).slice(1) }`; -} - /** * Camel case all the keys in an object. * @@ -146,36 +102,4 @@ export function sortBy(key : string) : (a : Record, b : Record(arr : T[], iteratee : string | ((item : T) => any)) : T[] -{ - const iter = (item : T) : any => - { - if(typeof iteratee === 'string') - { - return item[iteratee]; - } - else - { - iteratee(item); - } - }; - - return arr.filter((x, i, self) => i === self.findIndex((y) => iter(x) === iter(y))); -} - //---------------------------------------------------------------------------------------------------------------------- diff --git a/tsconfig.json b/tsconfig.json index 16aca2ff..5b07f744 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,16 @@ { "compilerOptions": { - "useDefineForClassFields": true, - "lib": [ "ES2022", "DOM", "DOM.Iterable" ], + "target": "ES2020", + "module": "CommonJS", + "lib": [ "ES2020" ], "skipLibCheck": true, - "target": "ES2022", - "module": "ES2022", + "useDefineForClassFields": true, /* Bundler mode */ - "moduleResolution": "bundler", + "moduleResolution": "node", "allowImportingTsExtensions": true, - // We can't use this, since we use const enums. - //"isolatedModules": true, + "resolveJsonModule": true, + "isolatedModules": true, "noEmit": true, "jsx": "preserve", @@ -18,15 +18,14 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "noImplicitAny": false + "noFallthroughCasesInSwitch": true }, "include": [ - "src", - "vite.config.ts" + "vite.config.ts", + "knexfile.ts" ], "references": [ - { "path": "./src/server/tsconfig.json" }, { "path": "./src/client/tsconfig.json" }, + { "path": "./src/server/tsconfig.json" } ] } diff --git a/vite.config.ts b/vite.config.ts index 3bfbe535..46aa1614 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,7 @@ // Vite Config //---------------------------------------------------------------------------------------------------------------------- +import 'dotenv/config'; import { defineConfig } from 'vite'; // Vite Plugins @@ -9,6 +10,21 @@ import vue from '@vitejs/plugin-vue'; import Components from 'unplugin-vue-components/vite'; import { BootstrapVueNextResolver } from 'unplugin-vue-components/resolvers'; +// Interfaces +import { ServerConfig } from './src/common/interfaces/config'; + +// Utils +import configUtil from '@strata-js/util-config'; + +// --------------------------------------------------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------------------------------------------------- + +const env = (process.env.ENVIRONMENT ?? 'local').toLowerCase(); +configUtil.load(`./config/${ env }.yml`); + +const config = configUtil.get(); + //---------------------------------------------------------------------------------------------------------------------- /** @type {import('vite').UserConfig} */ @@ -67,12 +83,14 @@ export default defineConfig({ } }, server: { - port: 5679, + host: config.http.host, + port: config.http.port, proxy: { - '/auth': 'http://localhost:5678', - '/api': 'http://localhost:5678', + '/auth': `http://127.0.0.1:${ config.http.port - 1 }`, + '/api': `http://127.0.0.1:${ config.http.port - 1 }`, + '/version': `http://127.0.0.1:${ config.http.port - 1 }`, '/socket.io': { - target: 'http://localhost:5678', + target: `http://127.0.0.1:${ config.http.port - 1 }`, ws: true } },