audit logs admin api
This commit is contained in:
parent
be604b24c6
commit
176fe16b2f
@ -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,
|
||||||
|
57
src/modules/api/admin/audit-admin.controller.ts
Normal file
57
src/modules/api/admin/audit-admin.controller.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
19
src/modules/objects/audit/audit.interfaces.ts
Normal file
19
src/modules/objects/audit/audit.interfaces.ts
Normal 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;
|
||||||
|
}
|
@ -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 };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user