Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions tools/star-tracker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,24 @@ function parseRangeMs(raw: string | undefined): number | null {

const app = new Hono<Env>();

// Matches a bare IPv4 (127.0.0.1) or bracketed IPv6 (`[::1]`) host — how
// `URL#hostname` renders each. Covers `wrangler dev --ip 0.0.0.0` and
// LAN-IP access during local testing, not just localhost.
const IP_HOST_RE = /^(\d{1,3}\.){3}\d{1,3}$|^\[[0-9a-f:]+\]$/i;

// GSC flagged http://stars.wavekat.com/ as a duplicate of the https:// URL
// with no canonical declared. Cloudflare's edge "Always Use HTTPS" redirect
// doesn't cover every path to this Worker, so enforce it here too. Skip
// localhost and IP hosts so `wrangler dev` (plain HTTP) keeps working.
app.use('*', async (c, next) => {
const url = new URL(c.req.url);
if (url.protocol === 'http:' && url.hostname !== 'localhost' && !IP_HOST_RE.test(url.hostname)) {
url.protocol = 'https:';
return c.redirect(url.toString(), 301);
}
await next();
});

// Resolve the current user (if any) from the session cookie on every request.
app.use('*', async (c, next) => {
const userId = await readSession(c, c.env.JWT_SECRET);
Expand Down Expand Up @@ -218,7 +236,7 @@ app.get('/favicon.svg', () => {

// -- Landing ----------------------------------------------------------------

app.get('/', (c) => c.html(pages.landing(c.get('user'))));
app.get('/', (c) => c.html(pages.landing(c.get('user'), c.env.PUBLIC_URL)));

// -- OAuth ------------------------------------------------------------------

Expand Down Expand Up @@ -289,7 +307,7 @@ app.get('/_admin', async (c) => {
const user = c.get('user');
if (!isAdmin(user, c.env.ADMIN_USERNAMES)) return c.notFound();
const tenants = await db.listAllTenantsWithStats(c.env.DB);
return c.html(pages.adminTenants(user!, tenants));
return c.html(pages.adminTenants(user!, tenants, c.env.PUBLIC_URL));
});

// -- Tenant creation --------------------------------------------------------
Expand Down
27 changes: 21 additions & 6 deletions tools/star-tracker/src/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function esc(s: string): string {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

function shell(title: string, user: User | null, body: string): string {
function shell(title: string, user: User | null, body: string, canonicalUrl?: string, noindex = false): string {
const nav = user
? `<a href="/dashboard">${esc(user.username)}</a> · <form method="POST" action="/auth/logout" style="display:inline"><button class="link" type="submit">Sign out</button></form>`
: `<a href="/auth/login">Sign in with GitHub</a>`;
Expand All @@ -34,6 +34,8 @@ function shell(title: string, user: User | null, body: string): string {
<html lang="en"><head><meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>${esc(title)}</title>
${canonicalUrl ? `<link rel="canonical" href="${esc(canonicalUrl)}"/>` : ''}
${noindex ? '<meta name="robots" content="noindex, nofollow"/>' : ''}
<link rel="icon" type="image/svg+xml" href="/favicon.svg"/>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-P2YBZ0W8HQ"></script>
<script>
Expand Down Expand Up @@ -288,7 +290,7 @@ ${body}
</body></html>`;
}

export function landing(user: User | null): string {
export function landing(user: User | null, publicUrl: string): string {
return shell(
'stars.wavekat.com',
user,
Expand All @@ -312,10 +314,11 @@ export function landing(user: User | null): string {
</ol>
<h2>Free, open source</h2>
<p>Source: <a href="https://github.com/wavekat/wavekat.com/tree/main/tools/star-tracker">github.com/wavekat/wavekat.com</a> · Apache-2.0.</p>`,
`${publicUrl}/`,
);
}

export function dashboard(user: User, tenants: Tenant[], _publicUrl: string, flash?: string): string {
export function dashboard(user: User, tenants: Tenant[], publicUrl: string, flash?: string): string {
const list = tenants.length === 0
? `<p class="muted">No tenants yet. Register your first below.</p>`
: `<ul class="tenants">${tenants.map((t) => `<li class="card">
Expand All @@ -335,6 +338,8 @@ ${list}
<p><button type="submit">Register</button></p>
</form>
<p class="muted">We'll verify you're an admin of the org (or that the login matches your GitHub username) before creating it.</p>`,
`${publicUrl}/dashboard`,
/* noindex */ true,
);
}

Expand Down Expand Up @@ -710,6 +715,8 @@ ${flash ? `<p class="warn">${esc(flash)}</p>` : ''}
</details>
${repos.length > 0 ? `<h2>Tracked repos</h2>
<ul class="repo-list">${repos.map((r) => repoListItem(r, /* showBadges */ true)).join('')}</ul>` : ''}`,
`${publicUrl}/${tenant.slug}`,
/* noindex */ true,
);
}

Expand Down Expand Up @@ -737,14 +744,15 @@ ${visibleCount > 0 ? `<h2>Tracked repos</h2>

<h2>Track your own org</h2>
<p>stars.wavekat.com is a free, open-source star-history service. ${user ? `<a href="/dashboard">Open your dashboard →</a>` : `<a href="/auth/login">Sign in with GitHub</a> to register a tracker for your org or personal account.`}</p>`,
`${publicUrl}/${tenant.slug}`,
);
}

// Landing page for /:slug when the slug isn't tracked yet. Anyone might
// arrive here from a shared link (a README that anticipated tracking, a
// blog post). The copy is share-friendly: admins get a register CTA,
// non-admins get a forwardable explanation.
export function notTrackedInvite(user: User | null, slug: string, _publicUrl: string): string {
export function notTrackedInvite(user: User | null, slug: string, publicUrl: string): string {
const isOwnAccount = user && user.username.toLowerCase() === slug;
const cta = isOwnAccount
? `<p><a class="btn-github" href="/dashboard">Register ${esc(slug)} on your dashboard →</a></p>
Expand All @@ -763,6 +771,10 @@ export function notTrackedInvite(user: User | null, slug: string, _publicUrl: st
${cta}
<h2>Not an admin?</h2>
<p class="muted">Forward this page to someone who is — once they install the webhook (one minute) and click "backfill all", the chart at <code>stars.wavekat.com/${esc(slug)}/chart.svg</code> will start working and any README that already embeds it will light up.</p>`,
`${publicUrl}/${slug}`,
// Near-identical boilerplate text for every unregistered slug — keep
// these out of the index so they don't read as duplicate/thin content.
/* noindex */ true,
);
}

Expand Down Expand Up @@ -804,12 +816,13 @@ ${views ? viewsPanel(views) : ''}

<h2>Other repos in ${esc(slug)}</h2>
<p>See the <a href="/${esc(slug)}">full ${esc(slug)} chart</a> for all tracked repos in this account.</p>`,
`${publicUrl}/${slug}/${name}`,
);
}

// Operator admin: every registered tenant, newest first. Reached only by
// users listed in ADMIN_USERNAMES; non-admins 404 before this renders.
export function adminTenants(user: User, tenants: TenantWithStats[]): string {
export function adminTenants(user: User, tenants: TenantWithStats[], publicUrl: string): string {
const sevenDays = 7 * 86400_000;
const now = Date.now();
const active7d = tenants.filter((t) => t.last_event_at && now - Date.parse(t.last_event_at) <= sevenDays).length;
Expand Down Expand Up @@ -861,9 +874,11 @@ export function adminTenants(user: User, tenants: TenantWithStats[]): string {
<p class="muted">Operator view — every org/user that's registered a tracker, newest first.</p>
${summary}
${rows}`,
`${publicUrl}/_admin`,
/* noindex */ true,
);
}

export function error(user: User | null, status: number, message: string): string {
return shell(`Error ${status}`, user, `<h1>${status}</h1><p>${esc(message)}</p>`);
return shell(`Error ${status}`, user, `<h1>${status}</h1><p>${esc(message)}</p>`, undefined, /* noindex */ true);
}
Loading