Wednesday, July 3, 2024

AEM Universal Visual Editor: Easily Author AEM Content Anywhere with In-Context Editing (Part 2)

 In my previous post AEM Universal Visual Editor: Easily Author AEM Content Anywhere with In-Context Editing (Part 1) | by Albin Issac | Tech Learnings | Jun, 2024 | Medium, we discussed the basics of the AEM Universal Visual Editor and how to use it to support both headless and headful content authoring. In this post, let’s review how to enable a component/container-based approach to drag and drop components from the property rail, as well as how to rearrange components, and more.

Components/Container:

Parts of the DOM can be marked as a container for other components or as individual components that are movable and deletable within the container.

data-aue-type
In our Part 1 post, we saw different data-aue-type values. Here are additional values to mark the DOM as a Container/Component:

  • container: The editable behaves as a container for components, also known as a Paragraph System.
  • component: The editable is a component. It doesn’t add additional functionality but is required to indicate movable/deletable parts of the DOM and to open the properties rail and its fields.

data-aue-behavior

  • component: Used to allow standalone text, rich text, and media to mimic components so they are also movable and deletable on the page. This also allows containers to be treated as their own components, making them movable and deletable on the page.

In the example below, the div is marked with data-aue-type="richtext", so it will act like a rich text editor for the property rail or inline editing. The attribute data-aue-behavior="component" marks this as a component and allows the moving and deleting of this div.

 <div
dangerouslySetInnerHTML={data.text ? { __html: data.text } : null}
data-aue-resource={`urn:aemconnection:${path}`}
id={data.id}
data-aue-type="richtext"
data-aue-prop="text"
data-aue-label="Rich Text"
data-aue-behavior="component"
/>

In the example below, the div is marked with data-aue-type="container", so it will act as a container for other components. The attribute data-aue-behavior="component" marks this div itself as a component and allows it to be moved and deleted.

<div
data-aue-type="container"
data-aue-behavior="component"
data-aue-resource={`urn:aemconnection:${BASE_CONTAINER_PATH}`}
data-aue-filter="container-filter"
>

I am maintaining a page /content/sites/test in AEM that has a container /content/sites/test/jcr:content/root/container/container with some child components. I am rendering that container along with the child components in my headless application. Here is the complete App.js for reference:

import React, { useEffect, useState } from "react";

// Function to fetch the JSON data for a given path
async function fetchData(path) {
const response = await fetch(path);
const data = await response.json();
return data;
}

// Base path for container
const BASE_CONTAINER_PATH = "/content/sites/test/jcr:content/root/container/container";

// Component to render text content
const TitleComponent = ({ data, itemKey }) => {
const TitleTag = data.type || "h2";
const path = `${BASE_CONTAINER_PATH}/${itemKey}`;
return (
<TitleTag
dangerouslySetInnerHTML={data.text ? { __html: data.text } : null}
data-aue-resource={`urn:aemconnection:${path}`}
data-aue-type="text"
id={data.id}
data-aue-prop="jcr:title"
data-aue-label="Title"
data-aue-model="title"
data-aue-behavior="component"
/>

);
};

// Component to render rich text content
const TextComponent = ({ data, itemKey }) => {
const path = `${BASE_CONTAINER_PATH}/${itemKey}`;
return (
<div
dangerouslySetInnerHTML={data.text ? { __html: data.text } : null}
data-aue-resource={`urn:aemconnection:${path}`}
id={data.id}
data-aue-type="richtext"
data-aue-prop="text"
data-aue-label="Rich Text"
data-aue-behavior="component"
/>

);
};

// Component to render image content
const ImageComponent = ({ data, itemKey }) => {
const path = `${BASE_CONTAINER_PATH}/${itemKey}`;
return (
<img
src={data.src}
id={data.id}
alt={data.alt}
srcSet={data.srcset}
data-aue-resource={`urn:aemconnection:${path}`}
data-aue-type="media"
data-aue-prop="fileReference"
data-aue-label="Image"
data-aue-model="image"
data-aue-behavior="component"
/>

);
};

// Map of aem component types to React components
const componentMap = {
"test/components/core/title": TitleComponent,
"test/components/core/text": TextComponent,
"test/components/core/image": ImageComponent,
// Add more mappings here as needed
};

// Component to render a dynamic component based on its type
const DynamicComponent = ({ data, itemKey }) => {
if (!data || !data[":type"]) return null;

const Component = componentMap[data[":type"]];
if (!Component) {
console.error(`No component found for type: ${data[":type"]}`);
return null;
}

return <Component data={data} itemKey={itemKey} />;
};

// Container component to allow drag-and-drop functionality
const Container = ({ data }) => {
return (
<div
data-aue-type="container"
data-aue-resource={`urn:aemconnection:${BASE_CONTAINER_PATH}`}
data-aue-filter="container-filter"
>

{data[":itemsOrder"] &&
data[":itemsOrder"].map((itemKey) => {
const child = data[":items"][itemKey];
return <DynamicComponent key={child.id} data={child} itemKey={itemKey} />;
})}
</div>

);
};

// Main App component
const App = () => {
const [containerData, setContainerData] = useState(null);
const [error, setError] = useState(null);

useEffect(() => {
async function fetchContainerData() {
try {
const data = await fetchData(
"/content/sites/test/jcr:content/root/container/container.model.json"
);
setContainerData(data);
} catch (err) {
setError("Failed to fetch container data");
}
}

fetchContainerData();
}, []);

if (error) {
return <div>Error: {error}</div>;
}

if (!containerData) {
return <div>Loading...</div>;
}

return (
<div>
<h1>Dynamic Container</h1>
<Container data={containerData} />
</div>

);
};

export default App;

Now the container and the child components are displayed in the order defined on the AEM page. The components can be deleted and rearranged within the container, and any content updates are persisted back to the AEM page.

Still, we will not be able to add new components to the container or customize the property rail for component authoring. The current property rail is the default one based on the defined data-aue-type.

Component Model/Definition/Filters:

To enable component addition and custom editing views on the property rail for components, we need to define component models, definitions, and filters. These can be defined inline on the page or in external files within the website. I am going to define them through external files.

  • component-models.json: Defines the model to edit the component on the property rail. You can use multiple component types to display the authoring view for the component. Refer to Model Definitions, Fields, and Component Types | Adobe Experience Manager for more details. I am not defining custom fields for the text component and am using the out-of-the-box (OOTB) field for authoring the content.
[
{
"id": "title",
"fields": [
{
"component": "text",
"valueType": "string",
"name": "jcr:title",
"value": "",
"label": "Text"
},
{
"component": "select",
"name": "type",
"value": "h1",
"label": "Type",
"valueType": "string",
"options": [
{
"name": "h1",
"value": "h1"
},
{
"name": "h2",
"value": "h2"
},
{
"name": "h3",
"value": "h3"
},
{
"name": "h4",
"value": "h4"
},
{
"name": "h5",
"value": "h5"
},
{
"name": "h6",
"value": "h6"
}
]
}
]
},
{
"id": "image",
"fields": [
{
"component": "text",
"valueType": "string",
"name": "alt",
"value": "Default alt",
"label": "Alt Text"
}
]
}
]
  • component-definition.json: Groups the components under different categories and maps the components to AEM resource types or other content sources.
{
"groups": [
{
"title": "Default Content",
"id": "default",
"components": [
{
"title": "Title",
"id": "title",
"plugins": {
"aem": {
"page": {
"resourceType": "test/components/core/title",
"template": {}
}
}
}
},
{
"title": "Text",
"id": "text",
"plugins": {
"aem": {
"page": {
"resourceType": "test/components/core/text",
"template": {}
}
}
}
},
{
"title": "Image",
"id": "image",
"plugins": {
"aem": {
"page": {
"resourceType": "test/components/core/image",
"template": {}
}
}
}
}
]
}
]
}
  • component-filters.json: Defines component filters for different containers, allowing specific components in specific containers.
[
{
"id": "container-filter",
"components": ["title","text","image"]
}
]

I stored these files under the static folder in the public folder of my React app. Now, add the following lines to the head section of the index.html file:

<script type="application/vnd.adobe.aue.component+json" src="%PUBLIC_URL%/static/component-definition.json"></script>
<script type="application/vnd.adobe.aue.model+json" src="%PUBLIC_URL%/static/component-models.json"></script>
<script type="application/vnd.adobe.aue.filter+json" src="%PUBLIC_URL%/static/component-filters.json"></script>

Now, associate the model for the DIVs and also associate the filter for the containers (refer to the App.js shared above).

<TitleTag
dangerouslySetInnerHTML={data.text ? { __html: data.text } : null}
data-aue-resource={`urn:aemconnection:${path}`}
data-aue-type="text"
id={data.id}
data-aue-prop="jcr:title"
data-aue-label="Title"
data-aue-model="title"
data-aue-behavior="component"
/>
 <img
src={data.src}
id={data.id}
alt={data.alt}
srcSet={data.srcset}
data-aue-resource={`urn:aemconnection:${path}`}
data-aue-type="media"
data-aue-prop="fileReference"
data-aue-label="Image"
data-aue-model="image"
data-aue-behavior="component"
/>
    <div
data-aue-type="container"
data-aue-resource={`urn:aemconnection:${BASE_CONTAINER_PATH}`}
data-aue-filter="container-filter"
>

Now you will be able to add new components under the container also able to edit the component with the model defined.

In this post, we discuss how to maintain and render a container with child components from an AEM page in a headless application. We outline the steps to define and associate models, definitions, and filters for components to enable custom editing views and component management using the Universal Editor. This includes adding components to the container and rearranging the components within the container. We will delve deeper into the use cases of the Universal Editor in upcoming posts.

The same approach can be applied for editing AEM pages based on Core components. As discussed in the previous post, the current version of Core components does not support the Universal Editor out-of-the-box, but we hope that future versions will include the necessary AEM changes to support the Universal Editor for editing pages based on Core components.

Friday, June 28, 2024

Implementing Cross-Domain Cookie Handling for Seamless API Integration

 When working with an application that integrates with external applications and APIs on different domains, maintaining a shared state, such as authentication, can be challenging due to browser restrictions on cross-domain cookies. While data can be shared using browser local/session storage and cookies can be set client-side, these approaches increase the risk of XSS attacks. Using Secure and HttpOnly cookies provides more security, as these cookies can only be managed through the backend and are seamlessly sent on every same-origin request. This post discusses the approach to making cookies set by an API running on a different domain available to the application’s domain in the browser.

Scenario:

The application runs on https://test.mydomain.com:8080 invokes an API at https://test.api.com:8081 from the client side. The API sets a cookie that should be available for test.mydomain.com. Additionally, all subsequent requests to https://test.mydomain.com or any subdomains on mydomain.com should include the cookie to maintain the state.

Problem:

In a cross-domain context, the request to the https://test.api.com:8081 API service cannot set the cookie for test.mydomain.com or the root domain.mydomain.com due to browser cross-domain restrictions. The browser rejects the cookie in this scenario.

Solution:

To solve this problem, proxy the API calls through the same domain. This approach ensures that the cookies are correctly handled and included in all requests. Here are the steps to achieve this:

Proxy API Calls Through the Same Domain:

  • Use a CDN: If a CDN is used, configure it to send the /api calls to the API servers.
  • Use Proxy Servers: Use proxy servers like Apache or Nginx to handle API requests.
  • Server Handling: The server handling https://test.mydomain.com requests can proxy the API requests to https://test.api.com.

Setting the Cookie Domain:

  • The API can avoid setting the cookie domain explicitly, allowing the browser to use the current domain (test.mydomain.com).
  • Alternatively, the API can set the cookie directly on test.mydomain.com or .mydomain.com. This ensures the cookie is available for test.mydomain.com and all subdomains under mydomain.com.

Dynamic Cookie Setting:

  • If the cookie needs to be set dynamically based on the requesting domain, use the Referer header to determine the domain and set the cookie accordingly.

Proxy Rewriting:

  • If you do not have control over the API to set the cookies on your original domain, configure the proxy to rewrite the cookie domain of the API response to the actual domain before sending the response to the client.

Forwarding Cookies:

  • In a forward flow, forward the cookie to the API domain. Most proxies support this.

Implementation Example:

Here’s a demonstration using two Express servers. The server.js handles all requests on https://test.mydomain.com:8080 and proxies API requests to api-server.js running on https://test.api.com:8081.

server.js:

const express = require("express");
const path = require("path");
const https = require("https");
const fs = require("fs");
const { createProxyMiddleware } = require("http-proxy-middleware");
const app = express();
const port = 8080;

// Load SSL key and certificate
const key = fs.readFileSync("SSL/server.key");
const cert = fs.readFileSync("SSL/server.crt");
const options = { key, cert };

// Serve static files from the "public" directory
app.use(express.static(path.join(__dirname, "public")));

// Ignore self-signed certificate errors (development only)
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

//https://test.mydomain.com:8080/api
// Proxy setup to forward API requests to the API server
app.use(
"/api",
createProxyMiddleware({
//target: "https://test.api.com:8081",
target: "https://test.synvie.de:8081",
changeOrigin: true,
pathRewrite: { "^/api": "" },
secure: false, // Allow self-signed certificates
/** on:{
proxyRes: (proxyRes, req, res) => {

console.log('Inside onProxyRes...'); // Log original cookies

const cookies = proxyRes.headers['set-cookie'];
if (cookies) {
console.log('Original cookies:', cookies); // Log original cookies
proxyRes.headers['Set-Cookie'] = cookies.map(cookie => {
const newCookie = cookie.replace(/Domain=[^;]+;/i, 'Domain=test.mydomain.com;');
console.log('Modified cookie:', newCookie); // Log modified cookie
return newCookie;
});
}
}
},**/
// if the target service can not set domain in appropriate client side root domain, the cookie need to be rewritten to the clinet side root domain from backend API domain using CDN(if supported) or another proxy server like Apache
onError: (err, req, res) => {
console.error("Proxy error:", err);
res.status(500).send("Proxy error occurred");
},
})
);

// Endpoint to check for cookies
//https://test.mydomain.com:8080/check
app.get("/check", (req, res) => {
const cookie = req.headers.cookie || "No cookies found";
console.log("Cookies received:", cookie); // Add logging
res.send(`Cookies received: ${cookie}`);
});

// Add error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send("Something broke!");
});

// Create HTTPS server
https.createServer(options, app).listen(port, () => {
console.log(`Client server running at https://test.mydomain.com:${port}`);
});

api-server.js:

const express = require("express");
const cors = require("cors");
const https = require("https");
const fs = require("fs");
const app = express();
const port = 8081;

// Load SSL key and certificate
const key = fs.readFileSync("SSL/server.key");
const cert = fs.readFileSync("SSL/server.crt");
const options = { key, cert };

// CORS configuration to allow requests from your client domain
app.use(
cors({
origin: "https://test.mydomain.com:8080",
credentials: true,
})
);

//https://test.api.com:8081

app.get("/", (req, res) => {
console.log("Server Name:" + req.hostname);
console.log("Referrer:" + req.get("Referer"));
// Get the referer header
const referer = req.get("Referer");
if (referer) {
// Extract the root domain from the referer
const url = new URL(referer);
const domain = url.hostname.split(".").slice(-2).join("."); // Get the root domain (e.g., mydomain.com)

console.log("Cookie Domain:" + domain);

// Set the cookie with the dynamic domain
res.cookie("testCookie", "cookieValue", {
httpOnly: true,
secure: true, // Set to true for HTTPS
sameSite: "Strict", // Required for cross-site cookies
domain: `.${domain}`, // Set the domain dynamically
});
res.send(`HttpOnly cookie has been set for domain: .${domain}`);
} else {
res.status(400).send("Referer header not found");
}
});

// Add error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send("Something broke!");
});

// Create HTTPS server
https.createServer(options, app).listen(port, () => {
console.log(`API server running at https://test.api.com:${port}`);
});

Index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cross-Domain Cookie Test</title>
</head>
<body>
<h1>Cross-Domain Cookie Test</h1>
<button id="invoke-button">Invoke API</button>
<button id="check-cookie-button">Check Cookie</button>
<p id="result"></p>

<script>
document.getElementById('invoke-button').addEventListener('click', async () => {
try {
const response = await fetch('/api/', {
method: 'GET',
credentials: 'include' // Important to include credentials
});

const data = await response.text();
console.log('Fetch Cookie response:', data); // Add logging
document.getElementById('result').innerText = `Response: ${data}`;
} catch (error) {
console.error('Fetch Cookie error:', error);
document.getElementById('result').innerText = `Error: ${error.message}`;
}
});

document.getElementById('check-cookie-button').addEventListener('click', async () => {
try {
const response = await fetch('/check', {
method: 'GET',
credentials: 'include' // Important to include credentials
});

const data = await response.text();
console.log('Check Cookie response:', data); // Add logging
document.getElementById('result').innerText = `Response: ${data}`;
} catch (error) {
console.error('Check Cookie error:', error);
document.getElementById('result').innerText = `Error: ${error.message}`;
}
});
</script>
</body>
</html>

Setup Instructions:

Install dependencies and start the servers:

cd Cross-Domain-Cookies
npm install
node server.js
node api-server.js

Access the application:

Navigate to https://test.mydomain.com:8080. The cookie from the API will be set on .mydomain.com. For your local testing g put host entry in your host file for test.mydomain.com and test.api.com pointing 127.0.0.1.

For local testing, add the following entries to your hosts file to point test.mydomain.com and test.api.com to 127.0.0.1:

127.0.0.1 test.mydomain.com
127.0.0.1 test.api.com

Conclusion:

By following these steps, you can effectively manage cookies across different domains, ensuring a consistent state across your web applications. This approach is useful when integrating with various applications and ensuring that users always see the same domain and have access to the same cookies. For sample code, refer to Cross-Domain Cookies.