Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b28d4b0
chore: Include version in server responses
sharat87 Nov 19, 2024
0da70ce
chore: fix tests and have client also send version
sharat87 Nov 20, 2024
ae0cb0e
chore: merge release branch
sharat87 Nov 20, 2024
868140b
Make version check on server optional for now
sharat87 Nov 20, 2024
dd9aff7
fix header in fake responses
sharat87 Nov 20, 2024
206dca9
chore: merge release branch
sharat87 Nov 20, 2024
e6478ce
fix response structure again
sharat87 Nov 20, 2024
7046104
remove console log
sharat87 Nov 20, 2024
db8d9c2
chore: merge release branch
sharat87 Nov 20, 2024
c026e7e
Merge branch 'release' into chore/versioned-responses
sharat87 Nov 20, 2024
4d5e3e9
Merge branch 'release' into chore/versioned-responses
sharat87 Nov 22, 2024
e231045
Show version in header in response as well
sharat87 Nov 22, 2024
c2c5a35
chore: merge release branch
sharat87 Nov 22, 2024
259ce4e
fix unit tests
sharat87 Nov 22, 2024
70a3fd6
only request sends version
sharat87 Nov 23, 2024
cf6e760
Discard changes to app/client/cypress/e2e/Regression/ClientSide/Templ…
sharat87 Nov 23, 2024
a01546a
Discard changes to app/client/cypress/e2e/Regression/ClientSide/Templ…
sharat87 Nov 23, 2024
fd6f7b0
Discard changes to app/client/cypress/e2e/Regression/ServerSide/Gener…
sharat87 Nov 23, 2024
cf17e9e
Discard changes to app/client/cypress/e2e/Regression/ServerSide/Gener…
sharat87 Nov 23, 2024
9e389e4
Discard changes to app/client/cypress/e2e/Sanity/Datasources/ArangoDa…
sharat87 Nov 23, 2024
d3db3d2
Discard changes to app/client/cypress/e2e/Sanity/Datasources/MsSQLDat…
sharat87 Nov 23, 2024
fbd9bba
Discard changes to app/client/cypress/e2e/Sanity/Datasources/MySQLDat…
sharat87 Nov 23, 2024
fdf799b
Discard changes to app/client/cypress/e2e/Sanity/Datasources/Redshift…
sharat87 Nov 23, 2024
b16aeca
Discard changes to app/client/cypress/support/Objects/FeatureFlags.ts
sharat87 Nov 23, 2024
7f378b5
Discard changes to app/client/cypress/support/Pages/DataSources.ts
sharat87 Nov 23, 2024
6483646
Discard changes to app/client/cypress/support/dataSourceCommands.js
sharat87 Nov 23, 2024
fc634f1
Discard changes to app/client/cypress/support/index.d.ts
sharat87 Nov 23, 2024
f6b9269
Discard changes to app/client/src/api/__tests__/apiSucessResponseInte…
sharat87 Nov 23, 2024
690b2be
Include expected version in error message
sharat87 Nov 23, 2024
66a653d
Handle showing toast when needed
sharat87 Nov 23, 2024
1095757
remove todo
sharat87 Nov 23, 2024
35d9ba6
No version update state
sharat87 Nov 25, 2024
3311cd2
Fix imports and error handling
sharat87 Nov 26, 2024
260e039
Fix type error
sharat87 Nov 26, 2024
d8244c9
Merge branch 'release' into chore/versioned-responses
sharat87 Nov 26, 2024
c4321f7
refactor interceptor
jsartisan Nov 26, 2024
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
2 changes: 1 addition & 1 deletion app/client/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@
: LOG_LEVELS[1],
cloudHosting: CLOUD_HOSTING,
appVersion: {
id: parseConfig('{{env "APPSMITH_VERSION_ID"}}'),
id: parseConfig('{{env "APPSMITH_VERSION_ID"}}') || "UNKNOWN",
sha: parseConfig('{{env "APPSMITH_VERSION_SHA"}}'),
releaseDate: parseConfig('{{env "APPSMITH_VERSION_RELEASE_DATE"}}'),
},
Expand Down
13 changes: 13 additions & 0 deletions app/client/src/api/interceptors/request/addVersionHeader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { InternalAxiosRequestConfig } from "axios";

export const addVersionHeader = (
config: InternalAxiosRequestConfig,
options: { version: string },
) => {
const { version } = options;

config.headers = config.headers || {};
config.headers["X-Appsmith-Version"] = version;

return config;
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import getQueryParamsObject from "utils/getQueryParamsObject";
import { addRequestedByHeader } from "./addRequestedByHeader";
import { increaseGitApiTimeout } from "./increaseGitApiTimeout";
import { getCurrentGitBranch } from "selectors/gitSyncSelectors";
import { addVersionHeader as _addVersionHeader } from "./addVersionHeader";
import { getCurrentEnvironmentId } from "ee/selectors/environmentSelectors";
import { addGitBranchHeader as _addGitBranchHeader } from "./addGitBranchHeader";
import { addPerformanceMonitoringHeaders } from "./addPerformanceMonitoringHeaders";
Expand Down Expand Up @@ -49,10 +50,18 @@ const addAnonymousUserIdHeader = (config: InternalAxiosRequestConfig) => {
return _addAnonymousUserIdHeader(config, { anonymousId, segmentEnabled });
};

const addVersionHeader = (config: InternalAxiosRequestConfig) => {
const appsmithConfig = getAppsmithConfigs();
const version = appsmithConfig.appVersion.id;

return _addVersionHeader(config, { version });
};

export const apiRequestInterceptor = (config: InternalAxiosRequestConfig) => {
const interceptorPipeline = compose<InternalAxiosRequestConfig>(
blockAirgappedRoutes,
addRequestedByHeader,
addVersionHeader,
addGitBranchHeader,
increaseGitApiTimeout,
addEnvironmentHeader,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const apiFailureResponseInterceptor = async (
failureHandlers.handleServerError,
failureHandlers.handleUnauthorizedError,
failureHandlers.handleNotFoundError,
failureHandlers.handleBadRequestError,
];

for (const handler of handlers) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { AxiosError } from "axios";
import type { ApiResponse } from "../../../ApiResponses";
import { handleVersionMismatch } from "sagas/WebsocketSagas/versionUpdatePrompt";
import { getAppsmithConfigs } from "ee/configs";
import { SERVER_ERROR_CODES } from "ee/constants/ApiConstants";

export const handleBadRequestError = async (error: AxiosError<ApiResponse>) => {
const errorCode = error?.response?.data?.responseMeta.error?.code;

if (
error?.response?.status === 400 &&
SERVER_ERROR_CODES.VERSION_MISMATCH.includes("" + errorCode)
) {
const responseData = error?.response?.data;
const message = responseData?.responseMeta.error?.message;
const serverVersion = (responseData?.data as { serverVersion: string })
.serverVersion;

handleVersionMismatch(getAppsmithConfigs().appVersion.id, serverVersion);

return Promise.reject({
...error,
clientDefinedError: true,
statusCode: errorCode,
message,
});
}

return null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { handleCancelError } from "./handleCancelError";
export { handleOfflineError } from "./handleOfflineError";
export { handleTimeoutError } from "./handleTimeoutError";
export { handleNotFoundError } from "./handleNotFoundError";
export { handleBadRequestError } from "./handleBadRequestError";
export { handleUnauthorizedError } from "./handleUnauthorizedError";
export { handleExecuteActionError } from "./handleExecuteActionError";
export { handleMissingResponseMeta } from "./handleMissingResponseMeta";
1 change: 1 addition & 0 deletions app/client/src/ce/constants/ApiConstants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const SERVER_ERROR_CODES = {
"AE-APP-4013",
],
UNABLE_TO_FIND_PAGE: ["AE-APP-4027", "AE-USR-4004"],
VERSION_MISMATCH: ["AE-BAD-4002"],
};

export enum ERROR_CODES {
Expand Down
11 changes: 0 additions & 11 deletions app/client/src/sagas/WebsocketSagas/handleAppLevelSocketEvents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { put } from "redux-saga/effects";
import { APP_LEVEL_SOCKET_EVENTS } from "./socketEvents";

import { collabSetAppEditors } from "actions/appCollabActions";
import { getAppsmithConfigs } from "ee/configs";
import { handleVersionUpdate } from "./versionUpdatePrompt";

// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -13,15 +11,6 @@ export default function* handleAppLevelSocketEvents(event: any) {
case APP_LEVEL_SOCKET_EVENTS.LIST_ONLINE_APP_EDITORS: {
yield put(collabSetAppEditors(event.payload[0]));

return;
}
// notification on release version
case APP_LEVEL_SOCKET_EVENTS.RELEASE_VERSION_NOTIFICATION: {
const { appVersion } = getAppsmithConfigs();
const [serverVersion] = event.payload;

handleVersionUpdate(appVersion, serverVersion);

return;
}
}
Expand Down
110 changes: 18 additions & 92 deletions app/client/src/sagas/WebsocketSagas/versionUpdatePrompt.ts
Original file line number Diff line number Diff line change
@@ -1,111 +1,37 @@
// Check if user is updating the app when toast is shown
// Check how many times does the user see a toast before updating

import { toast } from "@appsmith/ads";
import {
createMessage,
INFO_VERSION_MISMATCH_FOUND_RELOAD_REQUEST,
} from "ee/constants/messages";
import type { AppVersionData } from "ee/configs/types";
import {
getVersionUpdateState,
removeVersionUpdateState,
setVersionUpdateState,
} from "utils/storage";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";

enum UpdateStateEvent {
PROMPT_SHOWN = "PROMPT_SHOWN",
UPDATE_REQUESTED = "UPDATE_REQUESTED",
function handleUpdateRequested(fromVersion: string, toVersion: string) {
AnalyticsUtil.logEvent("VERSION_UPDATE_REQUESTED", {
fromVersion,
toVersion,
});
// Reload to fetch the latest app version
location.reload();
}

export interface VersionUpdateState {
currentVersion: string;
upgradeVersion: string;
timesShown: number;
event: UpdateStateEvent;
}
export async function handleVersionMismatch(
currentVersion: string,
serverVersion: string,
) {
// If no version is set, ignore
if (!currentVersion) return;

let timesShown = 0;
AnalyticsUtil.logEvent("VERSION_UPDATE_SHOWN", {
fromVersion: currentVersion,
toVersion: serverVersion,
});

function showPrompt(newUpdateState: VersionUpdateState) {
toast.show(createMessage(INFO_VERSION_MISMATCH_FOUND_RELOAD_REQUEST), {
kind: "info",
autoClose: false,
action: {
text: "refresh",
effect: () => handleUpdateRequested(newUpdateState),
effect: () => handleUpdateRequested(currentVersion, serverVersion),
},
});
}

function handleUpdateRequested(newUpdateState: VersionUpdateState) {
// store version update with timesShown counter
setVersionUpdateState({
...newUpdateState,
event: UpdateStateEvent.UPDATE_REQUESTED,
}).then(() => {
AnalyticsUtil.logEvent("VERSION_UPDATE_REQUESTED", {
fromVersion: newUpdateState.currentVersion,
toVersion: newUpdateState.upgradeVersion,
timesShown,
});
// Reload to fetch the latest app version
location.reload();
});
}

export async function handleVersionUpdate(
currentVersionData: AppVersionData,
serverVersion: string,
) {
const { edition, id: currentVersion } = currentVersionData;

// If no version is set, ignore
if (!currentVersion) return;

const versionState: VersionUpdateState | null = await getVersionUpdateState();

if (currentVersion === serverVersion) {
if (versionState) {
AnalyticsUtil.logEvent("VERSION_UPDATE_SUCCESS", {
fromVersion: versionState.currentVersion,
toVersion: versionState.upgradeVersion,
edition,
});
await removeVersionUpdateState();
}
}

if (currentVersion !== serverVersion) {
if (versionState) {
timesShown = versionState.timesShown;

if (
currentVersion === versionState.currentVersion &&
versionState.event === UpdateStateEvent.UPDATE_REQUESTED
) {
AnalyticsUtil.logEvent("VERSION_UPDATED_FAILED", {
fromVersion: versionState.currentVersion,
toVersion: versionState.upgradeVersion,
edition,
});
}
}

const newUpdateState: VersionUpdateState = {
currentVersion,
upgradeVersion: serverVersion,
// Increment the timesShown counter
timesShown: timesShown + 1,
event: UpdateStateEvent.PROMPT_SHOWN,
};

AnalyticsUtil.logEvent("VERSION_UPDATE_SHOWN", {
fromVersion: currentVersion,
toVersion: serverVersion,
timesShown,
});
showPrompt(newUpdateState);
}
}
20 changes: 0 additions & 20 deletions app/client/src/utils/storage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import log from "loglevel";
import moment from "moment";
import localforage from "localforage";
import type { VersionUpdateState } from "../sagas/WebsocketSagas/versionUpdatePrompt";
import { isNumber } from "lodash";
import { EditorModes } from "components/editorComponents/CodeEditor/EditorConfig";
import type { EditorViewMode } from "ee/entities/IDE/constants";
Expand Down Expand Up @@ -761,25 +760,6 @@ export const isUserSignedUpFlagSet = async (email: string) => {
}
};

export const setVersionUpdateState = async (state: VersionUpdateState) => {
try {
await store.setItem(STORAGE_KEYS.VERSION_UPDATE_STATE, state);
} catch (e) {
log.error("An error occurred while storing version update state", e);
}
};

export const getVersionUpdateState =
async (): Promise<VersionUpdateState | null> => {
return await store.getItem<VersionUpdateState | null>(
STORAGE_KEYS.VERSION_UPDATE_STATE,
);
};

export const removeVersionUpdateState = async () => {
return store.removeItem(STORAGE_KEYS.VERSION_UPDATE_STATE);
};

export const getAppKbState = async (appId: string) => {
try {
const aiKBApplicationMap: Record<
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.appsmith.server.configurations;

import com.appsmith.external.exceptions.ErrorDTO;
import com.appsmith.server.authentication.handlers.AccessDeniedHandler;
import com.appsmith.server.authentication.handlers.CustomServerOAuth2AuthorizationRequestResolver;
import com.appsmith.server.authentication.handlers.LogoutSuccessHandler;
Expand All @@ -8,6 +9,7 @@
import com.appsmith.server.constants.Url;
import com.appsmith.server.domains.User;
import com.appsmith.server.dtos.ResponseDTO;
import com.appsmith.server.exceptions.AppsmithErrorCode;
import com.appsmith.server.filters.CSRFFilter;
import com.appsmith.server.filters.ConditionalFilter;
import com.appsmith.server.filters.LoginRateLimitFilter;
Expand All @@ -24,6 +26,7 @@
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.InvalidMediaTypeException;
Expand Down Expand Up @@ -114,6 +117,9 @@ public class SecurityConfig {
@Autowired
private CustomOauth2ClientRepositoryManager oauth2ClientManager;

@Autowired
private ProjectProperties projectProperties;

@Value("${appsmith.internal.password}")
private String INTERNAL_PASSWORD;

Expand Down Expand Up @@ -284,10 +290,12 @@ private User createAnonymousUser() {
}

private Mono<Void> sanityCheckFilter(ServerWebExchange exchange, WebFilterChain chain) {
final HttpHeaders headers = exchange.getRequest().getHeaders();

// 1. Check if the content-type is valid at all. Mostly just checks if it contains a `/`.
MediaType contentType;
try {
contentType = exchange.getRequest().getHeaders().getContentType();
contentType = headers.getContentType();
} catch (InvalidMediaTypeException e) {
return writeErrorResponse(exchange, chain, e.getMessage());
}
Expand All @@ -300,6 +308,15 @@ private Mono<Void> sanityCheckFilter(ServerWebExchange exchange, WebFilterChain
return writeErrorResponse(exchange, chain, "Unsupported Content-Type");
}

// 3. Check Appsmith version, if present. Not making this a mandatory check for now, but reconsider later.
final String versionHeaderValue = headers.getFirst("X-Appsmith-Version");
final String serverVersion = projectProperties.getVersion();
if (versionHeaderValue != null && !serverVersion.equals(versionHeaderValue)) {
final ErrorDTO error = new ErrorDTO(
AppsmithErrorCode.VERSION_MISMATCH.getCode(), AppsmithErrorCode.VERSION_MISMATCH.getDescription());
return writeErrorResponse(exchange, chain, error, new VersionMismatchData(serverVersion));
}

return chain.filter(exchange);
}

Expand All @@ -315,4 +332,24 @@ private Mono<Void> writeErrorResponse(ServerWebExchange exchange, WebFilterChain
return chain.filter(exchange);
}
}

private <T> Mono<Void> writeErrorResponse(
ServerWebExchange exchange, WebFilterChain chain, ErrorDTO error, T data) {
final ServerHttpResponse response = exchange.getResponse();
final HttpStatus status = HttpStatus.BAD_REQUEST;
response.setStatusCode(status);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);

final ResponseDTO<T> responseBody = new ResponseDTO<>(status.value(), error);
responseBody.setData(data);

try {
return response.writeWith(
Mono.just(response.bufferFactory().wrap(objectMapper.writeValueAsBytes(responseBody))));
} catch (JsonProcessingException ex) {
return chain.filter(exchange);
}
}

record VersionMismatchData(String serverVersion) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public enum AppsmithErrorCode {
NAME_CLASH_NOT_ALLOWED_IN_REFACTOR("AE-AST-4009", "Name clash not allowed in refactor"),
GENERIC_BAD_REQUEST("AE-BAD-4000", "Generic bad request"),
MALFORMED_REQUEST("AE-BAD-4001", "Malformed request body"),
VERSION_MISMATCH("AE-BAD-4002", "Appsmith version mismatch"),
GOOGLE_RECAPTCHA_FAILED("AE-CAP-4035", "Google recaptcha failed"),
GOOGLE_RECAPTCHA_INVITE_FLOW_FAILED("AE-CAP-4100", "Google recaptcha failed"),
INVALID_CRUD_PAGE_REQUEST("AE-CRD-4039", "Invalid crud page request"),
Expand Down