forked from Nixius/authelia
181 lines
5.4 KiB
Go
181 lines
5.4 KiB
Go
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)
|
|
}
|