From 80afb5c460d83d006d4f80dbd743432364b5f563 Mon Sep 17 00:00:00 2001 From: goynov Date: Sat, 24 Jan 2026 21:25:04 +0200 Subject: [PATCH] integrate telemetry module #12 --- backend/app/AccessManager.js | 24 +- backend/app/Config.js | 6 + backend/app/bl/UserManager.js | 250 +++++++++++++++++++++ backend/controllers/api/UsersController.js | 178 +++++++++++++++ backend/main.js | 2 + package-lock.json | 243 +++++++++++++++++++- package.json | 10 +- src/pages/preview/[[id]].vue | 1 + src/plugins/api.js | 5 + 9 files changed, 712 insertions(+), 7 deletions(-) create mode 100644 backend/app/bl/UserManager.js create mode 100644 backend/controllers/api/UsersController.js diff --git a/backend/app/AccessManager.js b/backend/app/AccessManager.js index 66c0a5d..a748c8a 100644 --- a/backend/app/AccessManager.js +++ b/backend/app/AccessManager.js @@ -2,13 +2,19 @@ class AccessManager { name = 'am' init(app){ - + ['user', 'editor', 'admin'].forEach(f => { + this[f] = this.oneOfThese([f]); + }); } start(app){ } + getIp(req){ + return req.headers['x-forwarded-for'] || req.socket.remoteAddress; + } + async audit(req, action, objectId, custom){ let data = { t: Math.floor(Date.now() / 1000), @@ -19,7 +25,7 @@ class AccessManager { l: req?.lang?.code, u: req.user?._id && this.app.db.ObjectId(req.user._id), a: action, - o: objectId && this.app.db.ObjectId(objectId), + o: objectId, c: custom } await this.app.db.create('log', data); @@ -128,6 +134,20 @@ class AccessManager { return user && user.roles.indexOf(role) > -1; } + oneOfThese(roles){ + return function(req, res, next){ + let result = false; + roles.forEach(r=>{ + if (req.user && (req.user.roles?.indexOf(r)>-1 || req.user.groups?.filter(g=>g.startsWith(r+'.')).length) + || r == 'guest' ){ + result = true; + } + }); + if (result) next(); + else res.status(401).json({status:'error', error:'noPermissions'}).end(); + } + } + } export { AccessManager } \ No newline at end of file diff --git a/backend/app/Config.js b/backend/app/Config.js index c6a81c1..66e8838 100644 --- a/backend/app/Config.js +++ b/backend/app/Config.js @@ -123,6 +123,12 @@ class Config{ * @memberof CookieOptions */ maxAge: 1000 * 60 * 60 * 24 * 7 + }, + sso:{ + FACEBOOK_APP_ID:'your_fb_app_id', + FACEBOOK_APP_SECRET:'your_fb_app_secret', + GOOGLE_CLIENT_ID:'your_google_client_id', + GOOGLE_CLIENT_SECRET:'your_google_client_secret' } } diff --git a/backend/app/bl/UserManager.js b/backend/app/bl/UserManager.js new file mode 100644 index 0000000..2c4bdea --- /dev/null +++ b/backend/app/bl/UserManager.js @@ -0,0 +1,250 @@ +import md5 from 'md5'; +import passport from 'passport'; +import { v4 as uuidv4 } from 'uuid'; + +import { Strategy as LocalStrategy } from 'passport-local'; +import { Strategy as FacebookStrategy } from 'passport-facebook'; +import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; + +const collection = 'users'; +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])?)*$/; + + +class UserManager { + name = 'user'; + /** + * Class initializer, инициализация на плъгин + * @param {App} app Class initializer, основна апликация + */ + init(app) { + const { db, config, am, global, utils, custom } = app; + + this.passport = passport; + + passport.serializeUser(function (user, done) { + done(null, user); + }); + + passport.deserializeUser(function (user, done) { + done(null, user); + }); + + passport.use(new LocalStrategy({ usernameField: 'email', passwordField: 'password' }, + async function (email, password, done) { + let user = await db.get(collection, { email: { $regex: `^${global.JsUtils.escapeRegExp(email)}$`, $options: 'i' } }); + if (user) { + if (md5(md5(password) + config.am.salt) == user.password) { + return done(null, am.getUserProfile(user)); + } else { + await global.JsUtils.wait(3000); + return done(null, false, { message: 'invalidPassword' }); + } + } else { + await global.JsUtils.wait(3000); + return done(null, false, { message: 'invalidUsername' }); + } + } + )); + + passport.use(new FacebookStrategy({ + clientID: config.am.sso.FACEBOOK_APP_ID, + clientSecret: config.am.sso.FACEBOOK_APP_SECRET, + callbackURL: config.site.host + "/api/user/auth/facebook/callback", + profileFields: ['id', 'emails', 'name', 'photos'] + }, am.processSocialLogin)); + + passport.use(new GoogleStrategy({ + clientID: config.am.sso.GOOGLE_CLIENT_ID, + clientSecret: config.am.sso.GOOGLE_CLIENT_SECRET, + callbackURL: config.site.host + "/api/user/auth/google/callback", + userProfileURL: 'https://www.googleapis.com/oauth2/v3/userinfo' + }, am.processSocialLogin)); + + this.update = async function (ctx, data) { + if (!am.is(ctx.user, 'admin') && ctx.user._id != data._id) { + throw new Error('unauthorized'); + } + let newData = data; + let user = await db.get(collection, { '_id': db.ObjectId(newData._id) }); + + if (newData.password) { + if (newData.password != newData.passConfirm) { + throw new Error('passwordMismatch') + } + if (!am.is(ctx.user, 'admin') && md5(md5(newData.passCurrent) + config.am.salt) != user.password) { + throw new Error('invalidPassword') + } + user.password = md5(md5(newData.password) + config.am.salt); + } + + let hist = Object.assign({}, user); + await am.addToHistory(hist, collection, 'update'); + + user._meta = user._meta || {}; + am.setMeta(user._meta, 'update', ctx.user); + let safeData = {}; + + this.assignSafeUserData(ctx, safeData, newData, am.is(ctx.user, 'admin')); + this.assignSafeUserData(ctx, user, newData, am.is(ctx.user, 'admin')); + + await this.updateUserSession(ctx, user._id, safeData); + + am.audit(ctx, 'userUpdate', user._id); + + await db.update(collection, { '_id': db.ObjectId(user._id) }, user); + } + + this.assignSafeUserData = function (ctx, userObject, newData, isAdmin) { + ['displayName', 'firstName', 'lastName', ...(isAdmin ? ['roles', 'groups', 'email', 'status'] : [])].forEach(e => { + userObject[e] = newData[e]; + }); + } + + this.updateUserSession = async function (ctx, _uid, safeData) { + if (ctx.user?._id == _uid.toString()) { + Object.assign(ctx.session.passport.user, safeData); + } + if (am.is(ctx.user, 'admin') || ctx.allUserSessions) { + let userSessions = await db.list('user_sessions', { + query: { + 'session.passport.user._id': _uid + } + }); + for (let us of (userSessions?.data || [])) { + Object.assign(us.session.passport.user, safeData); + await db.update('user_sessions', { _id: us._id }, us); + } + } + } + + this.delete = async function (ctx, uid) { + let user = await db.get(collection, { '_id': db.ObjectId(uid) }); + let hist = Object.assign({}, user); + await am.addToHistory(hist, collection, 'remove'); + am.audit(ctx, 'userDelete', user._id); + await db.remove(collection, { '_id': db.ObjectId(user._id) }); + } + + this.signUp = async function (ctx, data) { + return new Promise(async (resolve, reject) => { + if (!emailRegexp.test(data.email)) { + return reject(new Error('invalidEmail')); + } + let exists = await db.get(collection, { email: { $regex: `^${global.JsUtils.escapeRegExp(data.email)}$`, $options: 'i' } }); + if (exists) { + return reject(new Error('emailExists')) + } + if (!data.password || data.password != data.passConfirm) { + return reject(new Error('passwordMismatch')) + } + if (!ctx.session.captcha || ctx.session.captcha?.toLowerCase() != data.captcha?.toLowerCase()) { + return reject(new Error('invalidCaptcha')) + } + delete ctx.session.captcha; + let dbUser = { + email: data.email, + password: data.password && md5(md5(data.password) + config.am.salt), + roles: ['user'], + status: 0 //0 - neutral + }; + if (config.am.validateEmail) { + dbUser.status = 9; // 9 - needs validation + dbUser.mailValidation = { + key: md5(md5(uuidv4() + data.email)) + } + this.sendValidationEmail(ctx, dbUser).catch(error => { + am.audit(ctx, 'errorSendingMail', null, { + email: data.email, + error + }); + }); + } + let r = await db.create(collection, dbUser); + dbUser._id = r.insertedId; + am.audit(ctx, 'signup', dbUser._id); + + ctx.login(am.getUserProfile(dbUser), (err) => { + if (err) { + console.error(err); + reject(new Error('unknown')); + } else { + resolve(ctx.user); + } + }) + }) + } + + this.forgotten = async function (ctx, data) { + if (!ctx.session.captcha || ctx.session.captcha?.toLowerCase() != data.captcha?.toLowerCase()) { + throw new Error('invalidCaptcha') + } + delete ctx.session.captcha; + let exists = await db.get(collection, { email: data.email }); + if (!exists) { + throw new Error('invalidEmail') + } + exists.pwdReset = { + key: md5(md5(uuidv4() + data.email)), + exp: (Date.now() / 1000) + 60 * 60 * 24 + }; + am.audit(ctx, 'userPwdForgotten', exists._id); + let l = ctx.lang.t; + + await utils.sendMail({ + from: `${config.apis.mailer.defaultMailFrom}`, + to: exists.email, + subject: `${l.siteName} - ${l['reset-password']}`, + html: l.forgottenPassMailContent(`${config.site.host}/${l._code}/user/change-password?key=${exists.pwdReset.key}`) + }) + + await db.update(collection, { '_id': db.ObjectId(exists._id) }, exists); + } + + this.reset = async function (ctx, data) { + let exists = await db.get(collection, { 'pwdReset.key': data.key }); + if (!exists) { + throw new Error('invalidActivationLink') + } + if (exists.pwdReset.exp < Date.now / 1000) { + throw new Error('activationLinkExpired') + } + if (!data.password || data.password != data.passConfirm) { + throw new Error('passwordMismatch') + } + + exists.password = data.password && md5(md5(data.password) + config.am.salt); + delete exists.pwdReset; + await db.update(collection, { '_id': db.ObjectId(exists._id) }, exists); + am.audit(ctx, 'userPwdReset', exists._id); + } + + this.sendValidationEmail = async function (ctx, data) { + let l = ctx.lang.t; + await utils.sendMail({ + from: `${config.apis.mailer.defaultMailFrom}`, + to: data.email, + subject: `${l.siteName} - ${l['validate-email']}`, + html: l.validationMailContent(`${config.site.host}/${l._code}/user/validate-email?key=${data.mailValidation.key}`) + }); + } + + this.validateEmail = async function (ctx, data) { + let exists = await db.get(collection, { 'mailValidation.key': data.key }); + if (!exists) { + throw new Error('invalidValidationLink') + } + exists.status = 10; + delete exists.mailValidation; + custom.emailVerified?.(exists); + let safeData = {}; + this.assignSafeUserData(ctx, safeData, exists, true); + let uid = exists._id; + await this.updateUserSession({ allUserSessions: true }, uid, safeData); + //db.update should be last, because it deletes exists._id, but I need it above! + await db.update(collection, { '_id': db.ObjectId(uid) }, exists); + am.audit(ctx, 'userMailValidated', uid); + } + } +} + +export { UserManager } \ No newline at end of file diff --git a/backend/controllers/api/UsersController.js b/backend/controllers/api/UsersController.js new file mode 100644 index 0000000..c24e75d --- /dev/null +++ b/backend/controllers/api/UsersController.js @@ -0,0 +1,178 @@ +import express from 'express'; +import svgCaptcha from 'svg-captcha'; + +const collection = 'users'; + +/** + * UsersController. API for the user management, граничен клас за комуникация с потребителския модул + */ +class UsersController { + name = 'userApi' + route = '/api/user' + + init(app) { + const { db, am, user, global } = app; + const router = express.Router(); + + router.get('/info', (req, res) => { + res.json({ user: req.user }); + }) + + router.post('/signin', function (req, res, next) { + user.passport.authenticate('local', function (err, user, info) { + if (err) { return next(err); } + if (!user) { + am.audit(req, 'login:error', null, { message: info.message }); + return res.json({ status: 'error', message: info.message }) + } + req.login(user, (err) => { + if (err) { + am.audit(req, 'login:error', null, { err }); + return next(err); + } + res.json({ status: 'OK', user: req.user }); + am.audit(req, 'login'); + }) + })(req, res, next); + }); + + router.post('/signup', async (req, res) => { + try { + await user.signUp(req, req.body); + res.json({ status: "OK", user: req.user }); + } catch (err) { + res.json({ status: 'error', message: err.message, user: null }); + } + }); + + router.get('/signout', async (req, res) => { + am.audit(req, 'logout'); + req.logout(then => { + res.json({ status: 'OK' }); + }); + }) + + router.get('/auth/facebook', user.passport.authenticate('facebook', { scope: ['email'] })); + router.get('/auth/facebook/callback', am.getSocialCallback('facebook')); + + router.get('/auth/google', user.passport.authenticate('google', { scope: ['profile', 'email'] })); + router.get('/auth/google/callback', am.getSocialCallback('google')); + + router.post('/tm', async (req, res) => { + am.audit(req, 'tm:' + req.body.action, req.body.object, req.body.data) + res.json({ status: "OK" }); + }) + + router.post('/update', am.user, async (req, res) => { + try { + await user.update(req, req.body); + res.json({ status: 'OK' }); + } catch (err) { + if (err.message == 'unauthorized') { + res.status(401).json({ status: 'error', message: 'Unauthorized' }) + } else { + console.error(err); + res.status(500).json({ status: 'error' }) + } + } + }) + + router.post('/forgotten', async (req, res) => { + try { + await user.forgotten(req, req.body); + res.json({ status: "OK" }); + } catch (err) { + res.json({ status: 'error', message: err.message }); + } + }) + + router.post('/reset', async (req, res) => { + try { + await user.reset(req, req.body); + res.json({ status: "OK" }); + } catch (err) { + res.json({ status: 'error', message: err.message }); + } + }); + + router.post('/send-validation-email', async (req, res) => { + let dbUser = await db.get(collection, { '_id': db.ObjectId(req.body._id) }); + if (dbUser.email != req.user?.email) { + res.json({ status: 'error', message: 'invalidEmail' }); + } + if (dbUser) { + if (dbUser.status == 9) { + await user.sendValidationEmail(req, dbUser); + res.json({ status: "OK" }); + } else if (dbUser.status == 10) { + res.json({ status: 'error', message: 'emailAlreadyValidated' }); + } + } else { + res.json({ status: 'error', message: 'invalidEmail' }); + } + }) + + router.post('/validate-email', async (req, res) => { + try { + await user.validateEmail(req, req.body); + res.json({ status: "OK" }); + } catch (err) { + res.json({ status: 'error', message: err.message }); + } + }) + + router.get('/get/:id', am.admin, async (req, res) => { + let user = await db.get(collection, { '_id': db.ObjectId(req.params.id) }, { password: 0 }); + res.json(user); + }) + + router.delete('/delete/:id', am.admin, async (req, res) => { + await user.delete(req, req.params.id); + res.json({ status: 'OK' }); + }) + + router.post('/list', am.admin, async (req, res) => { + let q = { + query: {}, + project: { password: 0 }, + limit: req.body.limit || 12, skip: req.body.skip || 0 + }; + if (req.body.email) { + q.query.email = { $regex: global.JsUtils.escapeRegExp(req.body.email), $options: 'i' } + } + let list = await db.list(collection, q); + res.json(list); + }) + + router.get('/captcha', (req, res) => { + let captcha = svgCaptcha.create({ + noise: 2, + color: true + }); + req.session.captcha = captcha.text; + res.type('svg'); + res.status(200).send(captcha.data); + }) + + router.post('/i-am-not-a-robot', async (req, res) => { + if (req.body.captcha?.toLowerCase() == req.session.captcha?.toLowerCase()) { + let cache = db.instance.collection('cache'); + let queryKey = { + scope: 'ip', key: req.clientIP + } + let ipInfo = await cache.findOne(queryKey) + if (ipInfo) { + ipInfo.objects = []; + await cache.replaceOne(queryKey, ipInfo); + } + res.json({ status: 'OK' }); + } else { + res.json({ status: 'error', message: 'invalidCaptcha' }); + } + }) + + app.webServer.xapp.use(this.route, router); + } +} + +export { UsersController } \ No newline at end of file diff --git a/backend/main.js b/backend/main.js index 9230ac6..8b8ccde 100644 --- a/backend/main.js +++ b/backend/main.js @@ -17,6 +17,7 @@ const modules = [ {name: 'GameObjectsManager', path:'app/bl/GameObjectsManager.js'}, {name: 'ScenariosManager', path:'app/bl/ScenariosManager.js'}, {name: 'GamesManager', path:'app/bl/GamesManager.js'}, + {name: 'UserManager', path:'app/bl/UserManager.js'}, {name: 'WebServer', path:'app/WebServer.js'}, @@ -24,6 +25,7 @@ const modules = [ {name: 'GameObjectsController', path:'controllers/api/GameObjectsController.js'}, {name: 'ScenariosController', path:'controllers/api/ScenariosController.js'}, {name: 'GamesController', path:'controllers/api/GamesController.js'}, + {name: 'UsersController', path:'controllers/api/UsersController.js'}, ] process.on('uncaughtException', err => { diff --git a/package-lock.json b/package-lock.json index 47c0eb9..072e16d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,11 +21,19 @@ "express": "^4.21.1", "express-session": "^1.18.1", "helmet": "^8.0.0", + "md5": "^2.3.0", "mongodb": "^6.10.0", + "nodemailer": "^7.0.12", + "passport": "^0.7.0", + "passport-facebook": "^3.0.0", + "passport-google-oauth": "^2.0.0", + "passport-google-oauth20": "^2.0.0", + "passport-local": "^1.0.0", "roboto-fontface": "*", "sharp": "^0.33.5", + "svg-captcha": "^1.4.0", "three-viewport-gizmo": "^2.2.0", - "uuid": "^11.0.2", + "uuid": "^11.1.0", "vue": "^3.5.13", "vuetify": "^3.10.5" }, @@ -2452,6 +2460,15 @@ ], "license": "MIT" }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bidi-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", @@ -2737,6 +2754,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -3103,6 +3129,15 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -5089,6 +5124,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT" + }, "node_modules/is-builtin-module": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", @@ -5548,6 +5589,17 @@ "node": ">=4" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "license": "BSD-3-Clause", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -5827,6 +5879,15 @@ "node": ">= 0.6" } }, + "node_modules/nodemailer": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.12.tgz", + "integrity": "sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -5850,6 +5911,12 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5982,6 +6049,18 @@ "wrappy": "1" } }, + "node_modules/opentype.js": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-0.7.3.tgz", + "integrity": "sha512-Veui5vl2bLonFJ/SjX/WRWJT3SncgiZNnKUyahmXCc2sa1xXW15u3R/3TN5+JFiP7RsjK5ER4HA5eWaEmV9deA==", + "license": "MIT", + "dependencies": { + "tiny-inflate": "^1.0.2" + }, + "bin": { + "ot": "bin/ot" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6054,6 +6133,133 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-facebook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/passport-facebook/-/passport-facebook-3.0.0.tgz", + "integrity": "sha512-K/qNzuFsFISYAyC1Nma4qgY/12V3RSLFdFVsPKXiKZt434wOvthFW1p7zKa1iQihQMRhaWorVE1o3Vi1o+ZgeQ==", + "license": "MIT", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-google-oauth": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth/-/passport-google-oauth-2.0.0.tgz", + "integrity": "sha512-JKxZpBx6wBQXX1/a1s7VmdBgwOugohH+IxCy84aPTZNq/iIPX6u7Mqov1zY7MKRz3niFPol0KJz8zPLBoHKtYA==", + "license": "MIT", + "dependencies": { + "passport-google-oauth1": "1.x.x", + "passport-google-oauth20": "2.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-google-oauth1": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth1/-/passport-google-oauth1-1.0.0.tgz", + "integrity": "sha512-qpCEhuflJgYrdg5zZIpAq/K3gTqa1CtHjbubsEsidIdpBPLkEVq6tB1I8kBNcH89RdSiYbnKpCBXAZXX/dtx1Q==", + "license": "MIT", + "dependencies": { + "passport-oauth1": "1.x.x" + } + }, + "node_modules/passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "license": "MIT", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-oauth1": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/passport-oauth1/-/passport-oauth1-1.3.0.tgz", + "integrity": "sha512-8T/nX4gwKTw0PjxP1xfD0QhrydQNakzeOpZ6M5Uqdgz9/a/Ag62RmJxnZQ4LkbdXGrRehQHIAHNAu11rCP46Sw==", + "license": "MIT", + "dependencies": { + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-oauth1/node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==", + "license": "MIT" + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6114,6 +6320,11 @@ "node": ">= 14.16" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -7143,6 +7354,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-captcha": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/svg-captcha/-/svg-captcha-1.4.0.tgz", + "integrity": "sha512-/fkkhavXPE57zRRCjNqAP3txRCSncpMx3NnNZL7iEoyAtYwUjPhJxW6FQTQPG5UPEmCrbFoXS10C3YdJlW7PDg==", + "license": "MIT", + "dependencies": { + "opentype.js": "^0.7.3" + }, + "engines": { + "node": ">=4.x" + } + }, "node_modules/tar-stream": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", @@ -7189,6 +7412,12 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "license": "MIT" }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -7515,6 +7744,12 @@ "node": ">= 0.8" } }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -7843,9 +8078,9 @@ } }, "node_modules/uuid": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz", - "integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" diff --git a/package.json b/package.json index 2adb048..6ba48ec 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,19 @@ "express": "^4.21.1", "express-session": "^1.18.1", "helmet": "^8.0.0", + "md5": "^2.3.0", "mongodb": "^6.10.0", + "nodemailer": "^7.0.12", + "passport": "^0.7.0", + "passport-facebook": "^3.0.0", + "passport-google-oauth": "^2.0.0", + "passport-google-oauth20": "^2.0.0", + "passport-local": "^1.0.0", "roboto-fontface": "*", "sharp": "^0.33.5", + "svg-captcha": "^1.4.0", "three-viewport-gizmo": "^2.2.0", - "uuid": "^11.0.2", + "uuid": "^11.1.0", "vue": "^3.5.13", "vuetify": "^3.10.5" }, diff --git a/src/pages/preview/[[id]].vue b/src/pages/preview/[[id]].vue index 3456042..d9765df 100644 --- a/src/pages/preview/[[id]].vue +++ b/src/pages/preview/[[id]].vue @@ -23,6 +23,7 @@ export default { async mounted(){ if (this.id && this.id != 'add') { this.object = (await this.$api.game.load(this.id)).data; + //this.$api.user.tm('test', 'test', {data: 'test'}) } this.scenarios = (await this.$api.scenario.search()).data.data; }, diff --git a/src/plugins/api.js b/src/plugins/api.js index bd82b4f..40421d5 100644 --- a/src/plugins/api.js +++ b/src/plugins/api.js @@ -62,6 +62,11 @@ export default { async remove(id){ return await $ax.delete(`/game/${id}`) } + }, + user:{ + async tm(action, object, data){ + return await $ax.post('/user/tm', {action, object, data}); + } } } }