first commit
This commit is contained in:
53
services/email_service.go
Normal file
53
services/email_service.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
|
||||
configs "ares/config"
|
||||
)
|
||||
|
||||
type EmailService struct{}
|
||||
|
||||
func NewEmailService() *EmailService {
|
||||
return &EmailService{}
|
||||
}
|
||||
|
||||
func (s *EmailService) Send(to, subject, body string) error {
|
||||
host := strings.TrimSpace(configs.AppConfig.EmailHost)
|
||||
port := strings.TrimSpace(configs.AppConfig.EmailPort)
|
||||
from := strings.TrimSpace(configs.AppConfig.EmailFrom)
|
||||
|
||||
if host == "" || port == "" || from == "" {
|
||||
return fmt.Errorf("email configuration is incomplete")
|
||||
}
|
||||
|
||||
addr := host + ":" + port
|
||||
username := strings.TrimSpace(configs.AppConfig.EmailHostUser)
|
||||
password := strings.TrimSpace(configs.AppConfig.EmailHostPassword)
|
||||
|
||||
var auth smtp.Auth
|
||||
if username != "" && password != "" {
|
||||
auth = smtp.PlainAuth("", username, password, host)
|
||||
}
|
||||
|
||||
message := "From: " + from + "\r\n" +
|
||||
"To: " + to + "\r\n" +
|
||||
"Subject: " + subject + "\r\n" +
|
||||
"MIME-Version: 1.0\r\n" +
|
||||
"Content-Type: text/plain; charset=UTF-8\r\n\r\n" +
|
||||
body
|
||||
|
||||
return smtp.SendMail(addr, auth, from, []string{to}, []byte(message))
|
||||
}
|
||||
|
||||
func (s *EmailService) SendVerificationEmail(to, firstName, verifyURL string) error {
|
||||
subject := "Email verification"
|
||||
body := fmt.Sprintf(
|
||||
"Hi %s,\n\nPlease verify your email by opening this link:\n%s\n\nIf you did not create this account, you can ignore this email.",
|
||||
firstName,
|
||||
verifyURL,
|
||||
)
|
||||
return s.Send(to, subject, body)
|
||||
}
|
||||
190
services/image_service.go
Normal file
190
services/image_service.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
configs "ares/config"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/h2non/bimg"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ImageOptions defines the parameters for image processing
|
||||
type ImageOptions struct {
|
||||
Width int
|
||||
Height int
|
||||
Quality int
|
||||
Format string // "avif", "webp", "png", "jpg"
|
||||
Folder string // e.g. "settings", "heroes"
|
||||
}
|
||||
|
||||
// ProcessAndSaveImage handles the file upload, processing, and saving
|
||||
func ProcessAndSaveImage(c fiber.Ctx, fieldName string, opts ImageOptions) (string, error) {
|
||||
// 1. Get file from form
|
||||
file, err := c.FormFile(fieldName)
|
||||
if err != nil {
|
||||
// If no file uploaded, return empty string (not an error)
|
||||
configs.Logger.Debug("no file uploaded", zap.Error(err), zap.String("field", fieldName))
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// 2. Open file
|
||||
f, err := file.Open()
|
||||
if err != nil {
|
||||
configs.Logger.Error("failed to open uploaded file", zap.Error(err), zap.String("filename", file.Filename), zap.String("field", fieldName))
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Read bytes
|
||||
buffer := make([]byte, file.Size)
|
||||
n, err := f.Read(buffer)
|
||||
if err != nil {
|
||||
configs.Logger.Error("failed to read uploaded file bytes", zap.Error(err), zap.String("filename", file.Filename), zap.Int64("size_expected", file.Size))
|
||||
return "", err
|
||||
}
|
||||
configs.Logger.Debug("read uploaded file bytes", zap.String("filename", file.Filename), zap.Int("read_bytes", n), zap.Int64("size_expected", file.Size))
|
||||
|
||||
// 3. Process with bimg
|
||||
options := bimg.Options{
|
||||
Width: opts.Width,
|
||||
Height: opts.Height,
|
||||
Quality: opts.Quality,
|
||||
}
|
||||
|
||||
// If both Width and Height are set, use Smart Crop (Cover)
|
||||
if opts.Width > 0 && opts.Height > 0 {
|
||||
options.Crop = true
|
||||
options.Gravity = bimg.GravitySmart
|
||||
}
|
||||
|
||||
// Allow enlarging smaller images to requested dimensions and strip metadata
|
||||
options.Enlarge = true
|
||||
options.StripMetadata = true
|
||||
|
||||
newImage, err := bimg.NewImage(buffer).Process(options)
|
||||
if err != nil {
|
||||
configs.Logger.Error("image processing failed", zap.Error(err), zap.Any("options", options))
|
||||
return "", fmt.Errorf("resim işleme hatası: %v", err)
|
||||
}
|
||||
|
||||
// 4. Convert Format (if requested)
|
||||
// Default to AVIF if not specified
|
||||
targetFormat := bimg.AVIF
|
||||
ext := ".avif"
|
||||
|
||||
if opts.Format != "" {
|
||||
switch strings.ToLower(opts.Format) {
|
||||
case "webp":
|
||||
targetFormat = bimg.WEBP
|
||||
ext = ".webp"
|
||||
case "png":
|
||||
targetFormat = bimg.PNG
|
||||
ext = ".png"
|
||||
case "jpg", "jpeg":
|
||||
targetFormat = bimg.JPEG
|
||||
ext = ".jpg"
|
||||
case "avif":
|
||||
targetFormat = bimg.AVIF
|
||||
ext = ".avif"
|
||||
}
|
||||
}
|
||||
|
||||
if newImage, err = bimg.NewImage(newImage).Convert(targetFormat); err != nil {
|
||||
configs.Logger.Error("format conversion failed", zap.Error(err), zap.String("target_format", strings.ToLower(opts.Format)), zap.String("ext", ext))
|
||||
return "", fmt.Errorf("format dönüştürme hatası: %v", err)
|
||||
}
|
||||
|
||||
// 5. Generate Filename and Path
|
||||
filename := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
|
||||
|
||||
// Ensure uploads directory exists
|
||||
// We save to ./uploads/{folder} (root uploads, served by main.go handler)
|
||||
uploadPath := filepath.Join("uploads", opts.Folder)
|
||||
if err := os.MkdirAll(uploadPath, 0755); err != nil {
|
||||
configs.Logger.Error("failed to create upload directory", zap.Error(err), zap.String("upload_path", uploadPath))
|
||||
return "", fmt.Errorf("dizin oluşturma hatası: %v", err)
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(uploadPath, filename)
|
||||
|
||||
// 6. Save File using bimg
|
||||
if err := bimg.Write(fullPath, newImage); err != nil {
|
||||
configs.Logger.Error("failed to write image to disk", zap.Error(err), zap.String("full_path", fullPath))
|
||||
return "", fmt.Errorf("dosya kaydetme hatası: %v", err)
|
||||
}
|
||||
|
||||
configs.Logger.Info("image saved", zap.String("path", fullPath), zap.String("url", fmt.Sprintf("/uploads/%s/%s", opts.Folder, filename)), zap.Int("width", opts.Width), zap.Int("height", opts.Height), zap.String("format", opts.Format), zap.Int("quality", opts.Quality))
|
||||
|
||||
// Return relative path for DB (e.g. /uploads/heroes/123.avif)
|
||||
return fmt.Sprintf("/uploads/%s/%s", opts.Folder, filename), nil
|
||||
}
|
||||
|
||||
// ProcessAndSaveImageFromBytes processes raw image bytes and saves the file similar to ProcessAndSaveImage
|
||||
func ProcessAndSaveImageFromBytes(buffer []byte, opts ImageOptions) (string, error) {
|
||||
configs.Logger.Debug("ProcessAndSaveImageFromBytes called", zap.Int("input_bytes", len(buffer)), zap.Any("options", opts))
|
||||
|
||||
// 1. Process with bimg
|
||||
options := bimg.Options{
|
||||
Width: opts.Width,
|
||||
Height: opts.Height,
|
||||
Quality: opts.Quality,
|
||||
}
|
||||
if opts.Width > 0 && opts.Height > 0 {
|
||||
options.Crop = true
|
||||
options.Gravity = bimg.GravitySmart
|
||||
}
|
||||
options.Enlarge = true
|
||||
options.StripMetadata = true
|
||||
|
||||
newImage, err := bimg.NewImage(buffer).Process(options)
|
||||
if err != nil {
|
||||
configs.Logger.Error("image processing from bytes failed", zap.Error(err), zap.Any("options", options))
|
||||
return "", fmt.Errorf("resim işleme hatası: %v", err)
|
||||
}
|
||||
|
||||
// Convert format
|
||||
targetFormat := bimg.AVIF
|
||||
ext := ".avif"
|
||||
if opts.Format != "" {
|
||||
switch strings.ToLower(opts.Format) {
|
||||
case "webp":
|
||||
targetFormat = bimg.WEBP
|
||||
ext = ".webp"
|
||||
case "png":
|
||||
targetFormat = bimg.PNG
|
||||
ext = ".png"
|
||||
case "jpg", "jpeg":
|
||||
targetFormat = bimg.JPEG
|
||||
ext = ".jpg"
|
||||
case "avif":
|
||||
targetFormat = bimg.AVIF
|
||||
ext = ".avif"
|
||||
}
|
||||
}
|
||||
|
||||
if newImage, err = bimg.NewImage(newImage).Convert(targetFormat); err != nil {
|
||||
configs.Logger.Error("format conversion from bytes failed", zap.Error(err), zap.String("target_format", strings.ToLower(opts.Format)), zap.String("ext", ext))
|
||||
return "", fmt.Errorf("format dönüştürme hatası: %v", err)
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
|
||||
uploadPath := filepath.Join("uploads", opts.Folder)
|
||||
if err := os.MkdirAll(uploadPath, 0755); err != nil {
|
||||
configs.Logger.Error("failed to create upload directory (from bytes)", zap.Error(err), zap.String("upload_path", uploadPath))
|
||||
return "", fmt.Errorf("dizin oluşturma hatası: %v", err)
|
||||
}
|
||||
fullPath := filepath.Join(uploadPath, filename)
|
||||
if err := bimg.Write(fullPath, newImage); err != nil {
|
||||
configs.Logger.Error("failed to write image to disk (from bytes)", zap.Error(err), zap.String("full_path", fullPath))
|
||||
return "", fmt.Errorf("dosya kaydetme hatası: %v", err)
|
||||
}
|
||||
|
||||
configs.Logger.Info("image saved from bytes", zap.String("path", fullPath), zap.String("url", fmt.Sprintf("/uploads/%s/%s", opts.Folder, filename)), zap.Int("width", opts.Width), zap.Int("height", opts.Height), zap.String("format", opts.Format), zap.Int("quality", opts.Quality))
|
||||
|
||||
return fmt.Sprintf("/uploads/%s/%s", opts.Folder, filename), nil
|
||||
}
|
||||
125
services/jwt_service.go
Normal file
125
services/jwt_service.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
configs "ares/config"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
TokenTypeAccess = "access"
|
||||
TokenTypeRefresh = "refresh"
|
||||
)
|
||||
|
||||
type JWTClaim struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
TokenType string `json:"token_type"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
type JWTService struct{}
|
||||
|
||||
func NewJWTService() *JWTService {
|
||||
return &JWTService{}
|
||||
}
|
||||
|
||||
func (s *JWTService) GenerateToken(
|
||||
userID uint,
|
||||
email string,
|
||||
isAdmin bool,
|
||||
firstName string,
|
||||
lastName string,
|
||||
tokenType string,
|
||||
expiration time.Duration,
|
||||
) (string, error) {
|
||||
now := time.Now()
|
||||
claims := &JWTClaim{
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
IsAdmin: isAdmin,
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
TokenType: tokenType,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Subject: strconv.FormatUint(uint64(userID), 10),
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(expiration)),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(configs.AppConfig.JWTSecret))
|
||||
}
|
||||
|
||||
func (s *JWTService) GenerateTokenPair(
|
||||
userID uint,
|
||||
email string,
|
||||
isAdmin bool,
|
||||
firstName string,
|
||||
lastName string,
|
||||
) (string, string, error) {
|
||||
access, err := s.GenerateToken(
|
||||
userID,
|
||||
email,
|
||||
isAdmin,
|
||||
firstName,
|
||||
lastName,
|
||||
TokenTypeAccess,
|
||||
time.Duration(configs.AppConfig.AccessTokenExpireMinutes)*time.Minute,
|
||||
)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
refresh, err := s.GenerateToken(
|
||||
userID,
|
||||
email,
|
||||
isAdmin,
|
||||
firstName,
|
||||
lastName,
|
||||
TokenTypeRefresh,
|
||||
time.Duration(configs.AppConfig.RefreshTokenExpireDays)*24*time.Hour,
|
||||
)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Log generated tokens only in development and prefer project logger if available
|
||||
if configs.AppConfig != nil && configs.AppConfig.Env == "development" {
|
||||
msg := "Generated token pair for user=%d email=%s access_exp=%dm refresh_exp=%dd"
|
||||
if configs.Logger != nil {
|
||||
configs.Logger.Sugar().Debugf(msg, userID, email, configs.AppConfig.AccessTokenExpireMinutes, configs.AppConfig.RefreshTokenExpireDays)
|
||||
configs.Logger.Sugar().Debugf("access: %s", access)
|
||||
configs.Logger.Sugar().Debugf("refresh: %s", refresh)
|
||||
}
|
||||
}
|
||||
|
||||
return access, refresh, nil
|
||||
}
|
||||
|
||||
func (s *JWTService) ValidateToken(signedToken string) (*JWTClaim, error) {
|
||||
token, err := jwt.ParseWithClaims(signedToken, &JWTClaim{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("unexpected signing method")
|
||||
}
|
||||
return []byte(configs.AppConfig.JWTSecret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*JWTClaim)
|
||||
if !ok || !token.Valid {
|
||||
return nil, errors.New("invalid token claims")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
Reference in New Issue
Block a user