feat: email confirmation, factor management endpoints, account security dashboard#9
Conversation
75c2a69 to
87da319
Compare
- Extract dashboard components into separate files (helpers, factor-row, password-row, totp-row, webauthn-row, pgp-row, recovery-codes-row) - Use useForm hook for password change form - Hide passkeys list when section is collapsed - Add confirmation step before deleting a passkey - Add reverse proxy support via axum-client-ip (trust_proxy config option) - Clean up expired email confirmation tokens on expiry check
TymekV
left a comment
There was a problem hiding this comment.
Generally for modals you should use something like ModalManager. It allows for fully typed modal props and implementing confirmation modals is very easy. It also exposes Promise-based APIs.
Backend: - confirm_email: emit warning when expired token cleanup fails - confirm_email: add GET handler so email links open in browser Frontend dashboard components: - factor-row: add cursor-pointer and rounded-[inherit] on hover button - helpers/ExpandForm: fix input outline clipping (overflow hidden → timed visible) - password-row: simplify description to "Authenticate using a password." - pgp-row: full rewrite with useForm, hide key when collapsed, trash icon outside fingerprint box (vertically centered), inline confirm/cancel - totp-row: full rewrite with useForm, add QR code with centered layout, instruction text, Copy button right-aligned - dashboard/page: add profile section, logout, delete account with animations Other: - add react-qr-code dependency - gitignore: add local config and key patterns Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ation and security dashboard
Backend:
- OIDC provider: RSA2048 key generation/persistence, JWT signing, JWKS endpoint,
OpenID Connect discovery, ID tokens with profile/email scope support
- OAuth2 authorization server: full authorization code flow, token endpoint,
refresh tokens, token introspection, userinfo endpoint, PKCE support
- Logout endpoint (session invalidation)
- Account settings API: profile GET/PATCH (display name, first/last name),
account DELETE with password confirmation
- Email confirmation: GET handler for browser-pasted links, warning on cleanup failure
- /api/logout endpoint
Frontend:
- Confirm email page: auto-confirms token on mount, success/error states
- Dashboard page: profile section with inline editing, security factors card,
logout button, danger zone with animated delete account form
- factor-row: cursor-pointer, rounded-[inherit] for hover bg on card corners
- helpers/ExpandForm: fix input outline clipping via timed overflow switch
- password-row: simplify description ("Authenticate using a password.")
- pgp-row: rewritten with useForm, key hidden when collapsed, trash icon outside
fingerprint box (vertically centered), inline confirm/cancel
- totp-row: rewritten with useForm, QR code centered with instruction text,
Copy button right-aligned, react-qr-code dependency added
- Extract dashboard components into separate files (helpers, factor-row, password-row, totp-row, webauthn-row, pgp-row, recovery-codes-row) - Use useForm hook for password change form - Hide passkeys list when section is collapsed - Add confirmation step before deleting a passkey - Add reverse proxy support via axum-client-ip (trust_proxy config option) - Clean up expired email confirmation tokens on expiry check
| const [profileError, setProfileError] = useState(''); | ||
|
|
||
| const fetchProfile = useCallback(() => { | ||
| fetch('/api/settings/profile').then(r => r.ok ? r.json() : null).then((p: Profile | null) => { |
- Use openidconnect crate for OIDC discovery, JWKS, and ID token signing - Fix refresh token replay vulnerability (rotate on use, revoke old token) - Import StatusCode and Query directly instead of fully qualified paths - Use validator crate with #[derive(Validate)] on UpdateProfileBody - Fix factor-row highlight overflow with overflow-hidden on card containers - Replace CopyButton plain button with shadcn Button - Use useMutation onSuccess/onError callbacks in pgp-row and totp-row - Migrate page.tsx to React Query (useQuery/useMutation), extract ProfileRow and DeleteAccountSection components - Add Next.js dev proxy for /api/* to backend port 8081 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use openidconnect crate for OIDC discovery, JWKS, and ID token signing - Fix refresh token replay vulnerability (rotate on use, revoke old token) - Import StatusCode and Query directly instead of fully qualified paths - Use validator crate with #[derive(Validate)] on UpdateProfileBody - Fix factor-row highlight overflow with overflow-hidden on card containers - Replace CopyButton plain button with shadcn Button - Use useMutation onSuccess/onError callbacks in pgp-row and totp-row - Migrate page.tsx to React Query (useQuery/useMutation), extract ProfileRow and DeleteAccountSection components
|
Please format your code with rustfmt. |
- Use openidconnect crate for OIDC discovery, JWKS, and ID token signing - Fix refresh token replay vulnerability (rotate on use, revoke old token) - Import StatusCode and Query directly instead of fully qualified paths - Use validator crate with #[derive(Validate)] on UpdateProfileBody - Fix factor-row highlight overflow with overflow-hidden on card containers - Replace CopyButton plain button with shadcn Button - Use useMutation onSuccess/onError callbacks in pgp-row and totp-row - Migrate page.tsx to React Query (useQuery/useMutation), extract ProfileRow and DeleteAccountSection components
- Remove unused imports (schema from utoipa, redundant hex) - Prefix unused function parameters with underscore - Remove dead code (unused Group struct, RegisterSuccess, object_id_as_string module) - Add #[allow(dead_code)] for WebAuthn spec types kept for future use - Add #[allow(clippy::enum_variant_names)] for spec-compliant enum - Collapse nested if-let chains in extract_client_credentials - Apply rustfmt formatting across all modified files
Address PR aginrocks#9 review comment #24: memoize onSubmit and handleToggle callbacks with useCallback to prevent unnecessary re-renders.
… allows for webauthn types
| pub nonce: Option<String>, | ||
| } | ||
|
|
||
| #[derive(Debug, Serialize, ToSchema)] |
There was a problem hiding this comment.
derive IntoParams instead of re-defining them in the utoipa macro.
| // Check user is authenticated | ||
| let user_id = session | ||
| .get::<mongodb::bson::oid::ObjectId>("user_id") | ||
| .await?; | ||
| let auth_state = session | ||
| .get::<crate::routes::api::AuthState>("auth_state") | ||
| .await?; | ||
|
|
||
| if user_id.is_none() | ||
| || !matches!( | ||
| auth_state, | ||
| Some(crate::routes::api::AuthState::Authenticated) | ||
| ) | ||
| { | ||
| return Err(AxumError::unauthorized(eyre::eyre!( | ||
| "Login required. Redirect to login page first." | ||
| ))); | ||
| } |
| .map(|s| s.to_string()) | ||
| .collect(); | ||
|
|
||
| let valid_scopes = ["openid", "profile", "email", "offline_access"]; |
There was a problem hiding this comment.
Move valid scopes to a static (or a lazy static) and reference it here as well as in the configuration endpoint
| redirect_url.push_str(if redirect_url.contains('?') { "&" } else { "?" }); | ||
| redirect_url.push_str(&format!("code={code}")); |
There was a problem hiding this comment.
Use a crate like serde_urlencoded or serde_qs
There was a problem hiding this comment.
Split the implementation into multiple files
| // Check user is authenticated | ||
| let user_id = session | ||
| .get::<mongodb::bson::oid::ObjectId>("user_id") | ||
| .await? | ||
| .ok_or_else(|| AxumError::unauthorized(eyre::eyre!("Not authenticated")))?; | ||
|
|
||
| let auth_state = session | ||
| .get::<crate::routes::api::AuthState>("auth_state") | ||
| .await?; | ||
| if !matches!( | ||
| auth_state, | ||
| Some(crate::routes::api::AuthState::Authenticated) | ||
| ) { | ||
| return Err(AxumError::unauthorized(eyre::eyre!("Not authenticated"))); | ||
| } |
| // Validate application | ||
| let app = state | ||
| .database | ||
| .collection::<Application>("applications") | ||
| .find_one(doc! { "client_id": &body.client_id }) | ||
| .await | ||
| .wrap_err("Database error")? | ||
| .ok_or_else(|| AxumError::bad_request(eyre::eyre!("Unknown client_id")))?; | ||
|
|
||
| if !app.redirect_uris.contains(&body.redirect_uri) { | ||
| return Err(AxumError::bad_request(eyre::eyre!("Invalid redirect_uri"))); | ||
| } | ||
|
|
||
| // Check user group access | ||
| let user = state | ||
| .database | ||
| .collection::<User>("users") | ||
| .find_one(doc! { "_id": &user_id }) | ||
| .await | ||
| .wrap_err("Database error")? | ||
| .ok_or_else(|| AxumError::bad_request(eyre::eyre!("User not found")))?; | ||
|
|
||
| if !app.allowed_groups.is_empty() { | ||
| let has_access = user.groups.iter().any(|g| app.allowed_groups.contains(g)); | ||
| if !has_access { | ||
| return Err(AxumError::forbidden(eyre::eyre!( | ||
| "You don't have access to this application" | ||
| ))); | ||
| } | ||
| } |
There was a problem hiding this comment.
Avoid repeating common logic and especially logic as important as permission checks.
How the settings should behaveEvery protected action should be confirmed with a second factor. When an user tries to do a protected action, he should be asked in a modal to authenticate using a second factor (we'll call it sudo mode). Change PasswordChange Password does not expand. Clicking it should invoke password change flow. Changing the password (modal): Sudo mode prompt -> Change password prompt Authenticator AppWhen expanded: Should show information if the authenticator is currently configured and an option to remove/add it. Adding the authenticator (modal): Sudo mode prompt -> Secret + QR Code -> Code confirmation PasskeysWhen expanded: Should show all added passkeys keys with options to manage them Adding a passkey (modal): Sudo mode prompt -> Prompt for the key name -> Enrollment PGPWhen expanded: Should show all added PGP keys with options to manage them Adding a PGP key (modal): Sudo mode prompt -> Prompt for key and key name Recovery CodesWhen expanded: Should show if the codes are currently generated and how many are remaining Regenerating keys (modal): Sudo mode prompt -> Displaying the keys |
How sudo mode should be implementedThere should be a unified flow for confirming dangerous actions. Confirmation flow
Implementation notesNote The same UI components and hooks that are used in login flow should be used for sudo mode authentication. Note In case the user doesn't have any second factors, password should be used instead. Note Branch |
…me oauth to oidc in api schema - delete-account-section: pass form data to mutate body so password is sent to server - password-row: include current_password in change-password request body - api.d.ts: rename /api/oauth/* paths to /api/oidc/* to match actual server routes
…angePasswordBody types
Add email confirmation flow, factor deletion/disable endpoints, login notifications, and a full account security dashboard.
Backend (server/)
POST /api/confirm-email— validate token (SHA256-hashed, 24h TTL, single-use viafind_one_and_delete), mark email as confirmedDELETE /api/settings/factors/totp/disable— remove TOTP factorDELETE /api/settings/factors/pgp/disable— remove PGP keyDELETE /api/settings/factors/webauthn/delete/{display_name}— remove passkey by namePOST /api/settings/factors/recovery-codes/reset— invalidate existing codes, generate fresh set of 10hash_passwordinto sharedutils(replaces inline argon2 in register)ConnectInfo<SocketAddr>wired for IP extractionFrontend (apps/frontend/)
/dashboard) — Google-style card layout with expandable rows for each factorscrollbar-gutter: stableto prevent layout shift