Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 1 addition & 2 deletions apps/customer-portal/backend/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,6 @@ modules = [
{org = "wso2", packageName = "customer_portal", moduleName = "customer_portal"},
{org = "wso2", packageName = "customer_portal", moduleName = "customer_portal.authorization"},
{org = "wso2", packageName = "customer_portal", moduleName = "customer_portal.entity"},
{org = "wso2", packageName = "customer_portal", moduleName = "customer_portal.scim"},
{org = "wso2", packageName = "customer_portal", moduleName = "customer_portal.types"}
{org = "wso2", packageName = "customer_portal", moduleName = "customer_portal.scim"}
]

2 changes: 1 addition & 1 deletion apps/customer-portal/backend/constants.bal
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const ERR_MSG_FETCHING_PROJECT_DETAILS = "Error retrieving project details!";
const ERR_MSG_PROJECT_ID_EMPTY = "Project ID cannot be empty!";
const ERR_MSG_CASE_ID_EMPTY = "Case ID cannot be empty!";
const ERR_LIMIT_OFFSET_INVALID = "Limit must be between 1 and 50. Offset must be a non-negative integer!";
const ERR_MSG_UNAUTHORIZED_ACCESS = "You're not authorized to access this service. Please sign in again." +
const ERR_MSG_UNAUTHORIZED_ACCESS = "You're not authorized to access this service. Please sign in again. " +
"If the issue continues, contact support.";

// Default Pagination Values
Expand Down
9 changes: 9 additions & 0 deletions apps/customer-portal/backend/modules/entity/entity.bal
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ public isolated function getCase(string idToken, string caseId) returns CaseResp
return csEntityClient->/cases/[caseId].get(generateHeaders(idToken));
}

# Add a new case.
Comment thread
Rashmika998 marked this conversation as resolved.
Resolved
#
# + idToken - ID token for authorization
# + payload - Case creation payload
# + return - Case creation response or error
public isolated function addCase(string idToken, CaseCreatePayload payload) returns CaseCreateResponse|error {
Comment thread
Rashmika998 marked this conversation as resolved.
Resolved
return csEntityClient->/cases.post(payload, generateHeaders(idToken));
}

# Search cases of a project.
#
# + idToken - ID token for authorization
Expand Down
53 changes: 51 additions & 2 deletions apps/customer-portal/backend/modules/entity/types.bal
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,53 @@ public type Subscription record {|
json...;
|};

# Payload for creating a case.
public type CaseCreatePayload record {|
# Project ID
@constraint:String {minLength: 1}
string projectId;
# Deployment ID
@constraint:String {minLength: 1}
string deploymentId;
# Product ID
@constraint:String {minLength: 1}
string productId;
# Case title
@constraint:String {minLength: 1, maxLength: 500}
string title;
# Case description
@constraint:String {minLength: 1, maxLength: 65000}
string description;
# Issue type ID
int issueTypeKey;
# Severity key
int severityKey;
|};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

# Response from creating a case.
public type CaseCreateResponse record {|
# Success message
string message;
# Created case details
CreatedCase case;
|};

# Created case details.
public type CreatedCase record {|
# System ID of the created case
string id;
# WSO2 internal ID of the case
string internalId;
# Case number
string number;
# User who created the case
string createdBy;
# Created date and time
string createdOn;
# Status
ChoiceListItem state;
|};

# Base case.
public type Case record {|
# Case ID
Expand All @@ -134,10 +181,12 @@ public type Case record {|
ReferenceTableItem? assignedEngineer;
# Associated project
ReferenceTableItem? project;
# Type of the case
ReferenceTableItem? 'type;
# Deployment information
ReferenceTableItem? deployment;
# Deployed product information
ReferenceTableItem? deployedProduct;
# issue type of the case
ChoiceListItem? issueType;
# Status information
ChoiceListItem? state;
# Severity information
Expand Down
51 changes: 51 additions & 0 deletions apps/customer-portal/backend/service.bal
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,57 @@ service http:InterceptableService / on new http:Listener(9090) {
return caseResponse;
}

# Add a new case.
Comment thread
Rashmika998 marked this conversation as resolved.
Resolved
#
# + payload - Case creation payload
# + return - Success message or error response
resource function post cases(http:RequestContext ctx, entity:CaseCreatePayload payload)
returns http:Created|http:BadRequest|http:Unauthorized|http:Forbidden|http:InternalServerError {

authorization:UserInfoPayload|error userInfo = ctx.getWithType(authorization:HEADER_USER_INFO);
if userInfo is error {
return <http:InternalServerError>{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

USER_INFO_HEADER_NOT_FOUND should not be 500. This is not a server error IMO it’s a client/auth issue. Should be;
<http:Unauthorized>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We (@shayanmalinda, @sacheeramesh, and I) already had a discussion in the past about whether this should return <http:Unauthorized>. I think, all our backends currently use a http:InternalServerError error for this case.

Should we revisit this? I also personally believe this should be a <http:Unauthorized> as well(including few errors in authorization.bal file).

Adding @yuk7hi and @kasunsiyambalapitiya as well

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, HEADER_USER_INFO is not a header passed from the consumer (e.g., the web app).
It is an internal attribute introduced in the request interceptor to pass logged-in user information (email, groups, idToken) to service.bal via the http:RequestContext.

If this attribute is missing, or if there is an issue deriving it in service.bal (for example, setting it with one name and reading it using a different name), it should be treated as an implementation issue and the correct response should be an InternalServerError.

Authorization is handled at the gateway level, not by the backend service.
If a request reaches the backend, it is already authorized. Therefore, the backend should not ideally return any Unauthorized responses.

@Rashmika998 Rashmika998 Feb 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @shayanmalinda. Agree that HEADER_USER_INFO is an internal context attribute and should be treated as an implementation issue (500) if missing.

One clarification here is that this behavior is specific to our setup. In Choreo, the gateway validates the Authorization: Bearer header and converts it into x-jwt-assertion before forwarding the request to the backend.

This conversion is platform-specific and not something all API gateways do by default.

Because of this, the backend currently depends on this contract with the gateway. If x-jwt-assertion is missing, it usually indicates an issue with the authentication flow at the gateway level (missing/invalid bearer token or routing/config issue), rather than something the consumer can directly fix.

We can add a comment in the interceptor to document this dependency on the gateway behavior, so it’s clear why this is treated as an internal error and mapped accordingly.

However I believe I need to fix the error returned when the custom header(x-user-id-token) is not found while checking it. Currently it returns 500.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's have a seperate revisit on this later. Will approve the PR due to the urgency.

body: {
message: ERR_MSG_USER_INFO_HEADER_NOT_FOUND

@shayanmalinda shayanmalinda Feb 10, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think returning ERR_MSG_USER_INFO_HEADER_NOT_FOUND is incorrect, since the user has no control over fixing this issue. As per the previous reply, this is an internal implementation problem, so it would be more appropriate to return a generic error such as Something went wrong while processing the request.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we have an offline discussion regarding this? Anyway the fix will be in a separate PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, +1

}
};
}

entity:CaseCreateResponse|error createdCaseResponse = entity:addCase(userInfo.idToken, payload);
if createdCaseResponse is error {
if getStatusCode(createdCaseResponse) == http:STATUS_UNAUTHORIZED {
log:printWarn(string `User: ${userInfo.userId} is not authorized to access the customer portal!`);
return <http:Unauthorized>{
Comment thread
shayanmalinda marked this conversation as resolved.
Resolved
body: {
message: ERR_MSG_UNAUTHORIZED_ACCESS
}
};
}

if getStatusCode(createdCaseResponse) == http:STATUS_FORBIDDEN {
log:printWarn(string `User: ${userInfo.userId} is forbidden to create a case for project: ${
payload.projectId}!`);
return <http:Forbidden>{
body: {
message: "You're not authorized to create a case for the selected project. " +
"Please check your access permissions or contact support."
}
};
}

string customError = "Failed to create a new case.";
log:printError(customError, createdCaseResponse);
return <http:InternalServerError>{
body: {
message: customError
}
};
}
return <http:Created>{
body: createdCaseResponse.case
};
}

# Search cases for a specific project with filters and pagination.
#
# + id - ID of the project
Expand Down
6 changes: 4 additions & 2 deletions apps/customer-portal/backend/types.bal
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,10 @@ public type Case record {|
ReferenceItem? assignedEngineer;
# Associated project
ReferenceItem? project;
# Type of the case
ReferenceItem? 'type;
# Deployed product information
ReferenceItem? deployedProduct;
# issueType of the case
ReferenceItem? issueType;
# Deployment
ReferenceItem? deployment;
# Severity of the case
Expand Down
6 changes: 4 additions & 2 deletions apps/customer-portal/backend/utils.bal
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ public isolated function searchCases(string idToken, string projectId, CaseSearc
entity:CaseSearchResponse casesResponse = check entity:searchCases(idToken, searchPayload);
Case[] cases = from entity:Case case in casesResponse.cases
let entity:ReferenceTableItem? project = case.project
let entity:ReferenceTableItem? 'type = case.'type
let entity:ReferenceTableItem? deployedProduct = case.deployedProduct
let entity:ChoiceListItem? issueType = case.issueType
let entity:ReferenceTableItem? deployment = case.deployment
let entity:ReferenceTableItem? assignedEngineer = case.assignedEngineer
let entity:ChoiceListItem? severity = case.severity
Expand All @@ -53,7 +54,8 @@ public isolated function searchCases(string idToken, string projectId, CaseSearc
createdOn: case.createdOn,
description: case.description,
project: project != () ? {id: project.id, label: project.name} : (),
'type: 'type != () ? {id: 'type.id, label: 'type.name} : (),
deployedProduct: deployedProduct != () ? {id: deployedProduct.id, label: deployedProduct.name} : (),
issueType: issueType != () ? {id: issueType.id.toString(), label: issueType.label} : (),
deployment: deployment != () ? {id: deployment.id, label: deployment.name} : (),
assignedEngineer: assignedEngineer != () ? {id: assignedEngineer.id, label: assignedEngineer.name} : (),
severity: severity != () ? {id: severity.id.toString(), label: severity.label} : (),
Expand Down