diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..279e27a --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +# Server Configuration +SERVER_PORT=8080 +GIN_MODE=debug + +# Security Configuration +JWT_SECRET=your-super-secret-jwt-key-here + +# Sync Configuration +SYNC_INTERVAL_MINUTES=1 + +# Default Admin User Configuration +ADMIN_EMAIL=admin@admin.com +ADMIN_PASSWORD=sheetless + +# Directory of your sheets +SHEETS_DIRECTORY=/data/sheets diff --git a/.envrc b/.envrc index 3550a30..f2b0f16 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,2 @@ use flake +dotenv .env diff --git a/.gitignore b/.gitignore index 6d61d87..3360daf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ sheetless-server sheetless.db /result +/.env diff --git a/src/config/config.go b/src/config/config.go new file mode 100644 index 0000000..b5a4109 --- /dev/null +++ b/src/config/config.go @@ -0,0 +1,66 @@ +package config + +import ( + "os" + "strconv" + "time" +) + +type Config struct { + Server struct { + Port string + Mode string // gin.Mode: debug, release, test + } + JWT struct { + Secret string + } + Sync struct { + Interval time.Duration + } + Admin struct { + Email string + Password string + } + SheetsDirectory string +} + +var AppConfig *Config + +func Load() { + cfg := &Config{} + + // Server configuration + cfg.Server.Port = getEnv("SERVER_PORT", ":8080") + cfg.Server.Mode = getEnv("GIN_MODE", "debug") + + // JWT configuration + cfg.JWT.Secret = getEnv("JWT_SECRET", "sheetless-default-jwt-secret-please-change") + + // Sync configuration + syncMinutes := getEnvInt("SYNC_INTERVAL_MINUTES", 1) + cfg.Sync.Interval = time.Duration(syncMinutes) * time.Minute + + // Admin configuration + cfg.Admin.Email = getEnv("ADMIN_EMAIL", "admin@admin.com") + cfg.Admin.Password = getEnv("ADMIN_PASSWORD", "sheetless") + + cfg.SheetsDirectory = getEnv("SHEETS_DIRECTORY", "./sheets_directory") + + AppConfig = cfg +} + +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +func getEnvInt(key string, defaultValue int) int { + if value := os.Getenv(key); value != "" { + if intValue, err := strconv.Atoi(value); err == nil { + return intValue + } + } + return defaultValue +} diff --git a/src/database/connection.go b/src/database/connection.go index 6d773b8..60b401b 100644 --- a/src/database/connection.go +++ b/src/database/connection.go @@ -2,8 +2,10 @@ package database import ( "log" + "sheetless-server/config" "sheetless-server/models" + "golang.org/x/crypto/bcrypt" "gorm.io/driver/sqlite" "gorm.io/gorm" ) @@ -12,16 +14,59 @@ var DB *gorm.DB func InitDatabase() { var err error + DB, err = gorm.Open(sqlite.Open("sheetless.db"), &gorm.Config{}) if err != nil { log.Fatal("Failed to connect to database:", err) } + tables, err := DB.Migrator().GetTables() + if err != nil { + log.Fatal("Failed to list tables of database:", err) + } + isNewDatabase := len(tables) == 0 + // Auto migrate the schema err = DB.AutoMigrate(&models.User{}, &models.Sheet{}, &models.Composer{}) if err != nil { log.Fatal("Failed to migrate database:", err) } + if isNewDatabase { + createDefaultAdminUser() + } + log.Println("Database connected and migrated successfully") } + +func createDefaultAdminUser() { + // Check if admin user already exists + var existingUser models.User + err := DB.Where("email = ?", config.AppConfig.Admin.Email).First(&existingUser).Error + if err == nil { + // Admin user already exists, don't recreate + log.Printf("Admin user already exists: %s", config.AppConfig.Admin.Email) + return + } + + // Hash the admin password + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(config.AppConfig.Admin.Password), bcrypt.DefaultCost) + if err != nil { + log.Printf("Failed to hash admin password: %v", err) + return + } + + // Create admin user + adminUser := models.User{ + Username: "admin", + Email: config.AppConfig.Admin.Email, + Password: string(hashedPassword), + } + + if err := DB.Create(&adminUser).Error; err != nil { + log.Printf("Failed to create admin user: %v", err) + return + } + + log.Printf("Default admin user created with email: %s", config.AppConfig.Admin.Email) +} diff --git a/src/handlers/auth.go b/src/handlers/auth.go index bedfbef..719a246 100644 --- a/src/handlers/auth.go +++ b/src/handlers/auth.go @@ -2,6 +2,7 @@ package handlers import ( "net/http" + "sheetless-server/config" "sheetless-server/database" "sheetless-server/models" "time" @@ -11,8 +12,6 @@ import ( "golang.org/x/crypto/bcrypt" ) -var jwtSecret = []byte("your-secret-key") // TODO: In production, use environment variable - type RegisterRequest struct { Username string `json:"username" binding:"required"` Email string `json:"email" binding:"required,email"` @@ -86,7 +85,7 @@ func Login(c *gin.Context) { "exp": time.Now().Add(time.Hour * 24).Unix(), }) - tokenString, err := token.SignedString(jwtSecret) + tokenString, err := token.SignedString([]byte(config.AppConfig.JWT.Secret)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) return diff --git a/src/handlers/sheets.go b/src/handlers/sheets.go index 5058d07..03ba492 100644 --- a/src/handlers/sheets.go +++ b/src/handlers/sheets.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "path/filepath" + "sheetless-server/config" "sheetless-server/database" "sheetless-server/models" "sheetless-server/utils" @@ -16,15 +17,6 @@ import ( "github.com/google/uuid" ) -const uploadDir = "./uploads" - -func init() { - // Create uploads directory if it doesn't exist - if err := os.MkdirAll(uploadDir, 0755); err != nil { - panic("Failed to create uploads directory: " + err.Error()) - } -} - func UploadSheet(c *gin.Context) { // Get form data title := c.PostForm("title") @@ -69,7 +61,7 @@ func UploadSheet(c *gin.Context) { // Generate unique filename filename := fmt.Sprintf("%d%s", time.Now().Unix(), filepath.Ext(header.Filename)) - filePath := filepath.Join(uploadDir, filename) + filePath := filepath.Join(config.AppConfig.SheetsDirectory, filename) // Save file out, err := os.Create(filePath) @@ -106,15 +98,15 @@ func UploadSheet(c *gin.Context) { // Create database record sheet := models.Sheet{ - Uuid: *uuid, - Title: title, - Description: description, - FilePath: filePath, - FileSize: fileInfo.Size(), - FileHash: fileHash, - ComposerId: composer.Uuid, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + Uuid: *uuid, + Title: title, + Description: description, + FilePath: filePath, + FileSize: fileInfo.Size(), + FileHash: fileHash, + ComposerUuid: composer.Uuid, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } if err := database.DB.Create(&sheet).Error; err != nil { diff --git a/src/main.go b/src/main.go index 2817277..8157133 100644 --- a/src/main.go +++ b/src/main.go @@ -2,6 +2,8 @@ package main import ( "log" + "os" + "sheetless-server/config" "sheetless-server/database" "sheetless-server/routes" "sheetless-server/sync" @@ -11,11 +13,18 @@ import ( ) func main() { + config.Load() + gin.SetMode(config.AppConfig.Server.Mode) + database.InitDatabase() + if err := os.MkdirAll(config.AppConfig.SheetsDirectory, 0755); err != nil { + panic("Failed to create uploads directory: " + err.Error()) + } + // Start sync runner go func() { - ticker := time.NewTicker(1 * time.Minute) + ticker := time.NewTicker(config.AppConfig.Sync.Interval) defer ticker.Stop() for { select { @@ -31,8 +40,8 @@ func main() { routes.SetupRoutes(r) - log.Println("Server starting on port 8080...") - if err := r.Run(":8080"); err != nil { + log.Printf("Server starting on port %s...", config.AppConfig.Server.Port) + if err := r.Run(config.AppConfig.Server.Port); err != nil { log.Fatal("Failed to start server:", err) } } diff --git a/src/middleware/auth.go b/src/middleware/auth.go index 45b3102..2054789 100644 --- a/src/middleware/auth.go +++ b/src/middleware/auth.go @@ -2,14 +2,13 @@ package middleware import ( "net/http" + "sheetless-server/config" "strings" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" ) -var jwtSecret = []byte("your-secret-key") // Should match the one in handlers/auth.go - func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") @@ -29,7 +28,7 @@ func AuthMiddleware() gin.HandlerFunc { // Parse and validate token token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - return jwtSecret, nil + return []byte(config.AppConfig.JWT.Secret), nil }) if err != nil || !token.Valid { @@ -47,4 +46,4 @@ func AuthMiddleware() gin.HandlerFunc { c.Next() } -} \ No newline at end of file +} diff --git a/src/models/sheet.go b/src/models/sheet.go index c5dce69..61591d2 100644 --- a/src/models/sheet.go +++ b/src/models/sheet.go @@ -8,15 +8,15 @@ import ( ) type Sheet struct { - Uuid uuid.UUID `json:"uuid" gorm:"type:uuid;primaryKey"` - Title string `json:"title" gorm:"not null"` - Description string `json:"description"` - FilePath string `json:"file_path" gorm:"not null"` - FileSize int64 `json:"file_size"` - FileHash uint64 `json:"file_hash"` - ComposerId uuid.UUID `json:"composer_id"` - Composer Composer `json:"composer" gorm:"foreignKey:ComposerId"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + Uuid uuid.UUID `json:"uuid" gorm:"type:uuid;primaryKey"` + Title string `json:"title" gorm:"not null"` + Description string `json:"description"` + FilePath string `json:"file_path" gorm:"not null"` + FileSize int64 `json:"file_size"` + FileHash uint64 `json:"file_hash"` + ComposerUuid uuid.UUID `json:"composer_uuid"` + Composer Composer `json:"composer" gorm:"foreignKey:ComposerUuid"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` } diff --git a/src/sync/sync.go b/src/sync/sync.go index 8d6c01e..96ab061 100644 --- a/src/sync/sync.go +++ b/src/sync/sync.go @@ -4,6 +4,7 @@ import ( "log" "os" "path/filepath" + "sheetless-server/config" "sheetless-server/database" "sheetless-server/handlers" "sheetless-server/models" @@ -11,8 +12,6 @@ import ( "time" ) -const uploadDir = "./uploads" - func SyncSheets() error { // Get all sheets var sheets []models.Sheet @@ -30,7 +29,7 @@ func SyncSheets() error { } // Walk uploads dir - files, err := os.ReadDir(uploadDir) + files, err := os.ReadDir(config.AppConfig.SheetsDirectory) if err != nil { return err } @@ -39,7 +38,7 @@ func SyncSheets() error { if file.IsDir() { continue } - filePath := filepath.Join(uploadDir, file.Name()) + filePath := filepath.Join(config.AppConfig.SheetsDirectory, file.Name()) hash, err := utils.FileHash(filePath) if err != nil { log.Printf("Error hashing file %s: %v", filePath, err)