Files
paste.es/api/auth.ts
Malin bc9f96cbd4 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>
2026-02-24 09:30:19 +01:00

177 lines
6.2 KiB
TypeScript

import { betterAuth } from 'better-auth';
import { prismaAdapter } from 'better-auth/adapters/prisma';
import { APIError } from 'better-auth/api';
import { admin, twoFactor, username } from 'better-auth/plugins';
import { genericOAuth } from 'better-auth/plugins/generic-oauth';
import { randomBytes } from 'crypto';
import config, { type SocialProviderConfig } from './config';
import prisma from './lib/db';
import { validatePassword } from './validations/password';
// Generate a unique username from email
const generateUsernameFromEmail = (email: string): string => {
const localPart = email.split('@')[0] || 'user';
// Sanitize: only keep alphanumeric characters and underscores
const sanitized = localPart.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase();
// Add random suffix to ensure uniqueness (cryptographically secure)
const randomSuffix = randomBytes(4).toString('hex').substring(0, 6);
return `${sanitized}_${randomSuffix}`;
};
// Build better-auth social providers configuration dynamically
const buildBetterAuthSocialProviders = () => {
const providers = config.getSocialProviders();
const betterAuthProviders: Record<
string,
{
clientId: string;
clientSecret: string;
tenantId?: string;
issuer?: string;
mapProfileToUser?: (profile: { email?: string; name?: string }) => { username: string };
}
> = {};
for (const [provider, providerConfig] of Object.entries(providers)) {
const typedConfig = providerConfig as SocialProviderConfig;
betterAuthProviders[provider] = {
clientId: typedConfig.clientId,
clientSecret: typedConfig.clientSecret,
...(typedConfig.tenantId && { tenantId: typedConfig.tenantId }),
...(typedConfig.issuer && { issuer: typedConfig.issuer }),
mapProfileToUser: (profile) => ({
username: generateUsernameFromEmail(profile.email || profile.name || 'user'),
}),
};
}
return betterAuthProviders;
};
// Build better-auth plugins array
const buildPlugins = () => {
const plugins: any[] = [username(), admin(), twoFactor()];
const genericProviders = config.getGenericOAuthProviders();
if (genericProviders.length > 0) {
plugins.push(
genericOAuth({
config: genericProviders.map((provider) => ({
...provider,
// Map profile to include username
mapProfileToUser: (profile: any) => ({
username: generateUsernameFromEmail(
profile.email || profile.name || 'user'
),
}),
})),
})
);
}
return plugins;
};
export const auth = betterAuth({
appName: 'Hemmelig',
baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:3000',
database: prismaAdapter(prisma, {
provider: 'sqlite',
}),
emailAndPassword: {
enabled: true,
// Set to 1 so better-auth doesn't reject weak current passwords during password change.
// Password strength for new passwords is enforced by our Zod schema (updatePasswordSchema)
// and for sign-up by the before hook below.
minPasswordLength: 1,
},
socialProviders: buildBetterAuthSocialProviders(),
account: {
accountLinking: {
enabled: true,
trustedProviders: [
'gitlab',
'github',
'google',
'microsoft',
'discord',
'apple',
'twitter',
// Add all generic OAuth provider IDs as trusted
...config.getGenericOAuthProviders().map((p) => p.providerId),
],
},
},
plugins: buildPlugins(),
trustedOrigins: config.get('trustedOrigins'),
hooks: {
before: async (context) => {
// Only apply validation to email/password sign-up
if (context.path !== '/sign-up/email') {
return;
}
const body = context.body as { email?: string; password?: string };
const email = body?.email;
const password = body?.password;
if (!email) {
return;
}
// Validate password strength for sign-up
if (password) {
const passwordError = validatePassword(password);
if (passwordError) {
throw new APIError('BAD_REQUEST', { message: passwordError });
}
}
// Get instance settings
const settings = await prisma.instanceSettings.findFirst({
select: { allowedEmailDomains: true, disableEmailPasswordSignup: true },
});
// Check if email/password signup is disabled
if (settings?.disableEmailPasswordSignup) {
throw new APIError('FORBIDDEN', {
message: 'Email/password registration is disabled. Please use social login.',
});
}
const allowedDomains = settings?.allowedEmailDomains?.trim();
// If no domains configured, allow all
if (!allowedDomains) {
return;
}
// Parse comma-separated domains
const domains = allowedDomains
.split(',')
.map((d) => d.trim().toLowerCase())
.filter((d) => d.length > 0);
if (domains.length === 0) {
return;
}
// Extract domain from email
const emailDomain = email.split('@')[1]?.toLowerCase();
if (!emailDomain || !domains.includes(emailDomain)) {
throw new APIError('FORBIDDEN', {
message: 'Email domain not allowed',
});
}
},
},
});
// Export enabled social providers for frontend consumption
export const getEnabledSocialProviders = (): string[] => {
const standardProviders = Object.keys(config.getSocialProviders());
const genericProviders = config.getGenericOAuthProviders().map((p) => p.providerId);
return [...standardProviders, ...genericProviders];
};