Compare commits

...

3 Commits

Author SHA1 Message Date
Admin
87b5ad1460 feat(auth): add debug-login bypass endpoint secured by DEBUG_LOGIN_TOKEN
All checks were successful
CI / Backend (push) Successful in 43s
CI / UI (push) Successful in 1m10s
Release / Check ui (push) Successful in 26s
Release / Test backend (push) Successful in 41s
CI / Backend (pull_request) Successful in 25s
Release / Docker / caddy (push) Successful in 47s
CI / UI (pull_request) Successful in 41s
Release / Docker / ui (push) Successful in 2m32s
Release / Docker / backend (push) Successful in 3m57s
Release / Docker / runner (push) Successful in 4m8s
Release / Gitea Release (push) Successful in 12s
2026-03-31 21:59:58 +05:00
Admin
168cb52ed0 fix(admin): use --color-surface for drawer bg (--color-bg was undefined)
Some checks failed
CI / Backend (push) Successful in 43s
CI / UI (push) Successful in 58s
Release / Check ui (push) Successful in 26s
Release / Test backend (push) Successful in 38s
Release / Docker / caddy (push) Successful in 45s
CI / Backend (pull_request) Successful in 26s
Release / Docker / ui (push) Failing after 10s
CI / UI (pull_request) Successful in 42s
Release / Docker / runner (push) Failing after 19s
Release / Docker / backend (push) Successful in 1m42s
Release / Gitea Release (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 18:53:58 +05:00
Admin
e1621a3ec2 fix(infra): move Redis to prod, fix LibreTranslate config loading
All checks were successful
CI / Backend (push) Successful in 46s
CI / UI (push) Successful in 52s
Release / Docker / caddy (push) Successful in 34s
Release / Test backend (push) Successful in 48s
Release / Check ui (push) Successful in 49s
CI / Backend (pull_request) Successful in 25s
CI / UI (pull_request) Successful in 56s
Release / Docker / runner (push) Successful in 1m46s
Release / Docker / ui (push) Successful in 2m30s
Release / Docker / backend (push) Successful in 2m47s
Release / Gitea Release (push) Successful in 13s
- Add Redis sidecar to prod docker-compose; backend connects locally (redis:6379)
- Caddy layer4 now proxies redis.libnovel.cc:6380 → local redis:6379 (not homelab LAN)
- Remove HOMELAB_REDIS_ADDR; homelab runner connects out to prod Redis via rediss://
- Remove local Redis from homelab runner compose; drop redis_data volume
- Fix config.Load() missing LibreTranslate section — LIBRETRANSLATE_URL was never read
2026-03-31 18:26:32 +05:00
6 changed files with 115 additions and 33 deletions

View File

@@ -58,9 +58,9 @@
# ── Redis TCP proxy via layer4 ────────────────────────────────────────────
# Exposes prod Redis over TLS for Asynq job enqueueing from the homelab runner.
# Listens on :6380 (all interfaces). TLS is terminated here using the cert
# Listens on :6380 (all interfaces). TLS is terminated here using the cert
# for redis.libnovel.cc; traffic is proxied to the local Redis sidecar.
# Requires the caddy-l4 module in the custom Caddy build.
# Requires the caddy-l4 module in the custom Caddy build.
layer4 {
:6380 {
route {
@@ -73,7 +73,7 @@
}
proxy {
upstream redis:6379
}
}
}
}
}

View File

@@ -203,6 +203,11 @@ func Load() Config {
URL: envOr("POCKET_TTS_URL", ""),
},
LibreTranslate: LibreTranslate{
URL: envOr("LIBRETRANSLATE_URL", ""),
APIKey: envOr("LIBRETRANSLATE_API_KEY", ""),
},
HTTP: HTTP{
Addr: envOr("BACKEND_HTTP_ADDR", ":8080"),
},

View File

@@ -126,6 +126,26 @@ services:
timeout: 5s
retries: 5
# ─── Redis (Asynq task queue — accessed locally by backend, remotely by homelab runner) ──
redis:
image: redis:7-alpine
restart: unless-stopped
command: >
redis-server
--appendonly yes
--requirepass "${REDIS_PASSWORD}"
# No public port — backend reaches it via internal network.
# Homelab runner reaches it via Caddy TLS proxy on :6380 → redis:6379.
expose:
- "6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
# ─── Backend API ──────────────────────────────────────────────────────────────
backend:
image: kalekber/libnovel-backend:${GIT_TAG:-latest}
@@ -151,6 +171,8 @@ services:
condition: service_healthy
valkey:
condition: service_healthy
redis:
condition: service_healthy
# No public port — all traffic is routed via Caddy.
expose:
- "8080"
@@ -164,10 +186,9 @@ services:
GLITCHTIP_DSN: "${GLITCHTIP_DSN}"
OTEL_EXPORTER_OTLP_ENDPOINT: "${OTEL_EXPORTER_OTLP_ENDPOINT}"
OTEL_SERVICE_NAME: "backend"
# Asynq task queue — backend enqueues jobs to homelab Redis via Caddy TLS proxy.
# Set to "rediss://:password@redis.libnovel.cc:6380" in Doppler prd config.
# Leave empty to fall back to PocketBase polling.
REDIS_ADDR: "${REDIS_ADDR}"
# Asynq task queue — backend enqueues jobs to local Redis sidecar.
# Homelab runner connects to the same Redis via Caddy TLS proxy on :6380.
REDIS_ADDR: "redis:6379"
REDIS_PASSWORD: "${REDIS_PASSWORD}"
healthcheck:
test: ["CMD", "/healthcheck", "http://localhost:8080/health"]
@@ -269,6 +290,7 @@ services:
POCKETBASE_ADMIN_EMAIL: "${POCKETBASE_ADMIN_EMAIL}"
POCKETBASE_ADMIN_PASSWORD: "${POCKETBASE_ADMIN_PASSWORD}"
AUTH_SECRET: "${AUTH_SECRET}"
DEBUG_LOGIN_TOKEN: "${DEBUG_LOGIN_TOKEN}"
PUBLIC_MINIO_PUBLIC_URL: "${MINIO_PUBLIC_ENDPOINT}"
# Valkey
VALKEY_ADDR: "valkey:6379"
@@ -382,12 +404,10 @@ services:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3 (QUIC)
- "6380:6380" # Redis TCP proxy (TLS) for homelab → Asynq
- "6380:6380" # Redis TCP proxy (TLS) for homelab runner → Asynq
environment:
DOMAIN: "${DOMAIN}"
CADDY_ACME_EMAIL: "${CADDY_ACME_EMAIL}"
# Homelab Redis address — Caddy TCP-proxies inbound :6380 to this.
HOMELAB_REDIS_ADDR: "${HOMELAB_REDIS_ADDR:?HOMELAB_REDIS_ADDR required for Redis TCP proxy}"
env_file:
- path: ./crowdsec/.crowdsec.env
required: false
@@ -421,6 +441,7 @@ volumes:
pb_data:
meili_data:
valkey_data:
redis_data:
caddy_data:
caddy_config:
caddy_logs:

View File

@@ -11,25 +11,10 @@
# - MEILI_URL → https://search.libnovel.cc (Caddy-proxied)
# - VALKEY_ADDR → unset (not exposed publicly)
# - RUNNER_SKIP_INITIAL_CATALOGUE_REFRESH=true
# - Redis service for Asynq task queue (local to homelab, exposed to prod via Caddy TCP proxy)
# - REDIS_ADDR → rediss://redis.libnovel.cc:6380 (prod Redis via Caddy TLS proxy)
# - LibreTranslate service for machine translation (internal network only)
services:
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis_data:/data
command: >
redis-server
--appendonly yes
--requirepass "${REDIS_PASSWORD}"
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
libretranslate:
image: libretranslate/libretranslate:latest
restart: unless-stopped
@@ -55,8 +40,6 @@ services:
restart: unless-stopped
stop_grace_period: 135s
depends_on:
redis:
condition: service_healthy
libretranslate:
condition: service_healthy
environment:
@@ -91,9 +74,10 @@ services:
LIBRETRANSLATE_URL: "http://libretranslate:5000"
LIBRETRANSLATE_API_KEY: "${LIBRETRANSLATE_API_KEY}"
# ── Asynq / Redis (local service) ───────────────────────────────────────
# The runner connects to the local Redis sidecar.
REDIS_ADDR: "redis:6379"
# ── Asynq / Redis (prod Redis via Caddy TLS proxy) ──────────────────────
# The runner connects to prod Redis over TLS: rediss://redis.libnovel.cc:6380.
# Caddy on prod terminates TLS and proxies to the local redis:6379 sidecar.
REDIS_ADDR: "${REDIS_ADDR}"
REDIS_PASSWORD: "${REDIS_PASSWORD}"
# ── Runner tuning ───────────────────────────────────────────────────────
@@ -117,6 +101,5 @@ services:
retries: 3
volumes:
redis_data:
libretranslate_models:
libretranslate_db:

View File

@@ -40,7 +40,7 @@
<aside
class="
fixed top-0 left-0 h-full z-50 w-56 shrink-0 border-r border-(--color-border) px-3 py-6 flex flex-col gap-6
bg-(--color-bg) transition-transform duration-200
bg-(--color-surface) transition-transform duration-200
{sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
md:relative md:translate-x-0 md:w-48 md:z-auto md:top-auto md:h-auto
"

View File

@@ -0,0 +1,73 @@
import { redirect, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getUserByUsername, createUserSession } from '$lib/server/pocketbase';
import { createAuthToken } from '../../../../hooks.server';
import { env } from '$env/dynamic/private';
import { log } from '$lib/server/logger';
import { randomBytes } from 'node:crypto';
const AUTH_COOKIE = 'libnovel_auth';
const ONE_YEAR = 60 * 60 * 24 * 365;
/**
* GET /api/auth/debug-login?token=<DEBUG_LOGIN_TOKEN>&username=<username>
*
* One-shot debug bypass: verifies a shared secret token, then mints a real
* auth cookie for the given user (defaults to the first admin account) and
* redirects to /.
*
* Requires DEBUG_LOGIN_TOKEN env var to be set. Disabled (404) when the var
* is absent or empty.
*/
export const GET: RequestHandler = async ({ url, cookies, request }) => {
const debugToken = env.DEBUG_LOGIN_TOKEN ?? '';
if (!debugToken) {
error(404, 'Not found');
}
const provided = url.searchParams.get('token') ?? '';
// Constant-time comparison to prevent timing attacks
if (provided.length !== debugToken.length) {
log.warn('api/auth/debug-login', 'bad token attempt');
error(401, 'Invalid token');
}
let diff = 0;
for (let i = 0; i < debugToken.length; i++) {
diff |= provided.charCodeAt(i) ^ debugToken.charCodeAt(i);
}
if (diff !== 0) {
log.warn('api/auth/debug-login', 'bad token attempt');
error(401, 'Invalid token');
}
const username = url.searchParams.get('username') ?? 'kamil_alekber_2e99';
const user = await getUserByUsername(username);
if (!user) {
error(404, `User '${username}' not found`);
}
const authSessionId = randomBytes(16).toString('hex');
const userAgent = request.headers.get('user-agent') ?? '';
const ip =
request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ??
request.headers.get('x-real-ip') ??
'debug';
createUserSession(user.id, authSessionId, userAgent, ip).catch((e) =>
log.warn('api/auth/debug-login', 'createUserSession failed (non-fatal)', { err: String(e) })
);
const token = createAuthToken(user.id, user.username, user.role ?? 'user', authSessionId);
cookies.set(AUTH_COOKIE, token, {
path: '/',
httpOnly: true,
sameSite: 'lax',
maxAge: ONE_YEAR
});
log.info('api/auth/debug-login', 'debug login used', { username: user.username, ip });
const next = url.searchParams.get('next') ?? '/';
redirect(302, next);
};