package handlers import ( "errors" "log" "net/http" "strconv" "strings" "time" "ginimageApi/app/accounts/models" "ginimageApi/app/middleware" "ginimageApi/configs" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) type adminUserResponse struct { ID uint `json:"id"` Username string `json:"username"` Email string `json:"email"` EmailVerified bool `json:"email_verified"` IsActive bool `json:"is_active"` IsAdmin bool `json:"is_admin"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } type adminUserListResponse struct { Items []adminUserResponse `json:"items"` Meta paginationMeta `json:"meta"` } type paginationMeta struct { Page int `json:"page"` Limit int `json:"limit"` Total int64 `json:"total"` } type adminCreateUserRequest struct { Username string `json:"username" binding:"required,min=3"` Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=6"` ConfirmPassword string `json:"confirm_password" binding:"required,eqfield=Password"` IsAdmin *bool `json:"is_admin"` IsActive *bool `json:"is_active"` } type adminUpdateUserRequest struct { Username string `json:"username" binding:"omitempty,min=3"` Email string `json:"email" binding:"omitempty,email"` Password string `json:"password" binding:"omitempty,min=6"` IsAdmin *bool `json:"is_admin"` IsActive *bool `json:"is_active"` } type adminUserStatusRequest struct { IsActive bool `json:"is_active" binding:"required"` } type adminUpdateProfileRequest struct { FirstName string `form:"first_name" binding:"omitempty,min=2"` LastName string `form:"last_name" binding:"omitempty,min=2"` } type adminProfileResponse struct { UserID uint64 `json:"user_id"` FirstName string `json:"first_name"` LastName string `json:"last_name"` AvatarURL string `json:"avatar_url"` } type adminIssueTokenRequest struct { DurationDays int `json:"duration_days" binding:"required,min=1,max=365"` } type adminIssueTokenResponse struct { AccessToken string `json:"access"` ExpiresAt string `json:"expires_at"` } type AdminUserResponse = adminUserResponse type AdminUserListResponse = adminUserListResponse type AdminCreateUserRequest = adminCreateUserRequest type AdminUpdateUserRequest = adminUpdateUserRequest type AdminUserStatusRequest = adminUserStatusRequest type AdminUpdateProfileRequest = adminUpdateProfileRequest type AdminProfileResponse = adminProfileResponse type AdminIssueTokenRequest = adminIssueTokenRequest type AdminIssueTokenResponse = adminIssueTokenResponse func adminActorID(c *gin.Context) any { actorID, ok := c.Get("user_id") if !ok { return "unknown" } return actorID } func maskEmail(email string) string { email = strings.TrimSpace(strings.ToLower(email)) parts := strings.Split(email, "@") if len(parts) != 2 || parts[0] == "" { return "invalid-email" } local := parts[0] domain := parts[1] if len(local) <= 2 { return local[:1] + "***@" + domain } return local[:2] + "***@" + domain } func getOrCreateProfileByUserID(userID uint64) (models.Profile, error) { var profile models.Profile err := configs.DB.Where("user_id = ?", userID).First(&profile).Error if err == nil { return profile, nil } if !errors.Is(err, gorm.ErrRecordNotFound) { return models.Profile{}, err } profile = models.Profile{UserID: userID} if err := configs.DB.Create(&profile).Error; err != nil { return models.Profile{}, err } return profile, nil } // ListAdminUsers godoc // @Summary Admin kullanicilari listeler // @Tags admin-users // @Produce json // @Security BearerAuth // @Param page query int false "Sayfa numarasi" default(1) // @Param limit query int false "Sayfa boyutu (max 100)" default(10) // @Param search query string false "Kullanici adi/email arama" // @Param is_admin query bool false "Admin filtresi" // @Param is_active query bool false "Aktiflik filtresi" // @Success 200 {object} AdminUserListResponse // @Failure 401 {object} ErrorResponse // @Failure 403 {object} ErrorResponse // @Failure 500 {object} ErrorResponse // @Router /api/v1/admin/users [get] func ListAdminUsers(c *gin.Context) { page := parsePositiveIntOrDefault(c.Query("page"), 1) limit := parsePositiveIntOrDefault(c.Query("limit"), 10) if limit > 100 { limit = 100 } search := strings.TrimSpace(c.Query("search")) isAdminFilter := strings.TrimSpace(c.Query("is_admin")) isActiveFilter := strings.TrimSpace(c.Query("is_active")) query := configs.DB.Model(&models.User{}) if search != "" { like := "%" + strings.ToLower(search) + "%" query = query.Where("LOWER(user_name) LIKE ? OR LOWER(email) LIKE ?", like, like) } if v, ok := parseOptionalBool(isAdminFilter); ok { query = query.Where("is_admin = ?", v) } if v, ok := parseOptionalBool(isActiveFilter); ok { query = query.Where("is_active = ?", v) } var total int64 if err := query.Count(&total).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "kullanicilar listelenemedi"}) return } var users []models.User if err := query. Order("id DESC"). Offset((page - 1) * limit). Limit(limit). Find(&users).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "kullanicilar listelenemedi"}) return } items := make([]adminUserResponse, 0, len(users)) for _, user := range users { items = append(items, toAdminUserResponse(user)) } c.JSON(http.StatusOK, adminUserListResponse{ Items: items, Meta: paginationMeta{ Page: page, Limit: limit, Total: total, }, }) } // GetAdminUser godoc // @Summary Admin panel icin kullanici detayi getirir // @Tags admin-users // @Produce json // @Security BearerAuth // @Param id path int true "Kullanici ID" // @Success 200 {object} AdminUserResponse // @Failure 400 {object} ErrorResponse // @Failure 401 {object} ErrorResponse // @Failure 403 {object} ErrorResponse // @Failure 404 {object} ErrorResponse // @Failure 500 {object} ErrorResponse // @Router /api/v1/admin/users/{id} [get] func GetAdminUser(c *gin.Context) { userID, ok := parseUintParam(c, "id") if !ok { return } var user models.User if err := configs.DB.First(&user, userID).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "kullanici bulunamadi"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "kullanici getirilemedi"}) return } c.JSON(http.StatusOK, toAdminUserResponse(user)) } // CreateAdminUser godoc // @Summary Admin panel icin kullanici olusturur // @Tags admin-users // @Accept json // @Produce json // @Security BearerAuth // @Param request body AdminCreateUserRequest true "Kullanici olusturma verisi" // @Success 201 {object} AdminUserResponse // @Failure 400 {object} ErrorResponse // @Failure 401 {object} ErrorResponse // @Failure 403 {object} ErrorResponse // @Failure 409 {object} ErrorResponse // @Failure 500 {object} ErrorResponse // @Router /api/v1/admin/users [post] func CreateAdminUser(c *gin.Context) { log.Printf("[ADMIN-USER-CREATE] stage=start actor_id=%v ip=%s ua=%q", adminActorID(c), c.ClientIP(), c.Request.UserAgent()) var req adminCreateUserRequest if err := c.ShouldBindJSON(&req); err != nil { log.Printf("[ADMIN-USER-CREATE] stage=bind_failed actor_id=%v ip=%s error=%q", adminActorID(c), c.ClientIP(), err.Error()) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } log.Printf( "[ADMIN-USER-CREATE] stage=payload_ok actor_id=%v username=%q email=%q is_admin=%v is_active=%v", adminActorID(c), req.Username, maskEmail(req.Email), req.IsAdmin, req.IsActive, ) var exists models.User err := configs.DB.Where("email = ?", req.Email).First(&exists).Error if err == nil { log.Printf( "[ADMIN-USER-CREATE] stage=conflict actor_id=%v reason=email_exists incoming_email=%q existing_user_id=%d existing_username=%q existing_active=%v existing_admin=%v", adminActorID(c), maskEmail(req.Email), exists.ID, exists.UserName, exists.IsActive != nil && *exists.IsActive, exists.IsAdmin != nil && *exists.IsAdmin, ) c.JSON(http.StatusConflict, gin.H{ "error": "email zaten kayitli", "code": "EMAIL_ALREADY_EXISTS", }) return } if err != nil && err != gorm.ErrRecordNotFound { log.Printf("[ADMIN-USER-CREATE] stage=check_failed actor_id=%v email=%q error=%q", adminActorID(c), maskEmail(req.Email), err.Error()) c.JSON(http.StatusInternalServerError, gin.H{"error": "kullanici kontrol edilemedi"}) return } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { log.Printf("[ADMIN-USER-CREATE] stage=hash_failed actor_id=%v email=%q error=%q", adminActorID(c), maskEmail(req.Email), err.Error()) c.JSON(http.StatusInternalServerError, gin.H{"error": "sifre islenemedi"}) return } isAdmin := false if req.IsAdmin != nil { isAdmin = *req.IsAdmin } isActive := true if req.IsActive != nil { isActive = *req.IsActive } user := models.User{ UserName: req.Username, Email: req.Email, Password: string(hashedPassword), EmailVerified: boolPtr(false), IsActive: boolPtr(isActive), IsAdmin: boolPtr(isAdmin), } if err := configs.DB.Create(&user).Error; err != nil { log.Printf("[ADMIN-USER-CREATE] stage=create_failed actor_id=%v email=%q error=%q", adminActorID(c), maskEmail(req.Email), err.Error()) c.JSON(http.StatusInternalServerError, gin.H{"error": "kullanici olusturulamadi"}) return } log.Printf("[ADMIN-USER-CREATE] stage=success actor_id=%v created_user_id=%d email=%q", adminActorID(c), user.ID, maskEmail(user.Email)) c.JSON(http.StatusCreated, toAdminUserResponse(user)) } // UpdateAdminUser godoc // @Summary Admin panel icin kullaniciyi gunceller // @Tags admin-users // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "Kullanici ID" // @Param request body AdminUpdateUserRequest true "Kullanici guncelleme verisi" // @Success 200 {object} AdminUserResponse // @Failure 400 {object} ErrorResponse // @Failure 401 {object} ErrorResponse // @Failure 403 {object} ErrorResponse // @Failure 404 {object} ErrorResponse // @Failure 409 {object} ErrorResponse // @Failure 500 {object} ErrorResponse // @Router /api/v1/admin/users/{id} [put] func UpdateAdminUser(c *gin.Context) { userID, ok := parseUintParam(c, "id") if !ok { return } var req adminUpdateUserRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var user models.User if err := configs.DB.First(&user, userID).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "kullanici bulunamadi"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "kullanici getirilemedi"}) return } if req.Email != "" && req.Email != user.Email { var exists models.User err := configs.DB.Where("email = ? AND id <> ?", req.Email, user.ID).First(&exists).Error if err == nil { c.JSON(http.StatusConflict, gin.H{"error": "email zaten kayitli"}) return } if err != nil && err != gorm.ErrRecordNotFound { c.JSON(http.StatusInternalServerError, gin.H{"error": "kullanici kontrol edilemedi"}) return } user.Email = req.Email } if req.Username != "" { user.UserName = req.Username } if req.IsAdmin != nil { user.IsAdmin = boolPtr(*req.IsAdmin) } if req.IsActive != nil { user.IsActive = boolPtr(*req.IsActive) } if req.Password != "" { hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "sifre islenemedi"}) return } user.Password = string(hashedPassword) } if err := configs.DB.Save(&user).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "kullanici guncellenemedi"}) return } c.JSON(http.StatusOK, toAdminUserResponse(user)) } // UpdateAdminUserStatus godoc // @Summary Admin panel icin kullanici aktiflik durumunu gunceller // @Tags admin-users // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "Kullanici ID" // @Param request body AdminUserStatusRequest true "Durum verisi" // @Success 200 {object} AdminUserResponse // @Failure 400 {object} ErrorResponse // @Failure 401 {object} ErrorResponse // @Failure 403 {object} ErrorResponse // @Failure 404 {object} ErrorResponse // @Failure 500 {object} ErrorResponse // @Router /api/v1/admin/users/{id}/status [patch] func UpdateAdminUserStatus(c *gin.Context) { userID, ok := parseUintParam(c, "id") if !ok { return } var req adminUserStatusRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } result := configs.DB.Model(&models.User{}).Where("id = ?", userID).Update("is_active", req.IsActive) if result.Error != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "kullanici durumu guncellenemedi"}) return } if result.RowsAffected == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "kullanici bulunamadi"}) return } var user models.User if err := configs.DB.First(&user, userID).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "kullanici getirilemedi"}) return } c.JSON(http.StatusOK, toAdminUserResponse(user)) } // DeleteAdminUser godoc // @Summary Admin panel icin kullanici siler // @Tags admin-users // @Produce json // @Security BearerAuth // @Param id path int true "Kullanici ID" // @Success 200 {object} MessageResponse // @Failure 400 {object} ErrorResponse // @Failure 401 {object} ErrorResponse // @Failure 403 {object} ErrorResponse // @Failure 404 {object} ErrorResponse // @Failure 500 {object} ErrorResponse // @Router /api/v1/admin/users/{id} [delete] func DeleteAdminUser(c *gin.Context) { userID, ok := parseUintParam(c, "id") if !ok { return } result := configs.DB.Delete(&models.User{}, userID) if result.Error != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "kullanici silinemedi"}) return } if result.RowsAffected == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "kullanici bulunamadi"}) return } c.JSON(http.StatusOK, gin.H{"message": "kullanici silindi"}) } // IssueAdminScopedToken godoc // @Summary Admin için gün bazlı access token üretir // @Description Sadece admin rolü için, istekle verilen gün kadar geçerli access token üretir. Refresh token üretilmez. // @Tags admin-users // @Accept json // @Produce json // @Security BearerAuth // @Param request body AdminIssueTokenRequest true "Token süresi (gün)" // @Success 200 {object} AdminIssueTokenResponse // @Failure 400 {object} ErrorResponse // @Failure 401 {object} ErrorResponse // @Failure 403 {object} ErrorResponse // @Failure 404 {object} ErrorResponse // @Failure 500 {object} ErrorResponse // @Router /api/v1/admin/tokens/issue [post] func IssueAdminScopedToken(c *gin.Context) { var req adminIssueTokenRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } userID, err := currentUserID(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return } var user models.User if err := configs.DB.First(&user, userID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "kullanici bulunamadi"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "kullanici getirilemedi"}) return } if user.IsAdmin == nil || !*user.IsAdmin { c.JSON(http.StatusForbidden, gin.H{"error": "admin yetkisi gerekli"}) return } tokenTTL := time.Duration(req.DurationDays) * 24 * time.Hour accessToken, err := middleware.GenerateAccessToken(user.ID, user.Email, user.UserName, tokenTTL) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "token olusturulamadi"}) return } c.JSON(http.StatusOK, adminIssueTokenResponse{ AccessToken: accessToken, ExpiresAt: time.Now().Add(tokenTTL).Format(time.RFC3339), }) } // GetAdminUserProfile godoc // @Summary Admin panel icin kullanicinin profilini getirir // @Tags admin-users // @Produce json // @Security BearerAuth // @Param id path int true "Kullanici ID" // @Success 200 {object} AdminProfileResponse // @Failure 400 {object} ErrorResponse // @Failure 401 {object} ErrorResponse // @Failure 403 {object} ErrorResponse // @Failure 404 {object} ErrorResponse // @Failure 500 {object} ErrorResponse // @Router /api/v1/admin/users/{id}/profile [get] func GetAdminUserProfile(c *gin.Context) { userID, ok := parseUintParam(c, "id") if !ok { return } var user models.User if err := configs.DB.First(&user, userID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "kullanici bulunamadi"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "kullanici getirilemedi"}) return } profile, err := getOrCreateProfileByUserID(uint64(userID)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "profil getirilemedi"}) return } c.JSON(http.StatusOK, adminProfileResponse{ UserID: profile.UserID, FirstName: profile.FirstName, LastName: profile.LastName, AvatarURL: profile.AvatarURL, }) } // UpdateAdminUserProfile godoc // @Summary Admin panel icin kullanici profilini gunceller // @Tags admin-users // @Accept multipart/form-data // @Produce json // @Security BearerAuth // @Param id path int true "Kullanici ID" // @Param first_name formData string false "Ad" // @Param last_name formData string false "Soyad" // @Param avatar formData file false "Avatar dosyasi" // @Success 200 {object} AdminProfileResponse // @Failure 400 {object} ErrorResponse // @Failure 401 {object} ErrorResponse // @Failure 403 {object} ErrorResponse // @Failure 404 {object} ErrorResponse // @Failure 500 {object} ErrorResponse // @Router /api/v1/admin/users/{id}/profile [put] func UpdateAdminUserProfile(c *gin.Context) { userID, ok := parseUintParam(c, "id") if !ok { return } var req adminUpdateProfileRequest _ = c.ShouldBind(&req) var user models.User if err := configs.DB.First(&user, userID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "kullanici bulunamadi"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "kullanici getirilemedi"}) return } profile, err := getOrCreateProfileByUserID(uint64(userID)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "profil getirilemedi"}) return } if req.FirstName != "" { profile.FirstName = req.FirstName } if req.LastName != "" { profile.LastName = req.LastName } oldAvatarURL := profile.AvatarURL avatarURL, hasAvatar, err := saveAvatarFromMultipart(c, "avatar") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "avatar dosyasi okunamadi"}) return } if hasAvatar { profile.AvatarURL = avatarURL } if err := configs.DB.Save(&profile).Error; err != nil { if hasAvatar { _ = deleteLocalAvatarByURL(avatarURL) } c.JSON(http.StatusInternalServerError, gin.H{"error": "profil guncellenemedi"}) return } if hasAvatar && oldAvatarURL != "" && oldAvatarURL != profile.AvatarURL { if err := deleteLocalAvatarByURL(oldAvatarURL); err != nil { log.Printf("[ADMIN-PROFILE-UPDATE] user_id=%d result=warn stage=delete_old_avatar error=%v", userID, err) } } c.JSON(http.StatusOK, adminProfileResponse{ UserID: profile.UserID, FirstName: profile.FirstName, LastName: profile.LastName, AvatarURL: profile.AvatarURL, }) } func parsePositiveIntOrDefault(raw string, fallback int) int { if strings.TrimSpace(raw) == "" { return fallback } v, err := strconv.Atoi(raw) if err != nil || v <= 0 { return fallback } return v } func parseOptionalBool(raw string) (bool, bool) { if strings.TrimSpace(raw) == "" { return false, false } v, err := strconv.ParseBool(raw) if err != nil { return false, false } return v, true } func parseUintParam(c *gin.Context, key string) (uint, bool) { raw := strings.TrimSpace(c.Param(key)) if raw == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "gecersiz kullanici id"}) return 0, false } id, err := strconv.ParseUint(raw, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "gecersiz kullanici id"}) return 0, false } return uint(id), true } func toAdminUserResponse(user models.User) adminUserResponse { return adminUserResponse{ ID: user.ID, Username: user.UserName, Email: user.Email, EmailVerified: user.IsEmailVerified(), IsActive: user.IsActive != nil && *user.IsActive, IsAdmin: user.IsAdmin != nil && *user.IsAdmin, CreatedAt: user.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), UpdatedAt: user.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), } }