package server import ( "context" "encoding/json" "fmt" "log" "net/http" "os" "regexp" "strings" "sync/atomic" "time" "odoo-go/pkg/service" "odoo-go/pkg/tools" ) var dbnamePattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) // isSetupNeeded checks if the current database has been initialized. func (s *Server) isSetupNeeded() bool { var count int err := s.pool.QueryRow(context.Background(), `SELECT COUNT(*) FROM res_company`).Scan(&count) return err != nil || count == 0 } // handleDatabaseManager serves the database manager page. // Mirrors: odoo/addons/web/controllers/database.py Database.manager() func (s *Server) handleDatabaseManager(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(databaseManagerHTML)) } // handleDatabaseCreate processes the database creation form. // Mirrors: odoo/addons/web/controllers/database.py Database.create() // Fields match Python Odoo: name, login, password, phone, lang, country_code, demo func (s *Server) handleDatabaseCreate(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var params struct { MasterPwd string `json:"master_pwd"` Name string `json:"name"` Login string `json:"login"` Password string `json:"password"` Phone string `json:"phone"` Lang string `json:"lang"` CountryCode string `json:"country_code"` Demo bool `json:"demo"` } if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { writeJSON(w, map[string]string{"error": "Invalid request"}) return } // Validate master password (default: "admin", configurable via ODOO_MASTER_PASSWORD env) masterPw := os.Getenv("ODOO_MASTER_PASSWORD") if masterPw == "" { masterPw = "admin" } if params.MasterPwd != masterPw { writeJSON(w, map[string]string{"error": "Invalid master password"}) return } // Validate if params.Login == "" || params.Password == "" { writeJSON(w, map[string]string{"error": "Email and password are required"}) return } if len(params.Password) < 4 { writeJSON(w, map[string]string{"error": "Password must be at least 4 characters"}) return } // Default values if params.Lang == "" { params.Lang = "en_US" } if params.CountryCode == "" { params.CountryCode = "DE" } // Map country code countryName := "Germany" phoneCode := "49" switch strings.ToUpper(params.CountryCode) { case "AT": countryName = "Austria" phoneCode = "43" case "CH": countryName = "Switzerland" phoneCode = "41" case "US": countryName = "United States" phoneCode = "1" case "GB": countryName = "United Kingdom" phoneCode = "44" case "FR": countryName = "France" phoneCode = "33" } // Determine chart of accounts from country chart := "skr03" switch strings.ToUpper(params.CountryCode) { case "AT", "CH": chart = "skr03" // Use SKR03 for DACH region default: chart = "skr03" } // Extract company name from email domain, or use default companyName := "My Company" if strings.Contains(params.Login, "@") { parts := strings.Split(params.Login, "@") if len(parts) == 2 { domain := parts[1] domainParts := strings.Split(domain, ".") if len(domainParts) > 0 { name := domainParts[0] if len(name) > 0 { companyName = strings.ToUpper(name[:1]) + name[1:] } } } } log.Printf("setup: creating database for %q (login: %s, country: %s)", companyName, params.Login, params.CountryCode) // Hash password hashedPw, err := tools.HashPassword(params.Password) if err != nil { writeJSON(w, map[string]string{"error": fmt.Sprintf("Password error: %v", err)}) return } // Seed the database setupCfg := service.SetupConfig{ CompanyName: companyName, CountryCode: strings.ToUpper(params.CountryCode), CountryName: countryName, PhoneCode: phoneCode, Phone: params.Phone, Email: params.Login, Chart: chart, AdminLogin: params.Login, AdminPassword: hashedPw, DemoData: params.Demo, } if err := service.SeedWithSetup(context.Background(), s.pool, setupCfg); err != nil { log.Printf("setup: error: %v", err) writeJSON(w, map[string]string{"error": fmt.Sprintf("Database error: %v", err)}) return } // Auto-login: create session and return session cookie // Mirrors: odoo/addons/web/controllers/database.py line 82-88 sess := s.sessions.New(1, 1, params.Login) http.SetCookie(w, &http.Cookie{ Name: "session_id", Value: sess.ID, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, }) log.Printf("setup: database initialized, auto-login as %q", params.Login) writeJSON(w, map[string]interface{}{ "status": "ok", "session_id": sess.ID, "redirect": "/odoo", }) } // handleDatabaseList returns the list of databases. // Mirrors: odoo/addons/web/controllers/database.py Database.list() func (s *Server) handleDatabaseListJSON(w http.ResponseWriter, r *http.Request) { writeJSON(w, []string{s.config.DBName}) } func writeJSON(w http.ResponseWriter, v interface{}) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(v) } // postSetupDone caches the result of isPostSetupNeeded to avoid a DB query on every request. var postSetupDone atomic.Bool // isPostSetupNeeded checks if the company still has default values (needs configuration). func (s *Server) isPostSetupNeeded() bool { if postSetupDone.Load() { return false } var name string err := s.pool.QueryRow(context.Background(), `SELECT COALESCE(name, '') FROM res_company WHERE id = 1`).Scan(&name) if err != nil { return false } needed := name == "" || name == "My Company" || strings.HasPrefix(name, "My ") if !needed { postSetupDone.Store(true) } return needed } // handleSetupWizard serves the post-setup configuration wizard. // Shown after first login when the company has not been configured yet. // Mirrors: odoo/addons/base_setup/views/res_config_settings_views.xml func (s *Server) handleSetupWizard(w http.ResponseWriter, r *http.Request) { sess := GetSession(r) if sess == nil { http.Redirect(w, r, "/web/login", http.StatusFound) return } // Load current company data var companyName, street, city, zip, phone, email, website, vat string var countryID int64 s.pool.QueryRow(context.Background(), `SELECT COALESCE(name,''), COALESCE(street,''), COALESCE(city,''), COALESCE(zip,''), COALESCE(phone,''), COALESCE(email,''), COALESCE(website,''), COALESCE(vat,''), COALESCE(country_id, 0) FROM res_company WHERE id = $1`, sess.CompanyID, ).Scan(&companyName, &street, &city, &zip, &phone, &email, &website, &vat, &countryID) w.Header().Set("Content-Type", "text/html; charset=utf-8") esc := htmlEscape fmt.Fprintf(w, setupWizardHTML, esc(companyName), esc(street), esc(city), esc(zip), esc(phone), esc(email), esc(website), esc(vat)) } // handleSetupWizardSave saves the post-setup wizard data. func (s *Server) handleSetupWizardSave(w http.ResponseWriter, r *http.Request) { sess := GetSession(r) if sess == nil { writeJSON(w, map[string]string{"error": "Not authenticated"}) return } var params struct { CompanyName string `json:"company_name"` Street string `json:"street"` City string `json:"city"` Zip string `json:"zip"` Phone string `json:"phone"` Email string `json:"email"` Website string `json:"website"` Vat string `json:"vat"` } if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { writeJSON(w, map[string]string{"error": "Invalid request"}) return } if params.CompanyName == "" { writeJSON(w, map[string]string{"error": "Company name is required"}) return } _, err := s.pool.Exec(context.Background(), `UPDATE res_company SET name=$1, street=$2, city=$3, zip=$4, phone=$5, email=$6, website=$7, vat=$8 WHERE id = $9`, params.CompanyName, params.Street, params.City, params.Zip, params.Phone, params.Email, params.Website, params.Vat, sess.CompanyID) if err != nil { writeJSON(w, map[string]string{"error": fmt.Sprintf("Save error: %v", err)}) return } // Also update the partner linked to the company s.pool.Exec(context.Background(), `UPDATE res_partner SET name=$1, street=$2, city=$3, zip=$4, phone=$5, email=$6, website=$7, vat=$8 WHERE id = (SELECT partner_id FROM res_company WHERE id = $9)`, params.CompanyName, params.Street, params.City, params.Zip, params.Phone, params.Email, params.Website, params.Vat, sess.CompanyID) postSetupDone.Store(true) // Mark setup as done so we don't redirect again writeJSON(w, map[string]interface{}{"status": "ok", "redirect": "/odoo"}) } var setupWizardHTML = ` Setup — Configure Your Company

Configure Your Company

Set up your company information

` // --- Database Manager HTML --- // Mirrors: odoo/addons/web/static/src/public/database_manager.create_form.qweb.html var databaseManagerHTML = ` Odoo — Database Manager

Create Database

Set up your Odoo database

Default: admin

Creating database...

` + fmt.Sprintf("", time.Now().Format(time.RFC3339))