Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
- Сервер: Employee.PinHash (PBKDF2, миграция Phase36); POST
/api/organization/employees/{id}/pin — админ задаёт/снимает PIN (4–6 цифр,
уникален среди активных сотрудников на пересекающихся точках). Кастомный
OpenIddict-грант grant_type=pin (retail_point_id + pin): находит кассира,
привязанного к точке, по PIN и выдаёт токены. Брутфорс-защита — in-memory по
точке (5 неверных → блок 5 мин). Клиент-сидер досоздаёт gt:pin существующим
клиентам (без пересоздания). Пароль остаётся запасным.
- Касса: PIN-пад на зарегистрированном устройстве (маскированный ввод, 0–9/⌫/
Войти), переключение на вход по логину; AuthService.PinLoginAsync.
- Тесты: unit PinHasher (9), integration (3): вход по PIN выдаёт рабочий токен;
5 неверных → блок; дубль PIN на точке → 409. + скриншот PIN-пада в галерее.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
66 lines
3.9 KiB
C#
66 lines
3.9 KiB
C#
using foodmarket.Domain.Common;
|
||
|
||
namespace foodmarket.Domain.Organizations;
|
||
|
||
/// <summary>Сотрудник организации. Может быть привязан к учётной записи
|
||
/// (UserId), а может существовать без логина (например, кассир, которого
|
||
/// добавили в HR, но логин ещё не выдали).</summary>
|
||
public class Employee : TenantEntity
|
||
{
|
||
/// <summary>FK на Identity-юзера. Заполняется когда сотруднику выдан логин.
|
||
/// Null — запись без учётки.</summary>
|
||
public Guid? UserId { get; set; }
|
||
|
||
public string LastName { get; set; } = "";
|
||
public string FirstName { get; set; } = "";
|
||
public string? MiddleName { get; set; }
|
||
public string? Position { get; set; }
|
||
|
||
public string? Email { get; set; }
|
||
public string? Phone { get; set; }
|
||
|
||
/// <summary>Оклад в валюте организации, опц.</summary>
|
||
public decimal? Salary { get; set; }
|
||
/// <summary>ИИН (12 цифр для физлица в РК), опционально.</summary>
|
||
public string? TaxNumber { get; set; }
|
||
/// <summary>Произвольное описание (комментарий HR'а).</summary>
|
||
public string? Description { get; set; }
|
||
/// <summary>Аватар сотрудника. URL до файла в общем images-стораж.</summary>
|
||
public string? ImageUrl { get; set; }
|
||
|
||
public Guid RoleId { get; set; }
|
||
public EmployeeRole Role { get; set; } = null!;
|
||
|
||
/// <summary>Активен ли сотрудник. False — заблокирован, не может логиниться.
|
||
/// Двухступенчатое удаление: сначала «Уволить» (IsActive=false + FiredAt),
|
||
/// затем «Удалить» (IsDeleted=true + DeletedAt). Физически не удаляем
|
||
/// никогда — у сотрудника есть FK из документов (продаж, поставок).</summary>
|
||
public bool IsActive { get; set; } = true;
|
||
public DateTime? FiredAt { get; set; }
|
||
|
||
/// <summary>Soft-delete-флаг. Ставится только из состояния «уволен» (IsActive=false).
|
||
/// В UI и связанных документах отображается «Иванов И.И. (удалён)».
|
||
/// В списках сотрудников по умолчанию скрыты, доступны через фильтр.</summary>
|
||
public bool IsDeleted { get; set; }
|
||
public DateTime? DeletedAt { get; set; }
|
||
|
||
public ICollection<EmployeeRetailPointAssignment> RetailPointAssignments { get; set; }
|
||
= new List<EmployeeRetailPointAssignment>();
|
||
|
||
// ─ Быстрый вход кассира по PIN на зарегистрированном устройстве ─────
|
||
/// <summary>Хэш PIN-кода (PBKDF2 с солью, base64). null — PIN не задан,
|
||
/// вход только по паролю. Задаёт администратор в карточке сотрудника.
|
||
/// Брутфорс-защита (5 неверных → блок 5 мин) — на уровне точки (in-memory),
|
||
/// т.к. вход по PIN без логина не идентифицирует конкретного сотрудника.</summary>
|
||
public string? PinHash { get; set; }
|
||
}
|
||
|
||
/// <summary>Привязка сотрудника к кассе (для роли Кассир): к каким RetailPoint'ам
|
||
/// он может вставать. Если назначений нет — может ко всем (поведение по умолчанию).</summary>
|
||
public class EmployeeRetailPointAssignment : TenantEntity
|
||
{
|
||
public Guid EmployeeId { get; set; }
|
||
public Employee Employee { get; set; } = null!;
|
||
public Guid RetailPointId { get; set; }
|
||
}
|