diff --git a/.env.example b/.env.example index 7215173..cf2482d 100644 --- a/.env.example +++ b/.env.example @@ -14,9 +14,58 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOi...replace-with-anon-key SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOi...replace-with-service-role-key # App URL (required) -# Public-facing base URL of this deployment. Used in OAuth redirects. +# Public-facing base URL of this deployment. Used in OAuth redirects and in every +# absolute link the app emits (dues emails, form-result links, the /me portal). +# Set this to your own domain: it replaces the old hackerspace.sh fallback, so a +# single-tenant instance never leaks the platform domain. No trailing slash. NEXT_PUBLIC_APP_URL=http://localhost:3000 +# ----------------------------------------------------------------------------- +# Single-tenant mode (optional). See docs/SINGLE_TENANT.md for the full guide. +# ----------------------------------------------------------------------------- +# Leave everything below unset to run the default MULTI-TENANT platform (one +# deployment hosts many spaces, users create/join spaces at signup — the +# hackerspace.sh model). Set NEXT_PUBLIC_SINGLE_TENANT=true to run a dedicated +# instance for exactly ONE space: +# - the "create a space" flow is disabled (server + UI), +# - signup becomes "join THE space", +# - the hackerspace.sh marketing/landing shell is hidden. +# These are NEXT_PUBLIC_ because the signup page (a client component) reads them; +# the server re-enforces every rule, so they are safe to expose. +NEXT_PUBLIC_SINGLE_TENANT= + +# Brand name shown throughout the UI (logo wordmark, page titles). White-label +# your instance here. Defaults to "hackerspace.sh" when unset. +NEXT_PUBLIC_SITE_NAME= + +# The slug of your one space (matches spaces.slug, created by `pnpm setup`). +# In single-tenant mode new signups join this space. If unset, the app falls +# back to "the only space in the database". +NEXT_PUBLIC_SINGLE_TENANT_SPACE_SLUG= + +# Set to "true" to let people sign up and join your space WITHOUT an invite code +# (still subject to the space's require_approval setting — approve them from the +# members page). Leave unset to require an invite code even in single-tenant mode. +NEXT_PUBLIC_SINGLE_TENANT_OPEN_JOIN= + +# Override the marketing shell visibility. Defaults to ON for the platform and +# OFF in single-tenant mode. Set "true" to keep the /resources subsite + landing +# page, or "false" to hide them on a multi-tenant deploy. +NEXT_PUBLIC_SHOW_MARKETING= + +# ----------------------------------------------------------------------------- +# First-admin bootstrap (used ONLY by `pnpm setup`, never read at runtime). +# `pnpm setup` provisions your one space and creates the first admin login from +# these values (or from interactive prompts / CLI flags). Fill them in, run the +# CLI once, then you can clear the password. See docs/SINGLE_TENANT.md. +# ----------------------------------------------------------------------------- +SETUP_SPACE_NAME= +SETUP_SPACE_SLUG= +SETUP_SPACE_CITY= +SETUP_ADMIN_EMAIL= +SETUP_ADMIN_PASSWORD= +SETUP_ADMIN_NAME= + # Secrets vault encryption key (required for the /ops Secrets feature). # 32 bytes of cryptographically random data, hex-encoded (64 hex chars). # Generate with: openssl rand -hex 32 diff --git a/app/layout.tsx b/app/layout.tsx index 4a707a1..ba79863 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,13 +1,16 @@ import type { Metadata } from 'next' import { Geist, Geist_Mono } from 'next/font/google' import { Toaster } from 'sonner' +import { tenantConfig } from '@/lib/tenant' import './globals.css' const _geist = Geist({ subsets: ["latin"] }); const _geistMono = Geist_Mono({ subsets: ["latin"] }); +const _tenant = tenantConfig() + export const metadata: Metadata = { - title: 'hackerspace.sh — Manage Your Space', + title: `${_tenant.siteName} — Manage Your Space`, description: 'The operating system for hackerspaces: members, tasks, projects, ops, payments, and comms.', icons: { icon: [{ url: '/logo.svg', type: 'image/svg+xml' }], diff --git a/app/signup/page.tsx b/app/signup/page.tsx index bf8d5f5..c549bc0 100644 --- a/app/signup/page.tsx +++ b/app/signup/page.tsx @@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation' import Link from 'next/link' import { createClient } from '@/lib/supabase/client' import { createSpace, joinSpace } from '@/lib/auth-actions' +import { tenantConfig } from '@/lib/tenant' function generateInviteCode() { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' @@ -19,9 +20,13 @@ function generateInviteCode() { export default function SignupPage() { const router = useRouter() + // Single-tenant instances host one space: there is no "create a space" path, + // so signup always means "join THE space". Preselect join and hide the + // chooser. createSpace is also refused server-side (defense in depth). + const tenant = tenantConfig() const [step, setStep] = useState<'account' | 'space'>('account') const [isReturningUser, setIsReturningUser] = useState(false) - const [mode, setMode] = useState<'create' | 'join' | null>(null) + const [mode, setMode] = useState<'create' | 'join' | null>(tenant.singleTenant ? 'join' : null) const [fullName, setFullName] = useState('') const [email, setEmail] = useState('') const [password, setPassword] = useState('') @@ -158,7 +163,7 @@ export default function SignupPage() {
{'{'} - hackerspace.sh + {tenant.siteName} {'}'}
@@ -216,6 +221,8 @@ export default function SignupPage() { /> + {!tenant.singleTenant && ( + <>

What are you doing?

@@ -257,6 +264,8 @@ export default function SignupPage() {

use an invite code

+ + )} {error &&

{error}

} @@ -356,6 +365,15 @@ export default function SignupPage() {

Default channels (general, announcements, ops) will be created automatically.

+ ) : tenant.singleTenant && tenant.openJoin ? ( +
+

+ You're joining {tenant.siteName}. +

+

+ An administrator may need to approve your membership before you gain full access. +

+
) : (