Rewrite identity and access management

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-12-03 19:23:15 +01:00
parent 4ed3f5a067
commit 74fc3b8cd1
201 changed files with 32895 additions and 23649 deletions

View File

@@ -31,7 +31,7 @@ import { formatDate } from "@probo/helpers";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { Controller } from "react-hook-form";
import { z } from "zod";
import { UnAuthenticatedError } from "/providers/RelayProviders";
import { UnAuthenticatedError } from "@probo/relay";
interface APIKey {
id: string;
@@ -56,10 +56,14 @@ interface Organization {
const createSchema = z.object({
name: z.string().min(1, "Name is required"),
expiresIn: z.enum(["1month", "3months", "6months", "1year"]),
organizations: z.array(z.object({
organizationId: z.string(),
role: z.string(),
})).min(1, "At least one organization is required"),
organizations: z
.array(
z.object({
organizationId: z.string(),
role: z.string(),
})
)
.min(1, "At least one organization is required"),
});
type CreateFormData = z.infer<typeof createSchema>;
@@ -79,19 +83,24 @@ export default function APIKeysPage() {
const [isCreating, setIsCreating] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [selectedOrganizations, setSelectedOrganizations] = useState<string[]>([]);
const [organizationRoles, setOrganizationRoles] = useState<Record<string, string>>({});
const [selectedOrganizations, setSelectedOrganizations] = useState<string[]>(
[]
);
const [organizationRoles, setOrganizationRoles] = useState<
Record<string, string>
>({});
const [editingAPIKey, setEditingAPIKey] = useState<APIKey | null>(null);
const [editingName, setEditingName] = useState<string>("");
const [error, setError] = useState<Error | null>(null);
const { formState, handleSubmit, reset, control, setValue, register } = useFormWithSchema(createSchema, {
defaultValues: {
name: new Date().toISOString().split('T')[0],
expiresIn: "1month",
organizations: [],
},
});
const { formState, handleSubmit, reset, control, setValue, register } =
useFormWithSchema(createSchema, {
defaultValues: {
name: new Date().toISOString().split("T")[0],
expiresIn: "1month",
organizations: [],
},
});
if (error) {
throw error;
@@ -99,14 +108,16 @@ export default function APIKeysPage() {
const fetchAPIKeys = async () => {
try {
const response = await fetch('/connect/api-keys', { credentials: 'include' });
const response = await fetch("/connect/api-keys", {
credentials: "include",
});
if (!response.ok) {
throw new Error('Failed to fetch API keys');
throw new Error("Failed to fetch API keys");
}
const data: { apiKeys: APIKey[] } = await response.json();
setApiKeys(data.apiKeys);
} catch (err) {
console.error('Failed to fetch API keys:', err);
console.error("Failed to fetch API keys:", err);
toast({
title: __("Error"),
description: __("Failed to load API keys"),
@@ -119,32 +130,40 @@ export default function APIKeysPage() {
const fetchData = async () => {
try {
const [apiKeysResponse, organizationsResponse] = await Promise.all([
fetch('/connect/api-keys', { credentials: 'include' }),
fetch('/connect/organizations?role=OWNER', { credentials: 'include' }),
fetch("/connect/api-keys", { credentials: "include" }),
fetch("/connect/organizations?role=OWNER", {
credentials: "include",
}),
]);
if (apiKeysResponse.status === 401 || organizationsResponse.status === 401) {
if (
apiKeysResponse.status === 401 ||
organizationsResponse.status === 401
) {
setError(new UnAuthenticatedError());
return;
}
if (!apiKeysResponse.ok) {
throw new Error('Failed to fetch API keys');
throw new Error("Failed to fetch API keys");
}
if (!organizationsResponse.ok) {
throw new Error('Failed to fetch organizations');
throw new Error("Failed to fetch organizations");
}
const apiKeysData: { apiKeys: APIKey[] } = await apiKeysResponse.json();
const orgsData: { organizations: Organization[] } = await organizationsResponse.json();
const orgsData: { organizations: Organization[] } =
await organizationsResponse.json();
const authenticatedOrgs = orgsData.organizations.filter(org => org.authStatus === "authenticated");
const authenticatedOrgs = orgsData.organizations.filter(
(org) => org.authStatus === "authenticated"
);
setApiKeys(apiKeysData.apiKeys);
setOrganizations(authenticatedOrgs);
} catch (err) {
console.error('Failed to fetch data:', err);
console.error("Failed to fetch data:", err);
toast({
title: __("Error"),
description: __("Failed to load data"),
@@ -179,12 +198,12 @@ export default function APIKeysPage() {
setIsCreating(true);
try {
const response = await fetch('/connect/api-keys', {
method: 'POST',
const response = await fetch("/connect/api-keys", {
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
credentials: 'include',
credentials: "include",
body: JSON.stringify({
name: formData.name,
expiresAt: expiresAt.toISOString(),
@@ -193,7 +212,7 @@ export default function APIKeysPage() {
});
if (!response.ok) {
throw new Error('Failed to create API key');
throw new Error("Failed to create API key");
}
const data: { apiKey: APIKey; key: string } = await response.json();
@@ -224,9 +243,9 @@ export default function APIKeysPage() {
const handleEdit = (apiKey: APIKey) => {
setEditingAPIKey(apiKey);
setEditingName(apiKey.name);
const orgIds = apiKey.organizations.map(org => org.organizationId);
const orgIds = apiKey.organizations.map((org) => org.organizationId);
const roles: Record<string, string> = {};
apiKey.organizations.forEach(org => {
apiKey.organizations.forEach((org) => {
roles[org.organizationId] = org.role;
});
setSelectedOrganizations(orgIds);
@@ -239,16 +258,16 @@ export default function APIKeysPage() {
setIsUpdating(true);
try {
const response = await fetch('/connect/api-keys', {
method: 'PUT',
const response = await fetch("/connect/api-keys", {
method: "PUT",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
credentials: 'include',
credentials: "include",
body: JSON.stringify({
id: editingAPIKey.id,
name: editingName,
organizations: selectedOrganizations.map(id => ({
organizations: selectedOrganizations.map((id) => ({
organizationId: id,
role: organizationRoles[id] || "FULL",
})),
@@ -256,7 +275,7 @@ export default function APIKeysPage() {
});
if (!response.ok) {
throw new Error('Failed to update API key');
throw new Error("Failed to update API key");
}
await fetchAPIKeys();
@@ -286,20 +305,20 @@ export default function APIKeysPage() {
async () => {
setIsDeleting(true);
try {
const response = await fetch('/connect/api-keys', {
method: 'DELETE',
const response = await fetch("/connect/api-keys", {
method: "DELETE",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
credentials: 'include',
credentials: "include",
body: JSON.stringify({ id }),
});
if (!response.ok) {
throw new Error('Failed to delete API key');
throw new Error("Failed to delete API key");
}
setApiKeys(apiKeys.filter(key => key.id !== id));
setApiKeys(apiKeys.filter((key) => key.id !== id));
toast({
title: __("Success"),
description: __("API Key deleted successfully"),
@@ -317,7 +336,9 @@ export default function APIKeysPage() {
}
},
{
message: __(`Are you sure you want to delete the API key "${name}"? This action cannot be undone.`),
message: __(
`Are you sure you want to delete the API key "${name}"? This action cannot be undone.`
),
}
);
};
@@ -326,11 +347,11 @@ export default function APIKeysPage() {
setIsLoadingKey(true);
try {
const response = await fetch(`/connect/api-keys/${id}`, {
credentials: 'include',
credentials: "include",
});
if (!response.ok) {
throw new Error('Failed to load API key');
throw new Error("Failed to load API key");
}
const data: { key: string } = await response.json();
@@ -400,7 +421,7 @@ export default function APIKeysPage() {
return (
<Card key={apiKey.id} padded className="w-full">
<div className="flex items-start justify-between gap-4">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2">
<h3 className="text-lg font-semibold">{apiKey.name}</h3>
@@ -464,7 +485,9 @@ export default function APIKeysPage() {
{__("Create an API key")}
</h2>
<p className="text-txt-tertiary mb-4">
{__("Generate a new API key for programmatic access to your organization")}
{__(
"Generate a new API key for programmatic access to your organization"
)}
</p>
<Button
onClick={() => dialogRef.current?.open()}
@@ -516,31 +539,34 @@ export default function APIKeysPage() {
type="button"
variant="tertiary"
onClick={() => {
const allSelected = selectedOrganizations.length === organizations.length;
const allSelected =
selectedOrganizations.length === organizations.length;
if (allSelected) {
setSelectedOrganizations([]);
setOrganizationRoles({});
setValue("organizations", []);
} else {
const allOrgIds = organizations.map(org => org.id);
const allOrgIds = organizations.map((org) => org.id);
const newRoles: Record<string, string> = {};
allOrgIds.forEach(id => {
allOrgIds.forEach((id) => {
newRoles[id] = organizationRoles[id] || "FULL";
});
setSelectedOrganizations(allOrgIds);
setOrganizationRoles(newRoles);
setValue(
"organizations",
allOrgIds.map(id => ({
allOrgIds.map((id) => ({
organizationId: id,
role: newRoles[id]
role: newRoles[id],
}))
);
}
}}
className="text-xs h-7 min-h-7"
>
{selectedOrganizations.length === organizations.length ? __("Clear All") : __("Select All")}
{selectedOrganizations.length === organizations.length
? __("Clear All")
: __("Select All")}
</Button>
)}
</div>
@@ -556,9 +582,7 @@ export default function APIKeysPage() {
<Th>{__("Name")}</Th>
<Th width={180}>{__("Role")}</Th>
<Th width={100}>
<div className="flex justify-end">
{__("Access")}
</div>
<div className="flex justify-end">{__("Access")}</div>
</Th>
</Tr>
</Thead>
@@ -570,30 +594,33 @@ export default function APIKeysPage() {
{org.name}
</div>
</Td>
<Td>
<div className="min-h-[36px] flex items-center">
{selectedOrganizations.includes(org.id) ? (
<Select
value={organizationRoles[org.id] || "FULL"}
onValueChange={(role) => {
const newRoles = { ...organizationRoles, [org.id]: role };
setOrganizationRoles(newRoles);
setValue(
"organizations",
selectedOrganizations.map(id => ({
organizationId: id,
role: newRoles[id] || "FULL"
}))
);
}}
>
<Option value="FULL">{__("Full")}</Option>
</Select>
) : (
<span className="text-txt-tertiary">—</span>
)}
</div>
</Td>
<Td>
<div className="min-h-[36px] flex items-center">
{selectedOrganizations.includes(org.id) ? (
<Select
value={organizationRoles[org.id] || "FULL"}
onValueChange={(role) => {
const newRoles = {
...organizationRoles,
[org.id]: role,
};
setOrganizationRoles(newRoles);
setValue(
"organizations",
selectedOrganizations.map((id) => ({
organizationId: id,
role: newRoles[id] || "FULL",
}))
);
}}
>
<Option value="FULL">{__("Full")}</Option>
</Select>
) : (
<span className="text-txt-tertiary">—</span>
)}
</div>
</Td>
<Td>
<div className="flex justify-end">
<Checkbox
@@ -602,21 +629,26 @@ export default function APIKeysPage() {
let newSelected: string[];
const newRoles = { ...organizationRoles };
if (checked) {
newSelected = [...selectedOrganizations, org.id];
newSelected = [
...selectedOrganizations,
org.id,
];
if (!newRoles[org.id]) {
newRoles[org.id] = "FULL";
}
} else {
newSelected = selectedOrganizations.filter(id => id !== org.id);
newSelected = selectedOrganizations.filter(
(id) => id !== org.id
);
delete newRoles[org.id];
}
setSelectedOrganizations(newSelected);
setOrganizationRoles(newRoles);
setValue(
"organizations",
newSelected.map(id => ({
newSelected.map((id) => ({
organizationId: id,
role: newRoles[id] || "FULL"
role: newRoles[id] || "FULL",
}))
);
}}
@@ -660,14 +692,15 @@ export default function APIKeysPage() {
type="button"
variant="tertiary"
onClick={() => {
const allSelected = selectedOrganizations.length === organizations.length;
const allSelected =
selectedOrganizations.length === organizations.length;
if (allSelected) {
setSelectedOrganizations([]);
setOrganizationRoles({});
} else {
const allOrgIds = organizations.map(org => org.id);
const allOrgIds = organizations.map((org) => org.id);
const newRoles: Record<string, string> = {};
allOrgIds.forEach(id => {
allOrgIds.forEach((id) => {
newRoles[id] = organizationRoles[id] || "FULL";
});
setSelectedOrganizations(allOrgIds);
@@ -676,7 +709,9 @@ export default function APIKeysPage() {
}}
className="text-xs h-7 min-h-7"
>
{selectedOrganizations.length === organizations.length ? __("Clear All") : __("Select All")}
{selectedOrganizations.length === organizations.length
? __("Clear All")
: __("Select All")}
</Button>
)}
</div>
@@ -692,9 +727,7 @@ export default function APIKeysPage() {
<Th>{__("Name")}</Th>
<Th width={180}>{__("Role")}</Th>
<Th width={100}>
<div className="flex justify-end">
{__("Access")}
</div>
<div className="flex justify-end">{__("Access")}</div>
</Th>
</Tr>
</Thead>
@@ -712,7 +745,10 @@ export default function APIKeysPage() {
<Select
value={organizationRoles[org.id] || "FULL"}
onValueChange={(role) => {
const newRoles = { ...organizationRoles, [org.id]: role };
const newRoles = {
...organizationRoles,
[org.id]: role,
};
setOrganizationRoles(newRoles);
}}
>
@@ -731,12 +767,17 @@ export default function APIKeysPage() {
let newSelected: string[];
const newRoles = { ...organizationRoles };
if (checked) {
newSelected = [...selectedOrganizations, org.id];
newSelected = [
...selectedOrganizations,
org.id,
];
if (!newRoles[org.id]) {
newRoles[org.id] = "FULL";
}
} else {
newSelected = selectedOrganizations.filter(id => id !== org.id);
newSelected = selectedOrganizations.filter(
(id) => id !== org.id
);
delete newRoles[org.id];
}
setSelectedOrganizations(newSelected);
@@ -754,7 +795,13 @@ export default function APIKeysPage() {
</div>
</DialogContent>
<DialogFooter>
<Button onClick={handleUpdate} disabled={isUpdating || (selectedOrganizations.length === 0 && !editingName.trim())}>
<Button
onClick={handleUpdate}
disabled={
isUpdating ||
(selectedOrganizations.length === 0 && !editingName.trim())
}
>
{isUpdating ? __("Updating...") : __("Update")}
</Button>
</DialogFooter>

View File

@@ -10,7 +10,6 @@ import {
IconCircleCheck,
} from "@probo/ui";
import { PDFPreview } from "../components/documents/PDFPreview";
import { buildEndpoint } from "/providers/RelayProviders";
import { useWindowSize } from "usehooks-ts";
import clsx from "clsx";
import { sprintf } from "@probo/helpers";
@@ -63,7 +62,7 @@ export default function DocumentSigningRequestsPage() {
async function fetchDocuments() {
try {
const response = await fetch(
buildEndpoint("/api/console/v1/documents/signing-requests"),
"/api/console/v1/documents/signing-requests",
{
method: "GET",
headers: {
@@ -114,9 +113,7 @@ export default function DocumentSigningRequestsPage() {
setSigning(true);
try {
const response = await fetch(
buildEndpoint(
`/api/console/v1/documents/signing-requests/${docToSign.document_version_id}/sign`
),
`/api/console/v1/documents/signing-requests/${docToSign.document_version_id}/sign`,
{
method: "POST",
headers: {
@@ -235,9 +232,7 @@ export default function DocumentSigningRequestsPage() {
// Build PDF URL with watermark
const pdfUrl = token
? `${buildEndpoint(
`/api/console/v1/documents/signing-requests/${currentDoc.document_version_id}/pdf`
)}?token=${encodeURIComponent(token)}`
? `${`/api/console/v1/documents/signing-requests/${currentDoc.document_version_id}/pdf`}?token=${encodeURIComponent(token)}`
: null;
return (
@@ -250,7 +245,10 @@ export default function DocumentSigningRequestsPage() {
<div className="grid lg:grid-cols-2 min-h-0 h-full">
<div className="max-w-[440px] mx-auto py-20 overflow-y-auto scrollbar-hide">
<h1 className="text-2xl font-semibold mb-6">
{sprintf(__("%s requests your signature"), signingData.organizationName)}
{sprintf(
__("%s requests your signature"),
signingData.organizationName
)}
</h1>
{allSigned ? (
<p className="text-txt-secondary text-base">
@@ -266,7 +264,10 @@ export default function DocumentSigningRequestsPage() {
<Card className="mb-6 overflow-hidden">
<div className="divide-y divide-border-solid">
{(() => {
const renderDocumentItem = (doc: Document, index: number) => (
const renderDocumentItem = (
doc: Document,
index: number
) => (
<div
key={doc.document_version_id}
className={clsx(
@@ -307,15 +308,15 @@ export default function DocumentSigningRequestsPage() {
doc.signed
? "bg-green-100 text-green-800"
: index === currentDocIndex
? "bg-blue-100 text-blue-800"
: "bg-gray-100 text-gray-700"
? "bg-blue-100 text-blue-800"
: "bg-gray-100 text-gray-700"
)}
>
{doc.signed
? __("Signed")
: index === currentDocIndex
? __("In review")
: __("Waiting signature")}
? __("In review")
: __("Waiting signature")}
</span>
</div>
</div>
@@ -352,8 +353,12 @@ export default function DocumentSigningRequestsPage() {
const currentIsLast = currentDocIndex === totalDocs - 1;
// Calculate hidden docs before and after current
const hiddenBeforeCurrent = currentIsFirst ? 0 : currentDocIndex - 1;
const hiddenAfterCurrent = currentIsLast ? 0 : totalDocs - currentDocIndex - 2;
const hiddenBeforeCurrent = currentIsFirst
? 0
: currentDocIndex - 1;
const hiddenAfterCurrent = currentIsLast
? 0
: totalDocs - currentDocIndex - 2;
return (
<>
@@ -367,12 +372,16 @@ export default function DocumentSigningRequestsPage() {
className="w-full py-3 px-4 text-sm text-txt-secondary hover:bg-level-1 transition-colors text-left flex items-center gap-2"
>
<span className="text-txt-tertiary">•••</span>
{sprintf(__("Show %s more documents"), hiddenBeforeCurrent)}
{sprintf(
__("Show %s more documents"),
hiddenBeforeCurrent
)}
</button>
)}
{/* Current document (if not first) */}
{!currentIsFirst && renderDocumentItem(currentDoc, currentDocIndex)}
{!currentIsFirst &&
renderDocumentItem(currentDoc, currentDocIndex)}
{/* Show more button for documents AFTER current (upcoming documents) */}
{hiddenAfterCurrent > 0 && (
@@ -381,7 +390,10 @@ export default function DocumentSigningRequestsPage() {
className="w-full py-3 px-4 text-sm text-txt-secondary hover:bg-level-1 transition-colors text-left flex items-center gap-2"
>
<span className="text-txt-tertiary">•••</span>
{sprintf(__("Show %s more documents"), hiddenAfterCurrent)}
{sprintf(
__("Show %s more documents"),
hiddenAfterCurrent
)}
</button>
)}
</>
@@ -390,9 +402,7 @@ export default function DocumentSigningRequestsPage() {
</div>
</Card>
<p className="text-txt-secondary text-sm mb-6">
{__(
"Please review the document carefully before signing."
)}
{__("Please review the document carefully before signing.")}
</p>
</>
)}
@@ -421,10 +431,7 @@ export default function DocumentSigningRequestsPage() {
</>
)}
{currentDoc.signed && !isLastDocument && (
<Button
onClick={handleNextDocument}
className="h-10 w-full mt-4"
>
<Button onClick={handleNextDocument} className="h-10 w-full mt-4">
{__("Next Document")}
</Button>
)}

View File

@@ -4,7 +4,6 @@ import { useTranslate } from "@probo/i18n";
import { z } from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { usePageTitle } from "@probo/hooks";
import { buildEndpoint } from "/providers/RelayProviders";
import { useState } from "react";
const schema = z.object({
@@ -22,17 +21,14 @@ export default function ForgotPasswordPage() {
});
const onSubmit = handleSubmit(async (data) => {
const response = await fetch(
buildEndpoint("/connect/forget-password"),
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify(data),
}
);
const response = await fetch("/connect/forget-password", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify(data),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));

View File

@@ -3,7 +3,6 @@ import { Button, Field, IconChevronLeft, useToast } from "@probo/ui";
import type { FormEventHandler } from "react";
import { useState } from "react";
import { Link, useSearchParams } from "react-router";
import { buildEndpoint } from "/providers/RelayProviders";
export default function LoginPage() {
const { __ } = useTranslate();
@@ -33,7 +32,7 @@ export default function LoginPage() {
setIsLoading(true);
try {
const res = await fetch(buildEndpoint("/connect/login"), {
const res = await fetch("/connect/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -68,7 +67,7 @@ export default function LoginPage() {
setIsChecking(true);
try {
const res = await fetch(buildEndpoint("/connect/check-sso"), {
const res = await fetch("/connect/check-sso", {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -78,15 +77,15 @@ export default function LoginPage() {
if (!res.ok) {
const error = await res.json();
throw new Error(error.message || __("SSO not available for this email domain"));
throw new Error(
error.message || __("SSO not available for this email domain")
);
}
const data = await res.json();
if (data.ssoAvailable && data.samlConfigId) {
window.location.href = buildEndpoint(
`/connect/saml/login/${data.samlConfigId}`
);
window.location.href = `/connect/saml/login/${data.samlConfigId}`;
} else {
throw new Error(__("SSO not available for this email domain"));
}
@@ -115,10 +114,7 @@ export default function LoginPage() {
{__("Choose your login method")}
</p>
<Button
className="w-full"
onClick={() => setMode("password")}
>
<Button className="w-full" onClick={() => setMode("password")}>
{__("Login with Email")}
</Button>
@@ -146,7 +142,10 @@ export default function LoginPage() {
<div className="text-center mt-6 text-sm text-txt-secondary">
{__("Don't have an account ?")}{" "}
<Link to="/auth/register" className="underline hover:text-txt-primary">
<Link
to="/auth/register"
className="underline hover:text-txt-primary"
>
{__("Register")}
</Link>
</div>
@@ -206,7 +205,10 @@ export default function LoginPage() {
<div className="text-center mt-6 text-sm text-txt-secondary">
{__("Don't have an account ?")}{" "}
<Link to="/auth/register" className="underline hover:text-txt-primary">
<Link
to="/auth/register"
className="underline hover:text-txt-primary"
>
{__("Register")}
</Link>
</div>
@@ -235,9 +237,7 @@ export default function LoginPage() {
<span className="text-sm">{__("Back")}</span>
</button>
<h1 className="text-center text-2xl font-bold">
{__("Login with SSO")}
</h1>
<h1 className="text-center text-2xl font-bold">{__("Login with SSO")}</h1>
<p className="text-center text-txt-tertiary mt-1 mb-6">
{__("Enter your work email to continue with SSO")}
</p>

View File

@@ -4,7 +4,6 @@ import { useTranslate } from "@probo/i18n";
import { z } from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { usePageTitle } from "@probo/hooks";
import { buildEndpoint } from "/providers/RelayProviders";
const schema = z.object({
email: z.string().email(),
@@ -25,17 +24,14 @@ export default function RegisterPage() {
});
const onSubmit = handleSubmit(async (data) => {
const response = await fetch(
buildEndpoint("/connect/register"),
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify(data),
const response = await fetch("/connect/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
);
credentials: "include",
body: JSON.stringify(data),
});
// Registration failed
if (!response.ok) {

View File

@@ -4,7 +4,6 @@ import { useTranslate } from "@probo/i18n";
import { z } from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { usePageTitle } from "@probo/hooks";
import { buildEndpoint } from "/providers/RelayProviders";
const schema = z
.object({
@@ -20,15 +19,12 @@ export default function ResetPasswordPage() {
const { __ } = useTranslate();
const navigate = useNavigate();
const { toast } = useToast();
const { register, handleSubmit, formState } = useFormWithSchema(
schema,
{
defaultValues: {
password: "",
confirmPassword: "",
},
}
);
const { register, handleSubmit, formState } = useFormWithSchema(schema, {
defaultValues: {
password: "",
confirmPassword: "",
},
});
const onSubmit = handleSubmit(async (data) => {
const searchParams = new URLSearchParams(location.search);
@@ -43,20 +39,17 @@ export default function ResetPasswordPage() {
return;
}
const response = await fetch(
buildEndpoint("/connect/reset-password"),
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({
token: token,
password: data.password,
}),
}
);
const response = await fetch("/connect/reset-password", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({
token: token,
password: data.password,
}),
});
// Reset failed
if (!response.ok) {

View File

@@ -4,7 +4,6 @@ import { useTranslate } from "@probo/i18n";
import { z } from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { usePageTitle } from "@probo/hooks";
import { buildEndpoint } from "/providers/RelayProviders";
import { useEffect } from "react";
const schema = z.object({
@@ -50,21 +49,18 @@ export default function SignupFromInvitationPage() {
return;
}
const response = await fetch(
buildEndpoint("/connect/signup-from-invitation"),
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({
token: token,
password: data.password,
fullName: data.fullName,
}),
}
);
const response = await fetch("/connect/signup-from-invitation", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({
token: token,
password: data.password,
fullName: data.fullName,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
@@ -78,7 +74,9 @@ export default function SignupFromInvitationPage() {
toast({
title: __("Success"),
description: __("Account created successfully. Please accept your invitation to join the organization."),
description: __(
"Account created successfully. Please accept your invitation to join the organization."
),
variant: "success",
});
navigate("/", { replace: true });