diff --git a/cmd/odoo-server/main.go b/cmd/odoo-server/main.go index 57a8a6f..9ee20df 100644 --- a/cmd/odoo-server/main.go +++ b/cmd/odoo-server/main.go @@ -114,9 +114,9 @@ func main() { log.Printf("odoo: schema migration warning: %v", err) } - // Check if setup is needed (first boot) + // Check if database needs setup if service.NeedsSetup(ctx, pool) { - log.Println("odoo: database is empty — setup wizard will be shown at /web/setup") + log.Println("odoo: database is empty — database manager will be shown at /web/database/manager") } else { log.Println("odoo: database already initialized") } diff --git a/docker-compose.yml b/docker-compose.yml index 0161d8c..ce67848 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,11 +12,8 @@ services: USER: odoo PASSWORD: odoo ODOO_DB_NAME: odoo - ODOO_ADDONS_PATH: /opt/odoo-src/addons,/opt/odoo-src/odoo/addons - ODOO_BUILD_DIR: /opt/build/js - volumes: - - ../odoo:/opt/odoo-src:ro - - ./build/js:/opt/build/js:ro + ODOO_FRONTEND_DIR: /app/frontend + ODOO_BUILD_DIR: /app/build restart: unless-stopped db: diff --git a/odoo-server b/odoo-server index a205fd1..bb85c7c 100755 Binary files a/odoo-server and b/odoo-server differ diff --git a/pkg/server/server.go b/pkg/server/server.go index 7d62338..8ec2015 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -95,9 +95,9 @@ func (s *Server) registerRoutes() { // Database endpoints s.mux.HandleFunc("/web/database/list", s.handleDBList) - // Setup wizard - s.mux.HandleFunc("/web/setup", s.handleSetup) - s.mux.HandleFunc("/web/setup/install", s.handleSetupInstall) + // Database manager (mirrors Python Odoo's /web/database/manager) + s.mux.HandleFunc("/web/database/manager", s.handleDatabaseManager) + s.mux.HandleFunc("/web/database/create", s.handleDatabaseCreate) // Image serving (placeholder for uploaded images) s.mux.HandleFunc("/web/image", s.handleImage) diff --git a/pkg/server/setup.go b/pkg/server/setup.go index 5c3c54b..4286783 100644 --- a/pkg/server/setup.go +++ b/pkg/server/setup.go @@ -6,12 +6,17 @@ import ( "fmt" "log" "net/http" + "regexp" + "strings" + "time" "odoo-go/pkg/service" "odoo-go/pkg/tools" ) -// isSetupNeeded checks if the database has been initialized. +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(), @@ -19,130 +24,257 @@ func (s *Server) isSetupNeeded() bool { return err != nil || count == 0 } -// handleSetup serves the setup wizard. -func (s *Server) handleSetup(w http.ResponseWriter, r *http.Request) { - if !s.isSetupNeeded() { - http.Redirect(w, r, "/web/login", http.StatusFound) +// 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 } - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(` + 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 + 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 { + companyName = strings.Title(domainParts[0]) + } + } + } + + 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) +} + +// --- Database Manager HTML --- +// Mirrors: odoo/addons/web/static/src/public/database_manager.create_form.qweb.html +var databaseManagerHTML = ` - Odoo — Setup + Odoo — Database Manager -
-

Odoo Setup

-

Richten Sie Ihre Datenbank ein

+
+

Create Database

+

Set up your Odoo database

-
-

Unternehmen

- - + + + +

Default: admin

+ + + + + + + + +
- - + +
- - -
-
- -
-
- - -
-
- - + + + + + +
- - - - - - - - - -

Kontenrahmen

- - -

Administrator

- - - - - - -

Optionen

- - + +
- +
-
-

Datenbank wird eingerichtet...

+
+

Creating database...

-`)) -} - -// SetupParams holds the setup wizard form data. -type SetupParams struct { - CompanyName string `json:"company_name"` - Street string `json:"street"` - Zip string `json:"zip"` - City string `json:"city"` - Country string `json:"country"` - Email string `json:"email"` - Phone string `json:"phone"` - VAT string `json:"vat"` - Chart string `json:"chart"` - AdminEmail string `json:"admin_email"` - AdminPassword string `json:"admin_password"` - DemoData bool `json:"demo_data"` -} - -// handleSetupInstall processes the setup wizard form submission. -func (s *Server) handleSetupInstall(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - var params SetupParams - 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": "Firmenname ist erforderlich"}) - return - } - if params.AdminEmail == "" || params.AdminPassword == "" { - writeJSON(w, map[string]string{"error": "Admin Email und Passwort sind erforderlich"}) - return - } - - log.Printf("setup: initializing database for %q", params.CompanyName) - - // Hash admin password - hashedPw, err := tools.HashPassword(params.AdminPassword) - if err != nil { - writeJSON(w, map[string]string{"error": fmt.Sprintf("Password hash error: %v", err)}) - return - } - - // Map country code to name - countryName := "Germany" - phoneCode := "49" - switch params.Country { - case "AT": - countryName = "Austria" - phoneCode = "43" - case "CH": - countryName = "Switzerland" - phoneCode = "41" - } - - // Run the seed with user-provided data - setupCfg := service.SetupConfig{ - CompanyName: params.CompanyName, - Street: params.Street, - Zip: params.Zip, - City: params.City, - CountryCode: params.Country, - CountryName: countryName, - PhoneCode: phoneCode, - Email: params.Email, - Phone: params.Phone, - VAT: params.VAT, - Chart: params.Chart, - AdminLogin: params.AdminEmail, - AdminPassword: hashedPw, - DemoData: params.DemoData, - } - - 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("Setup error: %v", err)}) - return - } - - log.Printf("setup: database initialized successfully for %q", params.CompanyName) - writeJSON(w, map[string]string{"status": "ok"}) -} - -func writeJSON(w http.ResponseWriter, v interface{}) { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(v) -} +` + fmt.Sprintf("", time.Now().Format(time.RFC3339)) diff --git a/pkg/server/webclient.go b/pkg/server/webclient.go index 201688e..9d5fd0f 100644 --- a/pkg/server/webclient.go +++ b/pkg/server/webclient.go @@ -66,9 +66,10 @@ func loadAssetList(name string, fs embed.FS) []string { // handleWebClient serves the Odoo webclient HTML shell. // Mirrors: odoo/addons/web/controllers/home.py Home.web_client() func (s *Server) handleWebClient(w http.ResponseWriter, r *http.Request) { - // Check if setup is needed + // Check if database needs initialization + // Mirrors: odoo/addons/web/controllers/home.py ensure_db() if s.isSetupNeeded() { - http.Redirect(w, r, "/web/setup", http.StatusFound) + http.Redirect(w, r, "/web/database/manager", http.StatusFound) return }