fix: пароль/orphan signup/tenant-guard toast/dashboard счётчик
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 48s
CI / Web (React + Vite) (push) Successful in 39s
Docker API / Build + push API (push) Successful in 1m9s
Docker Public / Build + push Public (push) Successful in 41s
Docker Web / Build + push Web (push) Successful in 31s
Docker API / Deploy API on stage (push) Successful in 17s
Docker Public / Deploy Public on stage (push) Successful in 11s
Docker Web / Deploy Web on stage (push) Successful in 12s
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 48s
CI / Web (React + Vite) (push) Successful in 39s
Docker API / Build + push API (push) Successful in 1m9s
Docker Public / Build + push Public (push) Successful in 41s
Docker Web / Build + push Web (push) Successful in 31s
Docker API / Deploy API on stage (push) Successful in 17s
Docker Public / Deploy Public on stage (push) Successful in 11s
Docker Web / Deploy Web on stage (push) Successful in 12s
- validation: убрана клиентская проверка «должна быть заглавная/цифра» — она расходилась с серверной политикой Identity и блокировала валидные пароли. Серверный Identity сам валидирует и возвращает конкретное сообщение в общий error-bar формы. Клиент проверяет только длину 8. - /api/auth/signup: если AppUser с таким email уже есть, но он orphan (org удалена / архивирована / IsActive=false) — реактивируем его и привязываем к новой org вместо отказа «уже зарегистрирован». SuperAdmin не реактивируется (ему сценарий неактуален). Пароль перезаписывается на тот, что юзер ввёл сейчас. - TenantRouteGuard: убран alert «Откройте конкретную организацию через Открыть как…». На каждом редиректе SuperAdmin'а из tenant-роута всплывал window.alert — раздражал. Поведение редиректа сохранено. AppLayout чистит legacy ключ из sessionStorage. - SuperAdmin dashboard: KPI «Пользователей» теперь показывает количество активных, а в подсказке — сколько деактивированных. Раньше показывал total (включая orphan) — юзер видел «2», но в реальных списках их не было, потому что orphan-юзеры уже деактивированы.
This commit is contained in:
parent
ed24f5e354
commit
691448201d
|
|
@ -42,7 +42,19 @@ public async Task<ActionResult<SignupResult>> Signup([FromBody] SignupInput inpu
|
|||
|
||||
var existing = await _userMgr.FindByEmailAsync(input.Email);
|
||||
if (existing is not null)
|
||||
return BadRequest(new { error = "Пользователь с таким email уже зарегистрирован." });
|
||||
{
|
||||
// Если AppUser orphan (org удалена / он деактивирован) — реактивируем
|
||||
// его с новой organization вместо отказа. SuperAdmin'а трогать
|
||||
// нельзя: у него email тоже email и блокировка не должна давать
|
||||
// обходить вход.
|
||||
var isSuperAdmin = await _userMgr.IsInRoleAsync(existing, "SuperAdmin");
|
||||
var orgAlive = existing.OrganizationId is not null
|
||||
&& await _db.Organizations.IgnoreQueryFilters()
|
||||
.AnyAsync(o => o.Id == existing.OrganizationId.Value && !o.IsArchived, ct);
|
||||
if (isSuperAdmin || orgAlive)
|
||||
return BadRequest(new { error = "Пользователь с таким email уже зарегистрирован." });
|
||||
// Иначе — переиспользуем существующий AppUser ниже.
|
||||
}
|
||||
|
||||
// 1. Organization + полный bootstrap tenant-сущностей.
|
||||
var kzt = await _db.Currencies.FirstOrDefaultAsync(c => c.Code == "KZT", ct);
|
||||
|
|
@ -58,25 +70,50 @@ public async Task<ActionResult<SignupResult>> Signup([FromBody] SignupInput inpu
|
|||
await _db.SaveChangesAsync(ct);
|
||||
await DevDataSeeder.SeedTenantReferencesAsync(_db, org.Id, ct);
|
||||
|
||||
// 2. AppUser в роли Identity Admin, привязан к этой организации.
|
||||
var user = new User
|
||||
// 2. AppUser — либо новый, либо реактивируем orphan (см. выше).
|
||||
User user;
|
||||
if (existing is not null)
|
||||
{
|
||||
UserName = input.Email.Trim(),
|
||||
Email = input.Email.Trim(),
|
||||
EmailConfirmed = true,
|
||||
FullName = input.OrganizationName.Trim(),
|
||||
OrganizationId = org.Id,
|
||||
IsActive = true,
|
||||
};
|
||||
var ur = await _userMgr.CreateAsync(user, input.Password);
|
||||
if (!ur.Succeeded)
|
||||
{
|
||||
// Откат: убираем органзацию чтобы не оставить orphan.
|
||||
_db.Organizations.Remove(org);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return BadRequest(new { error = string.Join("; ", ur.Errors.Select(e => e.Description)) });
|
||||
user = existing;
|
||||
user.OrganizationId = org.Id;
|
||||
user.IsActive = true;
|
||||
user.FullName = input.OrganizationName.Trim();
|
||||
user.Email = input.Email.Trim();
|
||||
user.EmailConfirmed = true;
|
||||
await _userMgr.UpdateAsync(user);
|
||||
// Сброс пароля на тот, что юзер ввёл сейчас.
|
||||
await _userMgr.RemovePasswordAsync(user);
|
||||
var pwRes = await _userMgr.AddPasswordAsync(user, input.Password);
|
||||
if (!pwRes.Succeeded)
|
||||
{
|
||||
_db.Organizations.Remove(org);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return BadRequest(new { error = string.Join("; ", pwRes.Errors.Select(e => e.Description)) });
|
||||
}
|
||||
// Roles: убедимся что есть Admin. SuperAdmin сюда не попадает (отсечено выше).
|
||||
if (!await _userMgr.IsInRoleAsync(user, "Admin"))
|
||||
await _userMgr.AddToRoleAsync(user, "Admin");
|
||||
}
|
||||
else
|
||||
{
|
||||
user = new User
|
||||
{
|
||||
UserName = input.Email.Trim(),
|
||||
Email = input.Email.Trim(),
|
||||
EmailConfirmed = true,
|
||||
FullName = input.OrganizationName.Trim(),
|
||||
OrganizationId = org.Id,
|
||||
IsActive = true,
|
||||
};
|
||||
var ur = await _userMgr.CreateAsync(user, input.Password);
|
||||
if (!ur.Succeeded)
|
||||
{
|
||||
_db.Organizations.Remove(org);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return BadRequest(new { error = string.Join("; ", ur.Errors.Select(e => e.Description)) });
|
||||
}
|
||||
await _userMgr.AddToRoleAsync(user, "Admin");
|
||||
}
|
||||
await _userMgr.AddToRoleAsync(user, "Admin");
|
||||
|
||||
// 3. Owner Employee с системной ролью «Администратор».
|
||||
var adminRole = await _db.EmployeeRoles.IgnoreQueryFilters()
|
||||
|
|
|
|||
|
|
@ -19,10 +19,13 @@ export function validateEmail(value: string): string | null {
|
|||
}
|
||||
|
||||
export function validatePassword(value: string): string | null {
|
||||
// На клиенте — только базовая проверка длины. Все правила сложности
|
||||
// (заглавные, цифры, спецсимволы) проверяет серверный ASP.NET Identity
|
||||
// и возвращает конкретное сообщение в общий error-bar формы. Дублировать
|
||||
// правила на фронте опасно: они расходятся с серверной политикой и
|
||||
// блокируют ввод валидных паролей.
|
||||
if (!value) return 'Это поле обязательно для заполнения'
|
||||
if (value.length < 8) return 'Пароль должен быть не менее 8 символов'
|
||||
if (!/[A-ZА-Я]/.test(value)) return 'Пароль должен содержать хотя бы одну заглавную букву'
|
||||
if (!/[0-9]/.test(value)) return 'Пароль должен содержать хотя бы одну цифру'
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -100,12 +100,9 @@ export function AppLayout() {
|
|||
// При активном «открыто как…» — слабая жёлтая тонировка фона tenant-области
|
||||
// даёт периферийный сигнал «я не в своей админке».
|
||||
const inOverride = typeof window !== 'undefined' && !!localStorage.getItem('superAdminAsOrg')
|
||||
// Toast на guard-message (TenantRouteGuard кладёт сообщение в sessionStorage
|
||||
// перед редиректом — показываем разово).
|
||||
useEffect(() => {
|
||||
const msg = sessionStorage.getItem('tenant-guard-msg')
|
||||
if (msg) { sessionStorage.removeItem('tenant-guard-msg'); alert(msg) }
|
||||
}, [])
|
||||
// Tenant-guard tost удалён — раздражал юзера на каждом редиректе.
|
||||
// Если когда-нибудь понадобится — лучше через ToastContainer, не alert().
|
||||
useEffect(() => { sessionStorage.removeItem('tenant-guard-msg') }, [])
|
||||
// Если SuperAdmin зашёл, а в системе ноль организаций — выкидываем
|
||||
// на setup wizard (нельзя пропустить, пока не создадут первую орг).
|
||||
const location2 = useLocation()
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ export function TenantRouteGuard({ children }: { children: React.ReactNode }) {
|
|||
// eslint-disable-next-line no-console
|
||||
console.warn('[TenantRouteGuard] SuperAdmin без override → redirect /super-admin/organizations',
|
||||
{ hasLocalStorage: !!localStorage.getItem('superAdminAsOrg') })
|
||||
try { sessionStorage.setItem('tenant-guard-msg', 'Откройте конкретную организацию через "Открыть как…"') } catch { /* ignore */ }
|
||||
navigate('/super-admin/organizations', { replace: true })
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -26,10 +26,12 @@ export function validateEmail(value: string): string | null {
|
|||
}
|
||||
|
||||
export function validatePassword(value: string): string | null {
|
||||
// На клиенте — только проверка длины. Сложность (заглавные/цифры/
|
||||
// спецсимволы) проверяет серверный Identity и возвращает конкретное
|
||||
// сообщение в error-bar. Дубль правил на фронте расходится с серверной
|
||||
// политикой и блокирует ввод валидных паролей.
|
||||
if (!value) return messages.required
|
||||
if (value.length < 8) return messages.passwordShort
|
||||
if (!/[A-ZА-Я]/.test(value)) return messages.passwordNoUpper
|
||||
if (!/[0-9]/.test(value)) return messages.passwordNoDigit
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -125,8 +125,11 @@ export function SuperAdminDashboardPage() {
|
|||
hint="Скоро · после внедрения биллинга" accent="rose" muted />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 sm:gap-4">
|
||||
<Kpi icon={Users} label="Пользователей" value={fmt.format(data?.totalUsers ?? 0)}
|
||||
hint={`${data?.activeUsers ?? 0} активных`} accent="sky" />
|
||||
<Kpi icon={Users} label="Пользователей" value={fmt.format(data?.activeUsers ?? 0)}
|
||||
hint={data && data.totalUsers > data.activeUsers
|
||||
? `+${data.totalUsers - data.activeUsers} деактивированных`
|
||||
: 'все активны'}
|
||||
accent="sky" />
|
||||
<Kpi icon={UserPlus} label="Регистраций за 30 дней" value={fmt.format(data?.registrationsLast30Days ?? 0)}
|
||||
hint="Новые организации" accent="amber" />
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue