Skip to content

Commit

Permalink
✨ Metrics API (#149)
Browse files Browse the repository at this point in the history
* ✨ feat: added metrics service

* ✨ feat: added metrics API controller

* ✨ feat: added metrics API middleware

* ✨ feat: added metrics API routes

* 🏷️ chore: added metrics API related types

* 🎨 chore: added events table to MatrixDB collections

* 🎨 chore: export metrics API

* 🎨 feat: integrate matrics API routes to main app

* 🚨 chore: lint

* 🧪 chore: added controller tests

* 🧪 chore: added middleware test

* 🧪 chore: added route test

* 🧪 chore: added metrics service test

* 🐛 fix: swagger openapi docs

* 📝 chore: generate swagger docs
  • Loading branch information
rezk2ll authored Dec 10, 2024
1 parent 3ca01a9 commit 7dce966
Show file tree
Hide file tree
Showing 13 changed files with 1,196 additions and 1 deletion.
2 changes: 1 addition & 1 deletion docs/openapi.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/matrix-identity-server/src/matrixDb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type Collections =
| 'room_aliases'
| 'room_stats_state'
| 'event_json'
| 'events'

type Get = (
table: Collections,
Expand Down
8 changes: 8 additions & 0 deletions packages/tom-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import VaultServer from './vault-api'
import WellKnown from './wellKnown'
import ActiveContacts from './active-contacts-api'
import QRCode from './qrcode-api'
import MetricsRouter from './metrics-api'

export default class TwakeServer {
conf: Config
Expand Down Expand Up @@ -147,6 +148,12 @@ export default class TwakeServer {
this.logger
)
const qrCodeApi = QRCode(this.idServer, this.conf, this.logger)
const metricsApi = MetricsRouter(
this.conf,
this.matrixDb.db,
this.idServer.authenticate,
this.logger
)

this.endpoints.use(privateNoteApi)
this.endpoints.use(mutualRoolsApi)
Expand All @@ -156,6 +163,7 @@ export default class TwakeServer {
this.endpoints.use(smsApi)
this.endpoints.use(activeContactsApi)
this.endpoints.use(qrCodeApi)
this.endpoints.use(metricsApi)

if (
this.conf.opensearch_is_activated != null &&
Expand Down
58 changes: 58 additions & 0 deletions packages/tom-server/src/metrics-api/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { type TwakeLogger } from '@twake/logger'
import { type MatrixDBBackend } from '@twake/matrix-identity-server'
import { type NextFunction, type Request, type Response } from 'express'
import { type IMetricsService } from '../types'
import MetricsService from '../services'

export default class MetricsApiController {
private readonly metricsService: IMetricsService

constructor(
private readonly db: MatrixDBBackend,
private readonly logger: TwakeLogger
) {
this.metricsService = new MetricsService(this.db, this.logger)
}

/**
* Fetches the users activity stats
*
* @param {Request} _req - the request object.
* @param {Response} res - the response object.
* @param {NextFunction} next - the next hundler
*/
getActivityStats = async (
_req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const stats = await this.metricsService.getUserActivityStats()

res.status(200).json(stats)
} catch (err) {
next(err)
}
}

/**
* Fetches the users message stats
*
* @param {Request} _req - the request object.
* @param {Response} res - the response object.
* @param {NextFunction} next - the next hundler
*/
getMessageStats = async (
_req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const stats = await this.metricsService.getUserMessageStats()

res.status(200).json(stats)
} catch (err) {
next(err)
}
}
}
1 change: 1 addition & 0 deletions packages/tom-server/src/metrics-api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './routes'
74 changes: 74 additions & 0 deletions packages/tom-server/src/metrics-api/middlewares/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { type MatrixDBBackend } from '@twake/matrix-identity-server'
import { type AuthRequest } from '../../types'
import { type NextFunction, type Response } from 'express'
import { type TwakeLogger } from '@twake/logger'

export default class MetricsApiMiddleware {
constructor(
private readonly matrixDb: MatrixDBBackend,
private readonly logger: TwakeLogger
) {}

/**
* Checks if the user is an admin
*
* @param {AuthRequest} req - request object
* @param {Response} res - response object
* @param {NextFunction} next - next function
*/
checkPermissions = async (
req: AuthRequest,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const { userId } = req

if (userId === undefined) {
throw new Error('Unauthenticated', {
cause: 'userId is missing'
})
}

const isAdmin = await this._checkAdmin(userId)

if (!isAdmin) {
this.logger.warn('User is not an admin', { userId })
res.status(403).json({ message: 'Forbidden' })

return
}

next()
} catch (err) {
res.status(400).json({ message: 'Bad Request' })
}
}

/**
* checks if the user is an admin
*
* @param {string} userId - the user id to check
* @returns {Promise<boolean>} - true if the user is an admin, false otherwise
*/
private readonly _checkAdmin = async (userId: string): Promise<boolean> => {
try {
const user = await this.matrixDb.get('users', ['name'], {
name: userId,
admin: 1
})

if (user.length === 0) {
this.logger.warn('User is not an admin', { userId })

return false
}

return true
} catch (error) {
this.logger.error('Failed to check if user is admin', { error })

return false
}
}
}
115 changes: 115 additions & 0 deletions packages/tom-server/src/metrics-api/routes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
import {
getLogger,
type Config as LoggerConfig,
type TwakeLogger
} from '@twake/logger'
import { type AuthenticationFunction, type Config } from '../../types'
import { type MatrixDBBackend } from '@twake/matrix-identity-server'
import { Router } from 'express'
import authMiddleware from '../../utils/middlewares/auth.middleware'
import MetricsApiController from '../controllers'
import MetricsApiMiddleware from '../middlewares'

export const PATH = '/_twake/v1/metrics'

export default (
config: Config,
matrixDb: MatrixDBBackend,
authenticator: AuthenticationFunction,
defaultLogger?: TwakeLogger
): Router => {
const logger = defaultLogger ?? getLogger(config as unknown as LoggerConfig)
const router = Router()
const authenticate = authMiddleware(authenticator, logger)
const controller = new MetricsApiController(matrixDb, logger)
const middleware = new MetricsApiMiddleware(matrixDb, logger)

/**
* @openapi
* components:
* schemas:
* ActivityMetric:
* type: object
* properties:
* dailyActiveUsers:
* type: number
* weeklyActiveUsers:
* type: number
* monthlyActiveUsers:
* type: number
* weeklyNewUsers:
* type: number
* monthlyNewUsers:
* type: number
* MessageMetric:
* type: array
* items:
* type: object
* properties:
* user_id:
* type: string
* message_count:
* type: number
*/

/**
* @openapi
* /_twake/v1/metrics/activity:
* get:
* tags:
* - Metrics
* description: Get user activity metrics
* responses:
* 200:
* description: Activity metrics found
* content:
* application/json:
* schema:
* type: object
* $ref: '#/components/schemas/ActivityMetric'
* 500:
* description: Internal error
* 400:
* description: Bad request
* 403:
* description: Forbidden
*/
router.get(
`${PATH}/activity`,
authenticate,
middleware.checkPermissions,
controller.getActivityStats
)

/**
* @openapi
* /_twake/v1/metrics/messages:
* get:
* tags:
* - Metrics
* description: Get user messages metrics
* responses:
* 200:
* description: Messages metrics found
* content:
* application/json:
* schema:
* type: object
* $ref: '#/components/schemas/MessageMetric'
* 500:
* description: Internal error
* 400:
* description: Bad request
* 403:
* description: Forbidden
*/
router.get(
`${PATH}/messages`,
authenticate,
middleware.checkPermissions,
controller.getMessageStats
)

return router
}
Loading

0 comments on commit 7dce966

Please sign in to comment.