diff --git a/src/google/auth/base.ts b/src/google/auth/base.ts index a089e82..85ef69c 100644 --- a/src/google/auth/base.ts +++ b/src/google/auth/base.ts @@ -1,5 +1,5 @@ -import * as google from 'googleapis'; +import * as google from 'googleapis' export interface AuthClient { - getAuth(): google.Auth.GoogleAuth + getAuthHeadersClient(): google.Auth.GoogleAuth | google.Auth.OAuth2Client } diff --git a/src/google/auth/index.ts b/src/google/auth/index.ts index 01971cc..eadb5f6 100644 --- a/src/google/auth/index.ts +++ b/src/google/auth/index.ts @@ -1,4 +1,5 @@ export * from './base' export * from './models' +export * from './oauth2' export * from './service_account' export * from './models' \ No newline at end of file diff --git a/src/google/auth/oauth2.ts b/src/google/auth/oauth2.ts new file mode 100644 index 0000000..6151ed0 --- /dev/null +++ b/src/google/auth/oauth2.ts @@ -0,0 +1,65 @@ +import * as google from 'googleapis'; +import { authenticate } from '@google-cloud/local-auth' + +import * as fs from 'fs'; +import { AuthClient } from './base'; + +export class Oauth2GoogleAuthClient implements AuthClient { + private auth!: google.Auth.OAuth2Client; + + private constructor(auth: google.Auth.OAuth2Client) { + this.auth = auth; + } + + public static async fromFile( + secretFilePath: string, + credsFilePath: string, + scopes: string[], + ): Promise { + const storedClient = await this.loadCredsIfExists(credsFilePath); + if (storedClient) { + return new Oauth2GoogleAuthClient(storedClient); + } + + const newClient = await authenticate({ + scopes: scopes, + keyfilePath: secretFilePath, + }); + + if (newClient.credentials) { + await this.storeCredentials(secretFilePath, credsFilePath, newClient); + } + return new Oauth2GoogleAuthClient(newClient); + } + + private static async loadCredsIfExists(credsFilePath: string): Promise { + try { + const content = await fs.promises.readFile(credsFilePath); + const credentials = JSON.parse(content.toString()); + return google.google.auth.fromJSON(credentials) as google.Auth.OAuth2Client; + } catch (err) { + return null; + } + } + + private static async storeCredentials( + secretFilePath: string, + credsFilePath: string, + client: google.Auth.OAuth2Client, + ) { + const content = await fs.promises.readFile(secretFilePath); + const keys = JSON.parse(content.toString()); + const key = keys.installed || keys.web; + const payload = JSON.stringify({ + type: 'authorized_user', + client_id: key.client_id, + client_secret: key.client_secret, + refresh_token: client.credentials.refresh_token, + }); + await fs.promises.writeFile(credsFilePath, payload); + } + + public getAuthHeadersClient(): google.Auth.GoogleAuth | google.Auth.OAuth2Client { + return this.auth!; + } +} \ No newline at end of file diff --git a/src/google/auth/service_account.ts b/src/google/auth/service_account.ts index cac84b8..5c478b3 100644 --- a/src/google/auth/service_account.ts +++ b/src/google/auth/service_account.ts @@ -25,7 +25,7 @@ export class ServiceAccountGoogleAuthClient implements AuthClient { return new ServiceAccountGoogleAuthClient(authClient); } - public getAuth(): google.Auth.GoogleAuth { + public getAuthHeadersClient(): google.Auth.GoogleAuth | google.Auth.OAuth2Client { return this.auth!; } } \ No newline at end of file diff --git a/src/google/sheets/wrapper.ts b/src/google/sheets/wrapper.ts index 60cffa0..b16051d 100644 --- a/src/google/sheets/wrapper.ts +++ b/src/google/sheets/wrapper.ts @@ -1,6 +1,5 @@ -import { google, sheets_v4 } from 'googleapis'; +import * as google from 'googleapis'; import axios, { AxiosInstance } from 'axios'; -import { GoogleAuth } from 'google-auth-library'; import { AuthClient } from '../auth/base'; import { @@ -20,13 +19,13 @@ import { } from './models'; export class Wrapper { - private googleAuth: GoogleAuth; - private service: sheets_v4.Sheets; + private authClient: google.Auth.GoogleAuth | google.Auth.OAuth2Client; + private service: google.sheets_v4.Sheets; private rawClient: AxiosInstance; constructor(auth: AuthClient) { - this.googleAuth = auth.getAuth(); - this.service = google.sheets({ version: 'v4', auth: this.googleAuth }); + this.authClient = auth.getAuthHeadersClient(); + this.service = google.google.sheets({ version: 'v4', auth: this.authClient }); this.rawClient = axios.create({ validateStatus: () => true }); } @@ -284,7 +283,7 @@ export class Wrapper { // This ensures the latest access token is used (and refreshed if needed). const response = await this.rawClient.get( url, - { headers: await this.googleAuth.getRequestHeaders() }, + { headers: await this.authClient.getRequestHeaders() }, ); if (response.status !== 200) { throw new Error(`Failed to query rows, status: ${response.status}`); diff --git a/tests/google/sheets/models.test.ts b/tests/google/sheets/models.test.ts index f275678..440a1f7 100644 --- a/tests/google/sheets/models.test.ts +++ b/tests/google/sheets/models.test.ts @@ -73,7 +73,7 @@ describe('RawQueryRowsResult', () => { }; const expected: QueryRowsResult = { rows: [] }; - const mockAuth = { getAuth: jest.fn() }; + const mockAuth = { getAuthHeadersClient: jest.fn() }; const wrapper = new Wrapper(mockAuth); const result = wrapper['toQueryRowsResult'](rawResult); expect(result).toEqual(expected); @@ -121,7 +121,7 @@ describe('RawQueryRowsResult', () => { ], }; - const mockAuth = { getAuth: jest.fn() }; + const mockAuth = { getAuthHeadersClient: jest.fn() }; const wrapper = new Wrapper(mockAuth); const result = wrapper['toQueryRowsResult'](rawResult); expect(result).toEqual(expected); @@ -147,7 +147,7 @@ describe('RawQueryRowsResult', () => { }, }; - const mockAuth = { getAuth: jest.fn() }; + const mockAuth = { getAuthHeadersClient: jest.fn() }; const wrapper = new Wrapper(mockAuth); expect(() => wrapper['toQueryRowsResult'](rawResult)).toThrow('Unsupported cell value type: something'); }); diff --git a/tests/google/sheets/wrapper.test.ts b/tests/google/sheets/wrapper.test.ts index ac596d7..31628b3 100644 --- a/tests/google/sheets/wrapper.test.ts +++ b/tests/google/sheets/wrapper.test.ts @@ -26,7 +26,7 @@ describe('Wrapper', () => { let mockSheetsService: jest.Mocked; beforeEach(() => { - mockAuth = { getAuth: jest.fn() }; + mockAuth = { getAuthHeadersClient: jest.fn() }; wrapper = new Wrapper(mockAuth); mockSheetsService = wrapper['service'] as unknown as jest.Mocked; });