Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added

- Require a License Manager resource (placeholder: `vbase-read-write`) in addition to admin role to access Download Requests, Process Unsent and Delete Request APIs.

## [1.14.2] - 2026-05-05

### Fixed
Expand Down
5 changes: 5 additions & 0 deletions dotnet/Data/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ public class Constants
public const string HTTP_FORWARDED_HEADER = "HTTP_X_FORWARDED_FOR";
public const string API_VERSION_HEADER = "'x-api-version";

// TODO: Replace with the actual LM product code for this app once the new resource is created in License Manager.
public const string REQUIRED_LM_PRODUCT_CODE = "0";
// TODO: Replace "vbase-read-write" with the actual LM resource code once the new resource is created in License Manager.
public const string REQUIRED_LM_RESOURCE_CODE = "vbase-read-write";

public const string BUCKET = "availability-notify";
public const string LOCK = "availability-notify-lock";
public const string UNSENT_CHECK = "check-unsent";
Expand Down
62 changes: 57 additions & 5 deletions dotnet/Services/VtexAPIService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@
this._applicationName =
$"{this._environmentVariableProvider.ApplicationVendor}.{this._environmentVariableProvider.ApplicationName}";

this.VerifySchema();

Check warning on line 45 in dotnet/Services/VtexAPIService.cs

View workflow job for this annotation

GitHub Actions / QE / Lint .Net

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

Check warning on line 45 in dotnet/Services/VtexAPIService.cs

View workflow job for this annotation

GitHub Actions / QE / Lint .Net

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.
this.CreateDefaultTemplate();

Check warning on line 46 in dotnet/Services/VtexAPIService.cs

View workflow job for this annotation

GitHub Actions / QE / Lint .Net

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

Check warning on line 46 in dotnet/Services/VtexAPIService.cs

View workflow job for this annotation

GitHub Actions / QE / Lint .Net

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.
}

public async Task<InventoryBySku> ListInventoryBySku(string sku, RequestContext requestContext)
Expand Down Expand Up @@ -1211,18 +1211,70 @@
return HttpStatusCode.BadRequest;
}

bool hasPermission = validatedUser != null &&
"Success".Equals(validatedUser.AuthStatus, StringComparison.OrdinalIgnoreCase) &&
"admin".Equals(validatedUser.Audience, StringComparison.OrdinalIgnoreCase);
if (validatedUser == null || string.IsNullOrEmpty(validatedUser.User))
{
_context.Vtex.Logger.Warn("IsValidAuthUser", null, "Could not resolve user from token");

return HttpStatusCode.Forbidden;
}

if (!hasPermission)
string account = this._httpContextAccessor.HttpContext.Request.Headers[Constants.VTEX_ACCOUNT_HEADER_NAME].ToString();
string authToken = this._httpContextAccessor.HttpContext.Request.Headers[Constants.HEADER_VTEX_CREDENTIAL];

bool hasResource = await HasLicenseManagerResourceAsync(account, validatedUser.User, authToken, Constants.REQUIRED_LM_PRODUCT_CODE, Constants.REQUIRED_LM_RESOURCE_CODE);

if (!hasResource)
{
_context.Vtex.Logger.Warn("IsValidAuthUser", null, "User Does Not Have Permission");
_context.Vtex.Logger.Warn("IsValidAuthUser", null, $"User '{validatedUser.User}' does not have required LM resource '{Constants.REQUIRED_LM_RESOURCE_CODE}'");

return HttpStatusCode.Forbidden;
}

return HttpStatusCode.OK;
}

private async Task<bool> HasLicenseManagerResourceAsync(string account, string userEmail, string credentialHeader, string productCode, string resourceCode)
{
if (string.IsNullOrWhiteSpace(account) || string.IsNullOrWhiteSpace(userEmail))
{
return false;
}

var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri($"http://{account}.vtexcommercestable.com.br/api/license-manager/pvt/accounts/{account}/products/{productCode}/logins/{Uri.EscapeDataString(userEmail)}/resources/{resourceCode}/granted")
};

request.Headers.Add(Constants.USE_HTTPS_HEADER_NAME, "true");

if (credentialHeader != null)
{
request.Headers.Add(Constants.AUTHORIZATION_HEADER_NAME, credentialHeader);
request.Headers.Add(Constants.VTEX_ID_HEADER_NAME, credentialHeader);
request.Headers.Add(Constants.PROXY_AUTHORIZATION_HEADER_NAME, credentialHeader);
}

try
{
var client = _clientFactory.CreateClient();
var response = await client.SendAsync(request);

if (!response.IsSuccessStatusCode)
{
return false;
}

string body = await response.Content.ReadAsStringAsync();

return bool.TryParse(body.Trim(), out bool granted) && granted;
}
catch (Exception ex)
{
_context.Vtex.Logger.Error("HasLicenseManagerResourceAsync", null, $"Error checking LM resource for user '{userEmail}'", ex);

return false;
}
}
}
}
3 changes: 2 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@
"admin/settings.process-unsent-helptext": "Process all unsent requests to notify and download an XLS file of the results.",
"admin/settings.verify-availability-helptext": "Runs a shipping simulation to verify that the item can be shipped to the shopper before sending a notification.",
"admin/settings.marketplace-to-notify-helptext": "Allows a seller account to specify a comma separated list of marketplace account names to notify of inventory updates.",
"admin/settings.label": "Settings"
"admin/settings.label": "Settings",
"admin/settings.noPermission.error": "You do not have permission to perform this action."
}
3 changes: 2 additions & 1 deletion messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@
"admin/settings.process-unsent-helptext": "Process all unsent requests to notify and download an XLS file of the results.",
"admin/settings.verify-availability-helptext": "Runs a shipping simulation to verify that the item can be shipped to the shopper before sending a notificaiton.",
"admin/settings.marketplace-to-notify-helptext": "Allows a seller account to specify a comma separated list of marketplace account names to notify of inventory updates.",
"admin/settings.label": "Settings"
"admin/settings.label": "Settings",
"admin/settings.noPermission.error": "Você não tem permissão para realizar esta ação."
}
32 changes: 23 additions & 9 deletions react/NotifyAdmin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import ProcessUnsentRequests from './graphql/processUnsentRequests.gql'

interface Props {
intl: any

Check warning on line 27 in react/NotifyAdmin.tsx

View workflow job for this annotation

GitHub Actions / QE / Lint Node.js

Unexpected any. Specify a different type
}

const messages = defineMessages({
Expand Down Expand Up @@ -60,6 +60,10 @@
id: 'admin/settings.saveSettings.button',
defaultMessage: 'Save',
},
noPermissionError: {
id: 'admin/settings.noPermission.error',
defaultMessage: 'You do not have permission to perform this action.',
},
settingsLabel: {
id: 'admin/settings.label',
defaultMessage: 'Settings',
Expand All @@ -85,8 +89,8 @@
},
})

const NotifyAdmin: FC<any> = ({ intl }: Props) => {

Check warning on line 92 in react/NotifyAdmin.tsx

View workflow job for this annotation

GitHub Actions / QE / Lint Node.js

Unexpected any. Specify a different type
const [state, setState] = useState<any>({

Check warning on line 93 in react/NotifyAdmin.tsx

View workflow job for this annotation

GitHub Actions / QE / Lint Node.js

Unexpected any. Specify a different type
loading: false,
processing: false,
})
Expand All @@ -107,7 +111,7 @@
marketplaceToNotify: '',
})

const handleSaveSettings = async (showToast: any) => {

Check warning on line 114 in react/NotifyAdmin.tsx

View workflow job for this annotation

GitHub Actions / QE / Lint Node.js

Unexpected any. Specify a different type
setSettingsLoading(true)

try {
Expand Down Expand Up @@ -136,14 +140,14 @@
useEffect(() => {
if (!data?.appSettings?.message) return

const parsedSettings: any = JSON.parse(data.appSettings.message)

Check warning on line 143 in react/NotifyAdmin.tsx

View workflow job for this annotation

GitHub Actions / QE / Lint Node.js

Unexpected any. Specify a different type

setSettingsState(parsedSettings)
}, [data])

const { loading, processing } = state

const downloadRequests = (allRequests: any) => {

Check warning on line 150 in react/NotifyAdmin.tsx

View workflow job for this annotation

GitHub Actions / QE / Lint Node.js

Unexpected any. Specify a different type
const header = [
'Id',
'Name',
Expand All @@ -154,7 +158,7 @@
'Sent At',
]

const result: any = []

Check warning on line 161 in react/NotifyAdmin.tsx

View workflow job for this annotation

GitHub Actions / QE / Lint Node.js

Unexpected any. Specify a different type

for (const request of allRequests) {
const requestData = {
Expand All @@ -179,7 +183,7 @@
XLSX.writeFile(wb, exportFileName)
}

const processRequestsResults = (processedRequests: any) => {

Check warning on line 186 in react/NotifyAdmin.tsx

View workflow job for this annotation

GitHub Actions / QE / Lint Node.js

Unexpected any. Specify a different type
const header = ['Sku Id', 'Quantity Available', 'Email', 'Sent', 'Updated']

const result: any = []
Expand All @@ -205,20 +209,27 @@
XLSX.writeFile(wb, exportFileName)
}

const getAllRequests = async () => {
const getAllRequests = async (showToast: any) => {
setState({ ...state, loading: true })

const result: any = await fetch(
`/_v/availability-notify/list-requests`
).then(response => response.json())
const response = await fetch(`/_v/availability-notify/list-requests`)

const requestArr = result
if (response.status === 401 || response.status === 403) {
showToast({
message: intl.formatMessage(messages.noPermissionError),
duration: 5000,
})
setState({ ...state, loading: false })
return
}

const requestArr = await response.json()

downloadRequests(requestArr)
setState({ ...state, loading: false })
}

const processUnsentRequests = async () => {
const processUnsentRequests = async (showToast: any) => {
setState({ ...state, processing: true })

try {
Expand All @@ -228,7 +239,10 @@

processRequestsResults(requestArr)
} catch (error) {
throw new Error(`processUnsentRequests-error: ${error.message}`)
showToast({
message: intl.formatMessage(messages.noPermissionError),
duration: 5000,
})
}

setState({ ...state, processing: false })
Expand Down Expand Up @@ -307,7 +321,7 @@
icon={download}
isLoading={loading}
onClick={() => {
getAllRequests()
getAllRequests(showToast)
}}
>
{intl.formatMessage(messages.download)}
Expand All @@ -324,7 +338,7 @@
icon={download}
isLoading={processing}
onClick={() => {
processUnsentRequests()
processUnsentRequests(showToast)
}}
>
{intl.formatMessage(messages.processUnsent)}
Expand Down
Loading