diff --git a/pb_migrations/1759244692_created_activities.js b/pb_migrations/1759244692_created_activities.js new file mode 100644 index 0000000..bf4480a --- /dev/null +++ b/pb_migrations/1759244692_created_activities.js @@ -0,0 +1,108 @@ +/// +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); +}) diff --git a/pb_migrations/1759245857_updated_activities.js b/pb_migrations/1759245857_updated_activities.js new file mode 100644 index 0000000..ddceac4 --- /dev/null +++ b/pb_migrations/1759245857_updated_activities.js @@ -0,0 +1,27 @@ +/// +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) +}) diff --git a/pb_migrations/1759246171_updated_activities.js b/pb_migrations/1759246171_updated_activities.js new file mode 100644 index 0000000..5051c81 --- /dev/null +++ b/pb_migrations/1759246171_updated_activities.js @@ -0,0 +1,43 @@ +/// +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) +}) diff --git a/src/features/matches/server.ts b/src/features/matches/server.ts index 2e4eb37..d4b5887 100644 --- a/src/features/matches/server.ts +++ b/src/features/matches/server.ts @@ -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); diff --git a/src/features/players/server.ts b/src/features/players/server.ts index 686b786..993fe5b 100644 --- a/src/features/players/server.ts +++ b/src/features/players/server.ts @@ -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; diff --git a/src/features/teams/server.ts b/src/features/teams/server.ts index 0781800..2313ecd 100644 --- a/src/features/teams/server.ts +++ b/src/features/teams/server.ts @@ -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; diff --git a/src/features/tournaments/server.ts b/src/features/tournaments/server.ts index 0e19efd..2b9a9fd 100644 --- a/src/features/tournaments/server.ts +++ b/src/features/tournaments/server.ts @@ -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; diff --git a/src/lib/pocketbase/client.ts b/src/lib/pocketbase/client.ts index 4f6e010..9ebee61 100644 --- a/src/lib/pocketbase/client.ts +++ b/src/lib/pocketbase/client.ts @@ -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, ReturnType, ReturnType, - ReturnType { + ReturnType, + ReturnType { authPromise: Promise; } diff --git a/src/lib/pocketbase/services/activities.ts b/src/lib/pocketbase/services/activities.ts new file mode 100644 index 0000000..e253552 --- /dev/null +++ b/src/lib/pocketbase/services/activities.ts @@ -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 { + const result = await pb.collection("activities").create(data); + return result; + }, + + async getRecentActivities(limit: number = 100): Promise { + const result = await pb.collection("activities").getList(1, limit, { + sort: "-created", + }); + return result.items; + }, + + async getActivitiesByUser(userId: string, limit: number = 50): Promise { + const result = await pb.collection("activities").getList(1, limit, { + filter: `user_id = "${userId}"`, + sort: "-created", + }); + return result.items; + }, + + async getActivitiesByFunction(functionName: string, limit: number = 50): Promise { + const result = await pb.collection("activities").getList(1, limit, { + filter: `function_name = "${functionName}"`, + sort: "-created", + }); + return result.items; + }, + }; +} \ No newline at end of file diff --git a/src/lib/supertokens/server.ts b/src/lib/supertokens/server.ts index 90982c5..b563c7f 100644 --- a/src/lib/supertokens/server.ts +++ b/src/lib/supertokens/server.ts @@ -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", diff --git a/src/utils/activities.ts b/src/utils/activities.ts new file mode 100644 index 0000000..96966cb --- /dev/null +++ b/src/utils/activities.ts @@ -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; + } +}); \ No newline at end of file