diff --git a/auth/routes/routes.ts b/auth/routes/routes.ts index 169b2d5d84..4aa84ed024 100644 --- a/auth/routes/routes.ts +++ b/auth/routes/routes.ts @@ -7,6 +7,8 @@ import { checkPassword } from '../ldap.controller'; import { AuthUsers } from '../schemas/authUsers'; import { Organizacion } from './../../core/tm/schemas/organizacion'; import { Auth } from './../auth.class'; +import { logUsuarioIngreso } from '../usuarioIngresos'; +import { SystemLog } from '../../core/log/system.log'; const sha1Hash = require('sha1'); @@ -96,6 +98,11 @@ router.post('/v2/organizaciones', Auth.authenticate(), async (req, res, next) => const dto = await generateTokenPayload(username, orgId, account_id); updateOrganizacion(usuario, orgId); if (dto) { + try { + await logUsuarioIngreso(req, dto.payload.usuario, dto.payload.organizacion); + } catch (logErr) { + await SystemLog.error('logUsuarioIngreso', { username, orgId }, logErr, req); + } return res.send({ token: dto.token }); @@ -118,6 +125,11 @@ router.post('/organizaciones', Auth.authenticate(), async (req, res, next) => { const oldToken: string = String(req.headers.authorization).substring(4); const nuevosPermisos = user.organizaciones.find(item => String(item._id) === String(org._id)); const refreshToken = Auth.refreshToken(oldToken, user, [...user.permisosGlobales, ...nuevosPermisos.permisos], org); + try { + await logUsuarioIngreso(req, { id: user._id, usuario: user.usuario }, org); + } catch (logErr) { + await SystemLog.error('logUsuarioIngreso:legacy', { username, orgId }, logErr, req); + } return res.send({ token: refreshToken }); diff --git a/auth/schemas/usuarioIngresos.schema.ts b/auth/schemas/usuarioIngresos.schema.ts new file mode 100644 index 0000000000..be4887f88d --- /dev/null +++ b/auth/schemas/usuarioIngresos.schema.ts @@ -0,0 +1,51 @@ +import { AuditPlugin } from '@andes/mongoose-plugin-audit'; +import { Document, Model, model, Schema, SchemaTypes, Types } from 'mongoose'; + +export const UsuarioIngresoSchema = new Schema({ + usuario: { + id: { type: SchemaTypes.ObjectId, required: true }, + usuario: { type: String, required: true } + }, + start: { type: Date, required: true }, + cantidad: { type: Number, default: 0 }, + bucketNumber: { type: Number, default: 0 }, + ingresos: [{ + fecha: { type: Date, required: true }, + organizacion: { + id: { type: SchemaTypes.ObjectId, required: true }, + nombre: { type: String, required: true } + }, + device: { + ip: String, + tipo: String, + os: String + } + }] +}); + +export interface IUsuarioIngresos extends Document { + usuario: { + id: Types.ObjectId; + usuario: string; + }; + start: Date; + cantidad: number; + bucketNumber: number; + ingresos: [{ + fecha: Date; + organizacion: { + id: Types.ObjectId; + nombre: string; + }; + device: { + ip: string; + tipo: string; + os: string; + }; + }]; +} + +UsuarioIngresoSchema.index({ 'usuario.id': 1, start: -1, bucketNumber: 1 }, { unique: true }); +UsuarioIngresoSchema.plugin(AuditPlugin); + +export const UsuarioIngreso: Model = model('usuarioIngreso', UsuarioIngresoSchema, 'usuarioIngresos'); diff --git a/auth/usuarioIngresos.ts b/auth/usuarioIngresos.ts new file mode 100644 index 0000000000..5e163bfffd --- /dev/null +++ b/auth/usuarioIngresos.ts @@ -0,0 +1,107 @@ +import { UsuarioIngreso } from './schemas/usuarioIngresos.schema'; +import * as moment from 'moment'; + +function parseUserAgent(uaString: string): { tipo: string; os: string } { + if (!uaString) { + return { tipo: 'Unknown', os: 'Unknown' }; + } + const ua = uaString.toLowerCase(); + + let tipo = 'Desktop'; + if (/mobile|android.*mobile|iphone|ipod|blackberry|windows phone/i.test(uaString)) { + tipo = 'Mobile'; + } else if (/tablet|ipad|android(?!.*mobile)/i.test(uaString)) { + tipo = 'Tablet'; + } + + let os = 'Unknown'; + if (/windows nt 10/i.test(ua)) { + os = 'Windows 10'; + } else if (/windows nt 6\.3/i.test(ua)) { + os = 'Windows 8.1'; + } else if (/windows nt 6\.2/i.test(ua)) { + os = 'Windows 8'; + } else if (/windows nt 6\.1/i.test(ua)) { + os = 'Windows 7'; + } else if (/windows/i.test(ua)) { + os = 'Windows'; + } else if (/android/i.test(ua)) { + const match = ua.match(/android\s([\d.]+)/); + os = match ? `Android ${match[1]}` : 'Android'; + } else if (/iphone os|ipad/i.test(ua)) { + const match = ua.match(/os ([\d_]+)/); + os = match ? `iOS ${match[1].replace(/_/g, '.')}` : 'iOS'; + } else if (/mac os x/i.test(ua)) { + os = 'macOS'; + } else if (/linux/i.test(ua)) { + os = 'Linux'; + } + + return { tipo, os }; +} + +export async function logUsuarioIngreso(req, user, organizacion) { + let bucketNumber = 0; + let retry = true; + while (retry) { + try { + await execLogIngreso(req, user, organizacion, bucketNumber); + retry = false; + } catch (err) { + if (err.code === 17419 || err.code === 11000) { + bucketNumber++; + } else { + retry = false; + throw err; + } + } + } +} + +async function execLogIngreso(req, user, organizacion, bucketNumber) { + const now = new Date(); + const start = moment(now).startOf('quarter').toDate(); + + const uaString = req.headers['user-agent'] || ''; + const { tipo: deviceType, os } = parseUserAgent(uaString); + const forwarded = req.headers['x-forwarded-for']; + const rawIp = (typeof forwarded === 'string' ? forwarded.split(',')[0].trim() : null) + || req.ip + || req.connection?.remoteAddress + || ''; + const ip = rawIp.startsWith('::ffff:') ? rawIp.substring(7) : rawIp; + + return UsuarioIngreso.update( + { + 'usuario.id': user.id || user._id, + start, + bucketNumber + }, + { + $inc: { cantidad: 1 }, + $setOnInsert: { + usuario: { + id: user.id || user._id, + usuario: user.usuario || user.username + }, + start, + bucketNumber + }, + $push: { + ingresos: { + fecha: now, + organizacion: { + id: organizacion.id || organizacion._id, + nombre: organizacion.nombre + }, + device: { + ip, + tipo: deviceType, + os + } + } + } + }, + { upsert: true } + ); +}