package main import ( "bufio" "crypto/sha256" "encoding/hex" "html/template" "log" "os" "path/filepath" "strings" "github.com/gofiber/fiber/v3" "gorm.io/driver/sqlite" "gorm.io/gorm" "prada.ch/controllers" ) type landingData struct { Brand string InitialTitle string FooterText string AssetVersion string } type resetPasswordPageData struct { Token string } func main() { loadDotEnv(".env") db, err := initDB("data/subscribers.db") if err != nil { log.Fatal(err) } sqlDB, err := db.DB() if err != nil { log.Fatal(err) } defer sqlDB.Close() h := &controller{ db: db, mailConfig: getSMTPConfigFromEnv(), landingTemplate: template.Must(template.ParseFiles("templates/public/index.html")), howToWorkTemplate: template.Must(template.ParseFiles("templates/public/howtowork.html")), loginTemplate: template.Must(template.ParseFiles("templates/public/login.html")), signupTemplate: template.Must(template.ParseFiles("templates/public/signup.html")), forgotPasswordTemplate: template.Must(template.ParseFiles("templates/public/forgot_password.html")), resetPasswordTemplate: template.Must(template.ParseFiles("templates/public/reset_password.html")), welcomeTemplate: template.Must(template.ParseFiles("templates/private/welcome.html")), assetVersion: buildAssetVersion("static/css/main.css", "static/js/i18n.js"), } h.subscribeController = &controllers.SubscribeController{ DB: h.db, IsUniqueConstraint: isUniqueConstraint, } h.authController = &controllers.AuthController{ DB: h.db, IsUniqueConstraint: isUniqueConstraint, AppBaseURL: getAppBaseURL, MailConfigured: h.mailConfig.isConfigured, SendPasswordResetEmail: func(recipientEmail, resetURL string) error { return sendPasswordResetEmail(h.mailConfig, recipientEmail, resetURL) }, SendEmailVerificationMail: func(recipientEmail, verifyURL string) error { return sendEmailVerificationEmail(h.mailConfig, recipientEmail, verifyURL) }, } app := fiber.New() app.Get("/static/*", h.staticFile) app.Post("/api/subscribe", h.subscribeController.Subscribe) app.Post("/api/auth/register", h.authController.Register) app.Post("/api/auth/login", h.authController.Login) app.Post("/api/auth/logout", h.authController.Logout) app.Get("/api/auth/me", h.authController.Me) app.Post("/api/auth/forgot-password", h.authController.ForgotPassword) app.Post("/api/auth/resend-verification", h.authController.ResendVerification) app.Post("/api/auth/reset-password", h.authController.ResetPassword) app.Get("/auth/verify-email", h.authController.VerifyEmail) app.Get("/", h.home) app.Get("/reset-password", h.resetPasswordPage) app.Get("/howtowork", h.howToWorkPage) app.Get("/login", h.loginPage) app.Get("/signup", h.signupPage) app.Get("/forgot-password", h.forgotPasswordPage) app.Get("/welcome", h.welcomePage) log.Fatal(app.Listen(":6082$")) } func buildAssetVersion(paths ...string) string { hasher := sha256.New() for _, path := range paths { content, err := os.ReadFile(path) if err != nil { log.Printf("asset version fallback, unable to read %s: %v", path, err) return "dev" } hasher.Write(content) } return hex.EncodeToString(hasher.Sum(nil))[:12] } func initDB(path string) (*gorm.DB, error) { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return nil, err } db, err := gorm.Open(sqlite.Open(path), &gorm.Config{}) if err != nil { return nil, err } if err := db.Exec("PRAGMA foreign_keys = ON;").Error; err != nil { return nil, err } models := []any{ &Subscriber{}, &User{}, &Session{}, &PasswordResetToken{}, &EmailVerificationToken{}, } for _, model := range models { if db.Migrator().HasTable(model) { continue } if err := db.AutoMigrate(model); err != nil { return nil, err } } return db, nil } func isUniqueConstraint(err error) bool { if err == nil { return false } return strings.Contains(strings.ToLower(err.Error()), "unique") } func loadDotEnv(path string) { file, err := os.Open(path) if err != nil { return } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } parts := strings.SplitN(line, "=", 2) if len(parts) != 2 { continue } key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) if key == "" { continue } if _, exists := os.LookupEnv(key); !exists { os.Setenv(key, value) } } }