Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
627 changes: 626 additions & 1 deletion CLAUDE.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -845,7 +845,7 @@ describe('DotContentTypesEditComponent', () => {
contentTypeForm = de.query(By.css('dot-content-types-form'));
});

it('should udpate content type', () => {
it('should update content type', () => {
const responseContentType = Object.assign({}, fakeContentType, {
fields: [{ hello: 'world' }]
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.dotcms.filters.interceptor.saml.SamlWebUtils;
import com.dotcms.rest.WebResource;
import com.dotcms.rest.annotation.NoCache;
import com.dotcms.rest.annotation.SwaggerCompliant;
import com.dotcms.saml.Attributes;
import com.dotcms.saml.DotSamlConstants;
import com.dotcms.saml.DotSamlException;
Expand All @@ -21,6 +22,12 @@
import com.dotmarketing.util.WebKeys;
import com.google.common.annotations.VisibleForTesting;
import com.liferay.portal.model.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.glassfish.jersey.server.JSONP;

Expand Down Expand Up @@ -49,6 +56,7 @@
* - metadata renders the XML metadata.
* @author jsanca
*/
@SwaggerCompliant(value = "Core authentication and user management APIs", batch = 1)
@Tag(name = "SAML Authentication")
@Path("/v1/dotsaml")
public class DotSamlResource implements Serializable {
Expand Down Expand Up @@ -98,12 +106,29 @@ protected DotSamlResource(final SamlConfigurationService samlConfigura
* @param httpServletResponse {@link HttpServletResponse}
* @return Response
*/
@Operation(
summary = "Initiate SAML login",
description = "Initiates a SAML authentication request by redirecting the user to the Identity Provider (IDP) login screen. Requires IDP metadata to determine the SSO login endpoint."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200",
description = "SAML authentication request initiated successfully (no body)"),
@ApiResponse(responseCode = "400",
description = "Bad request - invalid IDP configuration ID",
content = @Content(mediaType = "application/json")),
@ApiResponse(responseCode = "404",
description = "IDP configuration not found or not enabled",
content = @Content(mediaType = "application/json")),
@ApiResponse(responseCode = "500",
description = "Internal server error during SAML authentication initiation",
content = @Content(mediaType = "application/json"))
})
@GET
@Path( "/login/{idpConfigId}" )
@JSONP
@NoCache
@Produces( { MediaType.APPLICATION_JSON, "application/javascript" } )
public Response doLogin(@PathParam( "idpConfigId" ) final String idpConfigId,
@Produces( { MediaType.APPLICATION_JSON } )
public Response doLogin(@Parameter(description = "Identity Provider configuration ID (typically host ID)", required = true) @PathParam( "idpConfigId" ) final String idpConfigId,
@Context final HttpServletRequest httpServletRequest,
@Context final HttpServletResponse httpServletResponse) {

Expand Down Expand Up @@ -148,12 +173,36 @@ public Response doLogin(@PathParam( "idpConfigId" ) final String idpConfigId,
* @param httpServletResponse {@link HttpServletResponse}
* @throws IOException
*/
@Operation(
summary = "Process SAML login callback",
description = "Handles the callback from the Identity Provider after successful authentication. Extracts user information from the SAML assertion and creates/logs in the user to dotCMS.",
requestBody = @RequestBody(description = "SAML assertion data from Identity Provider", required = true,
content = {@Content(mediaType = "application/xml"),
@Content(mediaType = "application/x-www-form-urlencoded")})
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200",
description = "SAML login processed successfully - user logged in",
content = @Content(mediaType = "text/html")),
@ApiResponse(responseCode = "400",
description = "Bad request - invalid SAML assertion or missing data",
content = @Content(mediaType = "text/html")),
@ApiResponse(responseCode = "401",
description = "Unauthorized - SAML assertion validation failed",
content = @Content(mediaType = "text/html")),
@ApiResponse(responseCode = "404",
description = "IDP configuration not found or not enabled",
content = @Content(mediaType = "text/html")),
@ApiResponse(responseCode = "500",
description = "Internal server error during SAML login processing",
content = @Content(mediaType = "text/html"))
})
@POST
@Path("/login/{idpConfigId}")
@Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_FORM_URLENCODED})
@Produces( { MediaType.APPLICATION_XML, "text/html" } )
@NoCache
public void processLogin(@PathParam("idpConfigId") final String idpConfigId,
public void processLogin(@Parameter(description = "Identity Provider configuration ID (typically host ID)", required = true) @PathParam("idpConfigId") final String idpConfigId,
@Context final HttpServletRequest httpServletRequest,
@Context final HttpServletResponse httpServletResponse) throws IOException {

Expand Down Expand Up @@ -272,12 +321,33 @@ public void processLogin(@PathParam("idpConfigId") final String idpConfigId,
* @param httpServletResponse {@link HttpServletResponse}
* @throws IOException
*/
@Operation(
summary = "Get SAML metadata",
description = "Renders the XML metadata for the SAML Service Provider configuration. This endpoint is only accessible by administrators and provides the metadata required for IDP configuration."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200",
description = "SAML metadata rendered successfully",
content = @Content(mediaType = "application/xml")),
@ApiResponse(responseCode = "401",
description = "Unauthorized - admin access required",
content = @Content(mediaType = "application/xml")),
@ApiResponse(responseCode = "403",
description = "Forbidden - user is not an administrator",
content = @Content(mediaType = "application/xml")),
@ApiResponse(responseCode = "404",
description = "IDP configuration not found or not enabled",
content = @Content(mediaType = "application/xml")),
@ApiResponse(responseCode = "500",
description = "Internal server error rendering metadata",
content = @Content(mediaType = "application/xml"))
})
@GET
@Path( "/metadata/{idpConfigId}" )
@JSONP
@NoCache
@Produces( { MediaType.APPLICATION_XML, "application/xml" } )
public void metadata( @PathParam( "idpConfigId" ) final String idpConfigId,
public void metadata( @Parameter(description = "Identity Provider configuration ID (typically host ID)", required = true) @PathParam( "idpConfigId" ) final String idpConfigId,
@Context final HttpServletRequest httpServletRequest,
@Context final HttpServletResponse httpServletResponse ) throws IOException {

Expand Down Expand Up @@ -313,12 +383,27 @@ public void metadata( @PathParam( "idpConfigId" ) final String idpConfigId,
throw new DoesNotExistException(message);
}

@Operation(
summary = "Process SAML logout (POST)",
description = "Processes a SAML logout request via POST method. Handles logout callbacks from the Identity Provider and redirects to the configured logout endpoint."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200",
description = "SAML logout processed successfully",
content = @Content(mediaType = "text/html")),
@ApiResponse(responseCode = "404",
description = "IDP configuration not found or not enabled",
content = @Content(mediaType = "text/html")),
@ApiResponse(responseCode = "500",
description = "Internal server error during logout processing",
content = @Content(mediaType = "text/html"))
})
@POST
@Path("/logout/{idpConfigId}")
@NoCache
@Produces({MediaType.TEXT_HTML, MediaType.APPLICATION_XHTML_XML})
// Login configuration by id
public void logoutPost(@PathParam("idpConfigId") final String idpConfigId,
public void logoutPost(@Parameter(description = "Identity Provider configuration ID (typically host ID)", required = true) @PathParam("idpConfigId") final String idpConfigId,
@Context final HttpServletRequest httpServletRequest,
@Context final HttpServletResponse httpServletResponse) throws IOException, URISyntaxException {

Expand Down Expand Up @@ -350,12 +435,27 @@ public void logoutPost(@PathParam("idpConfigId") final String idpConfigId,
throw new DoesNotExistException(message);
}

@Operation(
summary = "Process SAML logout (GET)",
description = "Processes a SAML logout request via GET method. Initiates logout flow and redirects to the configured logout endpoint or builds a logout URL based on the request."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200",
description = "SAML logout processed successfully",
content = @Content(mediaType = "text/html")),
@ApiResponse(responseCode = "404",
description = "IDP configuration not found or not enabled",
content = @Content(mediaType = "text/html")),
@ApiResponse(responseCode = "500",
description = "Internal server error during logout processing",
content = @Content(mediaType = "text/html"))
})
@GET
@Path("/logout/{idpConfigId}")
@NoCache
@Produces({MediaType.TEXT_HTML, MediaType.APPLICATION_XHTML_XML})
// Login configuration by id
public void logoutGet(@PathParam("idpConfigId") final String idpConfigId,
public void logoutGet(@Parameter(description = "Identity Provider configuration ID (typically host ID)", required = true) @PathParam("idpConfigId") final String idpConfigId,
@Context final HttpServletRequest httpServletRequest,
@Context final HttpServletResponse httpServletResponse) throws IOException, URISyntaxException {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,28 @@
import com.dotcms.rest.ResponseEntityView;
import com.dotcms.rest.WebResource;
import com.dotcms.rest.annotation.NoCache;
import com.dotcms.rest.annotation.SwaggerCompliant;
import com.google.common.collect.ImmutableList;
import com.liferay.portal.model.User;

import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;

import static com.dotcms.util.CollectionsUtils.toImmutableList;

/**
* This end-point provides access to information associated to dotCMS FieldType.
*/
@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2)
@Path("/v1/fieldTypes")
@Tag(name = "Content Type Field", description = "Content type field definitions and configuration")
@Tag(name = "Content Type Field")
public class FieldTypeResource {

private final WebResource webResource;
Expand All @@ -43,10 +50,23 @@ public FieldTypeResource(final WebResource webresource, FieldTypeAPI fieldTypeAP
this.fieldTypeAPI = fieldTypeAPI;
}

@Operation(
summary = "Get field types",
description = "Retrieves all available field types in dotCMS for content type configuration"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200",
description = "Field types retrieved successfully",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ResponseEntityFieldTypeListView.class))),
@ApiResponse(responseCode = "401",
description = "Unauthorized - authentication required",
content = @Content(mediaType = "application/json"))
})
@GET
@JSONP
@NoCache
@Produces({ MediaType.APPLICATION_JSON, "application/javascript" })
@Produces({ MediaType.APPLICATION_JSON })
public Response getFieldTypes(@Context final HttpServletRequest req) {

final InitDataObject initData = this.webResource.init(null, true, req, true, null);
Expand All @@ -56,6 +76,6 @@ public Response getFieldTypes(@Context final HttpServletRequest req) {
.map(FieldType::toMap)
.collect(toImmutableList());

return Response.ok( new ResponseEntityView<List<Map<String, Object>>>( fieldTypesMap ) ).build();
return Response.ok( new ResponseEntityFieldTypeListView( fieldTypesMap ) ).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.dotcms.contenttype.model.field;

import com.dotcms.rest.ResponseEntityView;
import java.util.List;
import java.util.Map;

/**
* Entity View for field type collection responses.
* Contains list of field type configurations.
*
* @author Steve Bolton
*/
public class ResponseEntityFieldTypeListView extends ResponseEntityView<List<Map<String, Object>>> {
public ResponseEntityFieldTypeListView(final List<Map<String, Object>> entity) {
super(entity);
}
}
59 changes: 59 additions & 0 deletions dotCMS/src/main/java/com/dotcms/rest/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# dotCMS REST API Guide for Claude AI

## 📋 Essential Reference

**👉 REQUIRED: Read [README.md](./README.md) for comprehensive REST API development guidelines**

This file contains **AI-specific instructions only**. You MUST follow all patterns, standards, and checklists in README.md.

---

## 🤖 AI-Specific @Schema Rules

### Quick Decision Matrix

| Method Returns | Use @Schema |
|---------------|-------------|
| `ResponseEntity<UserView>` | `implementation = ResponseEntityUserView.class` |
| `ResponseEntity<List<T>>` | `implementation = ResponseEntityListView.class` |
| `List<MyEntity>` | `implementation = MyEntityListView.class` ¹ |
| `Map<String, MyType>` | `implementation = MapStringMyTypeView.class` ¹ |
| `Map<String, Object>` | `type = "object", description = "..."` |
| `JSONObject` / AI APIs | `type = "object", description = "...", example = "..."` |

¹ *Create if doesn't exist: `class MyEntityListView extends ArrayList<MyEntity>`*

### 🚨 FORBIDDEN for AI

```java
❌ @Schema(implementation = Object.class) // Use type = "object" instead
❌ @Schema(implementation = Map.class) // Use specific view class
❌ @Schema(implementation = HashMap.class) // Use specific view class
❌ Missing descriptions on type = "object" // Always add description
```

### ✅ AI Description Requirements

For `@Schema(type = "object")`:
- **Analyze the method's business logic** to understand what data is returned
- **Be specific**: "AI search results with contentlets and fragments" not "JSON object"
- **Add examples** for complex AI/external API responses
- **Pattern**: "[Service] [operation] response containing [specific data] and [metadata]"

---

## 🎯 AI Workflow

1. **READ** [README.md](./README.md) patterns and requirements
2. **ANALYZE** method return statement - wrapped vs unwrapped?
3. **CHECK** if specific view class exists for the return type
4. **APPLY** correct @Schema from decision matrix above
5. **VERIFY** against [README.md checklist](./README.md#summary-checklist)

### Critical AI Verification
- ✅ Schema matches actual return type (not generic Object.class)
- ✅ All `type = "object"` have meaningful descriptions
- ✅ Descriptions explain actual data structure
- ✅ No deprecated Object.class/Map.class patterns remain

**Goal**: Create precise API documentation that helps developers understand exactly what each endpoint returns.
Loading
Loading