Initial commit - Lost and Found Revisi

This commit is contained in:
bambang-code1
2025-12-20 00:01:08 +07:00
commit f4c838471c
210 changed files with 30706 additions and 0 deletions
+89
View File
@@ -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"
}
+333
View File
@@ -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
}
+132
View File
@@ -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
}