4 Commits

Author SHA1 Message Date
8f5e84523e Merge pull request 'chore(auth): redirect sign-in/sign-up to app.cameleer.io' (#5) from relaunch-2026-04-25 into main
All checks were successful
ci / build-test (push) Successful in 4m17s
Reviewed-on: #5
2026-04-25 09:33:23 +02:00
hsiegeln
fa12df8ec6 chore(auth): redirect sign-in/sign-up to app.cameleer.io
All checks were successful
ci / build-test (push) Successful in 3m41s
ci / build-test (pull_request) Successful in 4m12s
Both auth flows now navigate to the app domain rather than the
auth.cameleer.io subdomain:

  PUBLIC_AUTH_SIGNIN_URL → https://app.cameleer.io/sign-in
  PUBLIC_AUTH_SIGNUP_URL → https://app.cameleer.io/sign-in?first_screen=register

Updated:
- .env.example (the canonical reference values)
- OPERATOR-CHECKLIST.md (deploy-time secret values)
- src/config/auth.test.ts (test fixtures)
- src/middleware.ts (CSP-comment about <a> navigation target)
- src/pages/privacy.astro (visitor-facing external-links section
  in §6 of the privacy policy)

The auth.ts validator stays strict-https — the new URLs are still
absolute https URLs, just on a different host.  Logto itself may
still run at auth.cameleer.io as the OIDC backend; only the
visitor-facing /sign-in entry point moved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 09:28:02 +02:00
3b184488bb Merge pull request 'relaunch-2026-04-25' (#4) from relaunch-2026-04-25 into main
All checks were successful
ci / build-test (push) Successful in 4m21s
Reviewed-on: #4
2026-04-25 08:08:54 +02:00
hsiegeln
203e4bfb41 perf: replace 1.5 MB cameleer-logo.svg refs with optimised PNGs
All checks were successful
ci / build-test (push) Successful in 3m44s
ci / build-test (pull_request) Successful in 4m17s
The Inkscape-exported cameleer-logo.svg in public/ is 1.5 MB —
loaded eagerly in the SiteHeader (32×32) and Hero (64×64), it was
the dominant hit on the homepage's largest-contentful-paint. The
relaunch's added above-the-fold DOM nudged Lighthouse perf from
0.95 to 0.92 and tipped CI's >=0.95 threshold red.

Switch all four SVG references to the pre-optimised PNG icons that
already ship in public/icons/:
  - SiteHeader (32-displayed): /icons/cameleer-48.png   (4.4 KB)
  - Hero       (64-displayed): /icons/cameleer-192.png  (36 KB)
  - SiteFooter (24-displayed): /icons/cameleer-32.png   (2.4 KB)
  - BaseLayout favicon link: drop the SVG, keep the existing
    32 PNG fallback (already declared on the next line).

Local Lighthouse (http-server, no gzip) before: perf 0.72,
LCP 10.0s. After: perf 0.94, LCP 1.6s. CI on Linux + LH static
server should comfortably clear the 0.95 gate.

The SVG file itself is left in place — unreferenced, but kept in
case any external link still points at it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 07:59:30 +02:00
9 changed files with 23 additions and 24 deletions

View File

@@ -1,5 +1,5 @@
# Logto auth endpoints — the marketing site only performs <a href> navigations to these.
# No tokens, no cookies, no XHR — these are plain hyperlinks.
PUBLIC_AUTH_SIGNIN_URL=https://auth.cameleer.io/sign-in
PUBLIC_AUTH_SIGNUP_URL=https://auth.cameleer.io/sign-in?first_screen=register
PUBLIC_AUTH_SIGNIN_URL=https://app.cameleer.io/sign-in
PUBLIC_AUTH_SIGNUP_URL=https://app.cameleer.io/sign-in?first_screen=register
PUBLIC_SALES_EMAIL=sales@cameleer.io

View File

@@ -75,8 +75,8 @@ Add these under Repository settings → Actions → Secrets (or variables):
| `SFTP_PATH` | secret | Absolute path to the Apache vhost docroot configured in konsoleH (typically `/usr/www/users/<login>/public_html`). Mismatch → 404 on origin. |
| `SFTP_KEY` | secret | Contents of `~/.ssh/cameleer-website-deploy` (private key, PEM) |
| `SFTP_KNOWN_HOSTS` | secret | Contents of `hetzner-known-hosts.txt` (captured via `ssh-keyscan`) |
| `PUBLIC_AUTH_SIGNIN_URL` | secret | `https://auth.cameleer.io/sign-in` |
| `PUBLIC_AUTH_SIGNUP_URL` | secret | `https://auth.cameleer.io/sign-in?first_screen=register` |
| `PUBLIC_AUTH_SIGNIN_URL` | secret | `https://app.cameleer.io/sign-in` |
| `PUBLIC_AUTH_SIGNUP_URL` | secret | `https://app.cameleer.io/sign-in?first_screen=register` |
| `PUBLIC_SALES_EMAIL` | secret | `sales@cameleer.io` (or whatever sales alias you set up) |
These three are not actually secret (they end up in the built HTML), but Gitea's

View File

@@ -5,7 +5,7 @@ const year = new Date().getFullYear();
<div class="max-w-content mx-auto px-6 py-12 flex flex-col md:flex-row md:items-center md:justify-between gap-8">
<div class="flex items-center gap-3">
<img
src="/cameleer-logo.svg"
src="/icons/cameleer-32.png"
width="24"
height="24"
alt=""

View File

@@ -5,7 +5,7 @@ import CTAButtons from './CTAButtons.astro';
<div class="max-w-content mx-auto px-6 h-16 flex items-center justify-between gap-6">
<a href="/" class="flex items-center gap-2 group" aria-label="Cameleer home">
<img
src="/cameleer-logo.svg"
src="/icons/cameleer-48.png"
width="32"
height="32"
alt=""

View File

@@ -22,7 +22,7 @@ const pins: Pin[] = [
<div class="grid lg:grid-cols-12 gap-10 lg:gap-14 items-center">
<div class="lg:col-span-5">
<img
src="/cameleer-logo.svg"
src="/icons/cameleer-192.png"
width="64"
height="64"
alt=""

View File

@@ -4,57 +4,57 @@ import { resolveAuthConfig } from './auth';
describe('resolveAuthConfig', () => {
it('returns both URLs and sales email from env', () => {
const cfg = resolveAuthConfig({
PUBLIC_AUTH_SIGNIN_URL: 'https://auth.cameleer.io/sign-in',
PUBLIC_AUTH_SIGNUP_URL: 'https://auth.cameleer.io/sign-in?first_screen=register',
PUBLIC_AUTH_SIGNIN_URL: 'https://app.cameleer.io/sign-in',
PUBLIC_AUTH_SIGNUP_URL: 'https://app.cameleer.io/sign-in?first_screen=register',
PUBLIC_SALES_EMAIL: 'sales@cameleer.io',
});
expect(cfg.signInUrl).toBe('https://auth.cameleer.io/sign-in');
expect(cfg.signUpUrl).toBe('https://auth.cameleer.io/sign-in?first_screen=register');
expect(cfg.signInUrl).toBe('https://app.cameleer.io/sign-in');
expect(cfg.signUpUrl).toBe('https://app.cameleer.io/sign-in?first_screen=register');
expect(cfg.salesEmail).toBe('sales@cameleer.io');
});
it('throws if PUBLIC_AUTH_SIGNIN_URL is missing', () => {
expect(() => resolveAuthConfig({
PUBLIC_AUTH_SIGNUP_URL: 'https://auth.cameleer.io/sign-in?first_screen=register',
PUBLIC_AUTH_SIGNUP_URL: 'https://app.cameleer.io/sign-in?first_screen=register',
PUBLIC_SALES_EMAIL: 'sales@cameleer.io',
})).toThrow(/PUBLIC_AUTH_SIGNIN_URL/);
});
it('throws if a URL is not https', () => {
expect(() => resolveAuthConfig({
PUBLIC_AUTH_SIGNIN_URL: 'http://auth.cameleer.io/sign-in',
PUBLIC_AUTH_SIGNUP_URL: 'https://auth.cameleer.io/sign-in?first_screen=register',
PUBLIC_AUTH_SIGNIN_URL: 'http://app.cameleer.io/sign-in',
PUBLIC_AUTH_SIGNUP_URL: 'https://app.cameleer.io/sign-in?first_screen=register',
PUBLIC_SALES_EMAIL: 'sales@cameleer.io',
})).toThrow(/must be https/);
});
it('throws if sales email is not a valid mailto target', () => {
expect(() => resolveAuthConfig({
PUBLIC_AUTH_SIGNIN_URL: 'https://auth.cameleer.io/sign-in',
PUBLIC_AUTH_SIGNUP_URL: 'https://auth.cameleer.io/sign-in?first_screen=register',
PUBLIC_AUTH_SIGNIN_URL: 'https://app.cameleer.io/sign-in',
PUBLIC_AUTH_SIGNUP_URL: 'https://app.cameleer.io/sign-in?first_screen=register',
PUBLIC_SALES_EMAIL: 'not-an-email',
})).toThrow(/PUBLIC_SALES_EMAIL/);
});
it('throws if PUBLIC_AUTH_SIGNUP_URL is missing', () => {
expect(() => resolveAuthConfig({
PUBLIC_AUTH_SIGNIN_URL: 'https://auth.cameleer.io/sign-in',
PUBLIC_AUTH_SIGNIN_URL: 'https://app.cameleer.io/sign-in',
PUBLIC_SALES_EMAIL: 'sales@cameleer.io',
})).toThrow(/PUBLIC_AUTH_SIGNUP_URL/);
});
it('throws if PUBLIC_AUTH_SIGNUP_URL is not https', () => {
expect(() => resolveAuthConfig({
PUBLIC_AUTH_SIGNIN_URL: 'https://auth.cameleer.io/sign-in',
PUBLIC_AUTH_SIGNUP_URL: 'http://auth.cameleer.io/sign-in?first_screen=register',
PUBLIC_AUTH_SIGNIN_URL: 'https://app.cameleer.io/sign-in',
PUBLIC_AUTH_SIGNUP_URL: 'http://app.cameleer.io/sign-in?first_screen=register',
PUBLIC_SALES_EMAIL: 'sales@cameleer.io',
})).toThrow(/must be https/);
});
it('exposes signUpUrl distinct from signInUrl', () => {
const cfg = resolveAuthConfig({
PUBLIC_AUTH_SIGNIN_URL: 'https://auth.cameleer.io/sign-in',
PUBLIC_AUTH_SIGNUP_URL: 'https://auth.cameleer.io/sign-in?first_screen=register',
PUBLIC_AUTH_SIGNIN_URL: 'https://app.cameleer.io/sign-in',
PUBLIC_AUTH_SIGNUP_URL: 'https://app.cameleer.io/sign-in?first_screen=register',
PUBLIC_SALES_EMAIL: 'sales@cameleer.io',
});
expect(cfg.signUpUrl).not.toBe(cfg.signInUrl);

View File

@@ -31,7 +31,6 @@ const ogUrl = new URL(ogImage, Astro.site ?? 'https://www.cameleer.io').toString
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
<link rel="icon" type="image/svg+xml" href="/cameleer-logo.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/cameleer-32.png" />
<link rel="apple-touch-icon" href="/icons/cameleer-180.png" />

View File

@@ -20,7 +20,7 @@ export function buildSecurityHeaders(): Record<string, string> {
"connect-src 'self'",
"frame-ancestors 'none'",
"base-uri 'self'",
// No forms on this marketing site today (all auth redirects go to auth.cameleer.io
// No forms on this marketing site today (all auth redirects go to app.cameleer.io
// as plain <a> navigations). If a future form is added, relax to 'self' or an allow-list.
"form-action 'none'",
"object-src 'none'",

View File

@@ -67,7 +67,7 @@ const lastUpdated = '2026-04-24';
<section class="mb-10">
<h2 class="text-lg font-bold text-text mb-3">6. External links</h2>
<p class="text-text-muted leading-relaxed">
Sign-in and sign-up links on this site navigate you to <span class="font-mono text-accent">auth.cameleer.io</span> (Logto identity service) and subsequently <span class="font-mono text-accent">platform.cameleer.io</span>. Those services have their own privacy policies, which apply from the moment you arrive there.
Sign-in and sign-up links on this site navigate you to <span class="font-mono text-accent">app.cameleer.io</span> (the Cameleer app, where authentication is handled by Logto). That service has its own privacy policy, which applies from the moment you arrive there.
</p>
</section>