From f828877ff1090cc9001ede9e5be3cfdfa26cec7f Mon Sep 17 00:00:00 2001 From: JoachimLK Date: Fri, 24 Apr 2026 10:46:08 +0200 Subject: [PATCH 1/7] feat: add organization localization settings and candidate demographics - Implemented organization settings for name display format and date format. - Created API endpoints for fetching and updating organization settings. - Added new fields to the candidate schema: display name, gender, and date of birth. - Updated job schema to include a salary negotiable flag. - Created a new org_settings table in the database with appropriate constraints. - Added validation schemas for organization settings. - Developed a Vue component for managing localization settings in the dashboard. - Enhanced email sending functionality to support SMTP and Resend providers. Co-authored-by: Copilot --- .env.example | 18 ++ SELF-HOSTING.md | 37 ++- app/components/AppTopBar.vue | 32 +- app/components/SettingsMobileNav.vue | 8 +- app/components/SettingsSidebar.vue | 9 +- app/composables/useCandidate.ts | 3 + app/composables/useCandidates.ts | 3 + app/composables/useJob.ts | 12 +- app/composables/useOrgSettings.ts | 66 ++++ app/pages/dashboard/candidates/[id].vue | 107 ++++++- app/pages/dashboard/candidates/new.vue | 232 ++++++++++++++ app/pages/dashboard/jobs/[id]/settings.vue | 194 +++++++----- app/pages/dashboard/jobs/new.vue | 43 ++- app/pages/dashboard/settings/localization.vue | 252 +++++++++++++++ app/pages/jobs/[slug]/index.vue | 4 +- package-lock.json | 21 ++ package.json | 2 + server/api/candidates/[id].get.ts | 12 + server/api/candidates/[id].patch.ts | 3 + server/api/candidates/index.get.ts | 7 + server/api/candidates/index.post.ts | 6 + .../interviews/[id]/send-invitation.post.ts | 7 +- server/api/jobs/[id].get.ts | 1 + server/api/jobs/[id].patch.ts | 1 + server/api/jobs/index.post.ts | 2 + server/api/org-settings/index.get.ts | 21 ++ server/api/org-settings/index.patch.ts | 39 +++ server/api/public/jobs/[slug].get.ts | 1 + ...21_candidate_demographics_org_settings.sql | 38 +++ .../migrations/0022_salary_negotiable.sql | 7 + server/database/schema/app.ts | 37 +++ server/utils/email.ts | 291 ++++++++++-------- server/utils/env.ts | 37 ++- server/utils/schemas/candidate.ts | 21 ++ server/utils/schemas/job.ts | 33 +- server/utils/schemas/orgSettings.ts | 10 + 36 files changed, 1363 insertions(+), 254 deletions(-) create mode 100644 app/composables/useOrgSettings.ts create mode 100644 app/pages/dashboard/settings/localization.vue create mode 100644 server/api/org-settings/index.get.ts create mode 100644 server/api/org-settings/index.patch.ts create mode 100644 server/database/migrations/0021_candidate_demographics_org_settings.sql create mode 100644 server/database/migrations/0022_salary_negotiable.sql create mode 100644 server/utils/schemas/orgSettings.ts diff --git a/.env.example b/.env.example index 2f3a864..a110ad9 100644 --- a/.env.example +++ b/.env.example @@ -97,6 +97,24 @@ NUXT_PUBLIC_SITE_URL=http://localhost:3000 # Display name for the SSO button (default: "SSO") # OIDC_PROVIDER_NAME=Company SSO +# ─── Optional: Transactional Email ─────────────────────────────────────────── +# Reqcore logs emails to the console by default (safe for local dev). +# For production, configure one of the providers below. +# SMTP takes priority over Resend when SMTP_HOST is set. + +# Option A: SMTP (recommended for self-hosted / enterprise setups) +# Supports any SMTP server: Postfix, Gmail, Exchange, Mailcow, Mailu, etc. +# SMTP_HOST=smtp.example.com +# SMTP_PORT=587 +# SMTP_USER=reqcore@example.com +# SMTP_PASS=your-smtp-password +# SMTP_FROM="Reqcore " +# SMTP_SECURE=false # true = implicit TLS (port 465), false = STARTTLS (port 587) + +# Option B: Resend (free tier: 3,000 emails/month — resend.com) +# RESEND_API_KEY=re_xxxxxxxxxxxx +# RESEND_FROM_EMAIL="Reqcore " + # ─── Optional: Social Sign-In (Google, GitHub, Microsoft) ──────────────────── # Enable social login buttons on the sign-in and sign-up pages. # Each provider requires both CLIENT_ID and CLIENT_SECRET to be set. diff --git a/SELF-HOSTING.md b/SELF-HOSTING.md index 50813f3..cc487cc 100644 --- a/SELF-HOSTING.md +++ b/SELF-HOSTING.md @@ -440,7 +440,37 @@ docker compose up --build -d By default, Reqcore logs email content to the console (useful for development). For production, configure a transactional email service. -### Using Resend (Recommended) +**Priority:** When `SMTP_HOST` is set, SMTP is used. Otherwise, if `RESEND_API_KEY` is set, Resend is used. If neither is configured, emails are logged to the console. + +### Option A: SMTP (recommended for self-hosted setups) + +SMTP works with any mail server — Postfix, Gmail, Exchange, Mailcow, Mailu, etc. No external service dependency. + +1. Add to your `.env` file: + +```bash +SMTP_HOST=smtp.example.com +SMTP_PORT=587 +SMTP_USER=reqcore@example.com +SMTP_PASS=your-smtp-password +SMTP_FROM="Reqcore " +SMTP_SECURE=false # true for implicit TLS (port 465), false for STARTTLS (port 587) +``` + +2. Restart: `docker compose up --build -d` + +**Common setups:** + +| Provider | SMTP_HOST | SMTP_PORT | SMTP_SECURE | +|---|---|---|---| +| Gmail (App Password) | `smtp.gmail.com` | `587` | `false` | +| Outlook / Office 365 | `smtp.office365.com` | `587` | `false` | +| Mailcow / Mailu | your server hostname | `587` | `false` | +| Custom Postfix | your server hostname | `587` or `465` | `false` / `true` | + +> For Gmail, generate an [App Password](https://support.google.com/accounts/answer/185833) — your regular Gmail password will not work. + +### Option B: Resend 1. Sign up at [resend.com](https://resend.com) (free tier: 3,000 emails/month) 2. Verify your sending domain @@ -454,11 +484,6 @@ RESEND_FROM_EMAIL="Reqcore " 5. Restart: `docker compose up --build -d` -This enables: -- Team member invitation emails -- Interview scheduling notifications -- Candidate communication emails - --- ## Security Best Practices diff --git a/app/components/AppTopBar.vue b/app/components/AppTopBar.vue index b8dce83..e87f0f0 100644 --- a/app/components/AppTopBar.vue +++ b/app/components/AppTopBar.vue @@ -144,6 +144,22 @@ watch(() => route.path, () => { showGetStartedMenu.value = false }) +// ───────────────────────────────────────────── +// New Job button +// ───────────────────────────────────────────── + +const newJobResetSignal = useState('new-job-reset-signal', () => 0) + +function handleNewJobClick() { + const newJobPath = localePath('/dashboard/jobs/new') + if (route.path === newJobPath) { + // Already on the page: signal the wizard to reset instead of navigating + newJobResetSignal.value++ + } else { + navigateTo(newJobPath) + } +} + // Close user menu on outside click const userMenuRef = useTemplateRef('userMenuRoot') function onClickOutsideUser(e: MouseEvent) { @@ -264,13 +280,13 @@ onUnmounted(() => { - + - - -const isSubmitting = ref(false) -const errors = ref>({}) -const submitError = ref(null) - -const formSchema = z.object({ - firstName: z.string().min(1, 'First name is required').max(100), - lastName: z.string().min(1, 'Last name is required').max(100), - email: z.string().min(1, 'Email is required').email('Invalid email address').max(255), - phone: z.string().max(50).optional(), -}) - -function validate(): boolean { - const result = formSchema.safeParse(form.value) - if (!result.success) { - errors.value = {} - for (const issue of result.error.issues) { - const field = issue.path[0]?.toString() - if (field) errors.value[field] = issue.message - } - return false - } - errors.value = {} - return true -} - -async function handleSubmit() { - submitError.value = null - if (!validate()) return - - isSubmitting.value = true - try { - await createCandidate({ - firstName: form.value.firstName, - lastName: form.value.lastName, - email: form.value.email, - phone: form.value.phone || undefined, - }) - track('candidate_added') - await navigateTo(localePath('/dashboard/candidates')) - } catch (err: any) { - const message = err.data?.statusMessage ?? 'Something went wrong' - // Show email conflict as a field-level error - if (err.statusCode === 409 || err.data?.statusCode === 409) { - errors.value.email = message - } else { - submitError.value = message - } - } finally { - isSubmitting.value = false - } -} - - - From 6c238c2fae2341639bde2f961ba1bbd36708044f Mon Sep 17 00:00:00 2001 From: JoachimLK Date: Fri, 24 Apr 2026 11:47:55 +0200 Subject: [PATCH 3/7] feat: add salary input change handlers and update permissions for organization --- app/pages/dashboard/jobs/[id]/settings.vue | 14 ++++++- app/pages/dashboard/settings/ai.vue | 14 ++++++- package-lock.json | 49 ++++++++++++++-------- shared/permissions.ts | 4 ++ 4 files changed, 59 insertions(+), 22 deletions(-) diff --git a/app/pages/dashboard/jobs/[id]/settings.vue b/app/pages/dashboard/jobs/[id]/settings.vue index b7c9f39..f24d3af 100644 --- a/app/pages/dashboard/jobs/[id]/settings.vue +++ b/app/pages/dashboard/jobs/[id]/settings.vue @@ -216,6 +216,16 @@ const salaryUnitOptions = [ { value: 'MONTH', label: 'Per month' }, { value: 'HOUR', label: 'Per hour' }, ] + +function onSalaryMinChange(e: Event) { + const input = e.target as HTMLInputElement + if (!input.value) form.value.salaryMin = null +} + +function onSalaryMaxChange(e: Event) { + const input = e.target as HTMLInputElement + if (!input.value) form.value.salaryMax = null +}