diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index 22ff751..14da9f6 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -446,7 +446,7 @@ Gross margin: 14 SEK | Is a license plate personal data? | Yes (it directly identifies a vehicle owner). | | Is an address personal data? | Yes. | | What if we only process address transiently? | Data minimization is a GDPR principle (Art. 5(1)(c)). Transient processing with immediate deletion is a strong compliance posture. | -| Do we need to inform the recipient? | Yes, GDPR Art. 14 requires informing the data subject. The letter itself can serve this purpose — include a footer like: _"Detta brev skickades via BilHej.se. Din adress hämtades från Transportstyrelsens fordonsregister och har raderats efter utskick. För frågor: hej@bilhalsning.se"_ | +| Do we need to inform the recipient? | Yes, GDPR Art. 14 requires informing the data subject. The letter itself can serve this purpose — include a footer like: _"Detta brev skickades via BilHej.se. Din adress hämtades från Transportstyrelsens fordonsregister och har raderats efter utskick. För frågor: kontakt@bilhej.se"_ | ### 11.2 Transportstyrelsen Access diff --git a/docs/production-email-checklist.md b/docs/production-email-checklist.md index 36dddb3..fa4580f 100644 --- a/docs/production-email-checklist.md +++ b/docs/production-email-checklist.md @@ -54,3 +54,30 @@ Fallback: reset links still log when `MAIL_HOST` is empty. Keep using Mailpit (`docker compose up`, http://localhost:8025). Do not point local Docker at Resend unless you intend to send real mail. + +## 5. Inbound email on bilhej.se + +Inbound mail uses **Resend Receiving** on the root domain `bilhej.se`. No mailbox is created in +Strato; the MX record routes all `@bilhej.se` addresses to Resend. You do not create each address +separately in Resend. + +**Setup (done once):** + +1. Resend → **Domains** → `bilhej.se` → enable **Receiving** +2. Strato → **DNS** → add the receiving MX record (e.g. `inbound-smtp.eu-west-1.amazonaws.com`) +3. Wait until Resend shows receiving as **Verified** +4. Send test mail to `support@bilhej.se` and `kontakt@bilhej.se`; confirm both appear under **Emails → Receiving** + +**Reading mail:** open the [Resend Receiving inbox](https://resend.com/emails/receiving). There is +no automatic forward to Gmail unless you add a webhook handler later. + +| Address | Purpose | Where mail goes | +|---------|---------|-----------------| +| `support@bilhej.se` | Orders, Swish, status, technical issues | Resend dashboard | +| `kontakt@bilhej.se` | General contact, printed letter footer | Resend dashboard | +| `klagomal@bilhej.se` | Complaints (shown on `/kontakt`) | Resend dashboard | +| `noreply@bilhej.se` | Outbound only (password reset) | Not an inbox | + +**Optional later (same Resend inbox, no extra DNS):** `abuse@bilhej.se` if you want a published +address for misuse reports; `privacy@bilhej.se` if integritetspolicy asks for a dedicated +data-protection contact. diff --git a/frontend/src/__tests__/ContactPage.spec.ts b/frontend/src/__tests__/ContactPage.spec.ts index 86f39f2..30e922b 100644 --- a/frontend/src/__tests__/ContactPage.spec.ts +++ b/frontend/src/__tests__/ContactPage.spec.ts @@ -9,17 +9,26 @@ describe('ContactPage', () => { expect(wrapper.text()).toContain('klagomål') }) - it('renders general support email', () => { + it('renders support email', () => { const wrapper = mount(ContactPage) - const link = wrapper.find('a[href="mailto:kontakt@bilhej.se"]') - expect(link.exists()).toBe(true) - expect(link.text()).toBe('kontakt@bilhej.se') + expect(wrapper.text()).toContain('support@bilhej.se') + }) + + it('renders general contact email', () => { + const wrapper = mount(ContactPage) + expect(wrapper.text()).toContain('kontakt@bilhej.se') }) it('renders complaints email', () => { const wrapper = mount(ContactPage) - const link = wrapper.find('a[href="mailto:jcamorling@gmail.com"]') + expect(wrapper.text()).toContain('klagomal@bilhej.se') + }) + + it('links support to mailto', () => { + const wrapper = mount(ContactPage) + const link = wrapper.find('a[href="mailto:support@bilhej.se"]') expect(link.exists()).toBe(true) - expect(wrapper.text()).toContain('jcamorling@gmail.com') + expect(link.text()).toBe('support@bilhej.se') + expect(link.attributes('aria-label')).toBe('Skicka till support: support@bilhej.se') }) }) diff --git a/frontend/src/__tests__/PrivacyPolicyPage.spec.ts b/frontend/src/__tests__/PrivacyPolicyPage.spec.ts new file mode 100644 index 0000000..f7f0d2c --- /dev/null +++ b/frontend/src/__tests__/PrivacyPolicyPage.spec.ts @@ -0,0 +1,54 @@ +import { describe, it, expect } from 'vitest' +import { mount } from '@vue/test-utils' +import { createRouter, createMemoryHistory } from 'vue-router' +import PrivacyPolicyPage from '@/pages/PrivacyPolicyPage.vue' + +function createTestRouter() { + return createRouter({ + history: createMemoryHistory(), + routes: [ + { + path: '/integritetspolicy', + name: 'privacy', + component: PrivacyPolicyPage, + }, + { + path: '/kontakt', + name: 'contact', + component: { template: '
Kontakt
' }, + }, + ], + }) +} + +describe('PrivacyPolicyPage', () => { + it('renders title and lead', () => { + const router = createTestRouter() + const wrapper = mount(PrivacyPolicyPage, { + global: { plugins: [router] }, + }) + expect(wrapper.text()).toContain('Integritetspolicy') + expect(wrapper.text()).toContain('personuppgifter') + }) + + it('describes sender and recipient data handling', () => { + const router = createTestRouter() + const wrapper = mount(PrivacyPolicyPage, { + global: { plugins: [router] }, + }) + expect(wrapper.text()).toContain('Mottagarens postadress') + expect(wrapper.text()).toContain('sparas inte efter utskick') + expect(wrapper.text()).toContain('varken vi eller obehöriga') + }) + + it('links to contact email and contact page', () => { + const router = createTestRouter() + const wrapper = mount(PrivacyPolicyPage, { + global: { plugins: [router] }, + }) + expect(wrapper.find('a[href="mailto:kontakt@bilhej.se"]').exists()).toBe( + true, + ) + expect(wrapper.find('a.policy__link').attributes('href')).toBe('/kontakt') + }) +}) diff --git a/frontend/src/__tests__/Router.spec.ts b/frontend/src/__tests__/Router.spec.ts index 257431a..3a5f1de 100644 --- a/frontend/src/__tests__/Router.spec.ts +++ b/frontend/src/__tests__/Router.spec.ts @@ -32,6 +32,18 @@ describe('Router', () => { expect(router.currentRoute.value.name).toBe('forgot-password') }) + it('resolves /integritetspolicy to PrivacyPolicyPage', async () => { + await router.push('/integritetspolicy') + await router.isReady() + expect(router.currentRoute.value.name).toBe('privacy') + }) + + it('resolves /villkor to TermsOfServicePage', async () => { + await router.push('/villkor') + await router.isReady() + expect(router.currentRoute.value.name).toBe('terms') + }) + it('resolves /aterstall-losenord to ResetPasswordPage', async () => { await router.push('/aterstall-losenord?token=abc') await router.isReady() diff --git a/frontend/src/__tests__/TermsOfServicePage.spec.ts b/frontend/src/__tests__/TermsOfServicePage.spec.ts new file mode 100644 index 0000000..c21e0b2 --- /dev/null +++ b/frontend/src/__tests__/TermsOfServicePage.spec.ts @@ -0,0 +1,58 @@ +import { describe, it, expect } from 'vitest' +import { mount } from '@vue/test-utils' +import { createRouter, createMemoryHistory } from 'vue-router' +import TermsOfServicePage from '@/pages/TermsOfServicePage.vue' + +function createTestRouter() { + return createRouter({ + history: createMemoryHistory(), + routes: [ + { + path: '/villkor', + name: 'terms', + component: TermsOfServicePage, + }, + { + path: '/integritetspolicy', + name: 'privacy', + component: { template: '
Integritet
' }, + }, + { + path: '/kontakt', + name: 'contact', + component: { template: '
Kontakt
' }, + }, + ], + }) +} + +describe('TermsOfServicePage', () => { + it('renders title and lead', () => { + const router = createTestRouter() + const wrapper = mount(TermsOfServicePage, { + global: { plugins: [router] }, + }) + expect(wrapper.text()).toContain('Användarvillkor') + expect(wrapper.text()).toContain('49 kr') + }) + + it('describes payment and order rules', () => { + const router = createTestRouter() + const wrapper = mount(TermsOfServicePage, { + global: { plugins: [router] }, + }) + expect(wrapper.text()).toContain('Swish') + expect(wrapper.text()).toContain('Obetalda beställningar kan redigeras') + }) + + it('links to privacy policy and support email', () => { + const router = createTestRouter() + const wrapper = mount(TermsOfServicePage, { + global: { plugins: [router] }, + }) + expect(wrapper.find('a[href="/integritetspolicy"]').exists()).toBe(true) + expect(wrapper.find('a[href="mailto:support@bilhej.se"]').exists()).toBe( + true, + ) + }) +}) diff --git a/frontend/src/pages/AboutPage.vue b/frontend/src/pages/AboutPage.vue index 034999c..600d764 100644 --- a/frontend/src/pages/AboutPage.vue +++ b/frontend/src/pages/AboutPage.vue @@ -29,10 +29,6 @@ const highlights = [ Bilhej gör det enkelt att nå en bilägare med ett fysiskt brev. Du skriver meddelandet, vi sköter utskick och post.

- - -
-

Vad vi gör

Många situationer i trafiken eller på parkeringen är enklare att lösa @@ -126,12 +122,17 @@ const highlights = [ } .about__lead { - margin: 0; + margin: 0 0 var(--space-lg) 0; font-size: 1.0625rem; line-height: 1.75; color: var(--color-muted); } +.about__prose { + padding-top: var(--space-lg); + border-top: 1px solid var(--color-border); +} + .about__section { margin-bottom: var(--space-2xl); } diff --git a/frontend/src/pages/ContactPage.vue b/frontend/src/pages/ContactPage.vue index e3ba60e..462fed1 100644 --- a/frontend/src/pages/ContactPage.vue +++ b/frontend/src/pages/ContactPage.vue @@ -1,19 +1,27 @@ + + + + diff --git a/frontend/src/pages/TermsOfServicePage.vue b/frontend/src/pages/TermsOfServicePage.vue new file mode 100644 index 0000000..505eeb6 --- /dev/null +++ b/frontend/src/pages/TermsOfServicePage.vue @@ -0,0 +1,258 @@ + + + + + diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 86fa1d2..3ec2a98 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -3,6 +3,8 @@ import HomePage from '@/pages/HomePage.vue' import ComposePage from '@/pages/ComposePage.vue' import AboutPage from '@/pages/AboutPage.vue' import ContactPage from '@/pages/ContactPage.vue' +import PrivacyPolicyPage from '@/pages/PrivacyPolicyPage.vue' +import TermsOfServicePage from '@/pages/TermsOfServicePage.vue' import RegisterPage from '@/pages/RegisterPage.vue' import LoginPage from '@/pages/LoginPage.vue' import ForgotPasswordPage from '@/pages/ForgotPasswordPage.vue' @@ -97,6 +99,16 @@ const router = createRouter({ name: 'contact', component: ContactPage, }, + { + path: '/integritetspolicy', + name: 'privacy', + component: PrivacyPolicyPage, + }, + { + path: '/villkor', + name: 'terms', + component: TermsOfServicePage, + }, ], })