Proxy API routes through the vite dev server to keep local dev same-origin (#23779)
Replaces #23774 (closed), rebased on latest main. ## Problem Since the cookie-session migration (#23642), the front sends every request with `credentials: 'include'` and the server only reflects `Access-Control-Allow-Origin` for the exact origins in the credentialed allowlist (`SERVER_URL`, `FRONTEND_URL`, `AUTH_COOKIE_ALLOWED_ORIGINS`). Any other origin gets the `*` wildcard, which browsers reject for credentialed requests. Local dev is split-origin by default (front on `localhost:3001`, API on `localhost:3000`), and with `IS_MULTIWORKSPACE_ENABLED` every workspace subdomain (`apple.localhost:3001`, ...) is yet another origin. Each locally created workspace would need a manual `AUTH_COOKIE_ALLOWED_ORIGINS` entry. ## Solution Make local dev same-origin instead of widening the CORS policy: the vite dev server now proxies all top-level API route prefixes to the backend, and the front calls its own origin. - `vite.config.ts` adds a `server.proxy` covering the backend's top-level prefixes (`/graphql`, `/metadata`, `/admin-panel`, `/auth`, `/rest`, `/file`, `/client-config`, ...), defined in `src/config/apiProxyPrefixes.ts`. Keys are anchored regexes (`^/auth($|[/?])`) so SPA routes sharing a prefix (`/authorize`, `/settings`) are not swallowed. The target defaults to `http://localhost:3000` and follows `REACT_APP_SERVER_BASE_URL`. `changeOrigin` stays off so the backend sees the browser's Host: same-origin checks (CSRF, cookie issuance) and workspace resolution by subdomain work unchanged through the proxy. - `config/index.ts` collapses to `window._env_?.REACT_APP_SERVER_BASE_URL || window.location.origin`. Every supported production path injects `window._env_` (docker entrypoint fails hard without `REACT_APP_SERVER_BASE_URL`; a server-served front gets it from `generateFrontConfig()`), and in dev the current origin is correct on `localhost:3001` and every `*.localhost:3001` workspace subdomain thanks to the proxy. The removed `http://<hostname>:3000` fallback only served an un-injected production bundle browsed on localhost, a setup whose credentialed auth the cookie-session migration had already broken. The credentialed allowlist itself is unchanged and stays strict; since dev traffic is same-origin, the per-subdomain cookie-allowlist problem disappears without loosening any production CORS/CSRF policy. ## Tests - `src/config/__tests__/apiProxyPrefixes.test.ts` guards the proxy boundary in both directions: representative backend path shapes (including `/metadata?query=...` and `/auth/...`) must match, every SPA route from the `AppPath` enum and vite's own dev paths must not — so a future route collision fails unit tests instead of breaking dev. - Verified against running dev servers: API paths proxy to the backend from both `localhost:3001` and `apple.localhost:3001`, while SPA routes `/settings` and `/authorize` still serve the vite app; a same-origin POST from `apple.localhost:3001` goes through with no CORS involvement. - `lint:diff-with-main` and `typecheck` pass for twenty-front. --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -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)}($|[/?])`;
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+2
-1
@@ -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 {
|
||||
|
||||
+5
-4
@@ -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`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+6
-5
@@ -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'],
|
||||
|
||||
+4
-3
@@ -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}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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(
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+2
-1
@@ -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 {
|
||||
|
||||
+3
-1
@@ -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();
|
||||
|
||||
+2
-2
@@ -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(
|
||||
|
||||
+2
-1
@@ -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) {}
|
||||
|
||||
+2
-2
@@ -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(
|
||||
|
||||
+2
-1
@@ -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) {}
|
||||
|
||||
+2
-1
@@ -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(
|
||||
|
||||
+6
-2
@@ -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(
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+2
-1
@@ -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(
|
||||
|
||||
+3
-1
@@ -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) {}
|
||||
|
||||
|
||||
+5
-1
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+2
-2
@@ -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',
|
||||
|
||||
+2
-1
@@ -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) {}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 <token>' \\
|
||||
${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 <token>' \\
|
||||
// Testing purposes
|
||||
servers: [
|
||||
{
|
||||
url: `${serverUrl}/rest/${schemaName !== 'core' ? schemaName : ''}`,
|
||||
url: `${serverUrl}/${ApiPath.Rest}/${schemaName !== 'core' ? schemaName : ''}`,
|
||||
description: 'Production Development',
|
||||
},
|
||||
],
|
||||
|
||||
+2
-1
@@ -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(
|
||||
|
||||
+2
-1
@@ -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 {
|
||||
|
||||
+2
-1
@@ -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) {}
|
||||
|
||||
|
||||
+15
-8
@@ -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',
|
||||
},
|
||||
],
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+2
-3
@@ -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,
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
@@ -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 `
|
||||
<!DOCTYPE html>
|
||||
|
||||
@@ -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,
|
||||
|
||||
+5
-4
@@ -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,
|
||||
|
||||
+3
-4
@@ -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`,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
+3
-2
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
export enum AppBasePath {
|
||||
Auth = '/auth',
|
||||
Settings = '/settings',
|
||||
Root = '/',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ApiPath } from '@/types/ApiPath';
|
||||
import { AppBasePath } from '@/types/AppBasePath';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
|
||||
const apiPaths = new Set<string>(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);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user