Skip to content

Commit 403f3b2

Browse files
committed
Refinement
1 parent 768655b commit 403f3b2

5 files changed

Lines changed: 158 additions & 3 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"tool:backup": "tsx tools/backupDb.ts",
1818
"tool:restore": "tsx tools/restore.ts",
1919
"tool:pruneBackups": "tsx tools/pruneBackups.ts",
20+
"tool:seed-mirror": "tsx tools/seedMirror.ts",
2021
"secrets:encrypt": "sops -e --input-type dotenv --output-type dotenv --output .env.enc .env",
2122
"secrets:encrypt:dev": "sops -e --input-type dotenv --output-type dotenv --output .env.dev.enc .env.dev",
2223
"secrets:decrypt": "sops -d --input-type dotenv --output-type dotenv --output .env .env.enc",

src/layouts/Base.astro

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,22 @@ if (sessionCookie) {
5555
/>
5656
<span class="font-semibold text-ink tracking-tight text-base">Underlay</span>
5757
{mirrorConfig.enabled && (
58-
<span class="text-xs text-ink-muted font-normal tracking-normal">&middot; {mirrorConfig.nodeName}</span>
58+
<span class="text-sm font-medium text-ink-muted self-center ml-1">&middot; {mirrorConfig.nodeName}</span>
5959
)}
6060
</a>
6161
<div class="flex items-center gap-5 text-sm text-ink-muted">
6262
<a href="/explore" class="hover:text-ink transition-colors">Explore</a>
6363
<a href="/schemas" class="hover:text-ink transition-colors">Schemas</a>
6464
<a href="/docs" class="hover:text-ink transition-colors">Docs</a>
65-
<a href="/blog" class="hover:text-ink transition-colors">Blog</a>
65+
{!mirrorConfig.enabled && (
66+
<a href="/blog" class="hover:text-ink transition-colors">Blog</a>
67+
)}
6668
{mirrorConfig.enabled ? (
67-
<a href="/admin/mirror" class="hover:text-ink transition-colors">Admin</a>
69+
currentUser ? (
70+
<a href="/admin/mirror" class="hover:text-ink transition-colors">Admin</a>
71+
) : (
72+
<a href="/login" class="hover:text-ink transition-colors">Log in</a>
73+
)
6874
) : currentUser ? (
6975
<UserMenu slug={currentUser.slug} orgs={currentUser.orgs ?? []} client:load />
7076
) : (

src/pages/admin/mirror.astro

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,32 @@
22
import Base from "@/layouts/Base.astro";
33
import MirrorAdmin from "@/components/MirrorAdmin";
44
import { getMirrorConfig } from "@/lib/mirror-config";
5+
import { apiBase } from "@/lib/page-utils";
56
67
const config = getMirrorConfig();
78
89
// Redirect if not in mirror mode
910
if (!config.enabled) {
1011
return Astro.redirect("/");
1112
}
13+
14+
// Require authentication
15+
const sessionCookie = Astro.cookies.get("session")?.value;
16+
let authenticated = false;
17+
if (sessionCookie) {
18+
try {
19+
const res = await fetch(`${apiBase}/api/accounts/me`, {
20+
headers: { Cookie: `session=${sessionCookie}` },
21+
});
22+
authenticated = res.ok;
23+
} catch {
24+
// API unavailable
25+
}
26+
}
27+
28+
if (!authenticated) {
29+
return Astro.redirect("/login");
30+
}
1231
---
1332

1433
<Base title="Mirror Admin">

src/pages/index.astro

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,91 @@
11
---
22
import Base from "@/layouts/Base.astro";
3+
import { getMirrorConfig } from "@/lib/mirror-config";
4+
5+
const mirrorConfig = getMirrorConfig();
36
---
47

8+
{mirrorConfig.enabled ? (
9+
<Base title={`Underlay · ${mirrorConfig.nodeName}`}>
10+
<div class="max-w-5xl mx-auto px-4">
11+
<!-- Hero -->
12+
<section class="py-12 border-b border-rule">
13+
<div class="max-w-2xl">
14+
<h1 class="text-2xl font-semibold tracking-tight mb-3 font-sans">
15+
{mirrorConfig.nodeName}
16+
</h1>
17+
<p class="text-ink-muted text-sm leading-relaxed mb-2">
18+
This is a mirror of <a href={mirrorConfig.upstream} class="underline hover:text-ink">{mirrorConfig.upstream.replace(/^https?:\/\//, '')}</a>.
19+
It maintains a verified copy of all public collections for long-term preservation and local access.
20+
</p>
21+
<p class="text-ink-muted text-sm leading-relaxed mb-6">
22+
Every version, record, and file is content-addressed and hash-verified against the upstream source.
23+
If the primary server becomes unavailable, this mirror serves as an independent, complete archive.
24+
</p>
25+
<div class="flex gap-3">
26+
<a
27+
href="/explore"
28+
class="inline-block bg-ink text-parchment visited:text-parchment px-4 py-2 text-sm font-medium hover:bg-ink-light transition-colors"
29+
>
30+
Explore collections
31+
</a>
32+
<a
33+
href="/schemas"
34+
class="inline-block border border-ink px-4 py-2 text-sm font-medium hover:bg-parchment-dark transition-colors visited:text-ink"
35+
>
36+
Browse schemas
37+
</a>
38+
</div>
39+
</div>
40+
</section>
41+
42+
<!-- What this is -->
43+
<section class="py-8 border-b border-rule">
44+
<h2 class="text-xs uppercase tracking-widest text-ink-muted mb-5 font-semibold">What is this?</h2>
45+
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 text-sm">
46+
<div>
47+
<h3 class="font-semibold mb-1 font-sans">A preservation mirror</h3>
48+
<p class="text-ink-muted">
49+
This server replicates public collections from the canonical Underlay instance.
50+
Each copy is cryptographically verified — tamper-evident by design.
51+
</p>
52+
</div>
53+
<div>
54+
<h3 class="font-semibold mb-1 font-sans">Independent infrastructure</h3>
55+
<p class="text-ink-muted">
56+
Running on separate hardware with its own database and storage.
57+
No single point of failure — if the upstream goes down, the data persists here.
58+
</p>
59+
</div>
60+
<div>
61+
<h3 class="font-semibold mb-1 font-sans">Open and browsable</h3>
62+
<p class="text-ink-muted">
63+
Browse any collection, inspect any schema, view any record.
64+
The same API works here as on the primary server.
65+
</p>
66+
</div>
67+
</div>
68+
</section>
69+
70+
<!-- Bottom -->
71+
<section class="py-8">
72+
<div class="flex gap-8 text-sm">
73+
<div>
74+
<h3 class="font-semibold mb-1 font-sans">Powered by Underlay</h3>
75+
<p class="text-ink-muted">
76+
Underlay is open-source infrastructure for structured knowledge preservation.
77+
Anyone can run a mirror — same software, different server.
78+
</p>
79+
</div>
80+
<div>
81+
<h3 class="font-semibold mb-1 font-sans">Built by Knowledge Futures</h3>
82+
<p class="text-ink-muted">A 501(c)(3) nonprofit building open infrastructure for knowledge sharing.</p>
83+
</div>
84+
</div>
85+
</section>
86+
</div>
87+
</Base>
88+
) : (
589
<Base title="Underlay — A public registry for structured knowledge">
690
<div class="max-w-5xl mx-auto px-4">
791
<!-- Hero -->
@@ -111,3 +195,4 @@ GET /collections/:owner/:slug/export # full archive</code></pre>
111195
</section>
112196
</div>
113197
</Base>
198+
)}

tools/seedMirror.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Seed script for mirror mode — creates only an admin user.
3+
* Does not create collections, schemas, or records (those come from sync).
4+
*
5+
* Usage: npm run tool:seed-mirror
6+
*/
7+
8+
import { db, schema } from "../src/db/index.js";
9+
import bcrypt from "bcrypt";
10+
import { v4 as uuidv4 } from "uuid";
11+
12+
async function seedMirror() {
13+
console.log("[seed-mirror] Seeding mirror database...");
14+
15+
const existing = await db.select().from(schema.accounts).limit(1);
16+
if (existing.length > 0) {
17+
console.log("[seed-mirror] Admin account already exists, skipping.");
18+
process.exit(0);
19+
}
20+
21+
const password = process.env.MIRROR_ADMIN_PASSWORD ?? "admin";
22+
const email = process.env.MIRROR_ADMIN_EMAIL ?? "admin@mirror.underlay.org";
23+
24+
const passwordHash = await bcrypt.hash(password, 10);
25+
const adminId = uuidv4();
26+
27+
await db.insert(schema.accounts).values({
28+
id: adminId,
29+
slug: "admin",
30+
type: "user",
31+
displayName: "Mirror Admin",
32+
email,
33+
passwordHash,
34+
});
35+
36+
console.log(`[seed-mirror] Created admin user (${email} / ${password})`);
37+
console.log("[seed-mirror] Done. You can now log in to /admin/mirror.");
38+
process.exit(0);
39+
}
40+
41+
seedMirror().catch((err) => {
42+
console.error("[seed-mirror] Failed:", err);
43+
process.exit(1);
44+
});

0 commit comments

Comments
 (0)