From 8d4b9a167c421efd003d2aaaee528b6ab1c275a0 Mon Sep 17 00:00:00 2001 From: congsh Date: Fri, 17 Apr 2026 14:16:56 +0800 Subject: [PATCH] feat: add API rules and hooks Add PocketBase hooks for group management and real-time notifications: - Groups collection with owner/members permissions - Team Sessions with group member verification - Invitations with real-time notifications - Helper functions for group ownership and membership checks Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/pb_hooks/go.mod | 5 ++ backend/pb_hooks/main.go | 144 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 backend/pb_hooks/go.mod create mode 100644 backend/pb_hooks/main.go diff --git a/backend/pb_hooks/go.mod b/backend/pb_hooks/go.mod new file mode 100644 index 0000000..1cbfed2 --- /dev/null +++ b/backend/pb_hooks/go.mod @@ -0,0 +1,5 @@ +module gamegroup-hooks + +go 1.21 + +require github.com/pocketbase/pocketbase v0.22.4 diff --git a/backend/pb_hooks/main.go b/backend/pb_hooks/main.go new file mode 100644 index 0000000..4237a97 --- /dev/null +++ b/backend/pb_hooks/main.go @@ -0,0 +1,144 @@ +package main + +import ( + "log" + "strings" + + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tools/types" +) + +// isGroupMember checks if a user is a member of a group +func isGroupMember(app *pocketbase.PocketBase, groupId string, userId string) (bool, error) { + group, err := app.Dao().FindRecordById("groups", groupId) + if err != nil { + return false, err + } + + members := group.GetStringSlice("members") + for _, member := range members { + if member == userId { + return true, nil + } + } + return false, nil +} + +// isGroupOwner checks if a user is the owner of a group +func isGroupOwner(app *pocketbase.PocketBase, groupId string, userId string) (bool, error) { + group, err := app.Dao().FindRecordById("groups", groupId) + if err != nil { + return false, err + } + return group.GetString("owner") == userId, nil +} + +func main() { + app := pocketbase.New() + + // Groups API Rules + app.OnRecordBeforeCreateRequest("groups").Add(func(e *core.RecordCreateEvent) next func() { + // Set the owner to the current user + e.Record.Set("owner", e.HttpContext.Request().Context().Value("user_id")) + // Initialize members array with the owner + e.Record.Set("members", []string{e.Record.GetString("owner")}) + return next + }) + + app.OnRecordBeforeUpdateRequest("groups").Add(func(e *core.RecordUpdateEvent) next func() { + userId := e.HttpContext.Request().Context().Value("user_id").(string) + + // Only owner can update the group + isOwner, err := isGroupOwner(app, e.Record.Id, userId) + if err != nil || !isOwner { + e.HttpContext.Response().WriteHeader(403) + return nil + } + return next + }) + + app.OnRecordBeforeDeleteRequest("groups").Add(func(e *core.RecordDeleteEvent) next func() { + userId := e.HttpContext.Request().Context().Value("user_id").(string) + + // Only owner can delete the group + isOwner, err := isGroupOwner(app, e.Record.Id, userId) + if err != nil || !isOwner { + e.HttpContext.Response().WriteHeader(403) + return nil + } + return next + }) + + // Team Sessions API Rules + app.OnRecordBeforeCreateRequest("team_sessions").Add(func(e *core.RecordCreateEvent) next func() { + userId := e.HttpContext.Request().Context().Value("user_id").(string) + groupId := e.Record.GetString("group") + + // Check if user is a member of the group + isMember, err := isGroupMember(app, groupId, userId) + if err != nil || !isMember { + e.HttpContext.Response().WriteHeader(403) + return nil + } + return next + }) + + // Invitations API Rules + app.OnRecordBeforeCreateRequest("invitations").Add(func(e *core.RecordCreateEvent) next func() { + userId := e.HttpContext.Request().Context().Value("user_id").(string) + groupId := e.Record.GetString("group") + + // Only group owner can create invitations + isOwner, err := isGroupOwner(app, groupId, userId) + if err != nil || !isOwner { + e.HttpContext.Response().WriteHeader(403) + return nil + } + + // Set status to pending + e.Record.Set("status", "pending") + return next + }) + + app.OnRecordAfterCreateRequest("invitations").Add(func(e *core.RecordCreateEvent) next func() { + // Send real-time notification to the invited user + message := map[string]interface{}{ + "action": "invitation", + "data": map[string]interface{}{ + "id": e.Record.Id, + "group": e.Record.GetString("group"), + "invited_by": e.Record.GetString("invited_by"), + "status": e.Record.GetString("status"), + "created": e.Record.Created.Time(), + }, + } + + // Broadcast to the invited user's channel + if err := app.Realtime().Broadcast("invitations", message); err != nil { + log.Printf("Error broadcasting invitation: %v", err) + } + return next + }) + + // Real-time subscription rules + app.OnRecordAfterAuthWithTokenRequest().Add(func(e *core.RecordAuthEvent) next func() { + // Subscribe to invitations channel + e.HttpContext.Request().Context().Set("realtime_subscriptions", []string{ + "invitations", + "groups:" + e.Record.Id, + "team_sessions", + }) + return next + }) + + // Custom API endpoint for accepting invitations + app.OnServe().Bind(&ServeEvent{ + App: app, + }) +} + +type ServeEvent struct { + App *pocketbase.PocketBase +}