chore: prepare repo for public release — scrub internal infra, rewrite README
- Remove owner-specific / legacy deploy files (home/prod/server compose, deploy/). The home compose stays as a local untracked file for the maintainer's own deploy. - Genericise infra-specific code comments (egress-proxy examples) to neutral wording. - Replace the hardcoded contact email on the legal pages with the instance operator's configured admin email, served via the public /auth/config and shown with a neutral fallback — so each self-hosted instance shows its own contact. - Rewrite README for the current app + a copy-paste self-hosting quick start (prebuilt image + first-run wizard) with a build-from-source alternative; tidy .env.example.
This commit is contained in:
parent
381794d9ae
commit
0d44d3a34a
10 changed files with 149 additions and 119 deletions
|
|
@ -1,10 +1,47 @@
|
|||
// Shared shell for the public, login-free legal pages (/privacy, /terms). These must be
|
||||
// reachable unauthenticated — Google's OAuth consent screen links to them and reviewers
|
||||
// fetch them directly — so they render outside the authenticated App tree (see main.tsx).
|
||||
// fetch them directly — so they render outside the authenticated App tree (see main.tsx),
|
||||
// with no react-query provider: the operator contact is fetched with a plain fetch below.
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const CONTACT_EMAIL = "b1fr0stmailops@gmail.com";
|
||||
export const LAST_UPDATED = "June 14, 2026";
|
||||
|
||||
// Contact shown on the legal pages: the instance operator's configured admin email, from the
|
||||
// public /auth/config. One shared fetch across the page; null when the operator set none.
|
||||
let contactPromise: Promise<string | null> | null = null;
|
||||
function fetchOperatorContact(): Promise<string | null> {
|
||||
if (!contactPromise) {
|
||||
contactPromise = fetch("/auth/config")
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((c) => (c?.operator_contact as string | undefined) ?? null)
|
||||
.catch(() => null);
|
||||
}
|
||||
return contactPromise;
|
||||
}
|
||||
|
||||
export function useOperatorContact(): string | null {
|
||||
const [contact, setContact] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
fetchOperatorContact().then((c) => alive && setContact(c));
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, []);
|
||||
return contact;
|
||||
}
|
||||
|
||||
// The operator's contact rendered as a mailto link, or a neutral fallback phrase when unset.
|
||||
export function ContactEmail() {
|
||||
const email = useOperatorContact();
|
||||
if (!email) return <>your instance's administrator</>;
|
||||
return (
|
||||
<a href={`mailto:${email}`} className="text-accent hover:underline">
|
||||
{email}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export function LegalLink({ href, children }: { href: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<a href={href} target="_blank" rel="noreferrer" className="text-accent hover:underline">
|
||||
|
|
@ -20,6 +57,7 @@ export default function LegalLayout({
|
|||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const contact = useOperatorContact();
|
||||
return (
|
||||
<div className="min-h-screen bg-bg text-fg">
|
||||
<div className="mx-auto w-[min(92vw,720px)] py-12">
|
||||
|
|
@ -35,7 +73,9 @@ export default function LegalLayout({
|
|||
<a href="/" className="hover:text-fg transition">Home</a>
|
||||
<a href="/privacy" className="hover:text-fg transition">Privacy Policy</a>
|
||||
<a href="/terms" className="hover:text-fg transition">Terms of Service</a>
|
||||
<a href={`mailto:${CONTACT_EMAIL}`} className="hover:text-fg transition">Contact</a>
|
||||
{contact && (
|
||||
<a href={`mailto:${contact}`} className="hover:text-fg transition">Contact</a>
|
||||
)}
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import LegalLayout, { CONTACT_EMAIL, H2, LegalLink } from "./LegalLayout";
|
||||
import LegalLayout, { ContactEmail, H2, LegalLink } from "./LegalLayout";
|
||||
|
||||
export default function PrivacyPolicy() {
|
||||
return (
|
||||
|
|
@ -71,11 +71,7 @@ export default function PrivacyPolicy() {
|
|||
myaccount.google.com/permissions
|
||||
</LegalLink>
|
||||
; once revoked, the stored tokens can no longer be used. To have your account and
|
||||
associated data deleted, email{" "}
|
||||
<a href={`mailto:${CONTACT_EMAIL}`} className="text-accent hover:underline">
|
||||
{CONTACT_EMAIL}
|
||||
</a>
|
||||
.
|
||||
associated data deleted, contact <ContactEmail />.
|
||||
</p>
|
||||
|
||||
<H2>Cookies</H2>
|
||||
|
|
@ -100,11 +96,7 @@ export default function PrivacyPolicy() {
|
|||
|
||||
<H2>Contact</H2>
|
||||
<p>
|
||||
Questions about this policy or your data:{" "}
|
||||
<a href={`mailto:${CONTACT_EMAIL}`} className="text-accent hover:underline">
|
||||
{CONTACT_EMAIL}
|
||||
</a>
|
||||
.
|
||||
Questions about this policy or your data: contact <ContactEmail />.
|
||||
</p>
|
||||
</LegalLayout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import LegalLayout, { CONTACT_EMAIL, H2, LegalLink } from "./LegalLayout";
|
||||
import LegalLayout, { ContactEmail, H2, LegalLink } from "./LegalLayout";
|
||||
|
||||
export default function Terms() {
|
||||
return (
|
||||
|
|
@ -56,11 +56,7 @@ export default function Terms() {
|
|||
|
||||
<H2>Contact</H2>
|
||||
<p>
|
||||
Questions:{" "}
|
||||
<a href={`mailto:${CONTACT_EMAIL}`} className="text-accent hover:underline">
|
||||
{CONTACT_EMAIL}
|
||||
</a>
|
||||
.
|
||||
Questions: contact <ContactEmail />.
|
||||
</p>
|
||||
</LegalLayout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -773,8 +773,11 @@ export const api = {
|
|||
}),
|
||||
|
||||
// Public: which sign-in options this instance offers (e.g. hide Google when not configured).
|
||||
authConfig: (): Promise<{ google_enabled: boolean; allow_registration: boolean }> =>
|
||||
req("/auth/config"),
|
||||
authConfig: (): Promise<{
|
||||
google_enabled: boolean;
|
||||
allow_registration: boolean;
|
||||
operator_contact: string | null;
|
||||
}> => req("/auth/config"),
|
||||
|
||||
// --- first-run install wizard (token from the setup URL printed to the container logs) ---
|
||||
setupStatus: (): Promise<{ configured: boolean }> => req("/api/setup/status"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue