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"}) }