mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
- 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>
155 lines
4.1 KiB
JavaScript
155 lines
4.1 KiB
JavaScript
import { betterAuth } from 'better-auth';
|
|
import { Pool } from 'pg';
|
|
import { PostgresDialect } from 'kysely';
|
|
import { getMigrations } from 'better-auth/db/migration';
|
|
|
|
const env = process.env;
|
|
const defaultBackendURL = env.BACKEND_URL?.trim() || 'http://localhost:8082';
|
|
|
|
const asBool = (value, fallback = false) => {
|
|
if (value === undefined || value === null || value === '') return fallback;
|
|
return ['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase());
|
|
};
|
|
|
|
const splitCsv = (value) =>
|
|
String(value || '')
|
|
.split(',')
|
|
.map((item) => item.trim())
|
|
.filter(Boolean);
|
|
|
|
const buildPoolConfig = () => {
|
|
const host = env.DB_HOST?.trim();
|
|
const user = env.DB_USER?.trim();
|
|
const password = env.DB_PASSWORD ?? '';
|
|
const database = env.DB_NAME?.trim();
|
|
|
|
if (host && user && database) {
|
|
return {
|
|
host,
|
|
port: Number.parseInt(env.DB_PORT || '5432', 10),
|
|
user,
|
|
password,
|
|
database,
|
|
};
|
|
}
|
|
|
|
return {
|
|
connectionString: env.DATABASE_URL,
|
|
};
|
|
};
|
|
|
|
const pool = new Pool({
|
|
...buildPoolConfig(),
|
|
});
|
|
|
|
const database = new PostgresDialect({
|
|
pool,
|
|
});
|
|
|
|
const realCredential = (value) => {
|
|
const trimmed = (value || '').trim();
|
|
return trimmed && !trimmed.startsWith('PLACEHOLDER_') ? trimmed : '';
|
|
};
|
|
|
|
const githubClientId = realCredential(env.GITHUB_CLIENT_ID);
|
|
const githubClientSecret = realCredential(env.GITHUB_CLIENT_SECRET);
|
|
const googleClientId = realCredential(env.GOOGLE_CLIENT_ID);
|
|
const googleClientSecret = realCredential(env.GOOGLE_CLIENT_SECRET);
|
|
|
|
const socialProviders = {};
|
|
if (githubClientId && githubClientSecret) {
|
|
socialProviders.github = {
|
|
clientId: githubClientId,
|
|
clientSecret: githubClientSecret,
|
|
disableImplicitSignUp: true,
|
|
};
|
|
}
|
|
if (googleClientId && googleClientSecret) {
|
|
socialProviders.google = {
|
|
clientId: googleClientId,
|
|
clientSecret: googleClientSecret,
|
|
disableImplicitSignUp: true,
|
|
};
|
|
}
|
|
|
|
const staticTrustedOrigins = [
|
|
...splitCsv(env.BETTER_AUTH_TRUSTED_ORIGINS),
|
|
env.FRONTEND_URL,
|
|
env.BACKEND_URL,
|
|
env.BETTER_AUTH_URL,
|
|
defaultBackendURL,
|
|
'http://localhost:3000',
|
|
'http://localhost:8082',
|
|
'http://localhost:3001',
|
|
'http://localhost:5173',
|
|
'http://localhost:5174',
|
|
'http://localhost:5175',
|
|
'http://127.0.0.1:5173',
|
|
'http://127.0.0.1:5174',
|
|
'http://127.0.0.1:5175',
|
|
]
|
|
.map((origin) => (origin || '').trim())
|
|
.filter(Boolean);
|
|
|
|
// Self-hosted installs are reached over arbitrary hosts/IPs, so the static
|
|
// list above cannot cover them. Trust an Origin only when it matches the
|
|
// request's own Host header - that is genuine same-origin traffic (browsers
|
|
// cannot forge either header cross-origin). Do NOT consult x-forwarded-host
|
|
// here: clients can set it freely.
|
|
const resolveTrustedOrigins = (request) => {
|
|
const origins = new Set(staticTrustedOrigins);
|
|
const host = request?.headers?.get('host');
|
|
const origin = request?.headers?.get('origin');
|
|
if (host && origin) {
|
|
try {
|
|
if (new URL(origin).host === host) {
|
|
origins.add(origin);
|
|
}
|
|
} catch {
|
|
// malformed Origin header - leave untrusted
|
|
}
|
|
}
|
|
return [...origins];
|
|
};
|
|
|
|
export const auth = betterAuth({
|
|
appName: env.BETTER_AUTH_APP_NAME || 'Containr',
|
|
baseURL: env.BETTER_AUTH_URL || defaultBackendURL,
|
|
basePath: '/api/auth',
|
|
secret: env.BETTER_AUTH_SECRET || 'PLACEHOLDER_BETTER_AUTH_SECRET_CHANGE_ME_32CHARS_MIN',
|
|
trustedOrigins: resolveTrustedOrigins,
|
|
database,
|
|
user: {
|
|
modelName: 'auth_users',
|
|
},
|
|
session: {
|
|
modelName: 'auth_sessions',
|
|
},
|
|
account: {
|
|
modelName: 'auth_accounts',
|
|
},
|
|
verification: {
|
|
modelName: 'auth_verifications',
|
|
},
|
|
rateLimit: {
|
|
modelName: 'auth_rate_limits',
|
|
enabled: asBool(env.BETTER_AUTH_RATE_LIMIT_ENABLED, false),
|
|
},
|
|
emailAndPassword: {
|
|
enabled: true,
|
|
minPasswordLength: 8,
|
|
maxPasswordLength: 128,
|
|
},
|
|
socialProviders: Object.keys(socialProviders).length > 0 ? socialProviders : undefined,
|
|
});
|
|
|
|
export async function runAuthMigrations() {
|
|
const { runMigrations } = await getMigrations(auth.options);
|
|
|
|
await runMigrations();
|
|
}
|
|
|
|
export async function closeAuthDatabase() {
|
|
await pool.end();
|
|
}
|