activity logging middleware

This commit is contained in:
yohlo
2025-09-30 10:47:02 -05:00
parent 8b1bbe213d
commit 63ea515a31
11 changed files with 311 additions and 16 deletions

View File

@@ -0,0 +1,108 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": null,
"deleteRule": null,
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text1579384326",
"max": 0,
"min": 0,
"name": "name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "json4225120046",
"maxSize": 0,
"name": "arguments",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"cascadeDelete": false,
"collectionId": "pbc_3072146508",
"hidden": false,
"id": "relation2551806565",
"maxSelect": 1,
"minSelect": 0,
"name": "player",
"presentable": false,
"required": false,
"system": false,
"type": "relation"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text3293145029",
"max": 0,
"min": 0,
"name": "user_agent",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "autodate2990389176",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"id": "pbc_1262591861",
"indexes": [],
"listRule": null,
"name": "activities",
"system": false,
"type": "base",
"updateRule": null,
"viewRule": null
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1262591861");
return app.delete(collection);
})

View File

@@ -0,0 +1,27 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_1262591861")
// add field
collection.fields.addAt(5, new Field({
"hidden": false,
"id": "number2254405824",
"max": null,
"min": null,
"name": "duration",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
}))
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1262591861")
// remove field
collection.fields.removeById("number2254405824")
return app.save(collection)
})

View File

@@ -0,0 +1,43 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_1262591861")
// add field
collection.fields.addAt(6, new Field({
"hidden": false,
"id": "bool1862328242",
"name": "success",
"presentable": false,
"required": false,
"system": false,
"type": "bool"
}))
// add field
collection.fields.addAt(7, new Field({
"autogeneratePattern": "",
"hidden": false,
"id": "text1574812785",
"max": 0,
"min": 0,
"name": "error",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}))
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1262591861")
// remove field
collection.fields.removeById("bool1862328242")
// remove field
collection.fields.removeById("text1574812785")
return app.save(collection)
})

View File

@@ -9,6 +9,7 @@ import { MatchInput } from "@/features/matches/types";
import { serverEvents } from "@/lib/events/emitter";
import { superTokensFunctionMiddleware } from "@/utils/supertokens";
import { PlayerInfo } from "../players/types";
import { serverFnLoggingMiddleware } from "@/utils/activities";
const orderedTeamsSchema = z.object({
tournamentId: z.string(),
@@ -17,7 +18,7 @@ const orderedTeamsSchema = z.object({
export const generateTournamentBracket = createServerFn()
.inputValidator(orderedTeamsSchema)
.middleware([superTokensAdminFunctionMiddleware])
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ data: { tournamentId, orderedTeamIds } }) =>
toServerResult(async () => {
logger.info("Generating tournament bracket", {
@@ -138,7 +139,7 @@ export const generateTournamentBracket = createServerFn()
export const startMatch = createServerFn()
.inputValidator(z.string())
.middleware([superTokensAdminFunctionMiddleware])
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ data }) =>
toServerResult(async () => {
logger.info("Starting match", data);
@@ -171,7 +172,7 @@ const endMatchSchema = z.object({
});
export const endMatch = createServerFn()
.inputValidator(endMatchSchema)
.middleware([superTokensAdminFunctionMiddleware])
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ data: { matchId, home_cups, away_cups, ot_count } }) =>
toServerResult(async () => {
logger.info("Ending match", matchId);

View File

@@ -7,6 +7,7 @@ import { z } from "zod";
import { logger } from ".";
import { getRequest } from "@tanstack/react-start/server";
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
import { serverFnLoggingMiddleware } from "@/utils/activities";
export const fetchMe = createServerFn()
.handler(async () =>
@@ -46,7 +47,7 @@ export const getPlayer = createServerFn()
export const updatePlayer = createServerFn()
.inputValidator(playerUpdateSchema)
.middleware([superTokensFunctionMiddleware])
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ context, data }) =>
toServerResult(async () => {
const userAuthId = context.userAuthId;
@@ -98,7 +99,7 @@ export const createPlayer = createServerFn()
export const associatePlayer = createServerFn()
.inputValidator(z.string())
.middleware([superTokensFunctionMiddleware])
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ context, data }) =>
toServerResult(async () => {
const userAuthId = context.userAuthId;

View File

@@ -6,6 +6,7 @@ import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
import { teamInputSchema, teamUpdateSchema } from "./types";
import { logger } from "@/lib/logger";
import { Match } from "../matches/types";
import { serverFnLoggingMiddleware } from "@/utils/activities";
export const listTeamInfos = createServerFn()
@@ -30,7 +31,7 @@ export const getTeamInfo = createServerFn()
export const createTeam = createServerFn()
.inputValidator(teamInputSchema)
.middleware([superTokensFunctionMiddleware])
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ data, context }) =>
toServerResult(async () => {
const userId = context.userAuthId;
@@ -50,7 +51,7 @@ export const updateTeam = createServerFn()
id: z.string(),
updates: teamUpdateSchema
}))
.middleware([superTokensFunctionMiddleware])
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ data: { id, updates }, context }) =>
toServerResult(async () => {
const userId = context.userAuthId;

View File

@@ -5,6 +5,7 @@ import { tournamentInputSchema } from "@/features/tournaments/types";
import { logger } from ".";
import { z } from "zod";
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
import { serverFnLoggingMiddleware } from "@/utils/activities";
export const listTournaments = createServerFn()
.middleware([superTokensFunctionMiddleware])
@@ -14,7 +15,7 @@ export const listTournaments = createServerFn()
export const createTournament = createServerFn()
.inputValidator(tournamentInputSchema)
.middleware([superTokensAdminFunctionMiddleware])
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ data }) =>
toServerResult(() => pbAdmin.createTournament(data))
);
@@ -24,7 +25,7 @@ export const updateTournament = createServerFn()
id: z.string(),
updates: tournamentInputSchema.partial()
}))
.middleware([superTokensAdminFunctionMiddleware])
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ data }) =>
toServerResult(() => pbAdmin.updateTournament(data.id, data.updates))
);
@@ -48,7 +49,7 @@ export const enrollTeam = createServerFn()
tournamentId: z.string(),
teamId: z.string()
}))
.middleware([superTokensFunctionMiddleware])
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ data: { tournamentId, teamId }, context }) =>
toServerResult(async () => {
const userId = context.userAuthId;
@@ -74,7 +75,7 @@ export const unenrollTeam = createServerFn()
tournamentId: z.string(),
teamId: z.string()
}))
.middleware([superTokensFunctionMiddleware])
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ data: { tournamentId, teamId }, context }) =>
toServerResult(() => pbAdmin.unenrollTeam(tournamentId, teamId))
);
@@ -95,7 +96,7 @@ export const getFreeAgents = createServerFn()
export const enrollFreeAgent = createServerFn()
.inputValidator(z.object({ phone: z.string(), tournamentId: z.string() }))
.middleware([superTokensFunctionMiddleware])
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ context, data }) =>
toServerResult(async () => {
const userAuthId = context.userAuthId;
@@ -109,7 +110,7 @@ export const enrollFreeAgent = createServerFn()
export const unenrollFreeAgent = createServerFn()
.inputValidator(z.object({ tournamentId: z.string() }))
.middleware([superTokensFunctionMiddleware])
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ context, data }) =>
toServerResult(async () => {
const userAuthId = context.userAuthId;

View File

@@ -4,6 +4,7 @@ import { createTournamentsService } from "./services/tournaments";
import { createTeamsService } from "./services/teams";
import { createMatchesService } from "./services/matches";
import { createReactionsService } from "./services/reactions";
import { createActivitiesService } from "./services/activities";
import dotenv from 'dotenv';
dotenv.config();
@@ -35,6 +36,7 @@ class PocketBaseAdminClient {
Object.assign(this, createTournamentsService(this.pb));
Object.assign(this, createMatchesService(this.pb));
Object.assign(this, createReactionsService(this.pb));
Object.assign(this, createActivitiesService(this.pb));
});
}
@@ -54,7 +56,8 @@ interface AdminClient
ReturnType<typeof createTeamsService>,
ReturnType<typeof createTournamentsService>,
ReturnType<typeof createMatchesService>,
ReturnType<typeof createReactionsService> {
ReturnType<typeof createReactionsService>,
ReturnType<typeof createActivitiesService> {
authPromise: Promise<void>;
}

View File

@@ -0,0 +1,56 @@
import PocketBase from "pocketbase";
export interface Activity {
id: string;
name: string;
player?: string;
duration: number;
success: boolean;
error?: string;
arguments?: any;
user_agent?: string;
created: string;
updated: string;
}
export interface ActivityInput {
name: string;
player?: string;
duration: number;
success: boolean;
error?: string;
arguments?: any;
user_agent?: string;
}
export function createActivitiesService(pb: PocketBase) {
return {
async createActivity(data: ActivityInput): Promise<Activity> {
const result = await pb.collection("activities").create<Activity>(data);
return result;
},
async getRecentActivities(limit: number = 100): Promise<Activity[]> {
const result = await pb.collection("activities").getList<Activity>(1, limit, {
sort: "-created",
});
return result.items;
},
async getActivitiesByUser(userId: string, limit: number = 50): Promise<Activity[]> {
const result = await pb.collection("activities").getList<Activity>(1, limit, {
filter: `user_id = "${userId}"`,
sort: "-created",
});
return result.items;
},
async getActivitiesByFunction(functionName: string, limit: number = 50): Promise<Activity[]> {
const result = await pb.collection("activities").getList<Activity>(1, limit, {
filter: `function_name = "${functionName}"`,
sort: "-created",
});
return result.items;
},
};
}

View File

@@ -17,8 +17,8 @@ export const backendConfig = (): TypeInput => {
},
appInfo,
recipeList: [
PasswordlessTwilioVerify.init(),
//PasswordlessDevelopmentMode.init(),
//PasswordlessTwilioVerify.init(),
PasswordlessDevelopmentMode.init(),
Session.init({
cookieSameSite: "lax",
cookieSecure: import.meta.env.NODE_ENV === "production",

54
src/utils/activities.ts Normal file
View File

@@ -0,0 +1,54 @@
import { pbAdmin } from "@/lib/pocketbase/client";
import { createMiddleware } from "@tanstack/react-start";
import { getRequest } from "@tanstack/react-start/server";
export const serverFnLoggingMiddleware = createMiddleware({
type: "function",
}).server(async ({ next, data, functionId, context }) => {
const request = getRequest();
const serverFnName = functionId.split('--')[1]?.split('_')[0] || 'unknown';
const userId = (context as any)?.metadata?.player_id || 'unknown';
const startTime = Date.now();
try {
const result = await next();
const duration = Date.now() - startTime;
try {
await pbAdmin.authPromise;
await pbAdmin.createActivity({
name: serverFnName,
player: userId !== 'unknown' ? userId : undefined,
duration,
success: true,
arguments: data,
user_agent: request.headers.get('user-agent') || undefined,
});
} catch (activityError) {
}
return result;
} catch (error) {
const duration = Date.now() - startTime;
const errorMessage = error instanceof Error ? error.message : String(error);
try {
await pbAdmin.authPromise;
await pbAdmin.createActivity({
name: serverFnName,
player: userId !== 'unknown' ? userId : undefined,
duration,
success: false,
error: errorMessage,
arguments: data,
user_agent: request.headers.get('user-agent') || undefined,
});
} catch (activityError) {
}
throw error;
}
});