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'; import Utils from '../Utils.js'; 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: `^${Utils.escapeRegExp(email)}$`, $options: 'i' } }); if (user) { if (md5(md5(password) + config.am.salt) == user.password) { return done(null, am.getUserProfile(user)); } else { await Utils.wait(3000); return done(null, false, { message: 'invalidPassword' }); } } else { await Utils.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.addGameData = async function(ctx, data){ if (ctx.user?._id){ let user = await db.get(collection, { '_id': db.ObjectId(ctx.user._id) }); let gameData = (user.gameData || []).filter(gd=>gd.id != data.id); gameData.push(data); ctx.user.gameData = user.gameData = gameData; await db.update(collection, { '_id': db.ObjectId(user._id) }, user); //ctx.user.gameData = gameData; }else{ let gameData = (ctx.session.gameData || []).filter(gd=>gd.id != data.id); gameData.push(data); ctx.session.gameData = gameData } } 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: `^${Utils.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 }