222 lines
5.2 KiB
TypeScript
222 lines
5.2 KiB
TypeScript
import { Inject, Injectable } from '@nestjs/common';
|
|
import { Request } from 'express';
|
|
import {
|
|
FindManyOptions,
|
|
FindOptionsWhere,
|
|
ILike,
|
|
In,
|
|
Repository,
|
|
} from 'typeorm';
|
|
import { User } from '../user/user.entity';
|
|
import { AuditLog } from './audit.entity';
|
|
import { AuditAction } from './audit.enum';
|
|
import { Lookup, lookup } from 'geoip-lite';
|
|
import { Details, parse } from 'express-useragent';
|
|
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
|
|
import { AuditSearchClause, UserLoginEntry } from './audit.interfaces';
|
|
|
|
const PLUCK_LOCATION = ['country', 'city', 'timezone', 'll'];
|
|
const PLUCK_USER_AGENT = ['browser', 'version', 'os', 'platform'];
|
|
const AUTOFLAG = [
|
|
AuditAction.MALICIOUS_REQUEST,
|
|
AuditAction.THROTTLE,
|
|
AuditAction.DEACTIVATION_REQUEST,
|
|
AuditAction.DATA_DOWNLOAD_REQUEST,
|
|
];
|
|
|
|
@Injectable()
|
|
export class AuditService {
|
|
constructor(
|
|
@Inject('AUDIT_REPOSITORY')
|
|
private readonly audit: Repository<AuditLog>,
|
|
private readonly form: FormUtilityService,
|
|
) {}
|
|
|
|
public async insertAudit(
|
|
action: AuditAction,
|
|
comment?: string,
|
|
user?: User,
|
|
ip?: string,
|
|
ua?: string,
|
|
) {
|
|
const audit = new AuditLog();
|
|
audit.action = action as string;
|
|
audit.content = comment;
|
|
audit.actor_ip = ip;
|
|
audit.actor_ua = ua;
|
|
audit.actor = user;
|
|
|
|
if (AUTOFLAG.includes(action)) {
|
|
audit.flagged = true;
|
|
// TODO: email administrator
|
|
}
|
|
|
|
await this.updateAudit(audit);
|
|
return audit;
|
|
}
|
|
|
|
public async auditRequest(
|
|
req: Request,
|
|
type: AuditAction,
|
|
comment?: string,
|
|
user?: User,
|
|
) {
|
|
return this.insertAudit(
|
|
type,
|
|
comment,
|
|
user || req.user || null,
|
|
req.ip,
|
|
req.header('user-agent'),
|
|
);
|
|
}
|
|
|
|
public getIPLocation(ip: string) {
|
|
return lookup(ip);
|
|
}
|
|
|
|
public getUserAgentInfo(ua: string) {
|
|
return parse(ua);
|
|
}
|
|
|
|
public async searchForAuditCount(search: AuditSearchClause) {
|
|
return this.audit.count(this.buildAuditSearch(search));
|
|
}
|
|
|
|
public async searchForAudit(
|
|
limit = 50,
|
|
offset = 0,
|
|
search: AuditSearchClause,
|
|
): Promise<
|
|
[
|
|
(AuditLog & {
|
|
location?: Partial<Lookup>;
|
|
user_agent?: Partial<Details>;
|
|
})[],
|
|
number,
|
|
]
|
|
> {
|
|
const [list, num] = await this.audit.findAndCount({
|
|
...this.buildAuditSearch(search),
|
|
take: limit,
|
|
skip: offset,
|
|
order: { created_at: 'DESC' },
|
|
relations: ['actor'],
|
|
});
|
|
|
|
return [
|
|
list.map((entry) => ({
|
|
...entry,
|
|
location: entry.actor_ip
|
|
? this.form.pluckObject(
|
|
this.getIPLocation(entry.actor_ip),
|
|
PLUCK_LOCATION,
|
|
)
|
|
: null,
|
|
user_agent: entry.actor_ua
|
|
? this.form.pluckObject(
|
|
this.getUserAgentInfo(entry.actor_ua),
|
|
PLUCK_USER_AGENT,
|
|
)
|
|
: null,
|
|
actor: this.form.stripObject(entry.actor, ['password']),
|
|
})),
|
|
num,
|
|
];
|
|
}
|
|
|
|
public async getUserLogins(
|
|
user: User,
|
|
sessid?: string,
|
|
): Promise<UserLoginEntry[]> {
|
|
const userLogins: UserLoginEntry[] = [];
|
|
const auditEntries = await this.audit.find({
|
|
where: { actor: { id: user.id }, action: AuditAction.LOGIN },
|
|
order: { created_at: 'DESC' },
|
|
take: 10,
|
|
});
|
|
|
|
auditEntries.forEach((entry) => {
|
|
userLogins.push({
|
|
login_at: entry.created_at,
|
|
current: sessid === entry.content,
|
|
location: entry.actor_ip
|
|
? this.form.pluckObject(
|
|
this.getIPLocation(entry.actor_ip),
|
|
PLUCK_LOCATION,
|
|
)
|
|
: null,
|
|
user_agent: entry.actor_ua
|
|
? this.form.pluckObject(
|
|
this.getUserAgentInfo(entry.actor_ua),
|
|
PLUCK_USER_AGENT,
|
|
)
|
|
: null,
|
|
});
|
|
});
|
|
|
|
return userLogins;
|
|
}
|
|
|
|
public async getUserAccountCreation(user: User) {
|
|
const auditEntry = await this.audit.findOne({
|
|
where: { actor: { id: user.id }, action: AuditAction.REGISTRATION },
|
|
});
|
|
|
|
if (!auditEntry) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
created_at: auditEntry.created_at,
|
|
ip: auditEntry.actor_ip,
|
|
location: auditEntry.actor_ip
|
|
? this.form.pluckObject(
|
|
this.getIPLocation(auditEntry.actor_ip),
|
|
PLUCK_LOCATION,
|
|
)
|
|
: null,
|
|
user_agent: auditEntry.actor_ua
|
|
? this.form.pluckObject(
|
|
this.getUserAgentInfo(auditEntry.actor_ua),
|
|
PLUCK_USER_AGENT,
|
|
)
|
|
: null,
|
|
};
|
|
}
|
|
|
|
public async updateAudit(audit: AuditLog): Promise<void> {
|
|
await this.audit.save(audit);
|
|
}
|
|
|
|
private buildAuditSearch(
|
|
search: AuditSearchClause,
|
|
): FindManyOptions<AuditLog> {
|
|
const obj: FindOptionsWhere<AuditLog> = {};
|
|
if (search.actions) {
|
|
obj.action = In((search.actions as string).split(','));
|
|
}
|
|
|
|
if (search.content) {
|
|
obj.content = ILike(`%${search.content}%`);
|
|
}
|
|
|
|
if (search.ip) {
|
|
obj.actor_ip = ILike(`%${search.ip}%`);
|
|
}
|
|
|
|
if (search.ua) {
|
|
obj.actor_ua = ILike(`%${search.ua}%`);
|
|
}
|
|
|
|
if (search.user) {
|
|
obj.actor = { uuid: search.user };
|
|
}
|
|
|
|
if (search.flagged) {
|
|
obj.flagged = search.flagged;
|
|
}
|
|
|
|
return { where: obj };
|
|
}
|
|
}
|