Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions frontend/src/api/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,13 +262,14 @@ export async function getOIDCConfig(): Promise<OIDCConfigResponse> {
*
* 后端通过 `auth.registration_mode` 控制是否允许自助注册:
* - "self_serve" 保留现有自助注册入口(默认)
* - "invite_only" 关闭注册,要求管理员邀请
* - "invite_register" 仅持有效邀请链接可注册
* - "invite_only" 禁止注册,已有账号仍可接受邀请
*
* 失败时回落到 self_serve,避免接口异常导致注册入口直接消失。
* 失败时隐藏注册入口,避免误展示不允许的注册流程。
*/
export interface AuthConfigResponse {
success: boolean
registration_mode: 'self_serve' | 'invite_only' | string
registration_mode: 'self_serve' | 'invite_register' | 'invite_only' | string
complex_password_enabled: boolean
}

Expand All @@ -277,7 +278,7 @@ export async function getAuthConfig(): Promise<AuthConfigResponse> {
const response = await get('/api/v1/auth/config')
return response as unknown as AuthConfigResponse
} catch {
return { success: false, registration_mode: 'self_serve', complex_password_enabled: false }
return { success: false, registration_mode: '', complex_password_enabled: false }
}
}

Expand Down
7 changes: 4 additions & 3 deletions frontend/src/i18n/locales/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4620,7 +4620,7 @@ export default {
},
keyLabels: {
auth: {
registration_mode: 'Self-service registration mode',
registration_mode: 'Registration mode',
default_tenant_mode: 'Default workspace provisioning',
complex_password_enabled: 'Require complex password'
},
Expand Down Expand Up @@ -4651,7 +4651,7 @@ export default {
},
keyDescriptions: {
auth: {
registration_mode: 'Self-service registration mode. self_serve = anyone can register an account; invite_only = public registration is disabled and only Owners/Admins can invite. Takes effect immediately after saving, but use self_serve with care (the public internet will send spam sign-ups).',
registration_mode: 'Registration mode. Open registration allows anyone to create an account; invitation registration requires a valid invitation link; disabled registration prevents account creation while existing accounts can still accept invitations. Changes take effect immediately.',
default_tenant_mode: 'Workspace provisioning after public registration. create_personal creates an Owner workspace; tenantless creates only the account until the user accepts an invitation or creates a workspace. Applies to new users only.',
complex_password_enabled: 'Whether to require complex passwords. When enabled, passwords must contain uppercase and lowercase letters, numbers, and special characters. Changes take effect immediately and only apply to newly registered users or new password changes/resets. Special characters include {specialChars}'
},
Expand Down Expand Up @@ -4684,7 +4684,8 @@ export default {
auth: {
registration_mode: {
self_serve: 'Self-service (anyone can register)',
invite_only: 'Invite only (public registration disabled)'
invite_register: 'Invitation registration (valid link required)',
invite_only: 'Registration disabled (existing accounts can accept invitations)'
},
default_tenant_mode: {
create_personal: 'Create personal workspace',
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/i18n/locales/ja-JP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4620,7 +4620,7 @@ export default {
},
keyLabels: {
auth: {
registration_mode: 'セルフサービス登録モード',
registration_mode: '登録モード',
default_tenant_mode: 'デフォルトのワークスペース作成方式',
complex_password_enabled: '複雑なパスワードを必須にする'
},
Expand Down Expand Up @@ -4651,7 +4651,7 @@ export default {
},
keyDescriptions: {
auth: {
registration_mode: 'セルフサービス登録のモードです。self_serveは誰でもアカウントを登録でき、invite_onlyは公開登録を無効にし、オーナー/管理者による招待のみを許可します。保存後すぐに反映されますが、self_serveはインターネットからのスパム登録を招くため慎重に利用してください。',
registration_mode: '登録モード。公開登録では誰でもアカウントを作成できます。招待登録には有効な招待リンクが必要です。登録禁止でも既存アカウントは招待を承諾できます。保存後すぐに反映されます。',
default_tenant_mode: '公開登録後のワークスペース作成方式です。create_personalはオーナー権限のワークスペースを作成し、tenantlessはアカウントのみを作成して、ユーザが招待を承諾するかワークスペースを作成するまで待ちます。新規ユーザにのみ適用されます。',
complex_password_enabled: '複雑なパスワードを必須にするかどうかです。有効にすると、パスワードに大文字・小文字・数字・特殊文字を含める必要があります。変更はすぐに反映され、新規登録ユーザおよび新たなパスワード変更・リセットにのみ適用されます。特殊文字は{specialChars}です'
},
Expand Down Expand Up @@ -4684,7 +4684,8 @@ export default {
auth: {
registration_mode: {
self_serve: 'セルフサービス(誰でも登録可能)',
invite_only: '招待のみ(公開登録は無効)'
invite_register: '招待リンクでのみ登録可能',
invite_only: '登録禁止(既存アカウントは招待を承諾可能)'
},
default_tenant_mode: {
create_personal: '個人ワークスペースを作成',
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/i18n/locales/ko-KR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3409,7 +3409,8 @@ export default {
},
registration_mode: {
self_serve: '셀프 가입 (누구나 가입 가능)',
invite_only: '초대 전용 (공개 가입 비활성)'
invite_register: '초대 가입 (유효한 링크 필요)',
invite_only: '가입 금지 (기존 계정은 초대 수락 가능)'
}
}
},
Expand Down Expand Up @@ -3439,7 +3440,7 @@ export default {
docker_enabled: 'Docker 샌드박스 백엔드를 허용할지 설정합니다. 로컬 docker.sock은 호스트 root와 같으므로 기본값은 꺼짐입니다. 시스템 관리자만 켤 수 있으며 저장 즉시 적용됩니다. 데몬 소켓을 마운트했거나 TLS가 있는 원격 tcp:// 를 쓰는 프라이빗 단일 노드에서만 켜세요.'
},
auth: {
registration_mode: '셀프 가입 모드입니다. self_serve = 누구나 계정을 만들 수 있음; invite_only = 공개 가입을 끄고 Owner/Admin만 초대 가능. 저장 즉시 적용되며, self_serve는 스팸 가입이 들어올 수 있으니 신중히 사용하세요.',
registration_mode: '가입 모드입니다. 공개 가입은 누구나 계정을 만들 수 있고, 초대 가입은 유효한 초대 링크가 필요합니다. 가입 금지 상태에서도 기존 계정은 초대를 수락할 수 있습니다. 저장 즉시 적용됩니다.',
default_tenant_mode: '공개 가입 후 공간 초기화 정책입니다. create_personal은 개인 공간을 만들고 Owner를 부여하며, tenantless는 초대 수락 또는 직접 공간 생성 전까지 계정만 만듭니다.',
complex_password_enabled: '복잡한 비밀번호를 사용할지 여부입니다. 활성화하면 비밀번호에 대문자, 소문자, 숫자 및 특수 문자가 포함되어야 합니다. 변경 사항은 즉시 적용되며, 새로 가입하는 사용자 또는 비밀번호를 새로 변경하거나 재설정하는 경우에만 적용됩니다. 특수 문자는 다음을 포함합니다: {specialChars}'
}
Expand Down Expand Up @@ -3470,7 +3471,7 @@ export default {
docker_enabled: 'Docker 샌드박스 사용'
},
auth: {
registration_mode: '셀프 가입 모드',
registration_mode: '가입 모드',
default_tenant_mode: '기본 공간 프로비저닝',
complex_password_enabled: '복잡한 비밀번호 사용'
}
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/i18n/locales/ru-RU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3409,7 +3409,8 @@ export default {
},
registration_mode: {
self_serve: 'Самостоятельная (любой может зарегистрироваться)',
invite_only: 'Только по приглашению (открытая регистрация отключена)'
invite_register: 'Регистрация по приглашению (нужна действующая ссылка)',
invite_only: 'Регистрация закрыта (существующие аккаунты могут принять приглашение)'
}
}
},
Expand Down Expand Up @@ -3439,7 +3440,7 @@ export default {
docker_enabled: 'Разрешить бэкенд песочницы Docker. Локальный docker.sock равносилен root на хосте, поэтому по умолчанию выключено. Включить может только системный администратор; изменение действует сразу. Включайте только на частной одноузловой установке с примонтированным сокетом демона или удалённым tcp:// с TLS.'
},
auth: {
registration_mode: 'Режим самостоятельной регистрации. self_serve = любой может создать аккаунт; invite_only = открытая регистрация отключена, приглашать могут только Owner/Admin. Вступает в силу сразу после сохранения; используйте self_serve осторожно (в публичном интернете появятся спам-регистрации).',
registration_mode: 'Режим регистрации. Открытая регистрация позволяет любому создать аккаунт; регистрация по приглашению требует действующую ссылку; при закрытой регистрации существующие аккаунты могут принимать приглашения. Изменения применяются сразу.',
default_tenant_mode: 'Политика пространства после открытой регистрации. create_personal создаёт личное пространство с ролью Owner; tenantless создаёт только аккаунт до принятия приглашения или самостоятельного создания пространства.',
complex_password_enabled: 'Определяет, требуется ли сложный пароль. При включении пароль должен содержать прописные и строчные буквы, цифры и специальные символы. Изменение вступает в силу немедленно и применяется только к новым пользователям при регистрации, а также при изменении или сбросе пароля. Специальные символы включают: {specialChars}'
}
Expand Down Expand Up @@ -3470,7 +3471,7 @@ export default {
docker_enabled: 'Включить песочницу Docker'
},
auth: {
registration_mode: 'Режим самостоятельной регистрации',
registration_mode: 'Режим регистрации',
default_tenant_mode: 'Создание пространства по умолчанию',
complex_password_enabled: 'Включить сложные пароли'
}
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/i18n/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3411,7 +3411,8 @@ export default {
},
registration_mode: {
self_serve: '自助注册(任何人可注册)',
invite_only: '仅邀请(关闭公网注册)'
invite_register: '仅限邀请注册(需有效邀请链接)',
invite_only: '禁止注册(已有账号仍可接受邀请)'
}
}
},
Expand Down Expand Up @@ -3441,7 +3442,7 @@ export default {
docker_enabled: '是否允许 Docker 沙箱后端。本机 docker.sock 等同宿主机 root,默认关闭。仅系统管理员可打开;打开后立即生效,无需重启。私有化单机且已挂载 daemon socket,或配置了带 TLS 的远程 tcp:// 时再启用。'
},
auth: {
registration_mode: '自助注册模式。self_serve = 任何人可注册账号;invite_only = 关闭公网注册,仅 Owner/Admin 可邀请。修改后立即生效,但谨慎对待 self_serve(公网会接受 spam)。',
registration_mode: '注册模式。开放注册允许任何人创建账号;仅限邀请注册要求有效邀请链接;禁止注册不允许创建账号,但已有账号仍可接受邀请。保存后立即生效。',
default_tenant_mode: '公开注册后的空间初始化策略。create_personal 会自动创建个人空间并授予 Owner;tenantless 仅创建账户,用户需要接受邀请或主动创建空间。只影响之后注册的用户。',
complex_password_enabled: '是否启用复杂密码。开启后密码必须包含大小写字母、数字和特殊字符。修改后立即生效,只影响新注册用户或新密码修改/重置操作。特殊字符包含:{specialChars}'
}
Expand Down Expand Up @@ -3472,7 +3473,7 @@ export default {
docker_enabled: '启用 Docker 沙箱'
},
auth: {
registration_mode: '自助注册模式',
registration_mode: '注册模式',
default_tenant_mode: '注册默认空间策略',
complex_password_enabled: '启用复杂密码'
}
Expand Down
41 changes: 17 additions & 24 deletions frontend/src/views/auth/Login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@
<div class="form-panel">
<!-- Login Card -->
<div class="form-card" v-if="!isRegisterMode">
<!-- invite_only 模式下共享链接停在登录卡,同样需要邀请上下文。 -->
<!-- 登录已有账号时保留邀请上下文。 -->
<div v-if="inviteLookup" class="invite-banner">
<t-icon name="link" class="invite-banner__icon" />
<div class="invite-banner__text">
Expand Down Expand Up @@ -253,11 +253,8 @@
</div>
</div>

<!-- Register Card. Renders when the user is in register mode
AND either self-service registration is enabled OR they
arrived with a valid share-link token (which bypasses the
invite_only gate). -->
<div class="form-card" v-if="isRegisterMode && (registrationEnabled || inviteLookup)">
<!-- Registration must be allowed for this mode and invitation. -->
<div class="form-card" v-if="isRegisterMode && registrationEnabled">
<!-- Share-link banner: shown only when ?token= resolved to a
real invitation row. Sits above the form header so the
invitee instantly sees who invited them and into which
Expand Down Expand Up @@ -345,6 +342,7 @@ import { useRoute, useRouter } from 'vue-router'
import { MessagePlugin } from 'tdesign-vue-next'
import { useRoleLabel } from '@/composables/useRoleLabel'
import { notifyLoginSuccess } from '@/utils/loginNotify'
import { canRegister } from './registrationPolicy'
import { newPasswordRules } from '@/utils/passwordPolicy'
import { Swiper, SwiperSlide } from 'swiper/vue'
import { Autoplay, EffectFade, Pagination } from 'swiper/modules'
Expand Down Expand Up @@ -416,16 +414,14 @@ const isRegisterMode = ref(false)
const showLanguageMenu = ref(false)
const oidcEnabled = ref(false)
const oidcProviderName = ref('')
// registrationEnabled defaults to true so that on first paint the Register
// link is visible; the actual mode is fetched from /auth/config in onMounted.
// In invite_only mode the link/card are hidden.
const registrationEnabled = ref(true)
// Wait for configuration before showing a registration entry.
const registrationMode = ref('')
const registrationEnabled = computed(() => canRegister(registrationMode.value, !!inviteLookup.value))
const complexPasswordEnabled = ref(false)

// invite-link state. When the URL carries ?token=xxx we resolve it to
// the originating tenant + role and switch the form into a "register
// via invitation" mode. The token bypasses the normal invite_only
// gate — possessing it IS the authorisation. Submitting the register
// via invitation" mode when invitation registration is enabled. Submitting the register
// form with this set hits /auth/register-by-invite (auto-login on
// success) instead of /auth/register.
const inviteToken = ref('')
Expand Down Expand Up @@ -507,7 +503,7 @@ const registerRules = computed(() => ({

// Toggle login/register mode
const toggleMode = () => {
isRegisterMode.value = !isRegisterMode.value
isRegisterMode.value = !isRegisterMode.value && registrationEnabled.value

Object.keys(registerData).forEach(key => {
(registerData as any)[key] = ''
Expand Down Expand Up @@ -612,16 +608,14 @@ const loadOIDCConfig = async () => {
}
}

// loadAuthConfig fetches /auth/config and caches whether self-service
// registration is allowed. Failures fall back to "enabled" so a transient
// network glitch doesn't lock new users out of an open deployment.
// Registration stays hidden if configuration cannot be loaded.
const loadAuthConfig = async () => {
try {
const response = await getAuthConfig()
registrationEnabled.value = response.registration_mode !== 'invite_only'
registrationMode.value = response.success ? response.registration_mode : ''
complexPasswordEnabled.value = response.complex_password_enabled
} catch {
registrationEnabled.value = true
registrationMode.value = ''
complexPasswordEnabled.value = false
}
}
Expand Down Expand Up @@ -707,6 +701,7 @@ const handleLogin = async () => {
// login on success); without -> the normal self-service register
// (drops back to the login form for the user to sign in).
const handleRegister = async () => {
if (!registrationEnabled.value) return
try {
const valid = await registerFormRef.value?.validate()
if (valid !== true) return
Expand Down Expand Up @@ -794,17 +789,15 @@ onMounted(async () => {
inviteLookupLoading.value = false
}

// 2. 已登录则直接兑换 token 进入空间(两种模式通用)。
// 2. 已登录则直接兑换 token 进入空间(三种模式通用)。
if (authStore.isLoggedIn && (await authStore.refreshFromAuthMe())) {
await acceptAndEnter(tokenFromQuery)
return
}

// 3. 未登录:按注册模式决定界面。invite_only 停在登录页、登录后再兑换;self_serve 保持注册流程。
const cfg = await getAuthConfig()
const inviteOnly = cfg.registration_mode === 'invite_only'
registrationEnabled.value = !inviteOnly
isRegisterMode.value = !inviteOnly
// 3. Only enabled registration modes may create an invited account.
await loadAuthConfig()
isRegisterMode.value = registrationEnabled.value
loadOIDCConfig()
return
}
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/views/auth/registrationPolicy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { canRegister } from './registrationPolicy'

for (const [mode, withoutInvite, withInvite] of [
['self_serve', true, true],
['invite_register', false, true],
['invite_only', false, false],
['', false, false],
['unknown', false, false],
] as const) {
test(`registration entry for ${mode || 'unavailable configuration'}`, () => {
assert.equal(canRegister(mode, false), withoutInvite)
assert.equal(canRegister(mode, true), withInvite)
})
}
4 changes: 4 additions & 0 deletions frontend/src/views/auth/registrationPolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** Whether the login page may offer password registration. */
export function canRegister(mode: string, hasValidInvitation: boolean): boolean {
return mode === 'self_serve' || (mode === 'invite_register' && hasValidInvitation)
}
Loading
Loading