UBERF-9606: Limit a number of workspaces per user (#8192)

* UBERF-9606: Limit workspaces per user

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>

* Revert limit check for system account or admin

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>

* Fix review comment

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>

---------

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2025-03-11 17:16:21 +07:00 committed by GitHub
parent 1d9ed50a35
commit 6a51d96abe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 43 additions and 13 deletions

View File

@ -85,6 +85,7 @@ services:
environment:
- ACCOUNT_PORT=3000
- SERVER_SECRET=secret
- WORKSPACE_LIMIT_PER_USER=10000
- STATS_URL=http://host.docker.internal:4900
# - DB_URL=postgresql://postgres:example@postgres:5432
- DB_URL=${MONGO_URL}

View File

@ -74,7 +74,7 @@ const devProxy = {
const devProxyTest = {
'/account': {
target: 'http://localhost:3003',
target: 'http://huly.local:3003',
changeOrigin: true,
pathRewrite: { '^/account': '' },
logLevel: 'debug'

View File

@ -19,6 +19,7 @@
"AccountAlreadyConfirmed": "Účet již byl potvrzen",
"WorkspaceAlreadyExists": "Pracovní prostor již existuje",
"InvalidOtp": "Neplatný kód",
"InviteNotFound": "Pozvánka s email:{email} nebyla nalezena."
"InviteNotFound": "Pozvánka s email:{email} nebyla nalezena.",
"WorkspaceLimitReached": "Dosáhli jste limitu pracovních prostorů. Kontaktujte nás..."
}
}

View File

@ -19,6 +19,7 @@
"AccountAlreadyConfirmed": "Konto wurde bereits bestätigt",
"WorkspaceAlreadyExists": "Arbeitsbereich existiert bereits",
"InvalidOtp": "Ungültiger Code",
"InviteNotFound": "Einladung mit E-Mail: {email} nicht gefunden."
"InviteNotFound": "Einladung mit E-Mail: {email} nicht gefunden.",
"WorkspaceLimitReached": "Sie haben das Arbeitsbereichslimit erreicht. Bitte kontaktieren Sie uns..."
}
}

View File

@ -19,6 +19,7 @@
"AccountAlreadyConfirmed": "Account already confirmed",
"WorkspaceAlreadyExists": "Workspace already exists",
"InvalidOtp": "Invalid code",
"InviteNotFound": "Invitation with email:{email} not found."
"InviteNotFound": "Invitation with email:{email} not found.",
"WorkspaceLimitReached": "You have reached the workspace limit. Please contact us..."
}
}

View File

@ -19,6 +19,7 @@
"AccountAlreadyConfirmed": "La cuenta ya está confirmada",
"WorkspaceAlreadyExists": "El espacio de trabajo ya existe",
"InvalidOtp": "Código no válido",
"InviteNotFound": "No se encontró la invitación con email:{email}."
"InviteNotFound": "No se encontró la invitación con email:{email}.",
"WorkspaceLimitReached": "Ha alcanzado el límite de espacios de trabajo. Póngase en contacto con nosotros..."
}
}

View File

@ -19,6 +19,7 @@
"AccountAlreadyConfirmed": "Compte déjà confirmé",
"WorkspaceAlreadyExists": "L'espace de travail existe déjà",
"InvalidOtp": "Code invalide",
"InviteNotFound": "Invitation avec l'email:{email} introuvable."
"InviteNotFound": "Invitation avec l'email:{email} introuvable.",
"WorkspaceLimitReached": "Vous avez atteint la limite d'espace de travail. Veuillez contacter nous..."
}
}

View File

@ -19,6 +19,7 @@
"AccountAlreadyConfirmed": "Account già confermato",
"WorkspaceAlreadyExists": "Spazio di lavoro già esistente",
"InvalidOtp": "Codice non valido",
"InviteNotFound": "Invito con email:{email} non trovato."
"InviteNotFound": "Invito con email:{email} non trovato.",
"WorkspaceLimitReached": "Hai raggiunto il limite di spazi di lavoro. Contattaci..."
}
}

View File

@ -19,6 +19,7 @@
"AccountAlreadyConfirmed": "Conta já confirmada",
"WorkspaceAlreadyExists": "Espaço de trabalho já existe",
"InvalidOtp": "Código inválido",
"InviteNotFound": "Convite com email:{email} não encontrado."
"InviteNotFound": "Convite com email:{email} não encontrado.",
"WorkspaceLimitReached": "Você atingiu o limite de espaço de trabalho. Entre em contato conosco..."
}
}

View File

@ -19,6 +19,7 @@
"AccountAlreadyConfirmed": "Аккаунт уже подтвержден",
"WorkspaceAlreadyExists": "Рабочее пространство уже существует",
"InvalidOtp": "Неверный код",
"InviteNotFound": "Приглашение с email:{email} не найдено."
"InviteNotFound": "Приглашение с email:{email} не найдено.",
"WorkspaceLimitReached": "Вы достигли лимита рабочих пространств. Свяжитесь с нами..."
}
}

View File

@ -19,6 +19,7 @@
"AccountAlreadyConfirmed": "账户已确认",
"WorkspaceAlreadyExists": "工作区已存在",
"InvalidOtp": "无效的代码",
"InviteNotFound": "未找到 id 为 {email} 的邀请。"
"InviteNotFound": "未找到 id 为 {email} 的邀请。",
"WorkspaceLimitReached": "您已达到工作区限制。请联系我们..."
}
}

View File

@ -156,6 +156,7 @@ export default plugin(platformId, {
AccountAlreadyConfirmed: '' as StatusCode<{ account: string }>,
WorkspaceAlreadyExists: '' as StatusCode<{ workspace: string }>,
WorkspaceRateLimit: '' as StatusCode<{ workspace: string }>,
WorkspaceLimitReached: '' as StatusCode<{ workspace: string }>,
InvalidOtp: '' as StatusCode,
InviteNotFound: '' as StatusCode<{ email: string }>
},

View File

@ -3,7 +3,7 @@
"RequiredField": "Required field {field}",
"FieldsDoNotMatch": "{field} don't match {field2}",
"ConnectingToServer": "Connecting to server....",
"IncorrectValue": "Incorrect value {field}"
"IncorrectValue": "Incorrect value {field}"
},
"string": {
"LogIn": "Log In",

View File

@ -3,7 +3,7 @@
"RequiredField": "Campo obligatorio: {field}",
"FieldsDoNotMatch": "{field} no coincide con {field2}",
"ConnectingToServer": "Conectando al servidor...",
"IncorrectValue": "Valor incorrecto para {field}"
"IncorrectValue": "Valor incorrecto para {field}"
},
"string": {
"LogIn": "Iniciar sesión",

View File

@ -17,7 +17,7 @@
import { OK, Severity, Status, getEmbeddedLabel } from '@hcengineering/platform'
import { LoginInfo } from '@hcengineering/login'
import { ButtonMenu, DropdownLabels, getCurrentLocation, navigate } from '@hcengineering/ui'
import { ButtonMenu, getCurrentLocation, navigate } from '@hcengineering/ui'
import { workbenchId } from '@hcengineering/workbench'
import { onMount } from 'svelte'
import login from '../plugin'

View File

@ -53,6 +53,7 @@ services:
environment:
- ACCOUNT_PORT=3003
- SERVER_SECRET=secret
- WORKSPACE_LIMIT_PER_USER=100
- DB_URL=mongodb://mongodb:27018
- TRANSACTOR_URL=ws://transactor:3334;ws://host.docker.internal:3334
- STORAGE_CONFIG=${STORAGE_CONFIG}

View File

@ -94,6 +94,10 @@ import {
} from './utils'
import MD5 from 'crypto-js/md5'
const workspaceLimitPerUser =
process.env.WORKSPACE_LIMIT_PER_USER != null ? parseInt(process.env.WORKSPACE_LIMIT_PER_USER) : 10
function buildGravatarId (email: string): string {
return MD5(email.trim().toLowerCase()).toString()
}
@ -1566,6 +1570,16 @@ export async function createUserWorkspace (
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotConfirmed, { account: email }))
}
// Get a list of created workspaces
const created = (await db.workspace.find({ createdBy: email })).length
if (created >= (userAccount.workspaceLimit ?? workspaceLimitPerUser)) {
ctx.warn('created-by-limit', { email, workspace: workspaceName, limit: userAccount.workspaceLimit })
throw new PlatformError(
new Status(Severity.ERROR, platform.status.WorkspaceLimitReached, { workspace: workspaceName })
)
}
if (userAccount.lastWorkspace !== undefined && userAccount.admin === false) {
if (Date.now() - userAccount.lastWorkspace < 60 * 1000) {
throw new PlatformError(

View File

@ -49,6 +49,8 @@ export interface Account {
githubId?: string
githubUser?: string
openId?: string
workspaceLimit?: number
}
/**

View File

@ -58,6 +58,7 @@ services:
environment:
- ACCOUNT_PORT=3003
- SERVER_SECRET=secret
- WORKSPACE_LIMIT_PER_USER=100
- DB_URL=mongodb://mongodb:27018
- TRANSACTOR_URL=ws://transactor:3334;ws://localhost:3334
- STORAGE_CONFIG=${STORAGE_CONFIG}

View File

@ -63,6 +63,7 @@ services:
- REGION_INFO=|America;europe| # Europe without name will not be available for creation of new workspaces.
# - REGION_INFO=|America;europe|Europe
- ADMIN_EMAILS=admin
- WORKSPACE_LIMIT_PER_USER=100
- ACCOUNT_PORT=3003
- SERVER_SECRET=secret
- DB_URL=mongodb://huly.local:27018