feat(auth): несколько эталонных mail-доменов для верификации регистрации
Настройка «Эталон mail-домена» теперь хранит список доменов вместо
одного — email при регистрации принимается, если совпадает с любым из
них. Старое значение в БД ({"domain": str|None}) читается прозрачно
(обратная совместимость без Alembic-миграции) и переписывается в новую
форму ({"domains": [...]}) при первом же сохранении настроек. В админке
добавление/удаление доменов — списком чипов; на экране регистрации
подсказка о несовпадении домена перечисляет все эталонные варианты.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { AlertTriangle } from 'lucide-react'
|
||||
import { AlertTriangle, X } from 'lucide-react'
|
||||
import {
|
||||
getAdminSettings,
|
||||
sendTestEmail,
|
||||
@@ -60,7 +60,8 @@ function AdminSettingsForm({ data }: { data: SettingsOut }) {
|
||||
const [timezone, setTimezone] = useState(data.display_timezone)
|
||||
const [teamChoiceEnabled, setTeamChoiceEnabled] = useState(data.registration_team_choice)
|
||||
const [domainVerificationEnabled, setDomainVerificationEnabled] = useState(data.registration_email_domain_enabled)
|
||||
const [emailDomain, setEmailDomain] = useState(data.registration_email_domain ?? '')
|
||||
const [emailDomains, setEmailDomains] = useState(data.registration_email_domains)
|
||||
const [newDomainInput, setNewDomainInput] = useState('')
|
||||
const [contactEmailEnabled, setContactEmailEnabled] = useState(data.contact_email_enabled)
|
||||
const [contactEmail, setContactEmail] = useState(data.contact_email ?? '')
|
||||
const [testEmailTo, setTestEmailTo] = useState('')
|
||||
@@ -94,6 +95,20 @@ function AdminSettingsForm({ data }: { data: SettingsOut }) {
|
||||
},
|
||||
})
|
||||
|
||||
function addDomain() {
|
||||
const domain = newDomainInput.trim().toLowerCase().replace(/^@/, '')
|
||||
if (!domain || emailDomains.includes(domain)) {
|
||||
setNewDomainInput('')
|
||||
return
|
||||
}
|
||||
setEmailDomains([...emailDomains, domain])
|
||||
setNewDomainInput('')
|
||||
}
|
||||
|
||||
function removeDomain(domain: string) {
|
||||
setEmailDomains(emailDomains.filter((d) => d !== domain))
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
// Отправляем только реально изменённые поля (`SettingsUpdateIn` — набор
|
||||
// опциональных полей именно для этого): иначе, например, ai_level
|
||||
@@ -111,9 +126,8 @@ function AdminSettingsForm({ data }: { data: SettingsOut }) {
|
||||
if (domainVerificationEnabled !== data.registration_email_domain_enabled) {
|
||||
payload.registration_email_domain_enabled = domainVerificationEnabled
|
||||
}
|
||||
const trimmedDomain = emailDomain.trim() || null
|
||||
if (trimmedDomain !== (data.registration_email_domain ?? null)) {
|
||||
payload.registration_email_domain = trimmedDomain
|
||||
if (JSON.stringify(emailDomains) !== JSON.stringify(data.registration_email_domains)) {
|
||||
payload.registration_email_domains = emailDomains
|
||||
}
|
||||
if (contactEmailEnabled !== data.contact_email_enabled) {
|
||||
payload.contact_email_enabled = contactEmailEnabled
|
||||
@@ -245,19 +259,53 @@ function AdminSettingsForm({ data }: { data: SettingsOut }) {
|
||||
|
||||
<section className="settings-card">
|
||||
<h2>Эталон mail-домена</h2>
|
||||
<p className="desc">Домен, с которым сверяется email при регистрации, если включена верификация в «Модулях».</p>
|
||||
<p className="desc">Домены, с любым из которых сверяется email при регистрации, если включена верификация в «Модулях».</p>
|
||||
|
||||
<div className="settings-card-body settings-card-body--center">
|
||||
<div className="settings-card-body">
|
||||
{emailDomains.length > 0 && (
|
||||
<div className="participants-chips">
|
||||
{emailDomains.map((domain) => (
|
||||
<span className="participant-chip" key={domain}>
|
||||
{domain}
|
||||
<button
|
||||
type="button"
|
||||
className="participant-chip-remove"
|
||||
aria-label={`Убрать домен: ${domain}`}
|
||||
onClick={() => removeDomain(domain)}
|
||||
disabled={!domainVerificationEnabled}
|
||||
>
|
||||
<X style={{ width: 12, height: 12 }} aria-hidden="true" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="field" style={{ marginBottom: 0 }}>
|
||||
<label htmlFor="settings-email-domain">Домен корпоративной почты</label>
|
||||
<input
|
||||
id="settings-email-domain"
|
||||
type="text"
|
||||
placeholder="company.ru"
|
||||
value={emailDomain}
|
||||
disabled={!domainVerificationEnabled}
|
||||
onChange={(e) => setEmailDomain(e.target.value)}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: 'var(--space-2)' }}>
|
||||
<input
|
||||
id="settings-email-domain"
|
||||
type="text"
|
||||
placeholder="company.ru"
|
||||
value={newDomainInput}
|
||||
disabled={!domainVerificationEnabled}
|
||||
onChange={(e) => setNewDomainInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
addDomain()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={!domainVerificationEnabled || !newDomainInput.trim()}
|
||||
onClick={addDomain}
|
||||
>
|
||||
Добавить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user