feat(acme): add DB-backed ACME configuration management and OpsServer certificate settings UI

This commit is contained in:
2026-04-08 13:12:20 +00:00
parent 4fbe01823b
commit c224028495
18 changed files with 793 additions and 33 deletions

View File

@@ -197,6 +197,28 @@ export const certificateStatePart = await appState.getStatePart<ICertificateStat
'soft'
);
// ============================================================================
// ACME Config State (DB-backed singleton, managed via Domains > Certificates)
// ============================================================================
export interface IAcmeConfigState {
config: interfaces.data.IAcmeConfig | null;
isLoading: boolean;
error: string | null;
lastUpdated: number;
}
export const acmeConfigStatePart = await appState.getStatePart<IAcmeConfigState>(
'acmeConfig',
{
config: null,
isLoading: false,
error: null,
lastUpdated: 0,
},
'soft',
);
// ============================================================================
// Remote Ingress State
// ============================================================================
@@ -1953,6 +1975,72 @@ export const deleteDnsRecordAction = domainsStatePart.createAction<{ id: string;
},
);
// ============================================================================
// ACME Config Actions
// ============================================================================
export const fetchAcmeConfigAction = acmeConfigStatePart.createAction(
async (statePartArg): Promise<IAcmeConfigState> => {
const context = getActionContext();
const currentState = statePartArg.getState()!;
if (!context.identity) return currentState;
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetAcmeConfig
>('/typedrequest', 'getAcmeConfig');
const response = await request.fire({ identity: context.identity });
return {
config: response.config,
isLoading: false,
error: null,
lastUpdated: Date.now(),
};
} catch (error: unknown) {
return {
...currentState,
isLoading: false,
error: error instanceof Error ? error.message : 'Failed to fetch ACME config',
};
}
},
);
export const updateAcmeConfigAction = acmeConfigStatePart.createAction<{
accountEmail?: string;
enabled?: boolean;
useProduction?: boolean;
autoRenew?: boolean;
renewThresholdDays?: number;
}>(async (statePartArg, dataArg, actionContext): Promise<IAcmeConfigState> => {
const context = getActionContext();
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_UpdateAcmeConfig
>('/typedrequest', 'updateAcmeConfig');
const response = await request.fire({
identity: context.identity!,
accountEmail: dataArg.accountEmail,
enabled: dataArg.enabled,
useProduction: dataArg.useProduction,
autoRenew: dataArg.autoRenew,
renewThresholdDays: dataArg.renewThresholdDays,
});
if (!response.success) {
return {
...statePartArg.getState()!,
error: response.message || 'Failed to update ACME config',
};
}
return await actionContext!.dispatch(fetchAcmeConfigAction, null);
} catch (error: unknown) {
return {
...statePartArg.getState()!,
error: error instanceof Error ? error.message : 'Failed to update ACME config',
};
}
});
// ============================================================================
// Route Management Actions
// ============================================================================