diff --git a/packages/twenty-front/src/config/__tests__/apiProxyPrefixes.test.ts b/packages/twenty-front/src/config/__tests__/apiProxyPrefixes.test.ts new file mode 100644 index 0000000000..5fc9096af0 --- /dev/null +++ b/packages/twenty-front/src/config/__tests__/apiProxyPrefixes.test.ts @@ -0,0 +1,43 @@ +import { ApiPath, AppPath } from 'twenty-shared/types'; + +import { + API_PROXY_PATHS, + buildApiProxyMatcher, +} from '~/config/apiProxyPrefixes'; + +const matchers = API_PROXY_PATHS.map( + (apiPath) => new RegExp(buildApiProxyMatcher(apiPath)), +); + +const isProxiedPath = (path: string) => + matchers.some((matcher) => matcher.test(path)); + +describe('apiProxyPrefixes', () => { + it.each(Object.values(ApiPath))('should proxy the API path %s', (apiPath) => { + expect(isProxiedPath(`/${apiPath}`)).toBe(true); + expect(isProxiedPath(`/${apiPath}/nested-path`)).toBe(true); + expect(isProxiedPath(`/${apiPath}?query=value`)).toBe(true); + }); + + it.each(Object.values(AppPath))( + 'should not proxy the SPA route %s', + (appPath) => { + expect(isProxiedPath(appPath)).toBe(false); + }, + ); + + const viteDevServerPaths = [ + '/', + '/index.html', + '/src/main.tsx', + '/@vite/client', + '/node_modules/.vite/deps/react.js', + ]; + + it.each(viteDevServerPaths)( + 'should not proxy the vite dev server path %s', + (viteDevServerPath) => { + expect(isProxiedPath(viteDevServerPath)).toBe(false); + }, + ); +}); diff --git a/packages/twenty-front/src/config/apiProxyPrefixes.ts b/packages/twenty-front/src/config/apiProxyPrefixes.ts new file mode 100644 index 0000000000..6428ff22b6 --- /dev/null +++ b/packages/twenty-front/src/config/apiProxyPrefixes.ts @@ -0,0 +1,9 @@ +import { ApiPath } from 'twenty-shared/types'; + +export const API_PROXY_PATHS = Object.values(ApiPath); + +const escapeRegExp = (value: string) => + value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +export const buildApiProxyMatcher = (apiPath: ApiPath) => + `^/${escapeRegExp(apiPath)}($|[/?])`; diff --git a/packages/twenty-front/src/config/index.ts b/packages/twenty-front/src/config/index.ts index b31638c31e..957454e3b1 100644 --- a/packages/twenty-front/src/config/index.ts +++ b/packages/twenty-front/src/config/index.ts @@ -1,21 +1,2 @@ -const getDefaultUrl = () => { - if ( - window.location.hostname.endsWith('localhost') || - window.location.hostname.endsWith('127.0.0.1') - ) { - // In development environment front and backend usually run on separate ports - // we set the default value to localhost:3000. - // In dev context, we use env vars to overwrite it - return `http://${window.location.hostname}:3000`; - } else { - // Outside of localhost we assume that they run on the same port - // because the backend will serve the frontend - // In prod context, we use index.html + window var to ovewrite it - return `${window.location.protocol}//${window.location.hostname}${ - window.location.port ? `:${window.location.port}` : '' - }`; - } -}; - export const REACT_APP_SERVER_BASE_URL = - window._env_?.REACT_APP_SERVER_BASE_URL || getDefaultUrl(); + window._env_?.REACT_APP_SERVER_BASE_URL || window.location.origin; diff --git a/packages/twenty-front/vite.config.ts b/packages/twenty-front/vite.config.ts index 2820581202..6e08b2dae0 100644 --- a/packages/twenty-front/vite.config.ts +++ b/packages/twenty-front/vite.config.ts @@ -15,6 +15,11 @@ import svgr from 'vite-plugin-svgr'; import { createWywProfilingPlugin } from 'twenty-shared/vite'; +import { + API_PROXY_PATHS, + buildApiProxyMatcher, +} from './src/config/apiProxyPrefixes'; + export default defineConfig(({ mode }) => { const env = loadEnv(mode, __dirname, ''); @@ -24,6 +29,7 @@ export default defineConfig(({ mode }) => { SSL_CERT_PATH, SSL_KEY_PATH, REACT_APP_PORT, + REACT_APP_SERVER_BASE_URL, IS_DEBUG_MODE, } = env; @@ -31,6 +37,17 @@ export default defineConfig(({ mode }) => { ? parseInt(REACT_APP_PORT) : 3001; + const apiProxyTarget = isNonEmptyString(REACT_APP_SERVER_BASE_URL) + ? REACT_APP_SERVER_BASE_URL + : 'http://localhost:3000'; + + const apiProxy = Object.fromEntries( + API_PROXY_PATHS.map((apiPath) => [ + buildApiProxyMatcher(apiPath), + { target: apiProxyTarget }, + ]), + ); + const CHUNK_SIZE_WARNING_LIMIT = 1024 * 1024; // 1MB // Please don't increase this limit for main index chunk // If it gets too big then find modules in the code base @@ -49,6 +66,7 @@ export default defineConfig(({ mode }) => { server: { port: port, + proxy: apiProxy, ...(VITE_HOST ? { host: VITE_HOST } : {}), ...(SSL_KEY_PATH && SSL_CERT_PATH ? { @@ -260,9 +278,15 @@ export default defineConfig(({ mode }) => { // wyw-in-js 1.x resolves modules in its CSS evaluator via vite's // resolve.alias (not resolve.tsconfigPaths), so the `@/` and `~/` // tsconfig path aliases must be mirrored here. - { find: /^@\//, replacement: path.resolve(__dirname, 'src/modules') + '/' }, + { + find: /^@\//, + replacement: path.resolve(__dirname, 'src/modules') + '/', + }, { find: /^~\//, replacement: path.resolve(__dirname, 'src') + '/' }, - { find: 'path', replacement: 'rollup-plugin-node-polyfills/polyfills/path' }, + { + find: 'path', + replacement: 'rollup-plugin-node-polyfills/polyfills/path', + }, ], }, }; diff --git a/packages/twenty-server/src/app.module.ts b/packages/twenty-server/src/app.module.ts index 604332370a..832392c24f 100644 --- a/packages/twenty-server/src/app.module.ts +++ b/packages/twenty-server/src/app.module.ts @@ -13,6 +13,7 @@ import { join } from 'path'; import { YogaDriver, type YogaDriverConfig } from '@graphql-yoga/nestjs'; import { SentryModule } from '@sentry/nestjs/setup'; +import { ApiPath } from 'twenty-shared/types'; import { AdminPanelGraphQLApiModule } from 'src/engine/api/graphql/admin-panel-graphql-api.module'; import { CoreGraphQLApiModule } from 'src/engine/api/graphql/core-graphql-api.module'; @@ -131,7 +132,7 @@ export class AppModule { // A cross-origin form post from the identity provider, authenticated on the // assertion rather than the cookie. .exclude({ - path: 'auth/saml/callback/:identityProviderId', + path: `${ApiPath.Auth}/saml/callback/:identityProviderId`, method: RequestMethod.POST, }) .forRoutes({ path: '*path', method: RequestMethod.ALL }); @@ -141,30 +142,30 @@ export class AppModule { GraphQLHydrateRequestFromTokenMiddleware, WorkspaceAuthContextMiddleware, ) - .forRoutes({ path: 'graphql', method: RequestMethod.ALL }); + .forRoutes({ path: ApiPath.GraphQL, method: RequestMethod.ALL }); consumer .apply( GraphQLHydrateRequestFromTokenMiddleware, WorkspaceAuthContextMiddleware, ) - .forRoutes({ path: 'metadata', method: RequestMethod.ALL }); + .forRoutes({ path: ApiPath.Metadata, method: RequestMethod.ALL }); consumer .apply( GraphQLHydrateRequestFromTokenMiddleware, WorkspaceAuthContextMiddleware, ) - .forRoutes({ path: 'admin-panel', method: RequestMethod.ALL }); + .forRoutes({ path: ApiPath.AdminPanel, method: RequestMethod.ALL }); consumer .apply(McpMethodGuardMiddleware) - .forRoutes({ path: 'mcp', method: RequestMethod.ALL }); + .forRoutes({ path: ApiPath.Mcp, method: RequestMethod.ALL }); for (const method of MIGRATED_REST_METHODS) { consumer .apply(RestCoreMiddleware, WorkspaceAuthContextMiddleware) - .forRoutes({ path: 'rest/*path', method }); + .forRoutes({ path: `${ApiPath.Rest}/*path`, method }); } } } diff --git a/packages/twenty-server/src/engine/api/graphql/admin-panel.module-factory.ts b/packages/twenty-server/src/engine/api/graphql/admin-panel.module-factory.ts index 38553c25e0..bd8a75d789 100644 --- a/packages/twenty-server/src/engine/api/graphql/admin-panel.module-factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/admin-panel.module-factory.ts @@ -1,6 +1,7 @@ import { type YogaDriverConfig } from '@graphql-yoga/nestjs'; import * as Sentry from '@sentry/node'; import GraphQLJSON from 'graphql-type-json'; +import { ApiPath } from 'twenty-shared/types'; import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface'; @@ -29,7 +30,7 @@ export const adminPanelModuleFactory = async ( resolverSchemaScope: 'admin', buildSchemaOptions: {}, renderGraphiQL() { - return renderApolloPlayground({ path: 'admin-panel' }); + return renderApolloPlayground({ path: ApiPath.AdminPanel }); }, resolvers: { JSON: GraphQLJSON }, plugins: [ @@ -50,7 +51,7 @@ export const adminPanelModuleFactory = async ( checkDuplicateRootResolvers: true, }), ], - path: '/admin-panel', + path: `/${ApiPath.AdminPanel}`, context: () => ({ loaders: dataloaderService.createLoaders(), }), @@ -58,7 +59,7 @@ export const adminPanelModuleFactory = async ( if (twentyConfigService.get('NODE_ENV') === NodeEnvironment.DEVELOPMENT) { config.renderGraphiQL = () => { - return renderApolloPlayground({ path: 'admin-panel' }); + return renderApolloPlayground({ path: ApiPath.AdminPanel }); }; } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts index 291bee8a02..aae4c3b053 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts @@ -8,6 +8,7 @@ import { } from '@graphql-yoga/nestjs'; import * as Sentry from '@sentry/node'; import GraphQLJSON from 'graphql-type-json'; +import { ApiPath } from 'twenty-shared/types'; import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface'; @@ -86,6 +87,7 @@ export class GraphQLConfigService implements GqlOptionsFactory< buildSchemaOptions: {}, resolvers: { JSON: GraphQLJSON }, plugins: plugins, + path: `/${ApiPath.GraphQL}`, context: () => ({ loaders: this.dataloaderService.createLoaders(), }), diff --git a/packages/twenty-server/src/engine/api/graphql/metadata.module-factory.ts b/packages/twenty-server/src/engine/api/graphql/metadata.module-factory.ts index 0b370d9e35..730b3e94e7 100644 --- a/packages/twenty-server/src/engine/api/graphql/metadata.module-factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/metadata.module-factory.ts @@ -1,6 +1,7 @@ import { type YogaDriverConfig } from '@graphql-yoga/nestjs'; import * as Sentry from '@sentry/node'; import GraphQLJSON from 'graphql-type-json'; +import { ApiPath } from 'twenty-shared/types'; import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface'; @@ -40,7 +41,7 @@ export const metadataModuleFactory = async ( orphanedTypes: [ClientConfig], }, renderGraphiQL() { - return renderApolloPlayground({ path: 'metadata' }); + return renderApolloPlayground({ path: ApiPath.Metadata }); }, resolvers: { JSON: GraphQLJSON }, plugins: [ @@ -70,7 +71,7 @@ export const metadataModuleFactory = async ( checkDuplicateRootResolvers: true, }), ], - path: '/metadata', + path: `/${ApiPath.Metadata}`, context: () => ({ loaders: dataloaderService.createLoaders(), }), @@ -78,7 +79,7 @@ export const metadataModuleFactory = async ( if (twentyConfigService.get('NODE_ENV') === NodeEnvironment.DEVELOPMENT) { config.renderGraphiQL = () => { - return renderApolloPlayground({ path: 'metadata' }); + return renderApolloPlayground({ path: ApiPath.Metadata }); }; } diff --git a/packages/twenty-server/src/engine/api/mcp/controllers/mcp-core.controller.ts b/packages/twenty-server/src/engine/api/mcp/controllers/mcp-core.controller.ts index f24c484374..d08aee1291 100644 --- a/packages/twenty-server/src/engine/api/mcp/controllers/mcp-core.controller.ts +++ b/packages/twenty-server/src/engine/api/mcp/controllers/mcp-core.controller.ts @@ -13,6 +13,7 @@ import { } from '@nestjs/common'; import { type Response } from 'express'; +import { ApiPath } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { JsonRpc } from 'src/engine/api/mcp/dtos/json-rpc'; @@ -30,7 +31,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -@Controller('mcp') +@Controller(ApiPath.Mcp) @UseGuards(McpAuthGuard, WorkspaceAuthGuard, NoPermissionGuard) @UseFilters(RestApiExceptionFilter) export class McpCoreController { diff --git a/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core.controller.ts b/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core.controller.ts index 97917d957e..9d0462a6b3 100644 --- a/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core.controller.ts +++ b/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core.controller.ts @@ -13,6 +13,7 @@ import { } from '@nestjs/common'; import { Response } from 'express'; +import { ApiPath } from 'twenty-shared/types'; import { RestApiCoreService } from 'src/engine/api/rest/core/services/rest-api-core.service'; import { RestApiExceptionFilter } from 'src/engine/api/rest/rest-api-exception.filter'; @@ -21,7 +22,7 @@ import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -@Controller('rest') +@Controller(ApiPath.Rest) @UseGuards(JwtAuthGuard, WorkspaceAuthGuard, CustomPermissionGuard) @UseFilters(RestApiExceptionFilter) export class RestApiCoreController { diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils.ts index da69d296de..fafa0cda87 100644 --- a/packages/twenty-server/src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils.ts +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils.ts @@ -1,14 +1,15 @@ import { BadRequestException } from '@nestjs/common'; import { type Request } from 'express'; +import { ApiPath } from 'twenty-shared/types'; import { isDefined, isValidUuid } from 'twenty-shared/utils'; export const parseCorePath = ( request: Request, ): { object: string; id?: string } => { const queryAction = request.path - .replace('/rest/', '') - .replace('/rest', '') + .replace(`/${ApiPath.Rest}/`, '') + .replace(`/${ApiPath.Rest}`, '') .split('/') .filter(Boolean); @@ -17,13 +18,13 @@ export const parseCorePath = ( (queryAction.length > 3 && queryAction[0] === 'restore') ) { throw new BadRequestException( - `Query path '${request.path}' invalid. Valid examples: /rest/companies/id or /rest/companies or /rest/batch/companies`, + `Query path '${request.path}' invalid. Valid examples: /${ApiPath.Rest}/companies/id or /${ApiPath.Rest}/companies or /${ApiPath.Rest}/batch/companies`, ); } if (queryAction.length === 0) { throw new BadRequestException( - `Query path '${request.path}' invalid. Valid examples: /rest/companies/id or /rest/companies or /rest/batch/companies`, + `Query path '${request.path}' invalid. Valid examples: /${ApiPath.Rest}/companies/id or /${ApiPath.Rest}/companies or /${ApiPath.Rest}/batch/companies`, ); } diff --git a/packages/twenty-server/src/engine/core-modules/api-key/controllers/api-key.controller.ts b/packages/twenty-server/src/engine/core-modules/api-key/controllers/api-key.controller.ts index 07ce27b6bf..7f151d01c6 100644 --- a/packages/twenty-server/src/engine/core-modules/api-key/controllers/api-key.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/api-key/controllers/api-key.controller.ts @@ -12,6 +12,7 @@ import { import { type QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; import { PermissionFlagType } from 'twenty-shared/constants'; +import { ApiPath } from 'twenty-shared/types'; import { RestApiExceptionFilter } from 'src/engine/api/rest/rest-api-exception.filter'; import { type ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity'; @@ -30,7 +31,7 @@ import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/p * rest/apiKeys is deprecated, use rest/metadata/apiKeys instead * rest/apiKeys will be removed in the future */ -@Controller(['rest/apiKeys', 'rest/metadata/apiKeys']) +@Controller([`${ApiPath.Rest}/apiKeys`, `${ApiPath.Rest}/metadata/apiKeys`]) @UseGuards( JwtAuthGuard, WorkspaceAuthGuard, diff --git a/packages/twenty-server/src/engine/core-modules/application/application-oauth/controllers/oauth-discovery.controller.ts b/packages/twenty-server/src/engine/core-modules/application/application-oauth/controllers/oauth-discovery.controller.ts index 1783fbaa0f..cb21ee6e0b 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-oauth/controllers/oauth-discovery.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-oauth/controllers/oauth-discovery.controller.ts @@ -1,6 +1,7 @@ import { Controller, Get, Req, UseGuards } from '@nestjs/common'; import { type Request } from 'express'; +import { ApiPath } from 'twenty-shared/types'; import { ALL_OAUTH_SCOPES } from 'src/engine/core-modules/application/application-oauth/constants/oauth-scopes'; import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service'; @@ -12,7 +13,7 @@ import { cleanServerUrl } from 'src/utils/clean-server-url'; import { getRequestBaseUrl } from 'src/utils/get-request-base-url.util'; import { TWENTY_CLI_APPLICATION_REGISTRATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-cli-application-registration.constant'; -@Controller('.well-known') +@Controller(ApiPath.WellKnown) export class OAuthDiscoveryController { constructor( private readonly twentyConfigService: TwentyConfigService, @@ -40,10 +41,10 @@ export class OAuthDiscoveryController { return { issuer, authorization_endpoint: `${authorizeBase}/authorize`, - token_endpoint: `${issuer}/oauth/token`, - registration_endpoint: `${issuer}/oauth/register`, - revocation_endpoint: `${issuer}/oauth/revoke`, - introspection_endpoint: `${issuer}/oauth/introspect`, + token_endpoint: `${issuer}/${ApiPath.OAuth}/token`, + registration_endpoint: `${issuer}/${ApiPath.OAuth}/register`, + revocation_endpoint: `${issuer}/${ApiPath.OAuth}/revoke`, + introspection_endpoint: `${issuer}/${ApiPath.OAuth}/introspect`, scopes_supported: ALL_OAUTH_SCOPES, response_types_supported: ['code'], response_modes_supported: ['query'], diff --git a/packages/twenty-server/src/engine/core-modules/application/application-oauth/controllers/oauth-registration.controller.ts b/packages/twenty-server/src/engine/core-modules/application/application-oauth/controllers/oauth-registration.controller.ts index e3de7aee25..673a4389e0 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-oauth/controllers/oauth-registration.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-oauth/controllers/oauth-registration.controller.ts @@ -14,6 +14,7 @@ import { } from '@nestjs/common'; import { type Request, type Response } from 'express'; +import { ApiPath } from 'twenty-shared/types'; import { v4 } from 'uuid'; import { InjectRepository } from '@nestjs/typeorm'; @@ -41,7 +42,7 @@ const REGISTRATION_RATE_LIMIT_WINDOW_MS = 3_600_000; const ALLOWED_GRANT_TYPES = ['authorization_code', 'refresh_token']; const ALLOWED_RESPONSE_TYPES = ['code']; -@Controller('oauth') +@Controller(ApiPath.OAuth) @UseFilters(AuthRestApiExceptionFilter) export class OAuthRegistrationController { constructor( @@ -174,7 +175,7 @@ export class OAuthRegistrationController { token_endpoint_auth_method: tokenEndpointAuthMethod, scope: requestedScopes.join(' '), client_id_issued_at: Math.floor(Date.now() / 1000), - registration_client_uri: `${issuer}/oauth/register/${clientId}`, + registration_client_uri: `${issuer}/${ApiPath.OAuth}/register/${clientId}`, }; } @@ -216,7 +217,7 @@ export class OAuthRegistrationController { response_types: ['code'], token_endpoint_auth_method: 'none', scope: registration.oAuthScopes.join(' '), - registration_client_uri: `${issuer}/oauth/register/${registration.oAuthClientId}`, + registration_client_uri: `${issuer}/${ApiPath.OAuth}/register/${registration.oAuthClientId}`, }; } diff --git a/packages/twenty-server/src/engine/core-modules/application/application-oauth/controllers/oauth-token.controller.ts b/packages/twenty-server/src/engine/core-modules/application/application-oauth/controllers/oauth-token.controller.ts index 007669f0aa..984682efff 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-oauth/controllers/oauth-token.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-oauth/controllers/oauth-token.controller.ts @@ -12,6 +12,7 @@ import { } from '@nestjs/common'; import { type Request, type Response } from 'express'; +import { ApiPath } from 'twenty-shared/types'; import { OAuthIntrospectInput } from 'src/engine/core-modules/application/application-oauth/dtos/oauth-introspect.input'; import { OAuthRevokeInput } from 'src/engine/core-modules/application/application-oauth/dtos/oauth-revoke.input'; @@ -28,7 +29,7 @@ import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; const OAUTH_RATE_LIMIT_MAX = 60; const OAUTH_RATE_LIMIT_WINDOW_MS = 60_000; -@Controller('oauth') +@Controller(ApiPath.OAuth) @UseFilters(AuthRestApiExceptionFilter) export class OAuthTokenController { constructor( diff --git a/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration-claim.controller.ts b/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration-claim.controller.ts index 60acaa4b46..6523e2c82e 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration-claim.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration-claim.controller.ts @@ -1,7 +1,7 @@ import { Controller, Get, Query, Res, UseGuards } from '@nestjs/common'; import { Response } from 'express'; -import { SettingsPath } from 'twenty-shared/types'; +import { ApiPath, SettingsPath } from 'twenty-shared/types'; import { getSettingsPath } from 'twenty-shared/utils'; import { ApplicationRegistrationClaimService } from 'src/engine/core-modules/application/application-registration/application-registration-claim.service'; @@ -20,7 +20,7 @@ import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; // GitHub OAuth callback of the trusted-publishers claim flow. Auth context // travels in the signed state token, not in a session, hence the public // endpoint. -@Controller('application-registration-claim') +@Controller(ApiPath.ApplicationRegistrationClaim) export class ApplicationRegistrationClaimController { constructor( private readonly applicationRegistrationClaimService: ApplicationRegistrationClaimService, diff --git a/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts b/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts index 999aaffc5f..df03f6c108 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts @@ -1,6 +1,7 @@ import { UseGuards } from '@nestjs/common'; import { Args, Parent, Query, ResolveField } from '@nestjs/graphql'; +import { ApiPath } from 'twenty-shared/types'; import { isAbsoluteUrl, isDefined } from 'twenty-shared/utils'; import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator'; @@ -80,6 +81,6 @@ export class ApplicationResolver { const serverUrl = this.twentyConfigService.get('SERVER_URL'); - return `${serverUrl}/public-assets/${workspace.id}/${application.id}/${logo}`; + return `${serverUrl}/${ApiPath.PublicAssets}/${workspace.id}/${application.id}/${logo}`; } } diff --git a/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider-oauth.controller.ts b/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider-oauth.controller.ts index 4f881c6c29..c5ffd47102 100644 --- a/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider-oauth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider-oauth.controller.ts @@ -2,7 +2,7 @@ import { Controller, Get, Logger, Query, Res, UseGuards } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { type Response } from 'express'; -import { SettingsPath } from 'twenty-shared/types'; +import { ApiPath, SettingsPath } from 'twenty-shared/types'; import { getSettingsPath, isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; @@ -23,7 +23,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; -@Controller('auth/apps') +@Controller(`${ApiPath.Auth}/apps`) @UseGuards(PublicEndpointGuard, NoPermissionGuard) export class ConnectionProviderOAuthController { private readonly logger = new Logger(ConnectionProviderOAuthController.name); diff --git a/packages/twenty-server/src/engine/core-modules/application/connection-provider/connections/application-connections.controller.ts b/packages/twenty-server/src/engine/core-modules/application/connection-provider/connections/application-connections.controller.ts index 29eb1892f5..686b1006fd 100644 --- a/packages/twenty-server/src/engine/core-modules/application/connection-provider/connections/application-connections.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/application/connection-provider/connections/application-connections.controller.ts @@ -12,6 +12,7 @@ import { } from '@nestjs/common'; import { Request } from 'express'; +import { ApiPath } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { type AppConnectionDto } from 'src/engine/core-modules/application/connection-provider/connections/dtos/app-connection.dto'; @@ -26,7 +27,7 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; * queries on the metadata schema (ApplicationConnectionsResolver). The SDK * helpers (`listConnections`, `getConnection`) now call GraphQL. Kept for * backward compatibility with already-deployed app runtimes. */ -@Controller('apps/connections') +@Controller(`${ApiPath.Apps}/connections`) @UseGuards(JwtAuthGuard, WorkspaceAuthGuard, NoPermissionGuard) @UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })) export class ApplicationConnectionsController { diff --git a/packages/twenty-server/src/engine/core-modules/application/connection-provider/utils/build-callback-url.util.ts b/packages/twenty-server/src/engine/core-modules/application/connection-provider/utils/build-callback-url.util.ts index c97c918ec6..442aaaa667 100644 --- a/packages/twenty-server/src/engine/core-modules/application/connection-provider/utils/build-callback-url.util.ts +++ b/packages/twenty-server/src/engine/core-modules/application/connection-provider/utils/build-callback-url.util.ts @@ -1,5 +1,7 @@ +import { ApiPath } from 'twenty-shared/types'; + // Workspace-agnostic by design: the workspace identity travels in the // signed `state` parameter, so a single redirect URL configured at the // OAuth provider serves every workspace. export const buildAppOAuthCallbackUrl = (serverUrl: string): string => - new URL('/auth/apps/callback', serverUrl).toString(); + new URL(`/${ApiPath.Auth}/apps/callback`, serverUrl).toString(); diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts index befcfa78aa..5f34fa1d8f 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts @@ -9,7 +9,7 @@ import { import { InjectRepository } from '@nestjs/typeorm'; import { Response } from 'express'; -import { SettingsPath } from 'twenty-shared/types'; +import { ApiPath, SettingsPath } from 'twenty-shared/types'; import { getSettingsPath } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; @@ -31,7 +31,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; -@Controller('auth/google-apis') +@Controller(`${ApiPath.Auth}/google-apis`) @UseFilters(AuthRestApiExceptionFilter) export class GoogleAPIsAuthController { constructor( diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts index d4d41bf41f..1e5a97df07 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts @@ -8,6 +8,7 @@ import { } from '@nestjs/common'; import { Response } from 'express'; +import { ApiPath } from 'twenty-shared/types'; import { AuthOAuthExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-oauth-exception.filter'; import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter'; @@ -19,7 +20,7 @@ import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/worksp import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; -@Controller('auth/google') +@Controller(`${ApiPath.Auth}/google`) @UseFilters(AuthRestApiExceptionFilter) export class GoogleAuthController { constructor(private readonly authService: AuthService) {} diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller.ts index fdd9842258..1adc17d9e3 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller.ts @@ -9,7 +9,7 @@ import { import { InjectRepository } from '@nestjs/typeorm'; import { Response } from 'express'; -import { AppPath, SettingsPath } from 'twenty-shared/types'; +import { ApiPath, AppPath, SettingsPath } from 'twenty-shared/types'; import { getSettingsPath } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; @@ -31,7 +31,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; -@Controller('auth/microsoft-apis') +@Controller(`${ApiPath.Auth}/microsoft-apis`) @UseFilters(AuthRestApiExceptionFilter) export class MicrosoftAPIsAuthController { constructor( diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-auth.controller.ts index 59f9526f31..463249bce0 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-auth.controller.ts @@ -8,6 +8,7 @@ import { } from '@nestjs/common'; import { Response } from 'express'; +import { ApiPath } from 'twenty-shared/types'; import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter'; import { MicrosoftOAuthGuard } from 'src/engine/core-modules/auth/guards/microsoft-oauth.guard'; @@ -18,7 +19,7 @@ import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/worksp import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; -@Controller('auth/microsoft') +@Controller(`${ApiPath.Auth}/microsoft`) @UseFilters(AuthRestApiExceptionFilter) export class MicrosoftAuthController { constructor(private readonly authService: AuthService) {} diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/oauth-propagator.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/oauth-propagator.controller.ts index 758ae412f4..7840aea6c1 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/oauth-propagator.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/oauth-propagator.controller.ts @@ -10,6 +10,7 @@ import { } from '@nestjs/common'; import { Response } from 'express'; +import { ApiPath } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface'; @@ -21,7 +22,7 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; -@Controller('auth/oauth-propagator') +@Controller(`${ApiPath.Auth}/oauth-propagator`) @UseFilters(AuthRestApiExceptionFilter) export class OAuthPropagatorController { constructor( diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts index 01f1b90e18..fbdf442fed 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts @@ -13,7 +13,11 @@ import { InjectRepository } from '@nestjs/typeorm'; import { generateServiceProviderMetadata } from '@node-saml/node-saml'; import { Response } from 'express'; -import { AppPath, ConnectedAccountProvider } from 'twenty-shared/types'; +import { + ApiPath, + AppPath, + ConnectedAccountProvider, +} from 'twenty-shared/types'; import { assertIsDefinedOrThrow } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; @@ -43,7 +47,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; -@Controller('auth') +@Controller(ApiPath.Auth) @UseFilters(AuthRestApiExceptionFilter) export class SSOAuthController { constructor( diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-oauth.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-oauth.guard.ts index d9f04c3942..35079e274e 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-oauth.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-oauth.guard.ts @@ -3,6 +3,7 @@ import { AuthGuard } from '@nestjs/passport'; import { InjectRepository } from '@nestjs/typeorm'; import { type Request } from 'express'; +import { ApiPath } from 'twenty-shared/types'; import { parseJson } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; @@ -82,7 +83,7 @@ export class MicrosoftOAuthGuard extends AuthGuard('microsoft') { return false; } - const url = new URL('/auth/microsoft', 'http://localhost'); + const url = new URL(`/${ApiPath.Auth}/microsoft`, 'http://localhost'); url.searchParams.set('oauthRetryCount', String(oauthRetryCount + 1)); diff --git a/packages/twenty-server/src/engine/core-modules/billing-webhook/billing-webhook.controller.ts b/packages/twenty-server/src/engine/core-modules/billing-webhook/billing-webhook.controller.ts index 6bede63ad4..a065f4a548 100644 --- a/packages/twenty-server/src/engine/core-modules/billing-webhook/billing-webhook.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/billing-webhook/billing-webhook.controller.ts @@ -14,6 +14,7 @@ import { import { type Response } from 'express'; import Stripe from 'stripe'; +import { ApiPath } from 'twenty-shared/types'; import { BillingWebhookCustomerService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-customer.service'; import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-entitlement.service'; @@ -50,7 +51,7 @@ export class BillingWebhookController { private readonly billingWebhookSubscriptionScheduleService: BillingWebhookSubscriptionScheduleService, ) {} - @Post(['webhooks/stripe']) + @Post(`${ApiPath.Webhooks}/stripe`) @UseGuards(PublicEndpointGuard, NoPermissionGuard) async handleWebhooks( @Headers('stripe-signature') signature: string, diff --git a/packages/twenty-server/src/engine/core-modules/billing/app-billing/app-billing.controller.ts b/packages/twenty-server/src/engine/core-modules/billing/app-billing/app-billing.controller.ts index 5be6c58440..19ef1e6e02 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/app-billing/app-billing.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/app-billing/app-billing.controller.ts @@ -15,6 +15,7 @@ import { } from '@nestjs/common'; import { Request } from 'express'; +import { ApiPath } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { AppBillingService } from 'src/engine/core-modules/billing/app-billing/app-billing.service'; @@ -30,7 +31,7 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; const APP_BILLING_CHARGE_THROTTLE_LIMIT = 1000; const APP_BILLING_CHARGE_THROTTLE_TTL_MS = 60_000; -@Controller('app/billing') +@Controller(`${ApiPath.App}/billing`) @UseGuards(JwtAuthGuard, WorkspaceAuthGuard, NoPermissionGuard) export class AppBillingController { constructor( diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.controller.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.controller.ts index e790b59a67..2ef667d772 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.controller.ts @@ -1,11 +1,13 @@ import { Controller, Get, UseGuards } from '@nestjs/common'; +import { ApiPath } from 'twenty-shared/types'; + import { type ClientConfig } from 'src/engine/core-modules/client-config/client-config.entity'; import { ClientConfigService } from 'src/engine/core-modules/client-config/services/client-config.service'; import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; -@Controller('/client-config') +@Controller(ApiPath.ClientConfig) export class ClientConfigController { constructor(private readonly clientConfigService: ClientConfigService) {} diff --git a/packages/twenty-server/src/engine/core-modules/cloudflare/controllers/dns-cloudflare.controller.ts b/packages/twenty-server/src/engine/core-modules/cloudflare/controllers/dns-cloudflare.controller.ts index b572ac2129..75e3e0171a 100644 --- a/packages/twenty-server/src/engine/core-modules/cloudflare/controllers/dns-cloudflare.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/cloudflare/controllers/dns-cloudflare.controller.ts @@ -3,6 +3,7 @@ import { Controller, Post, Req, UseFilters, UseGuards } from '@nestjs/common'; import { Request } from 'express'; +import { ApiPath } from 'twenty-shared/types'; import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter'; import { CloudflareSecretMatchGuard } from 'src/engine/core-modules/cloudflare/guards/cloudflare-secret.guard'; @@ -20,7 +21,10 @@ export class DnsCloudflareController { private readonly twentyConfigService: TwentyConfigService, ) {} - @Post(['cloudflare/custom-hostname-webhooks', 'webhooks/cloudflare']) + @Post([ + `${ApiPath.Cloudflare}/custom-hostname-webhooks`, + `${ApiPath.Webhooks}/cloudflare`, + ]) @UseGuards(CloudflareSecretMatchGuard, PublicEndpointGuard, NoPermissionGuard) async customHostnameWebhooks(@Req() req: Request) { const hostname = req.body?.data?.data?.hostname; diff --git a/packages/twenty-server/src/engine/core-modules/file/controllers/file.controller.ts b/packages/twenty-server/src/engine/core-modules/file/controllers/file.controller.ts index daa8a50f55..f7e19edcba 100644 --- a/packages/twenty-server/src/engine/core-modules/file/controllers/file.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/file/controllers/file.controller.ts @@ -14,7 +14,7 @@ import { join } from 'path'; import { type Readable } from 'stream'; import { Request, Response } from 'express'; -import { FileFolder, ServerFileFolder } from 'twenty-shared/types'; +import { ApiPath, FileFolder, ServerFileFolder } from 'twenty-shared/types'; import { FileStorageException, @@ -51,7 +51,9 @@ export class FileController { // public folder path. These are instance-global marketplace resources, also // displayed on the public OAuth authorize page, hence no auth token, unlike // the workspace-scoped /file/:folder/:id. - @Get('files/application-registrations/:applicationRegistrationId/*path') + @Get( + `${ApiPath.Files}/application-registrations/:applicationRegistrationId/*path`, + ) @UseGuards(PublicEndpointGuard, NoPermissionGuard) async getApplicationRegistrationAsset( @Res() res: Response, @@ -110,7 +112,7 @@ export class FileController { } } - @Get('public-assets/:workspaceId/:applicationId/*path') + @Get(`${ApiPath.PublicAssets}/:workspaceId/:applicationId/*path`) @UseGuards(PublicEndpointGuard, NoPermissionGuard) async getPublicAssets( @Res() res: Response, @@ -183,7 +185,7 @@ export class FileController { } } - @Get('file/:fileFolder/:id') + @Get(`${ApiPath.File}/:fileFolder/:id`) @UseGuards(FileByIdGuard, NoPermissionGuard) async getFileById( @Res() res: Response, diff --git a/packages/twenty-server/src/engine/core-modules/file/file-upload/controllers/file-upload.controller.ts b/packages/twenty-server/src/engine/core-modules/file/file-upload/controllers/file-upload.controller.ts index 4a7605834b..cacc6b9699 100644 --- a/packages/twenty-server/src/engine/core-modules/file/file-upload/controllers/file-upload.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/file/file-upload/controllers/file-upload.controller.ts @@ -9,6 +9,7 @@ import { } from '@nestjs/common'; import { Request, Response } from 'express'; +import { ApiPath } from 'twenty-shared/types'; import { FileUploadApiExceptionFilter } from 'src/engine/core-modules/file/file-upload/filters/file-upload-api-exception.filter'; import { FileUploadTokenGuard } from 'src/engine/core-modules/file/file-upload/guards/file-upload-token.guard'; @@ -23,7 +24,7 @@ export class FileUploadController { // Streaming target for direct uploads when storage has no presigned upload // support (local driver, or S3 without presign enabled). The body is piped // to the storage driver without ever being buffered in memory. - @Put('file-upload/:id') + @Put(`${ApiPath.FileUpload}/:id`) @UseGuards(FileUploadTokenGuard, NoPermissionGuard) async uploadFileById( @Req() req: Request, diff --git a/packages/twenty-server/src/engine/core-modules/file/file-upload/services/file-upload.service.ts b/packages/twenty-server/src/engine/core-modules/file/file-upload/services/file-upload.service.ts index 64578d7685..b132da379c 100644 --- a/packages/twenty-server/src/engine/core-modules/file/file-upload/services/file-upload.service.ts +++ b/packages/twenty-server/src/engine/core-modules/file/file-upload/services/file-upload.service.ts @@ -7,7 +7,7 @@ import { pipeline } from 'stream/promises'; import { msg } from '@lingui/core/macro'; import { isNonEmptyString } from '@sniptt/guards'; import bytes from 'bytes'; -import { FileFolder } from 'twenty-shared/types'; +import { ApiPath, FileFolder } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; import { v4 } from 'uuid'; @@ -173,7 +173,7 @@ export class FileUploadService { return { fileId, - uploadUrl: `${serverUrl}/file-upload/${fileId}?token=${token}`, + uploadUrl: `${serverUrl}/${ApiPath.FileUpload}/${fileId}?token=${token}`, // octet-stream keeps the request body away from the server's json/text // body parsers; the real mime type is already on the file record. contentType: 'application/octet-stream', diff --git a/packages/twenty-server/src/engine/core-modules/health/controllers/health.controller.ts b/packages/twenty-server/src/engine/core-modules/health/controllers/health.controller.ts index 9a31af1436..b79f767a70 100644 --- a/packages/twenty-server/src/engine/core-modules/health/controllers/health.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/health/controllers/health.controller.ts @@ -1,10 +1,11 @@ import { Controller, Get, UseGuards } from '@nestjs/common'; import { HealthCheck, HealthCheckService } from '@nestjs/terminus'; +import { ApiPath } from 'twenty-shared/types'; import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; -@Controller('healthz') +@Controller(ApiPath.Health) export class HealthController { constructor(private readonly health: HealthCheckService) {} diff --git a/packages/twenty-server/src/engine/core-modules/open-api/open-api.controller.ts b/packages/twenty-server/src/engine/core-modules/open-api/open-api.controller.ts index dc262d60a0..9330b8fd3b 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/open-api.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/open-api.controller.ts @@ -1,6 +1,7 @@ import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common'; import { Request, Response } from 'express'; +import { ApiPath } from 'twenty-shared/types'; import { OpenApiService } from 'src/engine/core-modules/open-api/open-api.service'; import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; @@ -10,7 +11,7 @@ import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; export class OpenApiController { constructor(private readonly openApiService: OpenApiService) {} - @Get(['open-api/core', 'rest/open-api/core']) + @Get([`${ApiPath.OpenApi}/core`, `${ApiPath.Rest}/open-api/core`]) @UseGuards(PublicEndpointGuard, NoPermissionGuard) async generateOpenApiSchemaCore( @Req() request: Request, @@ -21,7 +22,7 @@ export class OpenApiController { res.send(data); } - @Get(['open-api/metadata', 'rest/open-api/metadata']) + @Get([`${ApiPath.OpenApi}/metadata`, `${ApiPath.Rest}/open-api/metadata`]) @UseGuards(PublicEndpointGuard, NoPermissionGuard) async generateOpenApiSchemaMetaData( @Req() request: Request, diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/base-schema.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/base-schema.utils.ts index 9dd855b09e..acf4b23e60 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/base-schema.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/base-schema.utils.ts @@ -1,4 +1,5 @@ import { type OpenAPIV3_1 } from 'openapi-types'; +import { ApiPath } from 'twenty-shared/types'; import { computeOpenApiPath } from 'src/engine/core-modules/open-api/utils/path.utils'; @@ -121,7 +122,7 @@ hand the file to your tool — never paste a tokenized URL into a chat: \`\`\`bash curl -H 'Authorization: Bearer ' \\ - ${serverUrl}/rest/open-api/${schemaName} > twenty-${schemaName}.json + ${serverUrl}/${ApiPath.Rest}/open-api/${schemaName} > twenty-${schemaName}.json \`\`\` `, termsOfService: @@ -138,7 +139,7 @@ curl -H 'Authorization: Bearer ' \\ // Testing purposes servers: [ { - url: `${serverUrl}/rest/${schemaName !== 'core' ? schemaName : ''}`, + url: `${serverUrl}/${ApiPath.Rest}/${schemaName !== 'core' ? schemaName : ''}`, description: 'Production Development', }, ], diff --git a/packages/twenty-server/src/engine/core-modules/sdk-client/controllers/sdk-client.controller.ts b/packages/twenty-server/src/engine/core-modules/sdk-client/controllers/sdk-client.controller.ts index bea09127d4..24d2b3b2df 100644 --- a/packages/twenty-server/src/engine/core-modules/sdk-client/controllers/sdk-client.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/sdk-client/controllers/sdk-client.controller.ts @@ -8,6 +8,7 @@ import { } from '@nestjs/common'; import { Response } from 'express'; +import { ApiPath } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { @@ -26,7 +27,7 @@ import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; -@Controller('rest/sdk-client') +@Controller(`${ApiPath.Rest}/sdk-client`) @UseGuards(WorkspaceAuthGuard) export class SdkClientController { constructor( diff --git a/packages/twenty-server/src/engine/core-modules/server-route-trigger/server-route-trigger.controller.ts b/packages/twenty-server/src/engine/core-modules/server-route-trigger/server-route-trigger.controller.ts index 3b62bc9f5c..75e80853bc 100644 --- a/packages/twenty-server/src/engine/core-modules/server-route-trigger/server-route-trigger.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/server-route-trigger/server-route-trigger.controller.ts @@ -9,6 +9,7 @@ import { } from '@nestjs/common'; import { Request, Response } from 'express'; +import { ApiPath } from 'twenty-shared/types'; import { sendRouteTriggerResponse } from 'src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/utils/route-trigger-response.util'; import { ServerRouteTriggerRestApiExceptionFilter } from 'src/engine/core-modules/server-route-trigger/exceptions/server-route-trigger-rest-api-exception-filter'; @@ -16,7 +17,7 @@ import { ServerRouteTriggerService } from 'src/engine/core-modules/server-route- import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; -@Controller('webhooks/server') +@Controller(`${ApiPath.Webhooks}/server`) @UseGuards(PublicEndpointGuard, NoPermissionGuard) @UseFilters(ServerRouteTriggerRestApiExceptionFilter) export class ServerRouteTriggerController { diff --git a/packages/twenty-server/src/engine/core-modules/well-known/controllers/well-known.controller.ts b/packages/twenty-server/src/engine/core-modules/well-known/controllers/well-known.controller.ts index ad940fec8c..b5ef230d66 100644 --- a/packages/twenty-server/src/engine/core-modules/well-known/controllers/well-known.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/well-known/controllers/well-known.controller.ts @@ -1,6 +1,7 @@ import { Controller, Get, Header, Req, UseGuards } from '@nestjs/common'; import { type Request } from 'express'; +import { ApiPath } from 'twenty-shared/types'; import { buildApiCatalog } from 'src/engine/core-modules/well-known/utils/build-api-catalog.util'; import { buildMcpServerCard } from 'src/engine/core-modules/well-known/utils/build-mcp-server-card.util'; @@ -13,7 +14,7 @@ import { extractVersionMajorMinorPatch } from 'src/utils/version/extract-version const DISCOVERY_CACHE_CONTROL = 'public, max-age=3600'; const FALLBACK_SERVER_VERSION = '0.0.0'; -@Controller('.well-known') +@Controller(ApiPath.WellKnown) export class WellKnownController { constructor(private readonly twentyConfigService: TwentyConfigService) {} diff --git a/packages/twenty-server/src/engine/core-modules/well-known/utils/build-api-catalog.util.ts b/packages/twenty-server/src/engine/core-modules/well-known/utils/build-api-catalog.util.ts index 4725ce2d52..f0ed135fe3 100644 --- a/packages/twenty-server/src/engine/core-modules/well-known/utils/build-api-catalog.util.ts +++ b/packages/twenty-server/src/engine/core-modules/well-known/utils/build-api-catalog.util.ts @@ -1,4 +1,5 @@ import { DOCUMENTATION_BASE_URL } from 'twenty-shared/constants'; +import { ApiPath } from 'twenty-shared/types'; const API_DOCS_URL = `${DOCUMENTATION_BASE_URL}/developers/extend/api`; const MCP_DOCS_URL = `${DOCUMENTATION_BASE_URL}/user-guide/ai/capabilities/mcp`; @@ -8,34 +9,40 @@ const MCP_DOCS_URL = `${DOCUMENTATION_BASE_URL}/user-guide/ai/capabilities/mcp`; export const buildApiCatalog = (baseUrl: string) => ({ linkset: [ { - anchor: `${baseUrl}/rest`, + anchor: `${baseUrl}/${ApiPath.Rest}`, 'service-desc': [ - { href: `${baseUrl}/rest/open-api/core`, type: 'application/json' }, + { + href: `${baseUrl}/${ApiPath.Rest}/open-api/core`, + type: 'application/json', + }, ], 'service-doc': [{ href: API_DOCS_URL, type: 'text/html' }], 'service-meta': [ { - href: `${baseUrl}/.well-known/oauth-protected-resource`, + href: `${baseUrl}/${ApiPath.WellKnown}/oauth-protected-resource`, type: 'application/json', }, ], }, { - anchor: `${baseUrl}/rest/metadata`, + anchor: `${baseUrl}/${ApiPath.Rest}/metadata`, 'service-desc': [ - { href: `${baseUrl}/rest/open-api/metadata`, type: 'application/json' }, + { + href: `${baseUrl}/${ApiPath.Rest}/open-api/metadata`, + type: 'application/json', + }, ], 'service-doc': [{ href: API_DOCS_URL, type: 'text/html' }], }, { - anchor: `${baseUrl}/graphql`, + anchor: `${baseUrl}/${ApiPath.GraphQL}`, 'service-doc': [{ href: API_DOCS_URL, type: 'text/html' }], }, { - anchor: `${baseUrl}/mcp`, + anchor: `${baseUrl}/${ApiPath.Mcp}`, 'service-desc': [ { - href: `${baseUrl}/.well-known/mcp/server-card.json`, + href: `${baseUrl}/${ApiPath.WellKnown}/mcp/server-card.json`, type: 'application/json', }, ], diff --git a/packages/twenty-server/src/engine/core-modules/workflow/controllers/workflow-trigger.controller.ts b/packages/twenty-server/src/engine/core-modules/workflow/controllers/workflow-trigger.controller.ts index 008440c617..3fb6532eb5 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/controllers/workflow-trigger.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/controllers/workflow-trigger.controller.ts @@ -10,7 +10,7 @@ import { import { InjectRepository } from '@nestjs/typeorm'; import { Request } from 'express'; -import { FieldActorSource } from 'twenty-shared/types'; +import { ApiPath, FieldActorSource } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; @@ -37,7 +37,7 @@ import { import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service'; -@Controller('webhooks') +@Controller(ApiPath.Webhooks) @UseFilters( WorkflowTriggerRestApiExceptionFilter, PermissionsGraphqlApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-generate-text/controllers/ai-generate-text.controller.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-generate-text/controllers/ai-generate-text.controller.ts index d6eaed1ad2..27fe4e0100 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-generate-text/controllers/ai-generate-text.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-generate-text/controllers/ai-generate-text.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Post, UseFilters, UseGuards } from '@nestjs/common'; import { generateText } from 'ai'; import { PermissionFlagType } from 'twenty-shared/constants'; +import { ApiPath } from 'twenty-shared/types'; import { RestApiExceptionFilter } from 'src/engine/api/rest/rest-api-exception.filter'; import { BillingUsageService } from 'src/engine/core-modules/billing/services/billing-usage.service'; @@ -22,7 +23,7 @@ import { GenerateTextInput } from 'src/engine/metadata-modules/ai/ai-generate-te import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service'; import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; -@Controller('rest/ai') +@Controller(`${ApiPath.Rest}/ai`) @UseGuards(JwtAuthGuard, WorkspaceAuthGuard) @UseFilters( PermissionsRestApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/controllers/field-metadata.controller.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/controllers/field-metadata.controller.ts index 62b29bfb34..c736baef15 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/controllers/field-metadata.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/controllers/field-metadata.controller.ts @@ -17,7 +17,7 @@ import { import { InjectRepository } from '@nestjs/typeorm'; import { PermissionFlagType } from 'twenty-shared/constants'; -import { FeatureFlagKey } from 'twenty-shared/types'; +import { ApiPath, FeatureFlagKey } from 'twenty-shared/types'; import { Repository } from 'typeorm'; import { parseEndingBeforeRestRequest } from 'src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util'; @@ -59,7 +59,7 @@ import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-mod import { computeUniqueFieldMetadataIdsFromFlatIndexMaps } from 'src/engine/metadata-modules/index-metadata/utils/compute-unique-field-metadata-ids-from-flat-index-maps.util'; import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; -@Controller('rest/metadata/fields') +@Controller(`${ApiPath.Rest}/metadata/fields`) @UseGuards( JwtAuthGuard, WorkspaceAuthGuard, diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/controllers/front-component.controller.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/controllers/front-component.controller.ts index 7b76f3473d..3bc5cd7719 100644 --- a/packages/twenty-server/src/engine/metadata-modules/front-component/controllers/front-component.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/front-component/controllers/front-component.controller.ts @@ -11,7 +11,7 @@ import { import { pipeline } from 'stream/promises'; import { Response } from 'express'; -import { FileFolder } from 'twenty-shared/types'; +import { ApiPath, FileFolder } from 'twenty-shared/types'; import { FileStorageException, @@ -34,7 +34,7 @@ import { FrontComponentService } from 'src/engine/metadata-modules/front-compone import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/workspace-manager/workspace-migration/filters/workspace-migration-runner-rest-api-exception.filter'; -@Controller('rest/front-components') +@Controller(`${ApiPath.Rest}/front-components`) @UseGuards(WorkspaceAuthGuard) @UseFilters( PermissionsRestApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/controllers/object-metadata.controller.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/controllers/object-metadata.controller.ts index 56c846fc5e..a626a1db94 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/controllers/object-metadata.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/controllers/object-metadata.controller.ts @@ -17,7 +17,7 @@ import { import { InjectRepository } from '@nestjs/typeorm'; import { PermissionFlagType } from 'twenty-shared/constants'; -import { FeatureFlagKey } from 'twenty-shared/types'; +import { ApiPath, FeatureFlagKey } from 'twenty-shared/types'; import { In, Repository } from 'typeorm'; import { parseEndingBeforeRestRequest } from 'src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util'; @@ -61,7 +61,7 @@ import { } from 'src/engine/metadata-modules/object-metadata/utils/to-legacy-object-metadata-response.util'; import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; -@Controller('rest/metadata/objects') +@Controller(`${ApiPath.Rest}/metadata/objects`) @UseGuards( JwtAuthGuard, WorkspaceAuthGuard, diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/controllers/page-layout-tab.controller.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/controllers/page-layout-tab.controller.ts index 45548c427f..0317ee095a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/controllers/page-layout-tab.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/controllers/page-layout-tab.controller.ts @@ -12,6 +12,7 @@ import { } from '@nestjs/common'; import { PermissionFlagType } from 'twenty-shared/constants'; +import { ApiPath } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -34,7 +35,7 @@ import { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout-ta import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/workspace-manager/workspace-migration/filters/workspace-migration-runner-rest-api-exception.filter'; -@Controller('rest/metadata/pageLayoutTabs') +@Controller(`${ApiPath.Rest}/metadata/pageLayoutTabs`) @UseGuards(WorkspaceAuthGuard) @UseFilters( PermissionsRestApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/controllers/page-layout-widget.controller.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/controllers/page-layout-widget.controller.ts index 14e105094f..44cc37418e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/controllers/page-layout-widget.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/controllers/page-layout-widget.controller.ts @@ -13,6 +13,7 @@ import { import { isDefined } from 'class-validator'; import { PermissionFlagType } from 'twenty-shared/constants'; +import { ApiPath } from 'twenty-shared/types'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; @@ -34,7 +35,7 @@ import { PageLayoutWidgetService } from 'src/engine/metadata-modules/page-layout import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/workspace-manager/workspace-migration/filters/workspace-migration-runner-rest-api-exception.filter'; -@Controller('rest/metadata/pageLayoutWidgets') +@Controller(`${ApiPath.Rest}/metadata/pageLayoutWidgets`) @UseGuards(WorkspaceAuthGuard) @UseFilters( PermissionsRestApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/controllers/page-layout.controller.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/controllers/page-layout.controller.ts index bb902c02be..a969a6e7dc 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/controllers/page-layout.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/controllers/page-layout.controller.ts @@ -12,6 +12,7 @@ import { } from '@nestjs/common'; import { PermissionFlagType } from 'twenty-shared/constants'; +import { ApiPath } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -29,7 +30,7 @@ import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/servi import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/workspace-manager/workspace-migration/filters/workspace-migration-runner-rest-api-exception.filter'; -@Controller('rest/metadata/pageLayouts') +@Controller(`${ApiPath.Rest}/metadata/pageLayouts`) @UseGuards(WorkspaceAuthGuard) @UseFilters( PermissionsRestApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.controller.ts b/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.controller.ts index eddeaeff98..0d4a795020 100644 --- a/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.controller.ts @@ -12,7 +12,7 @@ import { } from '@nestjs/common'; import { Request, Response } from 'express'; -import { HTTPMethod } from 'twenty-shared/types'; +import { ApiPath, HTTPMethod } from 'twenty-shared/types'; import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; @@ -20,7 +20,7 @@ import { RouteTriggerRestApiExceptionFilter } from 'src/engine/core-modules/logi import { RouteTriggerService } from 'src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/route-trigger.service'; import { sendRouteTriggerResponse } from 'src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/utils/route-trigger-response.util'; -@Controller('s') +@Controller(ApiPath.RouteTrigger) @UseGuards(PublicEndpointGuard, NoPermissionGuard) @UseFilters(RouteTriggerRestApiExceptionFilter) export class RouteTriggerController { diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field/controllers/view-field.controller.ts b/packages/twenty-server/src/engine/metadata-modules/view-field/controllers/view-field.controller.ts index cb9cf21d17..64607abefe 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-field/controllers/view-field.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-field/controllers/view-field.controller.ts @@ -11,6 +11,7 @@ import { UseGuards, } from '@nestjs/common'; +import { ApiPath } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -36,7 +37,7 @@ import { DeleteViewFieldPermissionGuard } from 'src/engine/metadata-modules/view import { UpdateViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/update-view-field-permission.guard'; import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/workspace-manager/workspace-migration/filters/workspace-migration-runner-rest-api-exception.filter'; -@Controller('rest/metadata/viewFields') +@Controller(`${ApiPath.Rest}/metadata/viewFields`) @UseGuards(WorkspaceAuthGuard) @UseFilters( PermissionsRestApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/view-filter-group/controllers/view-filter-group.controller.ts b/packages/twenty-server/src/engine/metadata-modules/view-filter-group/controllers/view-filter-group.controller.ts index c3931e7fc0..7463dd65d5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-filter-group/controllers/view-filter-group.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-filter-group/controllers/view-filter-group.controller.ts @@ -11,6 +11,7 @@ import { UseGuards, } from '@nestjs/common'; +import { ApiPath } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -36,7 +37,7 @@ import { DeleteViewFilterGroupPermissionGuard } from 'src/engine/metadata-module import { UpdateViewFilterGroupPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/update-view-filter-group-permission.guard'; import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/workspace-manager/workspace-migration/filters/workspace-migration-runner-rest-api-exception.filter'; -@Controller('rest/metadata/viewFilterGroups') +@Controller(`${ApiPath.Rest}/metadata/viewFilterGroups`) @UseGuards(WorkspaceAuthGuard) @UseFilters( PermissionsRestApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/view-filter/controllers/view-filter.controller.ts b/packages/twenty-server/src/engine/metadata-modules/view-filter/controllers/view-filter.controller.ts index 0859ed8fe3..c3f7bb920d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-filter/controllers/view-filter.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-filter/controllers/view-filter.controller.ts @@ -11,6 +11,7 @@ import { UseGuards, } from '@nestjs/common'; +import { ApiPath } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -36,7 +37,7 @@ import { DeleteViewFilterPermissionGuard } from 'src/engine/metadata-modules/vie import { UpdateViewFilterPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/update-view-filter-permission.guard'; import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/workspace-manager/workspace-migration/filters/workspace-migration-runner-rest-api-exception.filter'; -@Controller('rest/metadata/viewFilters') +@Controller(`${ApiPath.Rest}/metadata/viewFilters`) @UseGuards(WorkspaceAuthGuard) @UseFilters( PermissionsRestApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/view-group/controllers/view-group.controller.ts b/packages/twenty-server/src/engine/metadata-modules/view-group/controllers/view-group.controller.ts index e912d41eef..cb35b86548 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-group/controllers/view-group.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-group/controllers/view-group.controller.ts @@ -11,6 +11,7 @@ import { UseGuards, } from '@nestjs/common'; +import { ApiPath } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -36,7 +37,7 @@ import { DeleteViewGroupPermissionGuard } from 'src/engine/metadata-modules/view import { UpdateViewGroupPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/update-view-group-permission.guard'; import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/workspace-manager/workspace-migration/filters/workspace-migration-runner-rest-api-exception.filter'; -@Controller('rest/metadata/viewGroups') +@Controller(`${ApiPath.Rest}/metadata/viewGroups`) @UseGuards(WorkspaceAuthGuard) @UseFilters( PermissionsRestApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/view-sort/controllers/view-sort.controller.ts b/packages/twenty-server/src/engine/metadata-modules/view-sort/controllers/view-sort.controller.ts index 5da00cd433..d5f8949db5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-sort/controllers/view-sort.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-sort/controllers/view-sort.controller.ts @@ -11,6 +11,7 @@ import { UseGuards, } from '@nestjs/common'; +import { ApiPath, ViewSortDirection } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -34,9 +35,7 @@ import { import { ViewSortRestApiExceptionFilter } from 'src/engine/metadata-modules/view-sort/filters/view-sort-rest-api-exception.filter'; import { ViewSortService } from 'src/engine/metadata-modules/view-sort/services/view-sort.service'; import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/workspace-manager/workspace-migration/filters/workspace-migration-runner-rest-api-exception.filter'; -import { ViewSortDirection } from 'twenty-shared/types'; - -@Controller('rest/metadata/viewSorts') +@Controller(`${ApiPath.Rest}/metadata/viewSorts`) @UseGuards(WorkspaceAuthGuard) @UseFilters( PermissionsRestApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/view/controllers/view.controller.ts b/packages/twenty-server/src/engine/metadata-modules/view/controllers/view.controller.ts index f068718a9e..3c127244e9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/controllers/view.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/controllers/view.controller.ts @@ -12,6 +12,7 @@ import { } from '@nestjs/common'; import { type APP_LOCALES } from 'twenty-shared/translations'; +import { ApiPath } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; @@ -45,7 +46,7 @@ import { FlatEntityMapsRestApiExceptionFilter } from 'src/engine/metadata-module import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/workspace-manager/workspace-migration/filters/workspace-migration-runner-rest-api-exception.filter'; -@Controller('rest/metadata/views') +@Controller(`${ApiPath.Rest}/metadata/views`) @UseGuards(WorkspaceAuthGuard) @UseFilters( PermissionsRestApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/webhook/controllers/webhook.controller.ts b/packages/twenty-server/src/engine/metadata-modules/webhook/controllers/webhook.controller.ts index 5758d50eec..465842c4ca 100644 --- a/packages/twenty-server/src/engine/metadata-modules/webhook/controllers/webhook.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/webhook/controllers/webhook.controller.ts @@ -11,6 +11,7 @@ import { } from '@nestjs/common'; import { PermissionFlagType } from 'twenty-shared/constants'; +import { ApiPath } from 'twenty-shared/types'; import { RestApiExceptionFilter } from 'src/engine/api/rest/rest-api-exception.filter'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -26,7 +27,7 @@ import { type WebhookDTO } from 'src/engine/metadata-modules/webhook/dtos/webhoo import { WebhookService } from 'src/engine/metadata-modules/webhook/webhook.service'; import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/workspace-manager/workspace-migration/filters/workspace-migration-runner-rest-api-exception.filter'; -@Controller(['rest/webhooks', 'rest/metadata/webhooks']) +@Controller([`${ApiPath.Rest}/webhooks`, `${ApiPath.Rest}/metadata/webhooks`]) @UseGuards( JwtAuthGuard, WorkspaceAuthGuard, diff --git a/packages/twenty-server/src/engine/utils/render-apollo-playground.util.ts b/packages/twenty-server/src/engine/utils/render-apollo-playground.util.ts index f33d866b38..ee9b95e169 100644 --- a/packages/twenty-server/src/engine/utils/render-apollo-playground.util.ts +++ b/packages/twenty-server/src/engine/utils/render-apollo-playground.util.ts @@ -1,9 +1,11 @@ +import { ApiPath } from 'twenty-shared/types'; + interface ApolloPlaygroundOptions { - path?: string; + path?: ApiPath; } export const renderApolloPlayground = ({ - path = 'graphql', + path = ApiPath.GraphQL, }: ApolloPlaygroundOptions = {}) => { return ` diff --git a/packages/twenty-server/src/main.ts b/packages/twenty-server/src/main.ts index 3dbbd994a7..1d3f7e338c 100644 --- a/packages/twenty-server/src/main.ts +++ b/packages/twenty-server/src/main.ts @@ -8,6 +8,7 @@ import bytes from 'bytes'; import { useContainer } from 'class-validator'; import session from 'express-session'; import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; +import { ApiPath } from 'twenty-shared/types'; import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface'; @@ -84,7 +85,7 @@ const bootstrap = async () => { // Graphql file upload app.use( - '/graphql', + `/${ApiPath.GraphQL}`, graphqlUploadExpress({ maxFieldSize: bytes(settings.storage.maxFileSize)!, maxFiles: 10, @@ -92,7 +93,7 @@ const bootstrap = async () => { ); app.use( - '/metadata', + `/${ApiPath.Metadata}`, graphqlUploadExpress({ maxFieldSize: bytes(settings.storage.maxFileSize)!, maxFiles: 10, diff --git a/packages/twenty-server/src/modules/connected-account-sync-webhooks/connected-account-sync-webhooks.controller.ts b/packages/twenty-server/src/modules/connected-account-sync-webhooks/connected-account-sync-webhooks.controller.ts index d175a244b7..9b5665e70e 100644 --- a/packages/twenty-server/src/modules/connected-account-sync-webhooks/connected-account-sync-webhooks.controller.ts +++ b/packages/twenty-server/src/modules/connected-account-sync-webhooks/connected-account-sync-webhooks.controller.ts @@ -12,6 +12,7 @@ import { } from '@nestjs/common'; import { type Response } from 'express'; +import { ApiPath } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { escapeHtml } from 'src/engine/core-modules/emailing-domain/utils/escape-html.util'; @@ -37,7 +38,7 @@ export class ConnectedAccountSyncWebhooksController { private readonly microsoftCalendarNotificationHandler: MicrosoftCalendarNotificationHandler, ) {} - @Post('webhooks/google/messaging') + @Post(`${ApiPath.Webhooks}/google/messaging`) @HttpCode(HttpStatus.OK) async handleGoogleMessaging( @Body() body: GooglePubSubPushMessage, @@ -49,7 +50,7 @@ export class ConnectedAccountSyncWebhooksController { }); } - @Post('webhooks/google/calendar') + @Post(`${ApiPath.Webhooks}/google/calendar`) @HttpCode(HttpStatus.OK) async handleGoogleCalendar( @Headers('x-goog-channel-id') channelId: string | undefined, @@ -63,7 +64,7 @@ export class ConnectedAccountSyncWebhooksController { }); } - @Post('webhooks/microsoft/messaging') + @Post(`${ApiPath.Webhooks}/microsoft/messaging`) @HttpCode(HttpStatus.OK) async handleMicrosoftMessaging( @Body() body: MicrosoftGraphNotificationPayload, @@ -79,7 +80,7 @@ export class ConnectedAccountSyncWebhooksController { return ''; } - @Post('webhooks/microsoft/calendar') + @Post(`${ApiPath.Webhooks}/microsoft/calendar`) @HttpCode(HttpStatus.OK) async handleMicrosoftCalendar( @Body() body: MicrosoftGraphNotificationPayload, diff --git a/packages/twenty-server/src/modules/connected-account/webhook-subscription-manager/drivers/microsoft/microsoft-webhook-subscription.driver.ts b/packages/twenty-server/src/modules/connected-account/webhook-subscription-manager/drivers/microsoft/microsoft-webhook-subscription.driver.ts index 8d7af1a70c..780aca3257 100644 --- a/packages/twenty-server/src/modules/connected-account/webhook-subscription-manager/drivers/microsoft/microsoft-webhook-subscription.driver.ts +++ b/packages/twenty-server/src/modules/connected-account/webhook-subscription-manager/drivers/microsoft/microsoft-webhook-subscription.driver.ts @@ -2,10 +2,9 @@ import { Injectable } from '@nestjs/common'; import { type GraphError } from '@microsoft/microsoft-graph-client'; import { type Subscription } from '@microsoft/microsoft-graph-types'; +import { ApiPath, WebhookSubscriptionChannelType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { WebhookSubscriptionChannelType } from 'twenty-shared/types'; - import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { MICROSOFT_SUBSCRIPTION_TTL_MS } from 'src/modules/connected-account/webhook-subscription-manager/drivers/microsoft/constants/microsoft-subscription-ttl-ms.constant'; import { @@ -35,12 +34,12 @@ const MICROSOFT_GRAPH_RESOURCE_CONFIG_BY_CHANNEL_TYPE: Record< [WebhookSubscriptionChannelType.MESSAGING]: { resource: '/me/messages', changeType: 'created,updated', - notificationPath: 'webhooks/microsoft/messaging', + notificationPath: `${ApiPath.Webhooks}/microsoft/messaging`, }, [WebhookSubscriptionChannelType.CALENDAR]: { resource: '/me/events', changeType: 'created,updated,deleted', - notificationPath: 'webhooks/microsoft/calendar', + notificationPath: `${ApiPath.Webhooks}/microsoft/calendar`, }, }; diff --git a/packages/twenty-server/src/modules/dashboard/controllers/dashboard.controller.ts b/packages/twenty-server/src/modules/dashboard/controllers/dashboard.controller.ts index 296e37a79e..cb7dd79ba2 100644 --- a/packages/twenty-server/src/modules/dashboard/controllers/dashboard.controller.ts +++ b/packages/twenty-server/src/modules/dashboard/controllers/dashboard.controller.ts @@ -1,5 +1,7 @@ import { Controller, Param, Post, UseFilters, UseGuards } from '@nestjs/common'; +import { ApiPath } from 'twenty-shared/types'; + import { getWorkspaceAuthContext } from 'src/engine/core-modules/auth/storage/workspace-auth-context.storage'; import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard'; import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; @@ -8,7 +10,7 @@ import { DuplicatedDashboardDTO } from 'src/modules/dashboard/dtos/duplicated-da import { DashboardRestApiExceptionFilter } from 'src/modules/dashboard/filters/dashboard-rest-api-exception.filter'; import { DashboardDuplicationService } from 'src/modules/dashboard/services/dashboard-duplication.service'; -@Controller('rest/dashboards') +@Controller(`${ApiPath.Rest}/dashboards`) @UseGuards(JwtAuthGuard, WorkspaceAuthGuard, NoPermissionGuard) @UseFilters(DashboardRestApiExceptionFilter) export class DashboardController { diff --git a/packages/twenty-server/src/modules/emailing/controllers/unsubscribe.controller.ts b/packages/twenty-server/src/modules/emailing/controllers/unsubscribe.controller.ts index e83e8b0a0b..e3e2697112 100644 --- a/packages/twenty-server/src/modules/emailing/controllers/unsubscribe.controller.ts +++ b/packages/twenty-server/src/modules/emailing/controllers/unsubscribe.controller.ts @@ -11,6 +11,7 @@ import { } from '@nestjs/common'; import { isNonEmptyString } from '@sniptt/guards'; +import { ApiPath } from 'twenty-shared/types'; import { UnsubscribeTokenService } from 'src/engine/core-modules/emailing-domain/services/unsubscribe-token.service'; import { MessageSuppressionReason } from 'src/engine/core-modules/emailing-domain/types/message-suppression-reason.type'; @@ -24,8 +25,8 @@ import { MessageSuppressionService } from 'src/modules/emailing/services/message const UNSUBSCRIBE_TOKEN_FORMAT = /^[A-Za-z0-9_-]{1,1024}$/; -const UPDATE_PREFERENCES_PATH = '/emailing/unsubscribe/preferences'; -const UNSUBSCRIBE_ALL_PATH = '/emailing/unsubscribe/all'; +const UPDATE_PREFERENCES_PATH = `/${ApiPath.Emailing}/unsubscribe/preferences`; +const UNSUBSCRIBE_ALL_PATH = `/${ApiPath.Emailing}/unsubscribe/all`; const HTML_CONTENT_TYPE = 'text/html; charset=utf-8'; @@ -39,7 +40,7 @@ type UnsubscribeFormBody = { unsubscribeTopicId?: string | string[]; }; -@Controller('emailing/unsubscribe') +@Controller(`${ApiPath.Emailing}/unsubscribe`) @UseGuards(PublicEndpointGuard, NoPermissionGuard) export class UnsubscribeController { constructor( diff --git a/packages/twenty-server/src/modules/messaging-webhooks/messaging-webhooks.controller.ts b/packages/twenty-server/src/modules/messaging-webhooks/messaging-webhooks.controller.ts index f86a18bcc9..49be8b69f9 100644 --- a/packages/twenty-server/src/modules/messaging-webhooks/messaging-webhooks.controller.ts +++ b/packages/twenty-server/src/modules/messaging-webhooks/messaging-webhooks.controller.ts @@ -9,6 +9,7 @@ import { } from '@nestjs/common'; import { type Request } from 'express'; +import { ApiPath } from 'twenty-shared/types'; import { MessagingWebhookApiExceptionFilter } from 'src/modules/messaging-webhooks/filters/messaging-webhook-api-exception.filter'; import { MessagingWebhookExceptionCode } from 'src/modules/messaging-webhooks/messaging-webhook-exception-code.enum'; @@ -27,7 +28,7 @@ export class MessagingWebhooksController { private readonly sesOutboundWebhookRouterService: SesOutboundWebhookRouterService, ) {} - @Post(['webhooks/messaging/ses/inbound']) + @Post(`${ApiPath.Webhooks}/messaging/ses/inbound`) @UseGuards(PublicEndpointGuard, NoPermissionGuard) @HttpCode(200) async handleSesInboundWebhook( @@ -43,7 +44,7 @@ export class MessagingWebhooksController { await this.sesInboundWebhookRouterService.route(request.rawBody); } - @Post(['webhooks/messaging/ses/outbound']) + @Post(`${ApiPath.Webhooks}/messaging/ses/outbound`) @UseGuards(PublicEndpointGuard, NoPermissionGuard) @HttpCode(200) async handleSesOutboundWebhook( diff --git a/packages/twenty-shared/src/types/ApiPath.ts b/packages/twenty-shared/src/types/ApiPath.ts new file mode 100644 index 0000000000..f483886f38 --- /dev/null +++ b/packages/twenty-shared/src/types/ApiPath.ts @@ -0,0 +1,26 @@ +// Adding or renaming a value here also requires updating the nginx ingress rules +// in the infra repo, which route these prefixes to the server instead of the front. +export enum ApiPath { + AdminPanel = 'admin-panel', + App = 'app', + ApplicationRegistrationClaim = 'application-registration-claim', + Apps = 'apps', + Auth = 'auth', + ClientConfig = 'client-config', + Cloudflare = 'cloudflare', + Emailing = 'emailing', + File = 'file', + FileUpload = 'file-upload', + Files = 'files', + GraphQL = 'graphql', + Health = 'healthz', + Mcp = 'mcp', + Metadata = 'metadata', + OAuth = 'oauth', + OpenApi = 'open-api', + PublicAssets = 'public-assets', + Rest = 'rest', + RouteTrigger = 's', + Webhooks = 'webhooks', + WellKnown = '.well-known', +} diff --git a/packages/twenty-shared/src/types/AppBasePath.ts b/packages/twenty-shared/src/types/AppBasePath.ts index a9ae5f7579..404cdf4bea 100644 --- a/packages/twenty-shared/src/types/AppBasePath.ts +++ b/packages/twenty-shared/src/types/AppBasePath.ts @@ -1,5 +1,4 @@ export enum AppBasePath { - Auth = '/auth', Settings = '/settings', Root = '/', } diff --git a/packages/twenty-shared/src/types/__tests__/apiPathCollisions.spec.ts b/packages/twenty-shared/src/types/__tests__/apiPathCollisions.spec.ts new file mode 100644 index 0000000000..da6541b908 --- /dev/null +++ b/packages/twenty-shared/src/types/__tests__/apiPathCollisions.spec.ts @@ -0,0 +1,29 @@ +import { ApiPath } from '@/types/ApiPath'; +import { AppBasePath } from '@/types/AppBasePath'; +import { AppPath } from '@/types/AppPath'; + +const apiPaths = new Set(Object.values(ApiPath)); + +const getFirstPathSegment = (path: string) => + path.replace(/^\//, '').split('/')[0]; + +const frontRoutes = [...Object.values(AppPath), ...Object.values(AppBasePath)]; + +const frontRoutesWithSegment = frontRoutes + .map((frontRoute) => ({ + frontRoute, + firstPathSegment: getFirstPathSegment(frontRoute), + })) + .filter( + ({ firstPathSegment }) => + firstPathSegment !== '' && firstPathSegment !== '*', + ); + +describe('ApiPath and front route collisions', () => { + it.each(frontRoutesWithSegment)( + 'should not serve the front route $frontRoute from a path the server owns', + ({ firstPathSegment }) => { + expect(apiPaths.has(firstPathSegment)).toBe(false); + }, + ); +}); diff --git a/packages/twenty-shared/src/types/index.ts b/packages/twenty-shared/src/types/index.ts index 02307eb900..7b5c68c92c 100644 --- a/packages/twenty-shared/src/types/index.ts +++ b/packages/twenty-shared/src/types/index.ts @@ -11,6 +11,7 @@ export type { AllowedAddressSubField } from './AddressFieldsType'; export { ALLOWED_ADDRESS_SUBFIELDS } from './AddressFieldsType'; export { AggregateOperations } from './AggregateOperations'; export type { AllowedFullNameSortSubField } from './AllowedFullNameSortSubField'; +export { ApiPath } from './ApiPath'; export { AppBasePath } from './AppBasePath'; export { AppPath } from './AppPath'; export type { Arrayable } from './Arrayable';