diff --git a/apps/mail/app/api/v1/mail/auth/[providerId]/callback/route.ts b/apps/mail/app/api/v1/mail/auth/[providerId]/callback/route.ts
deleted file mode 100644
index f436c7ff57..0000000000
--- a/apps/mail/app/api/v1/mail/auth/[providerId]/callback/route.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { type NextRequest, NextResponse } from "next/server";
-import { createDriver } from "@/app/api/driver";
-import { connection } from "@zero/db/schema";
-import { db } from "@zero/db";
-
-export async function GET(
- request: NextRequest,
- { params }: { params: Promise<{ providerId: string }> },
-) {
- const searchParams = request.nextUrl.searchParams;
- const code = searchParams.get("code");
- const state = searchParams.get("state");
-
- if (!code || !state) {
- return NextResponse.redirect(
- `${process.env.NEXT_PUBLIC_APP_URL}/settings/email?error=missing_params`,
- );
- }
-
- const { providerId } = await params;
-
- const driver = await createDriver(providerId, {});
-
- try {
- // Exchange the authorization code for tokens
- const { tokens } = await driver.getTokens(code);
-
- if (!tokens.access_token || !tokens.refresh_token) {
- console.error("Missing tokens:", tokens);
- return new NextResponse(JSON.stringify({ error: "Could not get token" }), { status: 400 });
- }
-
- // Get user info using the access token
- const userInfo = await driver.getUserInfo({
- access_token: tokens.access_token,
- refresh_token: tokens.refresh_token,
- email: ''
- });
-
- if (!userInfo.data?.emailAddresses?.[0]?.value) {
- console.error("Missing email in user info:", userInfo);
- return new NextResponse(JSON.stringify({ error: 'Missing "email" in user info' }), {
- status: 400,
- });
- }
-
- // Store the connection in the database
- await db.insert(connection).values({
- providerId,
- id: crypto.randomUUID(),
- userId: state,
- email: userInfo.data.emailAddresses[0].value,
- name: userInfo.data.names?.[0]?.displayName || "Unknown",
- picture: userInfo.data.photos?.[0]?.url || "",
- accessToken: tokens.access_token,
- refreshToken: tokens.refresh_token,
- scope: driver.getScope(),
- expiresAt: new Date(Date.now() + (tokens.expiry_date || 3600000)),
- createdAt: new Date(),
- updatedAt: new Date(),
- });
-
- return NextResponse.redirect(new URL("/mail", request.url));
- } catch (error) {
- return new NextResponse(JSON.stringify({ error }));
- }
-}
diff --git a/apps/mail/app/api/v1/mail/auth/[providerId]/init/route.ts b/apps/mail/app/api/v1/mail/auth/[providerId]/init/route.ts
deleted file mode 100644
index b85dec4d36..0000000000
--- a/apps/mail/app/api/v1/mail/auth/[providerId]/init/route.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { type NextRequest, NextResponse } from "next/server";
-import { createDriver } from "@/app/api/driver";
-import { auth } from "@/lib/auth";
-
-export async function GET(
- request: NextRequest,
- { params }: { params: Promise<{ providerId: string }> },
-) {
- const session = await auth.api.getSession({ headers: request.headers });
- const userId = session?.user?.id;
-
- if (!userId) {
- return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
- }
-
- const { providerId } = await params;
- const driver = await createDriver(providerId, {});
- const authUrl = driver.generateConnectionAuthUrl(userId);
- return NextResponse.redirect(authUrl);
-}
diff --git a/apps/mail/components/connection/add.tsx b/apps/mail/components/connection/add.tsx
index 9bbed99a13..6d509f88ab 100644
--- a/apps/mail/components/connection/add.tsx
+++ b/apps/mail/components/connection/add.tsx
@@ -7,6 +7,7 @@ import {
DialogTrigger,
} from '../ui/dialog';
import { emailProviders } from '@/lib/constants';
+import { authClient } from '@/lib/auth-client';
import { Plus, UserPlus } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { Button } from '../ui/button';
@@ -52,9 +53,8 @@ export const AddConnectionDialog = ({
transition={{ duration: 0.3 }}
>
{emailProviders.map((provider, index) => (
-
+ await authClient.linkSocial({
+ provider: provider.providerId,
+ })
+ }
>
{provider.name}
-
+
))}
console.log(args) } };
+const connectionHandlerHook = async (account: Account) => {
+ if (!account.accessToken || !account.refreshToken) {
+ console.error('Missing Access/Refresh Tokens', { account });
+ throw new APIError('EXPECTATION_FAILED', { message: 'Missing Access/Refresh Tokens' });
+ }
+
+ const driver = await createDriver(account.providerId, {});
+ const userInfo = await driver
+ .getUserInfo({
+ access_token: account.accessToken,
+ refresh_token: account.refreshToken,
+ email: '',
+ })
+ .catch(() => {
+ throw new APIError('UNAUTHORIZED', { message: 'Failed to get user info' });
+ });
+
+ if (!userInfo.data?.emailAddresses?.[0]?.value) {
+ console.error('Missing email in user info:', { userInfo });
+ throw new APIError('BAD_REQUEST', { message: 'Missing "email" in user info' });
+ }
+
+ const updatingInfo = {
+ name: userInfo.data.names?.[0]?.displayName || 'Unknown',
+ picture: userInfo.data.photos?.[0]?.url || '',
+ accessToken: account.accessToken,
+ refreshToken: account.refreshToken,
+ scope: driver.getScope(),
+ expiresAt: new Date(Date.now() + (account.accessTokenExpiresAt?.getTime() || 3600000)),
+ };
+
+ await db
+ .insert(connection)
+ .values({
+ providerId: account.providerId,
+ id: crypto.randomUUID(),
+ email: userInfo.data.emailAddresses[0].value,
+ userId: account.userId,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ ...updatingInfo,
+ })
+ .onConflictDoUpdate({
+ target: [connection.email, connection.userId],
+ set: {
+ ...updatingInfo,
+ updatedAt: new Date(),
+ },
+ });
+};
+
const options = {
- database: drizzleAdapter(db, {
- provider: 'pg',
- }),
+ database: drizzleAdapter(db, { provider: 'pg' }),
advanced: {
ipAddress: {
disableIpTracking: true,
@@ -31,6 +82,23 @@ const options = {
updateAge: 60 * 60 * 24, // 1 day (every 1 day the session expiration is updated)
},
socialProviders: getSocialProviders(),
+ account: {
+ accountLinking: {
+ enabled: true,
+ allowDifferentEmails: true,
+ trustedProviders: ['google'],
+ },
+ },
+ databaseHooks: {
+ account: {
+ create: {
+ after: connectionHandlerHook,
+ },
+ update: {
+ after: connectionHandlerHook,
+ },
+ },
+ },
emailAndPassword: {
enabled: false,
requireEmailVerification: true,
diff --git a/apps/mail/lib/constants.ts b/apps/mail/lib/constants.ts
index a99c030f3b..6875462519 100644
--- a/apps/mail/lib/constants.ts
+++ b/apps/mail/lib/constants.ts
@@ -103,4 +103,4 @@ export const emailProviders = [
icon: "M11.99 13.9v-3.72h9.36c.14.63.25 1.22.25 2.05c0 5.71-3.83 9.77-9.6 9.77c-5.52 0-10-4.48-10-10S6.48 2 12 2c2.7 0 4.96.99 6.69 2.61l-2.84 2.76c-.72-.68-1.98-1.48-3.85-1.48c-3.31 0-6.01 2.75-6.01 6.12s2.7 6.12 6.01 6.12c3.83 0 5.24-2.65 5.5-4.22h-5.51z",
providerId: "google",
},
-];
+] as const;
diff --git a/apps/mail/package.json b/apps/mail/package.json
index abdb292d6b..1290999539 100644
--- a/apps/mail/package.json
+++ b/apps/mail/package.json
@@ -60,7 +60,7 @@
"@zero/db": "workspace:*",
"@zero/eslint-config": "workspace:*",
"axios": "1.8.1",
- "better-auth": "1.2.1",
+ "better-auth": "1.2.7",
"canvas-confetti": "1.9.3",
"cheerio": "1.0.0",
"class-variance-authority": "0.7.1",
diff --git a/bun.lock b/bun.lock
index debab39f48..9d9c722985 100644
--- a/bun.lock
+++ b/bun.lock
@@ -71,7 +71,7 @@
"@zero/db": "workspace:*",
"@zero/eslint-config": "workspace:*",
"axios": "1.8.1",
- "better-auth": "1.2.1",
+ "better-auth": "1.2.7",
"canvas-confetti": "1.9.3",
"cheerio": "1.0.0",
"class-variance-authority": "0.7.1",
@@ -195,7 +195,7 @@
"@babel/runtime-corejs3": ["@babel/runtime-corejs3@7.27.0", "", { "dependencies": { "core-js-pure": "^3.30.2", "regenerator-runtime": "^0.14.0" } }, "sha512-UWjX6t+v+0ckwZ50Y5ShZLnlk95pP5MyW/pon9tiYzl3+18pkTHTFNTKr7rQbfRXPkowt2QAn30o1b6oswszew=="],
- "@better-auth/utils": ["@better-auth/utils@0.2.3", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-Ap1GaSmo6JYhJhxJOpUB0HobkKPTNzfta+bLV89HfpyCAHN7p8ntCrmNFHNAVD0F6v0mywFVEUg1FUhNCc81Rw=="],
+ "@better-auth/utils": ["@better-auth/utils@0.2.4", "", { "dependencies": { "typescript": "^5.8.2", "uncrypto": "^0.1.3" } }, "sha512-ayiX87Xd5sCHEplAdeMgwkA0FgnXsEZBgDn890XHHwSWNqqRZDYOq3uj2Ei2leTv1I2KbG5HHn60Ah1i2JWZjQ=="],
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="],
@@ -851,9 +851,9 @@
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
- "better-auth": ["better-auth@1.2.1", "", { "dependencies": { "@better-auth/utils": "0.2.3", "@better-fetch/fetch": "^1.1.15", "@noble/ciphers": "^0.6.0", "@noble/hashes": "^1.6.1", "@simplewebauthn/browser": "^13.0.0", "@simplewebauthn/server": "^13.0.0", "better-call": "^1.0.3", "defu": "^6.1.4", "jose": "^5.9.6", "kysely": "^0.27.4", "nanostores": "^0.11.3", "valibot": "1.0.0-beta.15", "zod": "^3.24.1" } }, "sha512-ehECh654Y32pseRiAwHiDdqemCX5oM/B/N52heqVcRbgiVKC61FgdrBwBkQb9jV2jBk7E+C8iDZ5Nqshck3O1g=="],
+ "better-auth": ["better-auth@1.2.7", "", { "dependencies": { "@better-auth/utils": "0.2.4", "@better-fetch/fetch": "^1.1.18", "@noble/ciphers": "^0.6.0", "@noble/hashes": "^1.6.1", "@simplewebauthn/browser": "^13.0.0", "@simplewebauthn/server": "^13.0.0", "better-call": "^1.0.8", "defu": "^6.1.4", "jose": "^5.9.6", "kysely": "^0.27.6", "nanostores": "^0.11.3", "zod": "^3.24.1" } }, "sha512-2hCB263GSrgetsMUZw8vv9O1e4S4AlYJW3P4e8bX9u3Q3idv4u9BzDFCblpTLuL4YjYovghMCN0vurAsctXOAQ=="],
- "better-call": ["better-call@1.0.7", "", { "dependencies": { "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-p5kEthErx3HsW9dCCvvEx+uuEdncn0ZrlqrOG3TkR1aVYgynpwYbTVU90nY8/UwfMhROzqZWs8vryainSQxrNg=="],
+ "better-call": ["better-call@1.0.8", "", { "dependencies": { "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-/PV8JLqDRUN7JyBPbklVsS/8E4SO3pnf8hbpa8B7xrBrr+BBYpeOAxoqtnsyk/pRs35vNB4MZx8cn9dBuNlLDA=="],
"bignumber.js": ["bignumber.js@9.2.0", "", {}, "sha512-JocpCSOixzy5XFJi2ub6IMmV/G9i8Lrm2lZvwBv9xPdglmZM0ufDVBbjbrfU/zuLvBfD7Bv2eYxz9i+OHTgkew=="],
@@ -2053,8 +2053,6 @@
"uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
- "valibot": ["valibot@1.0.0-beta.15", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-BKy8XosZkDHWmYC+cJG74LBzP++Gfntwi33pP3D3RKztz2XV9jmFWnkOi21GoqARP8wAWARwhV6eTr1JcWzjGw=="],
-
"vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],