backend phase 2: identity — phone-OTP auth, sessions & roles (REST)

- six REST endpoints (auth/request_otp, verify_otp, refresh, logout, me,
  me/select_role) wrapping the existing JWE/TOTP/RBAC engine
- usr.UserSessions with refresh-token rotation + stolen-token (replay)
  detection → revoke-all + 401; logout rotates the security stamp
- users extended: gender, national_id (enc, NULL until KYC),
  shahkar_verified_at (auto-reset on phone change), phone_hash UNIQUE,
  is_active, deleted_at + soft-delete filter; phone/email/national_id
  encrypted at rest via IFieldEncryptor value converter
- user_roles grant/revoke audit trail + global revoked filter; 7 roles
  seeded; admin sub-roles never self-assignable (403)
- ISmsSender seam (mock logs the OTP code) replaces the TODO log lines
- OperationResult/BaseController learned enveloped 401/403
- auth knobs as platform_configs rows (resend/attempts/session TTL)
- migration IdentitySessionsAndUserExtensions applied to the dev DB
- 24 new tests incl. Baya.Test.Api (WebApplicationFactory over SQLite);
  47 total green, zero new build warnings; swagger snapshot + contract
  (identity-auth.md), handoff, report, mocks-registry updated

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
hamid
2026-07-02 02:34:11 +03:30
parent 94fdcbe0d1
commit 3a51305343
88 changed files with 4619 additions and 91 deletions
+652 -1
View File
@@ -7,7 +7,7 @@
},
"servers": [
{
"url": "https://localhost:5002"
"url": "http://localhost:5188"
}
],
"paths": {
@@ -114,6 +114,304 @@
]
}
},
"/api/v1/auth/request_otp": {
"post": {
"tags": [
"Auth"
],
"operationId": "Auth_RequestOtp",
"requestBody": {
"x-name": "command",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RequestOtpCommand"
}
}
},
"required": true,
"x-position": 1
},
"responses": {
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString"
}
}
}
},
"401": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResult"
}
}
}
},
"403": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResult"
}
}
}
},
"500": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResult"
}
}
}
},
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResultOfRequestOtpResult"
}
}
}
}
}
}
},
"/api/v1/auth/verify_otp": {
"post": {
"tags": [
"Auth"
],
"operationId": "Auth_VerifyOtp",
"requestBody": {
"x-name": "command",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VerifyOtpCommand"
}
}
},
"required": true,
"x-position": 1
},
"responses": {
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString"
}
}
}
},
"401": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResult"
}
}
}
},
"403": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResult"
}
}
}
},
"500": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResult"
}
}
}
},
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResultOfAuthTokensResult"
}
}
}
}
}
}
},
"/api/v1/auth/refresh": {
"post": {
"tags": [
"Auth"
],
"operationId": "Auth_Refresh",
"requestBody": {
"x-name": "command",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RefreshTokenCommand"
}
}
},
"required": true,
"x-position": 1
},
"responses": {
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString"
}
}
}
},
"401": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResult"
}
}
}
},
"403": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResult"
}
}
}
},
"500": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResult"
}
}
}
},
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResultOfAuthTokensResult"
}
}
}
}
},
"security": [
{
"bearer": []
}
]
}
},
"/api/v1/auth/logout": {
"post": {
"tags": [
"Auth"
],
"operationId": "Auth_Logout",
"requestBody": {
"x-name": "command",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LogoutCommand"
}
}
},
"required": true,
"x-position": 1
},
"responses": {
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString"
}
}
}
},
"401": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResult"
}
}
}
},
"403": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResult"
}
}
}
},
"500": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResult"
}
}
}
},
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResult"
}
}
}
}
},
"security": [
{
"Bearer": []
}
]
}
},
"/api/v1/holidays/get_holidays": {
"get": {
"tags": [
@@ -376,6 +674,150 @@
]
}
},
"/api/v1/me": {
"get": {
"tags": [
"Me"
],
"summary": "Retrieves a Me by unique id",
"operationId": "Me_GetMe",
"responses": {
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString"
}
}
}
},
"401": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResult"
}
}
}
},
"403": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResult"
}
}
}
},
"500": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResult"
}
}
}
},
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResultOfMeResult"
}
}
}
}
},
"security": [
{
"Bearer": []
}
]
}
},
"/api/v1/me/select_role": {
"post": {
"tags": [
"Me"
],
"description": "Role claims live inside the access token — after selecting a role the client should\n refresh its tokens to pick the new role up.",
"operationId": "Me_SelectRole",
"requestBody": {
"x-name": "command",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SelectRoleCommand"
}
}
},
"required": true,
"x-position": 1
},
"responses": {
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString"
}
}
}
},
"401": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResult"
}
}
}
},
"403": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResult"
}
}
}
},
"500": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResult"
}
}
}
},
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResultOfMeResult"
}
}
}
}
},
"security": [
{
"Bearer": []
}
]
}
},
"/api/v1/notifications/get_notifications": {
"get": {
"tags": [
@@ -1466,6 +1908,141 @@
}
}
},
"ApiResultOfRequestOtpResult": {
"allOf": [
{
"$ref": "#/components/schemas/ApiResult"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"data": {
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/RequestOtpResult"
}
]
}
}
}
]
},
"RequestOtpResult": {
"type": "object",
"additionalProperties": false,
"properties": {
"otpSent": {
"type": "boolean"
},
"resendAvailableInSeconds": {
"type": "integer",
"format": "int32"
}
}
},
"RequestOtpCommand": {
"type": "object",
"additionalProperties": false,
"properties": {
"phone": {
"type": "string"
}
}
},
"ApiResultOfAuthTokensResult": {
"allOf": [
{
"$ref": "#/components/schemas/ApiResult"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"data": {
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/AuthTokensResult"
}
]
}
}
}
]
},
"AuthTokensResult": {
"type": "object",
"additionalProperties": false,
"properties": {
"accessToken": {
"type": "string"
},
"refreshToken": {
"type": "string"
},
"accessExpiresAt": {
"type": "string",
"format": "date-time"
},
"refreshExpiresAt": {
"type": "string",
"format": "date-time"
},
"isNewUser": {
"type": "boolean"
},
"roles": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"VerifyOtpCommand": {
"type": "object",
"additionalProperties": false,
"properties": {
"phone": {
"type": "string"
},
"code": {
"type": "string"
},
"deviceInfo": {
"type": "string",
"nullable": true
}
}
},
"RefreshTokenCommand": {
"type": "object",
"additionalProperties": false,
"properties": {
"refreshToken": {
"type": "string"
},
"deviceInfo": {
"type": "string",
"nullable": true
}
}
},
"LogoutCommand": {
"type": "object",
"additionalProperties": false,
"properties": {
"refreshToken": {
"type": "string",
"nullable": true
},
"everywhere": {
"type": "boolean"
}
}
},
"ApiResultOfPagedResultOfHolidayDto": {
"allOf": [
{
@@ -1568,6 +2145,80 @@
}
}
},
"ApiResultOfMeResult": {
"allOf": [
{
"$ref": "#/components/schemas/ApiResult"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"data": {
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/MeResult"
}
]
}
}
}
]
},
"MeResult": {
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"type": "integer",
"format": "int32"
},
"phone": {
"type": "string"
},
"firstName": {
"type": "string",
"nullable": true
},
"lastName": {
"type": "string",
"nullable": true
},
"gender": {
"type": "string",
"nullable": true
},
"isActive": {
"type": "boolean"
},
"roles": {
"type": "array",
"items": {
"type": "string"
}
},
"hasCustomerProfile": {
"type": "boolean"
},
"hasNurseProfile": {
"type": "boolean"
},
"nurseVerificationStatus": {
"type": "string"
}
}
},
"SelectRoleCommand": {
"type": "object",
"additionalProperties": false,
"properties": {
"role": {
"type": "string",
"nullable": true
}
}
},
"ApiResultOfPagedResultOfNotificationDto": {
"allOf": [
{