Make all mailbox fields editable; pre-populate username from API response

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
Agent-Logs-Url: https://github.com/christianlouis/pop_puller_to_gmail/sessions/20fa7a89-23e5-462c-8b4d-9c6cdb4d0501
This commit is contained in:
copilot-swe-agent[bot]
2026-03-25 23:06:04 +00:00
parent 20170f2f2d
commit 8cf0d101bf
5 changed files with 46 additions and 37 deletions
+2 -2
View File
@@ -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`.
+8 -2
View File
@@ -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)
+1 -1
View File
@@ -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)
+27 -32
View File
@@ -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 && (
<button
type="button"
onClick={handleAutoDetect}
disabled={autoDetecting}
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 disabled:opacity-50"
>
{autoDetecting ? 'Detecting...' : 'Auto-Detect'}
</button>
)}
<button
type="button"
onClick={handleAutoDetect}
disabled={autoDetecting}
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 disabled:opacity-50"
>
{autoDetecting ? 'Detecting...' : 'Auto-Detect'}
</button>
</div>
{isEditMode && (
<p className="mt-1 text-xs text-gray-500 flex items-center gap-1">
<Lock className="h-3 w-3" />
Username and server settings cannot be changed after creation.
</p>
)}
</div>
<div>
@@ -284,8 +283,7 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
name="protocol"
value={formData.protocol}
onChange={handleChange}
disabled={isEditMode}
className="w-full 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="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="pop3">POP3</option>
<option value="pop3_ssl">POP3 (SSL)</option>
@@ -304,8 +302,7 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
value={formData.host}
onChange={handleChange}
required={!isEditMode}
disabled={isEditMode}
className="w-full 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="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="pop.gmail.com"
/>
</div>
@@ -320,8 +317,7 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
value={formData.port}
onChange={handleChange}
required={!isEditMode}
disabled={isEditMode}
className="w-full 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="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
@@ -334,10 +330,9 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
id="use_ssl"
checked={formData.use_ssl}
onChange={handleChange}
disabled={isEditMode}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded disabled:cursor-not-allowed"
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="use_ssl" className={`ml-2 block text-sm ${isEditMode ? 'text-gray-400' : 'text-gray-700'}`}>
<label htmlFor="use_ssl" className="ml-2 block text-sm text-gray-700">
Use SSL/TLS
</label>
</div>
+8
View File
@@ -58,6 +58,7 @@ export interface MailAccount {
port: number;
use_ssl: boolean;
use_tls: boolean;
username: string;
forward_to: string;
delivery_method: string;
is_enabled: boolean;
@@ -97,6 +98,13 @@ export interface MailAccountCreate {
export interface MailAccountUpdate {
name?: string;
email_address?: string;
protocol?: string;
host?: string;
port?: number;
use_ssl?: boolean;
use_tls?: boolean;
username?: string;
password?: string;
forward_to?: string;
delivery_method?: string;