Harden compliance portal auth and TLS

Align console references and OAuth branding with the
compliance-page model, and fix certificate cache eviction,
portal OAuth handlers, and magic-link edge cases left after
the trust-center rename.

Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
Bryan Frimin
2026-07-20 09:59:25 +02:00
parent b03acbd029
commit 43ce3a7c53
51 changed files with 626 additions and 458 deletions

View File

@@ -35,11 +35,11 @@ import { forwardRef, type ReactNode, useImperativeHandle, useState } from "react
import { z } from "zod";
import type { CompliancePageReferenceListItemFragment$data } from "#/__generated__/core/CompliancePageReferenceListItemFragment.graphql";
import {
useCreateTrustCenterReferenceMutation,
useUpdateTrustCenterReferenceMutation,
} from "#/hooks/graph/TrustCenterReferenceGraph";
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import {
useCreateCompliancePageReferenceMutation,
useUpdateCompliancePageReferenceMutation,
} from "#/pages/organizations/compliance-page/_lib/compliancePageReferenceMutations";
const referenceSchema = z.object({
name: z.string().min(1, "Name is required"),
@@ -65,8 +65,8 @@ export const CompliancePageReferenceDialog = forwardRef<CompliancePageReferenceD
const [editReference, setEditReference] = useState<CompliancePageReferenceListItemFragment$data | null>(null);
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
const [createReference, isCreating] = useCreateTrustCenterReferenceMutation();
const [updateReference, isUpdating] = useUpdateTrustCenterReferenceMutation();
const [createReference, isCreating] = useCreateCompliancePageReferenceMutation();
const [updateReference, isUpdating] = useUpdateCompliancePageReferenceMutation();
const { register, handleSubmit, formState: { errors }, reset } = useFormWithSchema(
referenceSchema,

View File

@@ -30,9 +30,9 @@ import {
useDialogRef,
} from "@probo/ui";
import type { TrustCenterReferenceGraphDeleteMutation } from "#/__generated__/core/TrustCenterReferenceGraphDeleteMutation.graphql";
import { deleteTrustCenterReferenceMutation } from "#/hooks/graph/TrustCenterReferenceGraph";
import type { compliancePageReferenceMutationsDeleteMutation } from "#/__generated__/core/compliancePageReferenceMutationsDeleteMutation.graphql";
import { useMutation } from "#/lib/relay/useMutation";
import { deleteCompliancePageReferenceMutation } from "#/pages/organizations/compliance-page/_lib/compliancePageReferenceMutations";
type Props = {
children: React.ReactNode;
@@ -52,8 +52,8 @@ export function DeleteCompliancePageReferenceDialog({
const { __ } = useTranslate();
const ref = useDialogRef();
const [mutate, isDeleting] = useMutation<TrustCenterReferenceGraphDeleteMutation>(
deleteTrustCenterReferenceMutation,
const [mutate, isDeleting] = useMutation<compliancePageReferenceMutationsDeleteMutation>(
deleteCompliancePageReferenceMutation,
{
successMessage: __("Reference deleted successfully"),
errorToast: __("Failed to delete reference"),

View File

@@ -1,135 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { graphql } from "react-relay";
import type { TrustCenterReferenceGraphCreateMutation } from "#/__generated__/core/TrustCenterReferenceGraphCreateMutation.graphql";
import type { TrustCenterReferenceGraphDeleteMutation } from "#/__generated__/core/TrustCenterReferenceGraphDeleteMutation.graphql";
import type { TrustCenterReferenceGraphUpdateMutation } from "#/__generated__/core/TrustCenterReferenceGraphUpdateMutation.graphql";
import type { TrustCenterReferenceGraphUpdateRankMutation } from "#/__generated__/core/TrustCenterReferenceGraphUpdateRankMutation.graphql";
import { useMutation } from "#/lib/relay/useMutation";
export const createTrustCenterReferenceMutation = graphql`
mutation TrustCenterReferenceGraphCreateMutation(
$input: CreateTrustCenterReferenceInput!
$connections: [ID!]!
) {
createTrustCenterReference(input: $input) {
trustCenterReferenceEdge @appendEdge(connections: $connections) {
cursor
node {
id
name
description
websiteUrl
logo {
downloadUrl
}
rank
createdAt
updatedAt
canUpdate: permission(action: "compliance-portal:portal-reference:update")
canDelete: permission(action: "compliance-portal:portal-reference:delete")
}
}
}
}
`;
export const updateTrustCenterReferenceMutation = graphql`
mutation TrustCenterReferenceGraphUpdateMutation(
$input: UpdateTrustCenterReferenceInput!
) {
updateTrustCenterReference(input: $input) {
trustCenterReference {
id
name
description
websiteUrl
logo {
downloadUrl
}
rank
createdAt
updatedAt
canUpdate: permission(action: "compliance-portal:portal-reference:update")
canDelete: permission(action: "compliance-portal:portal-reference:delete")
}
}
}
`;
export const deleteTrustCenterReferenceMutation = graphql`
mutation TrustCenterReferenceGraphDeleteMutation(
$input: DeleteTrustCenterReferenceInput!
$connections: [ID!]!
) {
deleteTrustCenterReference(input: $input) {
deletedTrustCenterReferenceId @deleteEdge(connections: $connections)
}
}
`;
export function useCreateTrustCenterReferenceMutation() {
return useMutation<TrustCenterReferenceGraphCreateMutation>(
createTrustCenterReferenceMutation,
{
successMessage: "Reference created successfully",
errorToast: "Failed to create reference",
},
);
}
export function useUpdateTrustCenterReferenceMutation() {
return useMutation<TrustCenterReferenceGraphUpdateMutation>(
updateTrustCenterReferenceMutation,
{
successMessage: "Reference updated successfully",
errorToast: "Failed to update reference",
},
);
}
export const updateTrustCenterReferenceRankMutation = graphql`
mutation TrustCenterReferenceGraphUpdateRankMutation(
$input: UpdateTrustCenterReferenceInput!
) {
updateTrustCenterReference(input: $input) {
trustCenterReference {
id
rank
}
}
}
`;
export function useUpdateTrustCenterReferenceRankMutation() {
return useMutation<TrustCenterReferenceGraphUpdateRankMutation>(
updateTrustCenterReferenceRankMutation,
{
successMessage: "Order updated successfully",
errorToast: "Failed to update order",
},
);
}
export function useDeleteTrustCenterReferenceMutation() {
return useMutation<TrustCenterReferenceGraphDeleteMutation>(
deleteTrustCenterReferenceMutation,
{
successMessage: "Reference deleted successfully",
errorToast: "Failed to delete reference",
},
);
}

View File

@@ -1,25 +1,29 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Button } from "@probo/ui";
import { useNavigate } from "react-router";
export default function MagicLinkAlreadyUsedPage() {
const { __ } = useTranslate();
const navigate = useNavigate();
usePageTitle(__("Link Already Used"));
@@ -33,10 +37,7 @@ export default function MagicLinkAlreadyUsedPage() {
)}
</p>
</div>
<Button
className="w-full h-10"
onClick={() => void navigate("/auth/login")}
>
<Button className="w-full h-10" to="/auth/login">
{__("Sign in")}
</Button>
</div>

View File

@@ -1,25 +1,29 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Button } from "@probo/ui";
import { useNavigate } from "react-router";
export default function MagicLinkExpiredPage() {
const { __ } = useTranslate();
const navigate = useNavigate();
usePageTitle(__("Link Expired"));
@@ -33,10 +37,7 @@ export default function MagicLinkExpiredPage() {
)}
</p>
</div>
<Button
className="w-full h-10"
onClick={() => void navigate("/auth/login")}
>
<Button className="w-full h-10" to="/auth/login">
{__("Sign in")}
</Button>
</div>

View File

@@ -42,9 +42,7 @@ export const signInPageQuery = graphql`
oauthClientBranding(clientId: $clientId) {
name
clientURL
logo {
downloadUrl
}
logoUrl
}
}
`;
@@ -85,7 +83,7 @@ export default function SignInPage(props: Props) {
<>
<OAuthClientBrandingSection
name={clientBranding.name}
logoDownloadUrl={clientBranding.logo?.downloadUrl}
logoDownloadUrl={clientBranding.logoUrl}
clientURL={clientBranding.clientURL}
/>
<div className="w-full border-t border-t-border-mid" />

View File

@@ -1,16 +1,22 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useTranslate } from "@probo/i18n";
import { Button, Field, useToast } from "@probo/ui";
@@ -67,12 +73,22 @@ export function MagicLinkForm() {
body.set("email", email);
body.set("continue", postAuthRedirectUrl);
const response = await fetch("/api/connect/v1/magic-link/send", {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
credentials: "include",
body,
});
let response: Response;
try {
response = await fetch("/api/connect/v1/magic-link/send", {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
credentials: "include",
body,
});
} catch {
toast({
title: __("Error"),
description: __("Cannot send magic link"),
variant: "error",
});
return;
}
if (!response.ok) {
toast({

View File

@@ -0,0 +1,141 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { graphql } from "react-relay";
import type { compliancePageReferenceMutationsCreateMutation } from "#/__generated__/core/compliancePageReferenceMutationsCreateMutation.graphql";
import type { compliancePageReferenceMutationsDeleteMutation } from "#/__generated__/core/compliancePageReferenceMutationsDeleteMutation.graphql";
import type { compliancePageReferenceMutationsUpdateMutation } from "#/__generated__/core/compliancePageReferenceMutationsUpdateMutation.graphql";
import type { compliancePageReferenceMutationsUpdateRankMutation } from "#/__generated__/core/compliancePageReferenceMutationsUpdateRankMutation.graphql";
import { useMutation } from "#/lib/relay/useMutation";
export const createCompliancePageReferenceMutation = graphql`
mutation compliancePageReferenceMutationsCreateMutation(
$input: CreateTrustCenterReferenceInput!
$connections: [ID!]!
) {
createTrustCenterReference(input: $input) {
trustCenterReferenceEdge @appendEdge(connections: $connections) {
cursor
node {
id
name
description
websiteUrl
logo {
downloadUrl
}
rank
createdAt
updatedAt
canUpdate: permission(action: "compliance-portal:portal-reference:update")
canDelete: permission(action: "compliance-portal:portal-reference:delete")
}
}
}
}
`;
export const updateCompliancePageReferenceMutation = graphql`
mutation compliancePageReferenceMutationsUpdateMutation(
$input: UpdateTrustCenterReferenceInput!
) {
updateTrustCenterReference(input: $input) {
trustCenterReference {
id
name
description
websiteUrl
logo {
downloadUrl
}
rank
createdAt
updatedAt
canUpdate: permission(action: "compliance-portal:portal-reference:update")
canDelete: permission(action: "compliance-portal:portal-reference:delete")
}
}
}
`;
export const deleteCompliancePageReferenceMutation = graphql`
mutation compliancePageReferenceMutationsDeleteMutation(
$input: DeleteTrustCenterReferenceInput!
$connections: [ID!]!
) {
deleteTrustCenterReference(input: $input) {
deletedTrustCenterReferenceId @deleteEdge(connections: $connections)
}
}
`;
export function useCreateCompliancePageReferenceMutation() {
return useMutation<compliancePageReferenceMutationsCreateMutation>(
createCompliancePageReferenceMutation,
{
successMessage: "Reference created successfully",
errorToast: "Failed to create reference",
},
);
}
export function useUpdateCompliancePageReferenceMutation() {
return useMutation<compliancePageReferenceMutationsUpdateMutation>(
updateCompliancePageReferenceMutation,
{
successMessage: "Reference updated successfully",
errorToast: "Failed to update reference",
},
);
}
export const updateCompliancePageReferenceRankMutation = graphql`
mutation compliancePageReferenceMutationsUpdateRankMutation(
$input: UpdateTrustCenterReferenceInput!
) {
updateTrustCenterReference(input: $input) {
trustCenterReference {
id
rank
}
}
}
`;
export function useUpdateCompliancePageReferenceRankMutation() {
return useMutation<compliancePageReferenceMutationsUpdateRankMutation>(
updateCompliancePageReferenceRankMutation,
{
successMessage: "Order updated successfully",
errorToast: "Failed to update order",
},
);
}
export function useDeleteCompliancePageReferenceMutation() {
return useMutation<compliancePageReferenceMutationsDeleteMutation>(
deleteCompliancePageReferenceMutation,
{
successMessage: "Reference deleted successfully",
errorToast: "Failed to delete reference",
},
);
}

View File

@@ -27,7 +27,7 @@ import { graphql } from "relay-runtime";
import type { CompliancePageReferenceListFragment$key } from "#/__generated__/core/CompliancePageReferenceListFragment.graphql";
import type { CompliancePageReferenceListItemFragment$data } from "#/__generated__/core/CompliancePageReferenceListItemFragment.graphql";
import type { CompliancePageReferenceListQuery } from "#/__generated__/core/CompliancePageReferenceListQuery.graphql";
import { useUpdateTrustCenterReferenceRankMutation } from "#/hooks/graph/TrustCenterReferenceGraph";
import { useUpdateCompliancePageReferenceRankMutation } from "#/pages/organizations/compliance-page/_lib/compliancePageReferenceMutations";
import { CompliancePageReferenceListItem } from "./CompliancePageReferenceListItem";
@@ -65,7 +65,7 @@ export function CompliancePageReferenceList(props: {
CompliancePageReferenceListQuery,
CompliancePageReferenceListFragment$key
>(fragment, fragmentRef);
const [updateRank] = useUpdateTrustCenterReferenceRankMutation();
const [updateRank] = useUpdateCompliancePageReferenceRankMutation();
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);

View File

@@ -94,7 +94,6 @@ export const currentTrustDocumentsQuery = graphql`
query TrustGraphCurrentDocumentsQuery {
currentTrustCenter {
id
title
documents(first: 50) {
edges {
node {
@@ -121,7 +120,6 @@ export const currentTrustSubprocessorsQuery = graphql`
query TrustGraphCurrentSubprocessorsQuery {
currentTrustCenter {
id
title
subprocessors(first: 50) {
edges {
node {