feat: rebrand Hemmelig to paste.es for cloudhost.es
- Set Spanish as default language with ephemeral/encrypted privacy focus - Translate all user-facing strings and legal pages to Spanish - Replace Norwegian flag with Spanish flag in footer - Remove Hemmelig/terces.cloud links, add cloudhost.es sponsorship - Rewrite PrivacyPage: zero data collection, ephemeral design emphasis - Rewrite TermsPage: Spanish law, RGPD, paste.es/CloudHost.es references - Update PWA manifest, HTML meta tags, package.json branding - Rename webhook headers to X-Paste-Event / X-Paste-Signature - Update API docs title and contact to paste.es / cloudhost.es Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
255
api/config.ts
Normal file
255
api/config.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import dlv from 'dlv';
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
// Helper to parse boolean from env, returns undefined if not set
|
||||
const parseBoolean = (value: string | undefined): boolean | undefined => {
|
||||
if (value === undefined || value === null || value === '') return undefined;
|
||||
return value.toLowerCase() === 'true';
|
||||
};
|
||||
|
||||
// Helper to parse integer from env, returns undefined if not set
|
||||
const parseInteger = (value: string | undefined): number | undefined => {
|
||||
if (value === undefined || value === null || value === '') return undefined;
|
||||
const parsed = parseInt(value, 10);
|
||||
return isNaN(parsed) ? undefined : parsed;
|
||||
};
|
||||
|
||||
// Social provider configuration type
|
||||
export interface SocialProviderConfig {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
tenantId?: string; // For Microsoft/Azure AD
|
||||
issuer?: string; // For self-hosted instances (e.g., GitLab)
|
||||
}
|
||||
|
||||
// Generic OAuth provider configuration type (for better-auth genericOAuth plugin)
|
||||
export interface GenericOAuthProviderConfig {
|
||||
providerId: string;
|
||||
discoveryUrl?: string;
|
||||
authorizationUrl?: string;
|
||||
tokenUrl?: string;
|
||||
userInfoUrl?: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
scopes?: string[];
|
||||
pkce?: boolean;
|
||||
}
|
||||
|
||||
// Build social providers config dynamically from env vars
|
||||
const buildSocialProviders = () => {
|
||||
const providers: Record<string, SocialProviderConfig> = {};
|
||||
|
||||
// GitHub
|
||||
if (process.env.HEMMELIG_AUTH_GITHUB_ID && process.env.HEMMELIG_AUTH_GITHUB_SECRET) {
|
||||
providers.github = {
|
||||
clientId: process.env.HEMMELIG_AUTH_GITHUB_ID,
|
||||
clientSecret: process.env.HEMMELIG_AUTH_GITHUB_SECRET,
|
||||
};
|
||||
}
|
||||
|
||||
// Google
|
||||
if (process.env.HEMMELIG_AUTH_GOOGLE_ID && process.env.HEMMELIG_AUTH_GOOGLE_SECRET) {
|
||||
providers.google = {
|
||||
clientId: process.env.HEMMELIG_AUTH_GOOGLE_ID,
|
||||
clientSecret: process.env.HEMMELIG_AUTH_GOOGLE_SECRET,
|
||||
};
|
||||
}
|
||||
|
||||
// Microsoft (Azure AD)
|
||||
if (process.env.HEMMELIG_AUTH_MICROSOFT_ID && process.env.HEMMELIG_AUTH_MICROSOFT_SECRET) {
|
||||
providers.microsoft = {
|
||||
clientId: process.env.HEMMELIG_AUTH_MICROSOFT_ID,
|
||||
clientSecret: process.env.HEMMELIG_AUTH_MICROSOFT_SECRET,
|
||||
tenantId: process.env.HEMMELIG_AUTH_MICROSOFT_TENANT_ID,
|
||||
};
|
||||
}
|
||||
|
||||
// Discord
|
||||
if (process.env.HEMMELIG_AUTH_DISCORD_ID && process.env.HEMMELIG_AUTH_DISCORD_SECRET) {
|
||||
providers.discord = {
|
||||
clientId: process.env.HEMMELIG_AUTH_DISCORD_ID,
|
||||
clientSecret: process.env.HEMMELIG_AUTH_DISCORD_SECRET,
|
||||
};
|
||||
}
|
||||
|
||||
// GitLab
|
||||
if (process.env.HEMMELIG_AUTH_GITLAB_ID && process.env.HEMMELIG_AUTH_GITLAB_SECRET) {
|
||||
providers.gitlab = {
|
||||
clientId: process.env.HEMMELIG_AUTH_GITLAB_ID,
|
||||
clientSecret: process.env.HEMMELIG_AUTH_GITLAB_SECRET,
|
||||
issuer: process.env.HEMMELIG_AUTH_GITLAB_ISSUER,
|
||||
};
|
||||
}
|
||||
|
||||
// Apple
|
||||
if (process.env.HEMMELIG_AUTH_APPLE_ID && process.env.HEMMELIG_AUTH_APPLE_SECRET) {
|
||||
providers.apple = {
|
||||
clientId: process.env.HEMMELIG_AUTH_APPLE_ID,
|
||||
clientSecret: process.env.HEMMELIG_AUTH_APPLE_SECRET,
|
||||
};
|
||||
}
|
||||
|
||||
// Twitter/X
|
||||
if (process.env.HEMMELIG_AUTH_TWITTER_ID && process.env.HEMMELIG_AUTH_TWITTER_SECRET) {
|
||||
providers.twitter = {
|
||||
clientId: process.env.HEMMELIG_AUTH_TWITTER_ID,
|
||||
clientSecret: process.env.HEMMELIG_AUTH_TWITTER_SECRET,
|
||||
};
|
||||
}
|
||||
|
||||
return providers;
|
||||
};
|
||||
|
||||
// Build generic OAuth providers from JSON env var
|
||||
const buildGenericOAuthProviders = (): GenericOAuthProviderConfig[] => {
|
||||
const genericOAuthEnv = process.env.HEMMELIG_AUTH_GENERIC_OAUTH;
|
||||
|
||||
if (!genericOAuthEnv) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(genericOAuthEnv);
|
||||
if (!Array.isArray(parsed)) {
|
||||
console.error('HEMMELIG_AUTH_GENERIC_OAUTH must be a JSON array');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Validate each provider config
|
||||
return parsed.filter((provider: any) => {
|
||||
if (!provider.providerId || !provider.clientId || !provider.clientSecret) {
|
||||
console.error(
|
||||
`Invalid generic OAuth provider config: missing required fields (providerId, clientId, or clientSecret)`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have either discoveryUrl OR all three URLs (authorization, token, userInfo)
|
||||
const hasDiscoveryUrl = !!provider.discoveryUrl;
|
||||
const hasManualUrls = !!(
|
||||
provider.authorizationUrl &&
|
||||
provider.tokenUrl &&
|
||||
provider.userInfoUrl
|
||||
);
|
||||
|
||||
if (!hasDiscoveryUrl && !hasManualUrls) {
|
||||
console.error(
|
||||
`Invalid generic OAuth provider config for "${provider.providerId}": must provide either discoveryUrl OR all of (authorizationUrl, tokenUrl, userInfoUrl)`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}) as GenericOAuthProviderConfig[];
|
||||
} catch (error) {
|
||||
console.error('Failed to parse HEMMELIG_AUTH_GENERIC_OAUTH:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const socialProviders = buildSocialProviders();
|
||||
const genericOAuthProviders = buildGenericOAuthProviders();
|
||||
|
||||
// Managed mode: all settings are controlled via environment variables
|
||||
const isManaged = parseBoolean(process.env.HEMMELIG_MANAGED) ?? false;
|
||||
|
||||
// Managed mode settings (only used when HEMMELIG_MANAGED=true)
|
||||
const managedSettings = isManaged
|
||||
? {
|
||||
// General settings
|
||||
instanceName: process.env.HEMMELIG_INSTANCE_NAME ?? '',
|
||||
instanceDescription: process.env.HEMMELIG_INSTANCE_DESCRIPTION ?? '',
|
||||
instanceLogo: process.env.HEMMELIG_INSTANCE_LOGO ?? '',
|
||||
allowRegistration: parseBoolean(process.env.HEMMELIG_ALLOW_REGISTRATION) ?? true,
|
||||
requireEmailVerification:
|
||||
parseBoolean(process.env.HEMMELIG_REQUIRE_EMAIL_VERIFICATION) ?? false,
|
||||
defaultSecretExpiration:
|
||||
parseInteger(process.env.HEMMELIG_DEFAULT_SECRET_EXPIRATION) ?? 72,
|
||||
maxSecretSize: parseInteger(process.env.HEMMELIG_MAX_SECRET_SIZE) ?? 1024,
|
||||
importantMessage: process.env.HEMMELIG_IMPORTANT_MESSAGE ?? '',
|
||||
|
||||
// Security settings
|
||||
allowPasswordProtection:
|
||||
parseBoolean(process.env.HEMMELIG_ALLOW_PASSWORD_PROTECTION) ?? true,
|
||||
allowIpRestriction: parseBoolean(process.env.HEMMELIG_ALLOW_IP_RESTRICTION) ?? true,
|
||||
enableRateLimiting: parseBoolean(process.env.HEMMELIG_ENABLE_RATE_LIMITING) ?? true,
|
||||
rateLimitRequests: parseInteger(process.env.HEMMELIG_RATE_LIMIT_REQUESTS) ?? 100,
|
||||
rateLimitWindow: parseInteger(process.env.HEMMELIG_RATE_LIMIT_WINDOW) ?? 60,
|
||||
|
||||
// Organization settings
|
||||
requireInviteCode: parseBoolean(process.env.HEMMELIG_REQUIRE_INVITE_CODE) ?? false,
|
||||
allowedEmailDomains: process.env.HEMMELIG_ALLOWED_EMAIL_DOMAINS ?? '',
|
||||
requireRegisteredUser:
|
||||
parseBoolean(process.env.HEMMELIG_REQUIRE_REGISTERED_USER) ?? false,
|
||||
disableEmailPasswordSignup:
|
||||
parseBoolean(process.env.HEMMELIG_DISABLE_EMAIL_PASSWORD_SIGNUP) ?? false,
|
||||
|
||||
// Webhook settings
|
||||
webhookEnabled: parseBoolean(process.env.HEMMELIG_WEBHOOK_ENABLED) ?? false,
|
||||
webhookUrl: process.env.HEMMELIG_WEBHOOK_URL ?? '',
|
||||
webhookSecret: process.env.HEMMELIG_WEBHOOK_SECRET ?? '',
|
||||
webhookOnView: parseBoolean(process.env.HEMMELIG_WEBHOOK_ON_VIEW) ?? true,
|
||||
webhookOnBurn: parseBoolean(process.env.HEMMELIG_WEBHOOK_ON_BURN) ?? true,
|
||||
|
||||
// Metrics settings
|
||||
metricsEnabled: parseBoolean(process.env.HEMMELIG_METRICS_ENABLED) ?? false,
|
||||
metricsSecret: process.env.HEMMELIG_METRICS_SECRET ?? '',
|
||||
|
||||
// File upload settings
|
||||
allowFileUploads: parseBoolean(process.env.HEMMELIG_ALLOW_FILE_UPLOADS) ?? true,
|
||||
}
|
||||
: null;
|
||||
|
||||
const config = {
|
||||
server: {
|
||||
port: Number(process.env.HEMMELIG_PORT) || 3000,
|
||||
},
|
||||
trustedOrigins: [
|
||||
...(!isProduction ? ['http://localhost:5173'] : []),
|
||||
process.env.HEMMELIG_TRUSTED_ORIGIN || '',
|
||||
].filter(Boolean),
|
||||
general: {
|
||||
instanceName: process.env.HEMMELIG_INSTANCE_NAME,
|
||||
instanceDescription: process.env.HEMMELIG_INSTANCE_DESCRIPTION,
|
||||
instanceLogo: process.env.HEMMELIG_INSTANCE_LOGO,
|
||||
allowRegistration: parseBoolean(process.env.HEMMELIG_ALLOW_REGISTRATION),
|
||||
},
|
||||
security: {
|
||||
allowPasswordProtection: parseBoolean(process.env.HEMMELIG_ALLOW_PASSWORD_PROTECTION),
|
||||
allowIpRestriction: parseBoolean(process.env.HEMMELIG_ALLOW_IP_RESTRICTION),
|
||||
},
|
||||
analytics: {
|
||||
enabled: parseBoolean(process.env.HEMMELIG_ANALYTICS_ENABLED) ?? true,
|
||||
hmacSecret:
|
||||
process.env.HEMMELIG_ANALYTICS_HMAC_SECRET || 'default-analytics-secret-change-me',
|
||||
},
|
||||
socialProviders,
|
||||
};
|
||||
|
||||
if (!process.env.HEMMELIG_ANALYTICS_HMAC_SECRET && config.analytics.enabled) {
|
||||
console.warn(
|
||||
'WARNING: HEMMELIG_ANALYTICS_HMAC_SECRET is not set. Analytics visitor IDs are generated ' +
|
||||
'with a default secret, making them predictable. Set a random secret for production use.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A type-safe utility to get a value from the configuration.
|
||||
* Its return type is inferred from the type of the default value.
|
||||
* @param path The dot-notation path to the config value (e.g., 'server.port').
|
||||
* @param defaultValue A default value to return if the path is not found.
|
||||
* @returns The found configuration value or the default value.
|
||||
*/
|
||||
function get<T>(path: string, defaultValue?: T): T {
|
||||
return dlv(config, path, defaultValue) as T;
|
||||
}
|
||||
|
||||
// Export the get function and social providers helper
|
||||
export default {
|
||||
get,
|
||||
getSocialProviders: () => config.socialProviders,
|
||||
getGenericOAuthProviders: () => genericOAuthProviders,
|
||||
isManaged: () => isManaged,
|
||||
getManagedSettings: () => managedSettings,
|
||||
};
|
||||
Reference in New Issue
Block a user