From 0387987612db2362811380e713f8ac5879b32496 Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Wed, 30 Apr 2025 00:23:30 -0700 Subject: [PATCH] Add signature handlers Signed-off-by: Bryan Frimin --- .../console/src/pages/SigningRequestsPage.tsx | 27 ++++++-- pkg/probo/policy_service.go | 46 +++++++++++++ pkg/server/api/api.go | 2 +- pkg/server/api/console/v1/resolver.go | 65 +++++++++++++++++++ 4 files changed, 132 insertions(+), 8 deletions(-) diff --git a/apps/console/src/pages/SigningRequestsPage.tsx b/apps/console/src/pages/SigningRequestsPage.tsx index 85a419a09..d0d061a6a 100644 --- a/apps/console/src/pages/SigningRequestsPage.tsx +++ b/apps/console/src/pages/SigningRequestsPage.tsx @@ -5,12 +5,13 @@ import { buildEndpoint } from "../utils"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; +import ReactMarkdown from "react-markdown"; type Document = { - id: string; + policy_version_id: string; title: string; content: string; - signed: boolean; + signed?: boolean; }; type SigningResponse = { @@ -38,7 +39,7 @@ export default function SigningRequestsPage() { async function fetchDocuments() { try { - const response = await fetch(buildEndpoint("/api/signing-requests"), { + const response = await fetch(buildEndpoint("/api/console/v1/policies/signing-requests"), { method: "GET", headers: { "Authorization": `Bearer ${token}`, @@ -50,8 +51,18 @@ export default function SigningRequestsPage() { throw new Error("Failed to fetch signing documents"); } - const data: SigningResponse = await response.json(); - setSigningData(data); + const documents: Document[] = await response.json(); + // Transform the API response to match our internal structure + const enhancedDocuments = documents.map(doc => ({ + ...doc, + signed: false + })); + + setSigningData({ + documents: enhancedDocuments, + requesterName: "Requester", // Default values as they're not in the API response + requesterOrganization: "Organization" + }); } catch (err) { setError(err instanceof Error ? err.message : "An unknown error occurred"); } finally { @@ -69,7 +80,7 @@ export default function SigningRequestsPage() { const docToSign = signingData.documents[currentDocIndex]; try { - const response = await fetch(buildEndpoint(`/api/signing-requests/${docToSign.id}/sign`), { + const response = await fetch(buildEndpoint(`/api/console/v1/policies/signing-requests/${docToSign.policy_version_id}/sign`), { method: "POST", headers: { "Authorization": `Bearer ${token}`, @@ -203,7 +214,9 @@ export default function SigningRequestsPage() {
-
+
+ {currentDoc.content} +
diff --git a/pkg/probo/policy_service.go b/pkg/probo/policy_service.go index 558dcb048..1993440ad 100644 --- a/pkg/probo/policy_service.go +++ b/pkg/probo/policy_service.go @@ -10,6 +10,7 @@ import ( "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/page" "github.com/getprobo/probo/pkg/statelesstoken" + "github.com/jackc/pgx/v5" "go.gearno.de/kit/pg" ) @@ -174,6 +175,51 @@ func (s *PolicyService) Create( return policy, policyVersion, nil } +func (s *PolicyService) ListSigningRequests( + ctx context.Context, + organizationID gid.GID, + peopleID gid.GID, +) ([]map[string]any, error) { + q := ` +SELECT + p.title, + pv.content, + pv.id AS policy_version_id +FROM + policies p + INNER JOIN policy_versions pv ON pv.policy_id = p.id + INNER JOIN policy_version_signatures pvs ON pvs.policy_version_id = pv.id +WHERE + p.tenant_id = $1 + AND pvs.signed_by = $2 + AND pvs.signed_at IS NULL +` + + var results []map[string]any + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + rows, err := conn.Query(ctx, q, s.svc.scope.GetTenantID(), peopleID) + if err != nil { + return fmt.Errorf("cannot query policies: %w", err) + } + + results, err = pgx.CollectRows(rows, pgx.RowToMap) + if err != nil { + return err + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return results, nil +} + func (s *PolicyService) SendSigningNotifications( ctx context.Context, organizationID gid.GID, diff --git a/pkg/server/api/api.go b/pkg/server/api/api.go index 7ad4b06d8..6aadc2940 100644 --- a/pkg/server/api/api.go +++ b/pkg/server/api/api.go @@ -90,7 +90,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { corsOpts := cors.Options{ AllowedOrigins: s.cfg.AllowedOrigins, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "HEAD"}, - AllowedHeaders: []string{"content-type", "traceparent"}, + AllowedHeaders: []string{"content-type", "traceparent", "authorization"}, ExposedHeaders: []string{"x-Request-id"}, AllowCredentials: true, MaxAge: 600, // 10 minutes (chrome >= 76 maximum value c.f. https://source.chromium.org/chromium/chromium/src/+/main:services/network/public/cpp/cors/preflight_result.cc;drc=52002151773d8cd9ffc5f557cd7cc880fddcae3e;l=36) diff --git a/pkg/server/api/console/v1/resolver.go b/pkg/server/api/console/v1/resolver.go index c6a064f2e..118c8264d 100644 --- a/pkg/server/api/console/v1/resolver.go +++ b/pkg/server/api/console/v1/resolver.go @@ -18,9 +18,11 @@ package console_v1 import ( "context" + "encoding/json" "errors" "fmt" "net/http" + "strings" "time" "github.com/99designs/gqlgen/graphql" @@ -35,6 +37,7 @@ import ( "github.com/getprobo/probo/pkg/saferedirect" "github.com/getprobo/probo/pkg/securecookie" "github.com/getprobo/probo/pkg/server/api/console/v1/schema" + "github.com/getprobo/probo/pkg/statelesstoken" "github.com/getprobo/probo/pkg/usrmgr" "github.com/go-chi/chi/v5" "github.com/vektah/gqlparser/v2/gqlerror" @@ -77,6 +80,68 @@ func UserFromContext(ctx context.Context) *coredata.User { func NewMux(proboSvc *probo.Service, usrmgrSvc *usrmgr.Service, authCfg AuthConfig, connectorRegistry *connector.ConnectorRegistry, safeRedirect *saferedirect.SafeRedirect) *chi.Mux { r := chi.NewMux() + r.Get( + "/policies/signing-requests", + func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("Authorization") + if token == "" { + http.Error(w, "token is required", http.StatusUnauthorized) + return + } + + token = strings.TrimPrefix(token, "Bearer ") + data, err := statelesstoken.ValidateToken[probo.SigningRequestData](authCfg.CookieSecret, probo.TokenTypeSigningRequest, token) + if err != nil { + http.Error(w, "invalid token", http.StatusUnauthorized) + return + } + + svc := proboSvc.WithTenant(data.Data.OrganizationID.TenantID()) + + requests, err := svc.Policies.ListSigningRequests(r.Context(), data.Data.OrganizationID, data.Data.PeopleID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(requests) + }, + ) + + r.Post( + "/policies/signing-requests/{policy_version_id}/sign", + func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("Authorization") + if token == "" { + http.Error(w, "token is required", http.StatusUnauthorized) + return + } + + token = strings.TrimPrefix(token, "Bearer ") + data, err := statelesstoken.ValidateToken[probo.SigningRequestData](authCfg.CookieSecret, probo.TokenTypeSigningRequest, token) + if err != nil { + http.Error(w, "invalid token", http.StatusUnauthorized) + return + } + + policyVersionID, err := gid.ParseGID(chi.URLParam(r, "policy_version_id")) + if err != nil { + http.Error(w, "invalid policy version id", http.StatusBadRequest) + return + } + + svc := proboSvc.WithTenant(data.Data.OrganizationID.TenantID()) + + if err := svc.Policies.SignPolicyVersion(r.Context(), policyVersionID, data.Data.PeopleID); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + }, + ) + r.Post("/auth/register", SignUpHandler(usrmgrSvc, authCfg)) r.Post("/auth/login", SignInHandler(usrmgrSvc, authCfg)) r.Delete("/auth/logout", SignOutHandler(usrmgrSvc, authCfg))