This is page 54 of 67. Use http://codebase.md/better-auth/better-auth?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ └── nextjs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── (auth) │ │ │ ├── forget-password │ │ │ │ └── page.tsx │ │ │ ├── reset-password │ │ │ │ └── page.tsx │ │ │ ├── sign-in │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── two-factor │ │ │ ├── otp │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── accept-invitation │ │ │ └── [id] │ │ │ ├── invitation-error.tsx │ │ │ └── page.tsx │ │ ├── admin │ │ │ └── page.tsx │ │ ├── api │ │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ │ ├── apps │ │ │ └── register │ │ │ └── page.tsx │ │ ├── client-test │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── change-plan.tsx │ │ │ ├── client.tsx │ │ │ ├── organization-card.tsx │ │ │ ├── page.tsx │ │ │ ├── upgrade-button.tsx │ │ │ └── user-card.tsx │ │ ├── device │ │ │ ├── approve │ │ │ │ └── page.tsx │ │ │ ├── denied │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── success │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── features.tsx │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── oauth │ │ │ └── authorize │ │ │ ├── concet-buttons.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── pricing │ │ └── page.tsx │ ├── components │ │ ├── account-switch.tsx │ │ ├── blocks │ │ │ └── pricing.tsx │ │ ├── logo.tsx │ │ ├── one-tap.tsx │ │ ├── sign-in-btn.tsx │ │ ├── sign-in.tsx │ │ ├── sign-up.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ ├── tier-labels.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── chart.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── copy-button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── password-input.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tabs2.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ └── wrapper.tsx │ ├── components.json │ ├── hooks │ │ └── use-toast.ts │ ├── lib │ │ ├── auth-client.ts │ │ ├── auth-types.ts │ │ ├── auth.ts │ │ ├── email │ │ │ ├── invitation.tsx │ │ │ ├── resend.ts │ │ │ └── reset-password.tsx │ │ ├── metadata.ts │ │ ├── shared.ts │ │ └── utils.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── proxy.ts │ ├── public │ │ ├── __og.png │ │ ├── _og.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── og.png │ ├── README.md │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── docker-compose.yml ├── docs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── ai-chat │ │ │ │ └── route.ts │ │ │ ├── analytics │ │ │ │ ├── conversation │ │ │ │ │ └── route.ts │ │ │ │ ├── event │ │ │ │ │ └── route.ts │ │ │ │ └── feedback │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ ├── og-release │ │ │ │ └── route.tsx │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ └── support │ │ │ └── route.ts │ │ ├── blog │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── blog-list.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── stat-field.tsx │ │ │ │ └── support.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── changelogs │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── grid-pattern.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── stat-field.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── community │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── stats.tsx │ │ │ └── page.tsx │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── lib │ │ │ └── get-llm-text.ts │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── llms.txt │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── reference │ │ │ └── route.ts │ │ ├── sitemap.xml │ │ ├── static.json │ │ │ └── route.ts │ │ └── v1 │ │ ├── _components │ │ │ └── v1-text.tsx │ │ ├── bg-line.tsx │ │ └── page.tsx │ ├── assets │ │ ├── Geist.ttf │ │ └── GeistMono.ttf │ ├── components │ │ ├── ai-chat-modal.tsx │ │ ├── anchor-scroll-fix.tsx │ │ ├── api-method-tabs.tsx │ │ ├── api-method.tsx │ │ ├── banner.tsx │ │ ├── blocks │ │ │ └── features.tsx │ │ ├── builder │ │ │ ├── beam.tsx │ │ │ ├── code-tabs │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-tabs.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-bar.tsx │ │ │ │ └── theme.ts │ │ │ ├── index.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── social-provider.tsx │ │ │ ├── store.ts │ │ │ └── tabs.tsx │ │ ├── display-techstack.tsx │ │ ├── divider-text.tsx │ │ ├── docs │ │ │ ├── docs.client.tsx │ │ │ ├── docs.tsx │ │ │ ├── layout │ │ │ │ ├── nav.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── toc-thumb.tsx │ │ │ │ └── toc.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ ├── shared.tsx │ │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── popover.tsx │ │ │ └── scroll-area.tsx │ │ ├── endpoint.tsx │ │ ├── features.tsx │ │ ├── floating-ai-search.tsx │ │ ├── fork-button.tsx │ │ ├── generate-apple-jwt.tsx │ │ ├── generate-secret.tsx │ │ ├── github-stat.tsx │ │ ├── icons.tsx │ │ ├── landing │ │ │ ├── gradient-bg.tsx │ │ │ ├── grid-pattern.tsx │ │ │ ├── hero.tsx │ │ │ ├── section-svg.tsx │ │ │ ├── section.tsx │ │ │ ├── spotlight.tsx │ │ │ └── testimonials.tsx │ │ ├── logo-context-menu.tsx │ │ ├── logo.tsx │ │ ├── markdown-renderer.tsx │ │ ├── markdown.tsx │ │ ├── mdx │ │ │ ├── add-to-cursor.tsx │ │ │ └── database-tables.tsx │ │ ├── message-feedback.tsx │ │ ├── mobile-search-icon.tsx │ │ ├── nav-bar.tsx │ │ ├── nav-link.tsx │ │ ├── nav-mobile.tsx │ │ ├── promo-card.tsx │ │ ├── resource-card.tsx │ │ ├── resource-grid.tsx │ │ ├── resource-section.tsx │ │ ├── ripple.tsx │ │ ├── search-dialog.tsx │ │ ├── side-bar.tsx │ │ ├── sidebar-content.tsx │ │ ├── techstack-icons.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggler.tsx │ │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aside-link.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── background-beams.tsx │ │ ├── background-boxes.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── callout.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── dynamic-code-block.tsx │ │ ├── fade-in.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── sparkles.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip-docs.tsx │ │ ├── tooltip.tsx │ │ └── use-copy-button.tsx │ ├── components.json │ ├── content │ │ ├── blogs │ │ │ ├── 0-supabase-auth-to-planetscale-migration.mdx │ │ │ ├── 1-3.mdx │ │ │ ├── authjs-joins-better-auth.mdx │ │ │ └── seed-round.mdx │ │ ├── changelogs │ │ │ ├── 1-2.mdx │ │ │ └── 1.0.mdx │ │ └── docs │ │ ├── adapters │ │ │ ├── community-adapters.mdx │ │ │ ├── drizzle.mdx │ │ │ ├── mongo.mdx │ │ │ ├── mssql.mdx │ │ │ ├── mysql.mdx │ │ │ ├── other-relational-databases.mdx │ │ │ ├── postgresql.mdx │ │ │ ├── prisma.mdx │ │ │ └── sqlite.mdx │ │ ├── authentication │ │ │ ├── apple.mdx │ │ │ ├── atlassian.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── dropbox.mdx │ │ │ ├── email-password.mdx │ │ │ ├── facebook.mdx │ │ │ ├── figma.mdx │ │ │ ├── github.mdx │ │ │ ├── gitlab.mdx │ │ │ ├── google.mdx │ │ │ ├── huggingface.mdx │ │ │ ├── kakao.mdx │ │ │ ├── kick.mdx │ │ │ ├── line.mdx │ │ │ ├── linear.mdx │ │ │ ├── linkedin.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── naver.mdx │ │ │ ├── notion.mdx │ │ │ ├── other-social-providers.mdx │ │ │ ├── paypal.mdx │ │ │ ├── reddit.mdx │ │ │ ├── roblox.mdx │ │ │ ├── salesforce.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── tiktok.mdx │ │ │ ├── twitch.mdx │ │ │ ├── twitter.mdx │ │ │ ├── vk.mdx │ │ │ └── zoom.mdx │ │ ├── basic-usage.mdx │ │ ├── comparison.mdx │ │ ├── concepts │ │ │ ├── api.mdx │ │ │ ├── cli.mdx │ │ │ ├── client.mdx │ │ │ ├── cookies.mdx │ │ │ ├── database.mdx │ │ │ ├── email.mdx │ │ │ ├── hooks.mdx │ │ │ ├── oauth.mdx │ │ │ ├── plugins.mdx │ │ │ ├── rate-limit.mdx │ │ │ ├── session-management.mdx │ │ │ ├── typescript.mdx │ │ │ └── users-accounts.mdx │ │ ├── examples │ │ │ ├── astro.mdx │ │ │ ├── next-js.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ └── svelte-kit.mdx │ │ ├── guides │ │ │ ├── auth0-migration-guide.mdx │ │ │ ├── browser-extension-guide.mdx │ │ │ ├── clerk-migration-guide.mdx │ │ │ ├── create-a-db-adapter.mdx │ │ │ ├── next-auth-migration-guide.mdx │ │ │ ├── optimizing-for-performance.mdx │ │ │ ├── saml-sso-with-okta.mdx │ │ │ ├── supabase-migration-guide.mdx │ │ │ └── your-first-plugin.mdx │ │ ├── installation.mdx │ │ ├── integrations │ │ │ ├── astro.mdx │ │ │ ├── convex.mdx │ │ │ ├── elysia.mdx │ │ │ ├── expo.mdx │ │ │ ├── express.mdx │ │ │ ├── fastify.mdx │ │ │ ├── hono.mdx │ │ │ ├── lynx.mdx │ │ │ ├── nestjs.mdx │ │ │ ├── next.mdx │ │ │ ├── nitro.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ ├── solid-start.mdx │ │ │ ├── svelte-kit.mdx │ │ │ ├── tanstack.mdx │ │ │ └── waku.mdx │ │ ├── introduction.mdx │ │ ├── meta.json │ │ ├── plugins │ │ │ ├── 2fa.mdx │ │ │ ├── admin.mdx │ │ │ ├── anonymous.mdx │ │ │ ├── api-key.mdx │ │ │ ├── autumn.mdx │ │ │ ├── bearer.mdx │ │ │ ├── captcha.mdx │ │ │ ├── community-plugins.mdx │ │ │ ├── device-authorization.mdx │ │ │ ├── dodopayments.mdx │ │ │ ├── dub.mdx │ │ │ ├── email-otp.mdx │ │ │ ├── generic-oauth.mdx │ │ │ ├── have-i-been-pwned.mdx │ │ │ ├── jwt.mdx │ │ │ ├── last-login-method.mdx │ │ │ ├── magic-link.mdx │ │ │ ├── mcp.mdx │ │ │ ├── multi-session.mdx │ │ │ ├── oauth-proxy.mdx │ │ │ ├── oidc-provider.mdx │ │ │ ├── one-tap.mdx │ │ │ ├── one-time-token.mdx │ │ │ ├── open-api.mdx │ │ │ ├── organization.mdx │ │ │ ├── passkey.mdx │ │ │ ├── phone-number.mdx │ │ │ ├── polar.mdx │ │ │ ├── siwe.mdx │ │ │ ├── sso.mdx │ │ │ ├── stripe.mdx │ │ │ └── username.mdx │ │ └── reference │ │ ├── contributing.mdx │ │ ├── faq.mdx │ │ ├── options.mdx │ │ ├── resources.mdx │ │ ├── security.mdx │ │ └── telemetry.mdx │ ├── hooks │ │ └── use-mobile.ts │ ├── ignore-build.sh │ ├── lib │ │ ├── blog.ts │ │ ├── chat │ │ │ └── inkeep-qa-schema.ts │ │ ├── constants.ts │ │ ├── export-search-indexes.ts │ │ ├── inkeep-analytics.ts │ │ ├── is-active.ts │ │ ├── metadata.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── proxy.ts │ ├── public │ │ ├── avatars │ │ │ └── beka.jpg │ │ ├── blogs │ │ │ ├── authjs-joins.png │ │ │ ├── seed-round.png │ │ │ └── supabase-ps.png │ │ ├── branding │ │ │ ├── better-auth-brand-assets.zip │ │ │ ├── better-auth-logo-dark.png │ │ │ ├── better-auth-logo-dark.svg │ │ │ ├── better-auth-logo-light.png │ │ │ ├── better-auth-logo-light.svg │ │ │ ├── better-auth-logo-wordmark-dark.png │ │ │ ├── better-auth-logo-wordmark-dark.svg │ │ │ ├── better-auth-logo-wordmark-light.png │ │ │ └── better-auth-logo-wordmark-light.svg │ │ ├── extension-id.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── images │ │ │ └── blogs │ │ │ └── better auth (1).png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── LogoDark.webp │ │ ├── LogoLight.webp │ │ ├── og.png │ │ ├── open-api-reference.png │ │ ├── people-say │ │ │ ├── code-with-antonio.jpg │ │ │ ├── dagmawi-babi.png │ │ │ ├── dax.png │ │ │ ├── dev-ed.png │ │ │ ├── egoist.png │ │ │ ├── guillermo-rauch.png │ │ │ ├── jonathan-wilke.png │ │ │ ├── josh-tried-coding.jpg │ │ │ ├── kitze.jpg │ │ │ ├── lazar-nikolov.png │ │ │ ├── nizzy.png │ │ │ ├── omar-mcadam.png │ │ │ ├── ryan-vogel.jpg │ │ │ ├── saltyatom.jpg │ │ │ ├── sebastien-chopin.png │ │ │ ├── shreyas-mididoddi.png │ │ │ ├── tech-nerd.png │ │ │ ├── theo.png │ │ │ ├── vybhav-bhargav.png │ │ │ └── xavier-pladevall.jpg │ │ ├── plus.svg │ │ ├── release-og │ │ │ ├── 1-2.png │ │ │ ├── 1-3.png │ │ │ └── changelog-og.png │ │ └── v1-og.png │ ├── README.md │ ├── scripts │ │ ├── endpoint-to-doc │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── output.mdx │ │ │ └── readme.md │ │ └── sync-orama.ts │ ├── source.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── turbo.json ├── e2e │ ├── integration │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── solid-vinxi │ │ │ ├── .gitignore │ │ │ ├── app.config.ts │ │ │ ├── e2e │ │ │ │ ├── test.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── src │ │ │ │ ├── app.tsx │ │ │ │ ├── entry-client.tsx │ │ │ │ ├── entry-server.tsx │ │ │ │ ├── global.d.ts │ │ │ │ ├── lib │ │ │ │ │ ├── auth-client.ts │ │ │ │ │ └── auth.ts │ │ │ │ └── routes │ │ │ │ ├── [...404].tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...all].ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── test-utils │ │ │ ├── package.json │ │ │ └── src │ │ │ └── playwright.ts │ │ └── vanilla-node │ │ ├── e2e │ │ │ ├── app.ts │ │ │ ├── domain.spec.ts │ │ │ ├── postgres-js.spec.ts │ │ │ ├── test.spec.ts │ │ │ └── utils.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── main.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── smoke │ ├── package.json │ ├── test │ │ ├── bun.spec.ts │ │ ├── cloudflare.spec.ts │ │ ├── deno.spec.ts │ │ ├── fixtures │ │ │ ├── bun-simple.ts │ │ │ ├── cloudflare │ │ │ │ ├── .gitignore │ │ │ │ ├── drizzle │ │ │ │ │ ├── 0000_clean_vector.sql │ │ │ │ │ └── meta │ │ │ │ │ ├── _journal.json │ │ │ │ │ └── 0000_snapshot.json │ │ │ │ ├── drizzle.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── auth-schema.ts │ │ │ │ │ ├── db.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── test │ │ │ │ │ ├── apply-migrations.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ └── index.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── worker-configuration.d.ts │ │ │ │ └── wrangler.json │ │ │ ├── deno-simple.ts │ │ │ ├── tsconfig-decelration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-exact-optional-property-types │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── user-additional-fields.ts │ │ │ │ │ └── username.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-verbatim-module-syntax-node10 │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ └── vite │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── client.ts │ │ │ │ └── server.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── ssr.ts │ │ ├── typecheck.spec.ts │ │ └── vite.spec.ts │ └── tsconfig.json ├── LICENSE.md ├── package.json ├── packages │ ├── better-auth │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ └── init.test.ts.snap │ │ │ ├── adapters │ │ │ │ ├── adapter-factory │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── adapter-factory.test.ts.snap │ │ │ │ │ │ └── adapter-factory.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── create-test-suite.ts │ │ │ │ ├── drizzle-adapter │ │ │ │ │ ├── drizzle-adapter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── adapter.drizzle.mysql.test.ts │ │ │ │ │ ├── adapter.drizzle.pg.test.ts │ │ │ │ │ ├── adapter.drizzle.sqlite.test.ts │ │ │ │ │ └── generate-schema.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely-adapter │ │ │ │ │ ├── bun-sqlite-dialect.ts │ │ │ │ │ ├── dialect.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── kysely-adapter.ts │ │ │ │ │ ├── node-sqlite-dialect.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── adapter.kysely.mssql.test.ts │ │ │ │ │ │ ├── adapter.kysely.mysql.test.ts │ │ │ │ │ │ ├── adapter.kysely.pg.test.ts │ │ │ │ │ │ ├── adapter.kysely.sqlite.test.ts │ │ │ │ │ │ └── node-sqlite-dialect.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── memory-adapter │ │ │ │ │ ├── adapter.memory.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── memory-adapter.ts │ │ │ │ ├── mongodb-adapter │ │ │ │ │ ├── adapter.mongo-db.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mongodb-adapter.ts │ │ │ │ ├── prisma-adapter │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── prisma-adapter.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── base.prisma │ │ │ │ │ ├── generate-auth-config.ts │ │ │ │ │ ├── generate-prisma-schema.ts │ │ │ │ │ ├── get-prisma-client.ts │ │ │ │ │ ├── prisma.mysql.test.ts │ │ │ │ │ ├── prisma.pg.test.ts │ │ │ │ │ ├── prisma.sqlite.test.ts │ │ │ │ │ └── push-prisma-schema.ts │ │ │ │ ├── test-adapter.ts │ │ │ │ ├── test.ts │ │ │ │ ├── tests │ │ │ │ │ ├── auth-flow.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── normal.ts │ │ │ │ │ ├── number-id.ts │ │ │ │ │ ├── performance.ts │ │ │ │ │ └── transactions.ts │ │ │ │ └── utils.ts │ │ │ ├── api │ │ │ │ ├── check-endpoint-conflicts.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── middlewares │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── origin-check.test.ts │ │ │ │ │ └── origin-check.ts │ │ │ │ ├── rate-limiter │ │ │ │ │ ├── index.ts │ │ │ │ │ └── rate-limiter.test.ts │ │ │ │ ├── routes │ │ │ │ │ ├── account.test.ts │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── callback.ts │ │ │ │ │ ├── email-verification.test.ts │ │ │ │ │ ├── email-verification.ts │ │ │ │ │ ├── error.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ok.ts │ │ │ │ │ ├── reset-password.test.ts │ │ │ │ │ ├── reset-password.ts │ │ │ │ │ ├── session-api.test.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── sign-in.test.ts │ │ │ │ │ ├── sign-in.ts │ │ │ │ │ ├── sign-out.test.ts │ │ │ │ │ ├── sign-out.ts │ │ │ │ │ ├── sign-up.test.ts │ │ │ │ │ ├── sign-up.ts │ │ │ │ │ ├── update-user.test.ts │ │ │ │ │ └── update-user.ts │ │ │ │ ├── to-auth-endpoints.test.ts │ │ │ │ └── to-auth-endpoints.ts │ │ │ ├── auth.test.ts │ │ │ ├── auth.ts │ │ │ ├── call.test.ts │ │ │ ├── client │ │ │ │ ├── client-ssr.test.ts │ │ │ │ ├── client.test.ts │ │ │ │ ├── config.ts │ │ │ │ ├── fetch-plugins.ts │ │ │ │ ├── index.ts │ │ │ │ ├── lynx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── lynx-store.ts │ │ │ │ ├── parser.ts │ │ │ │ ├── path-to-object.ts │ │ │ │ ├── plugins │ │ │ │ │ ├── index.ts │ │ │ │ │ └── infer-plugin.ts │ │ │ │ ├── proxy.ts │ │ │ │ ├── query.ts │ │ │ │ ├── react │ │ │ │ │ ├── index.ts │ │ │ │ │ └── react-store.ts │ │ │ │ ├── session-atom.ts │ │ │ │ ├── solid │ │ │ │ │ ├── index.ts │ │ │ │ │ └── solid-store.ts │ │ │ │ ├── svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── test-plugin.ts │ │ │ │ ├── types.ts │ │ │ │ ├── url.test.ts │ │ │ │ ├── vanilla.ts │ │ │ │ └── vue │ │ │ │ ├── index.ts │ │ │ │ └── vue-store.ts │ │ │ ├── cookies │ │ │ │ ├── check-cookies.ts │ │ │ │ ├── cookie-utils.ts │ │ │ │ ├── cookies.test.ts │ │ │ │ └── index.ts │ │ │ ├── crypto │ │ │ │ ├── buffer.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt.ts │ │ │ │ ├── password.test.ts │ │ │ │ ├── password.ts │ │ │ │ └── random.ts │ │ │ ├── db │ │ │ │ ├── db.test.ts │ │ │ │ ├── field.ts │ │ │ │ ├── get-migration.ts │ │ │ │ ├── get-schema.ts │ │ │ │ ├── get-tables.test.ts │ │ │ │ ├── get-tables.ts │ │ │ │ ├── index.ts │ │ │ │ ├── internal-adapter.test.ts │ │ │ │ ├── internal-adapter.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── secondary-storage.test.ts │ │ │ │ ├── to-zod.ts │ │ │ │ ├── utils.ts │ │ │ │ └── with-hooks.ts │ │ │ ├── index.ts │ │ │ ├── init.test.ts │ │ │ ├── init.ts │ │ │ ├── integrations │ │ │ │ ├── next-js.ts │ │ │ │ ├── node.ts │ │ │ │ ├── react-start.ts │ │ │ │ ├── solid-start.ts │ │ │ │ └── svelte-kit.ts │ │ │ ├── oauth2 │ │ │ │ ├── index.ts │ │ │ │ ├── link-account.test.ts │ │ │ │ ├── link-account.ts │ │ │ │ ├── state.ts │ │ │ │ └── utils.ts │ │ │ ├── plugins │ │ │ │ ├── access │ │ │ │ │ ├── access.test.ts │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── additional-fields │ │ │ │ │ ├── additional-fields.test.ts │ │ │ │ │ └── client.ts │ │ │ │ ├── admin │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── admin.test.ts │ │ │ │ │ ├── admin.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── anonymous │ │ │ │ │ ├── anon.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── api-key │ │ │ │ │ ├── api-key.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── create-api-key.ts │ │ │ │ │ │ ├── delete-all-expired-api-keys.ts │ │ │ │ │ │ ├── delete-api-key.ts │ │ │ │ │ │ ├── get-api-key.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list-api-keys.ts │ │ │ │ │ │ ├── update-api-key.ts │ │ │ │ │ │ └── verify-api-key.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── bearer │ │ │ │ │ ├── bearer.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── captcha │ │ │ │ │ ├── captcha.test.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-handlers │ │ │ │ │ ├── captchafox.ts │ │ │ │ │ ├── cloudflare-turnstile.ts │ │ │ │ │ ├── google-recaptcha.ts │ │ │ │ │ ├── h-captcha.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── custom-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-session.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── device-authorization │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── device-authorization.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── schema.ts │ │ │ │ ├── email-otp │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── email-otp.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── generic-oauth │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── generic-oauth.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── haveibeenpwned │ │ │ │ │ ├── haveibeenpwned.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── jwt.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── sign.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── last-login-method │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-prefix.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── last-login-method.test.ts │ │ │ │ ├── magic-link │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── magic-link.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── mcp │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mcp.test.ts │ │ │ │ ├── multi-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── multi-session.test.ts │ │ │ │ ├── oauth-proxy │ │ │ │ │ ├── index.ts │ │ │ │ │ └── oauth-proxy.test.ts │ │ │ │ ├── oidc-provider │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── oidc.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── ui.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── one-tap │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── one-time-token │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── one-time-token.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── open-api │ │ │ │ │ ├── generator.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logo.ts │ │ │ │ │ └── open-api.test.ts │ │ │ │ ├── organization │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── call.ts │ │ │ │ │ ├── client.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization-hook.test.ts │ │ │ │ │ ├── organization.test.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── permission.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── crud-access-control.test.ts │ │ │ │ │ │ ├── crud-access-control.ts │ │ │ │ │ │ ├── crud-invites.ts │ │ │ │ │ │ ├── crud-members.test.ts │ │ │ │ │ │ ├── crud-members.ts │ │ │ │ │ │ ├── crud-org.test.ts │ │ │ │ │ │ ├── crud-org.ts │ │ │ │ │ │ └── crud-team.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── team.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── passkey │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── passkey.test.ts │ │ │ │ ├── phone-number │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── phone-number-error.ts │ │ │ │ │ └── phone-number.test.ts │ │ │ │ ├── siwe │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── siwe.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── two-factor │ │ │ │ │ ├── backup-codes │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── constant.ts │ │ │ │ │ ├── error-code.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── otp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── totp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── two-factor.test.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-two-factor.ts │ │ │ │ └── username │ │ │ │ ├── client.ts │ │ │ │ ├── error-codes.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── username.test.ts │ │ │ ├── social-providers │ │ │ │ └── index.ts │ │ │ ├── social.test.ts │ │ │ ├── test-utils │ │ │ │ ├── headers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ └── test-instance.ts │ │ │ ├── types │ │ │ │ ├── adapter.ts │ │ │ │ ├── api.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── models.ts │ │ │ │ ├── plugins.ts │ │ │ │ └── types.test.ts │ │ │ └── utils │ │ │ ├── await-object.ts │ │ │ ├── boolean.ts │ │ │ ├── clone.ts │ │ │ ├── constants.ts │ │ │ ├── date.ts │ │ │ ├── ensure-utc.ts │ │ │ ├── get-request-ip.ts │ │ │ ├── hashing.ts │ │ │ ├── hide-metadata.ts │ │ │ ├── id.ts │ │ │ ├── import-util.ts │ │ │ ├── index.ts │ │ │ ├── is-atom.ts │ │ │ ├── is-promise.ts │ │ │ ├── json.ts │ │ │ ├── merger.ts │ │ │ ├── middleware-response.ts │ │ │ ├── misc.ts │ │ │ ├── password.ts │ │ │ ├── plugin-helper.ts │ │ │ ├── shim.ts │ │ │ ├── time.ts │ │ │ ├── url.ts │ │ │ └── wildcard.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── generate.ts │ │ │ │ ├── info.ts │ │ │ │ ├── init.ts │ │ │ │ ├── login.ts │ │ │ │ ├── mcp.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── secret.ts │ │ │ ├── generators │ │ │ │ ├── auth-config.ts │ │ │ │ ├── drizzle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely.ts │ │ │ │ ├── prisma.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── add-svelte-kit-env-modules.ts │ │ │ ├── check-package-managers.ts │ │ │ ├── format-ms.ts │ │ │ ├── get-config.ts │ │ │ ├── get-package-info.ts │ │ │ ├── get-tsconfig-info.ts │ │ │ └── install-dependencies.ts │ │ ├── test │ │ │ ├── __snapshots__ │ │ │ │ ├── auth-schema-mysql-enum.txt │ │ │ │ ├── auth-schema-mysql-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey.txt │ │ │ │ ├── auth-schema-mysql.txt │ │ │ │ ├── auth-schema-number-id.txt │ │ │ │ ├── auth-schema-pg-enum.txt │ │ │ │ ├── auth-schema-pg-passkey.txt │ │ │ │ ├── auth-schema-sqlite-enum.txt │ │ │ │ ├── auth-schema-sqlite-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey.txt │ │ │ │ ├── auth-schema-sqlite.txt │ │ │ │ ├── auth-schema.txt │ │ │ │ ├── migrations.sql │ │ │ │ ├── schema-mongodb.prisma │ │ │ │ ├── schema-mysql-custom.prisma │ │ │ │ ├── schema-mysql.prisma │ │ │ │ ├── schema-numberid.prisma │ │ │ │ └── schema.prisma │ │ │ ├── generate-all-db.test.ts │ │ │ ├── generate.test.ts │ │ │ ├── get-config.test.ts │ │ │ ├── info.test.ts │ │ │ └── migrate.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ └── tsdown.config.ts │ ├── core │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ └── index.ts │ │ │ ├── async_hooks │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── endpoint-context.ts │ │ │ │ ├── index.ts │ │ │ │ └── transaction.ts │ │ │ ├── db │ │ │ │ ├── adapter │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── schema │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── verification.ts │ │ │ │ └── type.ts │ │ │ ├── env │ │ │ │ ├── color-depth.ts │ │ │ │ ├── env-impl.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.test.ts │ │ │ │ └── logger.ts │ │ │ ├── error │ │ │ │ ├── codes.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── oauth2 │ │ │ │ ├── client-credentials-token.ts │ │ │ │ ├── create-authorization-url.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oauth-provider.ts │ │ │ │ ├── refresh-access-token.ts │ │ │ │ ├── utils.ts │ │ │ │ └── validate-authorization-code.ts │ │ │ ├── social-providers │ │ │ │ ├── apple.ts │ │ │ │ ├── atlassian.ts │ │ │ │ ├── cognito.ts │ │ │ │ ├── discord.ts │ │ │ │ ├── dropbox.ts │ │ │ │ ├── facebook.ts │ │ │ │ ├── figma.ts │ │ │ │ ├── github.ts │ │ │ │ ├── gitlab.ts │ │ │ │ ├── google.ts │ │ │ │ ├── huggingface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kakao.ts │ │ │ │ ├── kick.ts │ │ │ │ ├── line.ts │ │ │ │ ├── linear.ts │ │ │ │ ├── linkedin.ts │ │ │ │ ├── microsoft-entra-id.ts │ │ │ │ ├── naver.ts │ │ │ │ ├── notion.ts │ │ │ │ ├── paypal.ts │ │ │ │ ├── reddit.ts │ │ │ │ ├── roblox.ts │ │ │ │ ├── salesforce.ts │ │ │ │ ├── slack.ts │ │ │ │ ├── spotify.ts │ │ │ │ ├── tiktok.ts │ │ │ │ ├── twitch.ts │ │ │ │ ├── twitter.ts │ │ │ │ ├── vk.ts │ │ │ │ └── zoom.ts │ │ │ ├── types │ │ │ │ ├── context.ts │ │ │ │ ├── cookie.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── init-options.ts │ │ │ │ ├── plugin-client.ts │ │ │ │ └── plugin.ts │ │ │ └── utils │ │ │ ├── error-codes.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── expo │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── expo.test.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── sso │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── oidc.test.ts │ │ │ └── saml.test.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── stripe │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ ├── stripe.test.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── telemetry │ ├── package.json │ ├── src │ │ ├── detectors │ │ │ ├── detect-auth-config.ts │ │ │ ├── detect-database.ts │ │ │ ├── detect-framework.ts │ │ │ ├── detect-project-info.ts │ │ │ ├── detect-runtime.ts │ │ │ └── detect-system-info.ts │ │ ├── index.ts │ │ ├── project-id.ts │ │ ├── telemetry.test.ts │ │ ├── types.ts │ │ └── utils │ │ ├── hash.ts │ │ ├── id.ts │ │ ├── import-util.ts │ │ └── package-json.ts │ ├── tsconfig.json │ └── tsdown.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── SECURITY.md ├── tsconfig.json └── turbo.json ``` # Files -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/email-otp/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as z from "zod"; 2 | import { APIError, getSessionFromCtx } from "../../api"; 3 | import { 4 | createAuthEndpoint, 5 | createAuthMiddleware, 6 | } from "@better-auth/core/api"; 7 | import type { BetterAuthPlugin } from "@better-auth/core"; 8 | import { 9 | generateRandomString, 10 | symmetricDecrypt, 11 | symmetricEncrypt, 12 | } from "../../crypto"; 13 | import { getDate } from "../../utils/date"; 14 | import { setCookieCache, setSessionCookie } from "../../cookies"; 15 | import { getEndpointResponse } from "../../utils/plugin-helper"; 16 | import { defaultKeyHasher, splitAtLastColon } from "./utils"; 17 | import type { GenericEndpointContext } from "@better-auth/core"; 18 | import { defineErrorCodes } from "@better-auth/core/utils"; 19 | 20 | export interface EmailOTPOptions { 21 | /** 22 | * Function to send email verification 23 | */ 24 | sendVerificationOTP: ( 25 | data: { 26 | email: string; 27 | otp: string; 28 | type: "sign-in" | "email-verification" | "forget-password"; 29 | }, 30 | request?: Request, 31 | ) => Promise<void>; 32 | /** 33 | * Length of the OTP 34 | * 35 | * @default 6 36 | */ 37 | otpLength?: number; 38 | /** 39 | * Expiry time of the OTP in seconds 40 | * 41 | * @default 300 (5 minutes) 42 | */ 43 | expiresIn?: number; 44 | /** 45 | * Custom function to generate otp 46 | */ 47 | generateOTP?: ( 48 | data: { 49 | email: string; 50 | type: "sign-in" | "email-verification" | "forget-password"; 51 | }, 52 | request?: Request, 53 | ) => string | undefined; 54 | /** 55 | * Send email verification on sign-up 56 | * 57 | * @Default false 58 | */ 59 | sendVerificationOnSignUp?: boolean; 60 | /** 61 | * A boolean value that determines whether to prevent 62 | * automatic sign-up when the user is not registered. 63 | * 64 | * @Default false 65 | */ 66 | disableSignUp?: boolean; 67 | /** 68 | * Allowed attempts for the OTP code 69 | * @default 3 70 | */ 71 | allowedAttempts?: number; 72 | /** 73 | * Store the OTP in your database in a secure way 74 | * Note: This will not affect the OTP sent to the user, it will only affect the OTP stored in your database 75 | * 76 | * @default "plain" 77 | */ 78 | storeOTP?: 79 | | "hashed" 80 | | "plain" 81 | | "encrypted" 82 | | { hash: (otp: string) => Promise<string> } 83 | | { 84 | encrypt: (otp: string) => Promise<string>; 85 | decrypt: (otp: string) => Promise<string>; 86 | }; 87 | /** 88 | * Override the default email verification to use email otp instead 89 | * 90 | * @default false 91 | */ 92 | overrideDefaultEmailVerification?: boolean; 93 | } 94 | 95 | const types = ["email-verification", "sign-in", "forget-password"] as const; 96 | const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; 97 | const defaultOTPGenerator = (options: EmailOTPOptions) => 98 | generateRandomString(options.otpLength ?? 6, "0-9"); 99 | 100 | const ERROR_CODES = defineErrorCodes({ 101 | OTP_EXPIRED: "otp expired", 102 | INVALID_OTP: "Invalid OTP", 103 | INVALID_EMAIL: "Invalid email", 104 | USER_NOT_FOUND: "User not found", 105 | TOO_MANY_ATTEMPTS: "Too many attempts", 106 | }); 107 | 108 | export const emailOTP = (options: EmailOTPOptions) => { 109 | const opts = { 110 | expiresIn: 5 * 60, 111 | generateOTP: () => defaultOTPGenerator(options), 112 | storeOTP: "plain", 113 | ...options, 114 | } satisfies EmailOTPOptions; 115 | 116 | async function storeOTP(ctx: GenericEndpointContext, otp: string) { 117 | if (opts.storeOTP === "encrypted") { 118 | return await symmetricEncrypt({ 119 | key: ctx.context.secret, 120 | data: otp, 121 | }); 122 | } 123 | if (opts.storeOTP === "hashed") { 124 | return await defaultKeyHasher(otp); 125 | } 126 | if (typeof opts.storeOTP === "object" && "hash" in opts.storeOTP) { 127 | return await opts.storeOTP.hash(otp); 128 | } 129 | if (typeof opts.storeOTP === "object" && "encrypt" in opts.storeOTP) { 130 | return await opts.storeOTP.encrypt(otp); 131 | } 132 | 133 | return otp; 134 | } 135 | 136 | async function verifyStoredOTP( 137 | ctx: GenericEndpointContext, 138 | storedOtp: string, 139 | otp: string, 140 | ): Promise<boolean> { 141 | if (opts.storeOTP === "encrypted") { 142 | return ( 143 | (await symmetricDecrypt({ 144 | key: ctx.context.secret, 145 | data: storedOtp, 146 | })) === otp 147 | ); 148 | } 149 | if (opts.storeOTP === "hashed") { 150 | const hashedOtp = await defaultKeyHasher(otp); 151 | return hashedOtp === storedOtp; 152 | } 153 | if (typeof opts.storeOTP === "object" && "hash" in opts.storeOTP) { 154 | const hashedOtp = await opts.storeOTP.hash(otp); 155 | return hashedOtp === storedOtp; 156 | } 157 | if (typeof opts.storeOTP === "object" && "decrypt" in opts.storeOTP) { 158 | const decryptedOtp = await opts.storeOTP.decrypt(storedOtp); 159 | return decryptedOtp === otp; 160 | } 161 | 162 | return otp === storedOtp; 163 | } 164 | const endpoints = { 165 | /** 166 | * ### Endpoint 167 | * 168 | * POST `/email-otp/send-verification-otp` 169 | * 170 | * ### API Methods 171 | * 172 | * **server:** 173 | * `auth.api.sendVerificationOTP` 174 | * 175 | * **client:** 176 | * `authClient.emailOtp.sendVerificationOtp` 177 | * 178 | * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-email-otp-send-verification-otp) 179 | */ 180 | sendVerificationOTP: createAuthEndpoint( 181 | "/email-otp/send-verification-otp", 182 | { 183 | method: "POST", 184 | body: z.object({ 185 | email: z.string({}).meta({ 186 | description: "Email address to send the OTP", 187 | }), 188 | type: z.enum(types).meta({ 189 | description: "Type of the OTP", 190 | }), 191 | }), 192 | metadata: { 193 | openapi: { 194 | description: "Send verification OTP", 195 | responses: { 196 | 200: { 197 | description: "Success", 198 | content: { 199 | "application/json": { 200 | schema: { 201 | type: "object", 202 | properties: { 203 | success: { 204 | type: "boolean", 205 | }, 206 | }, 207 | }, 208 | }, 209 | }, 210 | }, 211 | }, 212 | }, 213 | }, 214 | }, 215 | async (ctx) => { 216 | if (!options?.sendVerificationOTP) { 217 | ctx.context.logger.error( 218 | "send email verification is not implemented", 219 | ); 220 | throw new APIError("BAD_REQUEST", { 221 | message: "send email verification is not implemented", 222 | }); 223 | } 224 | const email = ctx.body.email; 225 | if (!emailRegex.test(email)) { 226 | throw ctx.error("BAD_REQUEST", { 227 | message: ERROR_CODES.INVALID_EMAIL, 228 | }); 229 | } 230 | if (opts.disableSignUp) { 231 | const user = await ctx.context.internalAdapter.findUserByEmail(email); 232 | if (!user) { 233 | throw new APIError("BAD_REQUEST", { 234 | message: ERROR_CODES.USER_NOT_FOUND, 235 | }); 236 | } 237 | } else if (ctx.body.type === "forget-password") { 238 | const user = await ctx.context.internalAdapter.findUserByEmail(email); 239 | if (!user) { 240 | return ctx.json({ 241 | success: true, 242 | }); 243 | } 244 | } 245 | let otp = 246 | opts.generateOTP({ email, type: ctx.body.type }, ctx.request) || 247 | defaultOTPGenerator(opts); 248 | 249 | let storedOTP = await storeOTP(ctx, otp); 250 | 251 | await ctx.context.internalAdapter 252 | .createVerificationValue({ 253 | value: `${storedOTP}:0`, 254 | identifier: `${ctx.body.type}-otp-${email}`, 255 | expiresAt: getDate(opts.expiresIn, "sec"), 256 | }) 257 | .catch(async (error) => { 258 | // might be duplicate key error 259 | await ctx.context.internalAdapter.deleteVerificationByIdentifier( 260 | `${ctx.body.type}-otp-${email}`, 261 | ); 262 | //try again 263 | await ctx.context.internalAdapter.createVerificationValue({ 264 | value: `${storedOTP}:0`, 265 | identifier: `${ctx.body.type}-otp-${email}`, 266 | expiresAt: getDate(opts.expiresIn, "sec"), 267 | }); 268 | }); 269 | await options.sendVerificationOTP( 270 | { 271 | email, 272 | otp, 273 | type: ctx.body.type, 274 | }, 275 | ctx.request, 276 | ); 277 | return ctx.json({ 278 | success: true, 279 | }); 280 | }, 281 | ), 282 | }; 283 | 284 | return { 285 | id: "email-otp", 286 | init(ctx) { 287 | if (!opts.overrideDefaultEmailVerification) { 288 | return; 289 | } 290 | return { 291 | options: { 292 | emailVerification: { 293 | async sendVerificationEmail(data, request) { 294 | await endpoints.sendVerificationOTP({ 295 | //@ts-expect-error - we need to pass the context 296 | context: ctx, 297 | request: request, 298 | body: { 299 | email: data.user.email, 300 | type: "email-verification", 301 | }, 302 | ctx, 303 | }); 304 | }, 305 | }, 306 | }, 307 | }; 308 | }, 309 | endpoints: { 310 | ...endpoints, 311 | createVerificationOTP: createAuthEndpoint( 312 | "/email-otp/create-verification-otp", 313 | { 314 | method: "POST", 315 | body: z.object({ 316 | email: z.string({}).meta({ 317 | description: "Email address to send the OTP", 318 | }), 319 | type: z.enum(types).meta({ 320 | required: true, 321 | description: "Type of the OTP", 322 | }), 323 | }), 324 | metadata: { 325 | SERVER_ONLY: true, 326 | openapi: { 327 | description: "Create verification OTP", 328 | responses: { 329 | 200: { 330 | description: "Success", 331 | content: { 332 | "application/json": { 333 | schema: { 334 | type: "string", 335 | }, 336 | }, 337 | }, 338 | }, 339 | }, 340 | }, 341 | }, 342 | }, 343 | async (ctx) => { 344 | const email = ctx.body.email; 345 | const otp = 346 | opts.generateOTP({ email, type: ctx.body.type }, ctx.request) || 347 | defaultOTPGenerator(opts); 348 | let storedOTP = await storeOTP(ctx, otp); 349 | await ctx.context.internalAdapter.createVerificationValue({ 350 | value: `${storedOTP}:0`, 351 | identifier: `${ctx.body.type}-otp-${email}`, 352 | expiresAt: getDate(opts.expiresIn, "sec"), 353 | }); 354 | return otp; 355 | }, 356 | ), 357 | /** 358 | * ### Endpoint 359 | * 360 | * GET `/email-otp/get-verification-otp` 361 | * 362 | * ### API Methods 363 | * 364 | * **server:** 365 | * `auth.api.getVerificationOTP` 366 | * 367 | * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-email-otp-get-verification-otp) 368 | */ 369 | getVerificationOTP: createAuthEndpoint( 370 | "/email-otp/get-verification-otp", 371 | { 372 | method: "GET", 373 | query: z.object({ 374 | email: z.string({}).meta({ 375 | description: "Email address the OTP was sent to", 376 | }), 377 | type: z.enum(types).meta({ 378 | required: true, 379 | description: "Type of the OTP", 380 | }), 381 | }), 382 | metadata: { 383 | SERVER_ONLY: true, 384 | openapi: { 385 | description: "Get verification OTP", 386 | responses: { 387 | "200": { 388 | description: 389 | "OTP retrieved successfully or not found/expired", 390 | content: { 391 | "application/json": { 392 | schema: { 393 | type: "object", 394 | properties: { 395 | otp: { 396 | type: "string", 397 | nullable: true, 398 | description: 399 | "The stored OTP, or null if not found or expired", 400 | }, 401 | }, 402 | required: ["otp"], 403 | }, 404 | }, 405 | }, 406 | }, 407 | }, 408 | }, 409 | }, 410 | }, 411 | async (ctx) => { 412 | const email = ctx.query.email; 413 | const verificationValue = 414 | await ctx.context.internalAdapter.findVerificationValue( 415 | `${ctx.query.type}-otp-${email}`, 416 | ); 417 | if (!verificationValue || verificationValue.expiresAt < new Date()) { 418 | return ctx.json({ 419 | otp: null, 420 | }); 421 | } 422 | if ( 423 | opts.storeOTP === "hashed" || 424 | (typeof opts.storeOTP === "object" && "hash" in opts.storeOTP) 425 | ) { 426 | throw new APIError("BAD_REQUEST", { 427 | message: "OTP is hashed, cannot return the plain text OTP", 428 | }); 429 | } 430 | 431 | let [storedOtp, _attempts] = splitAtLastColon( 432 | verificationValue.value, 433 | ); 434 | let otp = storedOtp; 435 | if (opts.storeOTP === "encrypted") { 436 | otp = await symmetricDecrypt({ 437 | key: ctx.context.secret, 438 | data: storedOtp, 439 | }); 440 | } 441 | 442 | if (typeof opts.storeOTP === "object" && "decrypt" in opts.storeOTP) { 443 | otp = await opts.storeOTP.decrypt(storedOtp); 444 | } 445 | 446 | return ctx.json({ 447 | otp, 448 | }); 449 | }, 450 | ), 451 | /** 452 | * ### Endpoint 453 | * 454 | * GET `/email-otp/check-verification-otp` 455 | * 456 | * ### API Methods 457 | * 458 | * **server:** 459 | * `auth.api.checkVerificationOTP` 460 | * 461 | * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-email-otp-check-verification-otp) 462 | */ 463 | checkVerificationOTP: createAuthEndpoint( 464 | "/email-otp/check-verification-otp", 465 | { 466 | method: "POST", 467 | body: z.object({ 468 | email: z.string().meta({ 469 | description: "Email address the OTP was sent to", 470 | }), 471 | type: z.enum(types).meta({ 472 | required: true, 473 | description: "Type of the OTP", 474 | }), 475 | otp: z.string().meta({ 476 | required: true, 477 | description: "OTP to verify", 478 | }), 479 | }), 480 | metadata: { 481 | openapi: { 482 | description: "Check if a verification OTP is valid", 483 | responses: { 484 | 200: { 485 | description: "Success", 486 | content: { 487 | "application/json": { 488 | schema: { 489 | type: "object", 490 | properties: { 491 | success: { 492 | type: "boolean", 493 | }, 494 | }, 495 | }, 496 | }, 497 | }, 498 | }, 499 | }, 500 | }, 501 | }, 502 | }, 503 | async (ctx) => { 504 | const email = ctx.body.email; 505 | if (!emailRegex.test(email)) { 506 | throw new APIError("BAD_REQUEST", { 507 | message: ERROR_CODES.INVALID_EMAIL, 508 | }); 509 | } 510 | const user = await ctx.context.internalAdapter.findUserByEmail(email); 511 | if (!user) { 512 | throw new APIError("BAD_REQUEST", { 513 | message: ERROR_CODES.USER_NOT_FOUND, 514 | }); 515 | } 516 | const verificationValue = 517 | await ctx.context.internalAdapter.findVerificationValue( 518 | `${ctx.body.type}-otp-${email}`, 519 | ); 520 | if (!verificationValue) { 521 | throw new APIError("BAD_REQUEST", { 522 | message: ERROR_CODES.INVALID_OTP, 523 | }); 524 | } 525 | if (verificationValue.expiresAt < new Date()) { 526 | await ctx.context.internalAdapter.deleteVerificationValue( 527 | verificationValue.id, 528 | ); 529 | throw new APIError("BAD_REQUEST", { 530 | message: ERROR_CODES.OTP_EXPIRED, 531 | }); 532 | } 533 | 534 | const [otpValue, attempts] = splitAtLastColon( 535 | verificationValue.value, 536 | ); 537 | const allowedAttempts = options?.allowedAttempts || 3; 538 | if (attempts && parseInt(attempts) >= allowedAttempts) { 539 | await ctx.context.internalAdapter.deleteVerificationValue( 540 | verificationValue.id, 541 | ); 542 | throw new APIError("FORBIDDEN", { 543 | message: ERROR_CODES.TOO_MANY_ATTEMPTS, 544 | }); 545 | } 546 | const verified = await verifyStoredOTP(ctx, otpValue, ctx.body.otp); 547 | if (!verified) { 548 | await ctx.context.internalAdapter.updateVerificationValue( 549 | verificationValue.id, 550 | { 551 | value: `${otpValue}:${parseInt(attempts || "0") + 1}`, 552 | }, 553 | ); 554 | throw new APIError("BAD_REQUEST", { 555 | message: ERROR_CODES.INVALID_OTP, 556 | }); 557 | } 558 | return ctx.json({ 559 | success: true, 560 | }); 561 | }, 562 | ), 563 | /** 564 | * ### Endpoint 565 | * 566 | * POST `/email-otp/verify-email` 567 | * 568 | * ### API Methods 569 | * 570 | * **server:** 571 | * `auth.api.verifyEmailOTP` 572 | * 573 | * **client:** 574 | * `authClient.emailOtp.verifyEmail` 575 | * 576 | * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-email-otp-verify-email) 577 | */ 578 | verifyEmailOTP: createAuthEndpoint( 579 | "/email-otp/verify-email", 580 | { 581 | method: "POST", 582 | body: z.object({ 583 | email: z.string({}).meta({ 584 | description: "Email address to verify", 585 | }), 586 | otp: z.string().meta({ 587 | required: true, 588 | description: "OTP to verify", 589 | }), 590 | }), 591 | metadata: { 592 | openapi: { 593 | description: "Verify email with OTP", 594 | responses: { 595 | 200: { 596 | description: "Success", 597 | content: { 598 | "application/json": { 599 | schema: { 600 | type: "object", 601 | properties: { 602 | status: { 603 | type: "boolean", 604 | description: 605 | "Indicates if the verification was successful", 606 | enum: [true], 607 | }, 608 | token: { 609 | type: "string", 610 | nullable: true, 611 | description: 612 | "Session token if autoSignInAfterVerification is enabled, otherwise null", 613 | }, 614 | user: { 615 | $ref: "#/components/schemas/User", 616 | }, 617 | required: ["status", "token", "user"], 618 | }, 619 | }, 620 | }, 621 | }, 622 | }, 623 | }, 624 | }, 625 | }, 626 | }, 627 | async (ctx) => { 628 | const email = ctx.body.email; 629 | if (!emailRegex.test(email)) { 630 | throw new APIError("BAD_REQUEST", { 631 | message: ERROR_CODES.INVALID_EMAIL, 632 | }); 633 | } 634 | const verificationValue = 635 | await ctx.context.internalAdapter.findVerificationValue( 636 | `email-verification-otp-${email}`, 637 | ); 638 | 639 | if (!verificationValue) { 640 | throw new APIError("BAD_REQUEST", { 641 | message: ERROR_CODES.INVALID_OTP, 642 | }); 643 | } 644 | if (verificationValue.expiresAt < new Date()) { 645 | throw new APIError("BAD_REQUEST", { 646 | message: ERROR_CODES.OTP_EXPIRED, 647 | }); 648 | } 649 | 650 | const [otpValue, attempts] = splitAtLastColon( 651 | verificationValue.value, 652 | ); 653 | const allowedAttempts = options?.allowedAttempts || 3; 654 | if (attempts && parseInt(attempts) >= allowedAttempts) { 655 | await ctx.context.internalAdapter.deleteVerificationValue( 656 | verificationValue.id, 657 | ); 658 | throw new APIError("FORBIDDEN", { 659 | message: ERROR_CODES.TOO_MANY_ATTEMPTS, 660 | }); 661 | } 662 | const verified = await verifyStoredOTP(ctx, otpValue, ctx.body.otp); 663 | if (!verified) { 664 | await ctx.context.internalAdapter.updateVerificationValue( 665 | verificationValue.id, 666 | { 667 | value: `${otpValue}:${parseInt(attempts || "0") + 1}`, 668 | }, 669 | ); 670 | throw new APIError("BAD_REQUEST", { 671 | message: ERROR_CODES.INVALID_OTP, 672 | }); 673 | } 674 | await ctx.context.internalAdapter.deleteVerificationValue( 675 | verificationValue.id, 676 | ); 677 | const user = await ctx.context.internalAdapter.findUserByEmail(email); 678 | if (!user) { 679 | throw new APIError("BAD_REQUEST", { 680 | message: ERROR_CODES.USER_NOT_FOUND, 681 | }); 682 | } 683 | const updatedUser = await ctx.context.internalAdapter.updateUser( 684 | user.user.id, 685 | { 686 | email, 687 | emailVerified: true, 688 | }, 689 | ); 690 | await ctx.context.options.emailVerification?.onEmailVerification?.( 691 | updatedUser, 692 | ctx.request, 693 | ); 694 | 695 | if ( 696 | ctx.context.options.emailVerification?.autoSignInAfterVerification 697 | ) { 698 | const session = await ctx.context.internalAdapter.createSession( 699 | updatedUser.id, 700 | ); 701 | await setSessionCookie(ctx, { 702 | session, 703 | user: updatedUser, 704 | }); 705 | return ctx.json({ 706 | status: true, 707 | token: session.token, 708 | user: { 709 | id: updatedUser.id, 710 | email: updatedUser.email, 711 | emailVerified: updatedUser.emailVerified, 712 | name: updatedUser.name, 713 | image: updatedUser.image, 714 | createdAt: updatedUser.createdAt, 715 | updatedAt: updatedUser.updatedAt, 716 | }, 717 | }); 718 | } 719 | const currentSession = await getSessionFromCtx(ctx); 720 | if (currentSession && updatedUser.emailVerified) { 721 | const dontRememberMeCookie = await ctx.getSignedCookie( 722 | ctx.context.authCookies.dontRememberToken.name, 723 | ctx.context.secret, 724 | ); 725 | await setCookieCache( 726 | ctx, 727 | { 728 | session: currentSession.session, 729 | user: { 730 | ...currentSession.user, 731 | emailVerified: true, 732 | }, 733 | }, 734 | !!dontRememberMeCookie, 735 | ); 736 | } 737 | return ctx.json({ 738 | status: true, 739 | token: null, 740 | user: { 741 | id: updatedUser.id, 742 | email: updatedUser.email, 743 | emailVerified: updatedUser.emailVerified, 744 | name: updatedUser.name, 745 | image: updatedUser.image, 746 | createdAt: updatedUser.createdAt, 747 | updatedAt: updatedUser.updatedAt, 748 | }, 749 | }); 750 | }, 751 | ), 752 | /** 753 | * ### Endpoint 754 | * 755 | * POST `/sign-in/email-otp` 756 | * 757 | * ### API Methods 758 | * 759 | * **server:** 760 | * `auth.api.signInEmailOTP` 761 | * 762 | * **client:** 763 | * `authClient.signIn.emailOtp` 764 | * 765 | * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-sign-in-email-otp) 766 | */ 767 | signInEmailOTP: createAuthEndpoint( 768 | "/sign-in/email-otp", 769 | { 770 | method: "POST", 771 | body: z.object({ 772 | email: z.string({}).meta({ 773 | description: "Email address to sign in", 774 | }), 775 | otp: z.string().meta({ 776 | required: true, 777 | description: "OTP sent to the email", 778 | }), 779 | }), 780 | metadata: { 781 | openapi: { 782 | description: "Sign in with OTP", 783 | responses: { 784 | 200: { 785 | description: "Success", 786 | content: { 787 | "application/json": { 788 | schema: { 789 | type: "object", 790 | properties: { 791 | token: { 792 | type: "string", 793 | description: 794 | "Session token for the authenticated session", 795 | }, 796 | user: { 797 | $ref: "#/components/schemas/User", 798 | }, 799 | }, 800 | required: ["token", "user"], 801 | }, 802 | }, 803 | }, 804 | }, 805 | }, 806 | }, 807 | }, 808 | }, 809 | async (ctx) => { 810 | const email = ctx.body.email; 811 | const verificationValue = 812 | await ctx.context.internalAdapter.findVerificationValue( 813 | `sign-in-otp-${email}`, 814 | ); 815 | if (!verificationValue) { 816 | throw new APIError("BAD_REQUEST", { 817 | message: ERROR_CODES.INVALID_OTP, 818 | }); 819 | } 820 | if (verificationValue.expiresAt < new Date()) { 821 | throw new APIError("BAD_REQUEST", { 822 | message: ERROR_CODES.OTP_EXPIRED, 823 | }); 824 | } 825 | const [otpValue, attempts] = splitAtLastColon( 826 | verificationValue.value, 827 | ); 828 | const allowedAttempts = options?.allowedAttempts || 3; 829 | if (attempts && parseInt(attempts) >= allowedAttempts) { 830 | await ctx.context.internalAdapter.deleteVerificationValue( 831 | verificationValue.id, 832 | ); 833 | throw new APIError("FORBIDDEN", { 834 | message: ERROR_CODES.TOO_MANY_ATTEMPTS, 835 | }); 836 | } 837 | const verified = await verifyStoredOTP(ctx, otpValue, ctx.body.otp); 838 | if (!verified) { 839 | await ctx.context.internalAdapter.updateVerificationValue( 840 | verificationValue.id, 841 | { 842 | value: `${otpValue}:${parseInt(attempts || "0") + 1}`, 843 | }, 844 | ); 845 | throw new APIError("BAD_REQUEST", { 846 | message: ERROR_CODES.INVALID_OTP, 847 | }); 848 | } 849 | await ctx.context.internalAdapter.deleteVerificationValue( 850 | verificationValue.id, 851 | ); 852 | const user = await ctx.context.internalAdapter.findUserByEmail(email); 853 | if (!user) { 854 | if (opts.disableSignUp) { 855 | throw new APIError("BAD_REQUEST", { 856 | message: ERROR_CODES.USER_NOT_FOUND, 857 | }); 858 | } 859 | const newUser = await ctx.context.internalAdapter.createUser({ 860 | email, 861 | emailVerified: true, 862 | name: "", 863 | }); 864 | const session = await ctx.context.internalAdapter.createSession( 865 | newUser.id, 866 | ); 867 | await setSessionCookie(ctx, { 868 | session, 869 | user: newUser, 870 | }); 871 | return ctx.json({ 872 | token: session.token, 873 | user: { 874 | id: newUser.id, 875 | email: newUser.email, 876 | emailVerified: newUser.emailVerified, 877 | name: newUser.name, 878 | image: newUser.image, 879 | createdAt: newUser.createdAt, 880 | updatedAt: newUser.updatedAt, 881 | }, 882 | }); 883 | } 884 | 885 | if (!user.user.emailVerified) { 886 | await ctx.context.internalAdapter.updateUser(user.user.id, { 887 | emailVerified: true, 888 | }); 889 | } 890 | 891 | const session = await ctx.context.internalAdapter.createSession( 892 | user.user.id, 893 | ); 894 | await setSessionCookie(ctx, { 895 | session, 896 | user: user.user, 897 | }); 898 | return ctx.json({ 899 | token: session.token, 900 | user: { 901 | id: user.user.id, 902 | email: user.user.email, 903 | emailVerified: user.user.emailVerified, 904 | name: user.user.name, 905 | image: user.user.image, 906 | createdAt: user.user.createdAt, 907 | updatedAt: user.user.updatedAt, 908 | }, 909 | }); 910 | }, 911 | ), 912 | /** 913 | * ### Endpoint 914 | * 915 | * POST `/forget-password/email-otp` 916 | * 917 | * ### API Methods 918 | * 919 | * **server:** 920 | * `auth.api.forgetPasswordEmailOTP` 921 | * 922 | * **client:** 923 | * `authClient.forgetPassword.emailOtp` 924 | * 925 | * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-forget-password-email-otp) 926 | */ 927 | forgetPasswordEmailOTP: createAuthEndpoint( 928 | "/forget-password/email-otp", 929 | { 930 | method: "POST", 931 | body: z.object({ 932 | email: z.string().meta({ 933 | description: "Email address to send the OTP", 934 | }), 935 | }), 936 | metadata: { 937 | openapi: { 938 | description: "Send a password reset OTP to the user", 939 | responses: { 940 | 200: { 941 | description: "Success", 942 | content: { 943 | "application/json": { 944 | schema: { 945 | type: "object", 946 | properties: { 947 | success: { 948 | type: "boolean", 949 | description: 950 | "Indicates if the OTP was sent successfully", 951 | }, 952 | }, 953 | }, 954 | }, 955 | }, 956 | }, 957 | }, 958 | }, 959 | }, 960 | }, 961 | async (ctx) => { 962 | const email = ctx.body.email; 963 | const user = await ctx.context.internalAdapter.findUserByEmail(email); 964 | if (!user) { 965 | throw new APIError("BAD_REQUEST", { 966 | message: ERROR_CODES.USER_NOT_FOUND, 967 | }); 968 | } 969 | const otp = 970 | opts.generateOTP({ email, type: "forget-password" }, ctx.request) || 971 | defaultOTPGenerator(opts); 972 | let storedOTP = await storeOTP(ctx, otp); 973 | await ctx.context.internalAdapter.createVerificationValue({ 974 | value: `${storedOTP}:0`, 975 | identifier: `forget-password-otp-${email}`, 976 | expiresAt: getDate(opts.expiresIn, "sec"), 977 | }); 978 | await options.sendVerificationOTP( 979 | { 980 | email, 981 | otp, 982 | type: "forget-password", 983 | }, 984 | ctx.request, 985 | ); 986 | return ctx.json({ 987 | success: true, 988 | }); 989 | }, 990 | ), 991 | /** 992 | * ### Endpoint 993 | * 994 | * POST `/email-otp/reset-password` 995 | * 996 | * ### API Methods 997 | * 998 | * **server:** 999 | * `auth.api.resetPasswordEmailOTP` 1000 | * 1001 | * **client:** 1002 | * `authClient.emailOtp.resetPassword` 1003 | * 1004 | * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-email-otp-reset-password) 1005 | */ 1006 | resetPasswordEmailOTP: createAuthEndpoint( 1007 | "/email-otp/reset-password", 1008 | { 1009 | method: "POST", 1010 | body: z.object({ 1011 | email: z.string().meta({ 1012 | description: "Email address to reset the password", 1013 | }), 1014 | otp: z.string().meta({ 1015 | description: "OTP sent to the email", 1016 | }), 1017 | password: z.string().meta({ 1018 | description: "New password", 1019 | }), 1020 | }), 1021 | metadata: { 1022 | openapi: { 1023 | description: "Reset user password with OTP", 1024 | responses: { 1025 | 200: { 1026 | description: "Success", 1027 | contnt: { 1028 | "application/json": { 1029 | schema: { 1030 | type: "object", 1031 | properties: { 1032 | success: { 1033 | type: "boolean", 1034 | }, 1035 | }, 1036 | }, 1037 | }, 1038 | }, 1039 | }, 1040 | }, 1041 | }, 1042 | }, 1043 | }, 1044 | async (ctx) => { 1045 | const email = ctx.body.email; 1046 | const user = await ctx.context.internalAdapter.findUserByEmail( 1047 | email, 1048 | { 1049 | includeAccounts: true, 1050 | }, 1051 | ); 1052 | if (!user) { 1053 | throw new APIError("BAD_REQUEST", { 1054 | message: ERROR_CODES.USER_NOT_FOUND, 1055 | }); 1056 | } 1057 | const verificationValue = 1058 | await ctx.context.internalAdapter.findVerificationValue( 1059 | `forget-password-otp-${email}`, 1060 | ); 1061 | if (!verificationValue) { 1062 | throw new APIError("BAD_REQUEST", { 1063 | message: ERROR_CODES.INVALID_OTP, 1064 | }); 1065 | } 1066 | if (verificationValue.expiresAt < new Date()) { 1067 | await ctx.context.internalAdapter.deleteVerificationValue( 1068 | verificationValue.id, 1069 | ); 1070 | throw new APIError("BAD_REQUEST", { 1071 | message: ERROR_CODES.OTP_EXPIRED, 1072 | }); 1073 | } 1074 | const [otpValue, attempts] = splitAtLastColon( 1075 | verificationValue.value, 1076 | ); 1077 | const allowedAttempts = options?.allowedAttempts || 3; 1078 | if (attempts && parseInt(attempts) >= allowedAttempts) { 1079 | await ctx.context.internalAdapter.deleteVerificationValue( 1080 | verificationValue.id, 1081 | ); 1082 | throw new APIError("FORBIDDEN", { 1083 | message: ERROR_CODES.TOO_MANY_ATTEMPTS, 1084 | }); 1085 | } 1086 | const verified = await verifyStoredOTP(ctx, otpValue, ctx.body.otp); 1087 | if (!verified) { 1088 | await ctx.context.internalAdapter.updateVerificationValue( 1089 | verificationValue.id, 1090 | { 1091 | value: `${otpValue}:${parseInt(attempts || "0") + 1}`, 1092 | }, 1093 | ); 1094 | throw new APIError("BAD_REQUEST", { 1095 | message: ERROR_CODES.INVALID_OTP, 1096 | }); 1097 | } 1098 | await ctx.context.internalAdapter.deleteVerificationValue( 1099 | verificationValue.id, 1100 | ); 1101 | const passwordHash = await ctx.context.password.hash( 1102 | ctx.body.password, 1103 | ); 1104 | const account = user.accounts.find( 1105 | (account) => account.providerId === "credential", 1106 | ); 1107 | if (!account) { 1108 | await ctx.context.internalAdapter.createAccount({ 1109 | userId: user.user.id, 1110 | providerId: "credential", 1111 | accountId: user.user.id, 1112 | password: passwordHash, 1113 | }); 1114 | } else { 1115 | await ctx.context.internalAdapter.updatePassword( 1116 | user.user.id, 1117 | passwordHash, 1118 | ); 1119 | } 1120 | 1121 | if (ctx.context.options.emailAndPassword?.onPasswordReset) { 1122 | await ctx.context.options.emailAndPassword.onPasswordReset( 1123 | { 1124 | user: user.user, 1125 | }, 1126 | ctx.request, 1127 | ); 1128 | } 1129 | 1130 | if (!user.user.emailVerified) { 1131 | await ctx.context.internalAdapter.updateUser(user.user.id, { 1132 | emailVerified: true, 1133 | }); 1134 | } 1135 | 1136 | return ctx.json({ 1137 | success: true, 1138 | }); 1139 | }, 1140 | ), 1141 | }, 1142 | hooks: { 1143 | after: [ 1144 | { 1145 | matcher(context) { 1146 | return !!( 1147 | context.path?.startsWith("/sign-up") && 1148 | opts.sendVerificationOnSignUp 1149 | ); 1150 | }, 1151 | handler: createAuthMiddleware(async (ctx) => { 1152 | const response = await getEndpointResponse<{ 1153 | user: { email: string }; 1154 | }>(ctx); 1155 | const email = response?.user.email; 1156 | if (email) { 1157 | const otp = 1158 | opts.generateOTP({ email, type: ctx.body.type }, ctx.request) || 1159 | defaultOTPGenerator(opts); 1160 | let storedOTP = await storeOTP(ctx, otp); 1161 | await ctx.context.internalAdapter.createVerificationValue({ 1162 | value: `${storedOTP}:0`, 1163 | identifier: `email-verification-otp-${email}`, 1164 | expiresAt: getDate(opts.expiresIn, "sec"), 1165 | }); 1166 | await options.sendVerificationOTP( 1167 | { 1168 | email, 1169 | otp, 1170 | type: "email-verification", 1171 | }, 1172 | ctx.request, 1173 | ); 1174 | } 1175 | }), 1176 | }, 1177 | ], 1178 | }, 1179 | $ERROR_CODES: ERROR_CODES, 1180 | rateLimit: [ 1181 | { 1182 | pathMatcher(path) { 1183 | return path === "/email-otp/send-verification-otp"; 1184 | }, 1185 | window: 60, 1186 | max: 3, 1187 | }, 1188 | { 1189 | pathMatcher(path) { 1190 | return path === "/email-otp/check-verification-otp"; 1191 | }, 1192 | window: 60, 1193 | max: 3, 1194 | }, 1195 | { 1196 | pathMatcher(path) { 1197 | return path === "/email-otp/verify-email"; 1198 | }, 1199 | window: 60, 1200 | max: 3, 1201 | }, 1202 | { 1203 | pathMatcher(path) { 1204 | return path === "/sign-in/email-otp"; 1205 | }, 1206 | window: 60, 1207 | max: 3, 1208 | }, 1209 | ], 1210 | } satisfies BetterAuthPlugin; 1211 | }; 1212 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/organization/routes/crud-team.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as z from "zod"; 2 | import { createAuthEndpoint } from "@better-auth/core/api"; 3 | import { getOrgAdapter } from "../adapter"; 4 | import { orgMiddleware, orgSessionMiddleware } from "../call"; 5 | import { APIError } from "better-call"; 6 | import { getSessionFromCtx } from "../../../api"; 7 | import { ORGANIZATION_ERROR_CODES } from "../error-codes"; 8 | import type { OrganizationOptions } from "../types"; 9 | import { teamSchema } from "../schema"; 10 | import { hasPermission } from "../has-permission"; 11 | import { setSessionCookie } from "../../../cookies"; 12 | import { 13 | toZodSchema, 14 | type InferAdditionalFieldsFromPluginOptions, 15 | } from "../../../db"; 16 | import type { PrettifyDeep } from "../../../types/helper"; 17 | 18 | export const createTeam = <O extends OrganizationOptions>(options: O) => { 19 | const additionalFieldsSchema = toZodSchema({ 20 | fields: options?.schema?.team?.additionalFields ?? {}, 21 | isClientSide: true, 22 | }); 23 | const baseSchema = z.object({ 24 | name: z.string().meta({ 25 | description: 'The name of the team. Eg: "my-team"', 26 | }), 27 | organizationId: z 28 | .string() 29 | .meta({ 30 | description: 31 | 'The organization ID which the team will be created in. Defaults to the active organization. Eg: "organization-id"', 32 | }) 33 | .optional(), 34 | }); 35 | return createAuthEndpoint( 36 | "/organization/create-team", 37 | { 38 | method: "POST", 39 | body: z.object({ 40 | ...baseSchema.shape, 41 | ...additionalFieldsSchema.shape, 42 | }), 43 | use: [orgMiddleware], 44 | metadata: { 45 | $Infer: { 46 | body: {} as z.infer<typeof baseSchema> & 47 | InferAdditionalFieldsFromPluginOptions<"team", O>, 48 | }, 49 | openapi: { 50 | description: "Create a new team within an organization", 51 | responses: { 52 | "200": { 53 | description: "Team created successfully", 54 | content: { 55 | "application/json": { 56 | schema: { 57 | type: "object", 58 | properties: { 59 | id: { 60 | type: "string", 61 | description: "Unique identifier of the created team", 62 | }, 63 | name: { 64 | type: "string", 65 | description: "Name of the team", 66 | }, 67 | organizationId: { 68 | type: "string", 69 | description: 70 | "ID of the organization the team belongs to", 71 | }, 72 | createdAt: { 73 | type: "string", 74 | format: "date-time", 75 | description: "Timestamp when the team was created", 76 | }, 77 | updatedAt: { 78 | type: "string", 79 | format: "date-time", 80 | description: "Timestamp when the team was last updated", 81 | }, 82 | }, 83 | required: [ 84 | "id", 85 | "name", 86 | "organizationId", 87 | "createdAt", 88 | "updatedAt", 89 | ], 90 | }, 91 | }, 92 | }, 93 | }, 94 | }, 95 | }, 96 | }, 97 | }, 98 | async (ctx) => { 99 | const session = await getSessionFromCtx(ctx); 100 | const organizationId = 101 | ctx.body.organizationId || session?.session.activeOrganizationId; 102 | if (!session && (ctx.request || ctx.headers)) { 103 | throw new APIError("UNAUTHORIZED"); 104 | } 105 | 106 | if (!organizationId) { 107 | throw new APIError("BAD_REQUEST", { 108 | message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION, 109 | }); 110 | } 111 | const adapter = getOrgAdapter<O>(ctx.context, options as O); 112 | if (session) { 113 | const member = await adapter.findMemberByOrgId({ 114 | userId: session.user.id, 115 | organizationId, 116 | }); 117 | if (!member) { 118 | throw new APIError("FORBIDDEN", { 119 | message: 120 | ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION, 121 | }); 122 | } 123 | const canCreate = await hasPermission( 124 | { 125 | role: member.role, 126 | options: ctx.context.orgOptions, 127 | permissions: { 128 | team: ["create"], 129 | }, 130 | organizationId, 131 | }, 132 | ctx, 133 | ); 134 | 135 | if (!canCreate) { 136 | throw new APIError("FORBIDDEN", { 137 | message: 138 | ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION, 139 | }); 140 | } 141 | } 142 | 143 | const existingTeams = await adapter.listTeams(organizationId); 144 | const maximum = 145 | typeof ctx.context.orgOptions.teams?.maximumTeams === "function" 146 | ? await ctx.context.orgOptions.teams?.maximumTeams( 147 | { 148 | organizationId, 149 | session, 150 | }, 151 | ctx.request, 152 | ) 153 | : ctx.context.orgOptions.teams?.maximumTeams; 154 | 155 | const maxTeamsReached = maximum ? existingTeams.length >= maximum : false; 156 | if (maxTeamsReached) { 157 | throw new APIError("BAD_REQUEST", { 158 | message: 159 | ORGANIZATION_ERROR_CODES.YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS, 160 | }); 161 | } 162 | const { name, organizationId: _, ...additionalFields } = ctx.body; 163 | 164 | const organization = await adapter.findOrganizationById(organizationId); 165 | if (!organization) { 166 | throw new APIError("BAD_REQUEST", { 167 | message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND, 168 | }); 169 | } 170 | 171 | let teamData = { 172 | name, 173 | organizationId, 174 | createdAt: new Date(), 175 | updatedAt: new Date(), 176 | ...additionalFields, 177 | }; 178 | 179 | // Run beforeCreateTeam hook 180 | if (options?.organizationHooks?.beforeCreateTeam) { 181 | const response = await options?.organizationHooks.beforeCreateTeam({ 182 | team: { 183 | name, 184 | organizationId, 185 | ...additionalFields, 186 | }, 187 | user: session?.user, 188 | organization, 189 | }); 190 | if (response && typeof response === "object" && "data" in response) { 191 | teamData = { 192 | ...teamData, 193 | ...response.data, 194 | }; 195 | } 196 | } 197 | 198 | const createdTeam = await adapter.createTeam(teamData); 199 | 200 | // Run afterCreateTeam hook 201 | if (options?.organizationHooks?.afterCreateTeam) { 202 | await options?.organizationHooks.afterCreateTeam({ 203 | team: createdTeam, 204 | user: session?.user, 205 | organization, 206 | }); 207 | } 208 | 209 | return ctx.json(createdTeam); 210 | }, 211 | ); 212 | }; 213 | 214 | export const removeTeam = <O extends OrganizationOptions>(options: O) => 215 | createAuthEndpoint( 216 | "/organization/remove-team", 217 | { 218 | method: "POST", 219 | body: z.object({ 220 | teamId: z.string().meta({ 221 | description: `The team ID of the team to remove. Eg: "team-id"`, 222 | }), 223 | organizationId: z 224 | .string() 225 | .meta({ 226 | description: `The organization ID which the team falls under. If not provided, it will default to the user's active organization. Eg: "organization-id"`, 227 | }) 228 | .optional(), 229 | }), 230 | use: [orgMiddleware], 231 | metadata: { 232 | openapi: { 233 | description: "Remove a team from an organization", 234 | responses: { 235 | "200": { 236 | description: "Team removed successfully", 237 | content: { 238 | "application/json": { 239 | schema: { 240 | type: "object", 241 | properties: { 242 | message: { 243 | type: "string", 244 | description: 245 | "Confirmation message indicating successful removal", 246 | enum: ["Team removed successfully."], 247 | }, 248 | }, 249 | required: ["message"], 250 | }, 251 | }, 252 | }, 253 | }, 254 | }, 255 | }, 256 | }, 257 | }, 258 | async (ctx) => { 259 | const session = await getSessionFromCtx(ctx); 260 | const organizationId = 261 | ctx.body.organizationId || session?.session.activeOrganizationId; 262 | if (!organizationId) { 263 | return ctx.json(null, { 264 | status: 400, 265 | body: { 266 | message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION, 267 | }, 268 | }); 269 | } 270 | if (!session && (ctx.request || ctx.headers)) { 271 | throw new APIError("UNAUTHORIZED"); 272 | } 273 | const adapter = getOrgAdapter<O>(ctx.context, options); 274 | if (session) { 275 | const member = await adapter.findMemberByOrgId({ 276 | userId: session.user.id, 277 | organizationId, 278 | }); 279 | 280 | if (!member || session.session?.activeTeamId === ctx.body.teamId) { 281 | throw new APIError("FORBIDDEN", { 282 | message: 283 | ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM, 284 | }); 285 | } 286 | 287 | const canRemove = await hasPermission( 288 | { 289 | role: member.role, 290 | options: ctx.context.orgOptions, 291 | permissions: { 292 | team: ["delete"], 293 | }, 294 | organizationId, 295 | }, 296 | ctx, 297 | ); 298 | 299 | if (!canRemove) { 300 | throw new APIError("FORBIDDEN", { 301 | message: 302 | ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_TEAMS_IN_THIS_ORGANIZATION, 303 | }); 304 | } 305 | } 306 | const team = await adapter.findTeamById({ 307 | teamId: ctx.body.teamId, 308 | organizationId, 309 | }); 310 | if (!team || team.organizationId !== organizationId) { 311 | throw new APIError("BAD_REQUEST", { 312 | message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND, 313 | }); 314 | } 315 | 316 | if (!ctx.context.orgOptions.teams?.allowRemovingAllTeams) { 317 | const teams = await adapter.listTeams(organizationId); 318 | if (teams.length <= 1) { 319 | throw new APIError("BAD_REQUEST", { 320 | message: ORGANIZATION_ERROR_CODES.UNABLE_TO_REMOVE_LAST_TEAM, 321 | }); 322 | } 323 | } 324 | 325 | const organization = await adapter.findOrganizationById(organizationId); 326 | if (!organization) { 327 | throw new APIError("BAD_REQUEST", { 328 | message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND, 329 | }); 330 | } 331 | 332 | // Run beforeDeleteTeam hook 333 | if (options?.organizationHooks?.beforeDeleteTeam) { 334 | await options?.organizationHooks.beforeDeleteTeam({ 335 | team, 336 | user: session?.user, 337 | organization, 338 | }); 339 | } 340 | 341 | await adapter.deleteTeam(team.id); 342 | 343 | // Run afterDeleteTeam hook 344 | if (options?.organizationHooks?.afterDeleteTeam) { 345 | await options?.organizationHooks.afterDeleteTeam({ 346 | team, 347 | user: session?.user, 348 | organization, 349 | }); 350 | } 351 | 352 | return ctx.json({ message: "Team removed successfully." }); 353 | }, 354 | ); 355 | 356 | export const updateTeam = <O extends OrganizationOptions>(options: O) => { 357 | const additionalFieldsSchema = toZodSchema({ 358 | fields: options?.schema?.team?.additionalFields ?? {}, 359 | isClientSide: true, 360 | }); 361 | 362 | type Body = { 363 | teamId: string; 364 | data: Partial< 365 | PrettifyDeep< 366 | Omit<z.infer<typeof teamSchema>, "id" | "createdAt" | "updatedAt"> 367 | > & 368 | InferAdditionalFieldsFromPluginOptions<"team", O> 369 | >; 370 | }; 371 | 372 | return createAuthEndpoint( 373 | "/organization/update-team", 374 | { 375 | method: "POST", 376 | body: z.object({ 377 | teamId: z.string().meta({ 378 | description: `The ID of the team to be updated. Eg: "team-id"`, 379 | }), 380 | data: z 381 | .object({ 382 | ...teamSchema.shape, 383 | ...additionalFieldsSchema.shape, 384 | }) 385 | .partial(), 386 | }), 387 | requireHeaders: true, 388 | use: [orgMiddleware, orgSessionMiddleware], 389 | metadata: { 390 | $Infer: { body: {} as Body }, 391 | openapi: { 392 | description: "Update an existing team in an organization", 393 | responses: { 394 | "200": { 395 | description: "Team updated successfully", 396 | content: { 397 | "application/json": { 398 | schema: { 399 | type: "object", 400 | properties: { 401 | id: { 402 | type: "string", 403 | description: "Unique identifier of the updated team", 404 | }, 405 | name: { 406 | type: "string", 407 | description: "Updated name of the team", 408 | }, 409 | organizationId: { 410 | type: "string", 411 | description: 412 | "ID of the organization the team belongs to", 413 | }, 414 | createdAt: { 415 | type: "string", 416 | format: "date-time", 417 | description: "Timestamp when the team was created", 418 | }, 419 | updatedAt: { 420 | type: "string", 421 | format: "date-time", 422 | description: "Timestamp when the team was last updated", 423 | }, 424 | }, 425 | required: [ 426 | "id", 427 | "name", 428 | "organizationId", 429 | "createdAt", 430 | "updatedAt", 431 | ], 432 | }, 433 | }, 434 | }, 435 | }, 436 | }, 437 | }, 438 | }, 439 | }, 440 | async (ctx) => { 441 | const session = ctx.context.session; 442 | const organizationId = 443 | ctx.body.data.organizationId || session.session.activeOrganizationId; 444 | if (!organizationId) { 445 | return ctx.json(null, { 446 | status: 400, 447 | body: { 448 | message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION, 449 | }, 450 | }); 451 | } 452 | const adapter = getOrgAdapter<O>(ctx.context, options); 453 | const member = await adapter.findMemberByOrgId({ 454 | userId: session.user.id, 455 | organizationId, 456 | }); 457 | 458 | if (!member) { 459 | throw new APIError("FORBIDDEN", { 460 | message: 461 | ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM, 462 | }); 463 | } 464 | 465 | const canUpdate = await hasPermission( 466 | { 467 | role: member.role, 468 | options: ctx.context.orgOptions, 469 | permissions: { 470 | team: ["update"], 471 | }, 472 | organizationId, 473 | }, 474 | ctx, 475 | ); 476 | 477 | if (!canUpdate) { 478 | throw new APIError("FORBIDDEN", { 479 | message: 480 | ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM, 481 | }); 482 | } 483 | 484 | const team = await adapter.findTeamById({ 485 | teamId: ctx.body.teamId, 486 | organizationId, 487 | }); 488 | 489 | if (!team || team.organizationId !== organizationId) { 490 | throw new APIError("BAD_REQUEST", { 491 | message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND, 492 | }); 493 | } 494 | 495 | const { name, organizationId: __, ...additionalFields } = ctx.body.data; 496 | 497 | const organization = await adapter.findOrganizationById(organizationId); 498 | if (!organization) { 499 | throw new APIError("BAD_REQUEST", { 500 | message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND, 501 | }); 502 | } 503 | 504 | const updates = { 505 | name, 506 | ...additionalFields, 507 | }; 508 | 509 | // Run beforeUpdateTeam hook 510 | if (options?.organizationHooks?.beforeUpdateTeam) { 511 | const response = await options?.organizationHooks.beforeUpdateTeam({ 512 | team, 513 | updates, 514 | user: session.user, 515 | organization, 516 | }); 517 | if (response && typeof response === "object" && "data" in response) { 518 | // Allow the hook to modify the updates 519 | const modifiedUpdates = response.data; 520 | const updatedTeam = await adapter.updateTeam( 521 | team.id, 522 | modifiedUpdates, 523 | ); 524 | 525 | // Run afterUpdateTeam hook 526 | if (options?.organizationHooks?.afterUpdateTeam) { 527 | await options?.organizationHooks.afterUpdateTeam({ 528 | team: updatedTeam, 529 | user: session.user, 530 | organization, 531 | }); 532 | } 533 | 534 | return ctx.json(updatedTeam); 535 | } 536 | } 537 | 538 | const updatedTeam = await adapter.updateTeam(team.id, updates); 539 | 540 | // Run afterUpdateTeam hook 541 | if (options?.organizationHooks?.afterUpdateTeam) { 542 | await options?.organizationHooks.afterUpdateTeam({ 543 | team: updatedTeam, 544 | user: session.user, 545 | organization, 546 | }); 547 | } 548 | 549 | return ctx.json(updatedTeam); 550 | }, 551 | ); 552 | }; 553 | 554 | export const listOrganizationTeams = <O extends OrganizationOptions>( 555 | options: O, 556 | ) => 557 | createAuthEndpoint( 558 | "/organization/list-teams", 559 | { 560 | method: "GET", 561 | query: z.optional( 562 | z.object({ 563 | organizationId: z 564 | .string() 565 | .meta({ 566 | description: `The organization ID which the teams are under to list. Defaults to the users active organization. Eg: "organziation-id"`, 567 | }) 568 | .optional(), 569 | }), 570 | ), 571 | requireHeaders: true, 572 | metadata: { 573 | openapi: { 574 | description: "List all teams in an organization", 575 | responses: { 576 | "200": { 577 | description: "Teams retrieved successfully", 578 | content: { 579 | "application/json": { 580 | schema: { 581 | type: "array", 582 | items: { 583 | type: "object", 584 | properties: { 585 | id: { 586 | type: "string", 587 | description: "Unique identifier of the team", 588 | }, 589 | name: { 590 | type: "string", 591 | description: "Name of the team", 592 | }, 593 | organizationId: { 594 | type: "string", 595 | description: 596 | "ID of the organization the team belongs to", 597 | }, 598 | createdAt: { 599 | type: "string", 600 | format: "date-time", 601 | description: "Timestamp when the team was created", 602 | }, 603 | updatedAt: { 604 | type: "string", 605 | format: "date-time", 606 | description: 607 | "Timestamp when the team was last updated", 608 | }, 609 | }, 610 | required: [ 611 | "id", 612 | "name", 613 | "organizationId", 614 | "createdAt", 615 | "updatedAt", 616 | ], 617 | }, 618 | description: 619 | "Array of team objects within the organization", 620 | }, 621 | }, 622 | }, 623 | }, 624 | }, 625 | }, 626 | }, 627 | use: [orgMiddleware, orgSessionMiddleware], 628 | }, 629 | async (ctx) => { 630 | const session = ctx.context.session; 631 | const organizationId = 632 | ctx.query?.organizationId || session?.session.activeOrganizationId; 633 | if (!organizationId) { 634 | throw ctx.error("BAD_REQUEST", { 635 | message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION, 636 | }); 637 | } 638 | const adapter = getOrgAdapter<O>(ctx.context, options); 639 | const member = await adapter.findMemberByOrgId({ 640 | userId: session.user.id, 641 | organizationId: organizationId || "", 642 | }); 643 | if (!member) { 644 | throw new APIError("FORBIDDEN", { 645 | message: 646 | ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION, 647 | }); 648 | } 649 | const teams = await adapter.listTeams(organizationId); 650 | return ctx.json(teams); 651 | }, 652 | ); 653 | 654 | export const setActiveTeam = <O extends OrganizationOptions>(options: O) => 655 | createAuthEndpoint( 656 | "/organization/set-active-team", 657 | { 658 | method: "POST", 659 | body: z.object({ 660 | teamId: z 661 | .string() 662 | .meta({ 663 | description: 664 | "The team id to set as active. It can be null to unset the active team", 665 | }) 666 | .nullable() 667 | .optional(), 668 | }), 669 | use: [orgSessionMiddleware, orgMiddleware], 670 | metadata: { 671 | openapi: { 672 | description: "Set the active team", 673 | responses: { 674 | "200": { 675 | description: "Success", 676 | content: { 677 | "application/json": { 678 | schema: { 679 | type: "object", 680 | description: "The team", 681 | $ref: "#/components/schemas/Team", 682 | }, 683 | }, 684 | }, 685 | }, 686 | }, 687 | }, 688 | }, 689 | }, 690 | async (ctx) => { 691 | const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions); 692 | const session = ctx.context.session; 693 | 694 | if (ctx.body.teamId === null) { 695 | const sessionTeamId = session.session.activeTeamId; 696 | if (!sessionTeamId) { 697 | return ctx.json(null); 698 | } 699 | 700 | const updatedSession = await adapter.setActiveTeam( 701 | session.session.token, 702 | null, 703 | ctx, 704 | ); 705 | 706 | await setSessionCookie(ctx, { 707 | session: updatedSession, 708 | user: session.user, 709 | }); 710 | 711 | return ctx.json(null); 712 | } 713 | 714 | let teamId: string; 715 | 716 | if (!ctx.body.teamId) { 717 | const sessionTeamId = session.session.activeTeamId; 718 | if (!sessionTeamId) { 719 | return ctx.json(null); 720 | } else { 721 | teamId = sessionTeamId; 722 | } 723 | } else { 724 | teamId = ctx.body.teamId; 725 | } 726 | 727 | const team = await adapter.findTeamById({ teamId }); 728 | 729 | if (!team) { 730 | throw new APIError("BAD_REQUEST", { 731 | message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND, 732 | }); 733 | } 734 | 735 | const member = await adapter.findTeamMember({ 736 | teamId, 737 | userId: session.user.id, 738 | }); 739 | 740 | if (!member) { 741 | throw new APIError("FORBIDDEN", { 742 | message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM, 743 | }); 744 | } 745 | 746 | const updatedSession = await adapter.setActiveTeam( 747 | session.session.token, 748 | team.id, 749 | ctx, 750 | ); 751 | 752 | await setSessionCookie(ctx, { 753 | session: updatedSession, 754 | user: session.user, 755 | }); 756 | 757 | return ctx.json(team); 758 | }, 759 | ); 760 | 761 | export const listUserTeams = <O extends OrganizationOptions>(options: O) => 762 | createAuthEndpoint( 763 | "/organization/list-user-teams", 764 | { 765 | method: "GET", 766 | metadata: { 767 | openapi: { 768 | description: "List all teams that the current user is a part of.", 769 | responses: { 770 | "200": { 771 | description: "Teams retrieved successfully", 772 | content: { 773 | "application/json": { 774 | schema: { 775 | type: "array", 776 | items: { 777 | type: "object", 778 | description: "The team", 779 | $ref: "#/components/schemas/Team", 780 | }, 781 | description: 782 | "Array of team objects within the organization", 783 | }, 784 | }, 785 | }, 786 | }, 787 | }, 788 | }, 789 | }, 790 | use: [orgMiddleware, orgSessionMiddleware], 791 | }, 792 | async (ctx) => { 793 | const session = ctx.context.session; 794 | const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions); 795 | const teams = await adapter.listTeamsByUser({ 796 | userId: session.user.id, 797 | }); 798 | 799 | return ctx.json(teams); 800 | }, 801 | ); 802 | 803 | export const listTeamMembers = <O extends OrganizationOptions>(options: O) => 804 | createAuthEndpoint( 805 | "/organization/list-team-members", 806 | { 807 | method: "GET", 808 | query: z.optional( 809 | z.object({ 810 | teamId: z.string().optional().meta({ 811 | description: 812 | "The team whose members we should return. If this is not provided the members of the current active team get returned.", 813 | }), 814 | }), 815 | ), 816 | metadata: { 817 | openapi: { 818 | description: "List the members of the given team.", 819 | responses: { 820 | "200": { 821 | description: "Teams retrieved successfully", 822 | content: { 823 | "application/json": { 824 | schema: { 825 | type: "array", 826 | items: { 827 | type: "object", 828 | description: "The team member", 829 | properties: { 830 | id: { 831 | type: "string", 832 | description: "Unique identifier of the team member", 833 | }, 834 | userId: { 835 | type: "string", 836 | description: "The user ID of the team member", 837 | }, 838 | teamId: { 839 | type: "string", 840 | description: 841 | "The team ID of the team the team member is in", 842 | }, 843 | createdAt: { 844 | type: "string", 845 | format: "date-time", 846 | description: 847 | "Timestamp when the team member was created", 848 | }, 849 | }, 850 | required: ["id", "userId", "teamId", "createdAt"], 851 | }, 852 | description: "Array of team member objects within the team", 853 | }, 854 | }, 855 | }, 856 | }, 857 | }, 858 | }, 859 | }, 860 | use: [orgMiddleware, orgSessionMiddleware], 861 | }, 862 | async (ctx) => { 863 | const session = ctx.context.session; 864 | const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions); 865 | let teamId = ctx.query?.teamId || session?.session.activeTeamId; 866 | if (!teamId) { 867 | throw new APIError("BAD_REQUEST", { 868 | message: ORGANIZATION_ERROR_CODES.YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM, 869 | }); 870 | } 871 | const member = await adapter.findTeamMember({ 872 | userId: session.user.id, 873 | teamId, 874 | }); 875 | 876 | if (!member) { 877 | throw new APIError("BAD_REQUEST", { 878 | message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM, 879 | }); 880 | } 881 | const members = await adapter.listTeamMembers({ 882 | teamId, 883 | }); 884 | return ctx.json(members); 885 | }, 886 | ); 887 | 888 | export const addTeamMember = <O extends OrganizationOptions>(options: O) => 889 | createAuthEndpoint( 890 | "/organization/add-team-member", 891 | { 892 | method: "POST", 893 | body: z.object({ 894 | teamId: z.string().meta({ 895 | description: "The team the user should be a member of.", 896 | }), 897 | 898 | userId: z.coerce.string().meta({ 899 | description: 900 | "The user Id which represents the user to be added as a member.", 901 | }), 902 | }), 903 | metadata: { 904 | openapi: { 905 | description: "The newly created member", 906 | responses: { 907 | "200": { 908 | description: "Team member created successfully", 909 | content: { 910 | "application/json": { 911 | schema: { 912 | type: "object", 913 | description: "The team member", 914 | properties: { 915 | id: { 916 | type: "string", 917 | description: "Unique identifier of the team member", 918 | }, 919 | userId: { 920 | type: "string", 921 | description: "The user ID of the team member", 922 | }, 923 | teamId: { 924 | type: "string", 925 | description: 926 | "The team ID of the team the team member is in", 927 | }, 928 | createdAt: { 929 | type: "string", 930 | format: "date-time", 931 | description: 932 | "Timestamp when the team member was created", 933 | }, 934 | }, 935 | required: ["id", "userId", "teamId", "createdAt"], 936 | }, 937 | }, 938 | }, 939 | }, 940 | }, 941 | }, 942 | }, 943 | use: [orgMiddleware, orgSessionMiddleware], 944 | }, 945 | async (ctx) => { 946 | const session = ctx.context.session; 947 | const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions); 948 | 949 | if (!session.session.activeOrganizationId) { 950 | throw new APIError("BAD_REQUEST", { 951 | message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION, 952 | }); 953 | } 954 | 955 | const currentMember = await adapter.findMemberByOrgId({ 956 | userId: session.user.id, 957 | organizationId: session.session.activeOrganizationId, 958 | }); 959 | 960 | if (!currentMember) { 961 | throw new APIError("BAD_REQUEST", { 962 | message: 963 | ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION, 964 | }); 965 | } 966 | 967 | const canUpdateMember = await hasPermission( 968 | { 969 | role: currentMember.role, 970 | options: ctx.context.orgOptions, 971 | permissions: { 972 | member: ["update"], 973 | }, 974 | organizationId: session.session.activeOrganizationId, 975 | }, 976 | ctx, 977 | ); 978 | 979 | if (!canUpdateMember) { 980 | throw new APIError("FORBIDDEN", { 981 | message: 982 | ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER, 983 | }); 984 | } 985 | 986 | const toBeAddedMember = await adapter.findMemberByOrgId({ 987 | userId: ctx.body.userId, 988 | organizationId: session.session.activeOrganizationId, 989 | }); 990 | 991 | if (!toBeAddedMember) { 992 | throw new APIError("BAD_REQUEST", { 993 | message: 994 | ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION, 995 | }); 996 | } 997 | 998 | const team = await adapter.findTeamById({ 999 | teamId: ctx.body.teamId, 1000 | organizationId: session.session.activeOrganizationId, 1001 | }); 1002 | 1003 | if (!team) { 1004 | throw new APIError("BAD_REQUEST", { 1005 | message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND, 1006 | }); 1007 | } 1008 | 1009 | const organization = await adapter.findOrganizationById( 1010 | session.session.activeOrganizationId, 1011 | ); 1012 | if (!organization) { 1013 | throw new APIError("BAD_REQUEST", { 1014 | message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND, 1015 | }); 1016 | } 1017 | 1018 | const userBeingAdded = await ctx.context.internalAdapter.findUserById( 1019 | ctx.body.userId, 1020 | ); 1021 | if (!userBeingAdded) { 1022 | throw new APIError("BAD_REQUEST", { 1023 | message: "User not found", 1024 | }); 1025 | } 1026 | 1027 | // Run beforeAddTeamMember hook 1028 | if (options?.organizationHooks?.beforeAddTeamMember) { 1029 | const response = await options?.organizationHooks.beforeAddTeamMember({ 1030 | teamMember: { 1031 | teamId: ctx.body.teamId, 1032 | userId: ctx.body.userId, 1033 | }, 1034 | team, 1035 | user: userBeingAdded, 1036 | organization, 1037 | }); 1038 | if (response && typeof response === "object" && "data" in response) { 1039 | // Allow the hook to modify the data 1040 | } 1041 | } 1042 | 1043 | const teamMember = await adapter.findOrCreateTeamMember({ 1044 | teamId: ctx.body.teamId, 1045 | userId: ctx.body.userId, 1046 | }); 1047 | 1048 | // Run afterAddTeamMember hook 1049 | if (options?.organizationHooks?.afterAddTeamMember) { 1050 | await options?.organizationHooks.afterAddTeamMember({ 1051 | teamMember, 1052 | team, 1053 | user: userBeingAdded, 1054 | organization, 1055 | }); 1056 | } 1057 | 1058 | return ctx.json(teamMember); 1059 | }, 1060 | ); 1061 | 1062 | export const removeTeamMember = <O extends OrganizationOptions>(options: O) => 1063 | createAuthEndpoint( 1064 | "/organization/remove-team-member", 1065 | { 1066 | method: "POST", 1067 | body: z.object({ 1068 | teamId: z.string().meta({ 1069 | description: "The team the user should be removed from.", 1070 | }), 1071 | 1072 | userId: z.coerce.string().meta({ 1073 | description: "The user which should be removed from the team.", 1074 | }), 1075 | }), 1076 | metadata: { 1077 | openapi: { 1078 | description: "Remove a member from a team", 1079 | responses: { 1080 | "200": { 1081 | description: "Team member removed successfully", 1082 | content: { 1083 | "application/json": { 1084 | schema: { 1085 | type: "object", 1086 | properties: { 1087 | message: { 1088 | type: "string", 1089 | description: 1090 | "Confirmation message indicating successful removal", 1091 | enum: ["Team member removed successfully."], 1092 | }, 1093 | }, 1094 | required: ["message"], 1095 | }, 1096 | }, 1097 | }, 1098 | }, 1099 | }, 1100 | }, 1101 | }, 1102 | use: [orgMiddleware, orgSessionMiddleware], 1103 | }, 1104 | async (ctx) => { 1105 | const session = ctx.context.session; 1106 | const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions); 1107 | 1108 | if (!session.session.activeOrganizationId) { 1109 | throw new APIError("BAD_REQUEST", { 1110 | message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION, 1111 | }); 1112 | } 1113 | 1114 | const currentMember = await adapter.findMemberByOrgId({ 1115 | userId: session.user.id, 1116 | organizationId: session.session.activeOrganizationId, 1117 | }); 1118 | 1119 | if (!currentMember) { 1120 | throw new APIError("BAD_REQUEST", { 1121 | message: 1122 | ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION, 1123 | }); 1124 | } 1125 | 1126 | const canDeleteMember = await hasPermission( 1127 | { 1128 | role: currentMember.role, 1129 | options: ctx.context.orgOptions, 1130 | permissions: { 1131 | member: ["delete"], 1132 | }, 1133 | organizationId: session.session.activeOrganizationId, 1134 | }, 1135 | ctx, 1136 | ); 1137 | 1138 | if (!canDeleteMember) { 1139 | throw new APIError("FORBIDDEN", { 1140 | message: 1141 | ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER, 1142 | }); 1143 | } 1144 | 1145 | const toBeAddedMember = await adapter.findMemberByOrgId({ 1146 | userId: ctx.body.userId, 1147 | organizationId: session.session.activeOrganizationId, 1148 | }); 1149 | 1150 | if (!toBeAddedMember) { 1151 | throw new APIError("BAD_REQUEST", { 1152 | message: 1153 | ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION, 1154 | }); 1155 | } 1156 | 1157 | const team = await adapter.findTeamById({ 1158 | teamId: ctx.body.teamId, 1159 | organizationId: session.session.activeOrganizationId, 1160 | }); 1161 | 1162 | if (!team) { 1163 | throw new APIError("BAD_REQUEST", { 1164 | message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND, 1165 | }); 1166 | } 1167 | 1168 | const organization = await adapter.findOrganizationById( 1169 | session.session.activeOrganizationId, 1170 | ); 1171 | if (!organization) { 1172 | throw new APIError("BAD_REQUEST", { 1173 | message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND, 1174 | }); 1175 | } 1176 | 1177 | const userBeingRemoved = await ctx.context.internalAdapter.findUserById( 1178 | ctx.body.userId, 1179 | ); 1180 | if (!userBeingRemoved) { 1181 | throw new APIError("BAD_REQUEST", { 1182 | message: "User not found", 1183 | }); 1184 | } 1185 | 1186 | const teamMember = await adapter.findTeamMember({ 1187 | teamId: ctx.body.teamId, 1188 | userId: ctx.body.userId, 1189 | }); 1190 | 1191 | if (!teamMember) { 1192 | throw new APIError("BAD_REQUEST", { 1193 | message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM, 1194 | }); 1195 | } 1196 | 1197 | // Run beforeRemoveTeamMember hook 1198 | if (options?.organizationHooks?.beforeRemoveTeamMember) { 1199 | await options?.organizationHooks.beforeRemoveTeamMember({ 1200 | teamMember, 1201 | team, 1202 | user: userBeingRemoved, 1203 | organization, 1204 | }); 1205 | } 1206 | 1207 | await adapter.removeTeamMember({ 1208 | teamId: ctx.body.teamId, 1209 | userId: ctx.body.userId, 1210 | }); 1211 | 1212 | // Run afterRemoveTeamMember hook 1213 | if (options?.organizationHooks?.afterRemoveTeamMember) { 1214 | await options?.organizationHooks.afterRemoveTeamMember({ 1215 | teamMember, 1216 | team, 1217 | user: userBeingRemoved, 1218 | organization, 1219 | }); 1220 | } 1221 | 1222 | return ctx.json({ message: "Team member removed successfully." }); 1223 | }, 1224 | ); 1225 | ```