UBERF-9540: Fix invite message and add rate limit (#8123)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2025-03-03 01:11:25 +07:00 committed by Andrey Sobolev
parent a6b8d6cd09
commit 6a926a7ea7
No known key found for this signature in database
GPG Key ID: BD80F68D68D8F7F2
13 changed files with 70 additions and 14 deletions

View File

@ -135,6 +135,7 @@
"StartOfTheWeek": "Začátek týdne",
"SystemSetupString": "Nastavení systému ({day})",
"DefaultString": "Výchozí ({day})",
"AddAttribute": "Přidat atribut"
"AddAttribute": "Přidat atribut",
"WorkspaceNamePattern": "Název musí být 40 znaků nebo méně, není prázdný a nesmí obsahovat speciální znaky (<, >, /)"
}
}

View File

@ -135,6 +135,7 @@
"StartOfTheWeek": "Wochenstart",
"SystemSetupString": "Systemkonfiguration ({day})",
"DefaultString": "Stillschweigend ({day})",
"AddAttribute": "Attribut hinzufügen"
"AddAttribute": "Attribut hinzufügen",
"WorkspaceNamePattern": "Name muss 40 Zeichen oder weniger sein, nicht leer sein und keine Sonderzeichen (<, >, /) enthalten"
}
}

View File

@ -135,6 +135,7 @@
"StartOfTheWeek": "Start of the week",
"SystemSetupString": "System Setup ({day})",
"DefaultString": "Default ({day})",
"AddAttribute": "Add attribute"
"AddAttribute": "Add attribute",
"WorkspaceNamePattern": "Name must be 40 characters or less, not empty, and cannot contain special characters (<, >, /)"
}
}

View File

@ -126,6 +126,7 @@
"StartOfTheWeek": "Inicio de la semana",
"SystemSetupString": "Configuración del sistema ({day})",
"DefaultString": "Predeterminado ({day})",
"AddAttribute": "Añadir atributo"
"AddAttribute": "Añadir atributo",
"WorkspaceNamePattern": "El nombre debe tener 40 caracteres o menos, no esté vacío y no puede contener caracteres especiales (<, >, /)"
}
}

View File

@ -135,6 +135,7 @@
"StartOfTheWeek": "Début de semaine",
"SystemSetupString": "Configuration du système ({day})",
"DefaultString": "Par défaut ({day})",
"AddAttribute": "Ajouter un attribut"
"AddAttribute": "Ajouter un attribut",
"WorkspaceNamePattern": "Le nom doit comporter 40 caractères ou moins, ne doit pas être vide et ne doit pas contenir de caractères spéciaux (<, >, /)"
}
}

View File

@ -135,6 +135,7 @@
"StartOfTheWeek": "Inizio settimana",
"SystemSetupString": "Configurazione del sistema ({day})",
"DefaultString": "Predefinito ({day})",
"AddAttribute": "Aggiungi attributo"
"AddAttribute": "Aggiungi attributo",
"WorkspaceNamePattern": "Il nome deve essere di 40 caratteri o meno, non vuoto e non può contenere caratteri speciali (<, >, /)"
}
}

View File

@ -126,6 +126,7 @@
"StartOfTheWeek": "Início da semana",
"SystemSetupString": "Configuração do sistema ({day})",
"DefaultString": "Predefinição ({day})",
"AddAttribute": "Adicionar atributo"
"AddAttribute": "Adicionar atributo",
"WorkspaceNamePattern": "O nome deve ter 40 caracteres ou menos, não está vazio e não pode conter caracteres especiais (<, >, /)"
}
}

View File

@ -136,6 +136,7 @@
"StartOfTheWeek": "Начало недели",
"SystemSetupString": "Системная настройка ({day})",
"DefaultString": "По умолчанию ({day})",
"AddAttribute": "Добавить атрибут"
"AddAttribute": "Добавить атрибут",
"WorkspaceNamePattern": "Имя должно быть длиной 40 символов или меньше, не должно быть пустым и не может содержать специальные символы (<, >, /)"
}
}

View File

@ -135,6 +135,7 @@
"StartOfTheWeek": "本周开始",
"SystemSetupString": "系统设置({day})",
"DefaultString": "违约({day})",
"AddAttribute": "添加属性"
"AddAttribute": "添加属性",
"WorkspaceNamePattern": "名称必须为 40 个字符或更少,不能为空,不能包含特殊字符(<、>、/"
}
}

View File

@ -52,6 +52,14 @@
let name: string = ''
const accountClient = getAccountClient()
const disabledSet = ['\n', '<', '>', '/', '\\']
$: editNameDisabled =
isEditingName &&
(name.trim().length > 40 ||
name.trim() === oldName ||
name.trim() === '' ||
disabledSet.some((it) => name.includes(it)))
void loadWorkspaceName()
@ -80,8 +88,6 @@
isEditingName = false
}
$: editNameDisabled = isEditingName && (name.trim() === oldName || name.trim() === '')
async function handleDelete (): Promise<void> {
showPopup(MessageBox, {
label: setting.string.DeleteWorkspace,
@ -99,7 +105,7 @@
let workspaceSettings: WorkspaceSetting | undefined = undefined
const client = getClient()
client.findOne(setting.class.WorkspaceSetting, {}).then((r) => {
void client.findOne(setting.class.WorkspaceSetting, {}).then((r) => {
workspaceSettings = r
})

View File

@ -117,6 +117,7 @@ export default mergeIds(settingId, setting, {
Calendar: '' as IntlString,
StartOfTheWeek: '' as IntlString,
SystemSetupString: '' as IntlString,
DefaultString: '' as IntlString
DefaultString: '' as IntlString,
WorkspaceNamePattern: '' as IntlString
}
})

View File

@ -342,6 +342,8 @@ export async function createWorkspace (
const { workspaceName, region } = params
const { account } = decodeTokenVerbose(ctx, token)
checkRateLimit(account, workspaceName)
ctx.info('Creating workspace record', { workspaceName, account, region })
// Any confirmed social ID will do
@ -410,6 +412,12 @@ export async function createInviteLink (
})
}
// TODO: Temporary solution to prevent spam using sendInvite
const invitesSend = new Map<string, {
lastSend: number
totalSend: number
}>()
export async function sendInvite (
ctx: MeasureContext,
db: AccountDB,
@ -433,6 +441,8 @@ export async function sendInvite (
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspaceUuid }))
}
checkRateLimit(account, workspaceUuid)
const expHours = 48
const exp = expHours * 60 * 60 * 1000
@ -444,6 +454,34 @@ export async function sendInvite (
ctx.info('Invite has been sent', { to: inviteEmail.to, workspaceUuid: workspace.uuid, workspaceName: workspace.name })
}
function checkRateLimit (email: string, workspaceName: string): void {
const now = Date.now()
const lastInvites = invitesSend.get(email)
if (lastInvites !== undefined) {
lastInvites.totalSend++
lastInvites.lastSend = now
if (lastInvites.totalSend > 5 && (now - lastInvites.lastSend) < 60 * 1000) {
// Less 60 seconds between invites
throw new PlatformError(
new Status(Severity.ERROR, platform.status.WorkspaceRateLimit, { workspace: workspaceName })
)
}
invitesSend.delete(email)
} else {
invitesSend.set(email, {
lastSend: now,
totalSend: 1
})
}
// We need to cleanup map
for (const [k, vv] of invitesSend.entries()) {
if (vv.lastSend < now - 60 * 1000) {
invitesSend.delete(k)
}
}
}
export async function resendInvite (
ctx: MeasureContext,
db: AccountDB,
@ -463,6 +501,8 @@ export async function resendInvite (
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspaceUuid }))
}
checkRateLimit(account, workspaceUuid)
const expHours = 48
const newExp = Date.now() + expHours * 60 * 60 * 1000

View File

@ -1193,7 +1193,7 @@ export async function getInviteEmail (
): Promise<EmailInfo> {
const front = getFrontUrl(branding)
const link = concatLink(front, `/login/join?inviteId=${inviteId}`)
const ws = workspace.name !== '' ? workspace.name : workspace.url
const ws = (workspace.name !== '' ? workspace.name : workspace.url).replace(/[<>/]/g, '').slice(0, 40)
const lang = branding?.language
return {