Skip to content

Commit 2fc2603

Browse files
fix: ACME account removal, status display, and timeline icon clipping
- Add permanent delete endpoint for deactivated ACME accounts (DELETE /accounts/{id}/permanent) - Add "Remove from database" button in ACME Accounts modal for deactivated accounts - Fix ACME Account stat card showing "Active" for deactivated accounts - Fix Timeline dot/icon clipping in Applied/Rejected/Pending sections Made-with: Cursor
1 parent 93d7ad8 commit 2fc2603

3 files changed

Lines changed: 80 additions & 8 deletions

File tree

backend/routers/letsencrypt.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,41 @@ async def deactivate_account(account_id: int, authorization: str = Header(None))
108108
raise HTTPException(status_code=400, detail=str(e))
109109

110110

111+
@router.delete("/accounts/{account_id}/permanent")
112+
async def remove_account(account_id: int, authorization: str = Header(None)):
113+
from auth_middleware import get_current_user_from_token
114+
current_user = await get_current_user_from_token(authorization)
115+
if not current_user.get('is_admin', False):
116+
raise HTTPException(status_code=403, detail="Admin access required")
117+
118+
conn = await get_database_connection()
119+
try:
120+
account = await conn.fetchrow(
121+
"SELECT id, status, email FROM letsencrypt_accounts WHERE id = $1", account_id
122+
)
123+
if not account:
124+
raise HTTPException(status_code=404, detail="Account not found")
125+
if account['status'] != 'deactivated':
126+
raise HTTPException(
127+
status_code=409,
128+
detail="Only deactivated accounts can be permanently removed. Deactivate first."
129+
)
130+
131+
linked_orders = await conn.fetchval(
132+
"SELECT COUNT(*) FROM letsencrypt_orders WHERE account_id = $1", account_id
133+
)
134+
if linked_orders > 0:
135+
await conn.execute(
136+
"DELETE FROM letsencrypt_orders WHERE account_id = $1", account_id
137+
)
138+
139+
await conn.execute("DELETE FROM letsencrypt_accounts WHERE id = $1", account_id)
140+
logger.info(f"ACME account {account['email']} (id={account_id}) permanently removed by user {current_user.get('username')}")
141+
return {"message": f"Account {account['email']} permanently removed", "deleted_orders": linked_orders}
142+
finally:
143+
await close_database_connection(conn)
144+
145+
111146
# --- Certificate operations ---
112147

113148
@router.post("/certificates")

frontend/src/components/ACMEAutomation.js

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ const ACMEAutomation = () => {
6161
const nextRenewal = renewalSchedule.find(c => c.auto_renew && calcDaysLeft(c.expiry_date) > 0);
6262
const nextRenewalDays = nextRenewal ? calcDaysLeft(nextRenewal.expiry_date) : null;
6363
const pendingOrders = orders.filter(o => o.status === 'pending' || o.status === 'processing');
64-
const acmeAccount = accounts.length > 0 ? accounts[accounts.length - 1] : null;
64+
const activeAccount = accounts.find(a => a.status === 'valid') || null;
65+
const acmeAccount = activeAccount || (accounts.length > 0 ? accounts[accounts.length - 1] : null);
6566

6667
const handleRequestCert = async () => {
6768
try {
@@ -187,6 +188,32 @@ const ACMEAutomation = () => {
187188
});
188189
};
189190

191+
const handleRemoveAccount = (accountId, email) => {
192+
Modal.confirm({
193+
title: 'Permanently Remove Account',
194+
content: (
195+
<div>
196+
<p>Are you sure you want to permanently remove <strong>{email}</strong> from the database?</p>
197+
<p style={{ color: '#ff4d4f' }}>
198+
This will delete the account record and all associated orders from the local database.
199+
This action cannot be undone.
200+
</p>
201+
</div>
202+
),
203+
okText: 'Remove Permanently',
204+
okButtonProps: { danger: true },
205+
onOk: async () => {
206+
try {
207+
await axios.delete(`/api/letsencrypt/accounts/${accountId}/permanent`);
208+
message.success('Account permanently removed');
209+
fetchData();
210+
} catch (err) {
211+
message.error(err?.response?.data?.detail || 'Failed to remove account');
212+
}
213+
},
214+
});
215+
};
216+
190217
const statusTag = (status) => {
191218
const map = {
192219
pending: { color: 'processing', icon: <ClockCircleOutlined /> },
@@ -394,9 +421,9 @@ const ACMEAutomation = () => {
394421
<Card>
395422
<Statistic
396423
title="ACME Account"
397-
value={acmeAccount ? 'Active' : 'None'}
398-
prefix={acmeAccount ? <CheckCircleOutlined /> : <ExclamationCircleOutlined />}
399-
valueStyle={{ color: acmeAccount ? '#52c41a' : '#faad14' }}
424+
value={activeAccount ? 'Active' : acmeAccount ? 'Inactive' : 'None'}
425+
prefix={activeAccount ? <CheckCircleOutlined /> : <ExclamationCircleOutlined />}
426+
valueStyle={{ color: activeAccount ? '#52c41a' : '#faad14' }}
400427
/>
401428
{acmeAccount && (
402429
<div style={{ fontSize: 12, color: '#888', marginTop: 4 }}>{acmeAccount.email}</div>
@@ -612,7 +639,7 @@ const ACMEAutomation = () => {
612639
}}
613640
/>
614641
</Tooltip>
615-
{record.status !== 'deactivated' && (
642+
{record.status !== 'deactivated' ? (
616643
<Tooltip title="Deactivate">
617644
<Button
618645
icon={<DeleteOutlined />}
@@ -621,6 +648,16 @@ const ACMEAutomation = () => {
621648
onClick={() => handleDeactivateAccount(record.id, record.email)}
622649
/>
623650
</Tooltip>
651+
) : (
652+
<Tooltip title="Remove from database">
653+
<Button
654+
icon={<DeleteOutlined />}
655+
size="small"
656+
danger
657+
type="text"
658+
onClick={() => handleRemoveAccount(record.id, record.email)}
659+
/>
660+
</Tooltip>
624661
)}
625662
</Space>
626663
),

frontend/src/components/ApplyManagement.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,7 +1248,7 @@ const ApplyManagement = () => {
12481248
<ClockCircleOutlined style={{ marginRight: 8, color: '#faad14' }} />
12491249
Pending Changes ({pendingVersions.length})
12501250
</Title>
1251-
<div style={{ maxHeight: 240, overflowY: 'auto', paddingRight: 8 }}>
1251+
<div style={{ maxHeight: 240, overflowY: 'auto', paddingRight: 8, paddingTop: 4 }}>
12521252
<Timeline size="small">
12531253
{pendingVersions.map((version, index) => (
12541254
<Timeline.Item
@@ -1300,7 +1300,7 @@ const ApplyManagement = () => {
13001300
style={{ margin: '20px 0' }}
13011301
/>
13021302
) : (
1303-
<div style={{ maxHeight: 420, overflowY: 'auto', paddingRight: 8 }}>
1303+
<div style={{ maxHeight: 420, overflowY: 'auto', paddingRight: 8, paddingTop: 4 }}>
13041304
<Timeline size="small">
13051305
{appliedVersions.map((version, index) => (
13061306
<Timeline.Item
@@ -1347,7 +1347,7 @@ const ApplyManagement = () => {
13471347
style={{ margin: '20px 0' }}
13481348
/>
13491349
) : (
1350-
<div style={{ maxHeight: 420, overflowY: 'auto', paddingRight: 8 }}>
1350+
<div style={{ maxHeight: 420, overflowY: 'auto', paddingRight: 8, paddingTop: 4 }}>
13511351
<Timeline size="small">
13521352
{rejectedVersions.map((version, index) => (
13531353
<Timeline.Item

0 commit comments

Comments
 (0)