first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:40:14 +03:00
commit e04ba85564
129 changed files with 17541 additions and 0 deletions

View File

@@ -0,0 +1,709 @@
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"),
}
}