package handlers import ( "fmt" "log" "net/http" "strings" "git.nixc.us/a250/ss-atlas/internal/version" ) func (a *App) handleLanding(w http.ResponseWriter, r *http.Request) { remoteUser := r.Header.Get("Remote-User") if contains(r.Header.Get("Remote-Groups"), "customers") { http.Redirect(w, r, a.cfg.AppURL+"/dashboard", http.StatusSeeOther) return } // Logged-in user who paid but hasn't activated yet — send to activate. if remoteUser != "" { custID, _ := a.ldap.GetStripeCustomerID(remoteUser) if custID != "" { http.Redirect(w, r, a.cfg.AppURL+"/activate", http.StatusSeeOther) return } } data := map[string]any{ "AppURL": a.cfg.AppURL, "Commit": version.Commit, "BuildTime": version.BuildTime, } if err := a.tmpl.ExecuteTemplate(w, "landing.html", data); err != nil { log.Printf("template error: %v", err) http.Error(w, "internal error", http.StatusInternalServerError) } } func (a *App) handleCreateCheckout(w http.ResponseWriter, r *http.Request) { email := r.FormValue("email") if email == "" { http.Error(w, "email required", http.StatusBadRequest) return } sess, err := a.stripe.CreateCheckoutSession(email) if err != nil { log.Printf("stripe checkout error: %v", err) http.Error(w, "failed to create checkout", http.StatusInternalServerError) return } http.Redirect(w, r, sess.URL, http.StatusSeeOther) } func (a *App) handleSuccess(w http.ResponseWriter, r *http.Request) { sessionID := r.URL.Query().Get("session_id") if sessionID == "" { http.Redirect(w, r, "/", http.StatusSeeOther) return } sess, err := a.stripe.GetCheckoutSession(sessionID) if err != nil { log.Printf("stripe get session error: %v", err) http.Error(w, "could not verify payment", http.StatusInternalServerError) return } if sess.PaymentStatus != "paid" { http.Redirect(w, r, "/", http.StatusSeeOther) return } email := sess.CustomerDetails.Email customerID := sess.Customer.ID username := sanitizeUsername(email) result, err := a.ldap.ProvisionUser(username, email, customerID) if err != nil { log.Printf("ldap provision failed for %s: %v", email, err) http.Error(w, "account creation failed, contact support", http.StatusInternalServerError) return } inGroup, _ := a.ldap.IsInGroup(result.Username, "customers") if result.IsNew || !inGroup { // New or lapsed customer: send password setup email, show onboarding. // Group membership and stack deploy happen on /activate after they log in. if err := a.triggerPasswordReset(result.Username); err != nil { log.Printf("authelia reset trigger failed for %s: %v", username, err) } data := map[string]any{ "Username": result.Username, "IsNew": result.IsNew, "Email": email, "LoginURL": a.cfg.AutheliaURL, "ActivateURL": a.cfg.AppURL + "/activate", "DashboardURL": a.cfg.AppURL + "/dashboard", "InstanceURL": "https://" + result.Username + "." + a.cfg.CustomerDomain, } if err := a.tmpl.ExecuteTemplate(w, "welcome.html", data); err != nil { log.Printf("template error: %v", err) http.Error(w, "internal error", http.StatusInternalServerError) } return } // Returning active customer: ensure stack exists, go to dashboard stackName := fmt.Sprintf("customer-%s", result.Username) exists, _ := a.swarm.StackExists(stackName) if !exists { if err := a.swarm.DeployStack(stackName, result.Username, a.cfg.TraefikDomain); err != nil { log.Printf("resubscribe: stack deploy failed for %s: %v", result.Username, err) } } log.Printf("resubscribe: %s payment verified, redirecting to dashboard", result.Username) http.Redirect(w, r, a.cfg.AppURL+"/dashboard", http.StatusSeeOther) } func (a *App) handlePortal(w http.ResponseWriter, r *http.Request) { customerID := r.FormValue("customer_id") if customerID == "" { http.Error(w, "customer_id required", http.StatusBadRequest) return } sess, err := a.stripe.CreatePortalSession(customerID) if err != nil { log.Printf("stripe portal error: %v", err) http.Error(w, "failed to create portal session", http.StatusInternalServerError) return } http.Redirect(w, r, sess.URL, http.StatusSeeOther) } // handleResubscribe creates a fresh checkout session for an existing Stripe // customer whose subscription has expired/been cancelled. This differs from // the portal flow which only manages active or scheduled-to-cancel subs. func (a *App) handleResubscribe(w http.ResponseWriter, r *http.Request) { customerID := r.FormValue("customer_id") if customerID == "" { http.Error(w, "customer_id required", http.StatusBadRequest) return } sess, err := a.stripe.CreateCheckoutForCustomer(customerID) if err != nil { log.Printf("stripe resubscribe error: %v", err) http.Error(w, "failed to create checkout session", http.StatusInternalServerError) return } http.Redirect(w, r, sess.URL, http.StatusSeeOther) } func sanitizeUsername(email string) string { parts := strings.SplitN(email, "@", 2) local := parts[0] domain := "" if len(parts) == 2 { // Use second-level domain only (e.g. "nixc" from "nixc.us", "gmail" from "gmail.com") domainParts := strings.Split(parts[1], ".") if len(domainParts) >= 2 { domain = "-" + domainParts[len(domainParts)-2] } } clean := func(s string) string { return strings.Map(func(r rune) rune { if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_' { return r } return '-' }, strings.ToLower(s)) } return clean(local) + clean(domain) }