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=="],