diff --git a/README.md b/README.md
index e18bcdd..2a29c7b 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ An ERP for service companies: users and permissions, service catalog, clients an
## Highlights
- **Per-screen permissions.** Each permission profile gives one of four ordered levels (*Denied < Reader < Editor < Manager*) to every screen. A user may hold several profiles, and the effective level on each screen is the highest one among them.
-- **Invitation-based accounts.** Nobody signs up alone: a new user receives a single-use activation link by e-mail and creates their own password. Nobody ever sees or types someone else's password.
+- **Accounts created by the team.** Nobody signs up alone: whoever registers a user sets a temporary password, and the system makes the person replace it when they first sign in.
- **Safe concurrent editing.** Optimistic concurrency rejects a save when someone else changed the record in the meantime, and tells who did it.
- **Traceability.** Every record stores who created it, who last changed it and when. Users are deactivated, never deleted.
- **Test-driven development.** Every behavior starts as a failing test, in every layer, and business rules carry stable IDs that the tests cite ([decision 27](docs/decisoes-de-arquitetura.md)).
@@ -28,7 +28,6 @@ An ERP for service companies: users and permissions, service catalog, clients an
| Architecture | Clean Architecture, tactical DDD, CQRS without MediatR, Result pattern with Problem Details |
| Data | PostgreSQL, EF Core |
| Security | ASP.NET Core Identity, JWT with rotating refresh tokens, rate limiting |
-| E-mail | SMTP with MailKit; Mailpit as the local test inbox |
| Local environment | .NET Aspire (PostgreSQL and Mailpit in Docker, dashboard with logs and traces) |
| Tests | xUnit v3, Shouldly, NSubstitute, NetArchTest, WebApplicationFactory, Testcontainers |
| Quality | Warnings as errors, analyzers, central package management, GitHub Actions, Dependabot |
@@ -39,7 +38,7 @@ An ERP for service companies: users and permissions, service catalog, clients an
backend/ControlService/src
├── ControlService.Domain Entities, value objects and business rules (no dependencies)
├── ControlService.Application Use cases and the interfaces they need
-├── ControlService.Infrastructure EF Core, Identity, e-mail: implementations of those interfaces
+├── ControlService.Infrastructure EF Core, Identity: implementations of those interfaces
├── ControlService.API Minimal API endpoints and composition root
├── ControlService.AppHost Aspire orchestration for local development
└── ControlService.ServiceDefaults Telemetry, health checks and resilience
@@ -92,7 +91,7 @@ The current slice is tracked in the milestone [M1: Sign-in, users and permission
- [x] Persistence: EF Core, audit fields, concurrency, seeded system records
- [x] Sign-in, sessions and the current user (`/me`)
- [ ] Per-screen authorization
-- [ ] Permission profiles and users endpoints, activation and password reset links
+- [ ] Permission profiles and users endpoints, with temporary passwords
- [ ] Connect the front-end to the real API
- [ ] Remaining screens: catalog, commercial, finance and reports
diff --git a/backend/ControlService/README.md b/backend/ControlService/README.md
index dc69fea..e23546c 100644
--- a/backend/ControlService/README.md
+++ b/backend/ControlService/README.md
@@ -83,8 +83,8 @@ Domain ◄── Application ◄── Infrastructure
| Projeto | O que vai aqui | Pode referenciar |
|---|---|---|
| `ControlService.Domain` | Entidades, objetos de valor e regras de negócio. Nada de EF Core ou ASP.NET. | Nada |
-| `ControlService.Application` | Casos de uso (comandos e consultas), validação e as **interfaces** de que eles precisam (repositórios, e-mail, usuário atual). | Domain |
-| `ControlService.Infrastructure` | As **implementações** dessas interfaces: EF Core, Identity, envio de e-mail. | Application, Domain |
+| `ControlService.Application` | Casos de uso (comandos e consultas), validação e as **interfaces** de que eles precisam (repositórios, credenciais, usuário atual). | Domain |
+| `ControlService.Infrastructure` | As **implementações** dessas interfaces: EF Core, Identity. | Application, Domain |
| `ControlService.API` | Endpoints HTTP, autenticação, autorização e o `Program.cs`, que liga tudo. | Application, Infrastructure |
| `ControlService.AppHost` / `ServiceDefaults` | Ambiente local com Aspire e telemetria. | API |
@@ -116,6 +116,6 @@ Pastas previstas para a primeira etapa: `Access` (níveis, chaves das telas, ace
3. Autenticação (Identity, JWT, token de renovação).
4. Autorização por tela.
5. Perfis de permissão.
-6. Usuários e links de ativação e troca de senha.
+6. Usuários, com senha temporária e troca obrigatória.
As rotas de cada funcionalidade estão na seção *Operations* do documento dela, em [docs/product/features](../../docs/product/features/), e o que vale para todas as rotas está em [docs/api/conventions.md](../../docs/api/conventions.md).
diff --git a/backend/ControlService/src/ControlService.API/Auth/PasswordChangeRequiredFilter.cs b/backend/ControlService/src/ControlService.API/Auth/PasswordChangeRequiredFilter.cs
index 1a4bb2e..10a2c4b 100644
--- a/backend/ControlService/src/ControlService.API/Auth/PasswordChangeRequiredFilter.cs
+++ b/backend/ControlService/src/ControlService.API/Auth/PasswordChangeRequiredFilter.cs
@@ -3,9 +3,9 @@
namespace ControlService.API.Auth;
-/// While the Admin has not replaced the initial password, its access token carries
+/// While the account has not replaced its temporary password, its access token carries
/// `must_change_password` and every endpoint except the ones that opted out answers 401
-/// `password_change_required` (AUTH-14, T8).
+/// `password_change_required` (AUTH-14).
public sealed class PasswordChangeRequiredFilter : IEndpointFilter
{
public ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
@@ -22,7 +22,7 @@ public sealed class PasswordChangeRequiredFilter : IEndpointFilter
}
}
-/// Marks an endpoint the person may call before replacing the initial password: `me`, sign-out
+/// Marks an endpoint the person may call before replacing the temporary password: `me`, sign-out
/// and change-password.
public sealed record AllowPendingPasswordChangeMetadata;
diff --git a/backend/ControlService/src/ControlService.API/Auth/RateLimitingOptions.cs b/backend/ControlService/src/ControlService.API/Auth/RateLimitingOptions.cs
index 17b590d..0df8a11 100644
--- a/backend/ControlService/src/ControlService.API/Auth/RateLimitingOptions.cs
+++ b/backend/ControlService/src/ControlService.API/Auth/RateLimitingOptions.cs
@@ -1,7 +1,7 @@
namespace ControlService.API.Auth;
/// Requests per client address in a 60-second window (AUTH-22). Sign-in and refresh
-/// each have their own limit; the global limit and the e-mail routes come with their own issues.
+/// each have their own limit; no other route is limited.
public sealed class RateLimitingOptions
{
public const string SectionName = "RateLimiting";
diff --git a/backend/ControlService/src/ControlService.API/Common/ApiV1Group.cs b/backend/ControlService/src/ControlService.API/Common/ApiV1Group.cs
index 18cad8f..b53c740 100644
--- a/backend/ControlService/src/ControlService.API/Common/ApiV1Group.cs
+++ b/backend/ControlService/src/ControlService.API/Common/ApiV1Group.cs
@@ -7,7 +7,7 @@ namespace ControlService.API.Common;
public static class ApiV1Group
{
/// Every endpoint requires a signed-in person unless it opts out with `AllowAnonymous()`, and
- /// refuses a person who still has to replace the initial password unless it opts out with
+ /// refuses a person who still has to replace the temporary password unless it opts out with
/// `AllowPendingPasswordChange()` (AUTH-14).
public static RouteGroupBuilder MapApiV1(this IEndpointRouteBuilder app) =>
app.MapGroup("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/api/v1").RequireAuthorization().AddEndpointFilter();
diff --git a/backend/ControlService/src/ControlService.API/ControlService.API.http b/backend/ControlService/src/ControlService.API/ControlService.API.http
index bf2a7d9..96da82d 100644
--- a/backend/ControlService/src/ControlService.API/ControlService.API.http
+++ b/backend/ControlService/src/ControlService.API/ControlService.API.http
@@ -26,7 +26,7 @@ Content-Type: application/json
###
-# First access of the Admin: the initial password must be replaced (AUTH-14).
+# Previous plan, still in the code (decision 21): the Admin must replace its initial password.
POST {{ControlService.API_HostAddress}}/api/v1/auth/change-password
Authorization: Bearer {{accessToken}}
Content-Type: application/json
diff --git a/backend/ControlService/src/ControlService.Application/Auth/ChangePassword/ChangePasswordHandler.cs b/backend/ControlService/src/ControlService.Application/Auth/ChangePassword/ChangePasswordHandler.cs
index 1a0992f..50b9a79 100644
--- a/backend/ControlService/src/ControlService.Application/Auth/ChangePassword/ChangePasswordHandler.cs
+++ b/backend/ControlService/src/ControlService.Application/Auth/ChangePassword/ChangePasswordHandler.cs
@@ -30,7 +30,7 @@ public async Task> Handle(ChangePasswordCommand command, Ca
return Result.Failure(AuthErrors.NewPasswordEqualsInitial);
}
- // The account with a mandatory change is the Admin, and this is its own first access (USR-34).
+ // Previous plan, still in the code (decision 21): the account with a mandatory change is the Admin.
// The user is saved before the password changes: a retry after a failure repeats harmlessly.
user.CompleteFirstAccess(timeProvider.GetUtcNow());
var saved = await unitOfWork.SaveChangesAsync(cancellationToken);
diff --git a/backend/ControlService/src/ControlService.Application/Auth/ICredentialStore.cs b/backend/ControlService/src/ControlService.Application/Auth/ICredentialStore.cs
index 3422de2..42f4812 100644
--- a/backend/ControlService/src/ControlService.Application/Auth/ICredentialStore.cs
+++ b/backend/ControlService/src/ControlService.Application/Auth/ICredentialStore.cs
@@ -12,6 +12,6 @@ public interface ICredentialStore
/// Sets a new password and clears the mandatory-change flag.
Task ReplacePasswordAsync(Guid userId, string newPassword, CancellationToken cancellationToken);
- /// True while the account still has its initial password (AUTH-14).
+ /// True while the account still has a temporary password (AUTH-14).
Task MustChangePasswordAsync(Guid userId, CancellationToken cancellationToken);
}
diff --git a/backend/ControlService/src/ControlService.Domain/Users/User.cs b/backend/ControlService/src/ControlService.Domain/Users/User.cs
index 0a4fe3b..5be647b 100644
--- a/backend/ControlService/src/ControlService.Domain/Users/User.cs
+++ b/backend/ControlService/src/ControlService.Domain/Users/User.cs
@@ -68,8 +68,8 @@ public Result Activate(DateTimeOffset now)
return Result.Success();
}
- /// The Admin replacing its initial password is its own first access (USR-34): the system-record
- /// guard does not apply, for the same reason as AUTH-15.
+ /// Previous plan, still in the code (decision 21): the Admin replacing its initial password
+ /// fills its activation time. It is not an edit of the record, so the system-record guard does not apply.
public void CompleteFirstAccess(DateTimeOffset now) => ActivatedAt ??= now;
public Result Deactivate(Guid by, DateTimeOffset now)
diff --git a/backend/ControlService/src/ControlService.Infrastructure/Auth/UserCredential.cs b/backend/ControlService/src/ControlService.Infrastructure/Auth/UserCredential.cs
index b87d868..62c31eb 100644
--- a/backend/ControlService/src/ControlService.Infrastructure/Auth/UserCredential.cs
+++ b/backend/ControlService/src/ControlService.Infrastructure/Auth/UserCredential.cs
@@ -7,6 +7,6 @@ namespace ControlService.Infrastructure.Auth;
/// needs syncing when a login changes; Identity's e-mail and phone columns stay empty (D12).
public sealed class UserCredential : IdentityUser
{
- /// True while the account still has its initial password (AUTH-14).
+ /// True while the account still has a temporary password (AUTH-14).
public bool MustChangePassword { get; set; }
}
diff --git a/backend/ControlService/src/ControlService.Infrastructure/Persistence/AdminOptions.cs b/backend/ControlService/src/ControlService.Infrastructure/Persistence/AdminOptions.cs
index f640052..a34d50a 100644
--- a/backend/ControlService/src/ControlService.Infrastructure/Persistence/AdminOptions.cs
+++ b/backend/ControlService/src/ControlService.Infrastructure/Persistence/AdminOptions.cs
@@ -1,13 +1,14 @@
namespace ControlService.Infrastructure.Persistence;
/// The Admin's e-mail and initial password come from configuration, each read only when
-/// the record it creates is missing (USR-24, USR-33, AUTH-13).
+/// the record it creates is missing (USR-24, USR-33). The password from configuration is the previous
+/// plan: it becomes the fixed one of the demo account (AUTH-13, decision 21).
public sealed class AdminOptions
{
public const string SectionName = "Admin";
public required string Email { get; init; }
- /// Never logged, echoed or committed (AUTH-13): it lives in user secrets.
+ /// Never logged, echoed or committed: it lives in user secrets.
public required string InitialPassword { get; init; }
}
diff --git a/backend/ControlService/tests/ControlService.Api.IntegrationTests/Auth/AdminCredentialTests.cs b/backend/ControlService/tests/ControlService.Api.IntegrationTests/Auth/AdminCredentialTests.cs
index c19d3a7..f715d1e 100644
--- a/backend/ControlService/tests/ControlService.Api.IntegrationTests/Auth/AdminCredentialTests.cs
+++ b/backend/ControlService/tests/ControlService.Api.IntegrationTests/Auth/AdminCredentialTests.cs
@@ -11,7 +11,7 @@ namespace ControlService.Api.IntegrationTests.Auth;
public sealed class AdminCredentialTests(ApiFactory factory) : IClassFixture
{
[Fact]
- public async Task Startup_creates_the_admin_credential_with_a_mandatory_change() // AUTH-13
+ public async Task Startup_creates_the_admin_credential_with_a_mandatory_change() // previous plan, see decision 21 (was AUTH-13)
{
// The first host already created it; removing it lets the next start prove it is created when missing.
await AuthTestSupport.DeleteAdminCredentialAsync(factory.Services);
@@ -29,7 +29,7 @@ public async Task Startup_creates_the_admin_credential_with_a_mandatory_change()
}
[Fact]
- public async Task Restarting_does_not_change_the_admin_credential() // AUTH-13, T3
+ public async Task Restarting_does_not_change_the_admin_credential() // previous plan, see decision 21 (was AUTH-13), T3
{
await AuthTestSupport.DeleteAdminCredentialAsync(factory.Services);
using var first = factory.WithWebHostBuilder(_ => { });
@@ -54,7 +54,7 @@ public async Task Restarting_does_not_change_the_admin_credential() // AUTH-13,
[InlineData(null)] // the user secret was never set
[InlineData("")]
[InlineData("1234567")]
- public void Api_does_not_start_without_a_valid_initial_password(string? invalidPassword) // AUTH-13, AUTH-16
+ public void Api_does_not_start_without_a_valid_initial_password(string? invalidPassword) // AUTH-16; previous plan, see decision 21 (was AUTH-13)
{
using var invalid = factory.WithWebHostBuilder(builder =>
builder.ConfigureAppConfiguration((_, config) => config.AddInMemoryCollection(new Dictionary
diff --git a/backend/ControlService/tests/ControlService.Api.IntegrationTests/Auth/AdminFirstAccessApiTests.cs b/backend/ControlService/tests/ControlService.Api.IntegrationTests/Auth/AdminFirstAccessApiTests.cs
index 6b56bc8..ba3b2c8 100644
--- a/backend/ControlService/tests/ControlService.Api.IntegrationTests/Auth/AdminFirstAccessApiTests.cs
+++ b/backend/ControlService/tests/ControlService.Api.IntegrationTests/Auth/AdminFirstAccessApiTests.cs
@@ -20,7 +20,7 @@ public sealed class AdminFirstAccessApiTests(AuthApiFactory factory) : IClassFix
private const string ChangeRequired = "Por segurança, a senha inicial precisa ser trocada no primeiro acesso.";
[Fact]
- public async Task Admin_with_the_initial_password_signs_in_with_must_change_password() // AUTH-13, AUTH-14
+ public async Task Admin_with_the_initial_password_signs_in_with_must_change_password() // AUTH-14, on the Admin by the previous plan, see decision 21
{
await AuthTestSupport.ResetAdminAsync(factory.Services);
using var client = factory.CreateHttpsClient();
@@ -35,7 +35,7 @@ public async Task Admin_with_the_initial_password_signs_in_with_must_change_pass
}
[Fact]
- public async Task Admin_with_the_initial_password_gets_401_password_change_required_on_another_endpoint() // AUTH-14, AUTH-27
+ public async Task Admin_with_the_initial_password_gets_401_password_change_required_on_another_endpoint() // AUTH-14, AUTH-27, on the Admin by the previous plan, see decision 21
{
await AuthTestSupport.ResetAdminAsync(factory.Services);
using var client = factory.CreateHttpsClient();
@@ -61,7 +61,7 @@ public async Task Admin_with_the_initial_password_can_use_me_and_sign_out()
}
[Fact]
- public async Task Change_password_returns_new_tokens_and_unlocks_other_endpoints() // AUTH-14, USR-34, D3 of #7
+ public async Task Change_password_returns_new_tokens_and_unlocks_other_endpoints() // AUTH-14, D3 of #7; activation time is the previous plan, see decision 21 (was USR-34)
{
await AuthTestSupport.ResetAdminAsync(factory.Services);
using var client = factory.CreateHttpsClient();
diff --git a/backend/ControlService/tests/ControlService.Application.Tests/Auth/AuthTestBed.cs b/backend/ControlService/tests/ControlService.Application.Tests/Auth/AuthTestBed.cs
index 077c03b..876e736 100644
--- a/backend/ControlService/tests/ControlService.Application.Tests/Auth/AuthTestBed.cs
+++ b/backend/ControlService/tests/ControlService.Application.Tests/Auth/AuthTestBed.cs
@@ -73,7 +73,7 @@ public User AddActiveUser(string login, string password)
return user;
}
- /// The Admin as the seeder creates it, with the initial password that must be replaced (AUTH-13).
+ /// The Admin as the seeder creates it, with the initial password that must be replaced (previous plan, see decision 21).
public User AddAdminWithInitialPassword(string initialPassword)
{
var admin = User.CreateAdmin(EmailAddress.Create("admin@example.com").Value);
diff --git a/backend/ControlService/tests/ControlService.Application.Tests/Auth/ChangePasswordTests.cs b/backend/ControlService/tests/ControlService.Application.Tests/Auth/ChangePasswordTests.cs
index 8dec0dc..2f70d3f 100644
--- a/backend/ControlService/tests/ControlService.Application.Tests/Auth/ChangePasswordTests.cs
+++ b/backend/ControlService/tests/ControlService.Application.Tests/Auth/ChangePasswordTests.cs
@@ -122,7 +122,7 @@ public async Task Changing_the_password_ends_every_session_and_starts_a_new_one(
}
[Fact]
- public async Task Changing_the_initial_password_fills_the_admin_activation_time() // USR-34
+ public async Task Changing_the_initial_password_fills_the_admin_activation_time() // previous plan, see decision 21 (was USR-34)
{
var bed = new AuthTestBed();
var admin = bed.AddAdminWithInitialPassword("senha-inicial");
diff --git a/backend/ControlService/tests/ControlService.Application.Tests/Auth/SignInTests.cs b/backend/ControlService/tests/ControlService.Application.Tests/Auth/SignInTests.cs
index 556583a..b44bc45 100644
--- a/backend/ControlService/tests/ControlService.Application.Tests/Auth/SignInTests.cs
+++ b/backend/ControlService/tests/ControlService.Application.Tests/Auth/SignInTests.cs
@@ -63,7 +63,7 @@ public async Task Wrong_password_is_refused_with_the_same_error_as_an_unknown_lo
}
[Fact]
- public async Task Pending_account_is_refused_as_invalid_credentials() // AUTH-02, AUTH-08
+ public async Task Pending_account_is_refused_as_invalid_credentials() // AUTH-08; the pending status is the previous plan, see decision 21
{
var bed = new AuthTestBed();
var user = bed.AddPendingUser("bruno.lima");
diff --git a/backend/ControlService/tests/ControlService.Domain.Tests/Users/EmailAddressTests.cs b/backend/ControlService/tests/ControlService.Domain.Tests/Users/EmailAddressTests.cs
index 582c0c1..39662a6 100644
--- a/backend/ControlService/tests/ControlService.Domain.Tests/Users/EmailAddressTests.cs
+++ b/backend/ControlService/tests/ControlService.Domain.Tests/Users/EmailAddressTests.cs
@@ -14,7 +14,7 @@ public void EmailAddress_with_valid_format_is_accepted() // USR-07
}
[Fact]
- public void EmailAddress_left_blank_is_rejected() // USR-07
+ public void EmailAddress_left_blank_is_rejected() // previous plan, see decision 21: the e-mail became optional (USR-07)
{
var result = EmailAddress.Create("");
diff --git a/backend/ControlService/tests/ControlService.Domain.Tests/Users/UserTests.cs b/backend/ControlService/tests/ControlService.Domain.Tests/Users/UserTests.cs
index 723097d..a075bd2 100644
--- a/backend/ControlService/tests/ControlService.Domain.Tests/Users/UserTests.cs
+++ b/backend/ControlService/tests/ControlService.Domain.Tests/Users/UserTests.cs
@@ -44,7 +44,7 @@ public void Normalized_display_name_ignores_case_accents_and_surrounding_spaces(
}
[Fact]
- public void New_user_is_pending() // AUTH-02
+ public void New_user_is_pending() // previous plan, see decision 21 (was AUTH-02)
{
var user = TestData.NewUser();
@@ -62,7 +62,7 @@ public void New_user_gets_a_version_7_id() // CNV-04
}
[Fact]
- public void Activating_a_pending_user_makes_it_active_with_the_activation_date() // AUTH-02
+ public void Activating_a_pending_user_makes_it_active_with_the_activation_date() // previous plan, see decision 21 (was AUTH-02)
{
var user = TestData.NewUser();
var now = new DateTimeOffset(2026, 9, 12, 13, 5, 44, TimeSpan.Zero);
@@ -75,7 +75,7 @@ public void Activating_a_pending_user_makes_it_active_with_the_activation_date()
}
[Fact]
- public void Activating_an_active_user_is_refused_as_invalid_link() // AUTH-23
+ public void Activating_an_active_user_is_refused_as_invalid_link() // previous plan, see decision 21 (was AUTH-23)
{
var user = TestData.NewUser();
user.Activate(new DateTimeOffset(2026, 9, 12, 13, 5, 44, TimeSpan.Zero));
@@ -89,7 +89,7 @@ public void Activating_an_active_user_is_refused_as_invalid_link() // AUTH-23
}
[Fact]
- public void Activating_an_inactive_user_is_refused_as_invalid_link() // AUTH-23, AUTH-21
+ public void Activating_an_inactive_user_is_refused_as_invalid_link() // previous plan, see decision 21 (was AUTH-23)
{
var user = TestData.NewUser();
user.Deactivate(Guid.CreateVersion7(), new DateTimeOffset(2026, 9, 20, 17, 41, 2, TimeSpan.Zero));
@@ -170,7 +170,7 @@ public void Reactivating_a_user_who_had_a_password_makes_it_active() // USR-18
}
[Fact]
- public void Reactivating_a_user_who_never_had_a_password_makes_it_pending() // USR-18
+ public void Reactivating_a_user_who_never_had_a_password_makes_it_pending() // previous plan, see decision 21 (USR-18 no longer has this case)
{
var user = TestData.NewUser();
user.Deactivate(Guid.CreateVersion7(), new DateTimeOffset(2026, 9, 20, 17, 41, 2, TimeSpan.Zero));
@@ -234,7 +234,7 @@ public void Assigning_profiles_replaces_them_and_ignores_repeated_ids()
}
[Fact]
- public void Admin_has_the_fixed_system_values_and_the_configured_email() // USR-23, USR-24, USR-32
+ public void Admin_has_the_fixed_system_values_and_the_configured_email() // USR-23, USR-24
{
var email = EmailAddress.Create("admin@example.com").Value;
@@ -278,7 +278,7 @@ public void Admin_profiles_cannot_change() // USR-24, USR-28
}
[Fact]
- public void Completing_the_first_access_fills_activated_at() // USR-34
+ public void Completing_the_first_access_fills_activated_at() // previous plan, see decision 21 (was USR-34)
{
var admin = TestData.NewAdmin();
var now = new DateTimeOffset(2026, 9, 30, 8, 0, 0, TimeSpan.Zero);
@@ -290,7 +290,7 @@ public void Completing_the_first_access_fills_activated_at() // USR-34
}
[Fact]
- public void Completing_the_first_access_again_keeps_the_first_time() // USR-34
+ public void Completing_the_first_access_again_keeps_the_first_time() // previous plan, see decision 21 (was USR-34)
{
var admin = TestData.NewAdmin();
var first = new DateTimeOffset(2026, 9, 30, 8, 0, 0, TimeSpan.Zero);
diff --git a/docs/api/conventions.md b/docs/api/conventions.md
index 8f76836..4e38cd3 100644
--- a/docs/api/conventions.md
+++ b/docs/api/conventions.md
@@ -1,6 +1,6 @@
# API conventions
-What every endpoint of the back-end has in common: addresses, paging, versions, errors and the session mechanism the front-end relies on. The operations of each feature are in its document in [product/features](../product/features/).
+What every endpoint of the back-end has in common: addresses, paging, versions, errors and the session mechanism the front-end relies on. The operations of each feature are in its document in [product/features](../product/features/). Decisions cited by number are in the [decisions document](../decisoes-de-arquitetura.md).
The front-end calls the API only through its `api` object. Replacing the prototype's simulated server with an HTTP client that offers the same methods, responses and errors must require no change to the screens ([front-end prototype](../frontend/simulated-server.md)).
@@ -8,23 +8,23 @@ The front-end calls the API only through its `api` object. Replacing the prototy
| ID | Rule |
|---|---|
-| API-01 | Every route starts with `/api/v1` (ADR-0003). A breaking change goes to `/api/v2`. |
+| API-01 | Every route starts with `/api/v1` (decision 4). A breaking change goes to `/api/v2`. |
| API-02 | Request and response bodies are JSON with the field names of the data model (CNV-02). CPF, phone and CEP are accepted with or without a mask and returned digits only (CNV-07). |
| API-03 | Single-record responses include the display names of who acted: `createdByName`, `updatedByName` and, for users, `deactivatedByName`. |
-| API-04 | Personal data and tokens never travel in the URL, only in the body (for example `suggestLogin` and the authentication links), so they are not written to logs. |
+| API-04 | Personal data and passwords never travel in the URL, only in the body (for example `suggestLogin`), so they are not written to logs. |
## Creating and versioning
| ID | Rule |
|---|---|
| API-05 | Creation returns `201 Created` with the created record, including the `id` generated by the server, and a `Location` header. |
-| API-06 | Reading a record returns its `version` in the body and in the `ETag` header. Changing, deactivating, reactivating and deleting send it back in the `If-Match` header (ADR-0014, CNV-12). |
+| API-06 | Reading a record returns its `version` in the body and in the `ETag` header. Changing, deactivating, reactivating and deleting send it back in the `If-Match` header (decision 13, CNV-12). |
## Lists
| ID | Rule |
|---|---|
-| API-07 | Lists are **paged on the server**: the client asks only for what it shows ("page 1 of users with 'silva', 10 per page"), and the server searches, sorts, counts and returns `{ items, page, pageSize, totalCount }` (ADR-0017). This keeps large screens fast and never sends the browser data the person may not see. |
+| API-07 | Lists are **paged on the server**: the client asks only for what it shows ("page 1 of users with 'silva', 10 per page"), and the server searches, sorts, counts and returns `{ items, page, pageSize, totalCount }` (decision 17). This keeps large screens fast and never sends the browser data the person may not see. |
| API-08 | Small reference lists come whole: permission profiles, the screen catalog and, in the future, payment methods. |
| API-09 | Every uniqueness check (login, display name, profile name) is done by the server. |
@@ -39,7 +39,7 @@ The front-end calls the API only through its `api` object. Replacing the prototy
| ID | Rule |
|---|---|
-| API-12 | Expected failures return Problem Details (RFC 9457) with the extensions `code`, `message` (Portuguese, ready to show), `errors` and `details` when applicable, and `traceId` (ADR-0009). Unexpected failures return 500 with a generic Portuguese message; details go only to the logs. |
+| API-12 | Expected failures return Problem Details (RFC 9457) with the extensions `code`, `message` (Portuguese, ready to show), `errors` and `details` when applicable, and `traceId` (decision 7). Unexpected failures return 500 with a generic Portuguese message; details go only to the logs. |
| API-13 | Duplicated login, display name or profile name are validation errors (400 `validation_failed`) with the message under the field, like any other validation. |
| API-14 | The generic message for an unexpected failure (500 `unexpected_error`) is **"Não foi possível concluir a operação. Tente de novo em alguns minutos."** The real cause never reaches the response, only the logs. |
| API-15 | The `message` of a `validation_failed` response is **"Alguns campos precisam ser corrigidos."**; the message of each field is under `errors`. |
@@ -75,14 +75,13 @@ The front-end calls the API only through its `api` object. Replacing the prototy
| 401 | `invalid_credentials` | Wrong login or password | Shows the message on the sign-in screen |
| 401 | `session_expired` | The session ended | Shows the sign-in screen over the system, keeping the tabs |
| 401 | `account_inactive` | The account was deactivated **during a session** | Shows the sign-in screen with the message |
-| 401 | `password_change_required` | The initial password was not changed yet | Shows the change-password screen |
+| 401 | `password_change_required` | The temporary password was not changed yet | Shows the change-password screen |
| 403 | `account_inactive` | **At sign-in**: correct password, deactivated account | Shows the message on the sign-in screen |
| 403 | `forbidden` | Operation above the person's level | Refreshes the menu and shows the no-access message |
| 404 | `not_found` | Record does not exist | Shows "Registro não encontrado" |
| 409 | `concurrency_conflict` | Someone else changed the record; `details.updatedByName` says who | Asks whether to reload or stay on the screen (CNV-14) |
| 409 | `profile_in_use` | Profile assigned to users; `details.userNames` says who | Explains who uses the profile |
-| 409 | `system_record`, `self_deactivation`, `not_pending`, `not_inactive`, `email_missing` | Business rule refused | Shows the message in a notice |
-| 410 | `link_invalid` | Link expired or already used | Shows the invalid-link screen |
+| 409 | `system_record`, `self_deactivation`, `not_inactive` | Business rule refused | Shows the message in a notice |
| 429 | `locked_out` | Lockout or rate limit, with `Retry-After` and `details.retryAfterSeconds` | Shows the wait message |
| 500 | `unexpected_error` | Unexpected failure (API-14) | Shows a generic message and keeps what the person typed |
@@ -90,4 +89,4 @@ Any other failure shows a generic message and keeps what the person typed.
## OpenAPI
-The OpenAPI document is generated from the endpoints and browsable at `/scalar` in development (ADR-0004). Endpoint summaries and descriptions are in English; examples of user-facing messages stay in Portuguese.
+The OpenAPI document is generated from the endpoints and browsable at `/scalar` in development (decision 9). Endpoint summaries and descriptions are in English; examples of user-facing messages stay in Portuguese.
diff --git a/docs/decisoes-de-arquitetura.md b/docs/decisoes-de-arquitetura.md
index 75c65b2..be03cce 100644
--- a/docs/decisoes-de-arquitetura.md
+++ b/docs/decisoes-de-arquitetura.md
@@ -470,11 +470,11 @@ Este bloco descreve o desenho decidido. Parte dele ainda não está no código,
- **Admin.** Nesta fase de portfólio, o Admin é a conta de demonstração: o login `admin` e a senha `admin123` são fixos e ficam à mostra na tela de entrada, para qualquer visitante explorar o sistema inteiro. Por isso o Admin **não** passa pela troca obrigatória e não troca a senha.
2. **Que problema resolve.** Quem cadastrou conhece a senha temporária, então ela não pode continuar valendo. E um avaliador precisa entrar no sistema sem pedir acesso a ninguém.
3. **O que acontece sem isso.** O primeiro acesso dependeria de um link enviado por e-mail: servidor de e-mail, tokens de ativação e mais telas, antes de o produto ter um ciclo completo.
-4. **Quanto custa.** A senha temporária é passada por fora do sistema. Não existe "Esqueci minha senha". E uma conta com todas as permissões é pública: um visitante pode excluir perfis e desativar usuários. O dono aceita isso enquanto o sistema for um portfólio; o que nunca se perde são o Admin e o perfil Gerenciador, que o domínio protege (decisão 16).
+4. **Quanto custa.** A senha temporária é passada por fora do sistema. Não existe "Esqueci minha senha": quem esquece a senha pede a alguém com o nível Gerenciador na tela Usuários que defina outra senha temporária (regra AUTH-29). E uma conta com todas as permissões é pública: um visitante pode excluir perfis e desativar usuários. O dono aceita isso enquanto o sistema for um portfólio; o que nunca se perde são o Admin e o perfil Gerenciador, que o domínio protege (decisão 16).
5. **Alternativas mais simples.** Esta já é a mais simples. A mais completa é o link de ativação por e-mail, adiado até o ciclo do produto estar fechado.
6. **Por que escolhemos assim.** O objetivo agora é um produto que possa ser testado do início ao fim. O que precisa mudar antes de um uso real está em [Antes de ir para o mundo real](#antes-de-ir-para-o-mundo-real).
-**O código ainda reflete o plano antigo.** Hoje é o contrário do decidido: só o Admin nasce com a troca obrigatória, e os usuários cadastrados ainda não recebem senha (a issue #11 traz o cadastro). A senha inicial do Admin ainda vem da configuração (`Admin:InitialPassword`). Também sobram do plano antigo `User.Activate` com o erro `link_invalid`, a mensagem que manda usar "Esqueci minha senha", e as regras AUTH-14, AUTH-15 e USR-34 em [product/](product/), que serão reescritas.
+**O código ainda reflete o plano antigo.** Hoje é o contrário do decidido: só o Admin nasce com a troca obrigatória, e os usuários cadastrados ainda não recebem senha (a issue #11 traz o cadastro). A senha inicial do Admin ainda vem da configuração (`Admin:InitialPassword`). Também sobram do plano antigo o status `pending`, o campo `ActivatedAt` e `User.Activate` com o erro `link_invalid`, o e-mail obrigatório, e as mensagens que falam em "senha inicial" e em "Esqueci minha senha". As regras em [product/](product/) já descrevem o desenho decidido (AUTH-13 a AUTH-15, AUTH-29, USR-07 e USR-35); o código muda nas issues do marco M1.
- **Onde ver no código:** [PasswordChangeRequiredFilter.cs](../backend/ControlService/src/ControlService.API/Auth/PasswordChangeRequiredFilter.cs), [ApiV1Group.cs](../backend/ControlService/src/ControlService.API/Common/ApiV1Group.cs), [ChangePasswordHandler.cs](../backend/ControlService/src/ControlService.Application/Auth/ChangePassword/ChangePasswordHandler.cs).
- **Em uma frase:** a primeira senha é de quem cadastrou, então o sistema obriga a trocá-la antes de liberar qualquer tela.
diff --git a/docs/frontend/README.md b/docs/frontend/README.md
index bfe6853..02e4507 100644
--- a/docs/frontend/README.md
+++ b/docs/frontend/README.md
@@ -1,6 +1,6 @@
# Front-end prototype
-The front-end is a single-page prototype, `control-service-erp.html`, with no build step. It is **not in this repository yet**. It implements the navigation, the Users and Permissions screens and every business rule of [product](../product/), using a [simulated server](simulated-server.md) instead of the real API.
+The front-end is a single-page prototype, `control-service-erp.html`, with no build step. It is **not in this repository yet**. It implements the navigation, the Users and Permissions screens and the business rules of [product](../product/), using a [simulated server](simulated-server.md) instead of the real API. Its access screens still follow the model with e-mail links, postponed on 2026-10-05: this document describes the prototype as it is, and the [differences](simulated-server.md#differences-from-the-real-back-end) list what changes.
Back-end work does not need this document: the contract between both sides is in the [API conventions](../api/conventions.md) and in each [feature](../product/features/). Read it when working on the front-end or when connecting it to the real API.
@@ -118,4 +118,4 @@ The menu uses buttons with `aria-expanded` for the drawers and marks the current
## Connecting to the real back-end
-Every decision in [product](../product/) is already implemented in the prototype with the simulated server. The only step left is to replace the simulated server with an HTTP client for the real API, handling the differences listed in [simulated server](simulated-server.md#differences-from-the-real-back-end).
+The prototype implements the rules of [product](../product/) with the simulated server, except the access model, which changed on 2026-10-05 (a temporary password instead of e-mail links). Two steps are left: replace the simulated server with an HTTP client for the real API, and adjust the access screens. Both are listed in [simulated server](simulated-server.md#differences-from-the-real-back-end).
diff --git a/docs/frontend/simulated-server.md b/docs/frontend/simulated-server.md
index 7c1fbd5..e68b6c8 100644
--- a/docs/frontend/simulated-server.md
+++ b/docs/frontend/simulated-server.md
@@ -22,7 +22,7 @@ It uses the Claude artifact database when the page is open inside Claude. If tha
## Test inbox
-The simulation does not send real e-mails. Activation and reset e-mails are stored and shown by the **E-mails de teste** button (envelope icon in the top bar and on the sign-in screen), newest first. Each e-mail has the link button, which opens the matching screen. The `listEmails()` method exists only in the simulation; in the real back-end, Mailpit plays this role (ADR-0030). In the simulation, sending never fails.
+The simulation does not send real e-mails. Activation and reset e-mails are stored and shown by the **E-mails de teste** button (envelope icon in the top bar and on the sign-in screen), newest first. Each e-mail has the link button, which opens the matching screen. The `listEmails()` method exists only in the simulation; the real back-end sends no e-mail for now (decision 21 of the [decisions document](../decisoes-de-arquitetura.md)). In the simulation, sending never fails.
## Session
@@ -30,18 +30,23 @@ The simulated session is stored in the browser (`control-service:sessao`) and la
## Demo access
-On first access, the login is `admin` with the initial password `admin123`, and the system asks for a new password. The sign-in screen shows this instruction. How reviewers will sign in to the public demo of the real back-end is an [open question](../product/open-questions.md).
+On first access, the login is `admin` with the initial password `admin123`, and the system asks for a new password. The sign-in screen shows this instruction. In the real back-end, the Admin keeps this fixed password and is never asked to change it (AUTH-13, AUTH-15).
## Differences from the real back-end
-The simulation already follows the decided model. The HTTP client that replaces it must handle these differences:
+The simulation follows the model decided before 2026-10-05, when access by e-mail was postponed. The HTTP client that replaces it must handle these differences, and the first five rows also change the prototype's access screens:
| Topic | Real back-end | Simulated server |
|---|---|---|
-| Identifiers | UUID v7 generated by the server; fixed GUIDs for the system records (ADR-0012). The front-end stops generating ids and reads them from the creation response | UUID v4 generated by the simulation. The system records use `sistema-admin` and `sistema-gerenciador` |
-| System records | Stored in the database (ADR-0022) | Defined in the simulation's code and added to queries |
-| Signature | Still open: ADR-0018 proposes separate object storage; out of the first back-end slice | Stored inside the record as a *data URL* |
-| Passwords | Hashed by ASP.NET Core Identity (ADR-0019) | SHA-256 with a random salt per user; for the demo only |
+| Access of a new user | Temporary password set at registration, with a mandatory change at sign-in (AUTH-02, AUTH-14) | Activation link "sent" to the test inbox; the person creates the password through it |
+| Forgotten password | Reset by someone with the Manager level on Usuários (AUTH-29) | "Esqueci minha senha", with a reset link in the test inbox |
+| Account status | `active` or `inactive` | Also `pending`, until the activation link is used |
+| Admin | Fixed password, no mandatory change (AUTH-13, AUTH-15) | Asks for a new password on first access |
+| E-mail | Optional (USR-07); nothing is sent | Required; "Reenviar acesso" and the test inbox depend on it |
+| Identifiers | UUID v7 generated by the server; fixed GUIDs for the system records (decision 12). The front-end stops generating ids and reads them from the creation response | UUID v4 generated by the simulation. The system records use `sistema-admin` and `sistema-gerenciador` |
+| System records | Stored in the database (decision 16) | Defined in the simulation's code and added to queries |
+| Signature | A column of the user's record, returned only with the single record (USR-36); out of the first back-end slice | Stored inside the record as a *data URL* |
+| Passwords | Hashed by ASP.NET Core Identity (decision 18) | SHA-256 with a random salt per user; for the demo only |
| Session | Access token in memory and refresh cookie (AUTH-17) | Session record in `localStorage` |
| Errors | Problem Details, converted to `ApiError` by the client (API-12) | `ApiError` directly |
diff --git a/docs/product/conventions.md b/docs/product/conventions.md
index 6b8b0b3..28b19c2 100644
--- a/docs/product/conventions.md
+++ b/docs/product/conventions.md
@@ -2,7 +2,7 @@
Rules that apply to every record and every screen. Feature documents in [features/](features/) only describe what is specific to them; read this file together with any of them.
-Rule IDs (`CNV-nn`) are stable: cite them in tests, pull requests and conversations. See [how rule IDs work](../README.md#rule-ids).
+Rule IDs (`CNV-nn`) are stable: cite them in tests, pull requests and conversations. See [how rule IDs work](../README.md#rule-ids). Decisions cited by number are in the [decisions document](../decisoes-de-arquitetura.md).
## Enforcement
@@ -16,7 +16,7 @@ Rule IDs (`CNV-nn`) are stable: cite them in tests, pull requests and conversati
|---|---|
| CNV-02 | Field names are English camelCase, exactly as in code and in stored data (`fullName`, `profileIds`, `levels`). |
| CNV-03 | Optional fields left blank are an empty string (`""`), never `null` and never omitted. All text is stored without leading or trailing spaces. |
-| CNV-04 | Every record has an `id`: a GUID (UUID v7) **generated by the server** on creation. Clients never send it; it comes back in the creation response (ADR-0012). System records use fixed, well-known GUIDs. |
+| CNV-04 | Every record has an `id`: a GUID (UUID v7) **generated by the server** on creation. Clients never send it; it comes back in the creation response (decision 12). System records use fixed, well-known GUIDs. |
| CNV-05 | Dates use ISO 8601: `YYYY-MM-DD` for plain dates (birth date) and full UTC date-time for timestamps (`updatedAt`). |
## Informational documents: CPF, phone and CEP
@@ -38,13 +38,13 @@ Rule IDs (`CNV-nn`) are stable: cite them in tests, pull requests and conversati
| ID | Rule |
|---|---|
-| CNV-09 | When a name must be unique (user display name, profile name), the comparison ignores upper and lower case, accents and leading or trailing spaces: "Ana Souza", "ana souza" and "Ána Souza" are the same name. The database guarantees it with a unique index on a normalized copy (ADR-0011). |
+| CNV-09 | When a name must be unique (user display name, profile name), the comparison ignores upper and lower case, accents and leading or trailing spaces: "Ana Souza", "ana souza" and "Ána Souza" are the same name. The database guarantees it with a unique index on a normalized copy (decision 10). |
## Authorship (audit)
| ID | Rule |
|---|---|
-| CNV-10 | Every record stores who created it and who last changed it, and when. The server fills these fields; they can never be edited by the client (ADR-0015). |
+| CNV-10 | Every record stores who created it and who last changed it, and when. The server fills these fields; they can never be edited by the client (decision 14). |
| CNV-11 | Every open record shows a footer with the display name of who acted, in Brasília time. New records that were never saved show no footer. |
| CNV-19 | On creation, `updatedAt` and `updatedBy` get the same values as `createdAt` and `createdBy`, so the footer always has both lines. |
| CNV-20 | Records the system creates by itself (the initial system records) are authored by the Admin. |
@@ -67,7 +67,7 @@ A full field-by-field history is not part of this stage (see [open questions](op
| ID | Rule |
|---|---|
-| CNV-12 | Every record has a `version`, an opaque value that changes on every change. Whoever reads a record receives it and must send it back when saving, deactivating, reactivating or deleting (ADR-0014). |
+| CNV-12 | Every record has a `version`, an opaque value that changes on every change. Whoever reads a record receives it and must send it back when saving, deactivating, reactivating or deleting (decision 13). |
| CNV-13 | If the version changed in the meantime, the operation is **refused**: nothing is written, and what the person typed stays on screen. |
| CNV-14 | The person chooses between **"Recarregar"** (discard their changes and show the current version) and **"Continuar aqui"** (keep the screen as it is, to copy what they need before reloading). |
@@ -85,5 +85,3 @@ The dialog title is **Outra pessoa alterou este cadastro**, and the message is:
| CNV-16 | Messages are written **exactly** as they must appear: code copies them verbatim. A new message is added to the feature document in the same pull request. |
| CNV-17 | Confirmation messages refer to the person's record or access, not to the person ("Cadastro de Ana Souza criado"), so the grammar is correct for any name. |
| CNV-18 | When a form has errors, the screen shows at the top how many fields need fixing and takes the person to the first one. Each message appears below its field and disappears as soon as the field is fixed. |
-
-Messages that mention an e-mail include, in the prototype, the button **Ver e-mail**, which opens the test inbox.
diff --git a/docs/product/features/authentication.md b/docs/product/features/authentication.md
index 03b1dd1..5494b82 100644
--- a/docs/product/features/authentication.md
+++ b/docs/product/features/authentication.md
@@ -1,29 +1,32 @@
# Authentication
-Signing in, account activation, password reset, lockout and sessions. The account data itself (login, e-mail, status) belongs to [users](users.md).
+Signing in, temporary passwords, lockout and sessions. The account data itself (login, e-mail, status) belongs to [users](users.md).
-- **Screens:** sign-in, activate account, forgot password, create new password, first access of the Admin. They have their own routes and do not open tabs.
-- **Decisions:** ADR-0019 (Identity, JWT, activation links), ADR-0022 (Admin bootstrap), ADR-0023 (rate limiting), ADR-0030 and ADR-0031 (e-mails), ADR-0032 (session lifetime).
+- **Screens:** sign-in and create new password (the mandatory change). They have their own routes and do not open tabs.
+- **Decisions:** in the [decisions document](../../decisoes-de-arquitetura.md): 16 (system records), 18 (Identity), 19 (access token), 20 (sessions), 21 (temporary password and the Admin demo account), 22 (lockout and rate limiting).
- **Read with:** [conventions](../conventions.md), [API conventions](../../api/conventions.md).
-## Accounts and activation
+**Not built yet.** These rules are the decided behavior. Until the users endpoints are delivered (issue #11), the code still follows the previous plan: only the Admin has a mandatory password change, its initial password comes from the configuration, and registered users have no password. Decision 21 lists what is left over.
+
+Access by e-mail (activation links, "Esqueci minha senha") is postponed until the product cycle is closed; the rules that described it are retired below.
+
+## Accounts and temporary passwords
| ID | Rule |
|---|---|
-| AUTH-01 | Nobody creates their own account. A new user is always registered by someone with access to the Users screen, and nobody, at any moment, sees or types another person's password. |
-| AUTH-02 | A new user is created as **pending** (`pending`), and the system sends an **activation link** to the registered e-mail. Opening the link, the person creates their own password and the account becomes **active** (`active`). |
-| AUTH-03 | The activation link is single-use and valid for **72 hours**. |
-| AUTH-04 | While the user is pending and has an e-mail, their record shows **Reenviar acesso** (resend access) in the "Acesso ao sistema" section. It sends a new link to the e-mail saved at that moment and invalidates the previous link. |
-| AUTH-05 | The user is **always saved before** the e-mail is sent: the link must point to an existing record, and the login and display name must be reserved from the start. The e-mail is sent right after, in the same operation, and the response says whether it left (`emailSent`, ADR-0031). |
-| AUTH-06 | If sending fails, the user stays registered as pending and the screen says so, so someone can use "Reenviar acesso". Nobody ever needs to delete and register the user again: if the e-mail was wrong, fix it, save and resend. |
-
-If the record has unsaved changes, "Reenviar acesso" asks to save first: *"Este cadastro tem alterações que ainda não foram salvas. Salve primeiro, para o link ir para o e-mail correto."*
+| AUTH-01 | Nobody creates their own account. A new user is always registered by someone with access to the Users screen, who sets a **temporary password** and passes it on outside the system. The temporary password stops being valid as soon as the person signs in (AUTH-14); from then on, nobody knows another person's password. |
+| AUTH-02 | A new user is created **active** (`active`), with the temporary password set at registration (USR-35) and a mandatory password change (AUTH-14). |
+| AUTH-03 | *Retired: activation links are postponed (decision 21). It set the link as single-use and valid for 72 hours.* |
+| AUTH-04 | *Retired: activation links are postponed (decision 21). It described "Reenviar acesso"; AUTH-29 replaces it.* |
+| AUTH-05 | *Retired: no e-mail is sent (decision 21). It required saving the user before sending the activation e-mail.* |
+| AUTH-06 | *Retired: no e-mail is sent (decision 21). It described what happened when sending failed.* |
+| AUTH-29 | Someone with the Manager level on the Users screen can **reset the password** of another user ("Redefinir senha"): they set a new temporary password, the mandatory change comes back (AUTH-14), every session of that person ends and an active lockout is cleared. This is the only way back for someone who forgot the password. It is never available for the Admin (AUTH-15). |
## Signing in
| ID | Rule |
|---|---|
-| AUTH-07 | People sign in with **login and password**. The e-mail is not used to sign in, because several people may share it. |
+| AUTH-07 | People sign in with **login and password**. The e-mail is not used to sign in: it is optional, and several people may share it. |
| AUTH-08 | After **5 consecutive failed attempts**, that login is locked for **15 minutes**. The sign-in screen never reveals whether the login or the password was wrong. The **5th** wrong password already answers as locked; while locked, even the right password is refused and those attempts are not counted; a successful sign-in resets the count. |
| AUTH-09 | A deactivated account cannot sign in, even with the correct password. |
| AUTH-26 | Too many sign-in or refresh requests from the same address are refused (429 `locked_out`, with `Retry-After`) with *"Muitas tentativas em pouco tempo. Aguarde alguns instantes e tente de novo."* This is not the lockout of AUTH-08, whose message states the 15 minutes. |
@@ -32,47 +35,54 @@ If the record has unsaved changes, "Reenviar acesso" asks to save first: *"Este
## Forgot password
+There is no "Esqueci minha senha" for now. Someone who forgot the password asks for a reset (AUTH-29).
+
| ID | Rule |
|---|---|
-| AUTH-10 | The person provides their **login**, not the e-mail (see AUTH-07). If the login exists and is active, the system sends a single-use link valid for **2 hours** to the registered e-mail. |
-| AUTH-11 | The response is always the same, in content and in duration, whether or not the login exists, so it never reveals which logins exist. That is why reset e-mails are sent in the background (ADR-0031). |
-| AUTH-12 | Creating a new password through the link also clears an active lockout. |
+| AUTH-10 | *Retired: reset links are postponed (decision 21). It sent a reset link, valid for 2 hours, to the e-mail of the login provided.* |
+| AUTH-11 | *Retired: reset links are postponed (decision 21). It kept the response identical whether or not the login existed.* |
+| AUTH-12 | *Retired: reset links are postponed (decision 21). It cleared an active lockout when a password was created through the link; AUTH-29 does that now.* |
-## First access of the Admin
+## Mandatory password change
| ID | Rule |
|---|---|
-| AUTH-13 | The Admin is the only account without an activation link, because nobody can register it. It signs in with an initial password that comes from the server configuration, never from the code (ADR-0022). |
-| AUTH-14 | Right after that first sign-in, the system shows **"Crie uma senha nova"**: *"Por segurança, a senha inicial precisa ser trocada no primeiro acesso."* Until the password is changed, no other screen or operation is available. |
-| AUTH-15 | The Admin is a system record that "cannot be changed", but **can change its own password** and use "Esqueci minha senha": credentials are not part of the user record (AUTH-19). |
-| AUTH-24 | Changing the password through the change-password operation is only for the account with a mandatory change. For any other account it is refused (403 `forbidden`) with *"Sua senha já foi criada. Para trocá-la, use “Esqueci minha senha”."* |
+| AUTH-14 | An account with a temporary password, set at registration (AUTH-02) or by a reset (AUTH-29), must replace it right after signing in. The system shows **"Crie uma senha nova"**: *"Por segurança, a senha temporária precisa ser trocada antes de continuar."* Until the password is changed, no other screen or operation is available. |
+| AUTH-24 | Changing the password through the change-password operation is only for the account with a mandatory change. For any other account it is refused (403 `forbidden`) with *"Sua senha já foi criada. Para trocá-la, fale com o responsável pelo sistema."* |
+
+## The Admin demo account
+
+| ID | Rule |
+|---|---|
+| AUTH-13 | While the project is a portfolio, the Admin is the **demo account**: the login `admin` and the password `admin123` are fixed and shown on the sign-in screen, so any visitor can explore the whole system (decision 21). |
+| AUTH-15 | The Admin never has a mandatory password change and cannot change its password: AUTH-24 refuses it like any account without a mandatory change, and its password cannot be reset (AUTH-29). |
## Passwords
| ID | Rule |
|---|---|
-| AUTH-16 | A password has at least **8 characters**, with no requirement for symbols or digits: long passwords are safer and easier to remember than short complicated ones. The person types it twice to confirm. Passwords are never trimmed: spaces are part of the password. |
-| AUTH-25 | The new password must be different from the initial one: *"A nova senha precisa ser diferente da senha inicial."* (field `password`) |
+| AUTH-16 | A password has at least **8 characters**, with no requirement for symbols or digits: long passwords are safer and easier to remember than short complicated ones. The person creating their own password types it twice to confirm. Passwords are never trimmed: spaces are part of the password. The same minimum applies to a temporary password. |
+| AUTH-25 | The new password must be different from the temporary one: *"A nova senha precisa ser diferente da senha temporária."* (field `password`) |
## Sessions
| ID | Rule |
|---|---|
-| AUTH-17 | A session ends after **8 hours without any action**. In the back-end: a 15-minute access token kept only in the page's memory, and a refresh token in an `HttpOnly` cookie, valid for 8 hours from the last refresh and rotated on each use (ADR-0019, ADR-0032). The front-end refreshes only when it needs to make a request, never on a timer. |
+| AUTH-17 | A session ends after **8 hours without any action**. In the back-end: a 15-minute access token kept only in the page's memory, and a refresh token in an `HttpOnly` cookie, valid for 8 hours from the last refresh and rotated on each use (decisions 19 and 20). The front-end refreshes only when it needs to make a request, never on a timer. |
| AUTH-18 | The server checks on **every request** that the account is still active. A deactivated person is rejected on their next request, even with a valid access token. Refresh is anonymous (it carries only the cookie), so it makes the same check itself: a non-active account is refused with `account_inactive` and its session is removed. |
| AUTH-27 | The server's `message` for 401 `session_expired`, `password_change_required` and `account_inactive` (during a session) is, respectively, the "Session ended" message, the AUTH-14 text and the "Deactivated account" message. A missing, invalid or expired access token is answered as `session_expired`. |
When a session ends, the sign-in screen appears **over** the system without closing the tabs. If the same person signs in again, everything is where it was, including unsaved forms.
-## Credentials and links
+## Credentials
| ID | Rule |
|---|---|
-| AUTH-19 | Passwords, activation and reset links, and the lockout counter are **not part of the user record**. They are stored separately, only hashed, and never returned by any operation. |
-| AUTH-20 | Links carry only a random token. Tokens travel in the request body, never in the URL, and are never written to logs (ADR-0030). |
-| AUTH-21 | Deactivating a user invalidates their pending links and revokes their refresh tokens. |
+| AUTH-19 | Passwords and the lockout counter are **not part of the user record**. They are stored separately, only hashed, and never returned by any operation. |
+| AUTH-20 | Passwords travel only in the request body and the refresh token only in its cookie: never in the URL, and neither is ever written to logs. |
+| AUTH-21 | Deactivating a user ends every session of that person. |
| AUTH-22 | Every duration and limit in this document is an initial value, adjustable in the server configuration. |
-| AUTH-23 | Activating a user who is not `pending` (for example, opening the link after the account became active or was deactivated) is refused like any other invalid link, with the message *"Este link não vale mais. Peça a quem cadastrou você para reenviar o acesso."* |
+| AUTH-23 | *Retired: activation links are postponed (decision 21). It refused activating a user who was not pending, as an invalid link.* |
| AUTH-28 | Messages that state a duration or a limit show the configured value (AUTH-22): with the defaults, the texts below are exact; with 30 minutes of lockout, the message says "Aguarde 30 minutos". |
## Messages
@@ -81,22 +91,19 @@ Verbatim, in Portuguese.
| Situation | Message |
|---|---|
-| User created | Cadastro de *{nome}* criado. Enviamos o link de ativação para *{e-mail}*. |
-| Activation e-mail failed | Cadastro de *{nome}* criado, mas não conseguimos enviar o e-mail de ativação. Use “Reenviar acesso” para tentar de novo. |
-| Access resent | Enviamos um novo link de ativação para *{e-mail}*. |
+| User created | Cadastro de *{nome}* criado. Passe a senha temporária para a pessoa: ela vai criar a própria senha ao entrar. |
+| Temporary password left blank | Informe a senha temporária. |
+| Password reset (AUTH-29) | Senha temporária de *{nome}* definida. A pessoa vai criar a própria senha ao entrar. |
| Wrong login or password | Login ou senha incorretos. |
| Locked out | Muitas tentativas sem sucesso. Aguarde 15 minutos e tente de novo. |
| Too many requests (rate limit) | Muitas tentativas em pouco tempo. Aguarde alguns instantes e tente de novo. |
| Deactivated account (correct password) | Este acesso está desativado. Fale com o responsável pelo sistema. |
-| Activation link expired or used | Este link não vale mais. Peça a quem cadastrou você para reenviar o acesso. |
-| Reset link expired or used | Este link não vale mais. Peça um novo em “Esqueci minha senha”. |
-| Password reset requested | Se esse login existir, enviamos um link para o e-mail cadastrado. Confira sua caixa de entrada. |
| Password too short | A senha precisa ter pelo menos 8 caracteres. |
| Passwords differ | As duas senhas não são iguais. Digite de novo. |
-| New password equal to the initial one | A nova senha precisa ser diferente da senha inicial. |
-| Change password without a mandatory change | Sua senha já foi criada. Para trocá-la, use “Esqueci minha senha”. |
-| Initial password must be changed (`password_change_required`) | Por segurança, a senha inicial precisa ser trocada no primeiro acesso. |
-| Admin initial password changed | Senha criada. Tudo pronto para começar. |
+| New password equal to the temporary one | A nova senha precisa ser diferente da senha temporária. |
+| Change password without a mandatory change | Sua senha já foi criada. Para trocá-la, fale com o responsável pelo sistema. |
+| Temporary password must be changed (`password_change_required`) | Por segurança, a senha temporária precisa ser trocada antes de continuar. |
+| Temporary password replaced | Senha criada. Tudo pronto para começar. |
| Session ended | Sua sessão terminou. Entre de novo para continuar. Suas abas continuam abertas. |
| Signed out with the Sair button | Você saiu do sistema. |
| Sign out with unsaved changes | **Sair com alterações não salvas?** Algumas abas têm alterações que ainda não foram salvas. Se você sair agora, elas serão perdidas. |
@@ -109,13 +116,11 @@ Verbatim, in Portuguese.
| `hasSession()` | `POST /api/v1/auth/refresh` | Anyone | Uses the refresh cookie; called when the page opens. Same 200 body as sign-in and a rotated cookie; without a valid session, 401 `session_expired` |
| `signOut()` | `POST /api/v1/auth/sign-out` | Signed in | 204. Ends the session named by the `sid` claim of the access token (the cookie is scoped to the refresh route and never arrives here) and expires the cookie; the person's other sessions stay open |
| `changePassword(password, confirmation)` | `POST /api/v1/auth/change-password` | Signed in | Only for the account with a mandatory change (AUTH-24). Same 200 body as sign-in; ends every session of the person and starts a new one |
-| `checkLink(token, purpose)` | `POST /api/v1/auth/links/check` | Anyone | `purpose`: `activation` or `reset`. Returns `{ valid, displayName, login }` |
-| `activateAccount(token, password, confirmation)` | `POST /api/v1/auth/activate` | Anyone | |
-| `requestPasswordReset(login)` | `POST /api/v1/auth/password-reset/request` | Anyone | Always the same response (AUTH-11) |
-| `resetPassword(token, password, confirmation)` | `POST /api/v1/auth/password-reset` | Anyone | |
| `me()` | `GET /api/v1/me` | Signed in | `{ id, displayName, login, levels }`, where `levels` is `[{ "screen": "", "level": "" }]` with **every** screen of the catalog, in catalog order, including `negado` (same shape as a profile's `levels`) |
-Sign-in, refresh and the e-mail-sending operations are rate limited (ADR-0023, AUTH-26). Until the initial password is replaced, every operation except `me`, sign-out and change-password answers 401 `password_change_required` (AUTH-14).
+Resetting someone's password (AUTH-29) is an operation of the [users](users.md#operations) feature.
+
+Sign-in and refresh are rate limited (decision 22, AUTH-26). Until the temporary password is replaced, every operation except `me`, sign-out and change-password answers 401 `password_change_required` (AUTH-14).
## Errors
@@ -124,9 +129,8 @@ Sign-in, refresh and the e-mail-sending operations are rate limited (ADR-0023, A
| 401 | `invalid_credentials` | Wrong login or password |
| 403 | `account_inactive` | **At sign-in**: correct password, deactivated account |
| 401 | `account_inactive` | **During a session**, including at refresh: the account was deactivated (AUTH-18) |
-| 401 | `password_change_required` | The initial password was not changed yet (AUTH-14) |
+| 401 | `password_change_required` | The temporary password was not changed yet (AUTH-14) |
| 401 | `session_expired` | The session ended |
-| 410 | `link_invalid` | Link expired or already used, or the account is no longer pending (AUTH-23) |
| 403 | `forbidden` | Change password on an account without a mandatory change (AUTH-24) |
| 429 | `locked_out` | Lockout (AUTH-08) or rate limit (AUTH-26), with `Retry-After` and `details.retryAfterSeconds` (the seconds left, rounded up) |
| 400 | `validation_failed` | Password rules; fields `password` and `passwordConfirmation` |
diff --git a/docs/product/features/permission-profiles.md b/docs/product/features/permission-profiles.md
index 6a15914..8578704 100644
--- a/docs/product/features/permission-profiles.md
+++ b/docs/product/features/permission-profiles.md
@@ -3,7 +3,7 @@
Access levels, profiles, effective access and what each person sees. This is the most distinctive business rule of the project.
- **Screen:** Permissões, key `gerenciamento/permissoes`.
-- **Decisions:** ADR-0006 (effective access as a domain service), ADR-0020 (authorization per screen), ADR-0021 (screen keys), ADR-0022 (Gerenciador profile).
+- **Decisions:** in the [decisions document](../../decisoes-de-arquitetura.md): 3 (effective access as a domain service), 16 (Gerenciador profile), 23 (levels per screen), 24 (authorization per screen), 25 (screen keys).
- **Read with:** [conventions](../conventions.md), [API conventions](../../api/conventions.md), [overview: screen keys](../overview.md#screen-keys).
In this document, a **screen** is each submenu item (Usuários, Contas a Pagar) and an **area** is each menu group (Gerenciamento, Financeiro).
@@ -28,14 +28,14 @@ In this document, a **screen** is each submenu item (Usuários, Contas a Pagar)
| ID | Rule |
|---|---|
-| PERM-03 | Operations require a minimum level on their screen. This table is the reference for every screen; a screen with exceptions documents them (ADR-0020, confirmed by the owner on 2026-09-24). |
+| PERM-03 | Operations require a minimum level on their screen. This table is the reference for every screen; a screen with exceptions documents them (decision 24, confirmed by the owner on 2026-09-24). |
| PERM-04 | Exception: listing profiles is also allowed with Reader on the **Usuários** screen, even without access to Permissões, because the user form needs the profiles to choose from. |
| Operation | Minimum level |
|---|---|
| Open the screen, list and view records | Reader |
-| Create, change, duplicate a profile, resend access | Editor |
-| Delete a profile, deactivate or reactivate a user | Manager |
+| Create, change, duplicate a profile | Editor |
+| Delete a profile, deactivate or reactivate a user, reset a user's password (AUTH-29) | Manager |
A Reader on Usuários therefore sees people's signatures, as confirmed by the owner.
@@ -74,14 +74,14 @@ Changing a profile affects everyone who has it. Lowering a level in a profile on
| PERM-08 | A screen whose effective access is `negado` does not appear in the menu or in the screen search ("Buscar tela…"). An area where **every** screen is `negado` does not appear either. |
| PERM-09 | Opening a denied screen through a saved link or a restored tab shows *"Você não tem acesso a esta tela. Se precisar dela, fale com o responsável pelo sistema."* No data is loaded, because the server refuses the request. |
| PERM-10 | A person without access to any screen (no profiles, or profiles that deny everything) sees an empty menu, and the home screen shows *"Você ainda não tem acesso a nenhuma tela. Fale com o responsável pelo sistema."* |
-| PERM-11 | Buttons follow PERM-03. A **Reader** sees records read-only, with the notice *"Você pode consultar este cadastro, mas não alterar. Para mudar alguma informação, fale com quem tem acesso de Editor nesta tela."*; the buttons "Novo", "Salvar", "Duplicar perfil" and "Reenviar acesso" are hidden, and "Cancelar" becomes "Voltar para a lista". An **Editor** does not see "Excluir perfil", "Desativar usuário" or "Reativar usuário". Nobody sees "Desativar usuário" on their own record. |
+| PERM-11 | Buttons follow PERM-03. A **Reader** sees records read-only, with the notice *"Você pode consultar este cadastro, mas não alterar. Para mudar alguma informação, fale com quem tem acesso de Editor nesta tela."*; the buttons "Novo", "Salvar" and "Duplicar perfil" are hidden, and "Cancelar" becomes "Voltar para a lista". An **Editor** does not see "Excluir perfil", "Desativar usuário", "Reativar usuário" or "Redefinir senha". Nobody sees "Desativar usuário" on their own record. |
| PERM-12 | **The server decides.** Hiding screens and buttons helps people; the real barrier is the server, which refuses any request above the person's level (CNV-01). |
## When permissions change
| ID | Rule |
|---|---|
-| PERM-13 | A change to someone's profiles takes effect on the server from that person's next request, without signing out (ADR-0020, ADR-0032). |
+| PERM-13 | A change to someone's profiles takes effect on the server from that person's next request, without signing out (decision 24). |
| PERM-14 | The person's menu is refreshed when they sign in, when they reload the page and whenever they themselves save, deactivate, reactivate or delete a record. Opening a new tab does **not** refresh it. If they try something they lost in the meantime, the server refuses, and the menu is refreshed with the no-access message. |
| PERM-15 | A new screen appears on the Permissões screen with `negado` in every existing profile until someone changes and saves the profile. Only the Gerenciador profile sees it right away. This follows from "missing means `negado`" and needs no data migration. |
@@ -123,7 +123,7 @@ Collection `profiles`. Besides these fields, every profile has the audit fields
|---|---|---|---|
| PERM-16 | `name` | Required. | Dê um nome ao perfil. |
| PERM-17 | `name` | Unique among all profiles, **including the Gerenciador**, with the comparison of CNV-09: "Financeiro", "financeiro" and "FINANCEIRO" are the same name. | Já existe um perfil chamado *{nome existente}*. Escolha outro nome. |
-| PERM-24 | `levels` | Every `screen` must be a key of the catalog (ADR-0021). An unknown key refuses the whole save instead of being ignored: ignoring it would drop the level without warning, so a key mistyped in the front-end menu would look saved and grant nothing. It happens only with a page opened before a screen was retired, or with a front-end bug. | Esta tela não existe mais no sistema. Atualize a página e tente de novo. |
+| PERM-24 | `levels` | Every `screen` must be a key of the catalog (decision 25). An unknown key refuses the whole save instead of being ignored: ignoring it would drop the level without warning, so a key mistyped in the front-end menu would look saved and grant nothing. It happens only with a page opened before a screen was retired, or with a front-end bug. | Esta tela não existe mais no sistema. Atualize a página e tente de novo. |
### Creating, duplicating and deleting
@@ -131,7 +131,7 @@ Collection `profiles`. Besides these fields, every profile has the audit fields
|---|---|
| PERM-18 | A new profile starts with every screen at `negado`. |
| PERM-19 | Any profile, including the Gerenciador, can be duplicated. The copy is named "Cópia de *{nome original}*", with the same description and levels, and is stored only when the person saves. Duplicating is done by the front-end, which opens a new record prefilled with the original's data. |
-| PERM-20 | A profile can be truly deleted, but only when **no user** has it, whether active, pending or deactivated. A database constraint guarantees it even if someone assigns the profile at the same moment. The Gerenciador profile can never be deleted. |
+| PERM-20 | A profile can be truly deleted, but only when **no user** has it, whether active or deactivated. A database constraint guarantees it even if someone assigns the profile at the same moment. The Gerenciador profile can never be deleted. |
| PERM-21 | Each area on the Permissões screen has the selector **Mudar todas desta área para**, which sets the same level on every screen of the area and warns *"Telas de {área} marcadas como {nível}. Salve o perfil para confirmar."* Nothing is stored until the person saves. |
The delete confirmation is **Excluir este perfil?** *O perfil {nome} será apagado. Não dá para desfazer.* If someone has the profile, deletion is refused with **Este perfil está em uso** and *"{nomes} usa(m) este perfil. Tire o perfil dessa(s) pessoa(s) na tela Usuários e depois volte para excluir."*
@@ -140,7 +140,7 @@ The delete confirmation is **Excluir este perfil?** *O perfil {nome} será apaga
| ID | Rule |
|---|---|
-| PERM-22 | The **Gerenciador** profile always exists and cannot be changed or deleted. It is stored in the database with a fixed id and marked `isSystem: true` (ADR-0022). |
+| PERM-22 | The **Gerenciador** profile always exists and cannot be changed or deleted. It is stored in the database with a fixed id and marked `isSystem: true` (decision 16). |
| PERM-23 | Its levels are **not stored**. The server computes `gerenciador` for every screen of the catalog and returns the full `levels` list like any other profile, so a new screen is covered without data changes. |
| PERM-25 | Changing a level or deleting the Gerenciador profile is refused with the message *"O perfil Gerenciador é do sistema e não pode ser alterado nem excluído."* |
@@ -168,7 +168,7 @@ Verbatim, in Portuguese. The other messages of this feature are inside the rules
| `createProfile(data)` | `POST /api/v1/permission-profiles` | Editor | |
| `updateProfile(id, data, version)` | `PUT /api/v1/permission-profiles/{id}` | Editor | Version in `If-Match` |
| `deleteProfile(id, version)` | `DELETE /api/v1/permission-profiles/{id}` | Manager | Version in `If-Match` |
-| `listScreens()` | `GET /api/v1/screens` | Signed in | Areas, screens and keys (ADR-0021) |
+| `listScreens()` | `GET /api/v1/screens` | Signed in | Areas, screens and keys (decision 25) |
The effective level of the signed-in person on every screen comes from `me()` ([authentication](authentication.md#operations)).
diff --git a/docs/product/features/users.md b/docs/product/features/users.md
index a786bf7..a8e70e3 100644
--- a/docs/product/features/users.md
+++ b/docs/product/features/users.md
@@ -1,11 +1,13 @@
# Users
-Registering people, keeping their data, deactivating and reactivating them. Signing in, activation links and passwords are in [authentication](authentication.md); what a user can access comes from [permission profiles](permission-profiles.md).
+Registering people, keeping their data, deactivating and reactivating them. Signing in and passwords are in [authentication](authentication.md); what a user can access comes from [permission profiles](permission-profiles.md).
- **Screen:** Usuários, key `gerenciamento/usuarios`. The record form has the sections Dados pessoais, Acesso ao sistema, Telefone e endereço, Contato de emergência, Assinatura and Perfis de permissão.
-- **Decisions:** ADR-0006 (value objects), ADR-0012 (ids), ADR-0016 (deactivation), ADR-0017 (paging), ADR-0018 (signature, still proposed), ADR-0022 (Admin).
+- **Decisions:** in the [decisions document](../../decisoes-de-arquitetura.md): 3 (value objects), 12 (ids), 15 (deactivation), 16 (Admin), 17 (paging), 21 (temporary password).
- **Read with:** [conventions](../conventions.md), [API conventions](../../api/conventions.md), [glossary](../glossary.md).
+**Not built yet.** These rules are the decided behavior. The code still has the `pending` status, the `activatedAt` field and a required e-mail, left over from the plan with activation links; they change with the users endpoints (issue #11).
+
## Data model
Collection `users`. Besides the fields below, every user has the audit fields and `version` ([conventions](../conventions.md#authorship-audit)).
@@ -16,13 +18,12 @@ Collection `users`. Besides the fields below, every user has the audit fields an
|---|---|---|---|
| `id` | text | yes | GUID generated by the server (CNV-04) |
| `login` | text | yes | Name used to sign in, e.g. `ana.souza`. See USR-05 and USR-06 |
-| `email` | text | yes | Where activation and reset links go. **Not unique**: two people may share a department mailbox |
-| `status` | text | yes | `pending`, `active` or `inactive`. Controlled by the system, never edited directly |
-| `activatedAt` | text | no | When the person created their password the first time. `""` while pending |
+| `email` | text | no | Contact information only: no rule of the system depends on it while access by e-mail is postponed. **Not unique**: two people may share a department mailbox |
+| `status` | text | yes | `active` or `inactive`. Controlled by the system, never edited directly |
| `deactivatedAt` | text | no | Deactivation date and time. `""` if not deactivated |
| `deactivatedBy` | text | no | `id` of who deactivated. `""` if not deactivated |
-Passwords, links and the lockout counter are not part of the record (AUTH-19).
+Passwords and the lockout counter are not part of the record (AUTH-19). Whether the person still has a temporary password belongs to the credential, not to the record.
### Personal data
@@ -54,7 +55,6 @@ CPF, phone, address, emergency contact and blood type are personal data under th
"login": "ana.souza",
"email": "ana.souza@example.com",
"status": "active",
- "activatedAt": "2026-09-12T13:05:44Z",
"deactivatedAt": "",
"deactivatedBy": "",
"fullName": "Ana Paula Souza",
@@ -101,16 +101,17 @@ Messages are verbatim, in Portuguese (CNV-16). The error path of each field is t
| USR-04 | `login` | Required. | Informe o login. |
| USR-05 | `login` | 3 to 30 characters: unaccented letters, digits, dot, hyphen and underscore. Stored in lowercase. | O login deve ter de 3 a 30 caracteres, usando só letras sem acento, números, ponto, hífen ou sublinhado. |
| USR-06 | `login` | Unique among **all** users, including the Admin and deactivated users, ignoring case. | Já existe um usuário com o login “*{login}*”. Escolha outro. |
-| USR-07 | `email` | Required, and in a valid e-mail format. **Not unique.** | Missing: Informe o e-mail. É para ele que o link de ativação será enviado. — Invalid: Este e-mail não parece válido. Confira se ele tem @ e o domínio, por exemplo ana@empresa.com.br. |
+| USR-07 | `email` | Optional. When filled in, it must be in a valid e-mail format. **Not unique.** | Este e-mail não parece válido. Confira se ele tem @ e o domínio, por exemplo ana@empresa.com.br. |
| USR-08 | `cpf` | Optional. When filled in, it must pass the [check digits](#cpf-check-digits); CPFs with all digits equal, such as `111.111.111-11`, are refused. **Not unique.** | Este CPF não é válido. Confira os números ou deixe o campo em branco. |
| USR-09 | `birthDate` | When filled in, it cannot be after today. | A data de nascimento não pode ser no futuro. |
| USR-10 | `phone` | When filled in, 10 or 11 digits. | Digite o telefone com DDD. Ex.: (21) 98765-4321. |
| USR-11 | `address.cep` | When filled in, 8 digits. | O CEP precisa ter 8 números. |
| USR-12 | `emergencyContact.phone` | Same rule as USR-10. | Digite o telefone com DDD. Ex.: (21) 98765-4321. |
| USR-13 | `profileIds` | May be empty. Ids of profiles that do not exist are discarded on save. | — |
+| USR-35 | `temporaryPassword` | Only when registering a new user: required, with the minimum length of AUTH-16. It is never returned and is not part of the record (AUTH-19). | Missing: Informe a senha temporária. — Too short: A senha precisa ter pelo menos 8 caracteres. |
| USR-26 | `bloodType` | When filled in, must be one of the closed list `A+`, `A-`, `B+`, `B-`, `AB+`, `AB-`, `O+`, `O-`. | Este tipo sanguíneo não é válido. |
-Duplicates (USR-03, USR-06) are validation errors like the others, shown under the field (ADR-0009).
+Duplicates (USR-03, USR-06) are validation errors like the others, shown under the field (decision 7).
### CPF check digits
@@ -124,19 +125,19 @@ For the first check digit, multiply the first 9 digits by the weights 10, 9, 8,
| USR-15 | Special cases: a one-word name gives that word ("Madalena" → `madalena`); characters outside the login rule (apostrophes, extra spaces, symbols) are removed ("Joana D'Ávila" → `joana.davila`); a long result is cut to fit 30 characters **including** the appended number; a result shorter than 3 characters gives no suggestion ("Li"). |
| USR-31 | Name suffixes ("Filho", "Júnior", "Neto") are taken literally, as part of the last name: "João Souza Filho" gives `joao.filho`. When cutting a long result to fit 30 characters (USR-15) leaves a trailing `.`, `-` or `_`, that character is also removed. |
-In a new record, the login is suggested while the full name is typed, until someone types a login of their own. The save button of a new record is **Cadastrar e enviar acesso**.
+In a new record, the login is suggested while the full name is typed, until someone types a login of their own. The save button of a new record is **Cadastrar usuário**.
## Deactivation and reactivation
| ID | Rule |
|---|---|
-| USR-16 | Users are **never deleted**, only deactivated: authorship and activity records point to users, and deleting one would leave them without an owner (ADR-0016). |
-| USR-17 | Deactivating a user: status becomes `inactive`, with date and author of the deactivation; the person is signed out on their next action and cannot sign in again (AUTH-18, AUTH-09); pending links stop working (AUTH-21); the login and display name stay reserved; the name keeps appearing in history and authorship. |
-| USR-18 | A deactivated user can be reactivated with **Reativar usuário**, without a confirmation. If they had created a password, they become `active` again with the same password. If they were still pending, they become `pending` again and a new activation link is sent. |
+| USR-16 | Users are **never deleted**, only deactivated: authorship and activity records point to users, and deleting one would leave them without an owner (decision 15). |
+| USR-17 | Deactivating a user: status becomes `inactive`, with date and author of the deactivation; the person is signed out on their next action and cannot sign in again (AUTH-18, AUTH-09); their sessions end (AUTH-21); the login and display name stay reserved; the name keeps appearing in history and authorship. |
+| USR-18 | A deactivated user can be reactivated with **Reativar usuário**, without a confirmation. They become `active` again with the password they had; if it was still a temporary one, the mandatory change is still required (AUTH-14). |
| USR-29 | Reactivating a user who is not `inactive` is refused with the message *"Só é possível reativar um acesso desativado."* |
| USR-19 | Nobody can deactivate themselves, so nobody loses access by mistake. The Admin can never be deactivated. |
| USR-27 | Trying to deactivate one's own account is refused with the message *"Você não pode desativar o seu próprio acesso."* |
-| USR-20 | The list's **Situação** filter starts at "Ativos e pendentes", hiding deactivated users. The other options are "Só pendentes", "Só desativados" and "Todos". |
+| USR-20 | The list's **Situação** filter starts at "Ativos", hiding deactivated users. The other options are "Só desativados" and "Todos". |
| USR-30 | Deactivating a user who is already deactivated changes nothing: the status stays `inactive`, and the date and author of the **first** deactivation are kept. |
The deactivation confirmation is: *{Nome} não vai mais conseguir entrar no sistema. Você pode reativar depois.* If the record has unsaved changes, it adds: *As alterações não salvas neste cadastro serão descartadas.* After deactivating or reactivating, the tab goes back to the list.
@@ -152,12 +153,12 @@ The deactivation confirmation is: *{Nome} não vai mais conseguir entrar no sist
| ID | Rule |
|---|---|
-| USR-23 | The **Admin** user always exists and cannot be changed, deactivated or deleted. It is stored in the database with a fixed id and marked `isSystem: true`, which makes the interface show it read-only with a notice explaining why (ADR-0022). |
-| USR-24 | The Admin always has the Gerenciador profile and cannot lose it. Its e-mail and initial password come from the server configuration. |
+| USR-23 | The **Admin** user always exists and cannot be changed, deactivated or deleted. It is stored in the database with a fixed id and marked `isSystem: true`, which makes the interface show it read-only with a notice explaining why (decision 16). |
+| USR-24 | The Admin always has the Gerenciador profile and cannot lose it. Its e-mail comes from the server configuration; its password is the fixed one of the demo account (AUTH-13). |
| USR-28 | Changing or deactivating the Admin is refused with the message *"O usuário Admin é do sistema e não pode ser alterado nem desativado."* |
-| USR-32 | The Admin is created with `activatedAt` empty: it never creates a password through an activation link. |
+| USR-32 | *Retired: the `activatedAt` field left the record together with the `pending` status. It said the Admin was created with `activatedAt` empty.* |
| USR-33 | The Admin's e-mail is read from the configuration only when the Admin is first created; changing the configuration later does not change the stored record. |
-| USR-34 | The Admin's `activatedAt` is filled when it replaces the initial password on its first access ([AUTH-14](authentication.md#first-access-of-the-admin)). This does not break USR-23, for the same reason as AUTH-15: it is the Admin's own first access, not an edit of the record. |
+| USR-34 | *Retired: the Admin has no mandatory password change (AUTH-15), and the `activatedAt` field left the record. It filled the Admin's `activatedAt` when it replaced its initial password.* |
| Field | Value |
|---|---|
@@ -174,20 +175,20 @@ Whether the Admin may have personal data (phone, signature) is an [open question
| ID | Rule |
|---|---|
+| USR-36 | The signature is stored in the user's own record, in the database (decided by the owner on 2026-10-07). Because the image is large, only the single-record response returns it: the list and `me` never do. |
| USR-25 | The signature is drawn on a 1200 × 400 pixel canvas, with the mouse or a finger, or uploaded as a PNG, JPG or WebP image of up to 5 MB. An uploaded image is resized to fit the canvas, keeping its proportions. |
-The prototype stores the image inside the record as a *data URL* (PNG with a transparent background, or JPEG with a white background when larger than 180,000 characters). ADR-0018 proposes separate object storage. **The signature is out of the first back-end slice** until that decision is taken.
+The prototype stores the image inside the record as a *data URL* (PNG with a transparent background, or JPEG with a white background when larger than 180,000 characters). Separate object storage was evaluated and left out ("O que ficou de fora" in the [decisions document](../../decisoes-de-arquitetura.md)). **The signature is out of the first back-end slice**: it enters when a screen needs it on a document.
## Messages
-Verbatim, in Portuguese. Creation and resend messages are in [authentication](authentication.md#messages).
+Verbatim, in Portuguese. The creation and password reset messages are in [authentication](authentication.md#messages).
| Situation | Message |
|---|---|
| User changed | Cadastro de *{nome}* salvo. |
| User deactivated | Acesso de *{nome}* desativado. |
-| User reactivated, with a password | Acesso de *{nome}* reativado. A pessoa já pode entrar no sistema. |
-| User reactivated, still without a password | Acesso de *{nome}* reativado. Como a senha ainda não tinha sido criada, enviamos um novo link para *{e-mail}*. |
+| User reactivated | Acesso de *{nome}* reativado. A pessoa já pode entrar no sistema. |
## Operations
@@ -198,13 +199,13 @@ Minimum levels follow [permission profiles](permission-profiles.md#what-each-lev
| `listUsers({ page, pageSize, search, status })` | `GET /api/v1/users` | Reader | Paged, see below |
| `getUser(id)` | `GET /api/v1/users/{id}` | Reader | |
| `suggestLogin(fullName)` | `POST /api/v1/users/login-suggestion` | Editor | Full name in the body, never in the URL (personal data) |
-| `createUser(data)` | `POST /api/v1/users` | Editor | Returns `{ user, emailSent }` (AUTH-05) |
+| `createUser(data)` | `POST /api/v1/users` | Editor | `data` includes `temporaryPassword` (USR-35). Returns the created user |
| `updateUser(id, data, version)` | `PUT /api/v1/users/{id}` | Editor | Version in `If-Match` |
-| `resendAccess(id)` | `POST /api/v1/users/{id}/resend-access` | Editor | Returns `{ emailSent, email }` |
+| `resetUserPassword(id, temporaryPassword)` | `POST /api/v1/users/{id}/reset-password` | Manager | AUTH-29. Answers 204; the password is not part of the record, so no version is sent |
| `deactivateUser(id, version)` | `POST /api/v1/users/{id}/deactivate` | Manager | |
-| `reactivateUser(id, version)` | `POST /api/v1/users/{id}/reactivate` | Manager | Returns `{ user, emailSent }` |
+| `reactivateUser(id, version)` | `POST /api/v1/users/{id}/reactivate` | Manager | |
-**Listing.** `status` accepts `current` (active and pending, the default), `pending`, `inactive` or `all`. `search` looks in the display name, full name, login and CPF digits, ignoring accents and case. The response is `{ items, page, pageSize, totalCount }`, and each item has `profileNames` for the Perfis column. The prototype shows 10 users per page.
+**Listing.** `status` accepts `active` (the default), `inactive` or `all`. `search` looks in the display name, full name, login and CPF digits, ignoring accents and case. The response is `{ items, page, pageSize, totalCount }`, and each item has `profileNames` for the Perfis column. The prototype shows 10 users per page.
## Errors
@@ -214,8 +215,6 @@ Minimum levels follow [permission profiles](permission-profiles.md#what-each-lev
| 403 | `forbidden` | Operation above the person's level |
| 404 | `not_found` | Unknown user id |
| 409 | `concurrency_conflict` | CNV-13; `details.updatedByName` says who changed it |
-| 409 | `system_record` | Changing or deactivating the Admin (USR-23, USR-28) |
+| 409 | `system_record` | Changing or deactivating the Admin, or resetting its password (USR-23, USR-28, AUTH-15) |
| 409 | `self_deactivation` | USR-19, USR-27 |
-| 409 | `not_pending` | Resending access to a user who is not pending |
-| 409 | `email_missing` | Resending access to a user without an e-mail |
| 409 | `not_inactive` | Reactivating a user who is not deactivated (USR-18, USR-29) |
diff --git a/docs/product/glossary.md b/docs/product/glossary.md
index 32f3793..ccf1b53 100644
--- a/docs/product/glossary.md
+++ b/docs/product/glossary.md
@@ -2,14 +2,14 @@
The business speaks Portuguese; the code speaks English. This table is the single translation, so that people and agents name the same concept the same way (the *ubiquitous language* of DDD). Wire values are the ones the front-end already uses and must not change ([API conventions](../api/conventions.md)).
-The value objects (`Login`, `EmailAddress`, `Cpf`, `PhoneNumber`, `Cep`, `BloodType`, `AccessLevel`, `ScreenKey`), the aggregates (`User`, `PermissionProfile`), `UserStatus`, `SystemIds` and the domain services (`EffectiveAccess`, `LoginSuggestion`) already exist in the Domain project; the other code names are the ones the rest of the first slice must use. If implementation shows a better name, change it here in the same pull request, so the glossary and the code never disagree. When a new business term appears, add it too. Sources: the [feature documents](features/), [conventions](conventions.md) and ADR-0006.
+The value objects (`Login`, `EmailAddress`, `Cpf`, `PhoneNumber`, `Cep`, `BloodType`, `AccessLevel`, `ScreenKey`), the aggregates (`User`, `PermissionProfile`), `UserStatus`, `SystemIds` and the domain services (`EffectiveAccess`, `LoginSuggestion`) already exist in the Domain project; the other code names are the ones the rest of the first slice must use. If implementation shows a better name, change it here in the same pull request, so the glossary and the code never disagree. When a new business term appears, add it too. Sources: the [feature documents](features/), [conventions](conventions.md) and decision 3 of the [decisions document](../decisoes-de-arquitetura.md), where every decision cited by number is.
## Access and permissions
| Business term (pt-BR) | Code | Wire value / field | Notes |
|---|---|---|---|
| Tela | `Screen` | — | Each submenu item, such as Usuários or Contas a Pagar |
-| Chave da tela | `ScreenKey`, constants in `ScreenKeys` | `screen`, e.g. `gerenciamento/usuarios` | Fixed forever, never derived from the name (ADR-0021) |
+| Chave da tela | `ScreenKey`, constants in `ScreenKeys` | `screen`, e.g. `gerenciamento/usuarios` | Fixed forever, never derived from the name (decision 25) |
| Área | `Area` | `id`, e.g. `gerenciamento` | Menu group; a presentation concept, not a code boundary |
| Nível de acesso | `AccessLevel` | `level` | Ordered: `Denied < Reader < Editor < Manager` |
| Negado | `AccessLevel.Denied` | `negado` | Absence of permission, not a prohibition |
@@ -29,9 +29,9 @@ The value objects (`Login`, `EmailAddress`, `Cpf`, `PhoneNumber`, `Cep`, `BloodT
| Business term (pt-BR) | Code | Wire value / field | Notes |
|---|---|---|---|
| Usuário | `User` | `users` | Aggregate root |
-| Admin | System user, `SystemIds.AdminUser` | id `00000000-0000-7000-8000-000000000001`, login `admin` | Only account without an activation link |
+| Admin | System user, `SystemIds.AdminUser` | id `00000000-0000-7000-8000-000000000001`, login `admin` | Demo account: fixed password, no mandatory password change (AUTH-13, AUTH-15) |
| Login | `Login` (value object) | `login` | Unique, lowercase, 3–30 characters |
-| E-mail | `EmailAddress` (value object) | `email` | Required and **not** unique |
+| E-mail | `EmailAddress` (value object) | `email` | Optional and **not** unique |
| Nome completo | `FullName` | `fullName` | |
| Nome de exibição | `DisplayName` | `displayName` | Unique ignoring case, accents and outer spaces |
| CPF | `Cpf` (value object) | `cpf` | Digits only, informational, not unique |
@@ -40,16 +40,14 @@ The value objects (`Login`, `EmailAddress`, `Cpf`, `PhoneNumber`, `Cep`, `BloodT
| Tipo sanguíneo | `BloodType` (value object) | `bloodType` | Closed list; sensitive personal data |
| Endereço | `Address` | `address` | Always present, fields optional |
| Contato de emergência | `EmergencyContact` | `emergencyContact` | Always present, fields optional |
-| Assinatura | `Signature` | `signature` | Out of the first slice (ADR-0018 open) |
+| Assinatura | `Signature` | `signature` | Stored in the user's record (USR-36); out of the first slice |
| Situação da conta | `UserStatus` | `status` | |
-| Pendente | `UserStatus.Pending` | `pending` | Created, waiting for activation |
| Ativo | `UserStatus.Active` | `active` | |
| Desativado | `UserStatus.Inactive` | `inactive` | Users are deactivated, never deleted |
| Desativar / Reativar | `Deactivate` / `Reactivate` | `deactivateUser` / `reactivateUser` | Manager level |
-| Link de ativação | Access link, purpose `Activation` | `purpose: activation` | Single use, 72 hours |
-| Link de troca de senha | Access link, purpose `PasswordReset` | `purpose: reset` | Single use, 2 hours |
-| Reenviar acesso | `ResendAccess` | `resendAccess` | Issues a new activation link and invalidates the previous one |
-| Esqueci minha senha | `RequestPasswordReset` | `requestPasswordReset` | Always the same answer |
+| Senha temporária | Temporary password | `temporaryPassword` | Set by who registers the user or resets the password; replaced at sign-in (AUTH-14) |
+| Troca obrigatória de senha | `MustChangePassword` | `mustChangePassword`, `password_change_required` | True while the account has a temporary password |
+| Redefinir senha | `ResetUserPassword` | `resetUserPassword` | Manager level; sets a new temporary password (AUTH-29) |
| Sugestão de login | `LoginSuggestion` (domain service), `SuggestLogin` (use case) | `suggestLogin` | |
| Bloqueio por tentativas | Lockout | `locked_out` | 5 failures, 15 minutes |
@@ -57,6 +55,6 @@ The value objects (`Login`, `EmailAddress`, `Cpf`, `PhoneNumber`, `Cep`, `BloodT
| Business term (pt-BR) | Code | Wire value / field | Notes |
|---|---|---|---|
-| Autoria | Audit fields | `createdAt`, `createdBy`, `updatedAt`, `updatedBy` | Filled by the server (ADR-0015) |
+| Autoria | Audit fields | `createdAt`, `createdBy`, `updatedAt`, `updatedBy` | Filled by the server (decision 14) |
| Versão | Concurrency token (`xmin`) | `version`, `ETag`, `If-Match` | Opaque; conflicts return 409 |
| Cadastro | Record / aggregate | — | "Cadastro de Ana Souza" = Ana's user record |
diff --git a/docs/product/open-questions.md b/docs/product/open-questions.md
index 8cce582..066c785 100644
--- a/docs/product/open-questions.md
+++ b/docs/product/open-questions.md
@@ -8,7 +8,6 @@ What is still undecided and affects the back-end or its integration with the fro
| ID | Question |
|---|---|
-| OQ-01 | **Where to store the signature.** The prototype keeps the image inside the user record, as text of up to about 180,000 characters. ADR-0018 proposes separate object storage, with the record holding only a reference. The signature is out of the first back-end slice and must be decided before it enters (USR-25). |
| OQ-02 | **Full change history.** Authorship records who created a record and who changed it last, but not what changed. A field-by-field history is left for a later stage (CNV-10). |
| OQ-03 | **Live updates between people.** An open list does not refresh by itself when someone else changes a record; it refreshes when the page is reloaded, the list is reopened or the person saves something. Protection against simultaneous saves is decided (CNV-12); automatic list refresh is not. |
@@ -16,8 +15,6 @@ What is still undecided and affects the back-end or its integration with the fro
| ID | Question |
|---|---|
-| OQ-04 | **Production e-mail provider.** The system sends e-mails (ADR-0030); the provider for the public environment is not chosen yet. |
-| OQ-05 | **Demo access in the public environment.** The prototype shows the login `admin` and the password `admin123`, and e-mails go to its test inbox. For the real back-end published to reviewers, it is undecided how someone signs in without receiving an activation link: for example, a demo user with a password published in the README, limited profiles and data restored periodically. |
| OQ-06 | **Personal data of the Admin.** The Admin cannot be changed (USR-23). Should it still be possible to fill in data such as phone and signature, or does it stay a technical account? Until decided, it is a technical account. |
| OQ-07 | **Where the front-end is served.** The session refresh cookie works only if the page is on `localhost` or on the API's own site (API-10). Serving the HTML from the API or from another address is undecided, and it defines the CORS configuration. |
### Content
@@ -30,22 +27,27 @@ What is still undecided and affects the back-end or its integration with the fro
## Resolved
+Decisions cited by number are in the [decisions document](../decisoes-de-arquitetura.md).
+
| Question | Decision | Where |
|---|---|---|
-| There is no login | Unique `login` field, required non-unique e-mail, activation link sent by e-mail, password reset by login | [authentication](features/authentication.md), ADR-0019, ADR-0030 |
-| CPF, phone and CEP stored with masks | Informational, stored digits only, formatted on display; CPF not unique | [conventions](conventions.md#informational-documents-cpf-phone-and-cep), ADR-0006 |
-| Screen key derived from its name | Fixed keys declared in code, current values preserved | [overview](overview.md#screen-keys), ADR-0021 |
-| No authorship or creation date | Who created, who changed and when, filled by the server and shown in the record footer | [conventions](conventions.md#authorship-audit), ADR-0015 |
-| Users deleted for good | Users are deactivated and can be reactivated; unused profiles can still be deleted | [users](features/users.md#deactivation-and-reactivation), ADR-0016 |
-| Two people saving the same record | Version control: the second save is refused, naming who changed it | [conventions](conventions.md#several-people-editing-at-the-same-time-optimistic-concurrency), ADR-0014 |
-| Whole lists loaded at once | Server-side paging, search and sorting; small reference lists stay whole | [API conventions](../api/conventions.md#lists), ADR-0017 |
-| Permissions not enforced | Denied screens and empty areas are hidden, and the server refuses operations above the level | [permission profiles](features/permission-profiles.md#what-the-person-sees), ADR-0020 |
-| Levels of removed screens left orphaned | Removed screens have their key retired, and their levels are cleaned up | [overview](overview.md#screen-keys), ADR-0021 |
+| There is no login | Unique `login` field, optional non-unique e-mail, temporary password set at registration and replaced at the first sign-in | [authentication](features/authentication.md), decisions 18 and 21 |
+| CPF, phone and CEP stored with masks | Informational, stored digits only, formatted on display; CPF not unique | [conventions](conventions.md#informational-documents-cpf-phone-and-cep), decision 3 |
+| Screen key derived from its name | Fixed keys declared in code, current values preserved | [overview](overview.md#screen-keys), decision 25 |
+| No authorship or creation date | Who created, who changed and when, filled by the server and shown in the record footer | [conventions](conventions.md#authorship-audit), decision 14 |
+| Users deleted for good | Users are deactivated and can be reactivated; unused profiles can still be deleted | [users](features/users.md#deactivation-and-reactivation), decision 15 |
+| Two people saving the same record | Version control: the second save is refused, naming who changed it | [conventions](conventions.md#several-people-editing-at-the-same-time-optimistic-concurrency), decision 13 |
+| Whole lists loaded at once | Server-side paging, search and sorting; small reference lists stay whole | [API conventions](../api/conventions.md#lists), decision 17 |
+| Permissions not enforced | Denied screens and empty areas are hidden, and the server refuses operations above the level | [permission profiles](features/permission-profiles.md#what-the-person-sees), decision 24 |
+| Levels of removed screens left orphaned | Removed screens have their key retired, and their levels are cleaned up | [overview](overview.md#screen-keys), decision 25 |
| Unknown screen in a profile's levels (OQ-11) | The save is refused with a message to reload the page, so a mistyped key never looks saved | [permission profiles](features/permission-profiles.md#validation-on-save) (PERM-24) |
-| What each level allows | Reader views; Editor creates, changes, duplicates profiles and resends access; Manager also deletes, deactivates and reactivates. A Reader on Usuários sees signatures | [permission profiles](features/permission-profiles.md#what-each-level-allows), ADR-0020 |
-| Identifier format | UUID v7 generated by the server; fixed GUIDs for the Admin and the Gerenciador profile | [conventions](conventions.md#record-fields), ADR-0012 |
-| Back-end structure | Four projects by layer with feature folders, Minimal APIs everywhere | ADR-0005, ADR-0003 |
-| Knowing right away whether the activation e-mail left | The user is saved first as pending, and the e-mail is sent right after, in the same operation | [authentication](features/authentication.md#accounts-and-activation), ADR-0031 |
-| Session lifetime in the real back-end | 8 hours from the last action, with the account checked on every request | [authentication](features/authentication.md#sessions), ADR-0032 |
-| Duplicated login or name: field error or conflict | Validation error (400), with the message under the field | [API conventions](../api/conventions.md#errors), ADR-0009 |
+| What each level allows | Reader views; Editor creates, changes and duplicates profiles; Manager also deletes, deactivates, reactivates and resets passwords. A Reader on Usuários sees signatures | [permission profiles](features/permission-profiles.md#what-each-level-allows), decision 24 |
+| Identifier format | UUID v7 generated by the server; fixed GUIDs for the Admin and the Gerenciador profile | [conventions](conventions.md#record-fields), decision 12 |
+| Back-end structure | Four projects by layer with feature folders, Minimal APIs everywhere | decisions 2 and 4 |
+| Knowing right away whether the activation e-mail left | No longer applies: no e-mail is sent while access by e-mail is postponed | decision 21 |
+| Session lifetime in the real back-end | 8 hours from the last action, with the account checked on every request | [authentication](features/authentication.md#sessions), decisions 20 and 24 |
+| Duplicated login or name: field error or conflict | Validation error (400), with the message under the field | [API conventions](../api/conventions.md#errors), decision 7 |
+| Production e-mail provider (OQ-04) | Not needed for now: access by e-mail is postponed, and the question comes back with it | decision 21 |
+| Demo access in the public environment (OQ-05) | The Admin is the demo account, with a fixed login and password shown on the sign-in screen | [authentication](features/authentication.md#the-admin-demo-account) (AUTH-13), decision 21 |
+| Where to store the signature (OQ-01) | In a column of the user's own record, as the prototype does; only the single-record response returns it. It stays out of the first back-end slice | [users](features/users.md#signature) (USR-36), "O que ficou de fora" in the decisions document |
| Documentation language and structure | English by default, organized by feature, with stable rule IDs | [docs index](../README.md) |
diff --git a/docs/product/overview.md b/docs/product/overview.md
index dabdc9b..0736caa 100644
--- a/docs/product/overview.md
+++ b/docs/product/overview.md
@@ -17,7 +17,7 @@ The system will be used by people with very different roles, and most of them ar
## Access
-Every use of the system starts at the sign-in screen, with login and password. Nobody creates their own account: each person is registered by someone with access to the Users screen and receives an e-mail link to create their own password. See [authentication](features/authentication.md).
+Every use of the system starts at the sign-in screen, with login and password. Nobody creates their own account: each person is registered by someone with access to the Users screen, who sets a temporary password, and the person replaces it with their own when they first sign in. See [authentication](features/authentication.md).
## Navigation
@@ -34,7 +34,7 @@ Each screen has a fixed **key**, in the format `area/screen`, that identifies it
| Key | `relatorios/relatorio-de-vendas` | The system | **Never** |
| Name | "Relatório de Vendas" | People | Whenever needed |
-- **Defined in code** (`ScreenKeys` in the Domain project, ADR-0021), written once when the screen is created and never recalculated from the name. If keys followed names, renaming a screen would silently make it disappear for everyone except the Gerenciador profile, because stored permissions would point to the old key.
+- **Defined in code** (`ScreenKeys` in the Domain project, decision 25 of the [decisions document](../decisoes-de-arquitetura.md)), written once when the screen is created and never recalculated from the name. If keys followed names, renaming a screen would silently make it disappear for everyone except the Gerenciador profile, because stored permissions would point to the old key.
- **Format:** lowercase, without accents, hyphens instead of spaces. This is only a readability convention: once created, a key does not follow changes to the name.
- **Never reused.** A removed screen has its key retired, and its stored levels are cleaned up by a migration.
- **Values preserved.** The current keys were kept exactly as the prototype used them, so no stored permission needs migration.