feat: swap Redis for Dragonfly, fix remote installs, self-hosted email-only auth

- Replace redis:7-alpine with Dragonfly (Redis-protocol compatible; go-redis
  client unchanged) in root, infra, and backend-dev compose files. Bounded
  defaults --proactor_threads/--maxmemory so it boots on small VPSs.
- Frontend now defaults to same-origin API calls; nginx proxies /api/* to the
  backend (WebSocket-aware, docker DNS resolver for upstream re-resolution).
  Removes the baked localhost:8082 that broke installs reached by IP/domain.
- /auth/bootstrap returns configured OAuth providers; the UI hides
  GitHub/Google buttons unless real credentials exist. Self-hosted stays
  email/password only - OAuth reserved for a future managed offering.
- Sidecar ignores PLACEHOLDER_* OAuth credentials and trusts same-host
  origins dynamically (Origin host == request Host) so remote installs pass
  origin checks without config; cross-origin requests still rejected.
- install.sh writes ENVIRONMENT=production; COOKIE_SECURE=false in
  production is now a startup warning instead of fatal (plain-HTTP installs).
- Bootstrap failure surfaces an error notice instead of silently rendering
  the sign-in page.

Authored By: TDvorak <[email protected]>

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-21 17:24:43 +02:00
1 parent 9ba2bd06c5
commit 060038cb6f
21 files changed
+207 -83

No files matched your search

+2 -1
View File
@@ -348,7 +348,8 @@ type ProjectsResponse200 = paths['/projects']['get']['responses'][200]['content'
type JsonLike = Record<string, unknown>;
const rawBase = (import.meta.env.VITE_API_URL as string | undefined) ?? 'http://localhost:8082';
const configuredBase = ((import.meta.env.VITE_API_URL as string | undefined) ?? '').trim();
const rawBase = configuredBase || window.location.origin;
const normalizedBase = rawBase.replace(/\/$/, '');
const API_ROOT = normalizedBase.replace(/\/api\/v1$/, '').replace(/\/api$/, '');
const API_BASE = /\/api\/v1$/.test(normalizedBase) ? normalizedBase : `${normalizedBase}/api/v1`;
+10 -3
View File
@@ -20,6 +20,7 @@ export type AuthBootstrap = {
hasUsers: boolean;
userCount: number;
mode: 'register' | 'login';
providers: string[];
};
export class AuthError extends Error {
@@ -32,15 +33,16 @@ export class AuthError extends Error {
}
}
const rawAuthBase = (import.meta.env.VITE_AUTH_URL as string | undefined) ?? 'http://localhost:8082/api/auth';
const AUTH_BASE = rawAuthBase.replace(/\/$/, '');
const configuredAuthBase = ((import.meta.env.VITE_AUTH_URL as string | undefined) ?? '').trim();
const AUTH_BASE = (configuredAuthBase || `${window.location.origin}/api/auth`).replace(/\/$/, '');
export function getAuthBaseUrl(): string {
return AUTH_BASE;
}
export async function getAuthBootstrap(): Promise<AuthBootstrap> {
const apiBase = ((import.meta.env.VITE_API_URL as string | undefined) ?? 'http://localhost:8082').replace(/\/$/, '');
const configuredApiBase = ((import.meta.env.VITE_API_URL as string | undefined) ?? '').trim();
const apiBase = (configuredApiBase || window.location.origin).replace(/\/$/, '');
const normalizedApiBase = /\/api\/v1$/.test(apiBase) ? apiBase : `${apiBase}/api/v1`;
const response = await fetch(`${normalizedApiBase}/auth/bootstrap`, {
method: 'GET',
@@ -56,10 +58,15 @@ export async function getAuthBootstrap(): Promise<AuthBootstrap> {
throw new AuthError(message, response.status);
}
const providers = Array.isArray(payload?.providers)
? payload.providers.filter((provider): provider is string => typeof provider === 'string')
: [];
return {
hasUsers: Boolean(payload?.has_users),
userCount: typeof payload?.user_count === 'number' ? payload.user_count : 0,
mode: payload?.mode === 'register' ? 'register' : 'login',
providers,
};
}