first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:46:42 +03:00
commit 2a5b661443
202 changed files with 49770 additions and 0 deletions

View File

@@ -0,0 +1,615 @@
package controllers
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
database "goGin/app/database/config"
"goGin/app/database/models"
"goGin/app/middlewares"
"goGin/app/services"
configs "goGin/config"
utils "goGin/pkg/utis"
"github.com/gin-gonic/gin"
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
"golang.org/x/oauth2/google"
"gorm.io/gorm"
)
// AuthResponse
type AuthResponse struct {
User UserResponse `json:"user"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
// RegisterPayload
type RegisterPayload struct {
UserName string `json:"username" binding:"required"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
}
// LoginPayload
type LoginPayload struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
// RefreshPayload
type RefreshPayload struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
// Helper to generate secure token for email verification
func generateSecureToken() string {
b := make([]byte, 32)
rand.Read(b)
return hex.EncodeToString(b)
}
// Register godoc
// @Summary Register a new user
// @Description Register a new user. Sends verification email. Does NOT return tokens.
// @Tags auth
// @Accept json
// @Produce json
// @Param register body RegisterPayload true "Register payload"
// @Success 201 {object} controllers.AuthResponse
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/auth/register [post]
func Register(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
var payload RegisterPayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Check existing email
var existing models.User
if err := database.DB.Where("email = ?", payload.Email).First(&existing).Error; err == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "email already registered"})
return
}
hashedPwd, err := utils.HashPassword(payload.Password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to hash password"})
return
}
// Email Verification Token
verificationToken := generateSecureToken()
emailVerified := false
user := models.User{
UserName: payload.UserName,
Email: payload.Email,
Password: hashedPwd,
EmailVerified: &emailVerified,
EmailVerifyToken: verificationToken,
}
if err := database.DB.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Send Verification Email
go func() {
if err := utils.SendVerificationEmail(user.Email, verificationToken); err != nil {
fmt.Printf("Failed to send verification email to %s: %v\n", user.Email, err)
}
}()
// Response
c.JSON(http.StatusCreated, gin.H{
"message": "Registration successful. Please check your email to verify your account.",
"user": toUserResponse(user),
})
}
// VerifyEmail godoc
// @Summary Verify email address
// @Description Verify email using token
// @Tags auth
// @Accept json
// @Produce json
// @Param token query string true "Verification 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 *gin.Context) {
token := c.Query("token")
if token == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "token is required"})
return
}
var user models.User
if err := database.DB.Where("email_verify_token = ?", token).First(&user).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid or expired token"})
return
}
now := time.Now()
verified := true
user.EmailVerified = &verified
user.EmailVerifiedAt = &now
user.EmailVerifyToken = "" // Clear token
if err := database.DB.Save(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to verify email"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Email verified successfully"})
}
// Login godoc
// @Summary Login user
// @Description Login with email and password, returns tokens
// @Tags auth
// @Accept json
// @Produce json
// @Param login body LoginPayload true "Login payload"
// @Success 200 {object} controllers.AuthResponse
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Router /api/v1/auth/login [post]
func Login(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
var payload LoginPayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var user models.User
if err := database.DB.Where("email = ?", payload.Email).First(&user).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if !utils.CheckPasswordHash(payload.Password, user.Password) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
return
}
// Check if email is verified
if user.EmailVerified != nil && !*user.EmailVerified {
c.JSON(http.StatusUnauthorized, gin.H{"error": "email not verified"})
return
}
isAdmin := false
if user.IsAdmin != nil && *user.IsAdmin {
isAdmin = true
}
jwtService := services.NewJWTService()
accessToken, err := jwtService.GenerateToken(user.ID, isAdmin)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate access token"})
return
}
refreshToken, err := jwtService.GenerateRefreshToken(user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate refresh token"})
return
}
c.JSON(http.StatusOK, gin.H{
"user": toUserResponse(user),
"access_token": accessToken,
"refresh_token": refreshToken,
})
}
// Refresh godoc
// @Summary Refresh access token
// @Description usage: send refresh token to get new access token and refresh token
// @Tags auth
// @Accept json
// @Produce json
// @Param refresh body RefreshPayload true "Refresh token payload"
// @Success 200 {object} map[string]string "Returns both access_token and refresh_token"
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Router /api/v1/auth/refresh [post]
func Refresh(c *gin.Context) {
var payload RefreshPayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
jwtService := services.NewJWTService()
claims, err := jwtService.ValidateToken(payload.RefreshToken)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid refresh token"})
return
}
if claims.TokenType != "refresh" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "not a refresh token"})
return
}
// Get User
var userID uint
switch v := claims.UserID.(type) {
case float64:
userID = uint(v)
case uint:
userID = v
default:
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid user claim"})
return
}
var user models.User
if err := database.DB.First(&user, userID).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found"})
return
}
isAdmin := false
if user.IsAdmin != nil && *user.IsAdmin {
isAdmin = true
}
newAccessToken, err := jwtService.GenerateToken(user.ID, isAdmin)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"})
return
}
newRefreshToken, err := jwtService.GenerateRefreshToken(user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate refresh token"})
return
}
c.JSON(http.StatusOK, gin.H{
"access_token": newAccessToken,
"refresh_token": newRefreshToken,
})
}
// Me godoc
// @Summary Get current user (me)
// @Description Get current authenticated user information
// @Tags auth
// @Security BearerAuth
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Failure 401 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/auth/me [get]
func Me(c *gin.Context) {
claims, ok := middlewares.GetAuthClaims(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
var userID uint
switch v := claims.UserID.(type) {
case float64:
userID = uint(v)
case uint:
userID = v
default:
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid user claim"})
return
}
var user models.User
if err := database.DB.First(&user, userID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
isAdmin := false
if user.IsAdmin != nil && *user.IsAdmin {
isAdmin = true
}
isVerified := false
if user.EmailVerified != nil && *user.EmailVerified {
isVerified = true
}
// Frontend'in beklediği formata göre response döndür
c.JSON(http.StatusOK, gin.H{
"id": user.ID,
"username": user.UserName,
"email": user.Email,
"email_verified": isVerified,
"is_admin": isAdmin,
})
}
// OAuth Helpers
var (
googleOauthConfig = &oauth2.Config{
RedirectURL: "", // Will be set in init or handler
ClientID: "",
ClientSecret: "",
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"},
Endpoint: google.Endpoint,
}
githubOauthConfig = &oauth2.Config{
RedirectURL: "",
ClientID: "",
ClientSecret: "",
Scopes: []string{"user:email"},
Endpoint: github.Endpoint,
}
)
func getGoogleConfig() *oauth2.Config {
googleOauthConfig.ClientID = configs.AppConfig.GoogleClientID
googleOauthConfig.ClientSecret = configs.AppConfig.GoogleClientSecret
googleOauthConfig.RedirectURL = configs.AppConfig.GoogleRedirectURL
return googleOauthConfig
}
func getGithubConfig() *oauth2.Config {
githubOauthConfig.ClientID = configs.AppConfig.GithubClientID
githubOauthConfig.ClientSecret = configs.AppConfig.GithubClientSecret
githubOauthConfig.RedirectURL = configs.AppConfig.GithubRedirectURL
return githubOauthConfig
}
// GoogleLogin godoc
// @Summary Google OAuth2 Login
// @Description Redirects to Google for authentication
// @Tags auth
// @Success 302
// @Router /api/v1/auth/google [get]
func GoogleLogin(c *gin.Context) {
url := getGoogleConfig().AuthCodeURL("state_google", oauth2.AccessTypeOffline)
c.Redirect(http.StatusTemporaryRedirect, url)
}
// GoogleCallback godoc
// @Summary Google OAuth2 Callback
// @Description Handles Google OAuth2 callback
// @Tags auth
// @Success 200 {object} controllers.AuthResponse
// @Failure 500 {object} map[string]string
// @Router /api/v1/auth/google/callback [get]
func GoogleCallback(c *gin.Context) {
code := c.Query("code")
token, err := getGoogleConfig().Exchange(context.Background(), code)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token: " + err.Error()})
return
}
client := getGoogleConfig().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: " + err.Error()})
return
}
defer resp.Body.Close()
userData, err := io.ReadAll(resp.Body)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read user info"})
return
}
var googleUser struct {
ID string `json:"id"`
Email string `json:"email"`
VerifiedEmail bool `json:"verified_email"`
Name string `json:"name"`
Picture string `json:"picture"`
}
if err := json.Unmarshal(userData, &googleUser); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse user info"})
return
}
handleSocialLogin(c, "google", googleUser.ID, googleUser.Email, googleUser.Name, googleUser.Picture)
}
// GithubLogin godoc
// @Summary GitHub OAuth2 Login
// @Description Redirects to GitHub for authentication
// @Tags auth
// @Success 302
// @Router /api/v1/auth/github [get]
func GithubLogin(c *gin.Context) {
url := getGithubConfig().AuthCodeURL("state_github", oauth2.AccessTypeOffline)
c.Redirect(http.StatusTemporaryRedirect, url)
}
// GithubCallback godoc
// @Summary GitHub OAuth2 Callback
// @Description Handles GitHub OAuth2 callback
// @Tags auth
// @Success 200 {object} controllers.AuthResponse
// @Failure 500 {object} map[string]string
// @Router /api/v1/auth/github/callback [get]
func GithubCallback(c *gin.Context) {
code := c.Query("code")
token, err := getGithubConfig().Exchange(context.Background(), code)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token: " + err.Error()})
return
}
client := getGithubConfig().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: " + err.Error()})
return
}
defer resp.Body.Close()
userData, err := io.ReadAll(resp.Body)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read user info"})
return
}
var githubUser struct {
ID int64 `json:"id"`
Login string `json:"login"`
Name string `json:"name"`
Email string `json:"email"`
AvatarURL string `json:"avatar_url"`
}
if err := json.Unmarshal(userData, &githubUser); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse user info"})
return
}
// GitHub email might be private, need to fetch separately if empty
email := githubUser.Email
if email == "" {
// Fetch emails
emailResp, err := client.Get("https://api.github.com/user/emails")
if err == nil {
defer emailResp.Body.Close()
var emails []struct {
Email string `json:"email"`
Primary bool `json:"primary"`
Verified bool `json:"verified"`
}
if body, err := io.ReadAll(emailResp.Body); err == nil {
json.Unmarshal(body, &emails)
for _, e := range emails {
if e.Primary && e.Verified {
email = e.Email
break
}
}
}
}
}
if email == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Could not retrieve email from GitHub"})
return
}
handleSocialLogin(c, "github", fmt.Sprintf("%d", githubUser.ID), email, githubUser.Name, githubUser.AvatarURL)
}
func handleSocialLogin(c *gin.Context, provider, providerID, email, name, avatarURL string) {
var user models.User
var socialAccount models.SocialAccount
// Check if social account exists
err := database.DB.Where("provider = ? AND provider_id = ?", provider, providerID).First(&socialAccount).Error
if err == nil {
// Found social account, find user
if err := database.DB.First(&user, socialAccount.UserID).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "User record missing for social account"})
return
}
} else {
// Social account not found. Check if email exists
if err := database.DB.Where("email = ?", email).First(&user).Error; err == nil {
// User exists, add social account
newSocial := models.SocialAccount{
UserID: uint64(user.ID),
Provider: provider,
ProviderID: providerID,
Email: email,
Name: name,
AvatarURL: avatarURL,
}
database.DB.Create(&newSocial)
} else {
// Create new user
verified := true
now := time.Now()
// Generate random password
randomPass := generateSecureToken()
hashedPwd, _ := utils.HashPassword(randomPass)
user = models.User{
UserName: name, // Handle duplicate usernames?
Email: email,
Password: hashedPwd,
EmailVerified: &verified,
EmailVerifiedAt: &now,
}
// Fallback username if empty
if user.UserName == "" {
user.UserName = strings.Split(email, "@")[0]
}
if err := database.DB.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user: " + err.Error()})
return
}
newSocial := models.SocialAccount{
UserID: uint64(user.ID),
Provider: provider,
ProviderID: providerID,
Email: email,
Name: name,
AvatarURL: avatarURL,
}
database.DB.Create(&newSocial)
}
}
// Login logic
isAdmin := false
if user.IsAdmin != nil && *user.IsAdmin {
isAdmin = true
}
jwtService := services.NewJWTService()
accessToken, err := jwtService.GenerateToken(user.ID, isAdmin)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate access token"})
return
}
refreshToken, err := jwtService.GenerateRefreshToken(user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate refresh token"})
return
}
c.JSON(http.StatusOK, gin.H{
"user": toUserResponse(user),
"access_token": accessToken,
"refresh_token": refreshToken,
})
}