ALL 0.1.0 Code
This commit is contained in:
165
server/internal/handlers/apikeys.go
Normal file
165
server/internal/handlers/apikeys.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"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"
|
||||
)
|
||||
|
||||
// generateAPIKey returns a 32-byte random hex token (64 chars) prefixed with "fpmb_".
|
||||
func generateAPIKey() (raw string, hashed string, err error) {
|
||||
b := make([]byte, 32)
|
||||
if _, err = rand.Read(b); err != nil {
|
||||
return
|
||||
}
|
||||
raw = "fpmb_" + hex.EncodeToString(b)
|
||||
sum := sha256.Sum256([]byte(raw))
|
||||
hashed = hex.EncodeToString(sum[:])
|
||||
return
|
||||
}
|
||||
|
||||
// ListAPIKeys returns all non-revoked API keys for the current user (without exposing hashes).
|
||||
func ListAPIKeys(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("api_keys").Find(ctx, bson.M{
|
||||
"user_id": userID,
|
||||
"revoked_at": bson.M{"$exists": false},
|
||||
})
|
||||
if err != nil {
|
||||
return c.JSON([]fiber.Map{})
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var keys []models.APIKey
|
||||
cursor.All(ctx, &keys)
|
||||
|
||||
// Strip the hash before returning.
|
||||
type SafeKey struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Scopes []string `json:"scopes"`
|
||||
Prefix string `json:"prefix"`
|
||||
LastUsed *time.Time `json:"last_used,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
result := []SafeKey{}
|
||||
for _, k := range keys {
|
||||
result = append(result, SafeKey{
|
||||
ID: k.ID.Hex(),
|
||||
Name: k.Name,
|
||||
Scopes: k.Scopes,
|
||||
Prefix: k.Prefix,
|
||||
LastUsed: k.LastUsed,
|
||||
CreatedAt: k.CreatedAt,
|
||||
})
|
||||
}
|
||||
return c.JSON(result)
|
||||
}
|
||||
|
||||
// CreateAPIKey generates a new API key and stores its hash.
|
||||
func CreateAPIKey(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"`
|
||||
Scopes []string `json:"scopes"`
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil || body.Name == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name is required"})
|
||||
}
|
||||
if len(body.Scopes) == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "at least one scope is required"})
|
||||
}
|
||||
|
||||
// Validate scopes.
|
||||
valid := map[string]bool{
|
||||
"read:projects": true, "write:projects": true,
|
||||
"read:boards": true, "write:boards": true,
|
||||
"read:teams": true, "write:teams": true,
|
||||
"read:files": true, "write:files": true,
|
||||
"read:notifications": true,
|
||||
}
|
||||
for _, s := range body.Scopes {
|
||||
if !valid[s] {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "unknown scope: " + s})
|
||||
}
|
||||
}
|
||||
|
||||
raw, hashed, err := generateAPIKey()
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to generate key"})
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
key := models.APIKey{
|
||||
ID: primitive.NewObjectID(),
|
||||
UserID: userID,
|
||||
Name: body.Name,
|
||||
Scopes: body.Scopes,
|
||||
KeyHash: hashed,
|
||||
Prefix: raw[:10], // "fpmb_" + first 5 chars of random
|
||||
CreatedAt: now,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if _, err := database.GetCollection("api_keys").InsertOne(ctx, key); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to store key"})
|
||||
}
|
||||
|
||||
// Return the raw key only once.
|
||||
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
|
||||
"id": key.ID.Hex(),
|
||||
"name": key.Name,
|
||||
"scopes": key.Scopes,
|
||||
"prefix": key.Prefix,
|
||||
"key": raw,
|
||||
"created_at": key.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// RevokeAPIKey soft-deletes an API key belonging to the current user.
|
||||
func RevokeAPIKey(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"})
|
||||
}
|
||||
|
||||
keyID, err := primitive.ObjectIDFromHex(c.Params("keyId"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid key ID"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
now := time.Now()
|
||||
res, err := database.GetCollection("api_keys").UpdateOne(ctx,
|
||||
bson.M{"_id": keyID, "user_id": userID},
|
||||
bson.M{"$set": bson.M{"revoked_at": now}},
|
||||
)
|
||||
if err != nil || res.MatchedCount == 0 {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Key not found"})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"message": "Key revoked"})
|
||||
}
|
||||
199
server/internal/handlers/auth.go
Normal file
199
server/internal/handlers/auth.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/fpmb/server/internal/database"
|
||||
"github.com/fpmb/server/internal/middleware"
|
||||
"github.com/fpmb/server/internal/models"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func jwtSecret() []byte {
|
||||
s := os.Getenv("JWT_SECRET")
|
||||
if s == "" {
|
||||
s = "changeme-jwt-secret"
|
||||
}
|
||||
return []byte(s)
|
||||
}
|
||||
|
||||
func jwtRefreshSecret() []byte {
|
||||
s := os.Getenv("JWT_REFRESH_SECRET")
|
||||
if s == "" {
|
||||
s = "changeme-refresh-secret"
|
||||
}
|
||||
return []byte(s)
|
||||
}
|
||||
|
||||
func generateTokens(user *models.User) (string, string, error) {
|
||||
accessClaims := &middleware.JWTClaims{
|
||||
UserID: user.ID.Hex(),
|
||||
Email: user.Email,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
accessToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims).SignedString(jwtSecret())
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
refreshClaims := &middleware.JWTClaims{
|
||||
UserID: user.ID.Hex(),
|
||||
Email: user.Email,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
refreshToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims).SignedString(jwtRefreshSecret())
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return accessToken, refreshToken, nil
|
||||
}
|
||||
|
||||
func Register(c *fiber.Ctx) error {
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
||||
}
|
||||
if body.Name == "" || body.Email == "" || body.Password == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Name, email and password are required"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
col := database.GetCollection("users")
|
||||
existing := col.FindOne(ctx, bson.M{"email": body.Email})
|
||||
if existing.Err() == nil {
|
||||
return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": "Email already in use"})
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(body.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to hash password"})
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
user := &models.User{
|
||||
ID: primitive.NewObjectID(),
|
||||
Name: body.Name,
|
||||
Email: body.Email,
|
||||
PasswordHash: string(hash),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if _, err := col.InsertOne(ctx, user); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create user"})
|
||||
}
|
||||
|
||||
access, refresh, err := generateTokens(user)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to generate tokens"})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
|
||||
"access_token": access,
|
||||
"refresh_token": refresh,
|
||||
"user": fiber.Map{"id": user.ID, "name": user.Name, "email": user.Email},
|
||||
})
|
||||
}
|
||||
|
||||
func Login(c *fiber.Ctx) error {
|
||||
var body struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
||||
}
|
||||
if body.Email == "" || body.Password == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Email and password are required"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var user models.User
|
||||
err := database.GetCollection("users").FindOne(ctx, bson.M{"email": body.Email}).Decode(&user)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid credentials"})
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(body.Password)); err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid credentials"})
|
||||
}
|
||||
|
||||
access, refresh, err := generateTokens(&user)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to generate tokens"})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"access_token": access,
|
||||
"refresh_token": refresh,
|
||||
"user": fiber.Map{"id": user.ID, "name": user.Name, "email": user.Email},
|
||||
})
|
||||
}
|
||||
|
||||
func RefreshToken(c *fiber.Ctx) error {
|
||||
var body struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil || body.RefreshToken == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "refresh_token is required"})
|
||||
}
|
||||
|
||||
claims := &middleware.JWTClaims{}
|
||||
token, err := jwt.ParseWithClaims(body.RefreshToken, claims, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fiber.ErrUnauthorized
|
||||
}
|
||||
return jwtRefreshSecret(), nil
|
||||
})
|
||||
if err != nil || !token.Valid {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid or expired refresh token"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
userID, err := primitive.ObjectIDFromHex(claims.UserID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid token claims"})
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := database.GetCollection("users").FindOne(ctx, bson.M{"_id": userID}).Decode(&user); err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "User not found"})
|
||||
}
|
||||
|
||||
access, newRefresh, err := generateTokens(&user)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to generate tokens"})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"access_token": access,
|
||||
"refresh_token": newRefresh,
|
||||
})
|
||||
}
|
||||
|
||||
func Logout(c *fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{"message": "Logged out successfully"})
|
||||
}
|
||||
484
server/internal/handlers/board.go
Normal file
484
server/internal/handlers/board.go
Normal file
@@ -0,0 +1,484 @@
|
||||
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 GetBoard(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"})
|
||||
}
|
||||
|
||||
colCursor, err := database.GetCollection("board_columns").Find(ctx,
|
||||
bson.M{"project_id": projectID},
|
||||
options.Find().SetSort(bson.M{"position": 1}))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to fetch columns"})
|
||||
}
|
||||
defer colCursor.Close(ctx)
|
||||
|
||||
var columns []models.BoardColumn
|
||||
colCursor.All(ctx, &columns)
|
||||
|
||||
type ColumnWithCards struct {
|
||||
models.BoardColumn
|
||||
Cards []models.Card `json:"cards"`
|
||||
}
|
||||
|
||||
result := []ColumnWithCards{}
|
||||
for _, col := range columns {
|
||||
cardCursor, err := database.GetCollection("cards").Find(ctx,
|
||||
bson.M{"column_id": col.ID},
|
||||
options.Find().SetSort(bson.M{"position": 1}))
|
||||
if err != nil {
|
||||
result = append(result, ColumnWithCards{BoardColumn: col, Cards: []models.Card{}})
|
||||
continue
|
||||
}
|
||||
var cards []models.Card
|
||||
cardCursor.All(ctx, &cards)
|
||||
cardCursor.Close(ctx)
|
||||
if cards == nil {
|
||||
cards = []models.Card{}
|
||||
}
|
||||
result = append(result, ColumnWithCards{BoardColumn: col, Cards: cards})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"project_id": projectID, "columns": result})
|
||||
}
|
||||
|
||||
func CreateColumn(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"})
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil || body.Title == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Title is required"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
roleFlags, err := getProjectRole(ctx, projectID, userID)
|
||||
if err != nil || !hasPermission(roleFlags, RoleEditor) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
|
||||
}
|
||||
|
||||
count, _ := database.GetCollection("board_columns").CountDocuments(ctx, bson.M{"project_id": projectID})
|
||||
now := time.Now()
|
||||
col := &models.BoardColumn{
|
||||
ID: primitive.NewObjectID(),
|
||||
ProjectID: projectID,
|
||||
Title: body.Title,
|
||||
Position: int(count),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
database.GetCollection("board_columns").InsertOne(ctx, col)
|
||||
return c.Status(fiber.StatusCreated).JSON(col)
|
||||
}
|
||||
|
||||
func UpdateColumn(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"})
|
||||
}
|
||||
|
||||
columnID, err := primitive.ObjectIDFromHex(c.Params("columnId"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid column ID"})
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
c.BodyParser(&body)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
roleFlags, err := getProjectRole(ctx, projectID, userID)
|
||||
if err != nil || !hasPermission(roleFlags, RoleEditor) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
|
||||
}
|
||||
|
||||
update := bson.M{"updated_at": time.Now()}
|
||||
if body.Title != "" {
|
||||
update["title"] = body.Title
|
||||
}
|
||||
|
||||
col := database.GetCollection("board_columns")
|
||||
col.UpdateOne(ctx, bson.M{"_id": columnID, "project_id": projectID}, bson.M{"$set": update})
|
||||
|
||||
var column models.BoardColumn
|
||||
col.FindOne(ctx, bson.M{"_id": columnID}).Decode(&column)
|
||||
return c.JSON(column)
|
||||
}
|
||||
|
||||
func ReorderColumn(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"})
|
||||
}
|
||||
|
||||
columnID, err := primitive.ObjectIDFromHex(c.Params("columnId"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid column ID"})
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Position int `json:"position"`
|
||||
}
|
||||
c.BodyParser(&body)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
roleFlags, err := getProjectRole(ctx, projectID, userID)
|
||||
if err != nil || !hasPermission(roleFlags, RoleEditor) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
|
||||
}
|
||||
|
||||
database.GetCollection("board_columns").UpdateOne(ctx,
|
||||
bson.M{"_id": columnID, "project_id": projectID},
|
||||
bson.M{"$set": bson.M{"position": body.Position, "updated_at": time.Now()}},
|
||||
)
|
||||
return c.JSON(fiber.Map{"id": columnID, "position": body.Position})
|
||||
}
|
||||
|
||||
func DeleteColumn(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"})
|
||||
}
|
||||
|
||||
columnID, err := primitive.ObjectIDFromHex(c.Params("columnId"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid column 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"})
|
||||
}
|
||||
|
||||
database.GetCollection("board_columns").DeleteOne(ctx, bson.M{"_id": columnID, "project_id": projectID})
|
||||
database.GetCollection("cards").DeleteMany(ctx, bson.M{"column_id": columnID})
|
||||
return c.JSON(fiber.Map{"message": "Column deleted"})
|
||||
}
|
||||
|
||||
func CreateCard(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"})
|
||||
}
|
||||
|
||||
columnID, err := primitive.ObjectIDFromHex(c.Params("columnId"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid column ID"})
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Priority string `json:"priority"`
|
||||
Color string `json:"color"`
|
||||
DueDate string `json:"due_date"`
|
||||
Assignees []string `json:"assignees"`
|
||||
Subtasks []models.Subtask `json:"subtasks"`
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
||||
}
|
||||
if body.Title == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Title is required"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
roleFlags, err := getProjectRole(ctx, projectID, userID)
|
||||
if err != nil || !hasPermission(roleFlags, RoleEditor) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
|
||||
}
|
||||
|
||||
count, _ := database.GetCollection("cards").CountDocuments(ctx, bson.M{"column_id": columnID})
|
||||
now := time.Now()
|
||||
|
||||
if body.Assignees == nil {
|
||||
body.Assignees = []string{}
|
||||
}
|
||||
if body.Subtasks == nil {
|
||||
body.Subtasks = []models.Subtask{}
|
||||
}
|
||||
if body.Priority == "" {
|
||||
body.Priority = "Medium"
|
||||
}
|
||||
if body.Color == "" {
|
||||
body.Color = "neutral"
|
||||
}
|
||||
|
||||
var dueDate *time.Time
|
||||
if body.DueDate != "" {
|
||||
if parsed, parseErr := time.Parse("2006-01-02", body.DueDate); parseErr == nil {
|
||||
dueDate = &parsed
|
||||
}
|
||||
}
|
||||
|
||||
card := &models.Card{
|
||||
ID: primitive.NewObjectID(),
|
||||
ColumnID: columnID,
|
||||
ProjectID: projectID,
|
||||
Title: body.Title,
|
||||
Description: body.Description,
|
||||
Priority: body.Priority,
|
||||
Color: body.Color,
|
||||
DueDate: dueDate,
|
||||
Assignees: body.Assignees,
|
||||
Subtasks: body.Subtasks,
|
||||
Position: int(count),
|
||||
CreatedBy: userID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
database.GetCollection("cards").InsertOne(ctx, card)
|
||||
|
||||
for _, email := range card.Assignees {
|
||||
var assignee models.User
|
||||
if err := database.GetCollection("users").FindOne(ctx, bson.M{"email": email}).Decode(&assignee); err != nil {
|
||||
continue
|
||||
}
|
||||
if assignee.ID == userID {
|
||||
continue
|
||||
}
|
||||
createNotification(ctx, assignee.ID, "assign",
|
||||
"You have been assigned to the task \""+card.Title+"\"",
|
||||
card.ProjectID, card.ID)
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(card)
|
||||
}
|
||||
|
||||
func UpdateCard(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"})
|
||||
}
|
||||
|
||||
cardID, err := primitive.ObjectIDFromHex(c.Params("cardId"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid card ID"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var existing models.Card
|
||||
if err := database.GetCollection("cards").FindOne(ctx, bson.M{"_id": cardID}).Decode(&existing); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Card not found"})
|
||||
}
|
||||
|
||||
roleFlags, err := getProjectRole(ctx, existing.ProjectID, userID)
|
||||
if err != nil || !hasPermission(roleFlags, RoleEditor) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Title *string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
Priority *string `json:"priority"`
|
||||
Color *string `json:"color"`
|
||||
DueDate *string `json:"due_date"`
|
||||
Assignees []string `json:"assignees"`
|
||||
Subtasks []models.Subtask `json:"subtasks"`
|
||||
}
|
||||
c.BodyParser(&body)
|
||||
|
||||
update := bson.M{"updated_at": time.Now()}
|
||||
if body.Title != nil {
|
||||
update["title"] = *body.Title
|
||||
}
|
||||
if body.Description != nil {
|
||||
update["description"] = *body.Description
|
||||
}
|
||||
if body.Priority != nil {
|
||||
update["priority"] = *body.Priority
|
||||
}
|
||||
if body.Color != nil {
|
||||
update["color"] = *body.Color
|
||||
}
|
||||
if body.DueDate != nil {
|
||||
if *body.DueDate == "" {
|
||||
update["due_date"] = nil
|
||||
} else if parsed, parseErr := time.Parse("2006-01-02", *body.DueDate); parseErr == nil {
|
||||
update["due_date"] = parsed
|
||||
}
|
||||
}
|
||||
if body.Assignees != nil {
|
||||
update["assignees"] = body.Assignees
|
||||
}
|
||||
if body.Subtasks != nil {
|
||||
update["subtasks"] = body.Subtasks
|
||||
}
|
||||
|
||||
col := database.GetCollection("cards")
|
||||
col.UpdateOne(ctx, bson.M{"_id": cardID}, bson.M{"$set": update})
|
||||
|
||||
var card models.Card
|
||||
col.FindOne(ctx, bson.M{"_id": cardID}).Decode(&card)
|
||||
|
||||
if body.Assignees != nil {
|
||||
existingSet := make(map[string]bool)
|
||||
for _, e := range existing.Assignees {
|
||||
existingSet[e] = true
|
||||
}
|
||||
for _, email := range body.Assignees {
|
||||
if existingSet[email] {
|
||||
continue
|
||||
}
|
||||
var assignee models.User
|
||||
if err := database.GetCollection("users").FindOne(ctx, bson.M{"email": email}).Decode(&assignee); err != nil {
|
||||
continue
|
||||
}
|
||||
if assignee.ID == userID {
|
||||
continue
|
||||
}
|
||||
createNotification(ctx, assignee.ID, "assign",
|
||||
"You have been assigned to the task \""+card.Title+"\"",
|
||||
card.ProjectID, card.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(card)
|
||||
}
|
||||
|
||||
func MoveCard(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"})
|
||||
}
|
||||
|
||||
cardID, err := primitive.ObjectIDFromHex(c.Params("cardId"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid card ID"})
|
||||
}
|
||||
|
||||
var body struct {
|
||||
ColumnID string `json:"column_id"`
|
||||
Position int `json:"position"`
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil || body.ColumnID == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "column_id is required"})
|
||||
}
|
||||
|
||||
newColumnID, err := primitive.ObjectIDFromHex(body.ColumnID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid column_id"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var card models.Card
|
||||
if err := database.GetCollection("cards").FindOne(ctx, bson.M{"_id": cardID}).Decode(&card); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Card not found"})
|
||||
}
|
||||
|
||||
roleFlags, err := getProjectRole(ctx, card.ProjectID, userID)
|
||||
if err != nil || !hasPermission(roleFlags, RoleEditor) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
|
||||
}
|
||||
|
||||
col := database.GetCollection("cards")
|
||||
col.UpdateOne(ctx, bson.M{"_id": cardID}, bson.M{"$set": bson.M{
|
||||
"column_id": newColumnID,
|
||||
"position": body.Position,
|
||||
"updated_at": time.Now(),
|
||||
}})
|
||||
|
||||
var updated models.Card
|
||||
col.FindOne(ctx, bson.M{"_id": cardID}).Decode(&updated)
|
||||
return c.JSON(updated)
|
||||
}
|
||||
|
||||
func DeleteCard(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"})
|
||||
}
|
||||
|
||||
cardID, err := primitive.ObjectIDFromHex(c.Params("cardId"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid card ID"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var card models.Card
|
||||
if err := database.GetCollection("cards").FindOne(ctx, bson.M{"_id": cardID}).Decode(&card); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Card not found"})
|
||||
}
|
||||
|
||||
roleFlags, err := getProjectRole(ctx, card.ProjectID, userID)
|
||||
if err != nil || !hasPermission(roleFlags, RoleEditor) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
|
||||
}
|
||||
|
||||
database.GetCollection("cards").DeleteOne(ctx, bson.M{"_id": cardID})
|
||||
return c.JSON(fiber.Map{"message": "Card deleted"})
|
||||
}
|
||||
238
server/internal/handlers/chat.go
Normal file
238
server/internal/handlers/chat.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fpmb/server/internal/database"
|
||||
"github.com/fpmb/server/internal/models"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/websocket/v2"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
type chatRoom struct {
|
||||
clients map[*websocket.Conn]*wsClient
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var chatRooms = struct {
|
||||
m map[string]*chatRoom
|
||||
mu sync.RWMutex
|
||||
}{m: make(map[string]*chatRoom)}
|
||||
|
||||
func getChatRoom(teamID string) *chatRoom {
|
||||
chatRooms.mu.Lock()
|
||||
defer chatRooms.mu.Unlock()
|
||||
if room, ok := chatRooms.m[teamID]; ok {
|
||||
return room
|
||||
}
|
||||
room := &chatRoom{clients: make(map[*websocket.Conn]*wsClient)}
|
||||
chatRooms.m[teamID] = room
|
||||
return room
|
||||
}
|
||||
|
||||
func (r *chatRoom) broadcast(sender *websocket.Conn, msg []byte) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
for conn := range r.clients {
|
||||
if conn != sender {
|
||||
_ = conn.WriteMessage(websocket.TextMessage, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *chatRoom) broadcastAll(msg []byte) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
for conn := range r.clients {
|
||||
_ = conn.WriteMessage(websocket.TextMessage, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *chatRoom) onlineUsers() []map[string]string {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
seen := map[string]bool{}
|
||||
list := make([]map[string]string, 0)
|
||||
for _, c := range r.clients {
|
||||
if !seen[c.userID] {
|
||||
seen[c.userID] = true
|
||||
list = append(list, map[string]string{"user_id": c.userID, "name": c.name})
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func ListChatMessages(c *fiber.Ctx) error {
|
||||
teamID := c.Params("teamId")
|
||||
teamOID, err := primitive.ObjectIDFromHex(teamID)
|
||||
if err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "Invalid team ID"})
|
||||
}
|
||||
|
||||
limitStr := c.Query("limit", "50")
|
||||
limit := int64(50)
|
||||
if l, err := primitive.ParseDecimal128(limitStr); err == nil {
|
||||
if s := l.String(); s != "" {
|
||||
if n, err := parseIntFromString(s); err == nil && n > 0 && n <= 200 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
beforeStr := c.Query("before", "")
|
||||
filter := bson.M{"team_id": teamOID}
|
||||
if beforeStr != "" {
|
||||
if beforeID, err := primitive.ObjectIDFromHex(beforeStr); err == nil {
|
||||
filter["_id"] = bson.M{"$lt": beforeID}
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
opts := options.Find().SetSort(bson.D{{Key: "_id", Value: -1}}).SetLimit(limit)
|
||||
cursor, err := database.GetCollection("chat_messages").Find(ctx, filter, opts)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Failed to fetch messages"})
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var messages []models.ChatMessage
|
||||
if err := cursor.All(ctx, &messages); err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Failed to decode messages"})
|
||||
}
|
||||
if messages == nil {
|
||||
messages = []models.ChatMessage{}
|
||||
}
|
||||
|
||||
for i, j := 0, len(messages)-1; i < j; i, j = i+1, j-1 {
|
||||
messages[i], messages[j] = messages[j], messages[i]
|
||||
}
|
||||
|
||||
return c.JSON(messages)
|
||||
}
|
||||
|
||||
func parseIntFromString(s string) (int64, error) {
|
||||
var n int64
|
||||
for _, ch := range s {
|
||||
if ch < '0' || ch > '9' {
|
||||
break
|
||||
}
|
||||
n = n*10 + int64(ch-'0')
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func TeamChatWS(c *websocket.Conn) {
|
||||
teamID := c.Params("id")
|
||||
tokenStr := c.Query("token", "")
|
||||
userName := c.Query("name", "Anonymous")
|
||||
|
||||
userID, _, ok := parseWSToken(tokenStr)
|
||||
if !ok {
|
||||
_ = c.WriteJSON(map[string]string{"type": "error", "message": "unauthorized"})
|
||||
_ = c.Close()
|
||||
return
|
||||
}
|
||||
|
||||
room := getChatRoom(teamID)
|
||||
|
||||
client := &wsClient{conn: c, userID: userID, name: userName}
|
||||
room.mu.Lock()
|
||||
room.clients[c] = client
|
||||
room.mu.Unlock()
|
||||
|
||||
presenceMsg, _ := json.Marshal(map[string]interface{}{
|
||||
"type": "presence",
|
||||
"users": room.onlineUsers(),
|
||||
})
|
||||
room.broadcastAll(presenceMsg)
|
||||
|
||||
defer func() {
|
||||
room.mu.Lock()
|
||||
delete(room.clients, c)
|
||||
empty := len(room.clients) == 0
|
||||
room.mu.Unlock()
|
||||
|
||||
leaveMsg, _ := json.Marshal(map[string]interface{}{
|
||||
"type": "presence",
|
||||
"users": room.onlineUsers(),
|
||||
})
|
||||
room.broadcast(nil, leaveMsg)
|
||||
|
||||
if empty {
|
||||
chatRooms.mu.Lock()
|
||||
delete(chatRooms.m, teamID)
|
||||
chatRooms.mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
_, msg, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
|
||||
// unexpected error
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
var incoming struct {
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if json.Unmarshal(msg, &incoming) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if incoming.Type == "message" {
|
||||
content := strings.TrimSpace(incoming.Content)
|
||||
if content == "" || len(content) > 5000 {
|
||||
continue
|
||||
}
|
||||
|
||||
teamOID, err := primitive.ObjectIDFromHex(teamID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
userOID, err := primitive.ObjectIDFromHex(userID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
chatMsg := models.ChatMessage{
|
||||
ID: primitive.NewObjectID(),
|
||||
TeamID: teamOID,
|
||||
UserID: userOID,
|
||||
UserName: userName,
|
||||
Content: content,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
_, _ = database.GetCollection("chat_messages").InsertOne(ctx, chatMsg)
|
||||
cancel()
|
||||
|
||||
outMsg, _ := json.Marshal(map[string]interface{}{
|
||||
"type": "message",
|
||||
"message": chatMsg,
|
||||
})
|
||||
room.broadcastAll(outMsg)
|
||||
}
|
||||
|
||||
if incoming.Type == "typing" {
|
||||
typingMsg, _ := json.Marshal(map[string]interface{}{
|
||||
"type": "typing",
|
||||
"user_id": userID,
|
||||
"name": userName,
|
||||
})
|
||||
room.broadcast(c, typingMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
221
server/internal/handlers/docs.go
Normal file
221
server/internal/handlers/docs.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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 ListDocs(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("docs").Find(ctx,
|
||||
bson.M{"team_id": teamID},
|
||||
options.Find().SetSort(bson.M{"updated_at": -1}).SetProjection(bson.M{"content": 0}))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to fetch docs"})
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var docs []models.Doc
|
||||
cursor.All(ctx, &docs)
|
||||
if docs == nil {
|
||||
docs = []models.Doc{}
|
||||
}
|
||||
return c.JSON(docs)
|
||||
}
|
||||
|
||||
func CreateDoc(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 {
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil || body.Title == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Title 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()
|
||||
doc := &models.Doc{
|
||||
ID: primitive.NewObjectID(),
|
||||
TeamID: teamID,
|
||||
Title: body.Title,
|
||||
Content: body.Content,
|
||||
CreatedBy: userID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
database.GetCollection("docs").InsertOne(ctx, doc)
|
||||
|
||||
docDir := filepath.Join("../data/teams", teamID.Hex(), "docs")
|
||||
if err := os.MkdirAll(docDir, 0755); err != nil {
|
||||
log.Printf("CreateDoc: mkdir %s: %v", docDir, err)
|
||||
} else {
|
||||
content := fmt.Sprintf("# %s\n\n%s", doc.Title, doc.Content)
|
||||
if err := os.WriteFile(filepath.Join(docDir, doc.ID.Hex()+".md"), []byte(content), 0644); err != nil {
|
||||
log.Printf("CreateDoc: write file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(doc)
|
||||
}
|
||||
|
||||
func GetDoc(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"})
|
||||
}
|
||||
|
||||
docID, err := primitive.ObjectIDFromHex(c.Params("docId"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid doc ID"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var doc models.Doc
|
||||
if err := database.GetCollection("docs").FindOne(ctx, bson.M{"_id": docID}).Decode(&doc); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Doc not found"})
|
||||
}
|
||||
|
||||
if _, err := getTeamRole(ctx, doc.TeamID, userID); err != nil {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Access denied"})
|
||||
}
|
||||
|
||||
return c.JSON(doc)
|
||||
}
|
||||
|
||||
func UpdateDoc(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"})
|
||||
}
|
||||
|
||||
docID, err := primitive.ObjectIDFromHex(c.Params("docId"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid doc ID"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var existing models.Doc
|
||||
if err := database.GetCollection("docs").FindOne(ctx, bson.M{"_id": docID}).Decode(&existing); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Doc not found"})
|
||||
}
|
||||
|
||||
roleFlags, err := getTeamRole(ctx, existing.TeamID, userID)
|
||||
if err != nil || !hasPermission(roleFlags, RoleEditor) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
c.BodyParser(&body)
|
||||
|
||||
update := bson.M{"updated_at": time.Now()}
|
||||
if body.Title != "" {
|
||||
update["title"] = body.Title
|
||||
}
|
||||
if body.Content != "" {
|
||||
update["content"] = body.Content
|
||||
}
|
||||
|
||||
col := database.GetCollection("docs")
|
||||
col.UpdateOne(ctx, bson.M{"_id": docID}, bson.M{"$set": update})
|
||||
|
||||
var doc models.Doc
|
||||
col.FindOne(ctx, bson.M{"_id": docID}).Decode(&doc)
|
||||
|
||||
docDir := filepath.Join("../data/teams", existing.TeamID.Hex(), "docs")
|
||||
if err := os.MkdirAll(docDir, 0755); err != nil {
|
||||
log.Printf("UpdateDoc: mkdir %s: %v", docDir, err)
|
||||
} else {
|
||||
content := fmt.Sprintf("# %s\n\n%s", doc.Title, doc.Content)
|
||||
if err := os.WriteFile(filepath.Join(docDir, doc.ID.Hex()+".md"), []byte(content), 0644); err != nil {
|
||||
log.Printf("UpdateDoc: write file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(doc)
|
||||
}
|
||||
|
||||
func DeleteDoc(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"})
|
||||
}
|
||||
|
||||
docID, err := primitive.ObjectIDFromHex(c.Params("docId"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid doc ID"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var doc models.Doc
|
||||
if err := database.GetCollection("docs").FindOne(ctx, bson.M{"_id": docID}).Decode(&doc); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Doc not found"})
|
||||
}
|
||||
|
||||
roleFlags, err := getTeamRole(ctx, doc.TeamID, userID)
|
||||
if err != nil || !hasPermission(roleFlags, RoleAdmin) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
|
||||
}
|
||||
|
||||
database.GetCollection("docs").DeleteOne(ctx, bson.M{"_id": docID})
|
||||
|
||||
mdPath := filepath.Join("../data/teams", doc.TeamID.Hex(), "docs", docID.Hex()+".md")
|
||||
if err := os.Remove(mdPath); err != nil && !os.IsNotExist(err) {
|
||||
log.Printf("DeleteDoc: remove file %s: %v", mdPath, err)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"message": "Doc deleted"})
|
||||
}
|
||||
285
server/internal/handlers/events.go
Normal file
285
server/internal/handlers/events.go
Normal file
@@ -0,0 +1,285 @@
|
||||
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 ListTeamEvents(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"})
|
||||
}
|
||||
|
||||
filter := bson.M{"scope_id": teamID, "scope": "org"}
|
||||
if month := c.Query("month"); month != "" {
|
||||
filter["date"] = bson.M{"$regex": "^" + month}
|
||||
}
|
||||
|
||||
cursor, err := database.GetCollection("events").Find(ctx, filter,
|
||||
options.Find().SetSort(bson.M{"date": 1}))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to fetch events"})
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var events []models.Event
|
||||
cursor.All(ctx, &events)
|
||||
if events == nil {
|
||||
events = []models.Event{}
|
||||
}
|
||||
return c.JSON(events)
|
||||
}
|
||||
|
||||
func CreateTeamEvent(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 {
|
||||
Title string `json:"title"`
|
||||
Date string `json:"date"`
|
||||
Time string `json:"time"`
|
||||
Color string `json:"color"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil || body.Title == "" || body.Date == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "title and date are 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()
|
||||
event := &models.Event{
|
||||
ID: primitive.NewObjectID(),
|
||||
Title: body.Title,
|
||||
Date: body.Date,
|
||||
Time: body.Time,
|
||||
Color: body.Color,
|
||||
Description: body.Description,
|
||||
Scope: "org",
|
||||
ScopeID: teamID,
|
||||
CreatedBy: userID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
database.GetCollection("events").InsertOne(ctx, event)
|
||||
return c.Status(fiber.StatusCreated).JSON(event)
|
||||
}
|
||||
|
||||
func ListProjectEvents(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"})
|
||||
}
|
||||
|
||||
filter := bson.M{"scope_id": projectID, "scope": "project"}
|
||||
if month := c.Query("month"); month != "" {
|
||||
filter["date"] = bson.M{"$regex": "^" + month}
|
||||
}
|
||||
|
||||
cursor, err := database.GetCollection("events").Find(ctx, filter,
|
||||
options.Find().SetSort(bson.M{"date": 1}))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to fetch events"})
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var events []models.Event
|
||||
cursor.All(ctx, &events)
|
||||
if events == nil {
|
||||
events = []models.Event{}
|
||||
}
|
||||
return c.JSON(events)
|
||||
}
|
||||
|
||||
func CreateProjectEvent(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"})
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Title string `json:"title"`
|
||||
Date string `json:"date"`
|
||||
Time string `json:"time"`
|
||||
Color string `json:"color"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil || body.Title == "" || body.Date == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "title and date are required"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
roleFlags, err := getProjectRole(ctx, projectID, userID)
|
||||
if err != nil || !hasPermission(roleFlags, RoleEditor) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
event := &models.Event{
|
||||
ID: primitive.NewObjectID(),
|
||||
Title: body.Title,
|
||||
Date: body.Date,
|
||||
Time: body.Time,
|
||||
Color: body.Color,
|
||||
Description: body.Description,
|
||||
Scope: "project",
|
||||
ScopeID: projectID,
|
||||
CreatedBy: userID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
database.GetCollection("events").InsertOne(ctx, event)
|
||||
return c.Status(fiber.StatusCreated).JSON(event)
|
||||
}
|
||||
|
||||
func UpdateEvent(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"})
|
||||
}
|
||||
|
||||
eventID, err := primitive.ObjectIDFromHex(c.Params("eventId"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid event ID"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var event models.Event
|
||||
if err := database.GetCollection("events").FindOne(ctx, bson.M{"_id": eventID}).Decode(&event); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Event not found"})
|
||||
}
|
||||
|
||||
var roleFlags int
|
||||
var roleErr error
|
||||
if event.Scope == "org" {
|
||||
roleFlags, roleErr = getTeamRole(ctx, event.ScopeID, userID)
|
||||
} else {
|
||||
roleFlags, roleErr = getProjectRole(ctx, event.ScopeID, userID)
|
||||
}
|
||||
if roleErr != nil || !hasPermission(roleFlags, RoleEditor) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Title string `json:"title"`
|
||||
Date string `json:"date"`
|
||||
Time string `json:"time"`
|
||||
Color string `json:"color"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
c.BodyParser(&body)
|
||||
|
||||
update := bson.M{"updated_at": time.Now()}
|
||||
if body.Title != "" {
|
||||
update["title"] = body.Title
|
||||
}
|
||||
if body.Date != "" {
|
||||
update["date"] = body.Date
|
||||
}
|
||||
if body.Time != "" {
|
||||
update["time"] = body.Time
|
||||
}
|
||||
if body.Color != "" {
|
||||
update["color"] = body.Color
|
||||
}
|
||||
if body.Description != "" {
|
||||
update["description"] = body.Description
|
||||
}
|
||||
|
||||
col := database.GetCollection("events")
|
||||
col.UpdateOne(ctx, bson.M{"_id": eventID}, bson.M{"$set": update})
|
||||
|
||||
var updated models.Event
|
||||
col.FindOne(ctx, bson.M{"_id": eventID}).Decode(&updated)
|
||||
return c.JSON(updated)
|
||||
}
|
||||
|
||||
func DeleteEvent(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"})
|
||||
}
|
||||
|
||||
eventID, err := primitive.ObjectIDFromHex(c.Params("eventId"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid event ID"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var event models.Event
|
||||
if err := database.GetCollection("events").FindOne(ctx, bson.M{"_id": eventID}).Decode(&event); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Event not found"})
|
||||
}
|
||||
|
||||
var roleFlags int
|
||||
var roleErr error
|
||||
if event.Scope == "org" {
|
||||
roleFlags, roleErr = getTeamRole(ctx, event.ScopeID, userID)
|
||||
} else {
|
||||
roleFlags, roleErr = getProjectRole(ctx, event.ScopeID, userID)
|
||||
}
|
||||
if roleErr != nil || !hasPermission(roleFlags, RoleAdmin) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
|
||||
}
|
||||
|
||||
database.GetCollection("events").DeleteOne(ctx, bson.M{"_id": eventID})
|
||||
return c.JSON(fiber.Map{"message": "Event deleted"})
|
||||
}
|
||||
590
server/internal/handlers/files.go
Normal file
590
server/internal/handlers/files.go
Normal file
@@ -0,0 +1,590 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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 storageBase(ctx context.Context, projectID primitive.ObjectID) (string, error) {
|
||||
var project models.Project
|
||||
if err := database.GetCollection("projects").FindOne(ctx, bson.M{"_id": projectID}).Decode(&project); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if project.TeamID == primitive.NilObjectID {
|
||||
return filepath.Join("../data/users", project.CreatedBy.Hex()), nil
|
||||
}
|
||||
return filepath.Join("../data/teams", project.TeamID.Hex()), nil
|
||||
}
|
||||
|
||||
func ListFiles(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 {
|
||||
log.Printf("ListFiles getProjectRole error: %v (projectID=%s userID=%s)", err, projectID.Hex(), userID.Hex())
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Access denied"})
|
||||
}
|
||||
|
||||
filter := bson.M{"project_id": projectID}
|
||||
if parentID := c.Query("parent_id"); parentID != "" {
|
||||
oid, err := primitive.ObjectIDFromHex(parentID)
|
||||
if err == nil {
|
||||
filter["parent_id"] = oid
|
||||
}
|
||||
} else {
|
||||
filter["parent_id"] = bson.M{"$exists": false}
|
||||
}
|
||||
|
||||
cursor, err := database.GetCollection("files").Find(ctx, filter,
|
||||
options.Find().SetSort(bson.D{{Key: "type", Value: -1}, {Key: "name", Value: 1}}))
|
||||
if err != nil {
|
||||
log.Printf("ListFiles Find error: %v (filter=%v)", err, filter)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to fetch files"})
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var files []models.File
|
||||
cursor.All(ctx, &files)
|
||||
if files == nil {
|
||||
files = []models.File{}
|
||||
}
|
||||
return c.JSON(files)
|
||||
}
|
||||
|
||||
func CreateFolder(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"})
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
ParentID string `json:"parent_id"`
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil || body.Name == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Name is required"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
roleFlags, err := getProjectRole(ctx, projectID, userID)
|
||||
if err != nil || !hasPermission(roleFlags, RoleEditor) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
file := &models.File{
|
||||
ID: primitive.NewObjectID(),
|
||||
ProjectID: projectID,
|
||||
Name: body.Name,
|
||||
Type: "folder",
|
||||
CreatedBy: userID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if body.ParentID != "" {
|
||||
if oid, err := primitive.ObjectIDFromHex(body.ParentID); err == nil {
|
||||
file.ParentID = &oid
|
||||
}
|
||||
}
|
||||
|
||||
database.GetCollection("files").InsertOne(ctx, file)
|
||||
return c.Status(fiber.StatusCreated).JSON(file)
|
||||
}
|
||||
|
||||
func UploadFile(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(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
roleFlags, err := getProjectRole(ctx, projectID, userID)
|
||||
if err != nil || !hasPermission(roleFlags, RoleEditor) {
|
||||
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"})
|
||||
}
|
||||
|
||||
base, err := storageBase(ctx, projectID)
|
||||
if err != nil {
|
||||
log.Printf("UploadFile storageBase error: %v (projectID=%s)", err, projectID.Hex())
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to resolve storage path"})
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(base, 0755); err != nil {
|
||||
log.Printf("UploadFile MkdirAll error: %v (base=%s)", err, base)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create storage directory"})
|
||||
}
|
||||
|
||||
filename := fh.Filename
|
||||
ext := filepath.Ext(filename)
|
||||
stem := filename[:len(filename)-len(ext)]
|
||||
destPath := filepath.Join(base, filename)
|
||||
for n := 2; ; n++ {
|
||||
if _, statErr := os.Stat(destPath); statErr != nil {
|
||||
break
|
||||
}
|
||||
filename = fmt.Sprintf("%s (%d)%s", stem, n, ext)
|
||||
destPath = filepath.Join(base, filename)
|
||||
}
|
||||
|
||||
if err := c.SaveFile(fh, destPath); err != nil {
|
||||
log.Printf("UploadFile SaveFile error: %v (destPath=%s)", err, destPath)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to save file"})
|
||||
}
|
||||
|
||||
relPath := destPath[len("../data/"):]
|
||||
|
||||
now := time.Now()
|
||||
file := &models.File{
|
||||
ID: primitive.NewObjectID(),
|
||||
ProjectID: projectID,
|
||||
Name: fh.Filename,
|
||||
Type: "file",
|
||||
SizeBytes: fh.Size,
|
||||
StorageURL: relPath,
|
||||
CreatedBy: userID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
parentID := c.FormValue("parent_id")
|
||||
if parentID != "" {
|
||||
if oid, err := primitive.ObjectIDFromHex(parentID); err == nil {
|
||||
file.ParentID = &oid
|
||||
}
|
||||
}
|
||||
|
||||
database.GetCollection("files").InsertOne(ctx, file)
|
||||
return c.Status(fiber.StatusCreated).JSON(file)
|
||||
}
|
||||
|
||||
func ListTeamFiles(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"})
|
||||
}
|
||||
|
||||
filter := bson.M{"team_id": teamID}
|
||||
if parentID := c.Query("parent_id"); parentID != "" {
|
||||
oid, err := primitive.ObjectIDFromHex(parentID)
|
||||
if err == nil {
|
||||
filter["parent_id"] = oid
|
||||
}
|
||||
} else {
|
||||
filter["parent_id"] = bson.M{"$exists": false}
|
||||
}
|
||||
|
||||
cursor, err := database.GetCollection("files").Find(ctx, filter,
|
||||
options.Find().SetSort(bson.D{{Key: "type", Value: -1}, {Key: "name", Value: 1}}))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to fetch files"})
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var files []models.File
|
||||
cursor.All(ctx, &files)
|
||||
if files == nil {
|
||||
files = []models.File{}
|
||||
}
|
||||
return c.JSON(files)
|
||||
}
|
||||
|
||||
func CreateTeamFolder(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"`
|
||||
ParentID string `json:"parent_id"`
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil || body.Name == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "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()
|
||||
file := &models.File{
|
||||
ID: primitive.NewObjectID(),
|
||||
TeamID: teamID,
|
||||
Name: body.Name,
|
||||
Type: "folder",
|
||||
CreatedBy: userID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if body.ParentID != "" {
|
||||
if oid, err := primitive.ObjectIDFromHex(body.ParentID); err == nil {
|
||||
file.ParentID = &oid
|
||||
}
|
||||
}
|
||||
|
||||
database.GetCollection("files").InsertOne(ctx, file)
|
||||
return c.Status(fiber.StatusCreated).JSON(file)
|
||||
}
|
||||
|
||||
func UploadTeamFile(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(), 30*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"})
|
||||
}
|
||||
|
||||
fh, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "No file provided"})
|
||||
}
|
||||
|
||||
base := filepath.Join("../data/teams", teamID.Hex(), "files")
|
||||
if err := os.MkdirAll(base, 0755); err != nil {
|
||||
log.Printf("UploadTeamFile MkdirAll error: %v (base=%s)", err, base)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create storage directory"})
|
||||
}
|
||||
|
||||
filename := fh.Filename
|
||||
ext := filepath.Ext(filename)
|
||||
stem := filename[:len(filename)-len(ext)]
|
||||
destPath := filepath.Join(base, filename)
|
||||
for n := 2; ; n++ {
|
||||
if _, statErr := os.Stat(destPath); statErr != nil {
|
||||
break
|
||||
}
|
||||
filename = fmt.Sprintf("%s (%d)%s", stem, n, ext)
|
||||
destPath = filepath.Join(base, filename)
|
||||
}
|
||||
|
||||
if err := c.SaveFile(fh, destPath); err != nil {
|
||||
log.Printf("UploadTeamFile SaveFile error: %v (destPath=%s)", err, destPath)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to save file"})
|
||||
}
|
||||
|
||||
relPath := destPath[len("../data/"):]
|
||||
|
||||
now := time.Now()
|
||||
file := &models.File{
|
||||
ID: primitive.NewObjectID(),
|
||||
TeamID: teamID,
|
||||
Name: fh.Filename,
|
||||
Type: "file",
|
||||
SizeBytes: fh.Size,
|
||||
StorageURL: relPath,
|
||||
CreatedBy: userID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
parentID := c.FormValue("parent_id")
|
||||
if parentID != "" {
|
||||
if oid, err := primitive.ObjectIDFromHex(parentID); err == nil {
|
||||
file.ParentID = &oid
|
||||
}
|
||||
}
|
||||
|
||||
database.GetCollection("files").InsertOne(ctx, file)
|
||||
return c.Status(fiber.StatusCreated).JSON(file)
|
||||
}
|
||||
|
||||
func ListUserFiles(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()
|
||||
|
||||
filter := bson.M{"user_id": userID}
|
||||
if parentID := c.Query("parent_id"); parentID != "" {
|
||||
oid, err := primitive.ObjectIDFromHex(parentID)
|
||||
if err == nil {
|
||||
filter["parent_id"] = oid
|
||||
}
|
||||
} else {
|
||||
filter["parent_id"] = bson.M{"$exists": false}
|
||||
}
|
||||
|
||||
cursor, err := database.GetCollection("files").Find(ctx, filter,
|
||||
options.Find().SetSort(bson.D{{Key: "type", Value: -1}, {Key: "name", Value: 1}}))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to fetch files"})
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var files []models.File
|
||||
cursor.All(ctx, &files)
|
||||
if files == nil {
|
||||
files = []models.File{}
|
||||
}
|
||||
return c.JSON(files)
|
||||
}
|
||||
|
||||
func CreateUserFolder(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"`
|
||||
ParentID string `json:"parent_id"`
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil || body.Name == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Name is required"})
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
file := &models.File{
|
||||
ID: primitive.NewObjectID(),
|
||||
UserID: userID,
|
||||
Name: body.Name,
|
||||
Type: "folder",
|
||||
CreatedBy: userID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if body.ParentID != "" {
|
||||
if oid, err := primitive.ObjectIDFromHex(body.ParentID); err == nil {
|
||||
file.ParentID = &oid
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
database.GetCollection("files").InsertOne(ctx, file)
|
||||
return c.Status(fiber.StatusCreated).JSON(file)
|
||||
}
|
||||
|
||||
func UploadUserFile(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"})
|
||||
}
|
||||
|
||||
fh, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "No file provided"})
|
||||
}
|
||||
|
||||
base := filepath.Join("../data/users", userID.Hex(), "files")
|
||||
if err := os.MkdirAll(base, 0755); err != nil {
|
||||
log.Printf("UploadUserFile MkdirAll error: %v (base=%s)", err, base)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create storage directory"})
|
||||
}
|
||||
|
||||
filename := fh.Filename
|
||||
ext := filepath.Ext(filename)
|
||||
stem := filename[:len(filename)-len(ext)]
|
||||
destPath := filepath.Join(base, filename)
|
||||
for n := 2; ; n++ {
|
||||
if _, statErr := os.Stat(destPath); statErr != nil {
|
||||
break
|
||||
}
|
||||
filename = fmt.Sprintf("%s (%d)%s", stem, n, ext)
|
||||
destPath = filepath.Join(base, filename)
|
||||
}
|
||||
|
||||
if err := c.SaveFile(fh, destPath); err != nil {
|
||||
log.Printf("UploadUserFile SaveFile error: %v (destPath=%s)", err, destPath)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to save file"})
|
||||
}
|
||||
|
||||
relPath := destPath[len("../data/"):]
|
||||
|
||||
now := time.Now()
|
||||
file := &models.File{
|
||||
ID: primitive.NewObjectID(),
|
||||
UserID: userID,
|
||||
Name: fh.Filename,
|
||||
Type: "file",
|
||||
SizeBytes: fh.Size,
|
||||
StorageURL: relPath,
|
||||
CreatedBy: userID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
parentID := c.FormValue("parent_id")
|
||||
if parentID != "" {
|
||||
if oid, err := primitive.ObjectIDFromHex(parentID); err == nil {
|
||||
file.ParentID = &oid
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
database.GetCollection("files").InsertOne(ctx, file)
|
||||
return c.Status(fiber.StatusCreated).JSON(file)
|
||||
}
|
||||
|
||||
func DownloadFile(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"})
|
||||
}
|
||||
|
||||
fileID, err := primitive.ObjectIDFromHex(c.Params("fileId"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid file ID"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var file models.File
|
||||
if err := database.GetCollection("files").FindOne(ctx, bson.M{"_id": fileID}).Decode(&file); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "File not found"})
|
||||
}
|
||||
|
||||
if file.TeamID != primitive.NilObjectID {
|
||||
if _, err := getTeamRole(ctx, file.TeamID, userID); err != nil {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Access denied"})
|
||||
}
|
||||
} else if file.UserID != primitive.NilObjectID {
|
||||
if file.UserID != userID {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Access denied"})
|
||||
}
|
||||
} else {
|
||||
if _, err := getProjectRole(ctx, file.ProjectID, userID); err != nil {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Access denied"})
|
||||
}
|
||||
}
|
||||
|
||||
if file.Type == "folder" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot download a folder"})
|
||||
}
|
||||
|
||||
diskPath := filepath.Join("../data", file.StorageURL)
|
||||
if _, err := os.Stat(diskPath); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "File not found on disk"})
|
||||
}
|
||||
|
||||
c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, file.Name))
|
||||
return c.SendFile(diskPath)
|
||||
}
|
||||
|
||||
func DeleteFile(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"})
|
||||
}
|
||||
|
||||
fileID, err := primitive.ObjectIDFromHex(c.Params("fileId"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid file ID"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var file models.File
|
||||
if err := database.GetCollection("files").FindOne(ctx, bson.M{"_id": fileID}).Decode(&file); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "File not found"})
|
||||
}
|
||||
|
||||
var roleFlags int
|
||||
if file.TeamID != primitive.NilObjectID {
|
||||
roleFlags, err = getTeamRole(ctx, file.TeamID, userID)
|
||||
} else if file.UserID != primitive.NilObjectID {
|
||||
if file.UserID == userID {
|
||||
roleFlags = RoleOwner
|
||||
} else {
|
||||
err = fmt.Errorf("access denied")
|
||||
}
|
||||
} else {
|
||||
roleFlags, err = getProjectRole(ctx, file.ProjectID, userID)
|
||||
}
|
||||
if err != nil || !hasPermission(roleFlags, RoleEditor) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
|
||||
}
|
||||
|
||||
database.GetCollection("files").DeleteOne(ctx, bson.M{"_id": fileID})
|
||||
|
||||
if file.Type == "folder" {
|
||||
database.GetCollection("files").DeleteMany(ctx, bson.M{"parent_id": fileID})
|
||||
} else if file.StorageURL != "" {
|
||||
os.Remove(filepath.Join("../data", file.StorageURL))
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"message": "Deleted"})
|
||||
}
|
||||
111
server/internal/handlers/notifications.go
Normal file
111
server/internal/handlers/notifications.go
Normal file
@@ -0,0 +1,111 @@
|
||||
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 createNotification(ctx context.Context, userID primitive.ObjectID, notifType, message string, projectID primitive.ObjectID, cardID primitive.ObjectID) {
|
||||
n := &models.Notification{
|
||||
ID: primitive.NewObjectID(),
|
||||
UserID: userID,
|
||||
Type: notifType,
|
||||
Message: message,
|
||||
ProjectID: projectID,
|
||||
CardID: cardID,
|
||||
Read: false,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
database.GetCollection("notifications").InsertOne(ctx, n)
|
||||
}
|
||||
|
||||
func ListNotifications(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()
|
||||
|
||||
filter := bson.M{"user_id": userID}
|
||||
if c.Query("read") == "false" {
|
||||
filter["read"] = false
|
||||
}
|
||||
|
||||
cursor, err := database.GetCollection("notifications").Find(ctx, filter,
|
||||
options.Find().SetSort(bson.M{"created_at": -1}))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to fetch notifications"})
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var notifications []models.Notification
|
||||
cursor.All(ctx, ¬ifications)
|
||||
if notifications == nil {
|
||||
notifications = []models.Notification{}
|
||||
}
|
||||
return c.JSON(notifications)
|
||||
}
|
||||
|
||||
func MarkNotificationRead(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"})
|
||||
}
|
||||
|
||||
notifID, err := primitive.ObjectIDFromHex(c.Params("notifId"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid notification ID"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
database.GetCollection("notifications").UpdateOne(ctx,
|
||||
bson.M{"_id": notifID, "user_id": userID},
|
||||
bson.M{"$set": bson.M{"read": true}},
|
||||
)
|
||||
return c.JSON(fiber.Map{"message": "Marked as read"})
|
||||
}
|
||||
|
||||
func MarkAllNotificationsRead(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()
|
||||
|
||||
database.GetCollection("notifications").UpdateMany(ctx,
|
||||
bson.M{"user_id": userID, "read": false},
|
||||
bson.M{"$set": bson.M{"read": true}},
|
||||
)
|
||||
return c.JSON(fiber.Map{"message": "All notifications marked as read"})
|
||||
}
|
||||
|
||||
func DeleteNotification(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"})
|
||||
}
|
||||
|
||||
notifID, err := primitive.ObjectIDFromHex(c.Params("notifId"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid notification ID"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
database.GetCollection("notifications").DeleteOne(ctx, bson.M{"_id": notifID, "user_id": userID})
|
||||
return c.JSON(fiber.Map{"message": "Notification deleted"})
|
||||
}
|
||||
633
server/internal/handlers/projects.go
Normal file
633
server/internal/handlers/projects.go
Normal file
@@ -0,0 +1,633 @@
|
||||
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"})
|
||||
}
|
||||
602
server/internal/handlers/teams.go
Normal file
602
server/internal/handlers/teams.go
Normal file
@@ -0,0 +1,602 @@
|
||||
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")
|
||||
}
|
||||
230
server/internal/handlers/users.go
Normal file
230
server/internal/handlers/users.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func GetMe(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()
|
||||
|
||||
var user models.User
|
||||
if err := database.GetCollection("users").FindOne(ctx, bson.M{"_id": userID}).Decode(&user); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not found"})
|
||||
}
|
||||
|
||||
return c.JSON(user)
|
||||
}
|
||||
|
||||
func UpdateMe(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"`
|
||||
Email string `json:"email"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
}
|
||||
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()
|
||||
|
||||
update := bson.M{"updated_at": time.Now()}
|
||||
if body.Name != "" {
|
||||
update["name"] = body.Name
|
||||
}
|
||||
if body.Email != "" {
|
||||
update["email"] = body.Email
|
||||
}
|
||||
if body.AvatarURL != "" {
|
||||
update["avatar_url"] = body.AvatarURL
|
||||
}
|
||||
|
||||
col := database.GetCollection("users")
|
||||
if _, err := col.UpdateOne(ctx, bson.M{"_id": userID}, bson.M{"$set": update}); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to update user"})
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := col.FindOne(ctx, bson.M{"_id": userID}).Decode(&user); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to fetch updated user"})
|
||||
}
|
||||
|
||||
return c.JSON(user)
|
||||
}
|
||||
|
||||
func SearchUsers(c *fiber.Ctx) error {
|
||||
q := c.Query("q")
|
||||
if len(q) < 1 {
|
||||
return c.JSON([]fiber.Map{})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
filter := bson.M{
|
||||
"$or": bson.A{
|
||||
bson.M{"name": bson.M{"$regex": q, "$options": "i"}},
|
||||
bson.M{"email": bson.M{"$regex": q, "$options": "i"}},
|
||||
},
|
||||
}
|
||||
|
||||
cursor, err := database.GetCollection("users").Find(ctx, filter, options.Find().SetLimit(10))
|
||||
if err != nil {
|
||||
return c.JSON([]fiber.Map{})
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var users []models.User
|
||||
cursor.All(ctx, &users)
|
||||
|
||||
type UserResult struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
result := []UserResult{}
|
||||
for _, u := range users {
|
||||
result = append(result, UserResult{ID: u.ID.Hex(), Name: u.Name, Email: u.Email})
|
||||
}
|
||||
return c.JSON(result)
|
||||
}
|
||||
|
||||
func UploadUserAvatar(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"})
|
||||
}
|
||||
|
||||
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/users", userID.Hex())
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
log.Printf("UploadUserAvatar MkdirAll error: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create storage directory"})
|
||||
}
|
||||
|
||||
existingGlob := filepath.Join(dir, "avatar.*")
|
||||
if matches, _ := filepath.Glob(existingGlob); len(matches) > 0 {
|
||||
for _, m := range matches {
|
||||
os.Remove(m)
|
||||
}
|
||||
}
|
||||
|
||||
destPath := filepath.Join(dir, "avatar"+ext)
|
||||
if err := c.SaveFile(fh, destPath); err != nil {
|
||||
log.Printf("UploadUserAvatar SaveFile error: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to save image"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
col := database.GetCollection("users")
|
||||
if _, err := col.UpdateOne(ctx, bson.M{"_id": userID}, bson.M{"$set": bson.M{
|
||||
"avatar_url": "/api/users/me/avatar",
|
||||
"updated_at": time.Now(),
|
||||
}}); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to update user"})
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := col.FindOne(ctx, bson.M{"_id": userID}).Decode(&user); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to fetch updated user"})
|
||||
}
|
||||
|
||||
return c.JSON(user)
|
||||
}
|
||||
|
||||
func ServeUserAvatar(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"})
|
||||
}
|
||||
|
||||
dir := filepath.Join("../data/users", userID.Hex())
|
||||
for ext := range allowedImageExts {
|
||||
p := filepath.Join(dir, "avatar"+ext)
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return c.SendFile(p)
|
||||
}
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Avatar not found"})
|
||||
}
|
||||
|
||||
func ChangePassword(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 {
|
||||
CurrentPassword string `json:"current_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
||||
}
|
||||
if body.CurrentPassword == "" || body.NewPassword == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "current_password and new_password are required"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
col := database.GetCollection("users")
|
||||
var user models.User
|
||||
if err := col.FindOne(ctx, bson.M{"_id": userID}).Decode(&user); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not found"})
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(body.CurrentPassword)); err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Current password is incorrect"})
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(body.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to hash password"})
|
||||
}
|
||||
|
||||
if _, err := col.UpdateOne(ctx, bson.M{"_id": userID}, bson.M{"$set": bson.M{
|
||||
"password_hash": string(hash),
|
||||
"updated_at": time.Now(),
|
||||
}}); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to update password"})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"message": "Password updated successfully"})
|
||||
}
|
||||
220
server/internal/handlers/webhooks.go
Normal file
220
server/internal/handlers/webhooks.go
Normal file
@@ -0,0 +1,220 @@
|
||||
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 ListWebhooks(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("webhooks").Find(ctx,
|
||||
bson.M{"project_id": projectID},
|
||||
options.Find().SetSort(bson.M{"created_at": -1}))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to fetch webhooks"})
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var webhooks []models.Webhook
|
||||
cursor.All(ctx, &webhooks)
|
||||
if webhooks == nil {
|
||||
webhooks = []models.Webhook{}
|
||||
}
|
||||
return c.JSON(webhooks)
|
||||
}
|
||||
|
||||
func CreateWebhook(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"})
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
URL string `json:"url"`
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil || body.Name == "" || body.URL == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name and url are required"})
|
||||
}
|
||||
|
||||
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"})
|
||||
}
|
||||
|
||||
wType := body.Type
|
||||
if wType == "" {
|
||||
wType = "custom"
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
webhook := &models.Webhook{
|
||||
ID: primitive.NewObjectID(),
|
||||
ProjectID: projectID,
|
||||
Name: body.Name,
|
||||
Type: wType,
|
||||
URL: body.URL,
|
||||
Status: "active",
|
||||
CreatedBy: userID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if body.Secret != "" {
|
||||
webhook.SecretHash = body.Secret
|
||||
}
|
||||
|
||||
database.GetCollection("webhooks").InsertOne(ctx, webhook)
|
||||
return c.Status(fiber.StatusCreated).JSON(webhook)
|
||||
}
|
||||
|
||||
func UpdateWebhook(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"})
|
||||
}
|
||||
|
||||
webhookID, err := primitive.ObjectIDFromHex(c.Params("webhookId"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid webhook ID"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var wh models.Webhook
|
||||
if err := database.GetCollection("webhooks").FindOne(ctx, bson.M{"_id": webhookID}).Decode(&wh); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Webhook not found"})
|
||||
}
|
||||
|
||||
roleFlags, err := getProjectRole(ctx, wh.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"`
|
||||
URL string `json:"url"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
c.BodyParser(&body)
|
||||
|
||||
update := bson.M{"updated_at": time.Now()}
|
||||
if body.Name != "" {
|
||||
update["name"] = body.Name
|
||||
}
|
||||
if body.URL != "" {
|
||||
update["url"] = body.URL
|
||||
}
|
||||
if body.Type != "" {
|
||||
update["type"] = body.Type
|
||||
}
|
||||
|
||||
col := database.GetCollection("webhooks")
|
||||
col.UpdateOne(ctx, bson.M{"_id": webhookID}, bson.M{"$set": update})
|
||||
|
||||
var updated models.Webhook
|
||||
col.FindOne(ctx, bson.M{"_id": webhookID}).Decode(&updated)
|
||||
return c.JSON(updated)
|
||||
}
|
||||
|
||||
func ToggleWebhook(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"})
|
||||
}
|
||||
|
||||
webhookID, err := primitive.ObjectIDFromHex(c.Params("webhookId"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid webhook ID"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var wh models.Webhook
|
||||
if err := database.GetCollection("webhooks").FindOne(ctx, bson.M{"_id": webhookID}).Decode(&wh); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Webhook not found"})
|
||||
}
|
||||
|
||||
roleFlags, err := getProjectRole(ctx, wh.ProjectID, userID)
|
||||
if err != nil || !hasPermission(roleFlags, RoleAdmin) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
|
||||
}
|
||||
|
||||
newStatus := "active"
|
||||
if wh.Status == "active" {
|
||||
newStatus = "inactive"
|
||||
}
|
||||
|
||||
col := database.GetCollection("webhooks")
|
||||
col.UpdateOne(ctx, bson.M{"_id": webhookID}, bson.M{"$set": bson.M{
|
||||
"status": newStatus,
|
||||
"updated_at": time.Now(),
|
||||
}})
|
||||
|
||||
var updated models.Webhook
|
||||
col.FindOne(ctx, bson.M{"_id": webhookID}).Decode(&updated)
|
||||
return c.JSON(updated)
|
||||
}
|
||||
|
||||
func DeleteWebhook(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"})
|
||||
}
|
||||
|
||||
webhookID, err := primitive.ObjectIDFromHex(c.Params("webhookId"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid webhook ID"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var wh models.Webhook
|
||||
if err := database.GetCollection("webhooks").FindOne(ctx, bson.M{"_id": webhookID}).Decode(&wh); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Webhook not found"})
|
||||
}
|
||||
|
||||
roleFlags, err := getProjectRole(ctx, wh.ProjectID, userID)
|
||||
if err != nil || !hasPermission(roleFlags, RoleAdmin) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
|
||||
}
|
||||
|
||||
database.GetCollection("webhooks").DeleteOne(ctx, bson.M{"_id": webhookID})
|
||||
return c.JSON(fiber.Map{"message": "Webhook deleted"})
|
||||
}
|
||||
96
server/internal/handlers/whiteboard.go
Normal file
96
server/internal/handlers/whiteboard.go
Normal file
@@ -0,0 +1,96 @@
|
||||
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"
|
||||
)
|
||||
|
||||
func GetWhiteboard(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"})
|
||||
}
|
||||
|
||||
var wb models.Whiteboard
|
||||
err = database.GetCollection("whiteboards").FindOne(ctx, bson.M{"project_id": projectID}).Decode(&wb)
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return c.JSON(fiber.Map{"id": nil, "project_id": projectID, "data": "", "updated_at": nil})
|
||||
}
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to fetch whiteboard"})
|
||||
}
|
||||
|
||||
return c.JSON(wb)
|
||||
}
|
||||
|
||||
func SaveWhiteboard(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"})
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Data string `json:"data"`
|
||||
}
|
||||
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 := getProjectRole(ctx, projectID, userID)
|
||||
if err != nil || !hasPermission(roleFlags, RoleEditor) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Insufficient permissions"})
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
col := database.GetCollection("whiteboards")
|
||||
|
||||
var existing models.Whiteboard
|
||||
err = col.FindOne(ctx, bson.M{"project_id": projectID}).Decode(&existing)
|
||||
|
||||
if err == mongo.ErrNoDocuments {
|
||||
wb := &models.Whiteboard{
|
||||
ID: primitive.NewObjectID(),
|
||||
ProjectID: projectID,
|
||||
Data: body.Data,
|
||||
CreatedBy: userID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
col.InsertOne(ctx, wb)
|
||||
return c.JSON(fiber.Map{"id": wb.ID, "project_id": projectID, "updated_at": now})
|
||||
}
|
||||
|
||||
col.UpdateOne(ctx, bson.M{"project_id": projectID}, bson.M{"$set": bson.M{
|
||||
"data": body.Data,
|
||||
"updated_at": now,
|
||||
}})
|
||||
|
||||
return c.JSON(fiber.Map{"id": existing.ID, "project_id": projectID, "updated_at": now})
|
||||
}
|
||||
164
server/internal/handlers/ws.go
Normal file
164
server/internal/handlers/ws.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/gofiber/websocket/v2"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type wsMessage struct {
|
||||
Type string `json:"type"`
|
||||
Payload json.RawMessage `json:"payload,omitempty"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
X float64 `json:"x,omitempty"`
|
||||
Y float64 `json:"y,omitempty"`
|
||||
}
|
||||
|
||||
type wsClient struct {
|
||||
conn *websocket.Conn
|
||||
userID string
|
||||
name string
|
||||
}
|
||||
|
||||
type whiteboardRoom struct {
|
||||
clients map[*websocket.Conn]*wsClient
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var wsRooms = struct {
|
||||
m map[string]*whiteboardRoom
|
||||
mu sync.RWMutex
|
||||
}{m: make(map[string]*whiteboardRoom)}
|
||||
|
||||
func getRoom(boardID string) *whiteboardRoom {
|
||||
wsRooms.mu.Lock()
|
||||
defer wsRooms.mu.Unlock()
|
||||
if room, ok := wsRooms.m[boardID]; ok {
|
||||
return room
|
||||
}
|
||||
room := &whiteboardRoom{clients: make(map[*websocket.Conn]*wsClient)}
|
||||
wsRooms.m[boardID] = room
|
||||
return room
|
||||
}
|
||||
|
||||
func (r *whiteboardRoom) broadcast(sender *websocket.Conn, msg []byte) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
for conn := range r.clients {
|
||||
if conn != sender {
|
||||
_ = conn.WriteMessage(websocket.TextMessage, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *whiteboardRoom) userList() []map[string]string {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
list := make([]map[string]string, 0, len(r.clients))
|
||||
for _, c := range r.clients {
|
||||
list = append(list, map[string]string{"user_id": c.userID, "name": c.name})
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func parseWSToken(tokenStr string) (userID string, email string, ok bool) {
|
||||
secret := os.Getenv("JWT_SECRET")
|
||||
if secret == "" {
|
||||
secret = "changeme-jwt-secret"
|
||||
}
|
||||
|
||||
type claims struct {
|
||||
UserID string `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
c := &claims{}
|
||||
token, err := jwt.ParseWithClaims(tokenStr, c, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, jwt.ErrSignatureInvalid
|
||||
}
|
||||
return []byte(secret), nil
|
||||
})
|
||||
if err != nil || !token.Valid {
|
||||
return "", "", false
|
||||
}
|
||||
return c.UserID, c.Email, true
|
||||
}
|
||||
|
||||
func WhiteboardWS(c *websocket.Conn) {
|
||||
boardID := c.Params("id")
|
||||
tokenStr := c.Query("token", "")
|
||||
userName := c.Query("name", "Anonymous")
|
||||
|
||||
userID, _, ok := parseWSToken(tokenStr)
|
||||
if !ok {
|
||||
_ = c.WriteJSON(map[string]string{"type": "error", "payload": "unauthorized"})
|
||||
_ = c.Close()
|
||||
return
|
||||
}
|
||||
|
||||
room := getRoom(boardID)
|
||||
|
||||
client := &wsClient{conn: c, userID: userID, name: userName}
|
||||
room.mu.Lock()
|
||||
room.clients[c] = client
|
||||
room.mu.Unlock()
|
||||
|
||||
joinMsg, _ := json.Marshal(map[string]interface{}{
|
||||
"type": "join",
|
||||
"user_id": userID,
|
||||
"name": userName,
|
||||
"users": room.userList(),
|
||||
})
|
||||
room.broadcast(nil, joinMsg)
|
||||
|
||||
selfMsg, _ := json.Marshal(map[string]interface{}{
|
||||
"type": "users",
|
||||
"users": room.userList(),
|
||||
})
|
||||
_ = c.WriteMessage(websocket.TextMessage, selfMsg)
|
||||
|
||||
defer func() {
|
||||
room.mu.Lock()
|
||||
delete(room.clients, c)
|
||||
empty := len(room.clients) == 0
|
||||
room.mu.Unlock()
|
||||
|
||||
leaveMsg, _ := json.Marshal(map[string]interface{}{
|
||||
"type": "leave",
|
||||
"user_id": userID,
|
||||
"name": userName,
|
||||
"users": room.userList(),
|
||||
})
|
||||
room.broadcast(nil, leaveMsg)
|
||||
|
||||
if empty {
|
||||
wsRooms.mu.Lock()
|
||||
delete(wsRooms.m, boardID)
|
||||
wsRooms.mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
_, msg, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
|
||||
log.Printf("WS error board=%s user=%s: %v", boardID, userID, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
var incoming map[string]interface{}
|
||||
if json.Unmarshal(msg, &incoming) == nil {
|
||||
incoming["user_id"] = userID
|
||||
incoming["name"] = userName
|
||||
outMsg, _ := json.Marshal(incoming)
|
||||
room.broadcast(c, outMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user