audit logs admin api

This commit is contained in:
Evert Prants 2022-09-09 21:50:14 +03:00
parent be604b24c6
commit 176fe16b2f
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
4 changed files with 183 additions and 33 deletions

View File

@ -11,12 +11,14 @@ import { PrivilegeAdminController } from './privilege-admin.controller';
import { UserAdminController } from './user-admin.controller'; import { UserAdminController } from './user-admin.controller';
import { ConfigurationModule } from 'src/modules/config/config.module'; import { ConfigurationModule } from 'src/modules/config/config.module';
import { AdminService } from './admin.service'; import { AdminService } from './admin.service';
import { AuditAdminController } from './audit-admin.controller';
@Module({ @Module({
controllers: [ controllers: [
UserAdminController, UserAdminController,
PrivilegeAdminController, PrivilegeAdminController,
OAuth2AdminController, OAuth2AdminController,
AuditAdminController,
], ],
imports: [ imports: [
ObjectsModule, ObjectsModule,

View File

@ -0,0 +1,57 @@
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { Privileges } from 'src/decorators/privileges.decorator';
import { Scopes } from 'src/decorators/scopes.decorator';
import { OAuth2Guard } from 'src/guards/oauth2.guard';
import { PrivilegesGuard } from 'src/guards/privileges.guard';
import { ScopesGuard } from 'src/guards/scopes.guard';
import { AuditAction } from 'src/modules/objects/audit/audit.enum';
import { AuditSearchClause } from 'src/modules/objects/audit/audit.interfaces';
import { AuditService } from 'src/modules/objects/audit/audit.service';
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
import { PaginationService } from 'src/modules/utility/services/paginate.service';
import { PageOptions } from 'src/types/pagination.interfaces';
@ApiBearerAuth()
@ApiTags('admin')
@Controller('/api/admin/audit')
@UseGuards(OAuth2Guard, PrivilegesGuard, ScopesGuard)
export class AuditAdminController {
constructor(
private _paginate: PaginationService,
private _form: FormUtilityService,
private _audit: AuditService,
) {}
/**
* Get a list of all audit logs or search for a specific log
* @param search Search and pagination options
* @returns Paginated audit list
*/
@Get('')
@Scopes('management')
@Privileges('admin', 'admin:audit')
async userList(@Query() search: AuditSearchClause & PageOptions) {
const resultCount = await this._audit.searchForAuditCount(search);
const pagination = this._paginate.paginate(search, resultCount);
const [list] = await this._audit.searchForAudit(
pagination.pageSize,
pagination.offset,
search,
);
return {
pagination,
list: this._form.stripObjectArray(list, ['password']),
};
}
@Get('filter')
@Scopes('management')
@Privileges('admin', 'admin:audit')
async filterList() {
return Object.values(AuditAction);
}
}

View File

@ -0,0 +1,19 @@
import { Lookup } from 'geoip-lite';
import { Details } from 'express-useragent';
import { AuditAction } from './audit.enum';
export interface UserLoginEntry {
login_at: Date;
current: boolean;
location: Partial<Lookup>;
user_agent: Partial<Details>;
}
export interface AuditSearchClause {
actions?: AuditAction[] | string;
user?: string;
ip?: string;
ua?: string;
content?: string;
flagged?: boolean;
}

View File

@ -1,19 +1,22 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Request } from 'express'; import { Request } from 'express';
import { Repository } from 'typeorm'; import {
FindManyOptions,
FindOptionsWhere,
ILike,
In,
Repository,
} from 'typeorm';
import { User } from '../user/user.entity'; import { User } from '../user/user.entity';
import { AuditLog } from './audit.entity'; import { AuditLog } from './audit.entity';
import { AuditAction } from './audit.enum'; import { AuditAction } from './audit.enum';
import { lookup, Lookup } from 'geoip-lite'; import { Lookup, lookup } from 'geoip-lite';
import { parse, Details } from 'express-useragent'; import { Details, parse } from 'express-useragent';
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service'; import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
import { AuditSearchClause, UserLoginEntry } from './audit.interfaces';
export interface UserLoginEntry { const PLUCK_LOCATION = ['country', 'city', 'timezone', 'll'];
login_at: Date; const PLUCK_USER_AGENT = ['browser', 'version', 'os', 'platform'];
current: boolean;
location: Partial<Lookup>;
user_agent: Partial<Details>;
}
@Injectable() @Injectable()
export class AuditService { export class AuditService {
@ -72,6 +75,52 @@ export class AuditService {
return parse(ua); 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( public async getUserLogins(
user: User, user: User,
sessid?: string, sessid?: string,
@ -88,20 +137,16 @@ export class AuditService {
login_at: entry.created_at, login_at: entry.created_at,
current: sessid === entry.content, current: sessid === entry.content,
location: entry.actor_ip location: entry.actor_ip
? this.form.pluckObject(this.getIPLocation(entry.actor_ip), [ ? this.form.pluckObject(
'country', this.getIPLocation(entry.actor_ip),
'city', PLUCK_LOCATION,
'timezone', )
'll',
])
: null, : null,
user_agent: entry.actor_ua user_agent: entry.actor_ua
? this.form.pluckObject(this.getUserAgentInfo(entry.actor_ua), [ ? this.form.pluckObject(
'browser', this.getUserAgentInfo(entry.actor_ua),
'version', PLUCK_USER_AGENT,
'os', )
'platform',
])
: null, : null,
}); });
}); });
@ -122,20 +167,16 @@ export class AuditService {
created_at: auditEntry.created_at, created_at: auditEntry.created_at,
ip: auditEntry.actor_ip, ip: auditEntry.actor_ip,
location: auditEntry.actor_ip location: auditEntry.actor_ip
? this.form.pluckObject(this.getIPLocation(auditEntry.actor_ip), [ ? this.form.pluckObject(
'country', this.getIPLocation(auditEntry.actor_ip),
'city', PLUCK_LOCATION,
'timezone', )
'll',
])
: null, : null,
user_agent: auditEntry.actor_ua user_agent: auditEntry.actor_ua
? this.form.pluckObject(this.getUserAgentInfo(auditEntry.actor_ua), [ ? this.form.pluckObject(
'browser', this.getUserAgentInfo(auditEntry.actor_ua),
'version', PLUCK_USER_AGENT,
'os', )
'platform',
])
: null, : null,
}; };
} }
@ -143,4 +184,35 @@ export class AuditService {
public async updateAudit(audit: AuditLog): Promise<void> { public async updateAudit(audit: AuditLog): Promise<void> {
await this.audit.save(audit); 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 };
}
} }