({ email: "", password: "", name: "" });
const [consentGiven, setConsentGiven] = useState(false);
useEffect(() => {
// Generate temporary ID on mount
const id = `temp_${uuidv4()}`;
setTempId(id);
fetchTempToken(id);
}, []);
const fetchTempToken = async (id: string) => {
const response = await fetch("/api/consent/temp-token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ temp_id: id }),
});
const data = await response.json();
setTokens(data);
setShowConsent(true);
};
const handleConsentAccept = () => {
setConsentGiven(true);
setShowConsent(false);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const response = await fetch("/api/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...formData, temp_id: tempId }),
});
if (response.ok) {
// Registration complete, redirect to dashboard
window.location.href = "/dashboard";
}
};
return (
{/* Step 1: Show consent modal first */}
{showConsent && tokens && (
{
// User declined - cannot proceed with registration
window.location.href = "/consent-required";
}}
onError={(error) => console.error("Consent error:", error)}
/>
)}
{/* Step 2: Show registration form after consent */}
{consentGiven && (
)}
);
}
export default SignUpPage;
```
***
## User Log In
When an existing user logs in, authenticate them first, then let the SDK check if consent is valid or needs to be renewed.
### Sequence Diagram
```mermaid
sequenceDiagram
participant U as User
participant F as Frontend
participant B as Your Backend
participant R as Redacto API
U->>F: Enter credentials
F->>B: POST /login
B->>B: Authenticate user
B->>R: POST /tokens/generate (org_user_id)
R-->>B: JWT tokens
B-->>F: Return user + tokens
F->>F: Render RedactoNoticeConsent with validateAgainst
Note over F,R: SDK checks consent status automatically
alt Consent needed/expired
F->>F: Show consent modal
U->>F: Accept consent
F->>R: Submit consent (via SDK)
R-->>F: Consent recorded
end
F-->>U: Login complete
```
### Backend Code
```typescript
import express, { Request, Response } from "express";
import axios from "axios";
const app = express();
app.use(express.json());
const CMS_API_KEY = process.env.CMS_API_KEY;
const REDACTO_BASE_URL = process.env.REDACTO_BASE_URL || "https://api.redacto.io";
const ORG_UUID = process.env.ORG_UUID;
const WORKSPACE_UUID = process.env.WORKSPACE_UUID;
app.post("/api/login", async (req: Request, res: Response) => {
const { email, password } = req.body;
try {
// 1. Authenticate user in your system
const user = await authenticateUser(email, password);
if (!user) {
res.status(401).json({ error: "Invalid credentials" });
return;
}
// 2. Generate tokens for the user
const tokenResponse = await axios.post(
`${REDACTO_BASE_URL}/consent/public/organisations/${ORG_UUID}/workspaces/${WORKSPACE_UUID}/tokens/generate`,
{ org_user_id: user.id },
{
headers: {
"X-CMS-API-Key": CMS_API_KEY,
"Content-Type": "application/json",
},
}
);
res.json({
user: { id: user.id, email: user.email, name: user.name },
token: tokenResponse.data.detail.token,
refresh_token: tokenResponse.data.detail.refresh_token,
});
} catch (error) {
console.error("Login error:", error);
res.status(500).json({ error: "Login failed" });
}
});
// Helper function (implement based on your auth system)
async function authenticateUser(email: string, password: string) {
// Your authentication logic here
// Returns user object or null if invalid
return { id: "user_123", email, name: "John Doe" };
}
app.listen(3000, () => console.log("Server running on port 3000"));
```
### Frontend Code
```tsx
import { RedactoNoticeConsent } from "@redacto.io/consent-sdk-react";
import { useState } from "react";
const NOTICE_UUID = process.env.NEXT_PUBLIC_NOTICE_UUID;
const REDACTO_BASE_URL = process.env.NEXT_PUBLIC_REDACTO_BASE_URL;
interface Tokens {
token: string;
refresh_token: string;
}
interface User {
id: string;
email: string;
name: string;
}
function LoginPage() {
const [tokens, setTokens] = useState(null);
const [user, setUser] = useState(null);
const [showConsent, setShowConsent] = useState(false);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const response = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: formData.get("email"),
password: formData.get("password"),
}),
});
if (!response.ok) {
alert("Login failed");
return;
}
const data = await response.json();
setUser(data.user);
setTokens({ token: data.token, refresh_token: data.refresh_token });
setShowConsent(true);
};
const handleConsentComplete = () => {
// Consent accepted or already valid
setShowConsent(false);
window.location.href = "/dashboard";
};
return (
{/* SDK checks consent status and shows modal only if needed */}
{showConsent && tokens && (
{
// User declined - handle accordingly
window.location.href = "/consent-required";
}}
onError={(error) => {
// If error is because consent already exists, proceed
console.error("Consent error:", error);
handleConsentComplete();
}}
/>
)}
);
}
export default LoginPage;
```
***
## Consent on Anonymous IDs with Linking
Collect consent from anonymous visitors, then link to their real identity when they register or log in.
### Sequence Diagram
```mermaid
sequenceDiagram
participant U as User
participant F as Frontend
participant B as Your Backend
participant R as Redacto API
Note over U,R: Phase 1: Anonymous Consent
U->>F: Visit site (anonymous)
F->>F: Generate anonymous ID (UUID)
F->>B: Request token for anon ID
B->>R: POST /tokens/generate (org_user_id: anon_xxx)
R-->>B: JWT tokens
B-->>F: Return tokens
F->>F: Show RedactoNoticeConsentInline
U->>F: Accept consent
F->>R: Submit consent (via SDK)
R-->>F: Consent recorded (anon user created)
Note over U,R: Phase 2: User Registers
U->>F: Fill registration form
F->>B: Submit with anon ID
B->>B: Create user in your system
B->>R: POST /consent-users/ (org_user_id: user_123)
R-->>B: User created
B->>R: POST /consent-users/link-users
Note right of B: primary: user_123
alias: anon_xxx
R-->>B: Users linked
B-->>F: Registration complete
F-->>U: Continue with consent preserved
```
### Backend Code
```typescript
import express, { Request, Response } from "express";
import axios from "axios";
const app = express();
app.use(express.json());
const CMS_API_KEY = process.env.CMS_API_KEY;
const REDACTO_BASE_URL = process.env.REDACTO_BASE_URL || "https://api.redacto.io";
const ORG_UUID = process.env.ORG_UUID;
const WORKSPACE_UUID = process.env.WORKSPACE_UUID;
// Generate token for anonymous user
app.post("/api/consent/anonymous-token", async (req: Request, res: Response) => {
const { anonymous_id } = req.body;
if (!anonymous_id) {
res.status(400).json({ error: "anonymous_id is required" });
return;
}
try {
const response = await axios.post(
`${REDACTO_BASE_URL}/consent/public/organisations/${ORG_UUID}/workspaces/${WORKSPACE_UUID}/tokens/generate`,
{ org_user_id: anonymous_id },
{
headers: {
"X-CMS-API-Key": CMS_API_KEY,
"Content-Type": "application/json",
},
}
);
res.json({
token: response.data.detail.token,
refresh_token: response.data.detail.refresh_token,
});
} catch (error) {
console.error("Error generating token:", error);
res.status(500).json({ error: "Failed to generate token" });
}
});
// Register user and link anonymous consent
app.post("/api/register-with-anonymous", async (req: Request, res: Response) => {
const { email, password, name, anonymous_id } = req.body;
try {
// 1. Create user in your system
const user = await createUser({ email, password, name });
// 2. Create consent user in Redacto
await axios.post(
`${REDACTO_BASE_URL}/consent/organisations/${ORG_UUID}/workspaces/${WORKSPACE_UUID}/consent-ledger/consent-users/`,
{
org_user_id: user.id,
primary_email: email,
name: name,
},
{
headers: {
"X-CMS-API-Key": CMS_API_KEY,
"Content-Type": "application/json",
},
}
);
// 3. Link anonymous user to the new user
if (anonymous_id) {
await axios.post(
`${REDACTO_BASE_URL}/consent/organisations/${ORG_UUID}/workspaces/${WORKSPACE_UUID}/consent-ledger/consent-users/link-users`,
{
primary_org_user_id: user.id,
alias_org_user_ids: [anonymous_id],
},
{
headers: {
"X-CMS-API-Key": CMS_API_KEY,
"Content-Type": "application/json",
},
}
);
}
// 4. Generate new tokens for the registered user
const tokenResponse = await axios.post(
`${REDACTO_BASE_URL}/consent/public/organisations/${ORG_UUID}/workspaces/${WORKSPACE_UUID}/tokens/generate`,
{ org_user_id: user.id, email: email },
{
headers: {
"X-CMS-API-Key": CMS_API_KEY,
"Content-Type": "application/json",
},
}
);
res.json({
user: { id: user.id, email, name },
token: tokenResponse.data.detail.token,
refresh_token: tokenResponse.data.detail.refresh_token,
});
} catch (error) {
console.error("Registration error:", error);
res.status(500).json({ error: "Registration failed" });
}
});
// Helper function
async function createUser(data: { email: string; password: string; name: string }) {
return { id: `user_${Date.now()}`, ...data };
}
app.listen(3000, () => console.log("Server running on port 3000"));
```
### Frontend Code
```tsx
import { RedactoNoticeConsentInline } from "@redacto.io/consent-sdk-react";
import { useState, useEffect } from "react";
import { v4 as uuidv4 } from "uuid";
const ORG_UUID = process.env.NEXT_PUBLIC_ORG_UUID;
const WORKSPACE_UUID = process.env.NEXT_PUBLIC_WORKSPACE_UUID;
const NOTICE_UUID = process.env.NEXT_PUBLIC_NOTICE_UUID;
const REDACTO_BASE_URL = process.env.NEXT_PUBLIC_REDACTO_BASE_URL;
interface Tokens {
accessToken: string | undefined;
refreshToken: string | undefined;
}
function LandingPage() {
const [anonymousId, setAnonymousId] = useState("");
const [tokens, setTokens] = useState({ accessToken: undefined, refreshToken: undefined });
const [consentGiven, setConsentGiven] = useState(false);
useEffect(() => {
// Generate or retrieve anonymous ID
let anonId = localStorage.getItem("anon_consent_id");
if (!anonId) {
anonId = `anon_${uuidv4()}`;
localStorage.setItem("anon_consent_id", anonId);
}
setAnonymousId(anonId);
fetchAnonymousToken(anonId);
}, []);
const fetchAnonymousToken = async (anonId: string) => {
const response = await fetch("/api/consent/anonymous-token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ anonymous_id: anonId }),
});
const data = await response.json();
setTokens({ accessToken: data.token, refreshToken: data.refresh_token });
};
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const response = await fetch("/api/register-with-anonymous", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: formData.get("email"),
password: formData.get("password"),
name: formData.get("name"),
anonymous_id: anonymousId,
}),
});
if (response.ok) {
// Anonymous consent is now linked to the user
localStorage.removeItem("anon_consent_id");
window.location.href = "/dashboard";
}
};
return (
Welcome!
{/* Inline consent for anonymous users */}
setConsentGiven(valid)}
onAccept={() => console.log("Anonymous consent recorded")}
onError={(error) => console.error("Consent error:", error)}
/>
{/* Show registration form after consent */}
{consentGiven && (
)}
);
}
export default LandingPage;
```
***
## Consent from Multiple Channels with Linking
When users interact with your platform through different channels (web, mobile app, email), consolidate their consent records. Note that consent users are created when consent is submitted, not when tokens are generated.
### Sequence Diagram
```mermaid
sequenceDiagram
participant W as Web User
participant M as Mobile User
participant B as Your Backend
participant R as Redacto API
Note over W,R: Channel 1: Web (Email identifier)
W->>B: Consent via web form
B->>R: POST /tokens/generate (email: user@example.com)
R-->>B: Tokens
W->>R: Submit consent via SDK
R-->>W: Consent recorded (user created with email as org_user_id)
Note over M,R: Channel 2: Mobile App (Phone identifier)
M->>B: Consent via mobile app
B->>R: POST /tokens/generate (mobile: +1234567890)
R-->>B: Tokens
M->>R: Submit consent via SDK
R-->>M: Consent recorded (user created with phone as org_user_id)
Note over B,R: Channel 3: User identified as same person
B->>B: Detect same user across channels
B->>R: POST /consent-users/ (org_user_id: customer_123)
R-->>B: Primary user created
B->>R: POST /consent-users/link-users
Note right of B: primary: customer_123
aliases: [email, phone]
R-->>B: All consent consolidated
```
### Backend Code
```typescript
import express, { Request, Response } from "express";
import axios, { AxiosError } from "axios";
const app = express();
app.use(express.json());
const CMS_API_KEY = process.env.CMS_API_KEY;
const REDACTO_BASE_URL = process.env.REDACTO_BASE_URL || "https://api.redacto.io";
const ORG_UUID = process.env.ORG_UUID;
const WORKSPACE_UUID = process.env.WORKSPACE_UUID;
interface LinkResult {
primary_user_uuid: string;
linked: string[];
already_linked: string[];
not_found: string[];
conflicts: Array<{
org_user_id: string;
existing_primary_org_user_id: string;
}>;
}
// When user is identified across channels
app.post("/api/consolidate-user", async (req: Request, res: Response) => {
const { user_id, email, phone } = req.body;
try {
// 1. Create the primary consent user with your system's ID
try {
await axios.post(
`${REDACTO_BASE_URL}/consent/organisations/${ORG_UUID}/workspaces/${WORKSPACE_UUID}/consent-ledger/consent-users/`,
{
org_user_id: user_id,
primary_email: email,
primary_mobile: phone,
},
{
headers: {
"X-CMS-API-Key": CMS_API_KEY,
"Content-Type": "application/json",
},
}
);
} catch (error) {
const axiosError = error as AxiosError;
// Handle 409 if user already exists - that's fine
if (axiosError.response?.status !== 409) {
throw error;
}
}
// 2. Build list of potential alias identifiers
// These are the org_user_ids that were created when consent was submitted
const aliasIds: string[] = [];
// When consent is submitted with just email, the email becomes the org_user_id
if (email) {
aliasIds.push(email);
}
// When consent is submitted with just phone, the phone becomes the org_user_id
if (phone) {
aliasIds.push(phone);
}
// 3. Link all aliases to the primary user
let linkResult: LinkResult | null = null;
if (aliasIds.length > 0) {
const linkResponse = await axios.post(
`${REDACTO_BASE_URL}/consent/organisations/${ORG_UUID}/workspaces/${WORKSPACE_UUID}/consent-ledger/consent-users/link-users`,
{
primary_org_user_id: user_id,
alias_org_user_ids: aliasIds,
},
{
headers: {
"X-CMS-API-Key": CMS_API_KEY,
"Content-Type": "application/json",
},
}
);
linkResult = linkResponse.data.detail;
console.log("Link results:", linkResult);
}
res.json({
success: true,
link_result: linkResult,
});
} catch (error) {
console.error("Consolidation error:", error);
res.status(500).json({ error: "Failed to consolidate user" });
}
});
app.listen(3000, () => console.log("Server running on port 3000"));
```
### Handling Link Results
```typescript
interface LinkResult {
primary_user_uuid: string;
linked: string[];
already_linked: string[];
not_found: string[];
conflicts: Array<{
org_user_id: string;
existing_primary_org_user_id: string;
}>;
}
async function handleLinkResults(result: LinkResult): Promise {
// Successfully linked
if (result.linked.length > 0) {
console.log(`Linked ${result.linked.length} users`);
}
// Already linked (idempotent, no action needed)
if (result.already_linked.length > 0) {
console.log(`${result.already_linked.length} already linked`);
}
// Not found - these users haven't given consent yet
// (consent submission creates users, not token generation)
if (result.not_found.length > 0) {
console.log(`${result.not_found.length} not found - no consent submitted yet`);
// Store for later linking when they do submit consent
await storeForLaterLinking(result.not_found);
}
// Conflicts - already linked to another primary
if (result.conflicts.length > 0) {
// May need manual review or different resolution strategy
for (const conflict of result.conflicts) {
console.warn(
`${conflict.org_user_id} already linked to ${conflict.existing_primary_org_user_id}`
);
}
}
}
async function storeForLaterLinking(identifiers: string[]): Promise {
// Store identifiers to retry linking later
// Implementation depends on your system
}
```
### Use Case: CRM Integration
```typescript
interface CRMCustomer {
crm_id: string;
email?: string;
phone?: string;
web_session_id?: string;
app_device_id?: string;
}
// When CRM identifies a customer across touchpoints
async function syncCustomerConsent(crmCustomer: CRMCustomer): Promise {
const identifiers: string[] = [];
// Collect all known identifiers that may have been used for consent
if (crmCustomer.email) identifiers.push(crmCustomer.email);
if (crmCustomer.phone) identifiers.push(crmCustomer.phone);
if (crmCustomer.web_session_id) identifiers.push(crmCustomer.web_session_id);
if (crmCustomer.app_device_id) identifiers.push(crmCustomer.app_device_id);
// Create primary user with CRM ID
await createConsentUser({
org_user_id: crmCustomer.crm_id,
primary_email: crmCustomer.email,
primary_mobile: crmCustomer.phone,
metadata: { source: "crm_sync" },
});
// Link all identifiers
const result = await linkUsers({
primary_org_user_id: crmCustomer.crm_id,
alias_org_user_ids: identifiers,
});
// Handle results
await handleLinkResults(result);
// Now all consent from any channel is consolidated
}
// Helper functions (implement based on your setup)
async function createConsentUser(data: {
org_user_id: string;
primary_email?: string;
primary_mobile?: string;
metadata?: Record;
}): Promise {
// Call Redacto API to create user
}
async function linkUsers(data: {
primary_org_user_id: string;
alias_org_user_ids: string[];
}): Promise {
// Call Redacto API to link users
return {} as LinkResult;
}
```