11import { v } from "convex/values" ;
22import { internal } from "./_generated/api" ;
3- import { httpAction , internalMutation , internalQuery , mutation , query } from "./_generated/server" ;
3+ import {
4+ httpAction ,
5+ internalMutation ,
6+ internalQuery ,
7+ mutation ,
8+ query ,
9+ } from "./_generated/server" ;
410import { requireAdminToken } from "./_shared/adminToken" ;
511
612declare const process : { env : Record < string , string | undefined > } ;
@@ -12,7 +18,8 @@ const GMAIL_SCOPES = [
1218] ;
1319const GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth" ;
1420const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token" ;
15- const GMAIL_PROFILE_URL = "https://gmail.googleapis.com/gmail/v1/users/me/profile" ;
21+ const GMAIL_PROFILE_URL =
22+ "https://gmail.googleapis.com/gmail/v1/users/me/profile" ;
1623
1724type GoogleTokenResponse = {
1825 access_token ?: string ;
@@ -75,7 +82,8 @@ export const consumeAdminNonce = internalMutation({
7582 . query ( "gmailOAuthStates" )
7683 . withIndex ( "by_state" , ( q ) => q . eq ( "state" , args . nonce ) )
7784 . unique ( ) ;
78- if ( ! row || ! row . adminNonce || row . usedAt || row . expiresAt < Date . now ( ) ) return false ;
85+ if ( ! row || ! row . adminNonce || row . usedAt || row . expiresAt < Date . now ( ) )
86+ return false ;
7987 await ctx . db . patch ( row . _id , { usedAt : Date . now ( ) } ) ;
8088 return true ;
8189 } ,
@@ -89,8 +97,12 @@ export const start = httpAction(async (ctx, request) => {
8997 // compatibility, but the frontend should always use the nonce path.
9098 const nonce = url . searchParams . get ( "nonce" ) ;
9199 if ( nonce ) {
92- const valid = await ctx . runMutation ( internal . gmailOAuth . consumeAdminNonce , { nonce } ) ;
93- if ( ! valid ) return textResponse ( "Nonce is invalid, already used, or expired." , 403 ) ;
100+ const valid = await ctx . runMutation (
101+ internal . gmailOAuth . consumeAdminNonce ,
102+ { nonce } ,
103+ ) ;
104+ if ( ! valid )
105+ return textResponse ( "Nonce is invalid, already used, or expired." , 403 ) ;
94106 } else {
95107 const token = url . searchParams . get ( "token" ) ?? "" ;
96108 requireAdminToken ( token ) ;
@@ -122,19 +134,36 @@ export const callback = httpAction(async (ctx, request) => {
122134 const code = url . searchParams . get ( "code" ) ?? "" ;
123135 const oauthError = url . searchParams . get ( "error" ) ;
124136
125- if ( oauthError ) return htmlResponse ( renderResultPage ( "Gmail connection failed" , oauthError ) , 400 ) ;
126- if ( ! state || ! code ) return htmlResponse ( renderResultPage ( "Gmail connection failed" , "Missing OAuth state or code." ) , 400 ) ;
137+ if ( oauthError )
138+ return htmlResponse (
139+ renderResultPage ( "Gmail connection failed" , oauthError ) ,
140+ 400 ,
141+ ) ;
142+ if ( ! state || ! code )
143+ return htmlResponse (
144+ renderResultPage (
145+ "Gmail connection failed" ,
146+ "Missing OAuth state or code." ,
147+ ) ,
148+ 400 ,
149+ ) ;
127150
128151 try {
129- const stateRow = await ctx . runQuery ( internal . gmailOAuth . getValidOAuthState , { state } ) ;
152+ const stateRow = await ctx . runQuery (
153+ internal . gmailOAuth . getValidOAuthState ,
154+ { state } ,
155+ ) ;
130156 if ( ! stateRow ) throw new Error ( "OAuth state is invalid or expired." ) ;
131157
132158 const redirectUri = getRedirectUri ( request ) ;
133159 const tokenResponse = await exchangeCodeForTokens ( code , redirectUri ) ;
134160 if ( ! tokenResponse . refresh_token ) {
135- throw new Error ( "Google did not return a refresh token. Try reconnecting and approving consent again." ) ;
161+ throw new Error (
162+ "Google did not return a refresh token. Try reconnecting and approving consent again." ,
163+ ) ;
136164 }
137- if ( ! tokenResponse . access_token ) throw new Error ( "Google did not return an access token." ) ;
165+ if ( ! tokenResponse . access_token )
166+ throw new Error ( "Google did not return an access token." ) ;
138167
139168 const profile = await getGmailProfile ( tokenResponse . access_token ) ;
140169 const email = profile . emailAddress ?. toLowerCase ( ) ;
@@ -147,9 +176,17 @@ export const callback = httpAction(async (ctx, request) => {
147176 scopes : tokenResponse . scope ?. split ( " " ) ?? GMAIL_SCOPES ,
148177 } ) ;
149178
150- return htmlResponse ( renderResultPage ( "Gmail connected" , `Connected ${ email } . You can close this tab.` ) ) ;
179+ return htmlResponse (
180+ renderResultPage (
181+ "Gmail connected" ,
182+ `Connected ${ email } . You can close this tab.` ,
183+ ) ,
184+ ) ;
151185 } catch ( error ) {
152- return htmlResponse ( renderResultPage ( "Gmail connection failed" , formatError ( error ) ) , 400 ) ;
186+ return htmlResponse (
187+ renderResultPage ( "Gmail connection failed" , formatError ( error ) ) ,
188+ 400 ,
189+ ) ;
153190 }
154191} ) ;
155192
@@ -247,7 +284,9 @@ async function exchangeCodeForTokens(code: string, redirectUri: string) {
247284 const body = ( await response . json ( ) ) as GoogleTokenResponse ;
248285
249286 if ( ! response . ok ) {
250- throw new Error ( body . error_description ?? body . error ?? "Google token exchange failed." ) ;
287+ throw new Error (
288+ body . error_description ?? body . error ?? "Google token exchange failed." ,
289+ ) ;
251290 }
252291
253292 return body ;
@@ -257,7 +296,8 @@ async function getGmailProfile(accessToken: string) {
257296 const response = await fetch ( GMAIL_PROFILE_URL , {
258297 headers : { Authorization : `Bearer ${ accessToken } ` } ,
259298 } ) ;
260- if ( ! response . ok ) throw new Error ( `Gmail profile request failed: ${ await response . text ( ) } ` ) ;
299+ if ( ! response . ok )
300+ throw new Error ( `Gmail profile request failed: ${ await response . text ( ) } ` ) ;
261301 return ( await response . json ( ) ) as GmailProfileResponse ;
262302}
263303
@@ -268,11 +308,17 @@ function getRequiredEnv(name: string) {
268308}
269309
270310function textResponse ( text : string , status = 200 ) {
271- return new Response ( text , { status, headers : { "Content-Type" : "text/plain; charset=utf-8" } } ) ;
311+ return new Response ( text , {
312+ status,
313+ headers : { "Content-Type" : "text/plain; charset=utf-8" } ,
314+ } ) ;
272315}
273316
274317function htmlResponse ( html : string , status = 200 ) {
275- return new Response ( html , { status, headers : { "Content-Type" : "text/html; charset=utf-8" } } ) ;
318+ return new Response ( html , {
319+ status,
320+ headers : { "Content-Type" : "text/html; charset=utf-8" } ,
321+ } ) ;
276322}
277323
278324function renderResultPage ( title : string , message : string ) {
0 commit comments