Add signature handlers

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-04-30 00:23:30 -07:00
parent a2bec08d16
commit 0387987612
4 changed files with 132 additions and 8 deletions

View File

@@ -5,12 +5,13 @@ import { buildEndpoint } from "../utils";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import ReactMarkdown from "react-markdown";
type Document = { type Document = {
id: string; policy_version_id: string;
title: string; title: string;
content: string; content: string;
signed: boolean; signed?: boolean;
}; };
type SigningResponse = { type SigningResponse = {
@@ -38,7 +39,7 @@ export default function SigningRequestsPage() {
async function fetchDocuments() { async function fetchDocuments() {
try { try {
const response = await fetch(buildEndpoint("/api/signing-requests"), { const response = await fetch(buildEndpoint("/api/console/v1/policies/signing-requests"), {
method: "GET", method: "GET",
headers: { headers: {
"Authorization": `Bearer ${token}`, "Authorization": `Bearer ${token}`,
@@ -50,8 +51,18 @@ export default function SigningRequestsPage() {
throw new Error("Failed to fetch signing documents"); throw new Error("Failed to fetch signing documents");
} }
const data: SigningResponse = await response.json(); const documents: Document[] = await response.json();
setSigningData(data); // 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) { } catch (err) {
setError(err instanceof Error ? err.message : "An unknown error occurred"); setError(err instanceof Error ? err.message : "An unknown error occurred");
} finally { } finally {
@@ -69,7 +80,7 @@ export default function SigningRequestsPage() {
const docToSign = signingData.documents[currentDocIndex]; const docToSign = signingData.documents[currentDocIndex];
try { 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", method: "POST",
headers: { headers: {
"Authorization": `Bearer ${token}`, "Authorization": `Bearer ${token}`,
@@ -203,7 +214,9 @@ export default function SigningRequestsPage() {
<CardContent> <CardContent>
<div className="border rounded-md p-4 min-h-[400px] bg-muted/20"> <div className="border rounded-md p-4 min-h-[400px] bg-muted/20">
<div dangerouslySetInnerHTML={{ __html: currentDoc.content }} /> <div className="prose prose-olive max-w-none">
<ReactMarkdown>{currentDoc.content}</ReactMarkdown>
</div>
</div> </div>
</CardContent> </CardContent>

View File

@@ -10,6 +10,7 @@ import (
"github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/page" "github.com/getprobo/probo/pkg/page"
"github.com/getprobo/probo/pkg/statelesstoken" "github.com/getprobo/probo/pkg/statelesstoken"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg" "go.gearno.de/kit/pg"
) )
@@ -174,6 +175,51 @@ func (s *PolicyService) Create(
return policy, policyVersion, nil 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( func (s *PolicyService) SendSigningNotifications(
ctx context.Context, ctx context.Context,
organizationID gid.GID, organizationID gid.GID,

View File

@@ -90,7 +90,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
corsOpts := cors.Options{ corsOpts := cors.Options{
AllowedOrigins: s.cfg.AllowedOrigins, AllowedOrigins: s.cfg.AllowedOrigins,
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "HEAD"}, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "HEAD"},
AllowedHeaders: []string{"content-type", "traceparent"}, AllowedHeaders: []string{"content-type", "traceparent", "authorization"},
ExposedHeaders: []string{"x-Request-id"}, ExposedHeaders: []string{"x-Request-id"},
AllowCredentials: true, 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) 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)

View File

@@ -18,9 +18,11 @@ package console_v1
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql"
@@ -35,6 +37,7 @@ import (
"github.com/getprobo/probo/pkg/saferedirect" "github.com/getprobo/probo/pkg/saferedirect"
"github.com/getprobo/probo/pkg/securecookie" "github.com/getprobo/probo/pkg/securecookie"
"github.com/getprobo/probo/pkg/server/api/console/v1/schema" "github.com/getprobo/probo/pkg/server/api/console/v1/schema"
"github.com/getprobo/probo/pkg/statelesstoken"
"github.com/getprobo/probo/pkg/usrmgr" "github.com/getprobo/probo/pkg/usrmgr"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/vektah/gqlparser/v2/gqlerror" "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 { func NewMux(proboSvc *probo.Service, usrmgrSvc *usrmgr.Service, authCfg AuthConfig, connectorRegistry *connector.ConnectorRegistry, safeRedirect *saferedirect.SafeRedirect) *chi.Mux {
r := chi.NewMux() 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/register", SignUpHandler(usrmgrSvc, authCfg))
r.Post("/auth/login", SignInHandler(usrmgrSvc, authCfg)) r.Post("/auth/login", SignInHandler(usrmgrSvc, authCfg))
r.Delete("/auth/logout", SignOutHandler(usrmgrSvc, authCfg)) r.Delete("/auth/logout", SignOutHandler(usrmgrSvc, authCfg))