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