package web import ( "errors" "net/http" "time" "github.com/netcell-it/deklarix/internal/auth" "github.com/netcell-it/deklarix/internal/store" ) const oauthStateCookieName = "deklarix_oauth_state" // knownPlatforms sind alle Plattformen, die die Verbindungs-Übersicht // anzeigt — unabhängig davon, ob dafür schon ein Connector konfiguriert // ist (siehe cmd/deklarix/main.go). Eine unkonfigurierte Plattform zeigt // "nicht konfiguriert" statt eines Verbinden-Buttons. var knownPlatforms = []string{"instagram", "tiktok"} type connectionView struct { Platform string Configured bool Connected bool ConnectedAt string } type connectionsData struct { Title string Nav navData Connections []connectionView } // handleConnectionsList zeigt, welche Plattformen der Account verbunden // hat — Grundlage für die spätere automatische Beweissicherung // (Post per API statt manuellem Screenshot-Upload abrufen). func (s *Server) handleConnectionsList(w http.ResponseWriter, r *http.Request) { accountID := currentUser(r).AccountID existing, err := s.store.ListPlatformConnectionsForAccount(r.Context(), accountID) if err != nil { http.Error(w, "Verbindungen konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError) return } connectedAt := map[string]string{} for _, c := range existing { connectedAt[c.Platform] = c.ConnectedAt.Format("02.01.2006 15:04") } data := connectionsData{Title: "Verbindungen", Nav: navFor(r)} for _, platform := range knownPlatforms { _, configured := s.connectors[platform] at, connected := connectedAt[platform] data.Connections = append(data.Connections, connectionView{ Platform: platform, Configured: configured, Connected: connected, ConnectedAt: at, }) } if err := s.templates.ExecuteTemplate(w, "verbindungen", data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } } // handleOAuthStart leitet zum Consent-Screen der Plattform weiter. Der // state-Wert wird in einem kurzlebigen Cookie gehalten und beim // Callback gegengeprüft — Schutz gegen CSRF (ein Angreifer könnte sonst // einen fremden Autorisierungscode gegen das Konto des Opfers // einschleusen). func (s *Server) handleOAuthStart(w http.ResponseWriter, r *http.Request) { connector, ok := s.connectors[r.PathValue("platform")] if !ok { http.Error(w, "Plattform nicht konfiguriert", http.StatusNotFound) return } state, err := auth.NewSessionToken() if err != nil { http.Error(w, "Anfrage konnte nicht vorbereitet werden: "+err.Error(), http.StatusInternalServerError) return } http.SetCookie(w, &http.Cookie{ Name: oauthStateCookieName, Value: state, Path: "/oauth", HttpOnly: true, Secure: r.TLS != nil, SameSite: http.SameSiteLaxMode, Expires: time.Now().Add(10 * time.Minute), }) http.Redirect(w, r, connector.AuthorizationURL(state), http.StatusSeeOther) } // handleOAuthCallback verarbeitet die Rückleitung von der Plattform: // state prüfen, Code gegen ein Token tauschen, Verbindung speichern. func (s *Server) handleOAuthCallback(w http.ResponseWriter, r *http.Request) { connector, ok := s.connectors[r.PathValue("platform")] if !ok { http.Error(w, "Plattform nicht konfiguriert", http.StatusNotFound) return } // Der Nutzer hat die Autorisierung abgelehnt — kein Fehler unsererseits. if errParam := r.URL.Query().Get("error"); errParam != "" { http.Redirect(w, r, "/verbindungen", http.StatusSeeOther) return } stateCookie, err := r.Cookie(oauthStateCookieName) if err != nil || stateCookie.Value == "" || stateCookie.Value != r.URL.Query().Get("state") { http.Error(w, "ungültiger oder abgelaufener State-Parameter", http.StatusBadRequest) return } http.SetCookie(w, &http.Cookie{Name: oauthStateCookieName, Value: "", Path: "/oauth", MaxAge: -1}) code := r.URL.Query().Get("code") if code == "" { http.Error(w, "kein Autorisierungscode erhalten", http.StatusBadRequest) return } token, err := connector.Exchange(r.Context(), code) if err != nil { http.Error(w, "Verbindung fehlgeschlagen: "+err.Error(), http.StatusBadGateway) return } var expiresAt *time.Time if !token.ExpiresAt.IsZero() { expiresAt = &token.ExpiresAt } accountID := currentUser(r).AccountID if _, err := s.store.UpsertPlatformConnection(r.Context(), accountID, connector.Platform(), token.PlatformUserID, token.AccessToken, token.RefreshToken, expiresAt); err != nil { http.Error(w, "Verbindung konnte nicht gespeichert werden: "+err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/verbindungen", http.StatusSeeOther) } // handleDisconnect trennt eine Plattform-Verbindung. func (s *Server) handleDisconnect(w http.ResponseWriter, r *http.Request) { accountID := currentUser(r).AccountID platform := r.PathValue("platform") if err := s.store.DeletePlatformConnection(r.Context(), accountID, platform); err != nil && !errors.Is(err, store.ErrNotFound) { http.Error(w, "Verbindung konnte nicht getrennt werden: "+err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/verbindungen", http.StatusSeeOther) }