package handlers import ( "context" "fmt" "log" "os" "path/filepath" "strings" "time" "github.com/fpmb/server/internal/database" "github.com/fpmb/server/internal/models" "github.com/gofiber/fiber/v2" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo/options" ) const ( RoleViewer = 1 RoleEditor = 2 RoleAdmin = 4 RoleOwner = 8 ) func hasPermission(userRole, requiredRole int) bool { return userRole >= requiredRole } func roleName(flags int) string { switch { case flags&RoleOwner != 0: return "Owner" case flags&RoleAdmin != 0: return "Admin" case flags&RoleEditor != 0: return "Editor" default: return "Viewer" } } func getTeamRole(ctx context.Context, teamID, userID primitive.ObjectID) (int, error) { var member models.TeamMember err := database.GetCollection("team_members").FindOne(ctx, bson.M{ "team_id": teamID, "user_id": userID, }).Decode(&member) if err != nil { return 0, err } return member.RoleFlags, nil } func ListTeams(c *fiber.Ctx) error { userID, err := primitive.ObjectIDFromHex(c.Locals("user_id").(string)) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid user"}) } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() cursor, err := database.GetCollection("team_members").Find(ctx, bson.M{"user_id": userID}) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to fetch teams"}) } defer cursor.Close(ctx) var memberships []models.TeamMember if err := cursor.All(ctx, &memberships); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to decode memberships"}) } type TeamResponse struct { ID primitive.ObjectID `json:"id"` Name string `json:"name"` WorkspaceID string `json:"workspace_id"` MemberCount int64 `json:"member_count"` RoleFlags int `json:"role_flags"` RoleName string `json:"role_name"` CreatedAt time.Time `json:"created_at"` } result := []TeamResponse{} for _, m := range memberships { var team models.Team if err := database.GetCollection("teams").FindOne(ctx, bson.M{"_id": m.TeamID}).Decode(&team); err != nil { continue } count, _ := database.GetCollection("team_members").CountDocuments(ctx, bson.M{"team_id": m.TeamID}) result = append(result, TeamResponse{ ID: team.ID, Name: team.Name, WorkspaceID: team.WorkspaceID, MemberCount: count, RoleFlags: m.RoleFlags, RoleName: roleName(m.RoleFlags), CreatedAt: team.CreatedAt, }) } return c.JSON(result) } func CreateTeam(c *fiber.Ctx) error { userID, err := primitive.ObjectIDFromHex(c.Locals("user_id").(string)) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid user"}) } var body struct { Name string `json:"name"` WorkspaceID string `json:"workspace_id"` } if err := c.BodyParser(&body); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) } if body.Name == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Team name is required"}) } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() now := time.Now() team := &models.Team{ ID: primitive.NewObjectID(), Name: body.Name, WorkspaceID: body.WorkspaceID, CreatedBy: userID, CreatedAt: now, UpdatedAt: now, } if _, err := database.GetCollection("teams").InsertOne(ctx, team); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create team"}) } member := &models.TeamMember{ ID: primitive.NewObjectID(), TeamID: team.ID, UserID: userID, RoleFlags: RoleOwner, InvitedBy: userID, JoinedAt: now, } if _, err := database.GetCollection("team_members").InsertOne(ctx, member); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to add team owner"}) } return c.Status(fiber.StatusCreated).JSON(team) } func GetTeam(c *fiber.Ctx) error { userID, err := primitive.ObjectIDFromHex(c.Locals("user_id").(string)) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid user"}) } teamID, err := primitive.ObjectIDFromHex(c.Params("teamId")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid team ID"}) } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() roleFlags, err := getTeamRole(ctx, teamID, userID) if err != nil { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Access denied"}) } var team models.Team if err := database.GetCollection("teams").FindOne(ctx, bson.M{"_id": teamID}).Decode(&team); err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Team not found"}) } count, _ := database.GetCollection("team_members").CountDocuments(ctx, bson.M{"team_id": teamID}) return c.JSON(fiber.Map{ "id": team.ID, "name": team.Name, "workspace_id": team.WorkspaceID, "avatar_url": team.AvatarURL, "banner_url": team.BannerURL, "member_count": count, "role_flags": roleFlags, "role_name": roleName(roleFlags), "created_at": team.CreatedAt, "updated_at": team.UpdatedAt, }) } func UpdateTeam(c *fiber.Ctx) error { userID, err := primitive.ObjectIDFromHex(c.Locals("user_id").(string)) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid user"}) } teamID, err := primitive.ObjectIDFromHex(c.Params("teamId")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid team ID"}) } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() roleFlags, err := getTeamRole(ctx, teamID, userID) if err != nil || !hasPermission(roleFlags, RoleAdmin) { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"}) } var body struct { Name string `json:"name"` WorkspaceID string `json:"workspace_id"` } if err := c.BodyParser(&body); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) } update := bson.M{"updated_at": time.Now()} if body.Name != "" { update["name"] = body.Name } if body.WorkspaceID != "" { update["workspace_id"] = body.WorkspaceID } col := database.GetCollection("teams") if _, err := col.UpdateOne(ctx, bson.M{"_id": teamID}, bson.M{"$set": update}); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to update team"}) } var team models.Team col.FindOne(ctx, bson.M{"_id": teamID}).Decode(&team) return c.JSON(team) } func DeleteTeam(c *fiber.Ctx) error { userID, err := primitive.ObjectIDFromHex(c.Locals("user_id").(string)) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid user"}) } teamID, err := primitive.ObjectIDFromHex(c.Params("teamId")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid team ID"}) } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() roleFlags, err := getTeamRole(ctx, teamID, userID) if err != nil || !hasPermission(roleFlags, RoleOwner) { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Only owners can delete teams"}) } database.GetCollection("teams").DeleteOne(ctx, bson.M{"_id": teamID}) database.GetCollection("team_members").DeleteMany(ctx, bson.M{"team_id": teamID}) return c.JSON(fiber.Map{"message": "Team deleted"}) } func ListTeamMembers(c *fiber.Ctx) error { userID, err := primitive.ObjectIDFromHex(c.Locals("user_id").(string)) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid user"}) } teamID, err := primitive.ObjectIDFromHex(c.Params("teamId")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid team ID"}) } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if _, err := getTeamRole(ctx, teamID, userID); err != nil { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Access denied"}) } cursor, err := database.GetCollection("team_members").Find(ctx, bson.M{"team_id": teamID}, options.Find().SetSort(bson.M{"joined_at": 1})) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to fetch members"}) } defer cursor.Close(ctx) var memberships []models.TeamMember cursor.All(ctx, &memberships) type MemberResponse struct { ID primitive.ObjectID `json:"id"` UserID primitive.ObjectID `json:"user_id"` Name string `json:"name"` Email string `json:"email"` RoleFlags int `json:"role_flags"` RoleName string `json:"role_name"` JoinedAt time.Time `json:"joined_at"` } result := []MemberResponse{} for _, m := range memberships { var user models.User if err := database.GetCollection("users").FindOne(ctx, bson.M{"_id": m.UserID}).Decode(&user); err != nil { continue } result = append(result, MemberResponse{ ID: m.ID, UserID: m.UserID, Name: user.Name, Email: user.Email, RoleFlags: m.RoleFlags, RoleName: roleName(m.RoleFlags), JoinedAt: m.JoinedAt, }) } return c.JSON(result) } func InviteTeamMember(c *fiber.Ctx) error { inviterID, err := primitive.ObjectIDFromHex(c.Locals("user_id").(string)) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid user"}) } teamID, err := primitive.ObjectIDFromHex(c.Params("teamId")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid team ID"}) } var body struct { Email string `json:"email"` RoleFlags int `json:"role_flags"` } if err := c.BodyParser(&body); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) } if body.Email == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Email is required"}) } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() roleFlags, err := getTeamRole(ctx, teamID, inviterID) if err != nil || !hasPermission(roleFlags, RoleAdmin) { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"}) } var invitee models.User if err := database.GetCollection("users").FindOne(ctx, bson.M{"email": body.Email}).Decode(&invitee); err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User with that email not found"}) } existing := database.GetCollection("team_members").FindOne(ctx, bson.M{"team_id": teamID, "user_id": invitee.ID}) if existing.Err() == nil { return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": "User is already a member"}) } flags := body.RoleFlags if flags == 0 { flags = RoleViewer } member := &models.TeamMember{ ID: primitive.NewObjectID(), TeamID: teamID, UserID: invitee.ID, RoleFlags: flags, InvitedBy: inviterID, JoinedAt: time.Now(), } if _, err := database.GetCollection("team_members").InsertOne(ctx, member); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to add member"}) } var team models.Team if err := database.GetCollection("teams").FindOne(ctx, bson.M{"_id": teamID}).Decode(&team); err == nil { createNotification(ctx, invitee.ID, "team_invite", "You have been invited to team \""+team.Name+"\"", primitive.NilObjectID, primitive.NilObjectID) } return c.Status(fiber.StatusCreated).JSON(fiber.Map{ "message": "Member added successfully", "member": fiber.Map{ "user_id": invitee.ID, "name": invitee.Name, "email": invitee.Email, "role_flags": flags, "role_name": roleName(flags), }, }) } func UpdateTeamMemberRole(c *fiber.Ctx) error { requesterID, err := primitive.ObjectIDFromHex(c.Locals("user_id").(string)) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid user"}) } teamID, err := primitive.ObjectIDFromHex(c.Params("teamId")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid team ID"}) } targetUserID, err := primitive.ObjectIDFromHex(c.Params("userId")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid user ID"}) } var body struct { RoleFlags int `json:"role_flags"` } if err := c.BodyParser(&body); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() roleFlags, err := getTeamRole(ctx, teamID, requesterID) if err != nil || !hasPermission(roleFlags, RoleAdmin) { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"}) } if _, err := database.GetCollection("team_members").UpdateOne(ctx, bson.M{"team_id": teamID, "user_id": targetUserID}, bson.M{"$set": bson.M{"role_flags": body.RoleFlags}}, ); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to update role"}) } return c.JSON(fiber.Map{ "user_id": targetUserID, "role_flags": body.RoleFlags, "role_name": roleName(body.RoleFlags), }) } func RemoveTeamMember(c *fiber.Ctx) error { requesterID, err := primitive.ObjectIDFromHex(c.Locals("user_id").(string)) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid user"}) } teamID, err := primitive.ObjectIDFromHex(c.Params("teamId")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid team ID"}) } targetUserID, err := primitive.ObjectIDFromHex(c.Params("userId")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid user ID"}) } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() roleFlags, err := getTeamRole(ctx, teamID, requesterID) if err != nil || !hasPermission(roleFlags, RoleAdmin) { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"}) } database.GetCollection("team_members").DeleteOne(ctx, bson.M{"team_id": teamID, "user_id": targetUserID}) return c.JSON(fiber.Map{"message": "Member removed"}) } var allowedImageExts = map[string]bool{ ".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true, } func uploadTeamImage(c *fiber.Ctx, imageType string) error { userID, err := primitive.ObjectIDFromHex(c.Locals("user_id").(string)) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid user"}) } teamID, err := primitive.ObjectIDFromHex(c.Params("teamId")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid team ID"}) } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() roleFlags, err := getTeamRole(ctx, teamID, userID) if err != nil || !hasPermission(roleFlags, RoleAdmin) { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"}) } fh, err := c.FormFile("file") if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "No file provided"}) } ext := strings.ToLower(filepath.Ext(fh.Filename)) if !allowedImageExts[ext] { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid image type"}) } dir := filepath.Join("../data/teams", teamID.Hex()) if err := os.MkdirAll(dir, 0755); err != nil { log.Printf("uploadTeamImage MkdirAll error: %v", err) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create storage directory"}) } existingGlob := filepath.Join(dir, imageType+".*") if matches, _ := filepath.Glob(existingGlob); len(matches) > 0 { for _, m := range matches { os.Remove(m) } } destPath := filepath.Join(dir, imageType+ext) if err := c.SaveFile(fh, destPath); err != nil { log.Printf("uploadTeamImage SaveFile error: %v", err) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to save image"}) } imageURL := fmt.Sprintf("/api/teams/%s/%s", teamID.Hex(), imageType) field := imageType + "_url" col := database.GetCollection("teams") if _, err := col.UpdateOne(ctx, bson.M{"_id": teamID}, bson.M{"$set": bson.M{ field: imageURL, "updated_at": time.Now(), }}); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to update team"}) } var team models.Team col.FindOne(ctx, bson.M{"_id": teamID}).Decode(&team) count, _ := database.GetCollection("team_members").CountDocuments(ctx, bson.M{"team_id": teamID}) return c.JSON(fiber.Map{ "id": team.ID, "name": team.Name, "workspace_id": team.WorkspaceID, "avatar_url": team.AvatarURL, "banner_url": team.BannerURL, "member_count": count, "role_flags": roleFlags, "role_name": roleName(roleFlags), "created_at": team.CreatedAt, "updated_at": team.UpdatedAt, }) } func UploadTeamAvatar(c *fiber.Ctx) error { return uploadTeamImage(c, "avatar") } func UploadTeamBanner(c *fiber.Ctx) error { return uploadTeamImage(c, "banner") } func serveTeamImage(c *fiber.Ctx, imageType string) error { userID, err := primitive.ObjectIDFromHex(c.Locals("user_id").(string)) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid user"}) } teamID, err := primitive.ObjectIDFromHex(c.Params("teamId")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid team ID"}) } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if _, err := getTeamRole(ctx, teamID, userID); err != nil { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Access denied"}) } dir := filepath.Join("../data/teams", teamID.Hex()) for ext := range allowedImageExts { p := filepath.Join(dir, imageType+ext) if _, err := os.Stat(p); err == nil { return c.SendFile(p) } } return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Image not found"}) } func ServeTeamAvatar(c *fiber.Ctx) error { return serveTeamImage(c, "avatar") } func ServeTeamBanner(c *fiber.Ctx) error { return serveTeamImage(c, "banner") }