Files
honeyDueAPI/internal/database/database.go
Trey t c7dc56e2d2 Rebrand from MyCrib to Casera
- Update Go module from mycrib-api to casera-api
- Update all import statements across 69 Go files
- Update admin panel branding (title, sidebar, login form)
- Update email templates (subjects, bodies, signatures)
- Update PDF report generation branding
- Update Docker container names and network
- Update config defaults (database name, email sender, APNS topic)
- Update README and documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 21:10:48 -06:00

443 lines
14 KiB
Go

package database
import (
"fmt"
"time"
"github.com/rs/zerolog/log"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/treytartt/casera-api/internal/config"
"github.com/treytartt/casera-api/internal/models"
)
var db *gorm.DB
// Connect establishes a connection to the PostgreSQL database
func Connect(cfg *config.DatabaseConfig, debug bool) (*gorm.DB, error) {
// Configure GORM logger
logLevel := logger.Silent
if debug {
logLevel = logger.Info
}
gormConfig := &gorm.Config{
Logger: logger.Default.LogMode(logLevel),
NowFunc: func() time.Time {
return time.Now().UTC()
},
PrepareStmt: true, // Cache prepared statements
}
// Connect to database
var err error
db, err = gorm.Open(postgres.Open(cfg.DSN()), gormConfig)
if err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
// Get underlying sql.DB for connection pool settings
sqlDB, err := db.DB()
if err != nil {
return nil, fmt.Errorf("failed to get underlying sql.DB: %w", err)
}
// Configure connection pool
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
sqlDB.SetConnMaxLifetime(cfg.MaxLifetime)
// Test connection
if err := sqlDB.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
log.Info().
Str("host", cfg.Host).
Int("port", cfg.Port).
Str("database", cfg.Database).
Msg("Connected to PostgreSQL database")
return db, nil
}
// Get returns the database instance
func Get() *gorm.DB {
return db
}
// Close closes the database connection
func Close() error {
if db != nil {
sqlDB, err := db.DB()
if err != nil {
return err
}
return sqlDB.Close()
}
return nil
}
// WithTransaction executes a function within a database transaction
func WithTransaction(fn func(tx *gorm.DB) error) error {
return db.Transaction(fn)
}
// Paginate returns a GORM scope for pagination
func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 100
}
if pageSize > 1000 {
pageSize = 1000
}
offset := (page - 1) * pageSize
return db.Offset(offset).Limit(pageSize)
}
}
// Migrate runs database migrations for all models
func Migrate() error {
log.Info().Msg("Running database migrations...")
// Migrate all models in order (respecting foreign key constraints)
err := db.AutoMigrate(
// Lookup tables first (no foreign keys)
&models.ResidenceType{},
&models.TaskCategory{},
&models.TaskPriority{},
&models.TaskFrequency{},
&models.TaskStatus{},
&models.ContractorSpecialty{},
// User and auth tables
&models.User{},
&models.AuthToken{},
&models.UserProfile{},
&models.ConfirmationCode{},
&models.PasswordResetCode{},
// Admin users (separate from app users)
&models.AdminUser{},
// Main entity tables (order matters for foreign keys!)
&models.Residence{},
&models.ResidenceShareCode{},
&models.Contractor{}, // Contractor before Task (Task references Contractor)
&models.Task{},
&models.TaskCompletion{},
&models.TaskCompletionImage{}, // Multiple images per completion
&models.Document{},
&models.DocumentImage{}, // Multiple images per document
// Notification tables
&models.Notification{},
&models.NotificationPreference{},
&models.APNSDevice{},
&models.GCMDevice{},
// Subscription tables
&models.SubscriptionSettings{},
&models.UserSubscription{},
&models.UpgradeTrigger{},
&models.FeatureBenefit{},
&models.Promotion{},
&models.TierLimits{},
)
if err != nil {
return fmt.Errorf("failed to run migrations: %w", err)
}
// Run GoAdmin migrations
if err := migrateGoAdmin(); err != nil {
return fmt.Errorf("failed to run GoAdmin migrations: %w", err)
}
log.Info().Msg("Database migrations completed successfully")
return nil
}
// migrateGoAdmin creates GoAdmin required tables
func migrateGoAdmin() error {
log.Info().Msg("Running GoAdmin migrations...")
// GoAdmin session table
if err := db.Exec(`
CREATE TABLE IF NOT EXISTS goadmin_session (
id SERIAL PRIMARY KEY,
sid VARCHAR(50) NOT NULL DEFAULT '',
"values" TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`).Error; err != nil {
return err
}
db.Exec(`CREATE INDEX IF NOT EXISTS idx_goadmin_session_sid ON goadmin_session(sid)`)
// GoAdmin users table
if err := db.Exec(`
CREATE TABLE IF NOT EXISTS goadmin_users (
id SERIAL PRIMARY KEY,
username VARCHAR(100) NOT NULL DEFAULT '',
password VARCHAR(100) NOT NULL DEFAULT '',
name VARCHAR(100) NOT NULL DEFAULT '',
avatar VARCHAR(255) DEFAULT '',
remember_token VARCHAR(100) DEFAULT '',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`).Error; err != nil {
return err
}
db.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_goadmin_users_username ON goadmin_users(username)`)
// GoAdmin roles table
if err := db.Exec(`
CREATE TABLE IF NOT EXISTS goadmin_roles (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL DEFAULT '',
slug VARCHAR(50) NOT NULL DEFAULT '',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`).Error; err != nil {
return err
}
db.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_goadmin_roles_slug ON goadmin_roles(slug)`)
// GoAdmin permissions table
if err := db.Exec(`
CREATE TABLE IF NOT EXISTS goadmin_permissions (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL DEFAULT '',
slug VARCHAR(50) NOT NULL DEFAULT '',
http_method VARCHAR(255) DEFAULT '',
http_path TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`).Error; err != nil {
return err
}
db.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_goadmin_permissions_slug ON goadmin_permissions(slug)`)
// GoAdmin role_users table
if err := db.Exec(`
CREATE TABLE IF NOT EXISTS goadmin_role_users (
id SERIAL PRIMARY KEY,
role_id INT NOT NULL,
user_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`).Error; err != nil {
return err
}
db.Exec(`CREATE INDEX IF NOT EXISTS idx_goadmin_role_users_role_id ON goadmin_role_users(role_id)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_goadmin_role_users_user_id ON goadmin_role_users(user_id)`)
// GoAdmin user_permissions table
if err := db.Exec(`
CREATE TABLE IF NOT EXISTS goadmin_user_permissions (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
permission_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`).Error; err != nil {
return err
}
db.Exec(`CREATE INDEX IF NOT EXISTS idx_goadmin_user_permissions_user_id ON goadmin_user_permissions(user_id)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_goadmin_user_permissions_permission_id ON goadmin_user_permissions(permission_id)`)
// GoAdmin role_permissions table
if err := db.Exec(`
CREATE TABLE IF NOT EXISTS goadmin_role_permissions (
id SERIAL PRIMARY KEY,
role_id INT NOT NULL,
permission_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`).Error; err != nil {
return err
}
db.Exec(`CREATE INDEX IF NOT EXISTS idx_goadmin_role_permissions_role_id ON goadmin_role_permissions(role_id)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_goadmin_role_permissions_permission_id ON goadmin_role_permissions(permission_id)`)
// GoAdmin menu table
if err := db.Exec(`
CREATE TABLE IF NOT EXISTS goadmin_menu (
id SERIAL PRIMARY KEY,
parent_id INT NOT NULL DEFAULT 0,
type INT NOT NULL DEFAULT 0,
"order" INT NOT NULL DEFAULT 0,
title VARCHAR(50) NOT NULL DEFAULT '',
icon VARCHAR(50) NOT NULL DEFAULT '',
uri VARCHAR(3000) NOT NULL DEFAULT '',
header VARCHAR(150) DEFAULT '',
plugin_name VARCHAR(150) NOT NULL DEFAULT '',
uuid VARCHAR(150) DEFAULT '',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`).Error; err != nil {
return err
}
db.Exec(`CREATE INDEX IF NOT EXISTS idx_goadmin_menu_parent_id ON goadmin_menu(parent_id)`)
// GoAdmin role_menu table
if err := db.Exec(`
CREATE TABLE IF NOT EXISTS goadmin_role_menu (
id SERIAL PRIMARY KEY,
role_id INT NOT NULL,
menu_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`).Error; err != nil {
return err
}
db.Exec(`CREATE INDEX IF NOT EXISTS idx_goadmin_role_menu_role_id ON goadmin_role_menu(role_id)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_goadmin_role_menu_menu_id ON goadmin_role_menu(menu_id)`)
// GoAdmin operation_log table
if err := db.Exec(`
CREATE TABLE IF NOT EXISTS goadmin_operation_log (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
path VARCHAR(255) NOT NULL DEFAULT '',
method VARCHAR(10) NOT NULL DEFAULT '',
ip VARCHAR(15) NOT NULL DEFAULT '',
input TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`).Error; err != nil {
return err
}
db.Exec(`CREATE INDEX IF NOT EXISTS idx_goadmin_operation_log_user_id ON goadmin_operation_log(user_id)`)
// GoAdmin site table
if err := db.Exec(`
CREATE TABLE IF NOT EXISTS goadmin_site (
id SERIAL PRIMARY KEY,
key VARCHAR(100) NOT NULL DEFAULT '',
value TEXT NOT NULL,
description VARCHAR(3000) DEFAULT '',
state INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`).Error; err != nil {
return err
}
db.Exec(`CREATE INDEX IF NOT EXISTS idx_goadmin_site_key ON goadmin_site(key)`)
// Seed default admin user (password: admin - bcrypt hash)
db.Exec(`
INSERT INTO goadmin_users (username, password, name, avatar)
VALUES ('admin', '$2a$10$t.GCU24EqIWLSl7F51Hdz.IkkgFK.Qa9/BzEc5Bi2C/I2bXf1nJgm', 'Administrator', '')
ON CONFLICT DO NOTHING
`)
// Update existing admin password if it exists with wrong hash
db.Exec(`
UPDATE goadmin_users SET password = '$2a$10$t.GCU24EqIWLSl7F51Hdz.IkkgFK.Qa9/BzEc5Bi2C/I2bXf1nJgm'
WHERE username = 'admin'
`)
// Seed default roles
db.Exec(`INSERT INTO goadmin_roles (name, slug) VALUES ('Administrator', 'administrator') ON CONFLICT DO NOTHING`)
db.Exec(`INSERT INTO goadmin_roles (name, slug) VALUES ('Operator', 'operator') ON CONFLICT DO NOTHING`)
// Seed default permissions
db.Exec(`INSERT INTO goadmin_permissions (name, slug, http_method, http_path) VALUES ('All permissions', '*', '', '*') ON CONFLICT DO NOTHING`)
db.Exec(`INSERT INTO goadmin_permissions (name, slug, http_method, http_path) VALUES ('Dashboard', 'dashboard', 'GET', '/') ON CONFLICT DO NOTHING`)
// Assign admin user to administrator role (if not already assigned)
db.Exec(`
INSERT INTO goadmin_role_users (role_id, user_id)
SELECT r.id, u.id FROM goadmin_roles r, goadmin_users u
WHERE r.slug = 'administrator' AND u.username = 'admin'
AND NOT EXISTS (
SELECT 1 FROM goadmin_role_users ru
WHERE ru.role_id = r.id AND ru.user_id = u.id
)
`)
// Assign all permissions to administrator role (if not already assigned)
db.Exec(`
INSERT INTO goadmin_role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM goadmin_roles r, goadmin_permissions p
WHERE r.slug = 'administrator' AND p.slug = '*'
AND NOT EXISTS (
SELECT 1 FROM goadmin_role_permissions rp
WHERE rp.role_id = r.id AND rp.permission_id = p.id
)
`)
// Seed default menu items (only if menu is empty)
var menuCount int64
db.Raw(`SELECT COUNT(*) FROM goadmin_menu`).Scan(&menuCount)
if menuCount == 0 {
db.Exec(`
INSERT INTO goadmin_menu (parent_id, type, "order", title, icon, uri, plugin_name) VALUES
(0, 1, 1, 'Dashboard', 'fa-bar-chart', '/', ''),
(0, 1, 2, 'Admin', 'fa-tasks', '', ''),
(2, 1, 1, 'Users', 'fa-users', '/info/goadmin_users', ''),
(2, 1, 2, 'Roles', 'fa-user', '/info/goadmin_roles', ''),
(2, 1, 3, 'Permissions', 'fa-ban', '/info/goadmin_permissions', ''),
(2, 1, 4, 'Menu', 'fa-bars', '/menu', ''),
(2, 1, 5, 'Operation Log', 'fa-history', '/info/goadmin_operation_log', ''),
(0, 1, 3, 'MyCrib', 'fa-home', '', ''),
(8, 1, 1, 'Users', 'fa-user', '/info/users', ''),
(8, 1, 2, 'Residences', 'fa-building', '/info/residences', ''),
(8, 1, 3, 'Tasks', 'fa-tasks', '/info/tasks', ''),
(8, 1, 4, 'Contractors', 'fa-wrench', '/info/contractors', ''),
(8, 1, 5, 'Documents', 'fa-file', '/info/documents', ''),
(8, 1, 6, 'Notifications', 'fa-bell', '/info/notifications', '')
`)
// Assign all menus to administrator role
db.Exec(`
INSERT INTO goadmin_role_menu (role_id, menu_id)
SELECT r.id, m.id FROM goadmin_roles r, goadmin_menu m
WHERE r.slug = 'administrator'
`)
}
log.Info().Msg("GoAdmin migrations completed")
// Seed default Next.js admin user (email: admin@mycrib.com, password: admin123)
// bcrypt hash for "admin123": $2a$10$t5hGjdXQLxr9Z0193qx.Tef6hd1vYI3JvrfX/piKx2qS9UvQ41I9O
var adminCount int64
db.Raw(`SELECT COUNT(*) FROM admin_users WHERE email = 'admin@mycrib.com'`).Scan(&adminCount)
if adminCount == 0 {
log.Info().Msg("Seeding default admin user for Next.js admin panel...")
db.Exec(`
INSERT INTO admin_users (email, password, first_name, last_name, role, is_active, created_at, updated_at)
VALUES ('admin@mycrib.com', '$2a$10$t5hGjdXQLxr9Z0193qx.Tef6hd1vYI3JvrfX/piKx2qS9UvQ41I9O', 'Admin', 'User', 'super_admin', true, NOW(), NOW())
`)
log.Info().Msg("Default admin user created: admin@mycrib.com / admin123")
} else {
// Update existing admin password if needed
db.Exec(`
UPDATE admin_users SET password = '$2a$10$t5hGjdXQLxr9Z0193qx.Tef6hd1vYI3JvrfX/piKx2qS9UvQ41I9O'
WHERE email = 'admin@mycrib.com'
`)
log.Info().Msg("Updated admin@mycrib.com password to admin123")
}
return nil
}