| | | 1 | | using ArturRios.Heimdall.Command.Input; |
| | | 2 | | using ArturRios.Heimdall.Command.Output; |
| | | 3 | | using ArturRios.Heimdall.Domain.Enums; |
| | | 4 | | using ArturRios.Heimdall.Query.Input; |
| | | 5 | | using ArturRios.Heimdall.Query.Output; |
| | | 6 | | using ArturRios.Heimdall.Shared.Messages; |
| | | 7 | | using ArturRios.Heimdall.WebApi.Security; |
| | | 8 | | using ArturRios.Mediator.Command; |
| | | 9 | | using ArturRios.Mediator.Query; |
| | | 10 | | using ArturRios.Output; |
| | | 11 | | using ArturRios.Util.WebApi.AspNetCore; |
| | | 12 | | using ArturRios.Util.WebApi.Security.Attributes; |
| | | 13 | | using Microsoft.AspNetCore.Mvc; |
| | | 14 | | using Microsoft.AspNetCore.RateLimiting; |
| | | 15 | | |
| | | 16 | | namespace ArturRios.Heimdall.WebApi.Controllers; |
| | | 17 | | |
| | | 18 | | [Route("api")] |
| | 285 | 19 | | public class PersonController(CommandMediator commandMediator, QueryMediator queryMediator) : Controller |
| | | 20 | | { |
| | | 21 | | /// <summary> |
| | | 22 | | /// Creates a <c>ScopeAdmin</c> or <c>SystemAdmin</c> person with no scope (UC-06 path b). |
| | | 23 | | /// Restricted to System Admins (AF-06c). |
| | | 24 | | /// </summary> |
| | | 25 | | /// <remarks> |
| | | 26 | | /// Derives an Argon2id hash, so it is limited to thirty requests a minute per acting person and |
| | | 27 | | /// thirty per address; a rejection answers 429 "Too many requests." with a <c>Retry-After</c> |
| | | 28 | | /// header. |
| | | 29 | | /// </remarks> |
| | | 30 | | [HttpPost("persons")] |
| | | 31 | | [EnableRateLimiting(Startup.AccountCreationRateLimitPolicy)] |
| | | 32 | | [RoleRequirement((int)Roles.SystemAdmin)] |
| | | 33 | | public async Task<ActionResult<DataOutput<CreatePersonCommandOutput?>>> CreateAdmin( |
| | | 34 | | [FromBody] CreateAdminCommand command) |
| | | 35 | | { |
| | 71 | 36 | | var result = await commandMediator |
| | 71 | 37 | | .ExecuteCommandAsync<CreateAdminCommand, CreatePersonCommandOutput>(command); |
| | | 38 | | |
| | 71 | 39 | | return result.ToActionResult(statusMap: PersonMessageMap.StatusCodes); |
| | 71 | 40 | | } |
| | | 41 | | |
| | | 42 | | /// <summary> |
| | | 43 | | /// Creates a <c>User</c> within a scope (UC-06 path a). A System Admin or an owner of the scope |
| | | 44 | | /// may call it; the ownership check (AF-06e) is enforced by the handler from the acting user. |
| | | 45 | | /// </summary> |
| | | 46 | | /// <remarks> |
| | | 47 | | /// Derives an Argon2id hash, so it is limited to thirty requests a minute per acting person and |
| | | 48 | | /// thirty per address; a rejection answers 429 "Too many requests." with a <c>Retry-After</c> |
| | | 49 | | /// header. |
| | | 50 | | /// </remarks> |
| | | 51 | | [HttpPost("scopes/{scopeId:guid}/persons")] |
| | | 52 | | [EnableRateLimiting(Startup.AccountCreationRateLimitPolicy)] |
| | | 53 | | [RoleRequirement((int)Roles.SystemAdmin, (int)Roles.ScopeAdmin)] |
| | | 54 | | public async Task<ActionResult<DataOutput<CreatePersonCommandOutput?>>> CreateUser( |
| | | 55 | | Guid scopeId, [FromBody] CreateUserCommand command) |
| | | 56 | | { |
| | 12 | 57 | | command.ScopeId = scopeId; |
| | 12 | 58 | | HttpContext.ApplyActor(command); |
| | | 59 | | |
| | 12 | 60 | | var result = await commandMediator |
| | 12 | 61 | | .ExecuteCommandAsync<CreateUserCommand, CreatePersonCommandOutput>(command); |
| | | 62 | | |
| | 12 | 63 | | return result.ToActionResult(statusMap: PersonMessageMap.StatusCodes); |
| | 12 | 64 | | } |
| | | 65 | | |
| | | 66 | | /// <summary> |
| | | 67 | | /// Creates a brand-new <c>ScopeAdmin</c> person directly as a co-owner of a scope (UC-06 path |
| | | 68 | | /// c, FR-SC-12). A System Admin or an owner of the scope may call it; the ownership check |
| | | 69 | | /// (AF-06e) is enforced by the handler from the acting user. |
| | | 70 | | /// </summary> |
| | | 71 | | /// <remarks> |
| | | 72 | | /// Derives an Argon2id hash, so it is limited to thirty requests a minute per acting person and |
| | | 73 | | /// thirty per address; a rejection answers 429 "Too many requests." with a <c>Retry-After</c> |
| | | 74 | | /// header. |
| | | 75 | | /// </remarks> |
| | | 76 | | [HttpPost("scopes/{scopeId:guid}/owners")] |
| | | 77 | | [EnableRateLimiting(Startup.AccountCreationRateLimitPolicy)] |
| | | 78 | | [RoleRequirement((int)Roles.SystemAdmin, (int)Roles.ScopeAdmin)] |
| | | 79 | | public async Task<ActionResult<DataOutput<CreatePersonCommandOutput?>>> CreateScopeOwner( |
| | | 80 | | Guid scopeId, [FromBody] CreateScopeOwnerCommand command) |
| | | 81 | | { |
| | 5 | 82 | | command.ScopeId = scopeId; |
| | 5 | 83 | | HttpContext.ApplyActor(command); |
| | | 84 | | |
| | 5 | 85 | | var result = await commandMediator |
| | 5 | 86 | | .ExecuteCommandAsync<CreateScopeOwnerCommand, CreatePersonCommandOutput>(command); |
| | | 87 | | |
| | 5 | 88 | | return result.ToActionResult(statusMap: PersonMessageMap.StatusCodes); |
| | 5 | 89 | | } |
| | | 90 | | |
| | | 91 | | /// <summary> |
| | | 92 | | /// Adds an existing <c>ScopeAdmin</c> or <c>SystemAdmin</c> person as an additional owner |
| | | 93 | | /// of a scope (UC-21, FR-SC-08/FR-SC-09). The attribute keeps a <c>User</c> out — they can |
| | | 94 | | /// never be a System Admin nor an existing owner — while the rules that depend on data it |
| | | 95 | | /// cannot see are the handler's: whether the acting Scope Admin owns this scope (AF-21c), |
| | | 96 | | /// whether the scope is active (AF-21a), whether the named person is a usable administrator |
| | | 97 | | /// (AF-21b), and whether they already own it (AF-21d). |
| | | 98 | | /// </summary> |
| | | 99 | | [HttpPost("scopes/{scopeId:guid}/owners/{personId:guid}")] |
| | | 100 | | [RoleRequirement((int)Roles.SystemAdmin, (int)Roles.ScopeAdmin)] |
| | | 101 | | public async Task<ActionResult<DataOutput<AddScopeOwnerCommandOutput?>>> AddScopeOwner( |
| | | 102 | | Guid scopeId, Guid personId) |
| | | 103 | | { |
| | 10 | 104 | | var command = new AddScopeOwnerCommand { ScopeId = scopeId, PersonId = personId }; |
| | 10 | 105 | | HttpContext.ApplyActor(command); |
| | | 106 | | |
| | 10 | 107 | | var result = await commandMediator |
| | 10 | 108 | | .ExecuteCommandAsync<AddScopeOwnerCommand, AddScopeOwnerCommandOutput>(command); |
| | | 109 | | |
| | 10 | 110 | | return result.ToActionResult(statusMap: PersonMessageMap.StatusCodes); |
| | 10 | 111 | | } |
| | | 112 | | |
| | | 113 | | /// <summary> |
| | | 114 | | /// Promotes an existing <c>User</c> of a scope to <c>ScopeAdmin</c>, making them a co-owner of |
| | | 115 | | /// that scope (UC-23, FR-SC-08/FR-SC-13/FR-RO-03). The attribute keeps a <c>User</c> out — |
| | | 116 | | /// they can never be a System Admin nor an existing owner — while the rules that depend on |
| | | 117 | | /// data it cannot see are the handler's: whether the acting Scope Admin owns this scope |
| | | 118 | | /// (AF-23c), whether the scope is active (AF-23a), whether the named person is a <c>User</c> |
| | | 119 | | /// of it (AF-23b), and whether they already hold the role (AF-23d). |
| | | 120 | | /// </summary> |
| | | 121 | | [HttpPost("scopes/{scopeId:guid}/users/{personId:guid}/promote")] |
| | | 122 | | [RoleRequirement((int)Roles.SystemAdmin, (int)Roles.ScopeAdmin)] |
| | | 123 | | public async Task<ActionResult<DataOutput<PromoteScopeUserCommandOutput?>>> PromoteScopeUser( |
| | | 124 | | Guid scopeId, Guid personId) |
| | | 125 | | { |
| | 12 | 126 | | var command = new PromoteScopeUserCommand { ScopeId = scopeId, PersonId = personId }; |
| | 12 | 127 | | HttpContext.ApplyActor(command); |
| | | 128 | | |
| | 12 | 129 | | var result = await commandMediator |
| | 12 | 130 | | .ExecuteCommandAsync<PromoteScopeUserCommand, PromoteScopeUserCommandOutput>(command); |
| | | 131 | | |
| | 12 | 132 | | return result.ToActionResult(statusMap: PersonMessageMap.StatusCodes); |
| | 12 | 133 | | } |
| | | 134 | | |
| | | 135 | | /// <summary> |
| | | 136 | | /// Updates a person's name and email, and — for a System Admin — their role (UC-08). Open to |
| | | 137 | | /// any authenticated actor because a User may update their own record; the per-actor rule and |
| | | 138 | | /// the role-change restriction (AF-08c) are enforced by the handler. The response's |
| | | 139 | | /// <c>ownedScopeIds</c> follows FR-PE-19, as <c>GET /api/persons/{id}</c> does. |
| | | 140 | | /// </summary> |
| | | 141 | | /// <remarks> |
| | | 142 | | /// A caller changing their own email presents <c>currentPassword</c>, and <c>twoFactorCode</c> |
| | | 143 | | /// when their second factor is active (FR-PE-14) — six digits for an authenticator-app code, |
| | | 144 | | /// or a recovery code, matched as <c>2fa/verify</c> matches one (<c>XXXX-XXXX-XXXX-XXXX</c>, |
| | | 145 | | /// hyphens, spaces and case ignored, the shorter <c>XXXX-XXXX</c> codes still accepted, |
| | | 146 | | /// FR-2F-20) — so this action verifies a password like the |
| | | 147 | | /// credential endpoints do, and sits under their rate limit (NFR-18), as every |
| | | 148 | | /// password-verifying endpoint does. An own email change mails the previous address, so it is |
| | | 149 | | /// charged to the person's account-email budget once the credentials are accepted: 429 "An |
| | | 150 | | /// email of this kind was sent to you recently. Try again in a few minutes." inside the |
| | | 151 | | /// cooldown, 429 "You have reached the limit for account emails. Try again later." once |
| | | 152 | | /// the window's five are spent (AF-08j), both with <c>Retry-After</c>; nothing changes then, and |
| | | 153 | | /// no recovery or email code is spent (an accepted authenticator code's time step is recorded, |
| | | 154 | | /// as for any accepted code). While the caller's email sign-in method is enabled, an own |
| | | 155 | | /// email change answers 409 "Turn off the email sign-in method before changing your email |
| | | 156 | | /// address." (AF-08k), once the credentials are accepted. |
| | | 157 | | /// </remarks> |
| | | 158 | | [HttpPut("persons/{id:guid}")] |
| | | 159 | | [EnableRateLimiting(Startup.AuthEndpointRateLimitPolicy)] |
| | | 160 | | public async Task<ActionResult<DataOutput<UpdatePersonCommandOutput?>>> Update( |
| | | 161 | | Guid id, [FromBody] UpdatePersonCommand command) |
| | | 162 | | { |
| | 32 | 163 | | command.Id = id; |
| | 32 | 164 | | HttpContext.ApplyActor(command); |
| | | 165 | | |
| | 32 | 166 | | var result = await commandMediator |
| | 32 | 167 | | .ExecuteCommandAsync<UpdatePersonCommand, UpdatePersonCommandOutput>(command); |
| | | 168 | | |
| | 32 | 169 | | return result.ToActionResult(statusMap: PersonMessageMap.StatusCodes); |
| | 32 | 170 | | } |
| | | 171 | | |
| | | 172 | | /// <summary> |
| | | 173 | | /// Logically deletes a person by setting <c>IsDeleted = true</c> (UC-09, FR-PE-06). The |
| | | 174 | | /// attribute keeps a plain <c>User</c> out; the owner rule (AF-09c) is data-dependent and is |
| | | 175 | | /// therefore enforced by the handler, as are the self-deletion (AF-09d) and last-owner |
| | | 176 | | /// (AF-09e) refusals. |
| | | 177 | | /// </summary> |
| | | 178 | | [HttpDelete("persons/{id:guid}")] |
| | | 179 | | [RoleRequirement((int)Roles.SystemAdmin, (int)Roles.ScopeAdmin)] |
| | | 180 | | public async Task<ActionResult<DataOutput<DeletePersonCommandOutput?>>> Delete(Guid id) |
| | | 181 | | { |
| | 9 | 182 | | var command = new DeletePersonCommand { Id = id }; |
| | 9 | 183 | | HttpContext.ApplyActor(command); |
| | | 184 | | |
| | 9 | 185 | | var result = await commandMediator |
| | 9 | 186 | | .ExecuteCommandAsync<DeletePersonCommand, DeletePersonCommandOutput>(command); |
| | | 187 | | |
| | 9 | 188 | | return result.ToActionResult(statusMap: PersonMessageMap.StatusCodes); |
| | 9 | 189 | | } |
| | | 190 | | |
| | | 191 | | /// <summary> |
| | | 192 | | /// Permanently (hard) deletes a person, removing the applications they own, their password |
| | | 193 | | /// reset and email verification tokens, and their scope membership/ownership rows (UC-10, |
| | | 194 | | /// FR-PE-07). Restricted to System Admins; the self-deletion refusal (AF-10c) and the |
| | | 195 | | /// last-owner guard (AF-10b) are enforced by the handler. |
| | | 196 | | /// </summary> |
| | | 197 | | [HttpDelete("persons/{id:guid}/hard")] |
| | | 198 | | [RoleRequirement((int)Roles.SystemAdmin)] |
| | | 199 | | public async Task<ActionResult<DataOutput<HardDeletePersonCommandOutput?>>> HardDelete(Guid id) |
| | | 200 | | { |
| | 7 | 201 | | var command = new HardDeletePersonCommand { Id = id }; |
| | 7 | 202 | | HttpContext.ApplyActor(command); |
| | | 203 | | |
| | 7 | 204 | | var result = await commandMediator |
| | 7 | 205 | | .ExecuteCommandAsync<HardDeletePersonCommand, HardDeletePersonCommandOutput>(command); |
| | | 206 | | |
| | 7 | 207 | | return result.ToActionResult(statusMap: PersonMessageMap.StatusCodes); |
| | 7 | 208 | | } |
| | | 209 | | |
| | | 210 | | /// <summary> |
| | | 211 | | /// Removes a person's ownership of a scope (UC-22, FR-SC-08/FR-SC-10). The attribute keeps a |
| | | 212 | | /// <c>User</c> out — they can never be a System Admin nor an existing owner — while the rules |
| | | 213 | | /// that depend on data it cannot see are the handler's: whether the acting Scope Admin owns |
| | | 214 | | /// this scope (AF-22c), whether the scope is active and the named person actually owns it |
| | | 215 | | /// (AF-22a), and whether the scope would be left without an owner (AF-22b, NFR-12). |
| | | 216 | | /// </summary> |
| | | 217 | | [HttpDelete("scopes/{scopeId:guid}/owners/{personId:guid}")] |
| | | 218 | | [RoleRequirement((int)Roles.SystemAdmin, (int)Roles.ScopeAdmin)] |
| | | 219 | | public async Task<ActionResult<DataOutput<RemoveScopeOwnerCommandOutput?>>> RemoveScopeOwner( |
| | | 220 | | Guid scopeId, Guid personId) |
| | | 221 | | { |
| | 10 | 222 | | var command = new RemoveScopeOwnerCommand { ScopeId = scopeId, PersonId = personId }; |
| | 10 | 223 | | HttpContext.ApplyActor(command); |
| | | 224 | | |
| | 10 | 225 | | var result = await commandMediator |
| | 10 | 226 | | .ExecuteCommandAsync<RemoveScopeOwnerCommand, RemoveScopeOwnerCommandOutput>(command); |
| | | 227 | | |
| | 10 | 228 | | return result.ToActionResult(statusMap: PersonMessageMap.StatusCodes); |
| | 10 | 229 | | } |
| | | 230 | | |
| | | 231 | | /// <summary> |
| | | 232 | | /// Retrieves a single person by their public identifier (UC-07, FR-PE-03). Open to any |
| | | 233 | | /// authenticated actor; the per-actor visibility rule (AF-07b) is data-dependent and is |
| | | 234 | | /// therefore enforced by the handler. <c>ownedScopeIds</c> lists every scope the person owns |
| | | 235 | | /// for a System Admin and for the person themselves; a Scope Admin reading somebody else sees |
| | | 236 | | /// only the scopes they own too (FR-PE-19). |
| | | 237 | | /// </summary> |
| | | 238 | | [HttpGet("persons/{id:guid}")] |
| | | 239 | | public async Task<ActionResult<DataOutput<PersonOutput?>>> GetById( |
| | | 240 | | Guid id, [FromQuery] bool includeDeleted = false) |
| | | 241 | | { |
| | 46 | 242 | | var query = new GetPersonByIdQuery { Id = id, IncludeDeleted = includeDeleted }; |
| | 46 | 243 | | HttpContext.ApplyActor(query); |
| | | 244 | | |
| | 46 | 245 | | var result = await queryMediator.ExecuteQueryAsync<GetPersonByIdQuery, PersonOutput>(query); |
| | | 246 | | |
| | 46 | 247 | | return result.ToActionResult(statusMap: PersonMessageMap.StatusCodes); |
| | 46 | 248 | | } |
| | | 249 | | |
| | | 250 | | /// <summary> |
| | | 251 | | /// Lists the <c>User</c> persons of a scope (UC-07, FR-PE-04). A System Admin or an owner of |
| | | 252 | | /// the scope may call it; the ownership check (AF-07b) is enforced by the handler from the |
| | | 253 | | /// acting user. <c>ownedScopeIds</c> is trimmed for a Scope Admin as on |
| | | 254 | | /// <c>GET /api/persons/{id}</c> (FR-PE-19). |
| | | 255 | | /// </summary> |
| | | 256 | | [HttpGet("scopes/{scopeId:guid}/persons")] |
| | | 257 | | [RoleRequirement((int)Roles.SystemAdmin, (int)Roles.ScopeAdmin)] |
| | | 258 | | public async Task<ActionResult<PaginatedOutput<PersonOutput>>> ListScopePersons( |
| | | 259 | | Guid scopeId, [FromQuery] ListScopePersonsQuery query) |
| | | 260 | | { |
| | 26 | 261 | | query.ScopeId = scopeId; |
| | 26 | 262 | | HttpContext.ApplyActor(query); |
| | | 263 | | |
| | 26 | 264 | | var result = await queryMediator |
| | 26 | 265 | | .ExecutePaginatedQueryAsync<ListScopePersonsQuery, PersonOutput>(query); |
| | | 266 | | |
| | 26 | 267 | | return result.ToActionResult(statusMap: PersonMessageMap.StatusCodes); |
| | 26 | 268 | | } |
| | | 269 | | |
| | | 270 | | /// <summary> |
| | | 271 | | /// Lists the owners of a scope (UC-07, FR-PE-04). A System Admin or an |
| | | 272 | | /// owner of the scope may call it; the ownership check (AF-07b) is enforced by the handler |
| | | 273 | | /// from the acting user. For a Scope Admin each owner's <c>ownedScopeIds</c> lists only the |
| | | 274 | | /// scopes the caller owns too, this one among them; a System Admin sees them all (FR-PE-19). |
| | | 275 | | /// </summary> |
| | | 276 | | [HttpGet("scopes/{scopeId:guid}/owners")] |
| | | 277 | | [RoleRequirement((int)Roles.SystemAdmin, (int)Roles.ScopeAdmin)] |
| | | 278 | | public async Task<ActionResult<PaginatedOutput<PersonOutput>>> ListScopeOwners( |
| | | 279 | | Guid scopeId, [FromQuery] ListScopeOwnersQuery query) |
| | | 280 | | { |
| | 7 | 281 | | query.ScopeId = scopeId; |
| | 7 | 282 | | HttpContext.ApplyActor(query); |
| | | 283 | | |
| | 7 | 284 | | var result = await queryMediator |
| | 7 | 285 | | .ExecutePaginatedQueryAsync<ListScopeOwnersQuery, PersonOutput>(query); |
| | | 286 | | |
| | 7 | 287 | | return result.ToActionResult(statusMap: PersonMessageMap.StatusCodes); |
| | 7 | 288 | | } |
| | | 289 | | |
| | | 290 | | /// <summary> |
| | | 291 | | /// Lists the administrators who may own a scope (UC-07 read d, FR-PE-12) — every |
| | | 292 | | /// <c>ScopeAdmin</c>, and for a System Admin caller every <c>SystemAdmin</c> too — projected to |
| | | 293 | | /// identifier, name, and email: the source for an owner picker. A System Admin or a Scope |
| | | 294 | | /// Admin may call it. Optionally excludes the current owners of a named scope, in which case |
| | | 295 | | /// the handler requires the caller to be entitled to manage that scope. |
| | | 296 | | /// </summary> |
| | | 297 | | [HttpGet("persons/scope-admins")] |
| | | 298 | | [RoleRequirement((int)Roles.SystemAdmin, (int)Roles.ScopeAdmin)] |
| | | 299 | | public async Task<ActionResult<PaginatedOutput<PersonSummaryOutput>>> ListScopeAdmins( |
| | | 300 | | [FromQuery] ListScopeAdminsQuery query) |
| | | 301 | | { |
| | 8 | 302 | | HttpContext.ApplyActor(query); |
| | | 303 | | |
| | 8 | 304 | | var result = await queryMediator |
| | 8 | 305 | | .ExecutePaginatedQueryAsync<ListScopeAdminsQuery, PersonSummaryOutput>(query); |
| | | 306 | | |
| | 8 | 307 | | return result.ToActionResult(statusMap: PersonMessageMap.StatusCodes); |
| | 8 | 308 | | } |
| | | 309 | | |
| | | 310 | | /// <summary> |
| | | 311 | | /// Lists the system's <c>SystemAdmin</c> persons (UC-07 read e, FR-PE-13), projected to |
| | | 312 | | /// identifier, name, and email: who else holds full access to the deployment. System Admin |
| | | 313 | | /// only. |
| | | 314 | | /// </summary> |
| | | 315 | | /// <remarks> |
| | | 316 | | /// A Scope Admin is deliberately excluded: a System Admin's address is not tenant-facing, |
| | | 317 | | /// which is also why <c>GET /api/persons/scope-admins</c> offers System Admins only to a |
| | | 318 | | /// System Admin caller. |
| | | 319 | | /// </remarks> |
| | | 320 | | [HttpGet("persons/system-admins")] |
| | | 321 | | [RoleRequirement((int)Roles.SystemAdmin)] |
| | | 322 | | public async Task<ActionResult<PaginatedOutput<PersonSummaryOutput>>> ListSystemAdmins( |
| | | 323 | | [FromQuery] ListSystemAdminsQuery query) |
| | | 324 | | { |
| | 6 | 325 | | HttpContext.ApplyActor(query); |
| | | 326 | | |
| | 6 | 327 | | var result = await queryMediator |
| | 6 | 328 | | .ExecutePaginatedQueryAsync<ListSystemAdminsQuery, PersonSummaryOutput>(query); |
| | | 329 | | |
| | 6 | 330 | | return result.ToActionResult(statusMap: PersonMessageMap.StatusCodes); |
| | 6 | 331 | | } |
| | | 332 | | |
| | | 333 | | /// <summary> |
| | | 334 | | /// Has a fresh verification link mailed to a person's own address (UC-51, FR-EV-06): outstanding |
| | | 335 | | /// links are retired first, as UC-15 does, and the link uses the scope's verification URL. A |
| | | 336 | | /// System Admin may ask for any person; a Scope Admin only for a <c>User</c> of a scope they own |
| | | 337 | | /// (AF-51b). No body; the token is never returned. |
| | | 338 | | /// </summary> |
| | | 339 | | /// <remarks> |
| | | 340 | | /// Refused when the person is deleted or restricted (409, AF-51c) or already verified (400, |
| | | 341 | | /// AF-51d), and with 429 when an email of this kind went to them within the cooldown, |
| | | 342 | | /// <c>HEIMDALL_ADMIN_EMAIL_COOLDOWN_IN_SECONDS</c> (five minutes by default, AF-51e). 200 means |
| | | 343 | | /// the email was handed to the sender. |
| | | 344 | | /// </remarks> |
| | | 345 | | [HttpPost("persons/{id:guid}/verification-email")] |
| | | 346 | | [RoleRequirement((int)Roles.SystemAdmin, (int)Roles.ScopeAdmin)] |
| | | 347 | | public async Task<ActionResult<DataOutput<SendPersonVerificationEmailCommandOutput?>>> SendVerificationEmail( |
| | | 348 | | Guid id) |
| | | 349 | | { |
| | 13 | 350 | | var command = new SendPersonVerificationEmailCommand { Id = id }; |
| | 13 | 351 | | HttpContext.ApplyActor(command); |
| | | 352 | | |
| | 13 | 353 | | var result = await commandMediator |
| | 13 | 354 | | .ExecuteCommandAsync<SendPersonVerificationEmailCommand, SendPersonVerificationEmailCommandOutput>(command); |
| | | 355 | | |
| | 13 | 356 | | return result.ToActionResult(statusMap: PersonMessageMap.StatusCodes); |
| | 13 | 357 | | } |
| | | 358 | | |
| | | 359 | | /// <summary> |
| | | 360 | | /// Has a password-reset link mailed to a person's own address (UC-52, FR-PR-05). A System Admin |
| | | 361 | | /// may ask for any person; a Scope Admin only for a <c>User</c> of a scope they own (AF-52b). No |
| | | 362 | | /// body; neither a token nor a password is accepted or returned. |
| | | 363 | | /// </summary> |
| | | 364 | | /// <remarks> |
| | | 365 | | /// Eligibility is UC-12's: refused (409) when the person is deleted or restricted (AF-52c) or |
| | | 366 | | /// their account could not sign in — a User whose scope is deleted, a Scope Admin with no live |
| | | 367 | | /// scope (AF-52d). 429 when a reset went to them within the cooldown (AF-52e). Unlike UC-12 the |
| | | 368 | | /// send is not queued: the caller is authenticated and owed a real answer. 200 means the email |
| | | 369 | | /// was handed to the sender. |
| | | 370 | | /// </remarks> |
| | | 371 | | [HttpPost("persons/{id:guid}/password-reset")] |
| | | 372 | | [RoleRequirement((int)Roles.SystemAdmin, (int)Roles.ScopeAdmin)] |
| | | 373 | | public async Task<ActionResult<DataOutput<SendPersonPasswordResetCommandOutput?>>> SendPasswordReset(Guid id) |
| | | 374 | | { |
| | 11 | 375 | | var command = new SendPersonPasswordResetCommand { Id = id }; |
| | 11 | 376 | | HttpContext.ApplyActor(command); |
| | | 377 | | |
| | 11 | 378 | | var result = await commandMediator |
| | 11 | 379 | | .ExecuteCommandAsync<SendPersonPasswordResetCommand, SendPersonPasswordResetCommandOutput>(command); |
| | | 380 | | |
| | 11 | 381 | | return result.ToActionResult(statusMap: PersonMessageMap.StatusCodes); |
| | 11 | 382 | | } |
| | | 383 | | } |