Add signature handlers
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user