MINOR: App changes for collate support (#21068)

* feat: prep for collate support

- Added support for delayed background jobs. This is to delete the support user after a while.
- added ScheduleType.OnlyManual for applications to support only manual triggers
- Added a four hour token expiration
- Allow a null trigger type in job data (not sure what it's for)

* added test for delayed job

* added type checking for enumCleanupArgs in navbar

* - added a support toten type
- added job handler for delete token

* fixed whitelist links

* patch OnlyManual apps

* Update openmetadata-spec/src/main/resources/json/schema/entity/applications/app.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* added indexes for runAt

* use NO_JOB_SLEEP_SECONDS in background job test

* Fixed NavBar test

* removed backticls

* fix indexes

* change for support app

* fix tests

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Karan Hotchandani <33024356+karanh37@users.noreply.github.com>
Co-authored-by: karanh37 <karanh37@gmail.com>
Co-authored-by: Pere Miquel Brull <peremiquelbrull@gmail.com>
This commit is contained in:
Imri Paran 2025-05-26 12:02:14 +02:00 committed by GitHub
parent 6ea630d7ef
commit baee931b85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 356 additions and 93 deletions

View File

@ -0,0 +1,4 @@
ALTER TABLE background_jobs
ADD COLUMN runAt BIGINT;
CREATE INDEX background_jobs_run_at_index ON background_jobs(runAt);

View File

@ -0,0 +1,4 @@
ALTER TABLE background_jobs
ADD COLUMN runAt BIGINT;
CREATE INDEX background_jobs_run_at_index ON background_jobs(runAt);

View File

@ -60,6 +60,7 @@ import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ServerProperties;
import org.jdbi.v3.core.Jdbi;
import org.jdbi.v3.sqlobject.SqlObjects;
import org.jetbrains.annotations.NotNull;
import org.openmetadata.schema.api.security.AuthenticationConfiguration;
import org.openmetadata.schema.api.security.AuthorizerConfiguration;
import org.openmetadata.schema.api.security.ClientType;
@ -241,8 +242,7 @@ public class OpenMetadataApplication extends Application<OpenMetadataApplication
registerEventFilter(catalogConfig, environment);
environment.lifecycle().manage(new ManagedShutdown());
JobHandlerRegistry registry = new JobHandlerRegistry();
registry.register("EnumCleanupHandler", new EnumCleanupHandler(getDao(jdbi)));
JobHandlerRegistry registry = getJobHandlerRegistry();
environment
.lifecycle()
.manage(new GenericBackgroundWorker(jdbi.onDemand(JobDAO.class), registry));
@ -284,6 +284,12 @@ public class OpenMetadataApplication extends Application<OpenMetadataApplication
}
}
protected @NotNull JobHandlerRegistry getJobHandlerRegistry() {
JobHandlerRegistry registry = new JobHandlerRegistry();
registry.register("EnumCleanupHandler", new EnumCleanupHandler(getDao(jdbi)));
return registry;
}
private void registerHealthCheckJobs(OpenMetadataApplicationConfig catalogConfig) {
ServicesStatusJobHandler healthCheckStatusHandler =
ServicesStatusJobHandler.create(

View File

@ -8,6 +8,7 @@ import static org.openmetadata.service.resources.apps.AppResource.SCHEDULED_TYPE
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@ -66,10 +67,12 @@ public class AbstractNativeApplication implements NativeApplication {
@Override
public void install(String installedBy) {
// If the app does not have any Schedule Return without scheduling
if (Boolean.TRUE.equals(app.getDeleted()) || (app.getAppSchedule() == null)) {
return;
}
if (app.getAppType().equals(AppType.Internal)
if (Boolean.TRUE.equals(app.getDeleted())
|| (app.getAppSchedule() == null)
|| Set.of(ScheduleType.NoSchedule, ScheduleType.OnlyManual)
.contains(app.getScheduleType())) {
LOG.debug("App {} does not support scheduling.", app.getName());
} else if (app.getAppType().equals(AppType.Internal)
&& (SCHEDULED_TYPES.contains(app.getScheduleType()))) {
try {
ApplicationHandler.getInstance().removeOldJobs(app);
@ -101,7 +104,8 @@ public class AbstractNativeApplication implements NativeApplication {
@Override
public void triggerOnDemand(Map<String, Object> config) {
// Validate Native Application
if (app.getScheduleType().equals(ScheduleType.ScheduledOrManual)) {
if (Set.of(ScheduleType.ScheduledOrManual, ScheduleType.OnlyManual)
.contains(app.getScheduleType())) {
AppRuntime runtime = getAppRuntime(app);
validateServerExecutableApp(runtime);
// Trigger the application with the provided configuration payload

View File

@ -11,6 +11,7 @@ import com.cronutils.parser.CronParser;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@ -206,7 +207,11 @@ public class AppScheduler {
private JobDetail jobBuilder(App app, String jobIdentity) throws ClassNotFoundException {
JobDataMap dataMap = new JobDataMap();
dataMap.put(APP_NAME, app.getName());
dataMap.put("triggerType", app.getAppSchedule().getScheduleTimeline().value());
dataMap.put(
"triggerType",
Optional.ofNullable(app.getAppSchedule())
.map(v -> v.getScheduleTimeline().value())
.orElse(null));
Class<? extends NativeApplication> clz =
(Class<? extends NativeApplication>) Class.forName(app.getClassName());
JobBuilder jobBuilder =

View File

@ -70,6 +70,7 @@ import org.openmetadata.schema.auth.PasswordResetToken;
import org.openmetadata.schema.auth.PersonalAccessToken;
import org.openmetadata.schema.auth.RefreshToken;
import org.openmetadata.schema.auth.TokenType;
import org.openmetadata.schema.auth.collate.SupportToken;
import org.openmetadata.schema.configuration.AssetCertificationSettings;
import org.openmetadata.schema.configuration.WorkflowSettings;
import org.openmetadata.schema.dataInsight.DataInsightChart;
@ -5764,6 +5765,7 @@ public interface CollectionDAO {
case PASSWORD_RESET -> JsonUtils.readValue(json, PasswordResetToken.class);
case REFRESH_TOKEN -> JsonUtils.readValue(json, RefreshToken.class);
case PERSONAL_ACCESS_TOKEN -> JsonUtils.readValue(json, PersonalAccessToken.class);
case SUPPORT_TOKEN -> JsonUtils.readValue(json, SupportToken.class);
};
}
}

View File

@ -15,7 +15,7 @@ public class GenericBackgroundWorker implements Managed {
private static final int INITIAL_BACKOFF_SECONDS = 1;
private static final int MAX_BACKOFF_SECONDS = 600; // 10 minutes
private static final int NO_JOB_SLEEP_SECONDS = 10; // Sleep if no jobs are available
public static final int NO_JOB_SLEEP_SECONDS = 10; // Sleep if no jobs are available
private final JobDAO jobDao;
private final JobHandlerRegistry handlerRegistry;

View File

@ -26,37 +26,50 @@ public interface JobDAO {
default long insertJob(
BackgroundJob.JobType jobType, JobHandler handler, String jobArgs, String createdBy) {
return insertJob(jobType, handler, jobArgs, createdBy, null);
}
default long insertJob(
BackgroundJob.JobType jobType,
JobHandler handler,
String jobArgs,
String createdBy,
Long runAt) {
try {
JsonUtils.readTree(jobArgs);
} catch (Exception e) {
throw new IllegalArgumentException("jobArgs must be a valid JSON string");
}
return insertJobInternal(
jobType.name(), handler.getClass().getSimpleName(), jobArgs, createdBy);
jobType.name(), handler.getClass().getSimpleName(), jobArgs, createdBy, runAt);
}
@ConnectionAwareSqlUpdate(
value =
"INSERT INTO background_jobs (jobType, methodName, jobArgs, createdBy) "
+ "VALUES (:jobType, :methodName, :jobArgs, :createdBy)",
"INSERT INTO background_jobs (jobType, methodName, jobArgs, createdBy, runAt) "
+ "VALUES (:jobType, :methodName, :jobArgs, :createdBy, :runAt)",
connectionType = MYSQL)
@ConnectionAwareSqlUpdate(
value =
"INSERT INTO background_jobs (jobType, methodName, jobArgs,createdBy) VALUES (:jobType, :methodName, :jobArgs::jsonb,:createdBy) ",
"INSERT INTO background_jobs (jobType, methodName, jobArgs,createdBy,runAt) VALUES (:jobType, :methodName, :jobArgs::jsonb,:createdBy,:runAt) ",
connectionType = POSTGRES)
@GetGeneratedKeys
long insertJobInternal(
@Bind("jobType") String jobType,
@Bind("methodName") String methodName,
@Bind("jobArgs") String jobArgs,
@Bind("createdBy") String createdBy);
@Bind("createdBy") String createdBy,
@Bind("runAt") Long runAt);
default Optional<BackgroundJob> fetchPendingJob() throws BackgroundJobException {
return Optional.ofNullable(fetchPendingJobInternal());
}
@SqlQuery(
"SELECT id,jobType,methodName,jobArgs,status,createdAt,updatedAt,createdBy FROM background_jobs WHERE status = 'PENDING' ORDER BY createdAt LIMIT 1")
"SELECT id,jobType,methodName,jobArgs,status,createdAt,updatedAt,createdBy,runAt FROM background_jobs"
+ " WHERE status = 'PENDING'"
+ " AND COALESCE(runAt, 0) <= UNIX_TIMESTAMP(NOW(3)) * 1000"
+ " ORDER BY createdAt LIMIT 1")
@RegisterRowMapper(BackgroundJobMapper.class)
BackgroundJob fetchPendingJobInternal() throws StatementException;

View File

@ -10,10 +10,17 @@ public class JobHandlerRegistry {
private final Map<String, JobHandler> handlers = new HashMap<>();
public void register(String methodName, JobHandler handler) {
LOG.info("Registering background job handler for: {}", handler.getClass().getSimpleName());
LOG.info(
"Registering background job handler for: {} -> {}",
handler.getClass().getSimpleName(),
handler.getClass().getCanonicalName());
handlers.put(methodName, handler);
}
public void register(JobHandler handler) {
register(handler.getClass().getSimpleName(), handler);
}
public JobHandler getHandler(BackgroundJob job) {
String methodName = job.getMethodName();
Long jobId = job.getId();

View File

@ -107,7 +107,11 @@ public class AppResource extends EntityResource<App, AppRepository> {
static final String FIELDS = "owners";
private SearchRepository searchRepository;
public static final List<ScheduleType> SCHEDULED_TYPES =
List.of(ScheduleType.Scheduled, ScheduleType.ScheduledOrManual, ScheduleType.NoSchedule);
List.of(
ScheduleType.Scheduled,
ScheduleType.ScheduledOrManual,
ScheduleType.NoSchedule,
ScheduleType.OnlyManual);
private final AppMapper mapper = new AppMapper();
@Override

View File

@ -99,7 +99,8 @@ public class JwtFilter implements ContainerRequestFilter {
"v1/users/generatePasswordResetLink",
"v1/users/password/reset",
"v1/users/login",
"v1/users/refresh");
"v1/users/refresh",
"v1/collate/apps/support/login");
@SuppressWarnings("unused")
private JwtFilter() {}

View File

@ -6,11 +6,10 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.openmetadata.service.jobs.GenericBackgroundWorker.NO_JOB_SLEEP_SECONDS;
import static org.openmetadata.service.security.SecurityUtil.getPrincipalName;
import static org.openmetadata.service.util.TestUtils.ADMIN_AUTH_HEADERS;
import java.io.IOException;
import java.net.URISyntaxException;
import java.security.SecureRandom;
import java.util.List;
import java.util.Map;
@ -31,6 +30,7 @@ import org.openmetadata.service.OpenMetadataApplicationTest;
import org.openmetadata.service.jdbi3.CollectionDAO;
import org.openmetadata.service.jobs.BackgroundJobException;
import org.openmetadata.service.jobs.EnumCleanupHandler;
import org.openmetadata.service.jobs.GenericBackgroundWorker;
import org.openmetadata.service.jobs.JobDAO;
import org.openmetadata.service.jobs.JobHandler;
import org.openmetadata.service.jobs.JobHandlerRegistry;
@ -50,11 +50,12 @@ public class BackgroundJobWorkerTest extends OpenMetadataApplicationTest {
public static CustomProperty customPropertyMulti;
public static CustomProperty customPropertySingle;
public static GenericBackgroundWorker worker;
public static Table TABLE4;
@BeforeAll
public static void setup(TestInfo test) throws IOException, URISyntaxException {
public static void setup(TestInfo test) throws Exception {
registry = new JobHandlerRegistry();
jobDAO = Entity.getJobDAO();
collectionDao = Entity.getCollectionDAO();
@ -78,7 +79,7 @@ public class BackgroundJobWorkerTest extends OpenMetadataApplicationTest {
.withConfig(
Map.of(
"values",
List.of("single1", "single2", "single3", "single4", "\"single5\""),
List.of("\"single5\"", "single1", "single2", "single3", "single4"),
"multiSelect",
false)));
@ -92,7 +93,7 @@ public class BackgroundJobWorkerTest extends OpenMetadataApplicationTest {
.withConfig(
Map.of(
"values",
List.of("multi1", "multi2", "multi3", "multi4", "\"multi5\""),
List.of("\"multi5\"", "multi1", "multi2", "multi3", "multi4"),
"multiSelect",
true)));
CustomProperty[] customProperties = {customPropertySingle, customPropertyMulti};
@ -173,7 +174,6 @@ public class BackgroundJobWorkerTest extends OpenMetadataApplicationTest {
@Test
public final void testBackgroundJobTriggerWithValidArgs() {
EnumCleanupArgs enumCleanupArgs =
new EnumCleanupArgs()
.withPropertyName(customPropertyMulti.getName())
@ -204,4 +204,58 @@ public class BackgroundJobWorkerTest extends OpenMetadataApplicationTest {
assertEquals(enumCleanupArgs, actualArgs, "Job arguments should match");
assertEquals(job.getCreatedBy(), fetchedJob.getCreatedBy(), "Created by should match");
}
@Test
public final void testDelayedJobTrigger() throws InterruptedException {
// Create a delayed job for enum cleanup
EnumCleanupArgs enumCleanupArgs =
new EnumCleanupArgs()
.withPropertyName(customPropertyMulti.getName())
.withRemovedEnumKeys(List.of())
.withEntityType("table");
String jobArgs = JsonUtils.pojoToJson(enumCleanupArgs);
String createdBy = "admin";
long delayInMillis = 100; // 100ms delay
long jobId =
Entity.getJobDAO()
.insertJob(
BackgroundJob.JobType.CUSTOM_PROPERTY_ENUM_CLEANUP,
new EnumCleanupHandler(collectionDao),
jobArgs,
createdBy,
System.currentTimeMillis() + delayInMillis);
Optional<BackgroundJob> fetchedJobOptional = Entity.getJobDAO().fetchJobById(jobId);
assertTrue(fetchedJobOptional.isPresent(), "Delayed job should be present");
BackgroundJob fetchedJob = fetchedJobOptional.get();
assertEquals(
BackgroundJob.JobType.CUSTOM_PROPERTY_ENUM_CLEANUP,
fetchedJob.getJobType(),
"Job type should match");
assertEquals("EnumCleanupHandler", fetchedJob.getMethodName(), "Method name should match");
assertEquals(createdBy, fetchedJob.getCreatedBy(), "Created by should match");
// Verify the job arguments
EnumCleanupArgs actualArgs =
JsonUtils.readValue(JsonUtils.pojoToJson(fetchedJob.getJobArgs()), EnumCleanupArgs.class);
assertEquals(enumCleanupArgs, actualArgs, "Job arguments should match");
// Verify job is not executed immediately
Thread.sleep(delayInMillis - 50);
Optional<BackgroundJob> jobAfterShortWait = Entity.getJobDAO().fetchJobById(jobId);
assertTrue(jobAfterShortWait.isPresent(), "Job should still exist after short wait");
assertEquals(
BackgroundJob.Status.PENDING,
jobAfterShortWait.get().getStatus(),
"Job should not be completed yet");
// Wait for the next run cycle
Thread.sleep(delayInMillis + NO_JOB_SLEEP_SECONDS);
Optional<BackgroundJob> jobAfterDelay = Entity.getJobDAO().fetchJobById(jobId);
assertTrue(jobAfterDelay.isPresent(), "Job should still exist after delay");
assertEquals(
BackgroundJob.Status.COMPLETED, jobAfterDelay.get().getStatus(), "Job should be completed");
}
}

View File

@ -15,7 +15,8 @@
"REFRESH_TOKEN",
"EMAIL_VERIFICATION",
"PASSWORD_RESET",
"PERSONAL_ACCESS_TOKEN"
"PERSONAL_ACCESS_TOKEN",
"SUPPORT_TOKEN"
]
}
},

View File

@ -0,0 +1,42 @@
{
"$id": "https://open-metadata.org/schema/auth/supportToken.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "SupportToken",
"description": "This schema defines an access token used for support purposes. It is used only in Collate.",
"type": "object",
"javaType": "org.openmetadata.schema.auth.collate.SupportToken",
"javaInterfaces": ["org.openmetadata.schema.TokenInterface"],
"properties": {
"token": {
"description": "Unique Refresh Token for user",
"$ref": "../type/basic.json#/definitions/uuid"
},
"tokenName": {
"description": "Name of the token",
"type": "string"
},
"userId": {
"description": "User Id of the User this refresh token is given to",
"$ref": "../type/basic.json#/definitions/uuid"
},
"tokenType": {
"description": "Token Type",
"$ref": "./emailVerificationToken.json#/definitions/tokenType",
"default": "SUPPORT_TOKEN"
},
"expiryDate": {
"description": "Expiry Date-Time of the token",
"$ref": "../type/basic.json#/definitions/timestamp"
},
"jwtToken": {
"description": "JWT Auth Token.",
"type": "string"
}
},
"required": [
"token",
"userId",
"expiryDate"
],
"additionalProperties": false
}

View File

@ -3,7 +3,9 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "App",
"javaType": "org.openmetadata.schema.entity.app.App",
"javaInterfaces": ["org.openmetadata.schema.EntityInterface"],
"javaInterfaces": [
"org.openmetadata.schema.EntityInterface"
],
"description": "This schema defines the applications for Open-Metadata.",
"type": "object",
"definitions": {
@ -15,20 +17,29 @@
"Live",
"Scheduled",
"ScheduledOrManual",
"NoSchedule"
"NoSchedule",
"OnlyManual"
],
"javaEnums": [
{
"name": "Live"
"name": "Live",
"description": "An app that has other trigger mechanisms."
},
{
"name": "Scheduled"
"name": "Scheduled",
"description": "An app that has a schedule and cannot be run manually."
},
{
"name": "ScheduledOrManual"
"name": "ScheduledOrManual",
"description": "An app that has a schedule and can be run manually."
},
{
"name": "NoSchedule"
"name": "NoSchedule",
"description": "An app that has no schedule and cannot be run manually."
},
{
"name": "OnlyManual",
"description": "An app that has no schedule but can be run manually."
}
]
},
@ -36,7 +47,14 @@
"javaType": "org.openmetadata.schema.entity.app.ScheduleTimeline",
"description": "This schema defines the Application ScheduleTimeline Options",
"type": "string",
"enum": ["Hourly"," Daily", "Weekly", "Monthly", "Custom", "None"],
"enum": [
"Hourly",
"Daily",
"Weekly",
"Monthly",
"Custom",
"None"
],
"default": "Weekly"
},
"appSchedule": {
@ -51,7 +69,9 @@
"type": "string"
}
},
"required": ["scheduleTimeline"],
"required": [
"scheduleTimeline"
],
"additionalProperties": false
},
"appType": {
@ -173,7 +193,7 @@
"type": "boolean",
"default": false
},
"provider" : {
"provider": {
"$ref": "../../type/basic.json#/definitions/providerType"
},
"developer": {
@ -267,12 +287,12 @@
"appScreenshots": {
"description": "Application Screenshots.",
"type": "array",
"items":{
"items": {
"type": "string"
},
"uniqueItems": true
},
"domain" : {
"domain": {
"description": "Domain the asset belongs to. When not set, the asset inherits the domain from the parent it belongs to.",
"$ref": "../../type/entityReference.json"
},
@ -283,7 +303,7 @@
},
"eventSubscriptions": {
"description": "Event Subscriptions for the Application.",
"$ref": "../../type/entityReferenceList.json"
"$ref": "../../type/entityReferenceList.json"
}
},
"additionalProperties": false,

View File

@ -12,7 +12,7 @@
},
"jobType": {
"type": "string",
"enum": ["CUSTOM_PROPERTY_ENUM_CLEANUP"],
"enum": ["CUSTOM_PROPERTY_ENUM_CLEANUP", "DELETE_ENTITY", "DELETE_TOKEN"],
"description": "Type of the job."
},
"methodName": {
@ -23,10 +23,18 @@
"oneOf": [
{
"$ref": "./enumCleanupArgs.json"
},
{
"type": "object",
"additionalProperties": true
}
],
"description": "Object containing job arguments."
},
"runAt": {
"description": "Timestamp when the job was run in Unix epoch time milliseconds (default: as soon as possible).",
"$ref": "../type/basic.json#/definitions/timestamp"
},
"status": {
"type": "string",
"enum": ["COMPLETED", "FAILED", "RUNNING","PENDING"],

View File

@ -82,14 +82,17 @@ module.exports = {
moduleDirectories: ['node_modules', 'src'],
reporters: [
"default",
["jest-junit", {
outputDirectory: "../../../../target/test-reports",
outputName: "jest-junit.xml",
classNameTemplate: "{classname}",
titleTemplate: "{title}",
ancestorSeparator: " ",
usePathForSuiteName: "true"
}]
]
'default',
[
'jest-junit',
{
outputDirectory: '../../../../target/test-reports',
outputName: 'jest-junit.xml',
classNameTemplate: '{classname}',
titleTemplate: '{title}',
ancestorSeparator: ' ',
usePathForSuiteName: 'true',
},
],
],
};

View File

@ -26,10 +26,6 @@ jest.mock('../../hooks/useApplicationStore', () => {
};
});
jest.mock('../../utils/AuthProvider.util', () => ({
isProtectedRoute: jest.fn().mockReturnValue(true),
}));
jest.mock(
'../Settings/Applications/ApplicationsProvider/ApplicationsProvider',
() => {

View File

@ -21,7 +21,6 @@ import { useApplicationStore } from '../../hooks/useApplicationStore';
import { getLimitConfig } from '../../rest/limitsAPI';
import { getSettingsByType } from '../../rest/settingConfigAPI';
import applicationRoutesClass from '../../utils/ApplicationRoutesClassBase';
import { isProtectedRoute } from '../../utils/AuthProvider.util';
import { LimitBanner } from '../common/LimitBanner/LimitBanner';
import LeftSidebar from '../MyData/LeftSidebar/LeftSidebar.component';
import NavBar from '../NavBar/NavBar';
@ -75,7 +74,8 @@ const AppContainer = () => {
{/* Render main content */}
<Layout>
{/* Render Appbar */}
{isProtectedRoute(location.pathname) && isAuthenticated ? (
{applicationRoutesClass.isProtectedRoute(location.pathname) &&
isAuthenticated ? (
<NavBar />
) : null}

View File

@ -24,13 +24,15 @@ import PageNotFound from '../../pages/PageNotFound/PageNotFound';
import SignUpPage from '../../pages/SignUp/SignUpPage';
import AppContainer from '../AppContainer/AppContainer';
import Loader from '../common/Loader/Loader';
import { UnAuthenticatedAppRouter } from './UnAuthenticatedAppRouter';
import { LogoutPage } from '../../pages/LogoutPage/LogoutPage';
import SamlCallback from '../../pages/SamlCallback';
import applicationRoutesClass from '../../utils/ApplicationRoutesClassBase';
const AppRouter = () => {
const location = useCustomLocation();
const UnAuthenticatedAppRouter =
applicationRoutesClass.getUnAuthenticatedRouteElements();
// web analytics instance
const analytics = useAnalytics();

View File

@ -19,7 +19,7 @@ import { useApplicationStore } from '../../hooks/useApplicationStore';
import useCustomLocation from '../../hooks/useCustomLocation/useCustomLocation';
import PageNotFound from '../../pages/PageNotFound/PageNotFound';
import AccountActivationConfirmation from '../../pages/SignUp/account-activation-confirmation.component';
import { isProtectedRoute } from '../../utils/AuthProvider.util';
import applicationRoutesClass from '../../utils/ApplicationRoutesClassBase';
import Auth0Callback from '../Auth/AppCallbacks/Auth0Callback/Auth0Callback';
import withSuspenseFallback from './withSuspenseFallback';
@ -64,7 +64,7 @@ export const UnAuthenticatedAppRouter = () => {
}
}, [authConfig?.provider]);
if (isProtectedRoute(location.pathname)) {
if (applicationRoutesClass.isProtectedRoute(location.pathname)) {
return <Redirect to={ROUTES.SIGNIN} />;
}

View File

@ -64,13 +64,13 @@ import {
fetchAuthorizerConfig,
} from '../../../rest/miscAPI';
import { getLoggedInUser } from '../../../rest/userAPI';
import applicationRoutesClass from '../../../utils/ApplicationRoutesClassBase';
import TokenService from '../../../utils/Auth/TokenService/TokenServiceUtil';
import {
extractDetailsFromToken,
getAuthConfig,
getUrlPathnameExpiry,
getUserManagerConfig,
isProtectedRoute,
prepareUserProfileFromClaims,
} from '../../../utils/AuthProvider.util';
import {
@ -209,7 +209,7 @@ export const AuthProvider = ({
}, []);
const handledVerifiedUser = () => {
if (!isProtectedRoute(location.pathname)) {
if (!applicationRoutesClass.isProtectedRoute(location.pathname)) {
history.push(ROUTES.HOME);
}
};
@ -404,7 +404,7 @@ export const AuthProvider = ({
* Stores redirect URL for successful login
*/
const handleStoreProtectedRedirectPath = useCallback(() => {
if (isProtectedRoute(location.pathname)) {
if (applicationRoutesClass.isProtectedRoute(location.pathname)) {
storeRedirectPath(location.pathname);
}
}, [location.pathname, storeRedirectPath]);

View File

@ -58,12 +58,16 @@ import { useTourProvider } from '../../context/TourProvider/TourProvider';
import { useWebSocketConnector } from '../../context/WebSocketProvider/WebSocketProvider';
import { EntityTabs, EntityType } from '../../enums/entity.enum';
import { EntityReference } from '../../generated/entity/type';
import { BackgroundJob, JobType } from '../../generated/jobs/backgroundJob';
import {
BackgroundJob,
EnumCleanupArgs,
JobType,
} from '../../generated/jobs/backgroundJob';
import { useCurrentUserPreferences } from '../../hooks/currentUserStore/useCurrentUserStore';
import useCustomLocation from '../../hooks/useCustomLocation/useCustomLocation';
import { useDomainStore } from '../../hooks/useDomainStore';
import { getVersion } from '../../rest/miscAPI';
import { isProtectedRoute } from '../../utils/AuthProvider.util';
import applicationRoutesClass from '../../utils/ApplicationRoutesClassBase';
import brandClassBase from '../../utils/BrandData/BrandClassBase';
import {
hasNotificationPermission,
@ -259,6 +263,18 @@ const NavBar = () => {
const { jobArgs, status, jobType } = backgroundJobData;
if (jobType === JobType.CustomPropertyEnumCleanup) {
const enumCleanupArgs = jobArgs as EnumCleanupArgs;
if (!enumCleanupArgs.entityType) {
showErrorToast(
{
isAxiosError: true,
message: 'Invalid job arguments: entityType is required',
} as AxiosError,
t('message.unexpected-error')
);
break;
}
body = t('message.custom-property-update', {
propertyName: jobArgs.propertyName,
entityName: jobArgs.entityType,
@ -267,7 +283,7 @@ const NavBar = () => {
path = getSettingPath(
GlobalSettingsMenuCategory.CUSTOM_PROPERTIES,
getCustomPropertyEntityPathname(jobArgs.entityType)
getCustomPropertyEntityPathname(enumCleanupArgs.entityType)
);
}
@ -303,7 +319,10 @@ const NavBar = () => {
}
const handleDocumentVisibilityChange = async () => {
if (isProtectedRoute(location.pathname) && isTourRoute) {
if (
applicationRoutesClass.isProtectedRoute(location.pathname) &&
isTourRoute
) {
return;
}
const newVersion = await getVersion();

View File

@ -54,11 +54,9 @@ const AppSchedule = ({
const { config } = useLimitStore();
const showRunNowButton = useMemo(() => {
if (appData && appData.scheduleType === ScheduleType.ScheduledOrManual) {
return true;
}
return false;
return [ScheduleType.ScheduledOrManual, ScheduleType.OnlyManual].includes(
appData?.scheduleType
);
}, [appData]);
const { pipelineSchedules } =

View File

@ -54,4 +54,5 @@ export enum TokenType {
PasswordReset = "PASSWORD_RESET",
PersonalAccessToken = "PERSONAL_ACCESS_TOKEN",
RefreshToken = "REFRESH_TOKEN",
SupportToken = "SUPPORT_TOKEN",
}

View File

@ -0,0 +1,54 @@
/*
* Copyright 2025 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This schema defines an access token used for support purposes. It is used only in Collate.
*/
export interface SupporToken {
/**
* Expiry Date-Time of the token
*/
expiryDate: number;
/**
* JWT Auth Token.
*/
jwtToken?: string;
/**
* Unique Refresh Token for user
*/
token: string;
/**
* Name of the token
*/
tokenName?: string;
/**
* Token Type
*/
tokenType?: TokenType;
/**
* User Id of the User this refresh token is given to
*/
userId: string;
}
/**
* Token Type
*
* Different Type of User token
*/
export enum TokenType {
EmailVerification = "EMAIL_VERIFICATION",
PasswordReset = "PASSWORD_RESET",
PersonalAccessToken = "PERSONAL_ACCESS_TOKEN",
RefreshToken = "REFRESH_TOKEN",
SupportToken = "SUPPORT_TOKEN",
}

View File

@ -1324,6 +1324,7 @@ export interface ExecutionContext {
export enum ScheduleType {
Live = "Live",
NoSchedule = "NoSchedule",
OnlyManual = "OnlyManual",
Scheduled = "Scheduled",
ScheduledOrManual = "ScheduledOrManual",
}

View File

@ -39,6 +39,11 @@ export interface BackgroundJob {
* JobHandler name of the method that will be executed for this job.
*/
methodName: string;
/**
* Timestamp when the job was run in Unix epoch time milliseconds (default: as soon as
* possible).
*/
runAt?: number;
/**
* Current status of the job.
*/
@ -58,15 +63,16 @@ export interface EnumCleanupArgs {
/**
* Type of the entity.
*/
entityType: string;
entityType?: string;
/**
* Name of the property.
*/
propertyName: string;
propertyName?: string;
/**
* List of removed enum keys.
*/
removedEnumKeys: string[];
removedEnumKeys?: string[];
[property: string]: any;
}
/**
@ -74,6 +80,8 @@ export interface EnumCleanupArgs {
*/
export enum JobType {
CustomPropertyEnumCleanup = "CUSTOM_PROPERTY_ENUM_CLEANUP",
DeleteEntity = "DELETE_ENTITY",
DeleteToken = "DELETE_TOKEN",
}
/**

View File

@ -13,11 +13,37 @@
import { FC } from 'react';
import AuthenticatedAppRouter from '../components/AppRouter/AuthenticatedAppRouter';
import { UnAuthenticatedAppRouter } from '../components/AppRouter/UnAuthenticatedAppRouter';
import { ROUTES } from '../constants/constants';
class ApplicationRoutesClassBase {
public getRouteElements(): FC {
return AuthenticatedAppRouter;
}
public getUnAuthenticatedRouteElements(): FC {
return UnAuthenticatedAppRouter;
}
public isProtectedRoute(pathname: string): boolean {
return (
[
ROUTES.SIGNUP,
ROUTES.SIGNIN,
ROUTES.FORGOT_PASSWORD,
ROUTES.CALLBACK,
ROUTES.SILENT_CALLBACK,
ROUTES.SAML_CALLBACK,
ROUTES.REGISTER,
ROUTES.RESET_PASSWORD,
ROUTES.ACCOUNT_ACTIVATION,
ROUTES.HOME,
ROUTES.AUTH_CALLBACK,
ROUTES.NOT_FOUND,
ROUTES.LOGOUT,
].indexOf(pathname) === -1
);
}
}
const applicationRoutesClass = new ApplicationRoutesClassBase();

View File

@ -296,26 +296,6 @@ export const getNameFromUserData = (
return { name: userName, email: email, picture: user.picture };
};
export const isProtectedRoute = (pathname: string) => {
return (
[
ROUTES.SIGNUP,
ROUTES.SIGNIN,
ROUTES.FORGOT_PASSWORD,
ROUTES.CALLBACK,
ROUTES.SILENT_CALLBACK,
ROUTES.SAML_CALLBACK,
ROUTES.REGISTER,
ROUTES.RESET_PASSWORD,
ROUTES.ACCOUNT_ACTIVATION,
ROUTES.HOME,
ROUTES.AUTH_CALLBACK,
ROUTES.NOT_FOUND,
ROUTES.LOGOUT,
].indexOf(pathname) === -1
);
};
export const isTourRoute = (pathname: string) => {
return pathname === ROUTES.TOUR;
};

View File

@ -28,8 +28,8 @@ import ptBR from '../../locale/languages/pt-br.json';
import ptPT from '../../locale/languages/pt-pt.json';
import ruRU from '../../locale/languages/ru-ru.json';
import thTH from '../../locale/languages/th-th.json';
import zhCN from '../../locale/languages/zh-cn.json';
import trTR from '../../locale/languages/tr-tr.json';
import zhCN from '../../locale/languages/zh-cn.json';
export enum SupportedLocales {
English = 'en-US',