Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Metrics API #149

Merged
merged 15 commits into from
Dec 10, 2024
Merged
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
}
}
}
124 changes: 124 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,124 @@
/* 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: object
* 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
* 401:
* description: Unauthorized
* 403:
* description: Forbidden
* 404:
* description: Activity metrics not found
*/
router.get(
`${PATH}/activity`,
authenticate,
Dismissed Show dismissed Hide dismissed
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
* 401:
* description: Unauthorized
* 403:
* description: Forbidden
* 404:
* description: Messages metrics not found
*/
router.get(
`${PATH}/messages`,
authenticate,
Dismissed Show dismissed Hide dismissed
middleware.checkPermissions,
controller.getMessageStats
)

return router
}
Loading
Loading