package authapi import ( "encoding/json" "errors" "net/http" "net/url" "strings" "github.com/ultisuite/ulti-backend/internal/api/apiresponse" "github.com/ultisuite/ulti-backend/internal/authentik" ) type flowCompleteRequest struct { ReturnTo string `json:"returnTo"` } type flowCompleteResponse struct { RedirectURL string `json:"redirectUrl"` } func (h *Handler) CompleteFlow(w http.ResponseWriter, r *http.Request) { sessionID := readFlowSessionCookie(r) if sessionID == "" { apiresponse.WriteError(w, r, http.StatusUnauthorized, "flow_session_missing", "flow session cookie required", nil) return } var req flowCompleteRequest if r.Body != nil { _ = json.NewDecoder(r.Body).Decode(&req) } returnTo := strings.TrimSpace(req.ReturnTo) if returnTo == "" { returnTo = "/mail/inbox" } if !strings.HasPrefix(returnTo, "/") || strings.HasPrefix(returnTo, "//") { apiresponse.WriteError(w, r, http.StatusBadRequest, "invalid_return_to", "returnTo must be a relative path", nil) return } slug := FlowAuthentication cookies, err := h.flows.CompleteSession(r.Context(), sessionID, slug) if err != nil { if errors.Is(err, authentik.ErrFlowSessionNotFound) { clearFlowSessionCookie(w) apiresponse.WriteError(w, r, http.StatusGone, "flow_session_expired", "flow session expired; restart the flow", nil) return } if errors.Is(err, authentik.ErrFlowSessionNotCompleted) { apiresponse.WriteError(w, r, http.StatusConflict, "flow_not_completed", "authentication flow is not finished yet", nil) return } if errors.Is(err, authentik.ErrFlowSessionSlugMismatch) { apiresponse.WriteError(w, r, http.StatusConflict, "flow_session_mismatch", "flow slug does not match active session", nil) return } apiresponse.WriteError(w, r, http.StatusBadGateway, "flow_complete_failed", err.Error(), nil) return } clearFlowSessionCookie(w) setBrowserAuthentikCookies(w, cookies) loginURL := buildLoginRedirectURL(h.appURL, returnTo) apiresponse.WriteJSON(w, http.StatusOK, flowCompleteResponse{RedirectURL: loginURL}) } func buildLoginRedirectURL(appURL, returnTo string) string { base := strings.TrimRight(strings.TrimSpace(appURL), "/") if base == "" { base = "http://localhost:3004" } params := url.Values{} params.Set("returnTo", returnTo) return base + "/api/auth/login?" + params.Encode() } func setBrowserAuthentikCookies(w http.ResponseWriter, stored []authentik.SerializedCookie) { for _, c := range authentik.BrowserAuthentikCookies(stored) { http.SetCookie(w, c) } } func forwardFlowCookies(w http.ResponseWriter, stored []authentik.SerializedCookie) { setBrowserAuthentikCookies(w, stored) }