mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-13 08:57:55 -05:00
Compare commits
6 Commits
fix/slack-
...
feat/integ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d320e50cf | ||
|
|
5449aac183 | ||
|
|
be92f52a8a | ||
|
|
709dfcc0fa | ||
|
|
454e53e75b | ||
|
|
981370af44 |
@@ -70,7 +70,6 @@ Für selbst gehostete Bereitstellungen können Enterprise-Funktionen über Umgeb
|
||||
|----------|-------------|
|
||||
| `SSO_ENABLED`, `NEXT_PUBLIC_SSO_ENABLED` | Single Sign-On mit SAML/OIDC |
|
||||
| `CREDENTIAL_SETS_ENABLED`, `NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED` | Polling-Gruppen für E-Mail-Trigger |
|
||||
| `DISABLE_INVITATIONS`, `NEXT_PUBLIC_DISABLE_INVITATIONS` | Workspace-/Organisations-Einladungen global deaktivieren |
|
||||
|
||||
<Callout type="warn">
|
||||
BYOK ist nur im gehosteten Sim Studio verfügbar. Selbst gehostete Deployments konfigurieren AI-Provider-Schlüssel direkt über Umgebungsvariablen.
|
||||
|
||||
@@ -17,7 +17,7 @@ Define permission groups to control what features and integrations team members
|
||||
|
||||
- **Allowed Model Providers** - Restrict which AI providers users can access (OpenAI, Anthropic, Google, etc.)
|
||||
- **Allowed Blocks** - Control which workflow blocks are available
|
||||
- **Platform Settings** - Hide Knowledge Base, disable MCP tools, disable custom tools, or disable invitations
|
||||
- **Platform Settings** - Hide Knowledge Base, disable MCP tools, or disable custom tools
|
||||
|
||||
### Setup
|
||||
|
||||
@@ -68,7 +68,6 @@ For self-hosted deployments, enterprise features can be enabled via environment
|
||||
| `ACCESS_CONTROL_ENABLED`, `NEXT_PUBLIC_ACCESS_CONTROL_ENABLED` | Permission groups for access restrictions |
|
||||
| `SSO_ENABLED`, `NEXT_PUBLIC_SSO_ENABLED` | Single Sign-On with SAML/OIDC |
|
||||
| `CREDENTIAL_SETS_ENABLED`, `NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED` | Polling Groups for email triggers |
|
||||
| `DISABLE_INVITATIONS`, `NEXT_PUBLIC_DISABLE_INVITATIONS` | Globally disable workspace/organization invitations |
|
||||
|
||||
### Organization Management
|
||||
|
||||
@@ -88,23 +87,6 @@ curl -X POST https://your-instance/api/v1/admin/organizations/{orgId}/members \
|
||||
-d '{"userId": "user-id-here", "role": "admin"}'
|
||||
```
|
||||
|
||||
### Workspace Members
|
||||
|
||||
When invitations are disabled, use the Admin API to manage workspace memberships directly:
|
||||
|
||||
```bash
|
||||
# Add a user to a workspace
|
||||
curl -X POST https://your-instance/api/v1/admin/workspaces/{workspaceId}/members \
|
||||
-H "x-admin-key: YOUR_ADMIN_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"userId": "user-id-here", "permissions": "write"}'
|
||||
|
||||
# Remove a user from a workspace
|
||||
curl -X DELETE "https://your-instance/api/v1/admin/workspaces/{workspaceId}/members?userId=user-id-here" \
|
||||
-H "x-admin-key: YOUR_ADMIN_API_KEY"
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- Enabling `ACCESS_CONTROL_ENABLED` automatically enables organizations, as access control requires organization membership.
|
||||
- When `DISABLE_INVITATIONS` is set, users cannot send invitations. Use the Admin API to manage workspace and organization memberships instead.
|
||||
|
||||
@@ -70,7 +70,6 @@ Para implementaciones self-hosted, las funciones enterprise se pueden activar me
|
||||
|----------|-------------|
|
||||
| `SSO_ENABLED`, `NEXT_PUBLIC_SSO_ENABLED` | Inicio de sesión único con SAML/OIDC |
|
||||
| `CREDENTIAL_SETS_ENABLED`, `NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED` | Grupos de sondeo para activadores de correo electrónico |
|
||||
| `DISABLE_INVITATIONS`, `NEXT_PUBLIC_DISABLE_INVITATIONS` | Desactivar globalmente invitaciones a espacios de trabajo/organizaciones |
|
||||
|
||||
<Callout type="warn">
|
||||
BYOK solo está disponible en Sim Studio alojado. Las implementaciones autoalojadas configuran las claves de proveedor de IA directamente a través de variables de entorno.
|
||||
|
||||
@@ -70,7 +70,6 @@ Pour les déploiements auto-hébergés, les fonctionnalités entreprise peuvent
|
||||
|----------|-------------|
|
||||
| `SSO_ENABLED`, `NEXT_PUBLIC_SSO_ENABLED` | Authentification unique avec SAML/OIDC |
|
||||
| `CREDENTIAL_SETS_ENABLED`, `NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED` | Groupes de sondage pour les déclencheurs d'e-mail |
|
||||
| `DISABLE_INVITATIONS`, `NEXT_PUBLIC_DISABLE_INVITATIONS` | Désactiver globalement les invitations aux espaces de travail/organisations |
|
||||
|
||||
<Callout type="warn">
|
||||
BYOK est uniquement disponible sur Sim Studio hébergé. Les déploiements auto-hébergés configurent les clés de fournisseur d'IA directement via les variables d'environnement.
|
||||
|
||||
@@ -69,7 +69,6 @@ Sim Studioのホストキーの代わりに、AIモデルプロバイダー用
|
||||
|----------|-------------|
|
||||
| `SSO_ENABLED`、`NEXT_PUBLIC_SSO_ENABLED` | SAML/OIDCによるシングルサインオン |
|
||||
| `CREDENTIAL_SETS_ENABLED`、`NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED` | メールトリガー用のポーリンググループ |
|
||||
| `DISABLE_INVITATIONS`、`NEXT_PUBLIC_DISABLE_INVITATIONS` | ワークスペース/組織への招待をグローバルに無効化 |
|
||||
|
||||
<Callout type="warn">
|
||||
BYOKはホスト型Sim Studioでのみ利用可能です。セルフホスト型デプロイメントでは、環境変数を介してAIプロバイダーキーを直接設定します。
|
||||
|
||||
@@ -69,7 +69,6 @@ Sim Studio 企业版为需要更高安全性、合规性和管理能力的组织
|
||||
|----------|-------------|
|
||||
| `SSO_ENABLED`,`NEXT_PUBLIC_SSO_ENABLED` | 使用 SAML/OIDC 的单点登录 |
|
||||
| `CREDENTIAL_SETS_ENABLED`,`NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED` | 用于邮件触发器的轮询组 |
|
||||
| `DISABLE_INVITATIONS`,`NEXT_PUBLIC_DISABLE_INVITATIONS` | 全局禁用工作区/组织邀请 |
|
||||
|
||||
<Callout type="warn">
|
||||
BYOK 仅适用于托管版 Sim Studio。自托管部署需通过环境变量直接配置 AI 提供商密钥。
|
||||
|
||||
@@ -26,10 +26,6 @@ import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { sendEmail } from '@/lib/messaging/email/mailer'
|
||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||
import { hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils'
|
||||
import {
|
||||
InvitationsNotAllowedError,
|
||||
validateInvitationsAllowed,
|
||||
} from '@/executor/utils/permission-check'
|
||||
|
||||
const logger = createLogger('OrganizationInvitations')
|
||||
|
||||
@@ -120,8 +116,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
await validateInvitationsAllowed(session.user.id)
|
||||
|
||||
const { id: organizationId } = await params
|
||||
const url = new URL(request.url)
|
||||
const validateOnly = url.searchParams.get('validate') === 'true'
|
||||
@@ -433,10 +427,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof InvitationsNotAllowedError) {
|
||||
return NextResponse.json({ error: error.message }, { status: 403 })
|
||||
}
|
||||
|
||||
logger.error('Failed to create organization invitations', {
|
||||
organizationId: (await params).id,
|
||||
error,
|
||||
@@ -496,7 +486,10 @@ export async function DELETE(
|
||||
and(
|
||||
eq(invitation.id, invitationId),
|
||||
eq(invitation.organizationId, organizationId),
|
||||
or(eq(invitation.status, 'pending'), eq(invitation.status, 'rejected'))
|
||||
or(
|
||||
eq(invitation.status, 'pending'),
|
||||
eq(invitation.status, 'rejected') // Allow cancelling rejected invitations too
|
||||
)
|
||||
)
|
||||
)
|
||||
.returning()
|
||||
|
||||
@@ -17,12 +17,6 @@
|
||||
* Workspaces:
|
||||
* GET /api/v1/admin/workspaces - List all workspaces
|
||||
* GET /api/v1/admin/workspaces/:id - Get workspace details
|
||||
* GET /api/v1/admin/workspaces/:id/members - List workspace members
|
||||
* POST /api/v1/admin/workspaces/:id/members - Add/update workspace member
|
||||
* DELETE /api/v1/admin/workspaces/:id/members?userId=X - Remove workspace member
|
||||
* GET /api/v1/admin/workspaces/:id/members/:mid - Get workspace member details
|
||||
* PATCH /api/v1/admin/workspaces/:id/members/:mid - Update workspace member permissions
|
||||
* DELETE /api/v1/admin/workspaces/:id/members/:mid - Remove workspace member by ID
|
||||
* GET /api/v1/admin/workspaces/:id/workflows - List workspace workflows
|
||||
* DELETE /api/v1/admin/workspaces/:id/workflows - Delete all workspace workflows
|
||||
* GET /api/v1/admin/workspaces/:id/folders - List workspace folders
|
||||
@@ -101,7 +95,6 @@ export type {
|
||||
AdminWorkflowDetail,
|
||||
AdminWorkspace,
|
||||
AdminWorkspaceDetail,
|
||||
AdminWorkspaceMember,
|
||||
DbMember,
|
||||
DbOrganization,
|
||||
DbSubscription,
|
||||
|
||||
@@ -518,22 +518,6 @@ export interface AdminMemberDetail extends AdminMember {
|
||||
billingBlocked: boolean
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Workspace Member Types
|
||||
// =============================================================================
|
||||
|
||||
export interface AdminWorkspaceMember {
|
||||
id: string
|
||||
workspaceId: string
|
||||
userId: string
|
||||
permissions: 'admin' | 'write' | 'read'
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
userName: string
|
||||
userEmail: string
|
||||
userImage: string | null
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// User Billing Types
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
/**
|
||||
* GET /api/v1/admin/workspaces/[id]/members/[memberId]
|
||||
*
|
||||
* Get workspace member details.
|
||||
*
|
||||
* Response: AdminSingleResponse<AdminWorkspaceMember>
|
||||
*
|
||||
* PATCH /api/v1/admin/workspaces/[id]/members/[memberId]
|
||||
*
|
||||
* Update member permissions.
|
||||
*
|
||||
* Body:
|
||||
* - permissions: 'admin' | 'write' | 'read' - New permission level
|
||||
*
|
||||
* Response: AdminSingleResponse<AdminWorkspaceMember>
|
||||
*
|
||||
* DELETE /api/v1/admin/workspaces/[id]/members/[memberId]
|
||||
*
|
||||
* Remove member from workspace.
|
||||
*
|
||||
* Response: AdminSingleResponse<{ removed: true, memberId: string, userId: string }>
|
||||
*/
|
||||
|
||||
import { db } from '@sim/db'
|
||||
import { permissions, user, workspace } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
|
||||
import {
|
||||
badRequestResponse,
|
||||
internalErrorResponse,
|
||||
notFoundResponse,
|
||||
singleResponse,
|
||||
} from '@/app/api/v1/admin/responses'
|
||||
import type { AdminWorkspaceMember } from '@/app/api/v1/admin/types'
|
||||
|
||||
const logger = createLogger('AdminWorkspaceMemberDetailAPI')
|
||||
|
||||
interface RouteParams {
|
||||
id: string
|
||||
memberId: string
|
||||
}
|
||||
|
||||
export const GET = withAdminAuthParams<RouteParams>(async (_, context) => {
|
||||
const { id: workspaceId, memberId } = await context.params
|
||||
|
||||
try {
|
||||
const [workspaceData] = await db
|
||||
.select({ id: workspace.id })
|
||||
.from(workspace)
|
||||
.where(eq(workspace.id, workspaceId))
|
||||
.limit(1)
|
||||
|
||||
if (!workspaceData) {
|
||||
return notFoundResponse('Workspace')
|
||||
}
|
||||
|
||||
const [memberData] = await db
|
||||
.select({
|
||||
id: permissions.id,
|
||||
userId: permissions.userId,
|
||||
permissionType: permissions.permissionType,
|
||||
createdAt: permissions.createdAt,
|
||||
updatedAt: permissions.updatedAt,
|
||||
userName: user.name,
|
||||
userEmail: user.email,
|
||||
userImage: user.image,
|
||||
})
|
||||
.from(permissions)
|
||||
.innerJoin(user, eq(permissions.userId, user.id))
|
||||
.where(
|
||||
and(
|
||||
eq(permissions.id, memberId),
|
||||
eq(permissions.entityType, 'workspace'),
|
||||
eq(permissions.entityId, workspaceId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!memberData) {
|
||||
return notFoundResponse('Workspace member')
|
||||
}
|
||||
|
||||
const data: AdminWorkspaceMember = {
|
||||
id: memberData.id,
|
||||
workspaceId,
|
||||
userId: memberData.userId,
|
||||
permissions: memberData.permissionType,
|
||||
createdAt: memberData.createdAt.toISOString(),
|
||||
updatedAt: memberData.updatedAt.toISOString(),
|
||||
userName: memberData.userName,
|
||||
userEmail: memberData.userEmail,
|
||||
userImage: memberData.userImage,
|
||||
}
|
||||
|
||||
logger.info(`Admin API: Retrieved member ${memberId} from workspace ${workspaceId}`)
|
||||
|
||||
return singleResponse(data)
|
||||
} catch (error) {
|
||||
logger.error('Admin API: Failed to get workspace member', { error, workspaceId, memberId })
|
||||
return internalErrorResponse('Failed to get workspace member')
|
||||
}
|
||||
})
|
||||
|
||||
export const PATCH = withAdminAuthParams<RouteParams>(async (request, context) => {
|
||||
const { id: workspaceId, memberId } = await context.params
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
if (!body.permissions || !['admin', 'write', 'read'].includes(body.permissions)) {
|
||||
return badRequestResponse('permissions must be "admin", "write", or "read"')
|
||||
}
|
||||
|
||||
const [workspaceData] = await db
|
||||
.select({ id: workspace.id })
|
||||
.from(workspace)
|
||||
.where(eq(workspace.id, workspaceId))
|
||||
.limit(1)
|
||||
|
||||
if (!workspaceData) {
|
||||
return notFoundResponse('Workspace')
|
||||
}
|
||||
|
||||
const [existingMember] = await db
|
||||
.select({
|
||||
id: permissions.id,
|
||||
userId: permissions.userId,
|
||||
permissionType: permissions.permissionType,
|
||||
createdAt: permissions.createdAt,
|
||||
})
|
||||
.from(permissions)
|
||||
.where(
|
||||
and(
|
||||
eq(permissions.id, memberId),
|
||||
eq(permissions.entityType, 'workspace'),
|
||||
eq(permissions.entityId, workspaceId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!existingMember) {
|
||||
return notFoundResponse('Workspace member')
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
|
||||
await db
|
||||
.update(permissions)
|
||||
.set({ permissionType: body.permissions, updatedAt: now })
|
||||
.where(eq(permissions.id, memberId))
|
||||
|
||||
const [userData] = await db
|
||||
.select({ name: user.name, email: user.email, image: user.image })
|
||||
.from(user)
|
||||
.where(eq(user.id, existingMember.userId))
|
||||
.limit(1)
|
||||
|
||||
const data: AdminWorkspaceMember = {
|
||||
id: existingMember.id,
|
||||
workspaceId,
|
||||
userId: existingMember.userId,
|
||||
permissions: body.permissions,
|
||||
createdAt: existingMember.createdAt.toISOString(),
|
||||
updatedAt: now.toISOString(),
|
||||
userName: userData?.name ?? '',
|
||||
userEmail: userData?.email ?? '',
|
||||
userImage: userData?.image ?? null,
|
||||
}
|
||||
|
||||
logger.info(`Admin API: Updated member ${memberId} permissions to ${body.permissions}`, {
|
||||
workspaceId,
|
||||
previousPermissions: existingMember.permissionType,
|
||||
})
|
||||
|
||||
return singleResponse(data)
|
||||
} catch (error) {
|
||||
logger.error('Admin API: Failed to update workspace member', { error, workspaceId, memberId })
|
||||
return internalErrorResponse('Failed to update workspace member')
|
||||
}
|
||||
})
|
||||
|
||||
export const DELETE = withAdminAuthParams<RouteParams>(async (_, context) => {
|
||||
const { id: workspaceId, memberId } = await context.params
|
||||
|
||||
try {
|
||||
const [workspaceData] = await db
|
||||
.select({ id: workspace.id })
|
||||
.from(workspace)
|
||||
.where(eq(workspace.id, workspaceId))
|
||||
.limit(1)
|
||||
|
||||
if (!workspaceData) {
|
||||
return notFoundResponse('Workspace')
|
||||
}
|
||||
|
||||
const [existingMember] = await db
|
||||
.select({
|
||||
id: permissions.id,
|
||||
userId: permissions.userId,
|
||||
})
|
||||
.from(permissions)
|
||||
.where(
|
||||
and(
|
||||
eq(permissions.id, memberId),
|
||||
eq(permissions.entityType, 'workspace'),
|
||||
eq(permissions.entityId, workspaceId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!existingMember) {
|
||||
return notFoundResponse('Workspace member')
|
||||
}
|
||||
|
||||
await db.delete(permissions).where(eq(permissions.id, memberId))
|
||||
|
||||
logger.info(`Admin API: Removed member ${memberId} from workspace ${workspaceId}`, {
|
||||
userId: existingMember.userId,
|
||||
})
|
||||
|
||||
return singleResponse({
|
||||
removed: true,
|
||||
memberId,
|
||||
userId: existingMember.userId,
|
||||
workspaceId,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Admin API: Failed to remove workspace member', { error, workspaceId, memberId })
|
||||
return internalErrorResponse('Failed to remove workspace member')
|
||||
}
|
||||
})
|
||||
@@ -1,298 +0,0 @@
|
||||
/**
|
||||
* GET /api/v1/admin/workspaces/[id]/members
|
||||
*
|
||||
* List all members of a workspace with their permission details.
|
||||
*
|
||||
* Query Parameters:
|
||||
* - limit: number (default: 50, max: 250)
|
||||
* - offset: number (default: 0)
|
||||
*
|
||||
* Response: AdminListResponse<AdminWorkspaceMember>
|
||||
*
|
||||
* POST /api/v1/admin/workspaces/[id]/members
|
||||
*
|
||||
* Add a user to a workspace with a specific permission level.
|
||||
* If the user already has permissions, updates their permission level.
|
||||
*
|
||||
* Body:
|
||||
* - userId: string - User ID to add
|
||||
* - permissions: 'admin' | 'write' | 'read' - Permission level
|
||||
*
|
||||
* Response: AdminSingleResponse<AdminWorkspaceMember & { action: 'created' | 'updated' }>
|
||||
*
|
||||
* DELETE /api/v1/admin/workspaces/[id]/members
|
||||
*
|
||||
* Remove a user from a workspace.
|
||||
*
|
||||
* Query Parameters:
|
||||
* - userId: string - User ID to remove
|
||||
*
|
||||
* Response: AdminSingleResponse<{ removed: true }>
|
||||
*/
|
||||
|
||||
import crypto from 'crypto'
|
||||
import { db } from '@sim/db'
|
||||
import { permissions, user, workspace } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, count, eq } from 'drizzle-orm'
|
||||
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
|
||||
import {
|
||||
badRequestResponse,
|
||||
internalErrorResponse,
|
||||
listResponse,
|
||||
notFoundResponse,
|
||||
singleResponse,
|
||||
} from '@/app/api/v1/admin/responses'
|
||||
import {
|
||||
type AdminWorkspaceMember,
|
||||
createPaginationMeta,
|
||||
parsePaginationParams,
|
||||
} from '@/app/api/v1/admin/types'
|
||||
|
||||
const logger = createLogger('AdminWorkspaceMembersAPI')
|
||||
|
||||
interface RouteParams {
|
||||
id: string
|
||||
}
|
||||
|
||||
export const GET = withAdminAuthParams<RouteParams>(async (request, context) => {
|
||||
const { id: workspaceId } = await context.params
|
||||
const url = new URL(request.url)
|
||||
const { limit, offset } = parsePaginationParams(url)
|
||||
|
||||
try {
|
||||
const [workspaceData] = await db
|
||||
.select({ id: workspace.id })
|
||||
.from(workspace)
|
||||
.where(eq(workspace.id, workspaceId))
|
||||
.limit(1)
|
||||
|
||||
if (!workspaceData) {
|
||||
return notFoundResponse('Workspace')
|
||||
}
|
||||
|
||||
const [countResult, membersData] = await Promise.all([
|
||||
db
|
||||
.select({ count: count() })
|
||||
.from(permissions)
|
||||
.where(and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId))),
|
||||
db
|
||||
.select({
|
||||
id: permissions.id,
|
||||
userId: permissions.userId,
|
||||
permissionType: permissions.permissionType,
|
||||
createdAt: permissions.createdAt,
|
||||
updatedAt: permissions.updatedAt,
|
||||
userName: user.name,
|
||||
userEmail: user.email,
|
||||
userImage: user.image,
|
||||
})
|
||||
.from(permissions)
|
||||
.innerJoin(user, eq(permissions.userId, user.id))
|
||||
.where(and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId)))
|
||||
.orderBy(permissions.createdAt)
|
||||
.limit(limit)
|
||||
.offset(offset),
|
||||
])
|
||||
|
||||
const total = countResult[0].count
|
||||
const data: AdminWorkspaceMember[] = membersData.map((m) => ({
|
||||
id: m.id,
|
||||
workspaceId,
|
||||
userId: m.userId,
|
||||
permissions: m.permissionType,
|
||||
createdAt: m.createdAt.toISOString(),
|
||||
updatedAt: m.updatedAt.toISOString(),
|
||||
userName: m.userName,
|
||||
userEmail: m.userEmail,
|
||||
userImage: m.userImage,
|
||||
}))
|
||||
|
||||
const pagination = createPaginationMeta(total, limit, offset)
|
||||
|
||||
logger.info(`Admin API: Listed ${data.length} members for workspace ${workspaceId}`)
|
||||
|
||||
return listResponse(data, pagination)
|
||||
} catch (error) {
|
||||
logger.error('Admin API: Failed to list workspace members', { error, workspaceId })
|
||||
return internalErrorResponse('Failed to list workspace members')
|
||||
}
|
||||
})
|
||||
|
||||
export const POST = withAdminAuthParams<RouteParams>(async (request, context) => {
|
||||
const { id: workspaceId } = await context.params
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
if (!body.userId || typeof body.userId !== 'string') {
|
||||
return badRequestResponse('userId is required')
|
||||
}
|
||||
|
||||
if (!body.permissions || !['admin', 'write', 'read'].includes(body.permissions)) {
|
||||
return badRequestResponse('permissions must be "admin", "write", or "read"')
|
||||
}
|
||||
|
||||
const [workspaceData] = await db
|
||||
.select({ id: workspace.id, name: workspace.name })
|
||||
.from(workspace)
|
||||
.where(eq(workspace.id, workspaceId))
|
||||
.limit(1)
|
||||
|
||||
if (!workspaceData) {
|
||||
return notFoundResponse('Workspace')
|
||||
}
|
||||
|
||||
const [userData] = await db
|
||||
.select({ id: user.id, name: user.name, email: user.email, image: user.image })
|
||||
.from(user)
|
||||
.where(eq(user.id, body.userId))
|
||||
.limit(1)
|
||||
|
||||
if (!userData) {
|
||||
return notFoundResponse('User')
|
||||
}
|
||||
|
||||
const [existingPermission] = await db
|
||||
.select({
|
||||
id: permissions.id,
|
||||
permissionType: permissions.permissionType,
|
||||
createdAt: permissions.createdAt,
|
||||
updatedAt: permissions.updatedAt,
|
||||
})
|
||||
.from(permissions)
|
||||
.where(
|
||||
and(
|
||||
eq(permissions.userId, body.userId),
|
||||
eq(permissions.entityType, 'workspace'),
|
||||
eq(permissions.entityId, workspaceId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (existingPermission) {
|
||||
if (existingPermission.permissionType !== body.permissions) {
|
||||
const now = new Date()
|
||||
await db
|
||||
.update(permissions)
|
||||
.set({ permissionType: body.permissions, updatedAt: now })
|
||||
.where(eq(permissions.id, existingPermission.id))
|
||||
|
||||
logger.info(
|
||||
`Admin API: Updated user ${body.userId} permissions in workspace ${workspaceId}`,
|
||||
{
|
||||
previousPermissions: existingPermission.permissionType,
|
||||
newPermissions: body.permissions,
|
||||
}
|
||||
)
|
||||
|
||||
return singleResponse({
|
||||
id: existingPermission.id,
|
||||
workspaceId,
|
||||
userId: body.userId,
|
||||
permissions: body.permissions as 'admin' | 'write' | 'read',
|
||||
createdAt: existingPermission.createdAt.toISOString(),
|
||||
updatedAt: now.toISOString(),
|
||||
userName: userData.name,
|
||||
userEmail: userData.email,
|
||||
userImage: userData.image,
|
||||
action: 'updated' as const,
|
||||
})
|
||||
}
|
||||
|
||||
return singleResponse({
|
||||
id: existingPermission.id,
|
||||
workspaceId,
|
||||
userId: body.userId,
|
||||
permissions: existingPermission.permissionType,
|
||||
createdAt: existingPermission.createdAt.toISOString(),
|
||||
updatedAt: existingPermission.updatedAt.toISOString(),
|
||||
userName: userData.name,
|
||||
userEmail: userData.email,
|
||||
userImage: userData.image,
|
||||
action: 'already_member' as const,
|
||||
})
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const permissionId = crypto.randomUUID()
|
||||
|
||||
await db.insert(permissions).values({
|
||||
id: permissionId,
|
||||
userId: body.userId,
|
||||
entityType: 'workspace',
|
||||
entityId: workspaceId,
|
||||
permissionType: body.permissions,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
logger.info(`Admin API: Added user ${body.userId} to workspace ${workspaceId}`, {
|
||||
permissions: body.permissions,
|
||||
permissionId,
|
||||
})
|
||||
|
||||
return singleResponse({
|
||||
id: permissionId,
|
||||
workspaceId,
|
||||
userId: body.userId,
|
||||
permissions: body.permissions as 'admin' | 'write' | 'read',
|
||||
createdAt: now.toISOString(),
|
||||
updatedAt: now.toISOString(),
|
||||
userName: userData.name,
|
||||
userEmail: userData.email,
|
||||
userImage: userData.image,
|
||||
action: 'created' as const,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Admin API: Failed to add workspace member', { error, workspaceId })
|
||||
return internalErrorResponse('Failed to add workspace member')
|
||||
}
|
||||
})
|
||||
|
||||
export const DELETE = withAdminAuthParams<RouteParams>(async (request, context) => {
|
||||
const { id: workspaceId } = await context.params
|
||||
const url = new URL(request.url)
|
||||
const userId = url.searchParams.get('userId')
|
||||
|
||||
try {
|
||||
if (!userId) {
|
||||
return badRequestResponse('userId query parameter is required')
|
||||
}
|
||||
|
||||
const [workspaceData] = await db
|
||||
.select({ id: workspace.id })
|
||||
.from(workspace)
|
||||
.where(eq(workspace.id, workspaceId))
|
||||
.limit(1)
|
||||
|
||||
if (!workspaceData) {
|
||||
return notFoundResponse('Workspace')
|
||||
}
|
||||
|
||||
const [existingPermission] = await db
|
||||
.select({ id: permissions.id })
|
||||
.from(permissions)
|
||||
.where(
|
||||
and(
|
||||
eq(permissions.userId, userId),
|
||||
eq(permissions.entityType, 'workspace'),
|
||||
eq(permissions.entityId, workspaceId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!existingPermission) {
|
||||
return notFoundResponse('Workspace member')
|
||||
}
|
||||
|
||||
await db.delete(permissions).where(eq(permissions.id, existingPermission.id))
|
||||
|
||||
logger.info(`Admin API: Removed user ${userId} from workspace ${workspaceId}`)
|
||||
|
||||
return singleResponse({ removed: true, userId, workspaceId })
|
||||
} catch (error) {
|
||||
logger.error('Admin API: Failed to remove workspace member', { error, workspaceId, userId })
|
||||
return internalErrorResponse('Failed to remove workspace member')
|
||||
}
|
||||
})
|
||||
@@ -101,16 +101,6 @@ describe('Workspace Invitations API Route', () => {
|
||||
eq: vi.fn().mockImplementation((field, value) => ({ type: 'eq', field, value })),
|
||||
inArray: vi.fn().mockImplementation((field, values) => ({ type: 'inArray', field, values })),
|
||||
}))
|
||||
|
||||
vi.doMock('@/executor/utils/permission-check', () => ({
|
||||
validateInvitationsAllowed: vi.fn().mockResolvedValue(undefined),
|
||||
InvitationsNotAllowedError: class InvitationsNotAllowedError extends Error {
|
||||
constructor() {
|
||||
super('Invitations are not allowed based on your permission group settings')
|
||||
this.name = 'InvitationsNotAllowedError'
|
||||
}
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
describe('GET /api/workspaces/invitations', () => {
|
||||
|
||||
@@ -18,10 +18,6 @@ import { PlatformEvents } from '@/lib/core/telemetry'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { sendEmail } from '@/lib/messaging/email/mailer'
|
||||
import { getFromEmailAddress } from '@/lib/messaging/email/utils'
|
||||
import {
|
||||
InvitationsNotAllowedError,
|
||||
validateInvitationsAllowed,
|
||||
} from '@/executor/utils/permission-check'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -80,8 +76,6 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
await validateInvitationsAllowed(session.user.id)
|
||||
|
||||
const { workspaceId, email, role = 'member', permission = 'read' } = await req.json()
|
||||
|
||||
if (!workspaceId || !email) {
|
||||
@@ -219,9 +213,6 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
return NextResponse.json({ success: true, invitation: invitationData })
|
||||
} catch (error) {
|
||||
if (error instanceof InvitationsNotAllowedError) {
|
||||
return NextResponse.json({ error: error.message }, { status: 403 })
|
||||
}
|
||||
logger.error('Error creating workspace invitation:', error)
|
||||
return NextResponse.json({ error: 'Failed to create invitation' }, { status: 500 })
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ export function PaneContextMenu({
|
||||
disableAdmin = false,
|
||||
canUndo = false,
|
||||
canRedo = false,
|
||||
isInvitationsDisabled = false,
|
||||
}: PaneContextMenuProps) {
|
||||
return (
|
||||
<Popover
|
||||
@@ -143,21 +142,17 @@ export function PaneContextMenu({
|
||||
{isChatOpen ? 'Close Chat' : 'Open Chat'}
|
||||
</PopoverItem>
|
||||
|
||||
{/* Admin action - hidden when invitations are disabled */}
|
||||
{!isInvitationsDisabled && (
|
||||
<>
|
||||
<PopoverDivider />
|
||||
<PopoverItem
|
||||
disabled={disableAdmin}
|
||||
onClick={() => {
|
||||
onInvite()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Invite to Workspace
|
||||
</PopoverItem>
|
||||
</>
|
||||
)}
|
||||
{/* Admin action */}
|
||||
<PopoverDivider />
|
||||
<PopoverItem
|
||||
disabled={disableAdmin}
|
||||
onClick={() => {
|
||||
onInvite()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Invite to Workspace
|
||||
</PopoverItem>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
|
||||
@@ -94,6 +94,4 @@ export interface PaneContextMenuProps {
|
||||
canUndo?: boolean
|
||||
/** Whether redo is available */
|
||||
canRedo?: boolean
|
||||
/** Whether invitations are disabled (feature flag or permission group) */
|
||||
isInvitationsDisabled?: boolean
|
||||
}
|
||||
|
||||
@@ -64,7 +64,6 @@ import { getBlock } from '@/blocks'
|
||||
import { isAnnotationOnlyBlock } from '@/executor/constants'
|
||||
import { useWorkspaceEnvironment } from '@/hooks/queries/environment'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { usePermissionConfig } from '@/hooks/use-permission-config'
|
||||
import { useStreamCleanup } from '@/hooks/use-stream-cleanup'
|
||||
import { useChatStore } from '@/stores/chat/store'
|
||||
import { useCopilotTrainingStore } from '@/stores/copilot-training/store'
|
||||
@@ -282,9 +281,6 @@ const WorkflowContent = React.memo(() => {
|
||||
// Panel open states for context menu
|
||||
const isVariablesOpen = useVariablesStore((state) => state.isOpen)
|
||||
const isChatOpen = useChatStore((state) => state.isChatOpen)
|
||||
|
||||
// Permission config for invitation control
|
||||
const { isInvitationsDisabled } = usePermissionConfig()
|
||||
const snapGrid: [number, number] = useMemo(
|
||||
() => [snapToGridSize, snapToGridSize],
|
||||
[snapToGridSize]
|
||||
@@ -3430,7 +3426,6 @@ const WorkflowContent = React.memo(() => {
|
||||
disableAdmin={!effectivePermissions.canAdmin}
|
||||
canUndo={canUndo}
|
||||
canRedo={canRedo}
|
||||
isInvitationsDisabled={isInvitationsDisabled}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -342,12 +342,6 @@ export function AccessControl() {
|
||||
category: 'Logs',
|
||||
configKey: 'hideTraceSpans' as const,
|
||||
},
|
||||
{
|
||||
id: 'disable-invitations',
|
||||
label: 'Invitations',
|
||||
category: 'Collaboration',
|
||||
configKey: 'disableInvitations' as const,
|
||||
},
|
||||
],
|
||||
[]
|
||||
)
|
||||
@@ -875,8 +869,7 @@ export function AccessControl() {
|
||||
!editingConfig?.hideFilesTab &&
|
||||
!editingConfig?.disableMcpTools &&
|
||||
!editingConfig?.disableCustomTools &&
|
||||
!editingConfig?.hideTraceSpans &&
|
||||
!editingConfig?.disableInvitations
|
||||
!editingConfig?.hideTraceSpans
|
||||
setEditingConfig((prev) =>
|
||||
prev
|
||||
? {
|
||||
@@ -890,7 +883,6 @@ export function AccessControl() {
|
||||
disableMcpTools: allVisible,
|
||||
disableCustomTools: allVisible,
|
||||
hideTraceSpans: allVisible,
|
||||
disableInvitations: allVisible,
|
||||
}
|
||||
: prev
|
||||
)
|
||||
@@ -904,8 +896,7 @@ export function AccessControl() {
|
||||
!editingConfig?.hideFilesTab &&
|
||||
!editingConfig?.disableMcpTools &&
|
||||
!editingConfig?.disableCustomTools &&
|
||||
!editingConfig?.hideTraceSpans &&
|
||||
!editingConfig?.disableInvitations
|
||||
!editingConfig?.hideTraceSpans
|
||||
? 'Deselect All'
|
||||
: 'Select All'}
|
||||
</Button>
|
||||
|
||||
@@ -33,13 +33,11 @@ import {
|
||||
} from '@/hooks/queries/organization'
|
||||
import { useSubscriptionData } from '@/hooks/queries/subscription'
|
||||
import { useAdminWorkspaces } from '@/hooks/queries/workspace'
|
||||
import { usePermissionConfig } from '@/hooks/use-permission-config'
|
||||
|
||||
const logger = createLogger('TeamManagement')
|
||||
|
||||
export function TeamManagement() {
|
||||
const { data: session } = useSession()
|
||||
const { isInvitationsDisabled } = usePermissionConfig()
|
||||
|
||||
const { data: organizationsData } = useOrganizations()
|
||||
const activeOrganization = organizationsData?.activeOrganization
|
||||
@@ -387,8 +385,8 @@ export function TeamManagement() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action: Invite New Members - hidden when invitations are disabled */}
|
||||
{adminOrOwner && !isInvitationsDisabled && (
|
||||
{/* Action: Invite New Members */}
|
||||
{adminOrOwner && (
|
||||
<div>
|
||||
<MemberInvitationCard
|
||||
inviteEmail={inviteEmail}
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
import { ContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu'
|
||||
import { DeleteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal'
|
||||
import { InviteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal'
|
||||
import { usePermissionConfig } from '@/hooks/use-permission-config'
|
||||
|
||||
const logger = createLogger('WorkspaceHeader')
|
||||
|
||||
@@ -152,18 +151,12 @@ export function WorkspaceHeader({
|
||||
setIsMounted(true)
|
||||
}, [])
|
||||
|
||||
const { isInvitationsDisabled } = usePermissionConfig()
|
||||
|
||||
// Listen for open-invite-modal event from context menu
|
||||
useEffect(() => {
|
||||
const handleOpenInvite = () => {
|
||||
if (!isInvitationsDisabled) {
|
||||
setIsInviteModalOpen(true)
|
||||
}
|
||||
}
|
||||
const handleOpenInvite = () => setIsInviteModalOpen(true)
|
||||
window.addEventListener('open-invite-modal', handleOpenInvite)
|
||||
return () => window.removeEventListener('open-invite-modal', handleOpenInvite)
|
||||
}, [isInvitationsDisabled])
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Focus the inline list rename input when it becomes active
|
||||
@@ -465,8 +458,8 @@ export function WorkspaceHeader({
|
||||
</div>
|
||||
{/* Workspace Actions */}
|
||||
<div className='flex flex-shrink-0 items-center gap-[10px]'>
|
||||
{/* Invite - hidden in collapsed mode or when invitations are disabled */}
|
||||
{!isCollapsed && !isInvitationsDisabled && (
|
||||
{/* Invite - hidden in collapsed mode */}
|
||||
{!isCollapsed && (
|
||||
<Badge className='cursor-pointer' onClick={() => setIsInviteModalOpen(true)}>
|
||||
Invite
|
||||
</Badge>
|
||||
|
||||
@@ -42,13 +42,6 @@ export class CustomToolsNotAllowedError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export class InvitationsNotAllowedError extends Error {
|
||||
constructor() {
|
||||
super('Invitations are not allowed based on your permission group settings')
|
||||
this.name = 'InvitationsNotAllowedError'
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserPermissionConfig(
|
||||
userId: string
|
||||
): Promise<PermissionGroupConfig | null> {
|
||||
@@ -191,30 +184,3 @@ export async function validateCustomToolsAllowed(
|
||||
throw new CustomToolsNotAllowedError()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if the user is allowed to send invitations.
|
||||
* Also checks the global feature flag.
|
||||
*/
|
||||
export async function validateInvitationsAllowed(userId: string | undefined): Promise<void> {
|
||||
const { isInvitationsDisabled } = await import('@/lib/core/config/feature-flags')
|
||||
if (isInvitationsDisabled) {
|
||||
logger.warn('Invitations blocked by feature flag')
|
||||
throw new InvitationsNotAllowedError()
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return
|
||||
}
|
||||
|
||||
const config = await getUserPermissionConfig(userId)
|
||||
|
||||
if (!config) {
|
||||
return
|
||||
}
|
||||
|
||||
if (config.disableInvitations) {
|
||||
logger.warn('Invitations blocked by permission group', { userId })
|
||||
throw new InvitationsNotAllowedError()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1088,28 +1088,25 @@ export function useCollaborativeWorkflow() {
|
||||
userId: session?.user?.id || 'unknown',
|
||||
})
|
||||
|
||||
const currentValue = subBlockStore.getValue(blockId, subblockId)
|
||||
const valueActuallyChanged = currentValue !== value
|
||||
|
||||
subBlockStore.setValue(blockId, subblockId, value)
|
||||
|
||||
if (valueActuallyChanged) {
|
||||
try {
|
||||
const visited = options?._visited || new Set<string>()
|
||||
if (visited.has(subblockId)) return
|
||||
visited.add(subblockId)
|
||||
const blockType = useWorkflowStore.getState().blocks?.[blockId]?.type
|
||||
const blockConfig = blockType ? getBlock(blockType) : null
|
||||
if (blockConfig?.subBlocks && Array.isArray(blockConfig.subBlocks)) {
|
||||
const dependents = blockConfig.subBlocks.filter(
|
||||
(sb: any) => Array.isArray(sb.dependsOn) && sb.dependsOn.includes(subblockId)
|
||||
)
|
||||
for (const dep of dependents) {
|
||||
if (!dep?.id || dep.id === subblockId) continue
|
||||
collaborativeSetSubblockValue(blockId, dep.id, '', { _visited: visited })
|
||||
}
|
||||
try {
|
||||
const visited = options?._visited || new Set<string>()
|
||||
if (visited.has(subblockId)) return
|
||||
visited.add(subblockId)
|
||||
const blockType = useWorkflowStore.getState().blocks?.[blockId]?.type
|
||||
const blockConfig = blockType ? getBlock(blockType) : null
|
||||
if (blockConfig?.subBlocks && Array.isArray(blockConfig.subBlocks)) {
|
||||
const dependents = blockConfig.subBlocks.filter(
|
||||
(sb: any) => Array.isArray(sb.dependsOn) && sb.dependsOn.includes(subblockId)
|
||||
)
|
||||
for (const dep of dependents) {
|
||||
if (!dep?.id || dep.id === subblockId) continue
|
||||
collaborativeSetSubblockValue(blockId, dep.id, '', { _visited: visited })
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
} catch {
|
||||
// Best-effort; do not block on clearing
|
||||
}
|
||||
},
|
||||
[
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useMemo } from 'react'
|
||||
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
||||
import {
|
||||
DEFAULT_PERMISSION_GROUP_CONFIG,
|
||||
type PermissionGroupConfig,
|
||||
@@ -15,7 +14,6 @@ export interface PermissionConfigResult {
|
||||
filterProviders: (providerIds: string[]) => string[]
|
||||
isBlockAllowed: (blockType: string) => boolean
|
||||
isProviderAllowed: (providerId: string) => boolean
|
||||
isInvitationsDisabled: boolean
|
||||
}
|
||||
|
||||
export function usePermissionConfig(): PermissionConfigResult {
|
||||
@@ -61,11 +59,6 @@ export function usePermissionConfig(): PermissionConfigResult {
|
||||
}
|
||||
}, [config.allowedModelProviders])
|
||||
|
||||
const isInvitationsDisabled = useMemo(() => {
|
||||
const featureFlagDisabled = isTruthy(getEnv('NEXT_PUBLIC_DISABLE_INVITATIONS'))
|
||||
return featureFlagDisabled || config.disableInvitations
|
||||
}, [config.disableInvitations])
|
||||
|
||||
return {
|
||||
config,
|
||||
isLoading,
|
||||
@@ -74,6 +67,5 @@ export function usePermissionConfig(): PermissionConfigResult {
|
||||
filterProviders,
|
||||
isBlockAllowed,
|
||||
isProviderAllowed,
|
||||
isInvitationsDisabled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,9 +257,6 @@ export const env = createEnv({
|
||||
// Organizations - for self-hosted deployments
|
||||
ORGANIZATIONS_ENABLED: z.boolean().optional(), // Enable organizations on self-hosted (bypasses plan requirements)
|
||||
|
||||
// Invitations - for self-hosted deployments
|
||||
DISABLE_INVITATIONS: z.boolean().optional(), // Disable workspace invitations globally (for self-hosted deployments)
|
||||
|
||||
// SSO Configuration (for script-based registration)
|
||||
SSO_ENABLED: z.boolean().optional(), // Enable SSO functionality
|
||||
SSO_PROVIDER_TYPE: z.enum(['oidc', 'saml']).optional(), // [REQUIRED] SSO provider type
|
||||
@@ -340,7 +337,6 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED: z.boolean().optional(), // Enable credential sets (email polling) on self-hosted
|
||||
NEXT_PUBLIC_ACCESS_CONTROL_ENABLED: z.boolean().optional(), // Enable access control (permission groups) on self-hosted
|
||||
NEXT_PUBLIC_ORGANIZATIONS_ENABLED: z.boolean().optional(), // Enable organizations on self-hosted (bypasses plan requirements)
|
||||
NEXT_PUBLIC_DISABLE_INVITATIONS: z.boolean().optional(), // Disable workspace invitations globally (for self-hosted deployments)
|
||||
NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED: z.boolean().optional().default(true), // Control visibility of email/password login forms
|
||||
},
|
||||
|
||||
@@ -372,7 +368,6 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED: process.env.NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED,
|
||||
NEXT_PUBLIC_ACCESS_CONTROL_ENABLED: process.env.NEXT_PUBLIC_ACCESS_CONTROL_ENABLED,
|
||||
NEXT_PUBLIC_ORGANIZATIONS_ENABLED: process.env.NEXT_PUBLIC_ORGANIZATIONS_ENABLED,
|
||||
NEXT_PUBLIC_DISABLE_INVITATIONS: process.env.NEXT_PUBLIC_DISABLE_INVITATIONS,
|
||||
NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED: process.env.NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED,
|
||||
NEXT_PUBLIC_E2B_ENABLED: process.env.NEXT_PUBLIC_E2B_ENABLED,
|
||||
NEXT_PUBLIC_COPILOT_TRAINING_ENABLED: process.env.NEXT_PUBLIC_COPILOT_TRAINING_ENABLED,
|
||||
|
||||
@@ -103,12 +103,6 @@ export const isOrganizationsEnabled =
|
||||
*/
|
||||
export const isE2bEnabled = isTruthy(env.E2B_ENABLED)
|
||||
|
||||
/**
|
||||
* Are invitations disabled globally
|
||||
* When true, workspace invitations are disabled for all users
|
||||
*/
|
||||
export const isInvitationsDisabled = isTruthy(env.DISABLE_INVITATIONS)
|
||||
|
||||
/**
|
||||
* Get cost multiplier based on environment
|
||||
*/
|
||||
|
||||
@@ -11,7 +11,6 @@ export interface PermissionGroupConfig {
|
||||
disableMcpTools: boolean
|
||||
disableCustomTools: boolean
|
||||
hideTemplates: boolean
|
||||
disableInvitations: boolean
|
||||
}
|
||||
|
||||
export const DEFAULT_PERMISSION_GROUP_CONFIG: PermissionGroupConfig = {
|
||||
@@ -26,7 +25,6 @@ export const DEFAULT_PERMISSION_GROUP_CONFIG: PermissionGroupConfig = {
|
||||
disableMcpTools: false,
|
||||
disableCustomTools: false,
|
||||
hideTemplates: false,
|
||||
disableInvitations: false,
|
||||
}
|
||||
|
||||
export function parsePermissionGroupConfig(config: unknown): PermissionGroupConfig {
|
||||
@@ -49,6 +47,5 @@ export function parsePermissionGroupConfig(config: unknown): PermissionGroupConf
|
||||
disableMcpTools: typeof c.disableMcpTools === 'boolean' ? c.disableMcpTools : false,
|
||||
disableCustomTools: typeof c.disableCustomTools === 'boolean' ? c.disableCustomTools : false,
|
||||
hideTemplates: typeof c.hideTemplates === 'boolean' ? c.hideTemplates : false,
|
||||
disableInvitations: typeof c.disableInvitations === 'boolean' ? c.disableInvitations : false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,10 +147,6 @@ app:
|
||||
BLACKLISTED_PROVIDERS: "" # Comma-separated provider IDs to hide from UI (e.g., "openai,anthropic,google")
|
||||
BLACKLISTED_MODELS: "" # Comma-separated model names/prefixes to hide (e.g., "gpt-4,claude-*")
|
||||
|
||||
# Invitation Control
|
||||
DISABLE_INVITATIONS: "" # Set to "true" to disable workspace invitations globally
|
||||
NEXT_PUBLIC_DISABLE_INVITATIONS: "" # Set to "true" to hide invitation UI elements
|
||||
|
||||
# SSO Configuration (Enterprise Single Sign-On)
|
||||
# Set to "true" AFTER running the SSO registration script
|
||||
SSO_ENABLED: "" # Enable SSO authentication ("true" to enable)
|
||||
|
||||
Reference in New Issue
Block a user