diff --git a/README.md b/README.md index 5a1f12b0..63861685 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,13 @@ docker run -d --name dory \ -e DORY_AI_MODEL=gpt-4o-mini \ -e DORY_AI_API_KEY=your_api_key_here \ -e DORY_AI_URL=https://api.openai.com/v1 \ + -e NEXT_PUBLIC_REQUIRE_EMAIL_VERIFICATION=false \ dorylab/dory:latest ``` +To enable email verification, set `RESEND_API_KEY` to a valid [resend](https://resend.com) key and `EMAIL_FROM` to a validated email. + ### 🧠 Supported AI Providers @@ -62,14 +65,14 @@ You can freely switch between different model vendors by changing environment va Currently supported providers: -| Provider | Env `DORY_AI_PROVIDER` | Description | -|----------|-------------------------|-------------| -| OpenAI | `openai` | Default provider. Uses official OpenAI API. | -| OpenAI-Compatible | `openai-compatible` | Any service exposing an OpenAI-compatible API. | -| Anthropic | `anthropic` | Claude models via Anthropic official API. | -| Google | `google` | Gemini models via Google Generative AI API. | -| Qwen (Alibaba) | `qwen` | Qwen models via DashScope OpenAI-compatible endpoint. | -| xAI | `xai` | Grok models via xAI API. | +| Provider | Env `DORY_AI_PROVIDER` | Description | +| ----------------- | ---------------------- | ----------------------------------------------------- | +| OpenAI | `openai` | Default provider. Uses official OpenAI API. | +| OpenAI-Compatible | `openai-compatible` | Any service exposing an OpenAI-compatible API. | +| Anthropic | `anthropic` | Claude models via Anthropic official API. | +| Google | `google` | Gemini models via Google Generative AI API. | +| Qwen (Alibaba) | `qwen` | Qwen models via DashScope OpenAI-compatible endpoint. | +| xAI | `xai` | Grok models via xAI API. | ## ✨ Key Features @@ -153,14 +156,14 @@ Native ClickHouse user and role management UI. ## 🔌 Database Support -| Database | Status | -|--------------|---------------------| -| ClickHouse | ✅ Deeply integrated | -| PostgreSQL | ✅ Supported | -| MySQL | ✅ Supported | -| MariaDB | ✅ Supported | | -| SQLite | ✅ Supported (Beta) | -| DuckDB | 🚧 Planned +| Database | Status | +| ---------- | ------------------- | +| ClickHouse | ✅ Deeply integrated | +| PostgreSQL | ✅ Supported | +| MySQL | ✅ Supported | +| MariaDB | ✅ Supported | | +| SQLite | ✅ Supported (Beta) | +| DuckDB | 🚧 Planned | --- diff --git a/apps/web/app/(auth)/components/SignUpform.tsx b/apps/web/app/(auth)/components/SignUpform.tsx index 55e2daa8..2fb5753d 100644 --- a/apps/web/app/(auth)/components/SignUpform.tsx +++ b/apps/web/app/(auth)/components/SignUpform.tsx @@ -13,6 +13,8 @@ import { VerifyEmailPanel } from './verify-email-panel'; import { Card, CardContent } from '@/registry/new-york-v4/ui/card'; import { useTranslations } from 'next-intl'; import { IconBrandGithub } from '@tabler/icons-react'; +import { parseEnvFlag } from '@/lib/env'; +import { env } from 'next-runtime-env'; type Stage = 'form' | 'verify'; @@ -23,6 +25,7 @@ type SignUpFormProps = React.ComponentProps<'div'> & { export function SignUpForm({ className, callbackURL: callbackURLOverride, onRequestSignIn, ...props }: SignUpFormProps) { const t = useTranslations('Auth'); + const requireEmailVerification = parseEnvFlag(env('NEXT_PUBLIC_REQUIRE_EMAIL_VERIFICATION')); const [loading, setLoading] = useState(false); const [err, setErr] = useState(null); const [stage, setStage] = useState('form'); @@ -55,16 +58,22 @@ export function SignUpForm({ className, callbackURL: callbackURLOverride, onRequ if (!error) { posthog.identify(email, { email, name }); posthog.capture('user_signed_up', { method: 'email' }); - setEmailForVerify(email); - setStage('verify'); + if (requireEmailVerification) { + setEmailForVerify(email); + setStage('verify'); + } else { + window.location.href = callbackURL; + } } else { // Keep user on the form for regular errors (e.g. email already exists). // Only switch for "unverified account" type errors. if (verifyMatchRegex.test(error.message ?? '')) { posthog.identify(email, { email, name }); posthog.capture('user_signed_up', { method: 'email' }); - setEmailForVerify(email); - setStage('verify'); + if (requireEmailVerification) { + setEmailForVerify(email); + setStage('verify'); + } } else { posthog.capture('user_sign_up_failed', { method: 'email', error: error.message }); } diff --git a/apps/web/lib/auth.ts b/apps/web/lib/auth.ts index ca5ba1f3..5a1ffd9f 100644 --- a/apps/web/lib/auth.ts +++ b/apps/web/lib/auth.ts @@ -12,6 +12,7 @@ import { getClient } from './database/postgres/client'; import { getDatabaseProvider } from './database/provider'; import { schema } from './database/schema'; import { sendEmail } from './email'; +import { parseEnvFlag } from './env'; import { resolveOrganizationIdForSession, shouldCreateDefaultOrganization } from './auth/migration-state'; import { createProvisionedOrganization } from './auth/organization-provisioning'; import { translate } from './i18n/i18n'; @@ -22,6 +23,8 @@ import { canManageOrganizationBilling } from './billing/authz'; import { buildDefaultOrganizationValues, linkAnonymousOrganizationToUser } from './auth/anonymous'; import { isAnonymousUser } from './auth/anonymous-user'; +const REQUIRE_EMAIL_VERIFICATION = parseEnvFlag(process.env.NEXT_PUBLIC_REQUIRE_EMAIL_VERIFICATION); + type AuthUser = { id: string; email: string | null; @@ -498,7 +501,7 @@ function createAuth() { emailAndPassword: { enabled: true, - requireEmailVerification: true, + requireEmailVerification: REQUIRE_EMAIL_VERIFICATION, autoSignInAfterVerification: true, sendResetPassword: async ({ user, url, token }, request) => { @@ -538,7 +541,7 @@ function createAuth() { }, emailVerification: { - sendOnSignUp: true, + sendOnSignUp: REQUIRE_EMAIL_VERIFICATION, sendVerificationEmail: async ({ user, url, token }, request) => { const locale = await getServerLocale(); diff --git a/apps/web/lib/email.ts b/apps/web/lib/email.ts index 791c88ee..c59a85ba 100644 --- a/apps/web/lib/email.ts +++ b/apps/web/lib/email.ts @@ -7,6 +7,8 @@ if (!RESEND_API_KEY) { } const resend = new Resend(RESEND_API_KEY); +const EMAIL_FROM = process.env.EMAIL_FROM || 'Dory'; + export async function sendEmail({ to, subject, @@ -18,8 +20,7 @@ export async function sendEmail({ text: string; html?: string; }) { - // Sender must use a verified domain/subdomain and include a friendly name - const from = 'Dory'; // Example: your verified subdomain + const from = EMAIL_FROM; logger.info(`[email] payload to=${to} subject="${subject}" html=${Boolean(html)} htmlLen=${html?.length || 0} textLen=${text?.length || 0}`); try { const { data, error } = await resend.emails.send({ diff --git a/apps/web/lib/env.ts b/apps/web/lib/env.ts new file mode 100644 index 00000000..398993d2 --- /dev/null +++ b/apps/web/lib/env.ts @@ -0,0 +1,8 @@ +/** + * Parses an environment variable as a boolean flag. + * Returns `defaultValue` when the variable is undefined; `false` when the value is the string `"false"`. + */ +export function parseEnvFlag(value: string | undefined, defaultValue = true): boolean { + if (value === undefined) return defaultValue; + return value !== 'false'; +}