diff --git a/cmd/csat.go b/cmd/csat.go index 0171a0cf..d2c76992 100644 --- a/cmd/csat.go +++ b/cmd/csat.go @@ -86,7 +86,7 @@ func handleUpdateCSATResponse(r *fastglue.Request) error { }) } - if err := app.csat.UpdateResponse(uuid, rating, feedback, metaJSON); err != nil { + if err := app.conversation.UpdateCSATResponse(uuid, rating, feedback, metaJSON); err != nil { // Surface only known, user-safe messages; never leak raw driver errors // (e.g. a malformed UUID triggers a Postgres syntax error string). msg := app.i18n.T("globals.messages.somethingWentWrong") @@ -161,8 +161,7 @@ func handleSubmitCSATResponse(r *fastglue.Request) error { req.Feedback = truncateFeedback(req.Feedback) - // Update CSAT response - if err := app.csat.UpdateResponse(uuid, req.Rating, req.Feedback, nil); err != nil { + if err := app.conversation.UpdateCSATResponse(uuid, req.Rating, req.Feedback, nil /** meta **/); err != nil { return sendErrorEnvelope(r, err) } diff --git a/cmd/report.go b/cmd/report.go index f0dec2ef..91fda470 100644 --- a/cmd/report.go +++ b/cmd/report.go @@ -24,7 +24,7 @@ func handleOverviewCharts(r *fastglue.Request) error { app = r.Context.(*App) days, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("days"))) ) - charts, err := app.report.GetOverviewChart(days) + charts, err := app.report.GetOverviewChart(days, app.setting.GetAppTimezone()) if err != nil { return sendErrorEnvelope(r, err) } @@ -37,7 +37,7 @@ func handleOverviewSLA(r *fastglue.Request) error { app = r.Context.(*App) days, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("days"))) ) - sla, err := app.report.GetOverviewSLA(days) + sla, err := app.report.GetOverviewSLA(days, app.setting.GetAppTimezone()) if err != nil { return sendErrorEnvelope(r, err) } @@ -50,7 +50,7 @@ func handleOverviewCSAT(r *fastglue.Request) error { app = r.Context.(*App) days, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("days"))) ) - csat, err := app.report.GetOverviewCSAT(days) + csat, err := app.report.GetOverviewCSAT(days, app.setting.GetAppTimezone()) if err != nil { return sendErrorEnvelope(r, err) } @@ -63,7 +63,7 @@ func handleOverviewMessageVolume(r *fastglue.Request) error { app = r.Context.(*App) days, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("days"))) ) - volume, err := app.report.GetOverviewMessageVolume(days) + volume, err := app.report.GetOverviewMessageVolume(days, app.setting.GetAppTimezone()) if err != nil { return sendErrorEnvelope(r, err) } @@ -76,7 +76,7 @@ func handleOverviewTagDistribution(r *fastglue.Request) error { app = r.Context.(*App) days, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("days"))) ) - tags, err := app.report.GetOverviewTagDistribution(days) + tags, err := app.report.GetOverviewTagDistribution(days, app.setting.GetAppTimezone()) if err != nil { return sendErrorEnvelope(r, err) } diff --git a/frontend/apps/main/src/features/admin/inbox/formSchema.js b/frontend/apps/main/src/features/admin/inbox/formSchema.js index 99f5fe1e..7e49c666 100644 --- a/frontend/apps/main/src/features/admin/inbox/formSchema.js +++ b/frontend/apps/main/src/features/admin/inbox/formSchema.js @@ -56,8 +56,8 @@ export const createFormSchema = (t) => z.object({ smtp: z.object({ host: z.string().min(1, t('globals.messages.required')), port: z.number().min(1).max(65535), - username: z.string().min(1, t('globals.messages.required')), - password: z.string().min(1, t('globals.messages.required')), + username: z.string().default(''), + password: z.string().default(''), max_conns: z.number().min(1), max_msg_retries: z.number().min(0).max(100), idle_timeout: z.string().min(1, t('globals.messages.required')).refine(isGoDuration, { @@ -70,6 +70,13 @@ export const createFormSchema = (t) => z.object({ tls_skip_verify: z.boolean().optional(), hello_hostname: z.string().optional(), auth_protocol: z.enum(['login', 'cram', 'plain', 'none']) + }).superRefine((smtp, ctx) => { + if (smtp.auth_protocol === 'none') return + for (const field of ['username', 'password']) { + if (!smtp[field]) { + ctx.addIssue({ code: 'custom', path: [field], message: t('globals.messages.required') }) + } + } }) }).superRefine((values, ctx) => { const primary = extractEmailAddress(values.from).toLowerCase() diff --git a/frontend/apps/main/src/features/admin/inbox/formSchema.test.js b/frontend/apps/main/src/features/admin/inbox/formSchema.test.js index fa540b9e..d6c8bda7 100644 --- a/frontend/apps/main/src/features/admin/inbox/formSchema.test.js +++ b/frontend/apps/main/src/features/admin/inbox/formSchema.test.js @@ -248,6 +248,28 @@ describe('Email Inbox Form Schema', () => { } }) + test('unauthenticated SMTP accepts empty and omitted credentials', () => { + for (const credential of ['', undefined]) { + expect(() => schema.parse(withSmtp({ + auth_protocol: 'none', username: credential, password: credential + }))).not.toThrow() + } + }) + + test('authenticated SMTP requires both credentials', () => { + for (const auth_protocol of ['login', 'cram', 'plain']) { + for (const field of ['username', 'password']) { + for (const credential of ['', undefined]) { + const result = schema.safeParse(withSmtp({ auth_protocol, [field]: credential })) + expect(result.success).toBe(false) + expect(result.error.issues).toEqual(expect.arrayContaining([ + expect.objectContaining({ path: ['smtp', field] }) + ])) + } + } + } + }) + test('smtp hello_hostname optional', () => { expect(() => schema.parse(withSmtp({ hello_hostname: 'desk.example.com' }))).not.toThrow() }) diff --git a/frontend/apps/main/src/features/admin/notification/formSchema.js b/frontend/apps/main/src/features/admin/notification/formSchema.js index bcdc3829..55291a1f 100644 --- a/frontend/apps/main/src/features/admin/notification/formSchema.js +++ b/frontend/apps/main/src/features/admin/notification/formSchema.js @@ -3,9 +3,7 @@ import { isGoDuration } from '@shared-ui/utils/string'; export const createFormSchema = (t) => z.object({ enabled: z.boolean().default(false), - username: z.string().nonempty({ - message: t('globals.messages.required') - }), + username: z.string().default(''), host: z.string().nonempty({ message: t('globals.messages.required') }), @@ -17,9 +15,7 @@ export const createFormSchema = (t) => z.object({ .min(1, { message: t('validation.minmaxNumber', { min: 1, max: 65535 }) }) .max(65535, { message: t('validation.minmaxNumber', { min: 1, max: 65535 }) }) .default(587), - password: z.string().nonempty({ - message: t('globals.messages.required') - }), + password: z.string().default(''), max_conns: z .number({ invalid_type_error: t('globals.messages.mustBeNumber'), @@ -54,4 +50,11 @@ export const createFormSchema = (t) => z.object({ hello_hostname: z.string().optional(), tls_type: z.enum(['none', 'starttls', 'tls']), tls_skip_verify: z.boolean().optional(), +}).superRefine((smtp, ctx) => { + if (smtp.auth_protocol === 'none') return + for (const field of ['username', 'password']) { + if (!smtp[field]) { + ctx.addIssue({ code: 'custom', path: [field], message: t('globals.messages.required') }) + } + } }); diff --git a/frontend/apps/main/src/features/admin/notification/formSchema.test.js b/frontend/apps/main/src/features/admin/notification/formSchema.test.js index 105ee55c..2ef0fe01 100644 --- a/frontend/apps/main/src/features/admin/notification/formSchema.test.js +++ b/frontend/apps/main/src/features/admin/notification/formSchema.test.js @@ -161,6 +161,28 @@ describe('Notification Form Schema', () => { } }) + test('unauthenticated SMTP accepts empty and omitted credentials', () => { + for (const credential of ['', undefined]) { + expect(() => schema.parse({ + ...validForm, auth_protocol: 'none', username: credential, password: credential + })).not.toThrow() + } + }) + + test('authenticated SMTP requires both credentials', () => { + for (const auth_protocol of ['login', 'cram', 'plain']) { + for (const field of ['username', 'password']) { + for (const credential of ['', undefined]) { + const result = schema.safeParse({ ...validForm, auth_protocol, [field]: credential }) + expect(result.success).toBe(false) + expect(result.error.issues).toEqual(expect.arrayContaining([ + expect.objectContaining({ path: [field] }) + ])) + } + } + } + }) + test('tls_type missing', () => { const { tls_type, ...form } = validForm expect(() => schema.parse(form)).toThrow() diff --git a/frontend/apps/main/src/features/admin/templates/TemplateForm.vue b/frontend/apps/main/src/features/admin/templates/TemplateForm.vue index dce87c04..b737419b 100644 --- a/frontend/apps/main/src/features/admin/templates/TemplateForm.vue +++ b/frontend/apps/main/src/features/admin/templates/TemplateForm.vue @@ -55,7 +55,7 @@