diff --git a/.changeset/dry-gifts-sin.md b/.changeset/dry-gifts-sin.md new file mode 100644 index 00000000..06e9721e --- /dev/null +++ b/.changeset/dry-gifts-sin.md @@ -0,0 +1,5 @@ +--- +'@blinkk/root-cms': minor +--- + +feat: automatically share gsheets with editors diff --git a/packages/root-cms/package.json b/packages/root-cms/package.json index ff97f2ad..9bf76b61 100644 --- a/packages/root-cms/package.json +++ b/packages/root-cms/package.json @@ -99,6 +99,7 @@ "@tabler/icons-preact": "2.39.0", "@types/body-parser": "1.19.3", "@types/gapi": "0.0.47", + "@types/gapi.client.drive-v3": "0.0.4", "@types/gapi.client.sheets-v4": "0.0.4", "@types/google.accounts": "0.0.14", "@types/jsonwebtoken": "9.0.1", diff --git a/packages/root-cms/ui/hooks/useGapiClient.ts b/packages/root-cms/ui/hooks/useGapiClient.ts index 9ccf3cb9..a3dcfd6b 100644 --- a/packages/root-cms/ui/hooks/useGapiClient.ts +++ b/packages/root-cms/ui/hooks/useGapiClient.ts @@ -7,6 +7,7 @@ const SCOPES = [ ]; const DISCOVERY_DOCS = [ + 'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest', 'https://sheets.googleapis.com/$discovery/rest?version=v4', ]; diff --git a/packages/root-cms/ui/tsconfig.json b/packages/root-cms/ui/tsconfig.json index 89dc8691..7595ebb4 100644 --- a/packages/root-cms/ui/tsconfig.json +++ b/packages/root-cms/ui/tsconfig.json @@ -16,6 +16,7 @@ "outDir": "dist", "types": [ "@types/gapi", + "@types/gapi.client.drive-v3", "@types/gapi.client.sheets-v4", "@types/google.accounts", "vite/client" diff --git a/packages/root-cms/ui/utils/gsheets.ts b/packages/root-cms/ui/utils/gsheets.ts index b4de5252..757164f8 100644 --- a/packages/root-cms/ui/utils/gsheets.ts +++ b/packages/root-cms/ui/utils/gsheets.ts @@ -1,3 +1,5 @@ +import {getAllEditors} from './users.js'; + export interface GoogleSheetId { spreadsheetId: string; gid?: number; @@ -34,6 +36,11 @@ export class GSpreadsheet { const spreadsheet = res.result; const gspreadsheet = new GSpreadsheet(spreadsheet.spreadsheetId!); gspreadsheet.setSpreadsheet(spreadsheet); + + // Give all admins and editors "write" access. + const editors = await getAllEditors(); + await gspreadsheet.share(editors, 'writer'); + return gspreadsheet; } @@ -101,6 +108,34 @@ export class GSpreadsheet { }); this.sheets = gsheets; } + + private async share(users: string[], role: 'reader' | 'writer') { + await Promise.all( + users.map(async (user) => { + let permission: any; + if (user.startsWith('*@')) { + const domain = user.slice(2); + permission = { + type: 'domain', + domain: domain, + role: role, + }; + } else { + permission = { + type: 'user', + emailAddress: user, + role: role, + }; + } + await gapi.client.drive.permissions.create({ + fileId: this.spreadsheetId, + resource: permission, + sendNotificationEmail: false, + fields: 'id', + }); + }) + ); + } } export class GSheet { diff --git a/packages/root-cms/ui/utils/users.ts b/packages/root-cms/ui/utils/users.ts new file mode 100644 index 00000000..e6ab15ac --- /dev/null +++ b/packages/root-cms/ui/utils/users.ts @@ -0,0 +1,23 @@ +import {doc, getDoc} from 'firebase/firestore'; + +/** + * Returns a list of users that can edit, e.g. users with role EDITOR or ADMIN. + */ +export async function getAllEditors(): Promise { + const db = window.firebase.db; + const projectId = window.__ROOT_CTX.rootConfig.projectId || 'default'; + const docRef = doc(db, 'Projects', projectId); + const snapshot = await getDoc(docRef); + if (!snapshot.exists) { + return []; + } + const data: any = snapshot.data() || {}; + const roles: Record = data.roles || {}; + const editors: string[] = []; + Object.entries(roles).forEach(([email, role]) => { + if (role === 'ADMIN' || role === 'EDITOR') { + editors.push(email); + } + }); + return editors; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 87ea1d70..ed25dfbd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -418,6 +418,9 @@ importers: '@types/gapi': specifier: 0.0.47 version: 0.0.47 + '@types/gapi.client.drive-v3': + specifier: 0.0.4 + version: 0.0.4 '@types/gapi.client.sheets-v4': specifier: 0.0.4 version: 0.0.4 @@ -2227,8 +2230,15 @@ packages: '@types/gapi.client.discovery-v1': 0.0.4 dev: true - /@maxim_mazurok/gapi.client.sheets-v4@0.0.20240305: - resolution: {integrity: sha512-zV9vUH2YKkzLPxU/qbKqXgyvR//wxOCis3UprVIqd57YNWd1lnpk+oy4JYUOkK39kr3eFtoN/bpwHg/PFi5zrA==} + /@maxim_mazurok/gapi.client.drive-v3@0.0.20240314: + resolution: {integrity: sha512-pfECiEHEzexghj71X2twDIFUgl7j5m5myOttOA3yIbF0ho2NehauFpO+dBOA5ZNMsyrAHBWK+iV0WgZhi6eGoA==} + dependencies: + '@types/gapi.client': 1.0.8 + '@types/gapi.client.discovery-v1': 0.0.4 + dev: true + + /@maxim_mazurok/gapi.client.sheets-v4@0.0.20240318: + resolution: {integrity: sha512-Yf2t7YyYZ/Sdfwqihci44bf3UzthwaMmZP3v5eh+ST0W52WQtYwtit7RnM0SjjwI0LCk4B/r6aBAlYhstE59Hg==} dependencies: '@types/gapi.client': 1.0.8 '@types/gapi.client.discovery-v1': 0.0.4 @@ -2706,10 +2716,16 @@ packages: '@maxim_mazurok/gapi.client.discovery-v1': 0.1.20200806 dev: true + /@types/gapi.client.drive-v3@0.0.4: + resolution: {integrity: sha512-jE37dJ0EzAdY0aJPFOp20xmec/aO0P4HtUIA9k07RMPyedFDOcuMlSac1r0PklwQdgXF7BHaMoObNHNAnwSQUQ==} + dependencies: + '@maxim_mazurok/gapi.client.drive-v3': 0.0.20240314 + dev: true + /@types/gapi.client.sheets-v4@0.0.4: resolution: {integrity: sha512-6kTJ7aDMAElfdQV1XzVJmZWjgbibpa84DMuKuaN8Cwqci/dkglPyHXKvsGrRugmuYvgFYr35AQqwz6j3q8R0dw==} dependencies: - '@maxim_mazurok/gapi.client.sheets-v4': 0.0.20240305 + '@maxim_mazurok/gapi.client.sheets-v4': 0.0.20240318 dev: true /@types/gapi.client@1.0.8: @@ -6184,6 +6200,7 @@ packages: /indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + requiresBuild: true dev: true /inflight@1.0.6: @@ -7258,6 +7275,7 @@ packages: /minimatch@9.0.1: resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} engines: {node: '>=16 || 14 >=14.17'} + requiresBuild: true dependencies: brace-expansion: 2.0.1