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>
70 lines
2.3 KiB
TypeScript
70 lines
2.3 KiB
TypeScript
'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>
|
|
);
|
|
}
|