feat: open-source release prep, audit fixes, and deploy hardening

Production deploy completed (DigitalOcean Droplet, self-hosted Supabase,
Caddy + Let's Encrypt, GitHub Actions auto-deploy on push to main, daily
pg_dumpall backups). This commit lands the work that came out of the
deep audit pass and the open-source release prep.

Audit fixes:
- Webhook signing secret is now displayed in the settings UI with
  show/hide and copy-to-clipboard; the broken external docs.hackerspace.sh
  link is replaced with a link to the new local docs.
- Settings updateSpaceSettings parameter type now includes
  mission_statement so the field persists.
- members page enforces member_directory_visibility for non-admin viewers
  (admin_only, board_visible, members_visible).
- Mobile responsiveness pass: Projects Kanban grid, Members + Payments
  tables (column hiding on small screens), Proposal tally + voting
  buttons (2-col on mobile).

Open-source release scaffolding:
- LICENSE (MIT).
- CONTRIBUTING.md with dev setup, conventions, PR flow.
- SECURITY.md with disclosure policy.
- CHANGELOG.md.
- docs/WEBHOOKS.md with Node and Python signature verification examples.
- README.md rewritten for an open-source audience.

Removed v0/Vercel references:
- @vercel/analytics dependency removed.
- app/layout.tsx no longer imports Analytics, drops generator: 'v0.app'.
- Canonical deployment docs (docs/DEPLOYMENT.md, docs/README.md,
  docs/ARCHITECTURE.md, DATABASE.md, CLAUDE.md, README.md) now describe
  the self-hosted Droplet target only.
- e2e console-error check no longer filters [v0] noise.

Landing page:
- GitHub icon link to the public repo in the nav and footer.
This commit is contained in:
Moheeb Zara
2026-05-14 16:28:16 -07:00
parent b8cd36abc3
commit 8ad1ca44b7
27 changed files with 662 additions and 720 deletions
+88 -35
View File
@@ -134,6 +134,8 @@ export default function SettingsClient({ space, isAdmin, integrations, currentRo
// Webhook
const [webhookSecret, setWebhookSecret] = useState(space.webhook_secret || '')
const [copiedWebhook, setCopiedWebhook] = useState(false)
const [copiedSecret, setCopiedSecret] = useState(false)
const [showWebhookSecret, setShowWebhookSecret] = useState(false)
async function handleSaveSpace(e: React.FormEvent) {
e.preventDefault()
@@ -191,7 +193,8 @@ export default function SettingsClient({ space, isAdmin, integrations, currentRo
const result = await rotateWebhookSecret()
if (result.secret) {
setWebhookSecret(result.secret)
setMessage({ type: 'success', text: 'Webhook secret rotated!' })
setShowWebhookSecret(true)
setMessage({ type: 'success', text: 'New signing secret generated. Copy it now and store it securely.' })
}
setSaving(false)
}
@@ -202,6 +205,13 @@ export default function SettingsClient({ space, isAdmin, integrations, currentRo
setTimeout(() => setCopiedWebhook(false), 2000)
}
function copySecretToClipboard(text: string) {
if (!text) return
navigator.clipboard.writeText(text)
setCopiedSecret(true)
setTimeout(() => setCopiedSecret(false), 2000)
}
function openIntegrationModal(config: typeof INTEGRATIONS_CONFIG[0]) {
const existing = integrations.find(i => i.platform === config.platform)
const initialForm: Record<string, string> = {}
@@ -673,44 +683,87 @@ export default function SettingsClient({ space, isAdmin, integrations, currentRo
{/* Webhooks Tab */}
{activeTab === 'webhooks' && (
<div className="bg-card rounded border border-border p-6">
<p className="font-mono text-[10px] tracking-widest text-muted-foreground uppercase mb-6">Webhook Endpoint</p>
<p className="font-sans text-sm text-muted-foreground mb-4">
POST members to this endpoint for external integration or automation.
</p>
<div className="bg-muted/50 border border-border rounded p-4 mb-4">
<code className="font-mono text-xs text-primary break-all">
https://api.hackerspace.sh/{space.slug}/members
</code>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => copyToClipboard(`https://api.hackerspace.sh/${space.slug}/members`)}
className="font-mono text-[10px] border border-border px-3 py-1.5 rounded hover:border-primary hover:text-primary transition flex items-center gap-1"
>
<Copy className="w-3 h-3" />
{copiedWebhook ? 'Copied!' : 'Copy'}
</button>
<a
href="https://docs.hackerspace.sh/webhooks"
target="_blank"
rel="noopener noreferrer"
className="font-mono text-[10px] border border-border px-3 py-1.5 rounded hover:border-primary hover:text-primary transition"
>
View Docs
</a>
{isAdmin && (
<div className="bg-card rounded border border-border p-6 space-y-6">
<div>
<p className="font-mono text-[10px] tracking-widest text-muted-foreground uppercase mb-3">Webhook Endpoint</p>
<p className="font-sans text-sm text-muted-foreground mb-4">
POST members to this endpoint for external integration or automation.
</p>
<div className="bg-muted/50 border border-border rounded p-4 mb-4">
<code className="font-mono text-xs text-primary break-all">
https://api.hackerspace.sh/{space.slug}/members
</code>
</div>
<div className="flex flex-wrap items-center gap-2">
<button
onClick={handleRotateWebhook}
disabled={saving}
onClick={() => copyToClipboard(`https://api.hackerspace.sh/${space.slug}/members`)}
className="font-mono text-[10px] border border-border px-3 py-1.5 rounded hover:border-primary hover:text-primary transition flex items-center gap-1"
>
<RotateCcw className="w-3 h-3" />
Rotate Secret
<Copy className="w-3 h-3" />
{copiedWebhook ? 'Copied!' : 'Copy URL'}
</button>
<a
href="https://github.com/virgilvox/hackerspace-management/blob/main/docs/WEBHOOKS.md"
target="_blank"
rel="noopener noreferrer"
className="font-mono text-[10px] border border-border px-3 py-1.5 rounded hover:border-primary hover:text-primary transition"
>
View Docs
</a>
</div>
</div>
<div className="border-t border-border pt-6">
<p className="font-mono text-[10px] tracking-widest text-muted-foreground uppercase mb-3">Signing Secret</p>
<p className="font-sans text-sm text-muted-foreground mb-4">
Every delivery is signed with HMAC-SHA256 using this secret. See <a
href="https://github.com/virgilvox/hackerspace-management/blob/main/docs/WEBHOOKS.md#signing-and-verification"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>the docs</a> for verification examples in Node and Python.
</p>
<div className="bg-muted/50 border border-border rounded p-4 mb-4 flex items-center gap-3">
<code className="font-mono text-xs text-foreground break-all flex-1">
{webhookSecret
? (showWebhookSecret ? webhookSecret : '•'.repeat(Math.min(webhookSecret.length, 48)))
: <span className="text-muted-foreground italic">No secret set. Rotate to generate one.</span>}
</code>
{webhookSecret && (
<button
onClick={() => setShowWebhookSecret(v => !v)}
className="font-mono text-[10px] text-muted-foreground hover:text-foreground transition flex items-center gap-1 shrink-0"
title={showWebhookSecret ? 'Hide secret' : 'Show secret'}
>
{showWebhookSecret ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
</button>
)}
</div>
<div className="flex flex-wrap items-center gap-2">
{webhookSecret && (
<button
onClick={() => copySecretToClipboard(webhookSecret)}
className="font-mono text-[10px] border border-border px-3 py-1.5 rounded hover:border-primary hover:text-primary transition flex items-center gap-1"
>
<Copy className="w-3 h-3" />
{copiedSecret ? 'Copied!' : 'Copy secret'}
</button>
)}
{isAdmin && (
<button
onClick={handleRotateWebhook}
disabled={saving}
className="font-mono text-[10px] border border-border px-3 py-1.5 rounded hover:border-primary hover:text-primary transition flex items-center gap-1 disabled:opacity-50"
>
<RotateCcw className="w-3 h-3" />
{webhookSecret ? 'Rotate' : 'Generate'}
</button>
)}
</div>
{webhookSecret && (
<p className="font-sans text-[11px] text-muted-foreground mt-3">
Rotating immediately invalidates the previous secret. Update your receiver before rotating in production.
</p>
)}
</div>
</div>