OAuth Client — Unified ApplicationRegistration, OAuth server, and frontend (#18267)
## Summary Consolidates three separate PRs (#18260, #18261, #18262) into a single unified branch with all review feedback addressed: ### New features - **ApplicationRegistration entity** — server-level registration for OAuth apps with encrypted server variables - **OAuth 2.0 server** — authorization code, client credentials, refresh token grants with PKCE support - **OAuth discovery endpoint** — `.well-known/oauth-authorization-server` metadata - **Frontend UI** — app registration details page with credential management, redirect URI editing, and server variable configuration - **CLI integration** — `twenty dev` auto-registers apps and stores OAuth credentials locally - **Authorize consent screen** — OAuth consent page at `/authorize` showing requested scopes ### Review feedback addressed **Renames (PR #18260):** - `appRegistration` → `applicationRegistration` (entity, tables, files, imports, GraphQL types) - `appRegistrationVariable` → `applicationRegistrationVariable` - `clientId` → `oAuthClientId`, `clientSecretHash` → `oAuthClientSecretHash`, `redirectUris` → `oAuthRedirectUris`, `scopes` → `oAuthScopes` **Security fixes (PR #18261):** - Fixed redirect URI validation bypass when `oAuthRedirectUris` is an empty array - Fixed workspace isolation in `clientCredentialsGrant` — now uses `find()` with explicit handling for multiple installations - Added error logging in refresh token `catch` block instead of silently swallowing **Code quality (PR #18262):** - Split `VersionDistributionEntry` into its own file (one export per file) - Split GraphQL queries and mutations into individual files with a shared fragment - Removed unused `OAuth` entry from `AuthProviderEnum` - Added loading state to `handleRotateSecret` - Removed 27 narration-style comments from test files - Added proper guards (`PublicEndpointGuard`, `NoPermissionGuard`) to controllers and resolvers ## Test plan - [ ] Verify `twenty dev` registers an app and stores OAuth credentials - [ ] Test OAuth authorization code flow end-to-end (authorize → token → API call) - [ ] Test client credentials grant - [ ] Verify redirect URI validation rejects requests when no URIs are registered - [ ] Verify app registration detail page renders correctly - [ ] Test secret rotation with loading state - [ ] Verify server variable editing and saving - [ ] Run `npx nx database:reset twenty-server` to validate migration Closes #18260, #18261, #18262 Made with [Cursor](https://cursor.com) --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
This commit is contained in:
@@ -7,9 +7,9 @@ import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { AdminPanelHealthService } from 'src/engine/core-modules/admin-panel/admin-panel-health.service';
|
||||
import { AdminPanelQueueService } from 'src/engine/core-modules/admin-panel/admin-panel-queue.service';
|
||||
import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service';
|
||||
import { AdminAIModelsOutput } from 'src/engine/core-modules/client-config/client-config.entity';
|
||||
import { AdminAIModelsDTO } from 'src/engine/core-modules/client-config/client-config.entity';
|
||||
import { ConfigVariableDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variable.dto';
|
||||
import { ConfigVariablesOutput } from 'src/engine/core-modules/admin-panel/dtos/config-variables.output';
|
||||
import { ConfigVariablesDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variables.dto';
|
||||
import { DeleteJobsResponseDTO } from 'src/engine/core-modules/admin-panel/dtos/delete-jobs-response.dto';
|
||||
import { QueueJobsResponseDTO } from 'src/engine/core-modules/admin-panel/dtos/queue-jobs-response.dto';
|
||||
import { RetryJobsResponseDTO } from 'src/engine/core-modules/admin-panel/dtos/retry-jobs-response.dto';
|
||||
@@ -95,8 +95,8 @@ export class AdminPanelResolver {
|
||||
}
|
||||
|
||||
@UseGuards(AdminPanelGuard)
|
||||
@Query(() => ConfigVariablesOutput)
|
||||
async getConfigVariablesGrouped(): Promise<ConfigVariablesOutput> {
|
||||
@Query(() => ConfigVariablesDTO)
|
||||
async getConfigVariablesGrouped(): Promise<ConfigVariablesDTO> {
|
||||
return this.adminService.getConfigVariablesGrouped();
|
||||
}
|
||||
|
||||
@@ -142,8 +142,8 @@ export class AdminPanelResolver {
|
||||
}
|
||||
|
||||
@UseGuards(AdminPanelGuard)
|
||||
@Query(() => AdminAIModelsOutput)
|
||||
async getAdminAiModels(): Promise<AdminAIModelsOutput> {
|
||||
@Query(() => AdminAIModelsDTO)
|
||||
async getAdminAiModels(): Promise<AdminAIModelsDTO> {
|
||||
const models = this.aiModelRegistryService
|
||||
.getAllModelsWithStatus()
|
||||
.map(({ modelConfig, isAvailable, isAdminEnabled }) => ({
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as z from 'zod';
|
||||
|
||||
import { type ConfigVariableDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variable.dto';
|
||||
import { type ConfigVariablesGroupDataDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variables-group.dto';
|
||||
import { type ConfigVariablesOutput } from 'src/engine/core-modules/admin-panel/dtos/config-variables.output';
|
||||
import { type ConfigVariablesDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variables.dto';
|
||||
import { type UserLookup } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.dto';
|
||||
import { type VersionInfoDTO } from 'src/engine/core-modules/admin-panel/dtos/version-info.dto';
|
||||
import {
|
||||
@@ -112,7 +112,7 @@ export class AdminPanelService {
|
||||
};
|
||||
}
|
||||
|
||||
getConfigVariablesGrouped(): ConfigVariablesOutput {
|
||||
getConfigVariablesGrouped(): ConfigVariablesDTO {
|
||||
const rawEnvVars = this.twentyConfigService.getAll();
|
||||
const groupedData = new Map<ConfigVariablesGroup, ConfigVariableDTO[]>();
|
||||
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@ import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { ConfigVariablesGroupDataDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variables-group.dto';
|
||||
|
||||
@ObjectType('ConfigVariablesOutput')
|
||||
export class ConfigVariablesOutput {
|
||||
@ObjectType('ConfigVariables')
|
||||
export class ConfigVariablesDTO {
|
||||
@Field(() => [ConfigVariablesGroupDataDTO])
|
||||
groups: ConfigVariablesGroupDataDTO[];
|
||||
}
|
||||
+2
-2
@@ -3,8 +3,8 @@ import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { AuthToken } from 'src/engine/core-modules/auth/dto/auth-token.dto';
|
||||
import { WorkspaceUrlsAndIdDTO } from 'src/engine/core-modules/workspace/dtos/workspace-subdomain-id.dto';
|
||||
|
||||
@ObjectType('ImpersonateOutput')
|
||||
export class ImpersonateOutput {
|
||||
@ObjectType('Impersonate')
|
||||
export class ImpersonateDTO {
|
||||
@Field(() => AuthToken)
|
||||
loginToken: AuthToken;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagDTO } from 'src/engine/core-modules/feature-flag/dtos/feature-flag.dto';
|
||||
import { WorkspaceUrlsDTO } from 'src/engine/core-modules/workspace/dtos/workspace-urls.dto';
|
||||
|
||||
@ObjectType('UserInfo')
|
||||
@@ -42,8 +42,8 @@ class WorkspaceInfoDTO {
|
||||
@Field(() => [UserInfoDTO])
|
||||
users: UserInfoDTO[];
|
||||
|
||||
@Field(() => [FeatureFlagEntity])
|
||||
featureFlags: FeatureFlagEntity[];
|
||||
@Field(() => [FeatureFlagDTO])
|
||||
featureFlags: FeatureFlagDTO[];
|
||||
}
|
||||
|
||||
@ObjectType('UserLookup')
|
||||
|
||||
Reference in New Issue
Block a user