first commit
This commit is contained in:
12
pkg/utils/colors.go
Normal file
12
pkg/utils/colors.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package utils
|
||||
|
||||
const (
|
||||
ColorReset = "\033[0m"
|
||||
ColorRed = "\033[31m"
|
||||
ColorGreen = "\033[32m"
|
||||
ColorYellow = "\033[33m"
|
||||
ColorBlue = "\033[34m"
|
||||
ColorPurple = "\033[35m"
|
||||
ColorCyan = "\033[36m"
|
||||
ColorWhite = "\033[37m"
|
||||
)
|
||||
19
pkg/utils/db_utils.go
Normal file
19
pkg/utils/db_utils.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
// IsDuplicateKeyError checks if the error is a PostgreSQL duplicate key violation
|
||||
func IsDuplicateKeyError(err error) bool {
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
// 23505 is the PostgreSQL error code for unique_violation
|
||||
return pgErr.Code == "23505"
|
||||
}
|
||||
// Fallback for other drivers or if error wrapping is different
|
||||
return strings.Contains(err.Error(), "duplicate key value violates unique constraint")
|
||||
}
|
||||
55
pkg/utils/email.go
Normal file
55
pkg/utils/email.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gauth-central/config"
|
||||
"net/smtp"
|
||||
)
|
||||
|
||||
func SendVerificationEmail(toEmail, token string) error {
|
||||
// Get config
|
||||
host := config.AppConfig.EmailHost
|
||||
port := config.AppConfig.EmailPort
|
||||
from := config.AppConfig.EmailFrom
|
||||
if from == "" {
|
||||
from = "noreply@gauth.local"
|
||||
}
|
||||
|
||||
// Construct verification link
|
||||
// Assuming frontend handles verification at /verify-email?token=...
|
||||
// Or backend endpoint directly: /api/v1/auth/verify-email?token=...
|
||||
// Let's use APP_URL from config
|
||||
verifyLink := fmt.Sprintf("%s/v1/auth/verify-email?token=%s", config.AppConfig.AppURL, token)
|
||||
|
||||
// Email content
|
||||
subject := "Subject: Verify your email address\n"
|
||||
mime := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
|
||||
body := fmt.Sprintf(`
|
||||
<html>
|
||||
<body>
|
||||
<h2>Welcome to GAuth-Central!</h2>
|
||||
<p>Please click the link below to verify your email address:</p>
|
||||
<p><a href="%s">Verify Email</a></p>
|
||||
<p>Or copy and paste this link: %s</p>
|
||||
</body>
|
||||
</html>
|
||||
`, verifyLink, verifyLink)
|
||||
|
||||
msg := []byte(subject + mime + body)
|
||||
|
||||
// Address
|
||||
addr := fmt.Sprintf("%s:%s", host, port)
|
||||
|
||||
// Auth (if needed)
|
||||
var auth smtp.Auth
|
||||
if config.AppConfig.EmailHostUser != "" && config.AppConfig.EmailHostPassword != "" {
|
||||
auth = smtp.PlainAuth("", config.AppConfig.EmailHostUser, config.AppConfig.EmailHostPassword, host)
|
||||
}
|
||||
|
||||
// Send email
|
||||
if err := smtp.SendMail(addr, auth, from, []string{toEmail}, msg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
137
pkg/utils/image_processor.go
Normal file
137
pkg/utils/image_processor.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gauth-central/config"
|
||||
|
||||
"github.com/chai2010/webp"
|
||||
"github.com/disintegration/imaging"
|
||||
)
|
||||
|
||||
type ImageOptions struct {
|
||||
Width int
|
||||
Height int
|
||||
Quality float32 // 1-100 (WebP uses float32)
|
||||
Format string // "webp", "jpg", "png"
|
||||
Mode string // "cover", "contain", "resize"
|
||||
}
|
||||
|
||||
// SaveOptimizedImage processes and saves an image
|
||||
// Returns the relative path to the saved file (e.g., "/uploads/avatars/filename.webp")
|
||||
func SaveOptimizedImage(fileHeader *multipart.FileHeader, uploadDir string, userID string, opts *ImageOptions) (string, error) {
|
||||
// If opts is nil, use defaults from config
|
||||
if opts == nil {
|
||||
opts = &ImageOptions{
|
||||
Width: config.AppConfig.AvatarWidth,
|
||||
Height: config.AppConfig.AvatarHeight,
|
||||
Quality: float32(config.AppConfig.AvatarQuality),
|
||||
Format: config.AppConfig.AvatarFormat,
|
||||
Mode: config.AppConfig.AvatarMode,
|
||||
}
|
||||
}
|
||||
|
||||
// Open the file
|
||||
srcFile, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open file: %v", err)
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
// Decode image
|
||||
img, _, err := image.Decode(srcFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode image: %v", err)
|
||||
}
|
||||
|
||||
// Resize logic
|
||||
if opts.Width > 0 || opts.Height > 0 {
|
||||
switch strings.ToLower(opts.Mode) {
|
||||
case "cover":
|
||||
// Fill requires both dimensions to be effective for cropping.
|
||||
// If one is missing, we fall back to Resize which preserves aspect ratio.
|
||||
if opts.Width > 0 && opts.Height > 0 {
|
||||
img = imaging.Fill(img, opts.Width, opts.Height, imaging.Center, imaging.Lanczos)
|
||||
} else {
|
||||
img = imaging.Resize(img, opts.Width, opts.Height, imaging.Lanczos)
|
||||
}
|
||||
case "contain":
|
||||
// Fit fits the image within the box, preserving aspect ratio.
|
||||
// If one dimension is 0, Fit might behave like Resize(w, h) if implementation allows,
|
||||
// but imaging.Fit usually expects a box.
|
||||
// If one is 0, we assume the user wants to limit the other dimension.
|
||||
if opts.Width > 0 && opts.Height > 0 {
|
||||
img = imaging.Fit(img, opts.Width, opts.Height, imaging.Lanczos)
|
||||
} else {
|
||||
img = imaging.Resize(img, opts.Width, opts.Height, imaging.Lanczos)
|
||||
}
|
||||
default:
|
||||
// "resize" or empty: Resize preserves aspect ratio if one arg is 0.
|
||||
// If both are provided, it stretches unless we use Fill/Fit.
|
||||
// imaging.Resize stretches if both are non-zero.
|
||||
img = imaging.Resize(img, opts.Width, opts.Height, imaging.Lanczos)
|
||||
}
|
||||
}
|
||||
|
||||
// Determine output format and filename
|
||||
ext := "." + strings.ToLower(opts.Format)
|
||||
if opts.Format == "" {
|
||||
ext = ".webp" // Default to WebP
|
||||
opts.Format = "webp"
|
||||
}
|
||||
|
||||
// Generate filename
|
||||
filename := fmt.Sprintf("%s_%d%s", userID, time.Now().UnixNano(), ext)
|
||||
|
||||
// Ensure directory exists
|
||||
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("failed to create directory: %v", err)
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(uploadDir, filename)
|
||||
outFile, err := os.Create(fullPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create output file: %v", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
// Encode
|
||||
switch strings.ToLower(opts.Format) {
|
||||
case "jpg", "jpeg":
|
||||
quality := int(opts.Quality)
|
||||
if quality < 1 || quality > 100 {
|
||||
quality = 90
|
||||
}
|
||||
err = jpeg.Encode(outFile, img, &jpeg.Options{Quality: quality})
|
||||
case "png":
|
||||
err = png.Encode(outFile, img) // PNG is lossless
|
||||
case "webp", "":
|
||||
err = webp.Encode(outFile, img, &webp.Options{Lossless: false, Quality: opts.Quality})
|
||||
default:
|
||||
// Fallback to WebP
|
||||
err = webp.Encode(outFile, img, &webp.Options{Lossless: false, Quality: opts.Quality})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to encode image: %v", err)
|
||||
}
|
||||
|
||||
// Return relative path
|
||||
relPath := filepath.Join(uploadDir, filename)
|
||||
if strings.HasPrefix(relPath, ".") {
|
||||
relPath = relPath[1:]
|
||||
}
|
||||
if !strings.HasPrefix(relPath, "/") {
|
||||
relPath = "/" + relPath
|
||||
}
|
||||
|
||||
return relPath, nil
|
||||
}
|
||||
15
pkg/utils/password.go
Normal file
15
pkg/utils/password.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func CheckPasswordHash(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
15
pkg/utils/token.go
Normal file
15
pkg/utils/token.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// GenerateSecureToken returns a cryptographically random hex string (e.g. for email verification).
|
||||
func GenerateSecureToken(byteLength int) (string, error) {
|
||||
b := make([]byte, byteLength)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
Reference in New Issue
Block a user