Initial commit - Lost and Found Revisi
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Database DatabaseConfig
|
||||
JWT JWTConfig
|
||||
Server ServerConfig
|
||||
Groq GroqConfig // NEW: Groq configuration
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port string
|
||||
Environment string
|
||||
UploadPath string
|
||||
MaxUploadSize int64
|
||||
AllowedOrigins []string
|
||||
}
|
||||
|
||||
// NEW: Groq configuration struct
|
||||
type GroqConfig struct {
|
||||
APIKey string
|
||||
DefaultModel string
|
||||
MaxTokens int
|
||||
Temperature float64
|
||||
TopP float64
|
||||
}
|
||||
|
||||
func GetConfig() *Config {
|
||||
return &Config{
|
||||
Database: GetDatabaseConfig(),
|
||||
JWT: GetJWTConfig(),
|
||||
Server: GetServerConfig(),
|
||||
Groq: GetGroqConfig(), // NEW
|
||||
}
|
||||
}
|
||||
|
||||
func GetServerConfig() ServerConfig {
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
|
||||
env := os.Getenv("ENVIRONMENT")
|
||||
if env == "" {
|
||||
env = "development"
|
||||
}
|
||||
|
||||
uploadPath := os.Getenv("UPLOAD_PATH")
|
||||
if uploadPath == "" {
|
||||
uploadPath = "./uploads"
|
||||
}
|
||||
|
||||
return ServerConfig{
|
||||
Port: port,
|
||||
Environment: env,
|
||||
UploadPath: uploadPath,
|
||||
MaxUploadSize: 10 * 1024 * 1024,
|
||||
AllowedOrigins: []string{"*"},
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: Get Groq configuration from environment
|
||||
func GetGroqConfig() GroqConfig {
|
||||
apiKey := os.Getenv("GROQ_API_KEY")
|
||||
|
||||
model := os.Getenv("GROQ_MODEL")
|
||||
if model == "" {
|
||||
model = "llama-3.3-70b-versatile" // Default to best model
|
||||
}
|
||||
|
||||
return GroqConfig{
|
||||
APIKey: apiKey,
|
||||
DefaultModel: model,
|
||||
MaxTokens: 1024,
|
||||
Temperature: 0.7,
|
||||
TopP: 0.95,
|
||||
}
|
||||
}
|
||||
|
||||
func IsProduction() bool {
|
||||
return os.Getenv("ENVIRONMENT") == "production"
|
||||
}
|
||||
|
||||
func IsDevelopment() bool {
|
||||
return os.Getenv("ENVIRONMENT") != "production"
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"lost-and-found/internal/models"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
var db *gorm.DB
|
||||
|
||||
// DatabaseConfig holds database connection configuration
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port string
|
||||
User string
|
||||
Password string
|
||||
DBName string
|
||||
Charset string
|
||||
ParseTime string
|
||||
Loc string
|
||||
}
|
||||
|
||||
// GetDatabaseConfig returns database configuration from environment
|
||||
func GetDatabaseConfig() DatabaseConfig {
|
||||
return DatabaseConfig{
|
||||
Host: getEnv("DB_HOST", ""),
|
||||
Port: getEnv("DB_PORT", ""),
|
||||
User: getEnv("DB_USER", ""),
|
||||
Password: getEnv("DB_PASSWORD", ""),
|
||||
DBName: getEnv("DB_NAME", ""),
|
||||
Charset: getEnv("DB_CHARSET", "utf8mb4"),
|
||||
ParseTime: getEnv("DB_PARSE_TIME", "True"),
|
||||
Loc: getEnv("DB_LOC", "Local"),
|
||||
}
|
||||
}
|
||||
|
||||
// InitDB initializes database connection
|
||||
func InitDB() error {
|
||||
config := GetDatabaseConfig()
|
||||
|
||||
// Step 1: Connect to MySQL without specifying database (to create if not exists)
|
||||
if err := ensureDatabaseExists(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Step 2: Connect to the specific database
|
||||
dsn := fmt.Sprintf(
|
||||
"%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=%s&loc=%s&multiStatements=true",
|
||||
config.User,
|
||||
config.Password,
|
||||
config.Host,
|
||||
config.Port,
|
||||
config.DBName,
|
||||
config.Charset,
|
||||
config.ParseTime,
|
||||
config.Loc,
|
||||
)
|
||||
|
||||
// Configure GORM logger
|
||||
gormLogger := logger.Default
|
||||
if IsDevelopment() {
|
||||
gormLogger = logger.Default.LogMode(logger.Info)
|
||||
} else {
|
||||
gormLogger = logger.Default.LogMode(logger.Error)
|
||||
}
|
||||
|
||||
// Open database connection
|
||||
var err error
|
||||
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||
Logger: gormLogger,
|
||||
NowFunc: func() time.Time {
|
||||
return time.Now().Local()
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
// Get underlying SQL database
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get database instance: %w", err)
|
||||
}
|
||||
|
||||
// Set connection pool settings
|
||||
sqlDB.SetMaxIdleConns(10)
|
||||
sqlDB.SetMaxOpenConns(100)
|
||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||
|
||||
// Test connection
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
return fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
log.Println("✅ Database connected successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureDatabaseExists checks if database exists, creates it if not
|
||||
func ensureDatabaseExists(config DatabaseConfig) error {
|
||||
// Connect to MySQL without specifying a database
|
||||
dsn := fmt.Sprintf(
|
||||
"%s:%s@tcp(%s:%s)/?charset=%s&parseTime=%s&loc=%s",
|
||||
config.User,
|
||||
config.Password,
|
||||
config.Host,
|
||||
config.Port,
|
||||
config.Charset,
|
||||
config.ParseTime,
|
||||
config.Loc,
|
||||
)
|
||||
|
||||
tempDB, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to MySQL server: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("🔍 Checking if database '%s' exists...", config.DBName)
|
||||
|
||||
// Check if database exists
|
||||
var dbExists int64
|
||||
if err := tempDB.Raw(
|
||||
"SELECT COUNT(*) FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = ?",
|
||||
config.DBName,
|
||||
).Scan(&dbExists).Error; err != nil {
|
||||
return fmt.Errorf("failed to check database existence: %w", err)
|
||||
}
|
||||
|
||||
if dbExists == 0 {
|
||||
log.Printf("📝 Creating database '%s'...", config.DBName)
|
||||
createSQL := fmt.Sprintf(
|
||||
"CREATE DATABASE IF NOT EXISTS %s CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci",
|
||||
config.DBName,
|
||||
)
|
||||
if err := tempDB.Exec(createSQL).Error; err != nil {
|
||||
return fmt.Errorf("failed to create database: %w", err)
|
||||
}
|
||||
log.Printf("✅ Database '%s' created successfully", config.DBName)
|
||||
} else {
|
||||
log.Printf("✅ Database '%s' already exists", config.DBName)
|
||||
}
|
||||
|
||||
// Close temporary connection
|
||||
sqlDB, _ := tempDB.DB()
|
||||
sqlDB.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDB returns the database instance
|
||||
func GetDB() *gorm.DB {
|
||||
return db
|
||||
}
|
||||
|
||||
// RunMigrations runs database migrations from SQL files
|
||||
func RunMigrations(db *gorm.DB) error {
|
||||
log.Println("📊 Starting database migrations...")
|
||||
|
||||
// Check if tables already exist
|
||||
if db.Migrator().HasTable(&models.Role{}) {
|
||||
log.Println("✅ Database tables already exist, skipping migration")
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Println("📋 Tables not found, running migration scripts...")
|
||||
|
||||
// Step 1: Run schema.sql
|
||||
if err := runSQLFile(db, "database/schema.sql"); err != nil {
|
||||
return fmt.Errorf("❌ Failed to run schema.sql: %w", err)
|
||||
}
|
||||
log.Println("✅ Schema created successfully")
|
||||
|
||||
// Step 2: Run seed.sql
|
||||
if err := runSQLFile(db, "database/seed.sql"); err != nil {
|
||||
return fmt.Errorf("❌ Failed to run seed.sql: %w", err)
|
||||
}
|
||||
log.Println("✅ Seed data inserted successfully")
|
||||
|
||||
// Step 3: Run enhancement.sql (optional - for triggers, procedures, etc)
|
||||
if err := runSQLFile(db, "database/enhancement.sql"); err != nil {
|
||||
log.Printf("⚠️ Warning: Failed to run enhancement.sql: %v", err)
|
||||
log.Println("💡 Enhancement features (triggers, procedures) may not be available")
|
||||
} else {
|
||||
log.Println("✅ Enhancement features loaded successfully")
|
||||
}
|
||||
|
||||
log.Println("🎉 Database migration completed!")
|
||||
log.Println("🔧 Default admin: admin@lostandfound.com / password123")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runSQLFile executes SQL from file
|
||||
func runSQLFile(db *gorm.DB, filepath string) error {
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(filepath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("file not found: %s", filepath)
|
||||
}
|
||||
|
||||
log.Printf("📄 Reading SQL file: %s", filepath)
|
||||
|
||||
// Read file
|
||||
content, err := os.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
// Split SQL content by delimiter
|
||||
sqlContent := string(content)
|
||||
|
||||
// Remove comments and empty lines
|
||||
sqlContent = removeComments(sqlContent)
|
||||
|
||||
// Split by DELIMITER if exists (for procedures/triggers)
|
||||
if strings.Contains(sqlContent, "DELIMITER") {
|
||||
return executeSQLWithDelimiter(db, sqlContent)
|
||||
}
|
||||
|
||||
// Execute SQL normally
|
||||
if err := db.Exec(sqlContent).Error; err != nil {
|
||||
return fmt.Errorf("failed to execute SQL: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✅ SQL file executed: %s", filepath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeSQLWithDelimiter handles SQL with custom delimiters (for procedures/triggers)
|
||||
func executeSQLWithDelimiter(db *gorm.DB, content string) error {
|
||||
// Split by DELIMITER changes
|
||||
parts := strings.Split(content, "DELIMITER")
|
||||
|
||||
for i, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" || part == "$$" || part == ";" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Remove the delimiter declaration line
|
||||
lines := strings.Split(part, "\n")
|
||||
if len(lines) > 0 && (strings.HasPrefix(lines[0], "$$") || strings.HasPrefix(lines[0], ";")) {
|
||||
lines = lines[1:]
|
||||
}
|
||||
part = strings.Join(lines, "\n")
|
||||
|
||||
// Split by custom delimiter ($$)
|
||||
if i%2 == 1 { // Odd parts use $$ delimiter
|
||||
statements := strings.Split(part, "$$")
|
||||
for _, stmt := range statements {
|
||||
stmt = strings.TrimSpace(stmt)
|
||||
if stmt == "" || stmt == ";" {
|
||||
continue
|
||||
}
|
||||
if err := db.Exec(stmt).Error; err != nil {
|
||||
log.Printf("⚠️ Warning executing statement: %v", err)
|
||||
// Don't fail on enhancement errors (triggers, procedures)
|
||||
// These might fail if they already exist or MySQL version issues
|
||||
}
|
||||
}
|
||||
} else { // Even parts use ; delimiter
|
||||
statements := strings.Split(part, ";")
|
||||
for _, stmt := range statements {
|
||||
stmt = strings.TrimSpace(stmt)
|
||||
if stmt == "" {
|
||||
continue
|
||||
}
|
||||
if err := db.Exec(stmt).Error; err != nil {
|
||||
return fmt.Errorf("failed to execute statement: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeComments removes SQL comments from content
|
||||
func removeComments(sql string) string {
|
||||
lines := strings.Split(sql, "\n")
|
||||
var cleaned []string
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Skip empty lines
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip single-line comments
|
||||
if strings.HasPrefix(line, "--") || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Keep the line
|
||||
cleaned = append(cleaned, line)
|
||||
}
|
||||
|
||||
return strings.Join(cleaned, "\n")
|
||||
}
|
||||
|
||||
// CloseDB closes database connection
|
||||
func CloseDB() error {
|
||||
if db != nil {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sqlDB.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper function to get environment variable with default value
|
||||
func getEnv(key, defaultValue string) string {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// internal/config/jwt.go
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var jwtConfig *JWTConfig
|
||||
|
||||
// JWTConfig holds JWT configuration
|
||||
type JWTConfig struct {
|
||||
SecretKey string
|
||||
ExpirationHours int
|
||||
Issuer string
|
||||
}
|
||||
|
||||
// JWTClaims represents the JWT claims
|
||||
type JWTClaims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// InitJWT initializes JWT configuration
|
||||
func InitJWT() {
|
||||
secretKey := os.Getenv("JWT_SECRET_KEY")
|
||||
if secretKey == "" {
|
||||
secretKey = "your-secret-key-change-this-in-production" // Default for development
|
||||
}
|
||||
|
||||
jwtConfig = &JWTConfig{
|
||||
SecretKey: secretKey,
|
||||
ExpirationHours: 24 * 7, // 7 days
|
||||
Issuer: "lost-and-found-system",
|
||||
}
|
||||
}
|
||||
|
||||
// GetJWTConfig returns JWT configuration
|
||||
func GetJWTConfig() JWTConfig {
|
||||
if jwtConfig == nil {
|
||||
InitJWT()
|
||||
}
|
||||
return *jwtConfig
|
||||
}
|
||||
|
||||
// GenerateToken generates a new JWT token for a user
|
||||
func GenerateToken(userID uint, email, role string) (string, error) {
|
||||
config := GetJWTConfig()
|
||||
|
||||
// Create claims
|
||||
claims := JWTClaims{
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
Role: role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * time.Duration(config.ExpirationHours))),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: config.Issuer,
|
||||
},
|
||||
}
|
||||
|
||||
// Create token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
// Sign token with secret key
|
||||
tokenString, err := token.SignedString([]byte(config.SecretKey))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// ValidateToken validates a JWT token and returns the claims
|
||||
func ValidateToken(tokenString string) (*JWTClaims, error) {
|
||||
config := GetJWTConfig()
|
||||
|
||||
// Parse token
|
||||
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
// Validate signing method
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("invalid signing method")
|
||||
}
|
||||
return []byte(config.SecretKey), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Extract claims
|
||||
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
// RefreshToken generates a new token with extended expiration
|
||||
func RefreshToken(oldTokenString string) (string, error) {
|
||||
// Validate old token
|
||||
claims, err := ValidateToken(oldTokenString)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Generate new token with same user info
|
||||
return GenerateToken(claims.UserID, claims.Email, claims.Role)
|
||||
}
|
||||
|
||||
// ExtractUserID extracts user ID from token string
|
||||
func ExtractUserID(tokenString string) (uint, error) {
|
||||
claims, err := ValidateToken(tokenString)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return claims.UserID, nil
|
||||
}
|
||||
|
||||
// ExtractRole extracts role from token string
|
||||
func ExtractRole(tokenString string) (string, error) {
|
||||
claims, err := ValidateToken(tokenString)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return claims.Role, nil
|
||||
}
|
||||
Reference in New Issue
Block a user