diff --git a/src/config/auth.test.ts b/src/config/auth.test.ts new file mode 100644 index 0000000..b1b08c8 --- /dev/null +++ b/src/config/auth.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { resolveAuthConfig } from './auth'; + +describe('resolveAuthConfig', () => { + it('returns both URLs and sales email from env', () => { + const cfg = resolveAuthConfig({ + PUBLIC_AUTH_SIGNIN_URL: 'https://auth.cameleer.io/sign-in', + PUBLIC_AUTH_SIGNUP_URL: 'https://auth.cameleer.io/sign-in?first_screen=register', + PUBLIC_SALES_EMAIL: 'sales@cameleer.io', + }); + expect(cfg.signInUrl).toBe('https://auth.cameleer.io/sign-in'); + expect(cfg.signUpUrl).toBe('https://auth.cameleer.io/sign-in?first_screen=register'); + expect(cfg.salesEmail).toBe('sales@cameleer.io'); + }); + + it('throws if PUBLIC_AUTH_SIGNIN_URL is missing', () => { + expect(() => resolveAuthConfig({ + PUBLIC_AUTH_SIGNUP_URL: 'https://auth.cameleer.io/sign-in?first_screen=register', + PUBLIC_SALES_EMAIL: 'sales@cameleer.io', + })).toThrow(/PUBLIC_AUTH_SIGNIN_URL/); + }); + + it('throws if a URL is not https', () => { + expect(() => resolveAuthConfig({ + PUBLIC_AUTH_SIGNIN_URL: 'http://auth.cameleer.io/sign-in', + PUBLIC_AUTH_SIGNUP_URL: 'https://auth.cameleer.io/sign-in?first_screen=register', + PUBLIC_SALES_EMAIL: 'sales@cameleer.io', + })).toThrow(/must be https/); + }); + + it('throws if sales email is not a valid mailto target', () => { + expect(() => resolveAuthConfig({ + PUBLIC_AUTH_SIGNIN_URL: 'https://auth.cameleer.io/sign-in', + PUBLIC_AUTH_SIGNUP_URL: 'https://auth.cameleer.io/sign-in?first_screen=register', + PUBLIC_SALES_EMAIL: 'not-an-email', + })).toThrow(/PUBLIC_SALES_EMAIL/); + }); + + it('exposes signUpUrl distinct from signInUrl', () => { + const cfg = resolveAuthConfig({ + PUBLIC_AUTH_SIGNIN_URL: 'https://auth.cameleer.io/sign-in', + PUBLIC_AUTH_SIGNUP_URL: 'https://auth.cameleer.io/sign-in?first_screen=register', + PUBLIC_SALES_EMAIL: 'sales@cameleer.io', + }); + expect(cfg.signUpUrl).not.toBe(cfg.signInUrl); + }); +}); diff --git a/src/config/auth.ts b/src/config/auth.ts new file mode 100644 index 0000000..5d5bc62 --- /dev/null +++ b/src/config/auth.ts @@ -0,0 +1,56 @@ +export interface AuthConfig { + signInUrl: string; + signUpUrl: string; + salesEmail: string; + salesMailto: string; +} + +interface EnvLike { + PUBLIC_AUTH_SIGNIN_URL?: string; + PUBLIC_AUTH_SIGNUP_URL?: string; + PUBLIC_SALES_EMAIL?: string; +} + +function requireHttps(name: string, value: string | undefined): string { + if (!value) { + throw new Error(`${name} is required`); + } + if (!value.startsWith('https://')) { + throw new Error(`${name} must be https (got: ${value})`); + } + return value; +} + +function requireEmail(name: string, value: string | undefined): string { + if (!value) { + throw new Error(`${name} is required`); + } + // RFC-5322-ish minimal check — we just want to catch typos, not validate against RFC. + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { + throw new Error(`${name} must look like an email (got: ${value})`); + } + return value; +} + +export function resolveAuthConfig(env: EnvLike): AuthConfig { + const signInUrl = requireHttps('PUBLIC_AUTH_SIGNIN_URL', env.PUBLIC_AUTH_SIGNIN_URL); + const signUpUrl = requireHttps('PUBLIC_AUTH_SIGNUP_URL', env.PUBLIC_AUTH_SIGNUP_URL); + const salesEmail = requireEmail('PUBLIC_SALES_EMAIL', env.PUBLIC_SALES_EMAIL); + return { + signInUrl, + signUpUrl, + salesEmail, + salesMailto: `mailto:${salesEmail}`, + }; +} + +// Lazy accessor for Astro usage. Not evaluated at module load (so vitest can +// import this file without the PUBLIC_* env vars being set). Each call after +// the first returns the cached config. +let _cached: AuthConfig | null = null; +export function getAuthConfig(): AuthConfig { + if (_cached === null) { + _cached = resolveAuthConfig(import.meta.env as unknown as EnvLike); + } + return _cached; +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..7eeb3f8 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['src/**/*.test.ts'], + }, +});