package utils import ( "crypto/rand" "encoding/hex" "encoding/json" "errors" "fmt" "khairul169/garage-webui/schema" "os" "path/filepath" "strings" "sync" "time" "golang.org/x/crypto/bcrypt" ) type Database struct { Users map[string]*schema.User `json:"users"` Tenants map[string]*schema.Tenant `json:"tenants"` Sessions map[string]*schema.Session `json:"sessions"` mutex sync.RWMutex } var DB = &Database{ Users: make(map[string]*schema.User), Tenants: make(map[string]*schema.Tenant), Sessions: make(map[string]*schema.Session), } func InitDatabase() error { // Create data directory if it doesn't exist dataDir := GetEnv("DATA_DIR", "./data") if err := os.MkdirAll(dataDir, 0755); err != nil { return fmt.Errorf("failed to create data directory: %w", err) } // Load existing data if err := DB.Load(); err != nil { return fmt.Errorf("failed to load database: %w", err) } // Create default admin user if no users exist if len(DB.Users) == 0 { if err := DB.CreateDefaultAdmin(); err != nil { return fmt.Errorf("failed to create default admin: %w", err) } } return nil } func (db *Database) Load() error { db.mutex.Lock() defer db.mutex.Unlock() dataPath := filepath.Join(GetEnv("DATA_DIR", "./data"), "database.json") // If file doesn't exist, start with empty database if _, err := os.Stat(dataPath); os.IsNotExist(err) { return nil } data, err := os.ReadFile(dataPath) if err != nil { return err } return json.Unmarshal(data, db) } func (db *Database) Save() error { fmt.Println("Save: Attempting to acquire lock") db.mutex.Lock() defer db.mutex.Unlock() fmt.Println("Save: Lock acquired, marshaling data") dataPath := filepath.Join(GetEnv("DATA_DIR", "./data"), "database.json") data, err := json.MarshalIndent(db, "", " ") if err != nil { fmt.Printf("Save: Marshal failed: %v\n", err) return err } fmt.Println("Save: Data marshaled, writing to file") return os.WriteFile(dataPath, data, 0600) } // saveUnsafe saves without acquiring locks (for use when lock is already held) func (db *Database) saveUnsafe() error { fmt.Println("saveUnsafe: Marshaling data without lock") dataPath := filepath.Join(GetEnv("DATA_DIR", "./data"), "database.json") data, err := json.MarshalIndent(db, "", " ") if err != nil { fmt.Printf("saveUnsafe: Marshal failed: %v\n", err) return err } fmt.Println("saveUnsafe: Data marshaled, writing to file") return os.WriteFile(dataPath, data, 0600) } func (db *Database) CreateDefaultAdmin() error { // Check if we should create from environment variables (legacy support) userPass := strings.Split(GetEnv("AUTH_USER_PASS", ""), ":") if len(userPass) >= 2 { return db.createUserFromEnv(userPass[0], userPass[1]) } // Create default admin user defaultPassword := "admin" fmt.Printf("Creating default admin user with password: %s\n", defaultPassword) fmt.Println("IMPORTANT: Change this password after first login!") hashedPassword, err := bcrypt.GenerateFromPassword([]byte(defaultPassword), bcrypt.DefaultCost) if err != nil { return err } admin := &schema.User{ ID: GenerateID(), Username: "admin", Email: "admin@localhost", PasswordHash: string(hashedPassword), Role: schema.RoleAdmin, Enabled: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } db.Users[admin.ID] = admin return db.Save() } func (db *Database) createUserFromEnv(username, passwordHash string) error { admin := &schema.User{ ID: GenerateID(), Username: username, Email: username + "@localhost", PasswordHash: passwordHash, Role: schema.RoleAdmin, Enabled: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } db.Users[admin.ID] = admin return db.Save() } // User operations func (db *Database) CreateUser(req *schema.CreateUserRequest) (*schema.User, error) { db.mutex.Lock() defer db.mutex.Unlock() // Check if username already exists for _, user := range db.Users { if user.Username == req.Username { return nil, errors.New("username already exists") } if user.Email == req.Email { return nil, errors.New("email already exists") } } // Hash password hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { return nil, err } user := &schema.User{ ID: GenerateID(), Username: req.Username, Email: req.Email, PasswordHash: string(hashedPassword), Role: req.Role, TenantID: req.TenantID, Enabled: req.Enabled, CreatedAt: time.Now(), UpdatedAt: time.Now(), } db.Users[user.ID] = user if err := db.Save(); err != nil { return nil, err } return user, nil } func (db *Database) GetUser(id string) (*schema.User, error) { db.mutex.RLock() defer db.mutex.RUnlock() user, exists := db.Users[id] if !exists { return nil, errors.New("user not found") } return user, nil } func (db *Database) GetUserByUsername(username string) (*schema.User, error) { db.mutex.RLock() defer db.mutex.RUnlock() for _, user := range db.Users { if user.Username == username { return user, nil } } return nil, errors.New("user not found") } func (db *Database) UpdateUser(id string, req *schema.UpdateUserRequest) (*schema.User, error) { db.mutex.Lock() defer db.mutex.Unlock() user, exists := db.Users[id] if !exists { return nil, errors.New("user not found") } if req.Username != nil { user.Username = *req.Username } if req.Email != nil { user.Email = *req.Email } if req.Password != nil { hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost) if err != nil { return nil, err } user.PasswordHash = string(hashedPassword) } if req.Role != nil { user.Role = *req.Role } if req.TenantID != nil { user.TenantID = req.TenantID } if req.Enabled != nil { user.Enabled = *req.Enabled } user.UpdatedAt = time.Now() if err := db.Save(); err != nil { return nil, err } return user, nil } func (db *Database) DeleteUser(id string) error { db.mutex.Lock() defer db.mutex.Unlock() if _, exists := db.Users[id]; !exists { return errors.New("user not found") } delete(db.Users, id) return db.Save() } func (db *Database) ListUsers() ([]*schema.User, error) { db.mutex.RLock() defer db.mutex.RUnlock() users := make([]*schema.User, 0, len(db.Users)) for _, user := range db.Users { users = append(users, user) } return users, nil } // Tenant operations func (db *Database) CreateTenant(req *schema.CreateTenantRequest) (*schema.Tenant, error) { db.mutex.Lock() defer db.mutex.Unlock() // Check if name already exists for _, tenant := range db.Tenants { if tenant.Name == req.Name { return nil, errors.New("tenant name already exists") } } tenant := &schema.Tenant{ ID: GenerateID(), Name: req.Name, Description: req.Description, Enabled: req.Enabled, MaxBuckets: req.MaxBuckets, MaxKeys: req.MaxKeys, QuotaBytes: req.QuotaBytes, CreatedAt: time.Now(), UpdatedAt: time.Now(), } db.Tenants[tenant.ID] = tenant if err := db.Save(); err != nil { return nil, err } return tenant, nil } func (db *Database) GetTenant(id string) (*schema.Tenant, error) { db.mutex.RLock() defer db.mutex.RUnlock() tenant, exists := db.Tenants[id] if !exists { return nil, errors.New("tenant not found") } return tenant, nil } func (db *Database) UpdateTenant(id string, req *schema.UpdateTenantRequest) (*schema.Tenant, error) { db.mutex.Lock() defer db.mutex.Unlock() tenant, exists := db.Tenants[id] if !exists { return nil, errors.New("tenant not found") } if req.Name != nil { tenant.Name = *req.Name } if req.Description != nil { tenant.Description = *req.Description } if req.Enabled != nil { tenant.Enabled = *req.Enabled } if req.MaxBuckets != nil { tenant.MaxBuckets = *req.MaxBuckets } if req.MaxKeys != nil { tenant.MaxKeys = *req.MaxKeys } if req.QuotaBytes != nil { tenant.QuotaBytes = req.QuotaBytes } tenant.UpdatedAt = time.Now() if err := db.Save(); err != nil { return nil, err } return tenant, nil } func (db *Database) DeleteTenant(id string) error { db.mutex.Lock() defer db.mutex.Unlock() if _, exists := db.Tenants[id]; !exists { return errors.New("tenant not found") } delete(db.Tenants, id) return db.Save() } func (db *Database) ListTenants() ([]*schema.Tenant, error) { db.mutex.RLock() defer db.mutex.RUnlock() tenants := make([]*schema.Tenant, 0, len(db.Tenants)) for _, tenant := range db.Tenants { tenants = append(tenants, tenant) } return tenants, nil } // Session operations func (db *Database) CreateSession(userID string) (*schema.Session, error) { fmt.Println("CreateSession: Starting session creation") db.mutex.Lock() defer db.mutex.Unlock() fmt.Println("CreateSession: Generating token") token, err := GenerateToken() if err != nil { fmt.Printf("CreateSession: Token generation failed: %v\n", err) return nil, err } fmt.Println("CreateSession: Token generated successfully") session := &schema.Session{ ID: GenerateID(), UserID: userID, Token: token, ExpiresAt: time.Now().Add(24 * time.Hour), // 24 hours expiry CreatedAt: time.Now(), } db.Sessions[session.ID] = session fmt.Println("CreateSession: Session stored in memory") fmt.Println("CreateSession: Saving to database") if err := db.saveUnsafe(); err != nil { fmt.Printf("CreateSession: Database save failed: %v\n", err) return nil, err } fmt.Println("CreateSession: Database saved successfully") return session, nil } func (db *Database) GetSessionByToken(token string) (*schema.Session, error) { db.mutex.RLock() defer db.mutex.RUnlock() for _, session := range db.Sessions { if session.Token == token { if time.Now().After(session.ExpiresAt) { return nil, errors.New("session expired") } return session, nil } } return nil, errors.New("session not found") } func (db *Database) DeleteSession(id string) error { db.mutex.Lock() defer db.mutex.Unlock() delete(db.Sessions, id) return db.saveUnsafe() } func (db *Database) CleanupExpiredSessions() error { db.mutex.Lock() defer db.mutex.Unlock() now := time.Now() for id, session := range db.Sessions { if now.After(session.ExpiresAt) { delete(db.Sessions, id) } } return db.saveUnsafe() } // Utility functions func GenerateID() string { bytes := make([]byte, 16) rand.Read(bytes) return hex.EncodeToString(bytes) } func GenerateToken() (string, error) { bytes := make([]byte, 32) if _, err := rand.Read(bytes); err != nil { return "", err } return hex.EncodeToString(bytes), nil } // AuthenticateUser validates credentials and returns user func (db *Database) AuthenticateUser(username, password string) (*schema.User, error) { user, err := db.GetUserByUsername(username) if err != nil { return nil, errors.New("invalid credentials") } if !user.Enabled { return nil, errors.New("user account is disabled") } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil { return nil, errors.New("invalid credentials") } // Update last login user.LastLogin = &[]time.Time{time.Now()}[0] // Note: last login time will be saved when session is created return user, nil }