200 lines
6.0 KiB
Go
200 lines
6.0 KiB
Go
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"})
|
|
}
|