feat(v1.1.8): password reset flow (migration 0029 + revokes sessions)

migration 0029: app_user_password_resets table — same shape as
verification (token_hash PK, app_id + user_id FKs, expires_at,
consumed_at). One-shot via atomic UPDATE WHERE consumed_at IS NULL.
Default TTL 1h (shorter than verification's 48h — reset tokens are
higher-risk).

UsersServiceImpl gains password_resets: Arc<dyn AppUserPasswordResetRepo>.

users::request_password_reset(email, opts):
  * Returns Ok(()) regardless of whether the email matched — no
    existence-leak signal in script-land (per brief).
  * Email-not-configured surfaces as NotConfigured so scripts can
    fall back to a synchronous reset path. Other email errors are
    silently swallowed and logged server-side; surfacing them would
    leak which addresses produced a "real send attempted" signal vs
    a no-op.

users::complete_password_reset(token, new_password):
  * Atomically consumes the token, updates the Argon2id hash, and
    revokes EVERY active session for that user (anyone with a stale
    token shouldn't be able to ride out the reset). Emits
    users::password_changed.
  * Returns the user on success, () on bad/expired/already-used.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-06 12:06:47 +02:00
parent c855739559
commit 45242e2d92
5 changed files with 237 additions and 11 deletions

View File

@@ -21,9 +21,9 @@ use picloud_manager_core::{
FilesServiceImpl, FsFilesRepo, HttpConfig, HttpServiceImpl, KvServiceImpl, OutboxEventEmitter,
OutboxRepo, PostgresAbandonedRepo, PostgresAdminSessionRepository, PostgresAdminUserRepository,
PostgresApiKeyRepository, PostgresAppDomainRepository, PostgresAppMembersRepository,
PostgresAppRepository, PostgresAppSecretsRepo, PostgresAppUserRepository,
PostgresAppUserSessionRepository, PostgresAppUserVerificationRepo, PostgresDeadLetterRepo,
PostgresDeadLetterService,
PostgresAppRepository, PostgresAppSecretsRepo, PostgresAppUserPasswordResetRepo,
PostgresAppUserRepository, PostgresAppUserSessionRepository,
PostgresAppUserVerificationRepo, PostgresDeadLetterRepo, PostgresDeadLetterService,
PostgresDocsRepo, PostgresExecutionLogRepository, PostgresExecutionLogSink, PostgresKvRepo,
PostgresOutboxRepo, PostgresPubsubRepo, PostgresRouteRepository, PostgresScriptRepository,
PostgresSecretsRepo, PostgresTopicRepo, PostgresTriggerRepo, PrincipalResolver,
@@ -253,10 +253,13 @@ pub async fn build_app(
let app_user_sessions_repo = Arc::new(PostgresAppUserSessionRepository::new(pool.clone()));
let app_user_verifications_repo =
Arc::new(PostgresAppUserVerificationRepo::new(pool.clone()));
let app_user_password_resets_repo =
Arc::new(PostgresAppUserPasswordResetRepo::new(pool.clone()));
let users: Arc<dyn UsersService> = Arc::new(UsersServiceImpl::new(
app_users_repo.clone(),
app_user_sessions_repo.clone(),
app_user_verifications_repo.clone(),
app_user_password_resets_repo.clone(),
email.clone(),
authz.clone(),
events.clone(),