first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:43:40 +03:00
commit f34e54c5a5
100 changed files with 27342 additions and 0 deletions

View File

@@ -0,0 +1,361 @@
package handlers
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"gobeyhan/app/account/services"
settingsServices "gobeyhan/app/settings/services"
"gobeyhan/config"
"gobeyhan/database/models"
"github.com/gin-gonic/gin"
)
type AuthHandler struct {
userService *services.UserService
jwtService *settingsServices.JWTService
}
func NewAuthHandler(userService *services.UserService, jwtService *settingsServices.JWTService) *AuthHandler {
return &AuthHandler{
userService: userService,
jwtService: jwtService,
}
}
// Register godoc
// @Summary Register a new user
// @Description Create a new user account with email and password
// @Tags auth
// @Accept json
// @Produce json
// @Param request body object{email=string,password=string,username=string} true "Registration data"
// @Success 201 {object} object{token=string,user=models.User}
// @Failure 400 {object} object{error=string}
// @Router /api/v1/auth/register [post]
func (h *AuthHandler) Register(c *gin.Context) {
var input struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
Username string `json:"username" binding:"required,min=3"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Create user (password will be hashed by UserService)
user := &models.User{
Email: input.Email,
UserName: input.Username,
}
// Password is passed separately to be hashed
if err := h.userService.CreateUser(user, input.Password); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Assign default role
if err := h.userService.AssignDefaultRole(user.ID); err != nil {
// Log error but don't fail registration
// log.Printf("Failed to assign default role: %v", err)
}
// Generate JWT tokens
accessToken, refreshToken, err := h.jwtService.GenerateTokenPair(*user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate tokens"})
return
}
// Return tokens and user (without password)
user.Password = ""
c.JSON(http.StatusCreated, gin.H{
"access_token": accessToken,
"refresh_token": refreshToken,
"user": user,
})
}
// Login godoc
// @Summary Login user
// @Description Login with email and password
// @Tags auth
// @Accept json
// @Produce json
// @Param request body object{email=string,password=string} true "Login credentials"
// @Success 200 {object} object{token=string,user=models.User}
// @Failure 400 {object} object{error=string}
// @Failure 401 {object} object{error=string}
// @Router /api/v1/auth/login [post]
func (h *AuthHandler) Login(c *gin.Context) {
var input struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
TurnstileToken string `json:"turnstile_token"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Verify Turnstile
if !verifyTurnstile(input.TurnstileToken) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Turnstile doğrulaması başarısız"})
return
}
// Get user by email
user, err := h.userService.GetUserByEmail(input.Email)
if err != nil || user == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
return
}
// Verify password
if !h.userService.VerifyPassword(user.Password, input.Password) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
return
}
// Generate JWT tokens
accessToken, refreshToken, err := h.jwtService.GenerateTokenPair(*user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate tokens"})
return
}
// Set refresh token as HttpOnly cookie (secure, XSS-safe)
cookie := &http.Cookie{
Name: "refresh_token",
Value: refreshToken,
Path: "/",
Domain: "localhost", // Explicitly set for local dev
MaxAge: 7 * 24 * 60 * 60, // 7 days
Secure: false, // Set true for HTTPS in production
HttpOnly: true, // Cannot be accessed by JavaScript
SameSite: http.SameSiteLaxMode, // Lax is better for local dev
}
http.SetCookie(c.Writer, cookie)
fmt.Printf("[DEBUG] Login - Set-Cookie Raw: %s\n", cookie.String())
// Return tokens and user (without password)
user.Password = ""
c.JSON(http.StatusOK, gin.H{
"access_token": accessToken,
"refresh_token": refreshToken, // Also in response for fallback
"user": user,
})
}
// GetCurrentUser godoc
// @Summary Get current user
// @Description Get current authenticated user information
// @Tags auth
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} models.User
// @Failure 401 {object} object{error=string}
// @Router /api/v1/auth/me [get]
func (h *AuthHandler) GetCurrentUser(c *gin.Context) {
// Get user ID from context (set by auth middleware)
userIDStr, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
// Convert to uint64
var userID uint64
switch v := userIDStr.(type) {
case string:
parsed, _ := strconv.ParseUint(v, 10, 64)
userID = parsed
case uint64:
userID = v
case int:
userID = uint64(v)
case float64:
userID = uint64(v)
}
// Get user from database
user, err := h.userService.GetUserByID(userID)
if err != nil || user == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
// Return user (without password)
user.Password = ""
c.JSON(http.StatusOK, user)
}
// Logout godoc
// @Summary Logout user
// @Description Logout (client-side token removal)
// @Tags auth
// @Accept json
// @Produce json
// @Success 200 {object} object{message=string}
// @Router /api/v1/auth/logout [post]
func (h *AuthHandler) Logout(c *gin.Context) {
// Clear refresh token cookie
cookie := &http.Cookie{
Name: "refresh_token",
Value: "",
Path: "/",
Domain: "localhost",
MaxAge: -1, // Delete cookie
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}
http.SetCookie(c.Writer, cookie)
fmt.Printf("[DEBUG] Logout - Set-Cookie Raw: %s\n", cookie.String())
// For JWT, logout is typically handled client-side
// Server can implement token blacklisting if needed
c.JSON(http.StatusOK, gin.H{"message": "Logged out successfully"})
}
// RefreshToken godoc
// @Summary Refresh access token
// @Description Get a new access token using refresh token from HttpOnly cookie
// @Tags auth
// @Accept json
// @Produce json
// @Success 200 {object} object{access_token=string,refresh_token=string}
// @Failure 401 {object} object{error=string}
// @Router /api/v1/auth/refresh [post]
func (h *AuthHandler) RefreshToken(c *gin.Context) {
// Get refresh token from HttpOnly cookie
refreshToken, err := c.Cookie("refresh_token")
if err != nil {
fmt.Printf("[DEBUG] RefreshToken - Cookie Error: %v\n", err)
} else {
fmt.Printf("[DEBUG] RefreshToken - Cookie Found: %s...\n", refreshToken[:10])
}
if err != nil || refreshToken == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Refresh token not found"})
return
}
// Validate refresh token and get user ID
claims, err := h.jwtService.ValidateToken(refreshToken)
if err != nil {
fmt.Printf("[DEBUG] RefreshToken - ValidateToken Error: %v\n", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired refresh token"})
return
}
// Get user ID from claims
userID, err := claims.GetSubject()
if err != nil {
fmt.Printf("[DEBUG] RefreshToken - GetSubject Error: %v\n", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"})
return
}
// Parse user ID to uint64
// Sscan bazen boşluk vs. yüzünden hata verebilir, strconv daha güvenli
parsedUint, err := strconv.ParseUint(userID, 10, 64)
if err != nil {
fmt.Printf("[DEBUG] RefreshToken - ParseUint Error: %v (userID=%s)\n", err, userID)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid user ID format"})
return
}
uid := parsedUint
// Get user from database
user, err := h.userService.GetUserByID(uid)
if err != nil {
fmt.Printf("[DEBUG] RefreshToken - GetUserByID FAILED: %v\n", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Database error"})
return
}
if user == nil {
fmt.Printf("[DEBUG] RefreshToken - User not found in DB for ID: %d\n", uid)
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"})
return
}
fmt.Printf("[DEBUG] RefreshToken - User found: %s\n", user.Email)
// Generate new token pair
newAccessToken, newRefreshToken, err := h.jwtService.GenerateTokenPair(*user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate tokens"})
return
}
// Update refresh token cookie (token rotation)
cookie := &http.Cookie{
Name: "refresh_token",
Value: newRefreshToken,
Path: "/",
Domain: "localhost",
MaxAge: 7 * 24 * 60 * 60, // 7 days
Secure: false,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}
http.SetCookie(c.Writer, cookie)
// Return new access token and user info
user.Password = "" // Ensure password is not sent
c.JSON(http.StatusOK, gin.H{
"access_token": newAccessToken,
"refresh_token": newRefreshToken,
"user": user, // Critical for frontend session restore
})
}
// Helper: Verify Turnstile Token
func verifyTurnstile(token string) bool {
secret := config.AppConfig.TurnstileSecretKey
if secret == "" {
fmt.Println("[WARNING] Turnstile Secret Key not configured, skipping validation")
return true // Skip validation if not configured
}
if token == "" {
// If secret is configured, token is mandatory
return false
}
formData := url.Values{
"secret": {secret},
"response": {token},
}
resp, err := http.PostForm("https://challenges.cloudflare.com/turnstile/v0/siteverify", formData)
if err != nil {
fmt.Printf("[ERROR] Turnstile request failed: %v\n", err)
return false
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("[ERROR] Failed to read Turnstile response: %v\n", err)
return false
}
var result struct {
Success bool `json:"success"`
}
if err := json.Unmarshal(body, &result); err != nil {
fmt.Printf("[ERROR] Failed to parse Turnstile response: %v\n", err)
return false
}
return result.Success
}

View File

@@ -0,0 +1,297 @@
package handlers
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"gobeyhan/app/account/services"
settingsServices "gobeyhan/app/settings/services"
"gobeyhan/config"
"gobeyhan/database/models"
"github.com/gin-gonic/gin"
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
"golang.org/x/oauth2/google"
)
type OAuthHandler struct {
userService *services.UserService
socialAccountService *services.SocialAccountService
jwtService *settingsServices.JWTService
googleOAuthConfig *oauth2.Config
githubOAuthConfig *oauth2.Config
}
func NewOAuthHandler(
userService *services.UserService,
socialAccountService *services.SocialAccountService,
jwtService *settingsServices.JWTService,
) *OAuthHandler {
// Google OAuth config
googleConfig := &oauth2.Config{
ClientID: config.AppConfig.GoogleClientID,
ClientSecret: config.AppConfig.GoogleClientSecret,
RedirectURL: config.AppConfig.GoogleRedirectURL,
Scopes: []string{
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
},
Endpoint: google.Endpoint,
}
// GitHub OAuth config
githubConfig := &oauth2.Config{
ClientID: config.AppConfig.GithubClientID,
ClientSecret: config.AppConfig.GithubClientSecret,
RedirectURL: config.AppConfig.GithubRedirectURL,
Scopes: []string{"user:email"},
Endpoint: github.Endpoint,
}
return &OAuthHandler{
userService: userService,
socialAccountService: socialAccountService,
jwtService: jwtService,
googleOAuthConfig: googleConfig,
githubOAuthConfig: githubConfig,
}
}
// GoogleLogin godoc
// @Summary Google OAuth login
// @Description Redirect to Google OAuth
// @Tags auth,oauth
// @Produce json
// @Router /api/v1/auth/google [get]
func (h *OAuthHandler) GoogleLogin(c *gin.Context) {
url := h.googleOAuthConfig.AuthCodeURL("state", oauth2.AccessTypeOffline)
c.Redirect(http.StatusTemporaryRedirect, url)
}
// GoogleCallback godoc
// @Summary Google OAuth callback
// @Description Handle Google OAuth callback
// @Tags auth,oauth
// @Produce json
// @Param code query string true "Authorization code"
// @Success 200 {object} object{token=string,user=models.User}
// @Failure 400 {object} object{error=string}
// @Router /api/v1/auth/google/callback [get]
func (h *OAuthHandler) GoogleCallback(c *gin.Context) {
code := c.Query("code")
if code == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Code not found"})
return
}
// Exchange code for token
token, err := h.googleOAuthConfig.Exchange(context.Background(), code)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token"})
return
}
// Get user info from Google
client := h.googleOAuthConfig.Client(context.Background(), token)
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user info"})
return
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
var googleUser struct {
ID string `json:"id"`
Email string `json:"email"`
VerifiedEmail bool `json:"verified_email"`
Name string `json:"name"`
Picture string `json:"picture"`
}
json.Unmarshal(data, &googleUser)
// Find or create user
user, accessToken, refreshToken, err := h.findOrCreateOAuthUser(
googleUser.Email,
googleUser.Name,
"google",
googleUser.ID,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"token": accessToken,
"refresh_token": refreshToken,
"user": user,
})
}
// GithubLogin godoc
// @Summary GitHub OAuth login
// @Description Redirect to GitHub OAuth
// @Tags auth,oauth
// @Produce json
// @Router /api/v1/auth/github [get]
func (h *OAuthHandler) GithubLogin(c *gin.Context) {
url := h.githubOAuthConfig.AuthCodeURL("state", oauth2.AccessTypeOffline)
c.Redirect(http.StatusTemporaryRedirect, url)
}
// GithubCallback godoc
// @Summary GitHub OAuth callback
// @Description Handle GitHub OAuth callback
// @Tags auth,oauth
// @Produce json
// @Param code query string true "Authorization code"
// @Success 200 {object} object{token=string,user=models.User}
// @Failure 400 {object} object{error=string}
// @Router /api/v1/auth/github/callback [get]
func (h *OAuthHandler) GithubCallback(c *gin.Context) {
code := c.Query("code")
if code == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Code not found"})
return
}
// Exchange code for token
token, err := h.githubOAuthConfig.Exchange(context.Background(), code)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token"})
return
}
// Get user info from GitHub
client := h.githubOAuthConfig.Client(context.Background(), token)
resp, err := client.Get("https://api.github.com/user")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user info"})
return
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
var githubUser struct {
ID int `json:"id"`
Login string `json:"login"`
Email string `json:"email"`
Name string `json:"name"`
}
json.Unmarshal(data, &githubUser)
// If email is not public, fetch it separately
if githubUser.Email == "" {
emailResp, _ := client.Get("https://api.github.com/user/emails")
if emailResp != nil {
defer emailResp.Body.Close()
emailData, _ := io.ReadAll(emailResp.Body)
var emails []struct {
Email string `json:"email"`
Primary bool `json:"primary"`
Verified bool `json:"verified"`
}
json.Unmarshal(emailData, &emails)
for _, e := range emails {
if e.Primary && e.Verified {
githubUser.Email = e.Email
break
}
}
}
}
username := githubUser.Name
if username == "" {
username = githubUser.Login
}
// Find or create user
user, accessToken, refreshToken, err := h.findOrCreateOAuthUser(
githubUser.Email,
username,
"github",
strconv.Itoa(githubUser.ID),
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"token": accessToken,
"refresh_token": refreshToken,
"user": user,
})
}
// findOrCreateOAuthUser finds existing user or creates new one for OAuth
func (h *OAuthHandler) findOrCreateOAuthUser(
email, username, provider, providerUserID string,
) (*models.User, string, string, error) {
// Try to find existing user by email
user, err := h.userService.GetUserByEmail(email)
if err != nil {
return nil, "", "", err
}
// If user doesn't exist, create new one
if user == nil {
user = &models.User{
Email: email,
UserName: username,
}
// Create user with empty password
if err := h.userService.CreateUser(user, ""); err != nil {
return nil, "", "", fmt.Errorf("failed to create user: %w", err)
}
// Assign default role
if err := h.userService.AssignDefaultRole(user.ID); err != nil {
// Log error but continue
// fmt.Printf("Failed to assign default role: %v\n", err)
}
}
// Check if social account exists
accounts, err := h.socialAccountService.GetSocialAccountsByUser(user.ID)
if err != nil {
return nil, "", "", err
}
// Create social account if it doesn't exist
found := false
for _, acc := range accounts {
if acc.Provider == provider && acc.ProviderID == providerUserID {
found = true
break
}
}
if !found {
socialAccount := &models.SocialAccount{
UserID: user.ID,
Provider: provider,
ProviderID: providerUserID,
}
if err := h.socialAccountService.CreateSocialAccount(socialAccount); err != nil {
return nil, "", "", fmt.Errorf("failed to create social account: %w", err)
}
}
// Generate JWT tokens
accessToken, refreshToken, err := h.jwtService.GenerateTokenPair(*user)
if err != nil {
return nil, "", "", fmt.Errorf("failed to generate tokens: %w", err)
}
// Clear password before returning
user.Password = ""
return user, accessToken, refreshToken, nil
}

View File

@@ -0,0 +1,70 @@
package handlers
import (
"gobeyhan/app/account/services"
"gobeyhan/database/models"
"net/http"
"github.com/gin-gonic/gin"
)
type PermissionHandler struct {
service *services.PermissionService
}
func NewPermissionHandler(service *services.PermissionService) *PermissionHandler {
return &PermissionHandler{service: service}
}
// AdminGetAllPermissions godoc
// @Summary Get all permissions (Admin)
// @Description Get list of all permissions
// @Tags admin,permissions
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {array} models.Permission
// @Router /api/v1/admin/permissions [get]
func (h *PermissionHandler) AdminGetAllPermissions(c *gin.Context) {
permissions, err := h.service.GetAllPermissions()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": permissions})
}
// AdminCreatePermission godoc
// @Summary Create a new permission (Admin)
// @Description Create a new permission
// @Tags admin,permissions
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param permission body models.Permission true "Permission object"
// @Success 201 {object} models.Permission
// @Router /api/v1/admin/permissions [post]
func (h *PermissionHandler) AdminCreatePermission(c *gin.Context) {
var input struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
permission := &models.Permission{
Name: input.Name,
Description: input.Description,
}
if err := h.service.CreatePermission(permission); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": permission})
}

View File

@@ -0,0 +1,169 @@
package handlers
import (
"gobeyhan/app/account/services"
"gobeyhan/database/models"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type RoleHandler struct {
service *services.RoleService
}
func NewRoleHandler(service *services.RoleService) *RoleHandler {
return &RoleHandler{service: service}
}
// AdminGetAllRoles godoc
// @Summary Get all roles (Admin)
// @Description Get list of all roles with permissions
// @Tags admin,roles
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {array} models.Role
// @Router /api/v1/admin/roles [get]
func (h *RoleHandler) AdminGetAllRoles(c *gin.Context) {
roles, err := h.service.GetAllRoles()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": roles})
}
// AdminGetRoleByID godoc
// @Summary Get role by ID (Admin)
// @Description Get a single role by ID
// @Tags admin,roles
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Role ID"
// @Success 200 {object} models.Role
// @Router /api/v1/admin/roles/{id} [get]
func (h *RoleHandler) AdminGetRoleByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid role ID"})
return
}
role, err := h.service.GetRoleByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if role == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Role not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": role})
}
// AdminCreateRole godoc
// @Summary Create a new role (Admin)
// @Description Create a new role
// @Tags admin,roles
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param role body models.Role true "Role object"
// @Success 201 {object} models.Role
// @Router /api/v1/admin/roles [post]
func (h *RoleHandler) AdminCreateRole(c *gin.Context) {
var input struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
role := &models.Role{
Name: input.Name,
Description: input.Description,
}
if err := h.service.CreateRole(role); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": role})
}
// AdminUpdateRole godoc
// @Summary Update a role (Admin)
// @Description Update an existing role
// @Tags admin,roles
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Role ID"
// @Param role body models.Role true "Role object"
// @Success 200 {object} models.Role
// @Router /api/v1/admin/roles/{id} [put]
func (h *RoleHandler) AdminUpdateRole(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid role ID"})
return
}
var input map[string]interface{}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.UpdateRole(id, input); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Fetch updated role
role, err := h.service.GetRoleByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": role})
}
// AdminDeleteRole godoc
// @Summary Delete a role (Admin)
// @Description Delete a role by ID
// @Tags admin,roles
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Role ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/roles/{id} [delete]
func (h *RoleHandler) AdminDeleteRole(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid role ID"})
return
}
if err := h.service.DeleteRole(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Role deleted successfully"})
}

View File

@@ -0,0 +1,111 @@
package handlers
import (
"gobeyhan/app/account/services"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type SocialAccountHandler struct {
service *services.SocialAccountService
}
func NewSocialAccountHandler(service *services.SocialAccountService) *SocialAccountHandler {
return &SocialAccountHandler{service: service}
}
// GetUserSocialAccounts godoc
// @Summary Get user's social accounts
// @Description Get all social accounts for the authenticated user
// @Tags social-accounts
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {array} models.SocialAccount
// @Router /api/v1/user/social-accounts [get]
func (h *SocialAccountHandler) GetUserSocialAccounts(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
// Convert user_id to uint64
var uid uint64
switch v := userID.(type) {
case string:
uid, _ = strconv.ParseUint(v, 10, 64)
case uint64:
uid = v
case int:
uid = uint64(v)
case float64:
uid = uint64(v)
}
accounts, err := h.service.GetSocialAccountsByUser(uid)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": accounts})
}
// DeleteSocialAccount godoc
// @Summary Delete a social account
// @Description Delete a social account for the authenticated user
// @Tags social-accounts
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Social Account ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/user/social-accounts/{id} [delete]
func (h *SocialAccountHandler) DeleteSocialAccount(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid social account ID"})
return
}
// Verify ownership
account, err := h.service.GetSocialAccountByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if account == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Social account not found"})
return
}
userID, _ := c.Get("user_id")
var uid uint64
switch v := userID.(type) {
case string:
uid, _ = strconv.ParseUint(v, 10, 64)
case uint64:
uid = v
case int:
uid = uint64(v)
case float64:
uid = uint64(v)
}
if account.UserID != uid {
c.JSON(http.StatusForbidden, gin.H{"error": "You can only delete your own social accounts"})
return
}
if err := h.service.DeleteSocialAccount(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Social account deleted successfully"})
}

View File

@@ -0,0 +1,287 @@
package handlers
import (
"gobeyhan/app/account/services"
"gobeyhan/database/models"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type UserHandler struct {
service *services.UserService
}
func NewUserHandler(service *services.UserService) *UserHandler {
return &UserHandler{service: service}
}
// AdminGetAllUsers godoc
// @Summary Get all users (Admin)
// @Description Get paginated list of all users
// @Tags admin,users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param page query int false "Page number" default(1)
// @Param limit query int false "Items per page" default(10)
// @Param include_deleted query bool false "Include soft-deleted users"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/admin/users [get]
func (h *UserHandler) AdminGetAllUsers(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
includeDeleted := c.DefaultQuery("include_deleted", "false") == "true"
if page < 1 {
page = 1
}
if limit < 1 || limit > 100 {
limit = 10
}
users, total, err := h.service.GetAllUsers(includeDeleted, page, limit)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"data": users,
"total": total,
"page": page,
"limit": limit,
})
}
// AdminGetUserByID godoc
// @Summary Get user by ID (Admin)
// @Description Get a single user by ID
// @Tags admin,users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "User ID"
// @Success 200 {object} models.User
// @Router /api/v1/admin/users/{id} [get]
func (h *UserHandler) AdminGetUserByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
user, err := h.service.GetUserByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if user == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": user})
}
// AdminCreateUser godoc
// @Summary Create a new user (Admin)
// @Description Create a new user
// @Tags admin,users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param user body models.User true "User object"
// @Success 201 {object} models.User
// @Router /api/v1/admin/users [post]
func (h *UserHandler) AdminCreateUser(c *gin.Context) {
var input struct {
UserName string `json:"username"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
Avatar string `json:"avatar"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user := &models.User{
UserName: input.UserName,
Email: input.Email,
Avatar: input.Avatar,
}
if err := h.service.CreateUser(user, input.Password); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": user})
}
// AdminUpdateUser godoc
// @Summary Update a user (Admin)
// @Description Update an existing user
// @Tags admin,users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "User ID"
// @Param user body models.User true "User object"
// @Success 200 {object} models.User
// @Router /api/v1/admin/users/{id} [put]
func (h *UserHandler) AdminUpdateUser(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
var input map[string]interface{}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.UpdateUser(id, input); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Fetch updated user
user, err := h.service.GetUserByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": user})
}
// AdminDeleteUser godoc
// @Summary Delete a user (Admin)
// @Description Soft delete a user by ID
// @Tags admin,users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "User ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/users/{id} [delete]
func (h *UserHandler) AdminDeleteUser(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
if err := h.service.DeleteUser(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"})
}
// AdminRestoreUser godoc
// @Summary Restore a deleted user (Admin)
// @Description Restore a soft-deleted user
// @Tags admin,users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "User ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/users/{id}/restore [post]
func (h *UserHandler) AdminRestoreUser(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
if err := h.service.RestoreUser(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "User restored successfully"})
}
// AdminAssignRole godoc
// @Summary Assign role to user (Admin)
// @Description Assign a role to a user
// @Tags admin,users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "User ID"
// @Param role_id body int true "Role ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/users/{id}/roles [post]
func (h *UserHandler) AdminAssignRole(c *gin.Context) {
idStr := c.Param("id")
userID, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
var input struct {
RoleID uint64 `json:"role_id" binding:"required"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.AssignRole(userID, input.RoleID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Role assigned successfully"})
}
// AdminRemoveRole godoc
// @Summary Remove role from user (Admin)
// @Description Remove a role from a user
// @Tags admin,users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "User ID"
// @Param role_id path int true "Role ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/users/{id}/roles/{role_id} [delete]
func (h *UserHandler) AdminRemoveRole(c *gin.Context) {
userIDStr := c.Param("id")
userID, err := strconv.ParseUint(userIDStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
roleIDStr := c.Param("role_id")
roleID, err := strconv.ParseUint(roleIDStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid role ID"})
return
}
if err := h.service.RemoveRole(userID, roleID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Role removed successfully"})
}