mirror of
https://github.com/docmost/docmost.git
synced 2026-08-28 09:17:06 +08:00
fix: small refactor
This commit is contained in:
@@ -25,6 +25,7 @@ export const Feature = {
|
||||
BASES: 'bases',
|
||||
OAUTH: 'oauth',
|
||||
AI_CONTROLS: 'ai:controls',
|
||||
MCP_CONTROLS: 'mcp:controls',
|
||||
} as const;
|
||||
|
||||
export type FeatureKey = (typeof Feature)[keyof typeof Feature];
|
||||
|
||||
@@ -1,32 +1,15 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { Type } from 'class-transformer';
|
||||
import { CreateWorkspaceDto } from './create-workspace.dto';
|
||||
import { TrustedOAuthClient } from '../workspace.util';
|
||||
import {
|
||||
ArrayMaxSize,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsIn,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
Min,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
export class TrustedOAuthClientDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
origin: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(64)
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) {
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@@ -86,13 +69,6 @@ export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) {
|
||||
@IsIn(['read', 'edit'])
|
||||
defaultPageEditMode: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayMaxSize(50)
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => TrustedOAuthClientDto)
|
||||
trustedOauthClients?: TrustedOAuthClient[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
aiChatReadOnly: boolean;
|
||||
@@ -100,4 +76,8 @@ export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
aiChatWorkspaceKnowledgeOnly: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
mcpOauthOnly: boolean;
|
||||
}
|
||||
|
||||
@@ -30,10 +30,7 @@ import { DomainService } from '../../../integrations/environment/domain.service'
|
||||
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
||||
import { addDays } from 'date-fns';
|
||||
import { DISALLOWED_HOSTNAMES, WorkspaceStatus } from '../workspace.constants';
|
||||
import {
|
||||
isAdminActingOnOwner,
|
||||
normalizeTrustedOAuthClients,
|
||||
} from '../workspace.util';
|
||||
import { isAdminActingOnOwner } from '../workspace.util';
|
||||
import { v4 } from 'uuid';
|
||||
import { InjectQueue } from '@nestjs/bullmq';
|
||||
import { QueueJob, QueueName } from '../../../integrations/queue/constants';
|
||||
@@ -317,12 +314,6 @@ export class WorkspaceService {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
if (typeof updateWorkspaceDto.trustedOauthClients !== 'undefined') {
|
||||
updateWorkspaceDto.trustedOauthClients = normalizeTrustedOAuthClients(
|
||||
updateWorkspaceDto.trustedOauthClients,
|
||||
);
|
||||
}
|
||||
|
||||
if (updateWorkspaceDto.hostname) {
|
||||
const hostname = updateWorkspaceDto.hostname;
|
||||
if (DISALLOWED_HOSTNAMES.includes(hostname)) {
|
||||
@@ -344,9 +335,9 @@ export class WorkspaceService {
|
||||
typeof updateWorkspaceDto.allowMemberTemplates !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.isScimEnabled !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.allowPersonalSpaces !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.trustedOauthClients !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.aiChatReadOnly !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.aiChatWorkspaceKnowledgeOnly !== 'undefined'
|
||||
typeof updateWorkspaceDto.aiChatWorkspaceKnowledgeOnly !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.mcpOauthOnly !== 'undefined'
|
||||
) {
|
||||
const ws = await this.db
|
||||
.selectFrom('workspaces')
|
||||
@@ -374,18 +365,6 @@ export class WorkspaceService {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof updateWorkspaceDto.trustedOauthClients !== 'undefined') {
|
||||
if (
|
||||
!this.licenseCheckService.hasFeature(
|
||||
ws.licenseKey,
|
||||
Feature.OAUTH,
|
||||
ws.plan,
|
||||
)
|
||||
) {
|
||||
throw new ForbiddenException('This feature requires a valid license');
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof updateWorkspaceDto.allowPersonalSpaces !== 'undefined') {
|
||||
if (
|
||||
!this.licenseCheckService.hasFeature(
|
||||
@@ -413,6 +392,18 @@ export class WorkspaceService {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof updateWorkspaceDto.mcpOauthOnly !== 'undefined') {
|
||||
if (
|
||||
!this.licenseCheckService.hasFeature(
|
||||
ws.licenseKey,
|
||||
Feature.MCP_CONTROLS,
|
||||
ws.plan,
|
||||
)
|
||||
) {
|
||||
throw new ForbiddenException('This feature requires a valid license');
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
typeof updateWorkspaceDto.disablePublicSharing !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.trashRetentionDays !== 'undefined' ||
|
||||
@@ -583,6 +574,20 @@ export class WorkspaceService {
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof updateWorkspaceDto.mcpOauthOnly !== 'undefined') {
|
||||
const prev = settingsBefore?.ai?.mcpOauthOnly ?? false;
|
||||
if (prev !== updateWorkspaceDto.mcpOauthOnly) {
|
||||
before.mcpOauthOnly = prev;
|
||||
after.mcpOauthOnly = updateWorkspaceDto.mcpOauthOnly;
|
||||
}
|
||||
await this.workspaceRepo.updateAiSettings(
|
||||
workspaceId,
|
||||
'mcpOauthOnly',
|
||||
updateWorkspaceDto.mcpOauthOnly,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof updateWorkspaceDto.allowPersonalSpaces !== 'undefined') {
|
||||
const prev = settingsBefore?.spaces?.allowPersonal ?? false;
|
||||
if (prev !== updateWorkspaceDto.allowPersonalSpaces) {
|
||||
@@ -622,6 +627,7 @@ export class WorkspaceService {
|
||||
delete updateWorkspaceDto.defaultPageEditMode;
|
||||
delete updateWorkspaceDto.aiChatReadOnly;
|
||||
delete updateWorkspaceDto.aiChatWorkspaceKnowledgeOnly;
|
||||
delete updateWorkspaceDto.mcpOauthOnly;
|
||||
|
||||
await this.workspaceRepo.updateWorkspace(
|
||||
updateWorkspaceDto,
|
||||
@@ -661,7 +667,6 @@ export class WorkspaceService {
|
||||
'enforceMfa',
|
||||
'emailDomains',
|
||||
'isScimEnabled',
|
||||
'trustedOauthClients',
|
||||
],
|
||||
updateWorkspaceDto,
|
||||
workspaceBefore,
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { normalizeTrustedOAuthClients } from './workspace.util';
|
||||
|
||||
describe('normalizeTrustedOAuthClients', () => {
|
||||
it('lowercases origins and trims names', () => {
|
||||
expect(
|
||||
normalizeTrustedOAuthClients([
|
||||
{ origin: 'https://mcp.acme.com', name: ' Acme MCP ' },
|
||||
]),
|
||||
).toEqual([{ origin: 'https://mcp.acme.com', name: 'Acme MCP' }]);
|
||||
});
|
||||
|
||||
it('dedupes origins case-insensitively with the last entry winning', () => {
|
||||
expect(
|
||||
normalizeTrustedOAuthClients([
|
||||
{ origin: 'https://mcp.acme.com', name: 'First' },
|
||||
{ origin: 'https://mcp.acme.com', name: 'Second' },
|
||||
]),
|
||||
).toEqual([{ origin: 'https://mcp.acme.com', name: 'Second' }]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['http origin', 'http://mcp.acme.com'],
|
||||
['trailing slash', 'https://mcp.acme.com/'],
|
||||
['path suffix', 'https://mcp.acme.com/oauth'],
|
||||
['uppercase host', 'https://MCP.acme.com'],
|
||||
['not a url', 'mcp.acme.com'],
|
||||
])('rejects %s naming the origin', (_label, origin) => {
|
||||
expect(() =>
|
||||
normalizeTrustedOAuthClients([{ origin, name: 'Acme MCP' }]),
|
||||
).toThrow(BadRequestException);
|
||||
expect(() =>
|
||||
normalizeTrustedOAuthClients([{ origin, name: 'Acme MCP' }]),
|
||||
).toThrow(origin);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['blank', ' '],
|
||||
['too long', 'x'.repeat(65)],
|
||||
])('rejects a %s name', (_label, name) => {
|
||||
expect(() =>
|
||||
normalizeTrustedOAuthClients([{ origin: 'https://mcp.acme.com', name }]),
|
||||
).toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('returns an empty array for no entries', () => {
|
||||
expect(normalizeTrustedOAuthClients([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { UserRole } from '../../common/helpers/types/permission';
|
||||
|
||||
export function isAdminActingOnOwner(
|
||||
@@ -8,39 +7,6 @@ export function isAdminActingOnOwner(
|
||||
return authUserRole === UserRole.ADMIN && targetRole === UserRole.OWNER;
|
||||
}
|
||||
|
||||
export type TrustedOAuthClient = { origin: string; name: string };
|
||||
|
||||
// Origins must be exact https origins; duplicates collapse case-insensitively, last entry wins.
|
||||
export function normalizeTrustedOAuthClients(
|
||||
entries: { origin: string; name: string }[],
|
||||
): TrustedOAuthClient[] {
|
||||
const byOrigin = new Map<string, TrustedOAuthClient>();
|
||||
for (const entry of entries) {
|
||||
const name = entry.name.trim();
|
||||
if (name.length < 1 || name.length > 64) {
|
||||
throw new BadRequestException(
|
||||
`Invalid trusted application name for origin: ${entry.origin}`,
|
||||
);
|
||||
}
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(entry.origin);
|
||||
} catch {
|
||||
throw new BadRequestException(
|
||||
`Invalid trusted application origin: ${entry.origin}`,
|
||||
);
|
||||
}
|
||||
if (parsed.protocol !== 'https:' || parsed.origin !== entry.origin) {
|
||||
throw new BadRequestException(
|
||||
`Trusted application origin must be an exact https origin: ${entry.origin}`,
|
||||
);
|
||||
}
|
||||
const origin = entry.origin.toLowerCase();
|
||||
byOrigin.set(origin, { origin, name });
|
||||
}
|
||||
return Array.from(byOrigin.values());
|
||||
}
|
||||
|
||||
export type PageEditMode = 'read' | 'edit';
|
||||
|
||||
export function getWorkspaceDefaultPageEditMode(
|
||||
|
||||
@@ -78,17 +78,9 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.createIndex('oauth_tokens_access_expires_at_idx').on('oauth_tokens').column('access_expires_at').execute();
|
||||
await db.schema.createIndex('oauth_tokens_refresh_expires_at_idx').on('oauth_tokens').column('refresh_expires_at').execute();
|
||||
await db.schema.createIndex('oauth_tokens_revoked_at_idx').on('oauth_tokens').column('revoked_at').execute();
|
||||
|
||||
await db.schema
|
||||
.alterTable('workspaces')
|
||||
.addColumn('trusted_oauth_clients', 'jsonb', (col) => col.defaultTo(sql`'[]'::jsonb`))
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE workspaces DROP COLUMN IF EXISTS trusted_oauth_clients`.execute(
|
||||
db,
|
||||
);
|
||||
await db.schema.dropTable('oauth_tokens').execute();
|
||||
await db.schema.dropTable('oauth_grants').execute();
|
||||
await db.schema.dropTable('oauth_authorization_codes').execute();
|
||||
|
||||
@@ -20,7 +20,6 @@ export class WorkspaceRepo {
|
||||
'hostname',
|
||||
'customDomain',
|
||||
'settings',
|
||||
'trustedOauthClients',
|
||||
'defaultRole',
|
||||
'emailDomains',
|
||||
'defaultSpaceId',
|
||||
|
||||
-1
@@ -460,7 +460,6 @@ export interface Workspaces {
|
||||
status: string | null;
|
||||
stripeCustomerId: string | null;
|
||||
trialEndAt: Timestamp | null;
|
||||
trustedOauthClients: Generated<Json | null>;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
Submodule apps/server/src/ee updated: 1b3f1b90e9...70fdfb9933
Reference in New Issue
Block a user