feat: Swiss-compliant launch — Impressum/AGB/Contact, support panel, DSG exports, cookie banner
All checks were successful
Deploy to Production / deploy (push) Successful in 57s
All checks were successful
Deploy to Production / deploy (push) Successful in 57s
Legal (Swiss minimum, no individual named): - Impressum page (UWG Art. 3 lit. s) — provider, contact via support panel, no email required, jurisdiction = Switzerland - AGB page — subscription terms, payment, cancellation, suspension on payment fail, 14-day money-back, AI-processing-per-tier disclosure, Swiss law + Swiss venue, modeled after typical Schweizer SaaS terms - Privacy: Stripe added as subprocessor with full data-flow disclosure Support panel replaces email contact entirely: - @bmm/db: support_status enum + support_tickets + support_messages tables, migration applied to prod DB - @bmm/api: support routes (user create/list/view/reply, admin list/view/reply /set-status), public /v1/contact for logged-out visitors with per-IP rate limit of 3 submissions/day to prevent spam-flood - Web: /settings/support (list + new), /settings/support/[id] (conversation), /admin/support, /admin/support/[id] - Public /contact form with email collection for guest tickets Data rights (DSG Art. 25 / GDPR Art. 15+20): - /v1/account/export returns user-scoped JSON of profile, org, servers, builds, audit, support tickets and messages — excludes hashes, encrypted secrets, other-user data - /settings/account: download button + deletion-via-ticket workflow Production-readiness gaps closed: - org.suspended now blocks /v1/servers POST and /v1/servers/preview (402); webhook flagged this state but enforcement was missing - Cookie banner: minimal, essential-cookies-only disclosure (Swiss DSG + GDPR compliant without dark-pattern consent UI), mounts on both layouts Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
69
apps/web/components/cookie-banner.tsx
Normal file
69
apps/web/components/cookie-banner.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const STORAGE_KEY = 'bmm-cookie-ack-v1';
|
||||
|
||||
/**
|
||||
* BMM uses only strictly-necessary cookies (session + OAuth CSRF state). Under
|
||||
* Swiss DSG and GDPR strictly-necessary cookies do not require opt-in consent,
|
||||
* only clear disclosure — this banner satisfies the disclosure obligation
|
||||
* without dark-pattern cookie walls or false-choice "Reject all" UIs.
|
||||
*/
|
||||
export function CookieBanner() {
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const ack = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (!ack) setShow(true);
|
||||
} catch {
|
||||
// localStorage blocked (private mode etc.) — show banner anyway, it's
|
||||
// dismissable via a single click and never persists if storage fails.
|
||||
setShow(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
function acknowledge() {
|
||||
try {
|
||||
window.localStorage.setItem(STORAGE_KEY, new Date().toISOString());
|
||||
} catch {
|
||||
/* ignore — re-shown on next visit */
|
||||
}
|
||||
setShow(false);
|
||||
}
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-label="Cookie notice"
|
||||
className="fixed inset-x-0 bottom-0 z-50 border-t border-[--color-border] backdrop-blur-md"
|
||||
style={{
|
||||
backgroundColor: 'color-mix(in oklab, var(--color-bg-elevated) 92%, transparent)',
|
||||
paddingBottom: 'max(env(safe-area-inset-bottom), 0.75rem)',
|
||||
}}
|
||||
>
|
||||
<div className="mx-auto flex max-w-6xl flex-col gap-3 px-5 pt-3 sm:flex-row sm:items-center sm:gap-4 sm:px-6">
|
||||
<p className="flex-1 text-[12.5px] leading-relaxed text-[--color-fg-muted]">
|
||||
We use strictly-necessary cookies for login (session token) and CSRF
|
||||
protection. No tracking, no analytics, no third-party cookies on this
|
||||
domain. Details:{' '}
|
||||
<Link href="/privacy" className="text-[--color-accent] hover:underline">
|
||||
privacy policy
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={acknowledge}
|
||||
className="inline-flex h-9 shrink-0 items-center justify-center rounded-md bg-[--color-accent] px-4 text-[13px] font-medium text-white transition-colors hover:bg-[#5557e8]"
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user