a855cf1d80
CI/CD / CI · API (dotnet build + test) (push) Successful in 5m6s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 1m30s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m10s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 46s
CI/CD / CI · Koja (tsc) (push) Successful in 1m0s
CI/CD / Deploy · all services (push) Successful in 5m31s
Platform admins can generate a permanent recovery key per café (admin
panel → Cafés). The café Owner uses it to sign in when OTP access is lost;
once authenticated, all server-side data syncs as normal (data is per-café
on the server, the device only caches it).
Backend:
- Cafe.RecoveryKeyHash (SHA-256, unique index) + RecoveryKeyCreatedAt; migration
- RecoveryKeyGenerator util: MZ-XXXXX-XXXXX-XXXXX-XXXXX, ~190-bit entropy,
stored as SHA-256 (API-token pattern — raw key shown once, never retrievable)
- Admin: POST/DELETE /api/admin/cafes/{id}/recovery-key (key returned once);
café list now reports HasRecoveryKey + RecoveryKeyCreatedAt
- Login: POST /api/auth/login-key → exact-hash lookup → resolves café Owner →
issues normal JWT; rate-limited (auth-otp), suspended/no-owner guarded, logged
Admin UI: per-café generate / regenerate / revoke with one-time reveal + copy.
Dashboard login: discreet "ورود با کلید بازیابی" link → key field. fa/en/ar.
86 tests pass; all tsc clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
55 lines
2.4 KiB
C#
55 lines
2.4 KiB
C#
namespace Meezi.API.Models.Auth;
|
|
|
|
public record SendOtpRequest(string Phone);
|
|
|
|
/// <summary>Username + password login (alternative to OTP). Optional cafeId to scope to a specific café.</summary>
|
|
public record LoginWithPasswordRequest(string Username, string Password, string? CafeId = null);
|
|
|
|
/// <summary>Admin-issued recovery key login — logs the café Owner in when OTP access is lost.</summary>
|
|
public record LoginWithRecoveryKeyRequest(string Key);
|
|
|
|
public record VerifyOtpRequest(string Phone, string Code, string? CafeId = null);
|
|
|
|
public record RefreshTokenRequest(string RefreshToken);
|
|
|
|
public record SwitchCafeRequest(string CafeId);
|
|
|
|
/// <summary>Switch the active branch within the current café. Null = café-wide (Owner only).</summary>
|
|
public record SwitchBranchRequest(string? BranchId);
|
|
|
|
/// <summary>Step 1 of self-registration: send OTP to a new phone number.</summary>
|
|
/// <param name="Slug">Optional custom Koja slug (e.g. "lamiz-enghelab"). Auto-derived from CafeName if omitted.</param>
|
|
public record RegisterRequest(string Phone, string CafeName, string? Slug = null);
|
|
|
|
/// <summary>Step 2 of self-registration: verify OTP and create the cafe account.</summary>
|
|
public record VerifyRegisterRequest(string Phone, string Code);
|
|
|
|
/// <summary>One café membership entry returned when user belongs to multiple cafés.</summary>
|
|
public record CafeMembershipDto(string CafeId, string CafeName, string Role, string PlanTier);
|
|
|
|
/// <summary>A branch the signed-in employee may operate as, with their role there.</summary>
|
|
public record BranchMembershipDto(string BranchId, string BranchName, string Role);
|
|
|
|
public record AuthTokenResponse(
|
|
string AccessToken,
|
|
string RefreshToken,
|
|
DateTime ExpiresAt,
|
|
string UserId,
|
|
string CafeId,
|
|
string Role,
|
|
string PlanTier,
|
|
string Language,
|
|
string Actor = Meezi.Core.Constants.MeeziActorKinds.Merchant,
|
|
string? BranchId = null,
|
|
List<CafeMembershipDto>? Memberships = null,
|
|
string? BranchName = null,
|
|
bool IsCafeWide = false,
|
|
List<BranchMembershipDto>? Branches = null,
|
|
/// <summary>Effective capabilities for the active role — drives client-side page/action gating.</summary>
|
|
List<string>? Permissions = null);
|
|
|
|
public record SendOtpResponse(bool Sent, int ExpiresInSeconds);
|
|
|
|
/// <summary>Returned when a phone number belongs to multiple cafés and no CafeId was specified.</summary>
|
|
public record CafeChoicesResponse(List<CafeMembershipDto> Cafes);
|