Skip to content

Commit

Permalink
Merge pull request #365 from commons-stack/feat/user_event_log
Browse files Browse the repository at this point in the history
Feat/user event log
  • Loading branch information
kristoferlund authored May 18, 2022
2 parents 58af438 + 722263c commit 56c1575
Show file tree
Hide file tree
Showing 46 changed files with 1,081 additions and 95 deletions.
9 changes: 8 additions & 1 deletion packages/api/src/activate/controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
NotFoundError,
UnauthorizedError,
} from '@error/errors';
import { EventLogTypeKey } from '@eventlog/types';
import { logEvent } from '@eventlog/utils';
import { TypedRequestBody } from '@shared/types';
import { UserModel } from '@user/entities';
import { UserAccountModel } from '@useraccount/entities';
Expand Down Expand Up @@ -32,7 +34,7 @@ const activate = async (

// Find previously generated token
const userAccount = await UserAccountModel.findOne({ accountId })
.select('activateToken')
.select('name activateToken')
.exec();

if (!userAccount) throw new NotFoundError('UserAccount');
Expand Down Expand Up @@ -68,6 +70,11 @@ const activate = async (
userAccount.user = user;
await userAccount.save();

await logEvent(EventLogTypeKey.AUTHENTICATION, 'Activated account', {
userAccountId: userAccount._id,
userId: user._id,
});

res.status(200).json(user);
};

Expand Down
6 changes: 6 additions & 0 deletions packages/api/src/auth/controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
} from '@shared/types';
import { UserModel } from '@user/entities';
import { UserDocument } from '@user/types';
import { EventLogTypeKey } from '@eventlog/types';
import { logEvent } from '@eventlog/utils';
import { ethers } from 'ethers';
import { JwtService } from './JwtService';
import {
Expand Down Expand Up @@ -69,6 +71,10 @@ export const auth = async (
user.refreshToken = refreshToken;
await user.save();

await logEvent(EventLogTypeKey.AUTHENTICATION, 'Logged in', {
userId: user._id,
});

res.status(200).json({
accessToken,
refreshToken,
Expand Down
56 changes: 56 additions & 0 deletions packages/api/src/database/migrations/06_event_log_types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { EventLogTypeModel } from '../../eventlog/entities';
import { EventLogTypeKey } from '../../eventlog/types';

const eventLogTypes = [
{
key: EventLogTypeKey.PERMISSION,
label: 'User Permissions',
description: 'An action that changes user permissions',
},
{
key: EventLogTypeKey.AUTHENTICATION,
label: 'User Authentication',
description: 'An action to authenticate or register a user',
},
{
key: EventLogTypeKey.PERIOD,
label: 'Period',
description: 'An action on a period',
},
{
key: EventLogTypeKey.PRAISE,
label: 'Praise',
description: 'An action to give praise',
},
{
key: EventLogTypeKey.SETTING,
label: 'Setting',
description: 'An action that changes a setting',
},
{
key: EventLogTypeKey.QUANTIFICATION,
label: 'Quantification',
description: 'An action to quantify praise',
},
];

const up = async (): Promise<void> => {
const upsertQueries = eventLogTypes.map((s) => ({
updateOne: {
filter: { key: s.key },

// Insert item if not found, otherwise continue
update: { $setOnInsert: { ...s } },
upsert: true,
},
}));

await EventLogTypeModel.bulkWrite(upsertQueries);
};

const down = async (): Promise<void> => {
const allKeys = eventLogTypes.map((s) => s.key);
await EventLogTypeModel.deleteMany({ key: { $in: allKeys } });
};

export { up, down };
45 changes: 45 additions & 0 deletions packages/api/src/eventlog/controllers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { getQuerySort } from '@shared/functions';
import {
PaginatedResponseBody,
QueryInputParsedQs,
TypedRequestQuery,
TypedResponse,
} from '@shared/types';
import { BadRequestError } from '@error/errors';
import { StatusCodes } from 'http-status-codes';
import { EventLogModel } from './entities';
import { EventLogDto } from './types';
import { eventLogListTransformer } from './transformers';

/**
* Fetch a paginated list of EventLogs
*/
export const all = async (
req: TypedRequestQuery<QueryInputParsedQs>,
res: TypedResponse<PaginatedResponseBody<EventLogDto>>
): Promise<void> => {
if (!req.query.limit || !req.query.page)
throw new BadRequestError('limit and page are required');

const paginateQuery = {
query: {},
limit: parseInt(req.query.limit),
page: parseInt(req.query.page),
sort: getQuerySort(req.query),
};

const response = await EventLogModel.paginate(paginateQuery);

if (!response) throw new BadRequestError('Failed to query event logs');

const docs = response.docs ? response.docs : [];
const docsTransfomed = await eventLogListTransformer(
docs,
res.locals.currentUser.roles
);

res.status(StatusCodes.OK).json({
...response,
docs: docsTransfomed,
});
};
69 changes: 69 additions & 0 deletions packages/api/src/eventlog/entities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import mongoose, { Schema } from 'mongoose';
import { mongoosePagination, Pagination } from 'mongoose-paginate-ts';
import {
EventLogDocument,
EventLogTypeDocument,
EventLogTypeKey,
} from './types';

export const eventLogSchema = new mongoose.Schema(
{
user: {
type: Schema.Types.ObjectId,
ref: 'User',
index: true,
},
useraccount: {
type: Schema.Types.ObjectId,
ref: 'UserAccount',
index: true,
},

// "Related Period" of an eventlog - only used for quantification events
// which are restricted to ADMIN users when period is active
period: {
type: Schema.Types.ObjectId,
ref: 'Period',
index: true,
},

type: {
type: Schema.Types.ObjectId,
ref: 'EventLogType',
required: true,
index: true,
},
description: { type: String, required: true },
},
{
timestamps: true,
}
);

eventLogSchema.plugin(mongoosePagination);

export const EventLogModel = mongoose.model<
EventLogDocument,
Pagination<EventLogDocument>
>('EventLog', eventLogSchema);

export const eventLogTypeSchema = new mongoose.Schema(
{
key: {
type: String,
required: true,
unique: true,
enum: Object.values(EventLogTypeKey),
},
label: { type: String, required: true },
description: { type: String, required: true },
},
{
timestamps: true,
}
);

export const EventLogTypeModel = mongoose.model<
EventLogTypeDocument,
Pagination<EventLogTypeDocument>
>('EventLogType', eventLogTypeSchema);
9 changes: 9 additions & 0 deletions packages/api/src/eventlog/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Router } from '@awaitjs/express';
import * as controller from './controllers';

// Period-routes
const eventLogRouter = Router();

eventLogRouter.getAsync('/all', controller.all);

export { eventLogRouter };
86 changes: 86 additions & 0 deletions packages/api/src/eventlog/transformers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { PeriodModel } from '@period/entities';
import { PeriodStatusType } from '@period/types';
import { UserRole } from '@user/types';
import { UserAccountModel } from '@useraccount/entities';
import { userAccountTransformer } from '@useraccount/transformers';
import { EventLogTypeModel } from './entities';
import {
EventLogDocument,
EventLogDto,
EventLogTypeDocument,
EventLogTypeDto,
} from './types';

const eventLogTypeTransformer = (
eventLogType: EventLogTypeDocument
): EventLogTypeDto => {
const { key, label, description } = eventLogType;

return {
key,
label,
description,
} as EventLogTypeDto;
};

export const eventLogTransformer = async (
eventLog: EventLogDocument,
currentUserRoles: UserRole[] = [UserRole.USER]
): Promise<EventLogDto> => {
const eventLogType = await EventLogTypeModel.findOne({
_id: eventLog.type,
}).orFail();

const _id = eventLog._id.toString();
const createdAt = eventLog.createdAt.toISOString();
const updatedAt = eventLog.updatedAt.toISOString();
const eventLogTypeDto = eventLogTypeTransformer(eventLogType);
const user = eventLog.user ? eventLog.user : undefined;

let useraccount = undefined;
if (eventLog.useraccount) {
const userAccountDocument = await UserAccountModel.findOne({
_id: eventLog.useraccount,
}).orFail();
useraccount = userAccountTransformer(userAccountDocument);
}

// Hide eventLog contents if related to a period
// and period has status QUANTIFY
// and user is not ADMIN
const period = eventLog.period
? await PeriodModel.findOne({ _id: eventLog.period }).orFail()
: undefined;

let hidden = false;
let description = eventLog.description;
if (
period?.status === PeriodStatusType.QUANTIFY &&
!currentUserRoles.includes(UserRole.ADMIN)
) {
description = '';
hidden = true;
}

return {
_id,
user,
useraccount,
type: eventLogTypeDto,
description,
hidden,
createdAt,
updatedAt,
} as EventLogDto;
};

export const eventLogListTransformer = async (
eventLogs: EventLogDocument[],
currentUserRoles: UserRole[] = [UserRole.USER]
): Promise<EventLogDto[]> => {
const eventLogDtos = await Promise.all(
eventLogs.map((eventLog) => eventLogTransformer(eventLog, currentUserRoles))
);

return eventLogDtos;
};
49 changes: 49 additions & 0 deletions packages/api/src/eventlog/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { UserAccountDto } from '../useraccount/types';
import { Document, Types } from 'mongoose';

export enum EventLogTypeKey {
PERMISSION = 'PERMISSION',
AUTHENTICATION = 'AUTHENTICATION',
PERIOD = 'PERIOD',
PRAISE = 'PRAISE',
QUANTIFICATION = 'QUANTIFICATION',
SETTING = 'SETTING',
}

export interface EventLog {
user?: Types.ObjectId;
useraccount?: Types.ObjectId;
period?: Types.ObjectId;
type: Types.ObjectId;
description: string;
createdAt: Date;
updatedAt: Date;
}

export interface EventLogDocument extends EventLog, Document {}

export interface EventLogDto {
user?: string;
useraccount?: UserAccountDto;
type: EventLogTypeDto;
description: string;
hidden: boolean;
createdAt: string;
updatedAt: string;
}

export interface EventLogType {
key: string;
label: string;
description: string;
createdAt: Date;
updatedAt: Date;
}

export interface EventLogTypeDocument extends EventLogType, Document {}

export interface EventLogTypeDto {
key: string;
label: string;
description: string;
}
29 changes: 29 additions & 0 deletions packages/api/src/eventlog/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Types } from 'mongoose';
import { EventLogModel, EventLogTypeModel } from './entities';
import { EventLogTypeKey } from './types';

interface UserInfo {
userId?: Types.ObjectId;
userAccountId?: Types.ObjectId;
}

export const logEvent = async (
typeKey: EventLogTypeKey,
description: string,
userInfo: UserInfo = {},
periodId: Types.ObjectId | undefined = undefined
): Promise<void> => {
const type = await EventLogTypeModel.findOne({
key: typeKey.toString(),
}).orFail();

const data = {
type: type._id,
description,
user: userInfo.userId ? userInfo.userId : undefined,
useraccount: userInfo.userAccountId ? userInfo.userAccountId : undefined,
period: periodId,
};

await EventLogModel.create(data);
};
Loading

0 comments on commit 56c1575

Please sign in to comment.