package controllers import ( "encoding/json" "errors" "fmt" configs "goFiber/config" database "goFiber/database/config" "goFiber/database/models" "goFiber/middlewares" utils "goFiber/pkg/utis" "goFiber/services" "log" "net/http" "net/url" "os" "path/filepath" "strconv" "strings" "time" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v3" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) var validate = validator.New() type RegisterRequest struct { UserName string `json:"username" validate:"required,min=3"` Email string `json:"email" validate:"required,email"` Password string `json:"password" validate:"required,min=6"` FirstName string `json:"first_name" validate:"required"` LastName string `json:"last_name" validate:"required"` } type LoginRequest struct { Email string `json:"email" validate:"required,email"` Password string `json:"password" validate:"required"` } type RefreshRequest struct { RefreshToken string `json:"refresh_token" validate:"required"` } type ResendVerificationRequest struct { Email string `json:"email" validate:"required,email"` } // UpdateUserRequest represents allowed fields for updating a user type UpdateUserRequest struct { UserName string `json:"username,omitempty" example:"jdoe"` Email string `json:"email,omitempty" example:"jdoe@example.com"` IsAdmin *bool `json:"is_admin,omitempty" example:"false"` Password string `json:"password,omitempty" example:"#secret"` FirstName string `json:"first_name,omitempty" example:"John"` LastName string `json:"last_name,omitempty" example:"Doe"` AvatarURL string `json:"avatar_url,omitempty" example:"/uploads/avatar.jpg"` EmailVerified *bool `json:"email_verified,omitempty" example:"true"` // Accept avatar file via multipart/form-data with field name "avatar" when using form upload } func GetUser(c fiber.Ctx) error { return c.Status(fiber.StatusOK).SendString("Get User") } // AdminListUsers godoc // @Summary List active users (admin only) // @Tags Users // @Produce json // @Security BearerAuth // @Success 200 {object} map[string]interface{} // @Failure 401 {object} map[string]string // @Failure 403 {object} map[string]string // @Router /api/v1/users/list [get] func AdminListUsers(c fiber.Ctx) error { if database.DB == nil { return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"}) } var users []models.User if err := database.DB.Preload("Profile").Order("id DESC").Find(&users).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } return c.JSON(fiber.Map{ "count": len(users), "users": users, }) } // AdminListDeletedUsers godoc // @Summary List soft-deleted users (admin only) // @Tags Users // @Produce json // @Security BearerAuth // @Success 200 {object} map[string]interface{} // @Failure 401 {object} map[string]string // @Failure 403 {object} map[string]string // @Router /api/v1/users/list/deleted [get] func AdminListDeletedUsers(c fiber.Ctx) error { if database.DB == nil { return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"}) } var users []models.User if err := database.DB.Unscoped(). Preload("Profile"). Where("deleted_at IS NOT NULL"). Order("deleted_at DESC"). Find(&users).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } return c.JSON(fiber.Map{ "count": len(users), "users": users, }) } func GetUserOne(c fiber.Ctx) error { return c.Status(fiber.StatusOK).SendString("Get User One") } // UpdateUser godoc // @Summary Update user (admin only) // @Tags Users // @Accept mpfd // @Produce json // @Security BearerAuth // @Param id path int true "User ID" // @Param username formData string false "Username" // @Param email formData string false "Email" // @Param is_admin formData boolean false "Is Admin" // @Param password formData string false "Password" // @Param first_name formData string false "First Name" // @Param last_name formData string false "Last Name" // @Param email_verified formData boolean false "Email Verified" // @Param avatar formData file false "Avatar Image" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Failure 403 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /api/v1/users/{id} [put] func UpdateUser(c fiber.Ctx) error { if database.DB == nil { return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"}) } id, err := strconv.ParseUint(c.Params("id"), 10, 64) if err != nil || id == 0 { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid user id"}) } var user models.User if err := database.DB.Preload("Profile").First(&user, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "user not found"}) } return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } // Parse incoming JSON or multipart/form-data into map to allow partial updates including false values var payload map[string]interface{} // Prefer detecting multipart by trying to read the multipart form first if mf, err := c.MultipartForm(); err == nil && mf != nil { payload = map[string]interface{}{} // form values for k, vals := range mf.Value { if len(vals) > 0 { payload[k] = vals[0] } } // handle avatar file if present if files, ok := mf.File["avatar"]; ok && len(files) > 0 { file := files[0] if _, err := os.Stat("./uploads/avatars"); os.IsNotExist(err) { os.MkdirAll("./uploads/avatars", 0755) } filename := fmt.Sprintf("%d_%s", time.Now().Unix(), file.Filename) filePath := filepath.Join("./uploads/avatars", filename) if err := c.SaveFile(file, filePath); err == nil { payload["avatar_url"] = "/uploads/avatars/" + filename } else { log.Printf("failed to save avatar: %v", err) } } } else { // fallback to JSON body if err := json.Unmarshal(c.Body(), &payload); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } } // Prepare updates for user table userUpdates := map[string]interface{}{} if v, ok := payload["username"].(string); ok { userUpdates["user_name"] = v userUpdates["user_name"] = v user.UserName = v } if v, ok := payload["email"].(string); ok { userUpdates["email"] = v user.Email = v } if v, ok := payload["is_admin"]; ok { // handle bool or string representations switch val := v.(type) { case bool: userUpdates["is_admin"] = val user.IsAdmin = &val case string: if parsed, err := strconv.ParseBool(val); err == nil { userUpdates["is_admin"] = parsed user.IsAdmin = &parsed } } } // Handle email_verified explicitly (bool or string) if v, ok := payload["email_verified"]; ok { switch val := v.(type) { case bool: userUpdates["email_verified"] = val now := time.Now() if val { userUpdates["email_verified_at"] = now user.EmailVerified = &val user.EmailVerifiedAt = &now } else { userUpdates["email_verified_at"] = nil user.EmailVerified = &val user.EmailVerifiedAt = nil } case string: if parsed, err := strconv.ParseBool(val); err == nil { userUpdates["email_verified"] = parsed now := time.Now() if parsed { userUpdates["email_verified_at"] = now user.EmailVerified = &parsed user.EmailVerifiedAt = &now } else { userUpdates["email_verified_at"] = nil user.EmailVerified = &parsed user.EmailVerifiedAt = nil } } } } if v, ok := payload["password"].(string); ok && v != "" { hashed, err := bcrypt.GenerateFromPassword([]byte(v), bcrypt.DefaultCost) if err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not hash password"}) } userUpdates["password"] = string(hashed) user.Password = string(hashed) } // Apply user updates if any if len(userUpdates) > 0 { if err := database.DB.Model(&user).Updates(userUpdates).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "user could not be updated"}) } } // Handle profile updates (first_name, last_name, avatar_url) profileUpdates := map[string]interface{}{} if v, ok := payload["first_name"].(string); ok { profileUpdates["first_name"] = v } if v, ok := payload["last_name"].(string); ok { profileUpdates["last_name"] = v } if v, ok := payload["avatar_url"].(string); ok { profileUpdates["avatar_url"] = v } if len(profileUpdates) > 0 { // Profile may be stored as slice; update first profile if exists else create var profile models.Profile if len(user.Profile) > 0 { profile = user.Profile[0] if err := database.DB.Model(&profile).Updates(profileUpdates).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "profile could not be updated"}) } } else { profile = models.Profile{ UserID: uint64(user.ID), } if v, ok := profileUpdates["first_name"].(string); ok { profile.FirstName = v } if v, ok := profileUpdates["last_name"].(string); ok { profile.LastName = v } if v, ok := profileUpdates["avatar_url"].(string); ok { profile.AvatarURL = v } if err := database.DB.Create(&profile).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "profile could not be created"}) } } } // Reload user with profile if err := database.DB.Preload("Profile").First(&user, id).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } return c.JSON(fiber.Map{"message": "user updated", "user": user}) } // DeleteUser godoc // @Summary Soft delete user (admin only) // @Tags Users // @Produce json // @Security BearerAuth // @Param id path int true "User ID" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Failure 403 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /api/v1/users/{id} [delete] func DeleteUser(c fiber.Ctx) error { if database.DB == nil { return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"}) } id, err := strconv.ParseUint(c.Params("id"), 10, 64) if err != nil || id == 0 { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid user id"}) } var user models.User if err := database.DB.First(&user, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "user not found"}) } return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } if err := database.DB.Delete(&user).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "user could not be deleted"}) } return c.JSON(fiber.Map{ "message": "user soft-deleted successfully", "user_id": id, }) } // HardDeleteUser godoc // @Summary Hard delete user permanently (admin only) // @Tags Users // @Produce json // @Security BearerAuth // @Param id path int true "User ID" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Failure 403 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /api/v1/users/{id}/hard [delete] func HardDeleteUser(c fiber.Ctx) error { if database.DB == nil { return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"}) } id, err := strconv.ParseUint(c.Params("id"), 10, 64) if err != nil || id == 0 { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid user id"}) } var user models.User if err := database.DB.Unscoped().First(&user, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "user not found"}) } return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } err = database.DB.Transaction(func(tx *gorm.DB) error { if err := tx.Unscoped().Where("user_id = ?", id).Delete(&models.Profile{}).Error; err != nil { return err } if err := tx.Unscoped().Where("user_id = ?", id).Delete(&models.SocialAccount{}).Error; err != nil { return err } return tx.Unscoped().Delete(&models.User{}, id).Error }) if err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "user hard-delete failed"}) } return c.JSON(fiber.Map{ "message": "user permanently deleted", "user_id": id, }) } // RestoreUser godoc // @Summary Restore soft-deleted user (admin only) // @Tags Users // @Produce json // @Security BearerAuth // @Param id path int true "User ID" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Failure 403 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /api/v1/users/{id}/restore [post] func RestoreUser(c fiber.Ctx) error { if database.DB == nil { return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"}) } id, err := strconv.ParseUint(c.Params("id"), 10, 64) if err != nil || id == 0 { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid user id"}) } var user models.User if err := database.DB.Unscoped().First(&user, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "user not found"}) } return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } if !user.DeletedAt.Valid { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "user is not soft-deleted"}) } if err := database.DB.Unscoped().Model(&models.User{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "user could not be restored"}) } return c.JSON(fiber.Map{ "message": "user restored successfully", "user_id": id, }) } // Register godoc // @Summary Register user // @Tags Auth // @Accept json // @Produce json // @Param request body RegisterRequest true "Register payload" // @Success 201 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 409 {object} map[string]string // @Router /api/v1/auth/register [post] func Register(c fiber.Ctx) error { if database.DB == nil { return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"}) } var req RegisterRequest if err := c.Bind().JSON(&req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } if err := validate.Struct(req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "password could not be hashed"}) } verifyToken, err := utils.GenerateSecureToken(32) if err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "verify token could not be generated"}) } user := models.User{ UserName: req.UserName, Email: req.Email, Password: string(hashedPassword), EmailVerifyToken: verifyToken, } if err := database.DB.Create(&user).Error; err != nil { return c.Status(http.StatusConflict).JSON(fiber.Map{"error": "email already exists"}) } profile := models.Profile{ UserID: uint64(user.ID), FirstName: req.FirstName, LastName: req.LastName, } if err := database.DB.Create(&profile).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "profile could not be created"}) } appURL := strings.TrimRight(configs.AppConfig.AppURL, "/") verifyURL := fmt.Sprintf("%s/api/v1/auth/verify-email?token=%s", appURL, url.QueryEscape(verifyToken)) emailService := services.NewEmailService() err = emailService.SendVerificationEmail(user.Email, profile.FirstName, verifyURL) if err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "verification email could not be sent"}) } return c.Status(http.StatusCreated).JSON(fiber.Map{ "message": "registration successful, please verify your email before login", "user": fiber.Map{ "id": user.ID, "username": user.UserName, "email": user.Email, "is_admin": boolPtrValue(user.IsAdmin), "email_verified": false, "first_name": profile.FirstName, "last_name": profile.LastName, }, }) } // Login godoc // @Summary Login user // @Tags Auth // @Accept json // @Produce json // @Param request body LoginRequest true "Login payload" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Router /api/v1/auth/login [post] func Login(c fiber.Ctx) error { if database.DB == nil { return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"}) } var req LoginRequest if err := c.Bind().JSON(&req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } if err := validate.Struct(req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } var user models.User if err := database.DB.Preload("Profile").Where("email = ?", req.Email).First(&user).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "invalid email or password"}) } return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "invalid email or password"}) } if !user.IsEmailVerified() { return c.Status(http.StatusForbidden).JSON(fiber.Map{"error": "please verify your email before login"}) } firstName, lastName := extractProfileName(user.Profile) jwtService := services.NewJWTService() accessToken, refreshToken, err := jwtService.GenerateTokenPair( user.ID, user.Email, boolPtrValue(user.IsAdmin), firstName, lastName, ) if err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "tokens could not be generated"}) } return c.JSON(fiber.Map{ "user": fiber.Map{ "id": user.ID, "username": user.UserName, "email": user.Email, "is_admin": boolPtrValue(user.IsAdmin), "first_name": firstName, "last_name": lastName, }, "access_token": accessToken, "refresh_token": refreshToken, }) } // RefreshToken godoc // @Summary Refresh access token // @Tags Auth // @Accept json // @Produce json // @Param request body RefreshRequest true "Refresh payload" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Router /api/v1/auth/refresh [post] func RefreshToken(c fiber.Ctx) error { if database.DB == nil { return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"}) } var req RefreshRequest if err := c.Bind().JSON(&req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } if err := validate.Struct(req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } jwtService := services.NewJWTService() claims, err := jwtService.ValidateToken(req.RefreshToken) if err != nil || claims.TokenType != services.TokenTypeRefresh { return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "invalid refresh token"}) } var user models.User if err := database.DB.Preload("Profile").First(&user, claims.UserID).Error; err != nil { return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "user not found"}) } firstName, lastName := extractProfileName(user.Profile) accessToken, refreshToken, err := jwtService.GenerateTokenPair( user.ID, user.Email, boolPtrValue(user.IsAdmin), firstName, lastName, ) if err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "tokens could not be generated"}) } fmt.Println(accessToken, "Access Token Yenilendi !!!") return c.JSON(fiber.Map{ "access_token": accessToken, "refresh_token": refreshToken, }) } // VerifyEmail godoc // @Summary Verify email address with token // @Tags Auth // @Produce json // @Param token query string true "Email verify token" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /api/v1/auth/verify-email [get] func VerifyEmail(c fiber.Ctx) error { if database.DB == nil { return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"}) } token := strings.TrimSpace(c.Query("token")) if token == "" { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "token is required"}) } var user models.User if err := database.DB.Where("email_verify_token = ?", token).First(&user).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "invalid or expired token"}) } return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } now := time.Now() isVerified := true user.EmailVerified = &isVerified user.EmailVerifiedAt = &now user.EmailVerifyToken = "" if err := database.DB.Save(&user).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "email verification could not be saved"}) } return c.JSON(fiber.Map{"message": "email verified successfully"}) } // ResendVerificationEmail godoc // @Summary Resend verification email // @Tags Auth // @Accept json // @Produce json // @Param request body ResendVerificationRequest true "Resend verification payload" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /api/v1/auth/resend-verification [post] func ResendVerificationEmail(c fiber.Ctx) error { if database.DB == nil { return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"}) } var req ResendVerificationRequest if err := c.Bind().JSON(&req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } if err := validate.Struct(req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } var user models.User if err := database.DB.Preload("Profile").Where("email = ?", req.Email).First(&user).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "user not found"}) } return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } if user.IsEmailVerified() { return c.JSON(fiber.Map{"message": "email is already verified"}) } verifyToken, err := utils.GenerateSecureToken(32) if err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "verify token could not be generated"}) } user.EmailVerifyToken = verifyToken if err := database.DB.Save(&user).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "verification token could not be saved"}) } firstName, _ := extractProfileName(user.Profile) appURL := strings.TrimRight(configs.AppConfig.AppURL, "/") verifyURL := fmt.Sprintf("%s/api/v1/auth/verify-email?token=%s", appURL, url.QueryEscape(verifyToken)) emailService := services.NewEmailService() if err := emailService.SendVerificationEmail(user.Email, firstName, verifyURL); err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "verification email could not be sent"}) } return c.JSON(fiber.Map{"message": "verification email has been sent"}) } // Me godoc // @Summary Get current user from token // @Tags Auth // @Produce json // @Security BearerAuth // @Success 200 {object} map[string]interface{} // @Failure 401 {object} map[string]string // @Router /api/v1/auth/me [get] func Me(c fiber.Ctx) error { claims, ok := middlewares.GetAuthClaims(c) if !ok { return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"}) } return c.JSON(fiber.Map{ "user": fiber.Map{ "id": claims.UserID, "email": claims.Email, "is_admin": claims.IsAdmin, "first_name": claims.FirstName, "last_name": claims.LastName, }, }) } // AdminOnlyExample godoc // @Summary Admin-only sample endpoint // @Tags Auth // @Produce json // @Security BearerAuth // @Success 200 {object} map[string]string // @Failure 401 {object} map[string]string // @Failure 403 {object} map[string]string // @Router /api/v1/auth/admin/example [get] func AdminOnlyExample(c fiber.Ctx) error { claims, ok := middlewares.GetAuthClaims(c) if !ok { return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"}) } return c.JSON(fiber.Map{ "message": "only admins can access this endpoint", "user": claims.Email, }) } // UserOnlyExample godoc // @Summary Normal-user-only sample endpoint // @Tags Auth // @Produce json // @Security BearerAuth // @Success 200 {object} map[string]string // @Failure 401 {object} map[string]string // @Failure 403 {object} map[string]string // @Router /api/v1/auth/user/example [get] func UserOnlyExample(c fiber.Ctx) error { claims, ok := middlewares.GetAuthClaims(c) if !ok { return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"}) } return c.JSON(fiber.Map{ "message": "only normal users can access this endpoint", "user": claims.Email, }) } func GoogleAuth(c fiber.Ctx) error { if configs.AppConfig.GoogleClientID == "" { return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "google oauth is not configured"}) } stateToken, err := utils.GenerateSecureToken(16) if err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "state token could not be generated"}) } authURL := "https://accounts.google.com/o/oauth2/v2/auth?" + url.Values{ "client_id": []string{configs.AppConfig.GoogleClientID}, "redirect_uri": []string{configs.AppConfig.GoogleRedirectURL}, "response_type": []string{"code"}, "scope": []string{"openid email profile"}, "state": []string{stateToken}, }.Encode() return c.JSON(fiber.Map{"provider": "google", "auth_url": authURL, "state": stateToken}) } func GoogleAuthCallback(c fiber.Ctx) error { code := c.Query("code") if code == "" { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "google callback code is missing"}) } // OAuth token exchange is intentionally left simple for now. return c.JSON(fiber.Map{ "provider": "google", "message": "google callback infrastructure is ready, token exchange can be added next", "code": code, "state": c.Query("state"), }) } func GithubAuth(c fiber.Ctx) error { if configs.AppConfig.GithubClientID == "" { return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "github oauth is not configured"}) } stateToken, err := utils.GenerateSecureToken(16) if err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "state token could not be generated"}) } authURL := "https://github.com/login/oauth/authorize?" + url.Values{ "client_id": []string{configs.AppConfig.GithubClientID}, "redirect_uri": []string{configs.AppConfig.GithubRedirectURL}, "scope": []string{"read:user user:email"}, "state": []string{stateToken}, }.Encode() return c.JSON(fiber.Map{"provider": "github", "auth_url": authURL, "state": stateToken}) } func GithubAuthCallback(c fiber.Ctx) error { code := c.Query("code") if code == "" { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "github callback code is missing"}) } // OAuth token exchange is intentionally left simple for now. return c.JSON(fiber.Map{ "provider": "github", "message": "github callback infrastructure is ready, token exchange can be added next", "code": code, "state": c.Query("state"), }) } func extractProfileName(profiles []models.Profile) (string, string) { if len(profiles) == 0 { return "", "" } return profiles[0].FirstName, profiles[0].LastName } func boolPtrValue(v *bool) bool { if v == nil { return false } return *v }