integrate telemetry module #12

This commit is contained in:
2026-01-24 21:25:04 +02:00
parent e5a49f5e8e
commit 80afb5c460
9 changed files with 712 additions and 7 deletions
+22 -2
View File
@@ -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 }
+6
View File
@@ -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'
}
}
+250
View File
@@ -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 }
+178
View File
@@ -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 }
+2
View File
@@ -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 => {