Files
FPMB/server/internal/handlers/projects.go
2026-02-28 04:21:27 +00:00

634 lines
20 KiB
Go

package handlers
import (
"context"
"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"
)
func getProjectRole(ctx context.Context, projectID, userID primitive.ObjectID) (int, error) {
var pm models.ProjectMember
err := database.GetCollection("project_members").FindOne(ctx, bson.M{
"project_id": projectID,
"user_id": userID,
}).Decode(&pm)
if err == nil {
return pm.RoleFlags, nil
}
var project models.Project
if err := database.GetCollection("projects").FindOne(ctx, bson.M{"_id": projectID}).Decode(&project); err != nil {
return 0, err
}
if project.TeamID == primitive.NilObjectID {
return 0, fiber.ErrForbidden
}
return getTeamRole(ctx, project.TeamID, userID)
}
func ListProjects(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()
type ProjectResponse struct {
ID primitive.ObjectID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
TeamID primitive.ObjectID `json:"team_id"`
TeamName string `json:"team_name"`
RoleFlags int `json:"role_flags"`
RoleName string `json:"role_name"`
IsPublic bool `json:"is_public"`
IsArchived bool `json:"is_archived"`
UpdatedAt time.Time `json:"updated_at"`
}
result := []ProjectResponse{}
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
cursor.All(ctx, &memberships)
for _, m := range memberships {
var team models.Team
database.GetCollection("teams").FindOne(ctx, bson.M{"_id": m.TeamID}).Decode(&team)
projCursor, err := database.GetCollection("projects").Find(ctx, bson.M{"team_id": m.TeamID})
if err != nil {
continue
}
var projects []models.Project
projCursor.All(ctx, &projects)
projCursor.Close(ctx)
for _, p := range projects {
roleFlags := m.RoleFlags
var pm models.ProjectMember
if err := database.GetCollection("project_members").FindOne(ctx, bson.M{
"project_id": p.ID,
"user_id": userID,
}).Decode(&pm); err == nil {
roleFlags = pm.RoleFlags
}
result = append(result, ProjectResponse{
ID: p.ID,
Name: p.Name,
Description: p.Description,
TeamID: p.TeamID,
TeamName: team.Name,
RoleFlags: roleFlags,
RoleName: roleName(roleFlags),
IsPublic: p.IsPublic,
IsArchived: p.IsArchived,
UpdatedAt: p.UpdatedAt,
})
}
}
personalCursor, err := database.GetCollection("project_members").Find(ctx, bson.M{
"user_id": userID,
})
if err == nil {
defer personalCursor.Close(ctx)
var pms []models.ProjectMember
personalCursor.All(ctx, &pms)
for _, pm := range pms {
var p models.Project
if err := database.GetCollection("projects").FindOne(ctx, bson.M{
"_id": pm.ProjectID,
"team_id": primitive.NilObjectID,
}).Decode(&p); err != nil {
continue
}
result = append(result, ProjectResponse{
ID: p.ID,
Name: p.Name,
Description: p.Description,
TeamID: p.TeamID,
TeamName: "Personal",
RoleFlags: pm.RoleFlags,
RoleName: roleName(pm.RoleFlags),
IsPublic: p.IsPublic,
IsArchived: p.IsArchived,
UpdatedAt: p.UpdatedAt,
})
}
}
return c.JSON(result)
}
func CreatePersonalProject(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"`
Description string `json:"description"`
IsPublic bool `json:"is_public"`
}
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": "Project name is required"})
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
now := time.Now()
project := &models.Project{
ID: primitive.NewObjectID(),
TeamID: primitive.NilObjectID,
Name: body.Name,
Description: body.Description,
IsPublic: body.IsPublic,
IsArchived: false,
CreatedBy: userID,
CreatedAt: now,
UpdatedAt: now,
}
if _, err := database.GetCollection("projects").InsertOne(ctx, project); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create project"})
}
member := &models.ProjectMember{
ID: primitive.NewObjectID(),
ProjectID: project.ID,
UserID: userID,
RoleFlags: RoleOwner,
AddedAt: now,
}
database.GetCollection("project_members").InsertOne(ctx, member)
defaultColumns := []string{"To Do", "In Progress", "Done"}
for i, title := range defaultColumns {
col := &models.BoardColumn{
ID: primitive.NewObjectID(),
ProjectID: project.ID,
Title: title,
Position: i,
CreatedAt: now,
UpdatedAt: now,
}
database.GetCollection("board_columns").InsertOne(ctx, col)
}
return c.Status(fiber.StatusCreated).JSON(project)
}
func ListTeamProjects(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()
teamRole, err := getTeamRole(ctx, teamID, userID)
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Access denied"})
}
cursor, err := database.GetCollection("projects").Find(ctx, bson.M{"team_id": teamID},
options.Find().SetSort(bson.M{"updated_at": -1}))
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to fetch projects"})
}
defer cursor.Close(ctx)
var projects []models.Project
cursor.All(ctx, &projects)
type ProjectResponse struct {
models.Project
RoleFlags int `json:"role_flags"`
RoleName string `json:"role_name"`
}
result := []ProjectResponse{}
for _, p := range projects {
flags := teamRole
var pm models.ProjectMember
if err := database.GetCollection("project_members").FindOne(ctx, bson.M{
"project_id": p.ID, "user_id": userID,
}).Decode(&pm); err == nil {
flags = pm.RoleFlags
}
result = append(result, ProjectResponse{Project: p, RoleFlags: flags, RoleName: roleName(flags)})
}
return c.JSON(result)
}
func CreateProject(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"})
}
var body struct {
Name string `json:"name"`
Description string `json:"description"`
IsPublic bool `json:"is_public"`
}
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": "Project name is required"})
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
roleFlags, err := getTeamRole(ctx, teamID, userID)
if err != nil || !hasPermission(roleFlags, RoleEditor) {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
}
now := time.Now()
project := &models.Project{
ID: primitive.NewObjectID(),
TeamID: teamID,
Name: body.Name,
Description: body.Description,
IsPublic: body.IsPublic,
IsArchived: false,
CreatedBy: userID,
CreatedAt: now,
UpdatedAt: now,
}
if _, err := database.GetCollection("projects").InsertOne(ctx, project); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create project"})
}
defaultColumns := []string{"To Do", "In Progress", "Done"}
for i, title := range defaultColumns {
col := &models.BoardColumn{
ID: primitive.NewObjectID(),
ProjectID: project.ID,
Title: title,
Position: i,
CreatedAt: now,
UpdatedAt: now,
}
database.GetCollection("board_columns").InsertOne(ctx, col)
}
return c.Status(fiber.StatusCreated).JSON(project)
}
func GetProject(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"})
}
projectID, err := primitive.ObjectIDFromHex(c.Params("projectId"))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid project ID"})
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
roleFlags, err := getProjectRole(ctx, projectID, userID)
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Access denied"})
}
var project models.Project
if err := database.GetCollection("projects").FindOne(ctx, bson.M{"_id": projectID}).Decode(&project); err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Project not found"})
}
return c.JSON(fiber.Map{
"id": project.ID,
"team_id": project.TeamID,
"name": project.Name,
"description": project.Description,
"visibility": project.Visibility,
"is_public": project.IsPublic,
"is_archived": project.IsArchived,
"role_flags": roleFlags,
"role_name": roleName(roleFlags),
"created_at": project.CreatedAt,
"updated_at": project.UpdatedAt,
})
}
func UpdateProject(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"})
}
projectID, err := primitive.ObjectIDFromHex(c.Params("projectId"))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid project ID"})
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
roleFlags, err := getProjectRole(ctx, projectID, 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"`
Description string `json:"description"`
IsPublic *bool `json:"is_public"`
Visibility string `json:"visibility"`
}
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.Description != "" {
update["description"] = body.Description
}
if body.IsPublic != nil {
update["is_public"] = *body.IsPublic
}
if body.Visibility != "" {
update["visibility"] = body.Visibility
update["is_public"] = body.Visibility == "public"
}
col := database.GetCollection("projects")
col.UpdateOne(ctx, bson.M{"_id": projectID}, bson.M{"$set": update})
var project models.Project
col.FindOne(ctx, bson.M{"_id": projectID}).Decode(&project)
return c.JSON(project)
}
func ArchiveProject(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"})
}
projectID, err := primitive.ObjectIDFromHex(c.Params("projectId"))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid project ID"})
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
roleFlags, err := getProjectRole(ctx, projectID, userID)
if err != nil || !hasPermission(roleFlags, RoleAdmin) {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
}
var project models.Project
if err := database.GetCollection("projects").FindOne(ctx, bson.M{"_id": projectID}).Decode(&project); err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Project not found"})
}
database.GetCollection("projects").UpdateOne(ctx, bson.M{"_id": projectID}, bson.M{"$set": bson.M{
"is_archived": !project.IsArchived,
"updated_at": time.Now(),
}})
return c.JSON(fiber.Map{"is_archived": !project.IsArchived})
}
func DeleteProject(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"})
}
projectID, err := primitive.ObjectIDFromHex(c.Params("projectId"))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid project ID"})
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
roleFlags, err := getProjectRole(ctx, projectID, userID)
if err != nil || !hasPermission(roleFlags, RoleOwner) {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Only owners can delete projects"})
}
database.GetCollection("projects").DeleteOne(ctx, bson.M{"_id": projectID})
database.GetCollection("board_columns").DeleteMany(ctx, bson.M{"project_id": projectID})
database.GetCollection("cards").DeleteMany(ctx, bson.M{"project_id": projectID})
database.GetCollection("project_members").DeleteMany(ctx, bson.M{"project_id": projectID})
database.GetCollection("events").DeleteMany(ctx, bson.M{"scope_id": projectID, "scope": "project"})
database.GetCollection("files").DeleteMany(ctx, bson.M{"project_id": projectID})
database.GetCollection("webhooks").DeleteMany(ctx, bson.M{"project_id": projectID})
database.GetCollection("whiteboards").DeleteMany(ctx, bson.M{"project_id": projectID})
return c.JSON(fiber.Map{"message": "Project deleted"})
}
func ListProjectMembers(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"})
}
projectID, err := primitive.ObjectIDFromHex(c.Params("projectId"))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid project ID"})
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if _, err := getProjectRole(ctx, projectID, userID); err != nil {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Access denied"})
}
cursor, err := database.GetCollection("project_members").Find(ctx, bson.M{"project_id": projectID})
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to fetch members"})
}
defer cursor.Close(ctx)
var members []models.ProjectMember
cursor.All(ctx, &members)
type MemberResponse struct {
UserID primitive.ObjectID `json:"user_id"`
Name string `json:"name"`
Email string `json:"email"`
RoleFlags int `json:"role_flags"`
RoleName string `json:"role_name"`
}
result := []MemberResponse{}
for _, m := range members {
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{
UserID: m.UserID,
Name: user.Name,
Email: user.Email,
RoleFlags: m.RoleFlags,
RoleName: roleName(m.RoleFlags),
})
}
return c.JSON(result)
}
func AddProjectMember(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"})
}
projectID, err := primitive.ObjectIDFromHex(c.Params("projectId"))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid project ID"})
}
var body struct {
UserID string `json:"user_id"`
RoleFlags int `json:"role_flags"`
}
if err := c.BodyParser(&body); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
}
targetUserID, err := primitive.ObjectIDFromHex(body.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 := getProjectRole(ctx, projectID, requesterID)
if err != nil || !hasPermission(roleFlags, RoleAdmin) {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
}
flags := body.RoleFlags
if flags == 0 {
flags = RoleViewer
}
member := &models.ProjectMember{
ID: primitive.NewObjectID(),
ProjectID: projectID,
UserID: targetUserID,
RoleFlags: flags,
AddedAt: time.Now(),
}
database.GetCollection("project_members").InsertOne(ctx, member)
return c.Status(fiber.StatusCreated).JSON(member)
}
func UpdateProjectMemberRole(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"})
}
projectID, err := primitive.ObjectIDFromHex(c.Params("projectId"))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid project 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"`
}
c.BodyParser(&body)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
roleFlags, err := getProjectRole(ctx, projectID, requesterID)
if err != nil || !hasPermission(roleFlags, RoleAdmin) {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
}
database.GetCollection("project_members").UpdateOne(ctx,
bson.M{"project_id": projectID, "user_id": targetUserID},
bson.M{"$set": bson.M{"role_flags": body.RoleFlags}},
)
return c.JSON(fiber.Map{"user_id": targetUserID, "role_flags": body.RoleFlags, "role_name": roleName(body.RoleFlags)})
}
func RemoveProjectMember(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"})
}
projectID, err := primitive.ObjectIDFromHex(c.Params("projectId"))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid project 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 := getProjectRole(ctx, projectID, requesterID)
if err != nil || !hasPermission(roleFlags, RoleAdmin) {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
}
database.GetCollection("project_members").DeleteOne(ctx, bson.M{"project_id": projectID, "user_id": targetUserID})
return c.JSON(fiber.Map{"message": "Member removed"})
}