diff --git a/CHANGELOG.md b/CHANGELOG.md index a9923f7..f24241a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `auth_service.py` `get_google_user_info` now returns `access_token`, `refresh_token`, `expires_in`, and `scope` alongside user info so the login endpoint can persist Gmail credentials in the same request. ### Fixed -- Fixed mailbox edit form: username field was marked `required` but was never pre-populated (the backend intentionally excludes credentials from responses), making it impossible to save edits without re-entering the username. In edit mode the username is now pre-populated with the account's `email_address`, marked as `required` only when creating, and rendered as disabled together with the other immutable connection fields (protocol, host, port, Use SSL/TLS). The Auto-Detect button is also hidden in edit mode since server settings are locked after creation. +- Fixed mailbox edit form: username field was marked `required` but was never pre-populated (the backend intentionally excludes credentials from responses), making it impossible to save edits without re-entering the username. The backend now returns `username` in `MailAccountResponse` so the edit form can pre-populate it. All connection fields (protocol, host, port, use\_ssl, username) are now fully editable in edit mode. The Auto-Detect button is also shown in edit mode to re-detect server settings after a protocol change. - Fixed mailbox edit form silently overwriting stored credentials with an empty string: when the password field was left blank during an edit the frontend sent `password: ""`, which the backend encrypted and stored, locking the user out. The frontend now only includes `password` in the update payload when it is non-empty, and the backend additionally guards against empty-string passwords. -- Fixed mailbox edit form sending all `MailAccountCreate` fields (including immutable ones like `username`, `host`, `port`) on update requests. The submit handler now builds a `MailAccountUpdate` payload containing only the fields the backend accepts for update (`name`, `forward_to`, `delivery_method`, `is_enabled`, `check_interval_minutes`, `max_emails_per_check`, `delete_after_forward`, and optionally `password`). +- Fixed mailbox edit form sending all `MailAccountCreate` fields (including immutable ones like `username`, `host`, `port`) on update requests. The submit handler now builds a `MailAccountUpdate` payload containing all editable fields; `MailAccountUpdate` now covers every field in `MailAccountBase` (protocol, host, port, use\_ssl, use\_tls, username, email\_address, and the previously supported subset). - Gmail API `verify_access()` returning 403 for tokens that lacked a read-capable scope: added `gmail.readonly` to all scope lists. - Fixed three ESLint errors that caused CI to fail: removed unused `_setUser` store binding and unused `useAuthStore` import from `login/page.tsx`; replaced unused `_err` catch binding with a bare `catch {}` in `login/page.tsx`; removed a `useEffect` in `settings/page.tsx` that called `setProfileForm` synchronously (flagged by `react-hooks/set-state-in-effect`) — the effect was redundant because `useState` already initialises the form from the auth store's `user` object, which is the same value passed as `initialData` to `useQuery`. diff --git a/backend/app/models/schemas.py b/backend/app/models/schemas.py index 6321eb0..4c99c59 100644 --- a/backend/app/models/schemas.py +++ b/backend/app/models/schemas.py @@ -120,6 +120,13 @@ class MailAccountCreate(MailAccountBase): class MailAccountUpdate(BaseModel): name: Optional[str] = Field(None, max_length=255) + email_address: Optional[EmailStr] = None + protocol: Optional[MailProtocol] = None + host: Optional[str] = Field(None, max_length=255) + port: Optional[int] = Field(None, gt=0, lt=65536) + use_ssl: Optional[bool] = None + use_tls: Optional[bool] = None + username: Optional[str] = Field(None, max_length=255) password: Optional[str] = None forward_to: Optional[EmailStr] = None delivery_method: Optional[DeliveryMethod] = None @@ -145,9 +152,8 @@ class MailAccountResponse(MailAccountBase): created_at: datetime updated_at: datetime - # Don't expose password or username in responses + # Don't expose password in responses; username is safe to return password: str = Field(exclude=True, default="") - username: str = Field(exclude=True, default="") model_config = ConfigDict(from_attributes=True) diff --git a/docs/TODO.md b/docs/TODO.md index 9b0bc4e..7dc86fe 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -215,7 +215,7 @@ because the API client layer is missing. - [x] Mail accounts list with CRUD operations + enable/disable toggle - [x] Settings page — Profile, Gmail API connection, SMTP relay, Account info, Security - [x] `AddMailAccountModal` component (auto-detect, test connection, all required fields, is_enabled checkbox) -- [x] Fix `AddMailAccountModal` edit mode: pre-populate username from `email_address`; make connection fields (username, protocol, host, port, SSL) read-only; remove hard `required` on username; send only `MailAccountUpdate`-compatible payload on edit; skip empty-string password both in the frontend payload and as a backend guard +- [x] Fix `AddMailAccountModal` edit mode: backend now returns `username` in `MailAccountResponse`; all fields (including protocol, host, port, use\_ssl, username) are editable in edit mode and pre-populated from the stored account; Auto-Detect is shown in edit mode too; only password is omitted from the update payload when left blank - [x] `DashboardLayout` with responsive sidebar - [x] `AuthGuard` for protected routes - [x] Fix wizard grey screen (Tailwind v4 `bg-opacity` → `/75` syntax, modal restructure) diff --git a/frontend/src/components/AddMailAccountModal.tsx b/frontend/src/components/AddMailAccountModal.tsx index 9bf51ed..30059ef 100644 --- a/frontend/src/components/AddMailAccountModal.tsx +++ b/frontend/src/components/AddMailAccountModal.tsx @@ -4,7 +4,7 @@ import { useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { mailAccountsApi, MailAccount, MailAccountCreate, MailAccountUpdate } from '@/lib/api'; import { useAuthStore } from '@/store/authStore'; -import { X, Loader2, CheckCircle, XCircle, Lock } from 'lucide-react'; +import { X, Loader2, CheckCircle, XCircle } from 'lucide-react'; import { ProviderWizard } from './ProviderWizard'; interface AddMailAccountModalProps { @@ -29,7 +29,7 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro protocol: account?.protocol || 'pop3_ssl', host: account?.host || '', port: account?.port || 995, - username: account?.email_address || '', + username: account?.username || '', password: '', use_ssl: account?.use_ssl ?? true, use_tls: account?.use_tls ?? false, @@ -146,11 +146,18 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro e.preventDefault(); try { if (isEditMode) { - // For updates only send the fields the backend allows changing. - // Never send an empty password – the backend treats a non-empty value - // as an intentional credential change. + // Send all fields so the user can change any aspect of the account. + // Password is the only exception: omit it when blank so the stored + // credential is preserved. const updateData: MailAccountUpdate = { name: formData.name, + email_address: formData.email_address, + protocol: formData.protocol, + host: formData.host, + port: formData.port, + use_ssl: formData.use_ssl, + use_tls: formData.use_tls, + username: formData.username, forward_to: formData.forward_to, delivery_method: formData.delivery_method, is_enabled: formData.is_enabled, @@ -237,27 +244,19 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro value={formData.username} onChange={handleChange} required={!isEditMode} - disabled={isEditMode} - className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-50 disabled:text-gray-500 disabled:cursor-not-allowed" + className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="user@example.com" /> - {!isEditMode && ( - - )} + - {isEditMode && ( -
-