diff --git a/backend/internal/dto/app_config_dto.go b/backend/internal/dto/app_config_dto.go index baef5847..dccb446a 100644 --- a/backend/internal/dto/app_config_dto.go +++ b/backend/internal/dto/app_config_dto.go @@ -46,4 +46,5 @@ type AppConfigUpdateDto struct { EmailOneTimeAccessAsAdminEnabled string `json:"emailOneTimeAccessAsAdminEnabled" binding:"required"` EmailOneTimeAccessAsUnauthenticatedEnabled string `json:"emailOneTimeAccessAsUnauthenticatedEnabled" binding:"required"` EmailLoginNotificationEnabled string `json:"emailLoginNotificationEnabled" binding:"required"` + EmailApiKeyExpirationEnabled string `json:"emailApiKeyExpirationEnabled" binding:"required"` } diff --git a/backend/internal/job/api_key_expiry_job.go b/backend/internal/job/api_key_expiry_job.go index 5a44f369..8887d6ed 100644 --- a/backend/internal/job/api_key_expiry_job.go +++ b/backend/internal/job/api_key_expiry_job.go @@ -30,6 +30,10 @@ func RegisterApiKeyExpiryJob(ctx context.Context, apiKeyService *service.ApiKeyS } func (j *ApiKeyEmailJobs) checkAndNotifyExpiringApiKeys(ctx context.Context) error { + // Skip if the feature is disabled + if !j.appConfigService.GetDbConfig().EmailApiKeyExpirationEnabled.IsTrue() { + return nil + } apiKeys, err := j.apiKeyService.ListExpiringApiKeys(ctx, 7) if err != nil { diff --git a/backend/internal/model/app_config.go b/backend/internal/model/app_config.go index 0e03f62f..4e15009e 100644 --- a/backend/internal/model/app_config.go +++ b/backend/internal/model/app_config.go @@ -51,6 +51,7 @@ type AppConfig struct { EmailLoginNotificationEnabled AppConfigVariable `key:"emailLoginNotificationEnabled"` EmailOneTimeAccessAsUnauthenticatedEnabled AppConfigVariable `key:"emailOneTimeAccessAsUnauthenticatedEnabled,public"` // Public EmailOneTimeAccessAsAdminEnabled AppConfigVariable `key:"emailOneTimeAccessAsAdminEnabled,public"` // Public + EmailApiKeyExpirationEnabled AppConfigVariable `key:"emailApiKeyExpirationEnabled"` // LDAP LdapEnabled AppConfigVariable `key:"ldapEnabled,public"` // Public LdapUrl AppConfigVariable `key:"ldapUrl"` diff --git a/backend/internal/service/app_config_service.go b/backend/internal/service/app_config_service.go index d6a511fe..700aceb0 100644 --- a/backend/internal/service/app_config_service.go +++ b/backend/internal/service/app_config_service.go @@ -75,6 +75,7 @@ func (s *AppConfigService) getDefaultDbConfig() *model.AppConfig { EmailLoginNotificationEnabled: model.AppConfigVariable{Value: "false"}, EmailOneTimeAccessAsUnauthenticatedEnabled: model.AppConfigVariable{Value: "false"}, EmailOneTimeAccessAsAdminEnabled: model.AppConfigVariable{Value: "false"}, + EmailApiKeyExpirationEnabled: model.AppConfigVariable{Value: "false"}, // LDAP LdapEnabled: model.AppConfigVariable{Value: "false"}, LdapUrl: model.AppConfigVariable{}, diff --git a/frontend/messages/en-US.json b/frontend/messages/en-US.json index fd0f7a54..5f07209f 100644 --- a/frontend/messages/en-US.json +++ b/frontend/messages/en-US.json @@ -340,5 +340,7 @@ "login_code_email_success": "The login code has been sent to the user.", "send_email": "Send Email", "show_code": "Show Code", - "callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security." + "callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.", + "api_key_expiration": "API Key Expiration", + "send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire." } diff --git a/frontend/src/lib/types/application-configuration.ts b/frontend/src/lib/types/application-configuration.ts index b9a01a9b..7855ac8c 100644 --- a/frontend/src/lib/types/application-configuration.ts +++ b/frontend/src/lib/types/application-configuration.ts @@ -20,6 +20,7 @@ export type AllAppConfig = AppConfig & { smtpTls: 'none' | 'starttls' | 'tls'; smtpSkipCertVerify: boolean; emailLoginNotificationEnabled: boolean; + emailApiKeyExpirationEnabled: boolean; // LDAP ldapUrl: string; ldapBindDn: string; diff --git a/frontend/src/routes/settings/admin/application-configuration/forms/app-config-email-form.svelte b/frontend/src/routes/settings/admin/application-configuration/forms/app-config-email-form.svelte index 10c2ae4b..bb149f5d 100644 --- a/frontend/src/routes/settings/admin/application-configuration/forms/app-config-email-form.svelte +++ b/frontend/src/routes/settings/admin/application-configuration/forms/app-config-email-form.svelte @@ -41,7 +41,8 @@ smtpSkipCertVerify: z.boolean(), emailOneTimeAccessAsUnauthenticatedEnabled: z.boolean(), emailOneTimeAccessAsAdminEnabled: z.boolean(), - emailLoginNotificationEnabled: z.boolean() + emailLoginNotificationEnabled: z.boolean(), + emailApiKeyExpirationEnabled: z.boolean() }); const { inputs, ...form } = createForm(formSchema, appConfig); @@ -134,18 +135,25 @@ description={m.send_an_email_to_the_user_when_they_log_in_from_a_new_device()} bind:checked={$inputs.emailLoginNotificationEnabled.value} /> - + + +
diff --git a/frontend/tests/application-configuration.spec.ts b/frontend/tests/application-configuration.spec.ts index ffc93b54..c4a93bab 100644 --- a/frontend/tests/application-configuration.spec.ts +++ b/frontend/tests/application-configuration.spec.ts @@ -34,6 +34,7 @@ test('Update email configuration', async ({ page }) => { await page.getByLabel('Email Login Notification').click(); await page.getByLabel('Email Login Code Requested by User').click(); await page.getByLabel('Email Login Code from Admin').click(); + await page.getByLabel('API Key Expiration').click(); await page.getByRole('button', { name: 'Save' }).nth(1).click(); @@ -49,6 +50,7 @@ test('Update email configuration', async ({ page }) => { await expect(page.getByLabel('Email Login Notification')).toBeChecked(); await expect(page.getByLabel('Email Login Code Requested by User')).toBeChecked(); await expect(page.getByLabel('Email Login Code from Admin')).toBeChecked(); + await expect(page.getByLabel('API Key Expiration')).toBeChecked(); }); test('Update LDAP configuration', async ({ page }) => {