Skip to content

feat: email confirmation, factor management endpoints, account security dashboard#9

Merged
TymekV merged 16 commits into
aginrocks:masterfrom
PawiX25:feat/email-confirm-login-notif-delete-factors
Apr 6, 2026
Merged

feat: email confirmation, factor management endpoints, account security dashboard#9
TymekV merged 16 commits into
aginrocks:masterfrom
PawiX25:feat/email-confirm-login-notif-delete-factors

Conversation

@PawiX25

@PawiX25 PawiX25 commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

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 via find_one_and_delete), mark email as confirmed
  • DELETE /api/settings/factors/totp/disable — remove TOTP factor
  • DELETE /api/settings/factors/pgp/disable — remove PGP key
  • DELETE /api/settings/factors/webauthn/delete/{display_name} — remove passkey by name
  • POST /api/settings/factors/recovery-codes/reset — invalidate existing codes, generate fresh set of 10
  • Login notification email sent on successful password auth (includes IP address)
  • Confirmation email sent on registration
  • Factor added/removed email notifications on all factor changes
  • Extract hash_password into shared utils (replaces inline argon2 in register)
  • ConnectInfo<SocketAddr> wired for IP extraction

Frontend (apps/frontend/)

  • Account security dashboard (/dashboard) — Google-style card layout with expandable rows for each factor
  • Password, TOTP, WebAuthn, PGP, Recovery Codes management with inline forms
  • Spring animations (framer-motion) for expand/collapse, smooth content resize
  • scrollbar-gutter: stable to prevent layout shift

@PawiX25 PawiX25 force-pushed the feat/email-confirm-login-notif-delete-factors branch from 75c2a69 to 87da319 Compare April 4, 2026 03:30
Comment thread server/src/routes/api/login/password.rs Outdated
Comment thread apps/frontend/app/dashboard/page.tsx Outdated
Comment thread apps/frontend/app/dashboard/page.tsx Outdated
Comment thread apps/frontend/app/dashboard/page.tsx Outdated
Comment thread apps/frontend/app/dashboard/page.tsx
Comment thread apps/frontend/app/dashboard/page.tsx
Comment thread apps/frontend/app/dashboard/page.tsx Outdated
Comment thread apps/frontend/app/dashboard/page.tsx Outdated
Comment thread server/src/routes/api/confirm_email.rs Outdated
PawiX25 added 2 commits April 4, 2026 06:00
- 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
Comment thread apps/frontend/app/dashboard/components/webauthn-row.tsx Outdated
Comment thread apps/frontend/app/dashboard/page.tsx Outdated
Comment thread server/src/routes/api/confirm_email.rs Outdated
Comment thread apps/frontend/app/dashboard/components/totp-row.tsx Outdated
Comment thread apps/frontend/app/dashboard/components/totp-row.tsx Outdated
Comment thread apps/frontend/app/dashboard/components/password-row.tsx Outdated
Comment thread apps/frontend/app/dashboard/components/factor-row.tsx
Comment thread apps/frontend/app/dashboard/components/totp-row.tsx Outdated
Comment thread apps/frontend/app/dashboard/components/factor-row.tsx

@TymekV TymekV left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread apps/frontend/app/dashboard/components/pgp-row.tsx Outdated
Comment thread apps/frontend/app/dashboard/components/pgp-row.tsx Outdated
Comment thread apps/frontend/app/dashboard/components/pgp-row.tsx Outdated
PawiX25 added a commit to PawiX25/agin-auth that referenced this pull request Apr 4, 2026
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
PawiX25 added a commit to PawiX25/agin-auth that referenced this pull request Apr 4, 2026
- 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
Comment thread apps/frontend/app/dashboard/components/factor-row.tsx
Comment thread apps/frontend/app/dashboard/components/helpers.tsx
Comment thread apps/frontend/app/dashboard/components/pgp-row.tsx Outdated
Comment thread apps/frontend/app/dashboard/components/totp-row.tsx Outdated
Comment thread apps/frontend/app/dashboard/page.tsx Outdated
const [profileError, setProfileError] = useState('');

const fetchProfile = useCallback(() => {
fetch('/api/settings/profile').then(r => r.ok ? r.json() : null).then((p: Profile | null) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use React Query

Comment thread server/src/routes/api/settings/account.rs Outdated
Comment thread server/src/routes/api/settings/profile.rs
Comment thread server/src/routes/api/confirm_email.rs Outdated
Comment thread server/src/oidc.rs
Comment thread server/src/routes/oidc_routes.rs
PawiX25 added a commit to PawiX25/agin-auth that referenced this pull request Apr 4, 2026
- 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>
PawiX25 added a commit to PawiX25/agin-auth that referenced this pull request Apr 4, 2026
- 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
@TymekV

TymekV commented Apr 4, 2026

Copy link
Copy Markdown
Member

Please format your code with rustfmt.

Comment thread apps/frontend/app/dashboard/components/password-row.tsx Outdated
Comment thread apps/frontend/app/dashboard/components/pgp-row.tsx Outdated
Comment thread apps/frontend/app/dashboard/components/pgp-row.tsx Outdated
Comment thread apps/frontend/app/dashboard/components/recovery-codes-row.tsx Outdated
Comment thread apps/frontend/app/dashboard/components/webauthn-row.tsx Outdated
Comment thread server/src/routes/api/settings/factors/webauthn/delete.rs Outdated
Comment thread server/src/routes/api/settings/account.rs
Comment thread server/src/routes/api/settings/profile.rs Outdated
Comment thread server/src/routes/api/confirm_email.rs Outdated
Comment thread server/src/routes/api/confirm_email.rs Outdated
PawiX25 added 3 commits April 5, 2026 10:36
- 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.
Comment thread server/src/init.rs Outdated
Comment thread server/src/webauthn/types.rs
Comment thread server/src/oidc.rs Outdated
Comment thread server/src/oidc.rs Outdated
pub nonce: Option<String>,
}

#[derive(Debug, Serialize, ToSchema)]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

derive IntoParams instead of re-defining them in the utoipa macro.

Comment on lines +103 to +120
// 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."
)));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a middleware

.map(|s| s.to_string())
.collect();

let valid_scopes = ["openid", "profile", "email", "offline_access"];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move valid scopes to a static (or a lazy static) and reference it here as well as in the configuration endpoint

Comment on lines +274 to +275
redirect_url.push_str(if redirect_url.contains('?') { "&" } else { "?" });
redirect_url.push_str(&format!("code={code}"));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a crate like serde_urlencoded or serde_qs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split the implementation into multiple files

Comment on lines +203 to +217
// 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")));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use middleware

Comment on lines +219 to +248
// 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"
)));
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid repeating common logic and especially logic as important as permission checks.

@TymekV

TymekV commented Apr 5, 2026

Copy link
Copy Markdown
Member

How the settings should behave

Every 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 Password

Change Password does not expand. Clicking it should invoke password change flow.

Changing the password (modal): Sudo mode prompt -> Change password prompt

Authenticator App

When 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

Passkeys

When 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

PGP

When 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 Codes

When expanded: Should show if the codes are currently generated and how many are remaining

Regenerating keys (modal): Sudo mode prompt -> Displaying the keys

@TymekV

TymekV commented Apr 6, 2026

Copy link
Copy Markdown
Member

How sudo mode should be implemented

There should be a unified flow for confirming dangerous actions.

Confirmation flow

  1. User uses an endpoint to start the flow -> this endpoint also returns possible factors
  2. User confirms their identity with a 2FA factor of their choice using the same endpoints as when logging in
  3. On success, user's session is promoted to a trusted state for a short period of time (like 5 minutes).

Implementation notes

Note

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 feat/refactor completely overhauls factors system, unifying them under the Factor trait, which makes it way easier to implement features like this. It should be reconsidered if implemeting sudo mode falls in the scope of this PR or not.

PawiX25 added 2 commits April 6, 2026 04:02
…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
Comment thread apps/frontend/app/dashboard/components/dashboard-header.tsx
Comment thread apps/frontend/app/dashboard/components/password-row.tsx
Comment thread apps/frontend/app/dashboard/components/totp-row.tsx
Comment thread apps/frontend/lib/api-error.ts Outdated
Comment thread server/src/routes/api/register.rs Outdated
@TymekV TymekV merged commit f1ce0ca into aginrocks:master Apr 6, 2026
2 checks passed
@PawiX25 PawiX25 deleted the feat/email-confirm-login-notif-delete-factors branch April 9, 2026 22:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants