From 74a873e5742dc4ca9ca570034b365a8c80958ba8 Mon Sep 17 00:00:00 2001 From: julianpoyourow Date: Sat, 27 Jan 2024 20:39:19 +0000 Subject: [PATCH 1/3] feat: sign in with google --- example.env | 2 + package-lock.json | 278 ++++++++++++------ package.json | 1 + .../sign-in-with-google.component.html | 6 + .../sign-in-with-google.component.scss | 0 .../sign-in-with-google.component.ts | 64 ++++ .../sign-in-with-google.module.ts | 14 + .../src/app/pages/auth/auth.module.ts | 2 + .../src/app/pages/auth/auth.page.html | 8 + .../frontend/src/app/pages/auth/auth.page.ts | 15 + packages/frontend/src/assets/i18n/en-us.json | 2 + packages/trpc/src/index.ts | 2 + .../src/procedures/users/signInWithGoogle.ts | 56 ++++ packages/trpc/src/services/config.ts | 32 ++ .../trpc/src/services/user/generateSession.ts | 31 ++ 15 files changed, 421 insertions(+), 92 deletions(-) create mode 100644 packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.component.html create mode 100644 packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.component.scss create mode 100644 packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.component.ts create mode 100644 packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.module.ts create mode 100644 packages/trpc/src/procedures/users/signInWithGoogle.ts create mode 100644 packages/trpc/src/services/config.ts create mode 100644 packages/trpc/src/services/user/generateSession.ts diff --git a/example.env b/example.env index 5b9df9b42..bcb9b206c 100644 --- a/example.env +++ b/example.env @@ -4,3 +4,5 @@ STRIPE_SK=KEY STRIPE_WEBHOOK_SECRET=KEY SENTRY_DSN=VAL OPENAI_API_KEY=VAL +GOOGLE_GSI_CLIENT_ID=VAL +GOOGLE_GSI_CLIENT_SECRET=VAL diff --git a/package-lock.json b/package-lock.json index 245d69b92..86ace7bc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "firebase-admin": "^11.11.0", "fraction.js": "^4.3.7", "fs-extra": "^11.1.1", + "google-auth-library": "^9.4.2", "grip": "^1.5.0", "he": "^1.2.0", "https-proxy-agent": "^7.0.2", @@ -6296,6 +6297,65 @@ "node": ">=12" } }, + "node_modules/@google-cloud/storage/node_modules/gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "optional": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@google-cloud/storage/node_modules/google-auth-library": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz", + "integrity": "sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.3.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@google-cloud/storage/node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "optional": true, + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/storage/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@google-cloud/storage/node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -6305,6 +6365,12 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/@google-cloud/storage/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, "node_modules/@google-cloud/vision": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@google-cloud/vision/-/vision-4.0.2.tgz", @@ -6337,48 +6403,6 @@ "node": ">= 6.0.0" } }, - "node_modules/@google-cloud/vision/node_modules/gaxios": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.1.1.tgz", - "integrity": "sha512-bw8smrX+XlAoo9o1JAksBwX+hi/RG15J+NTSxmNPIclKC3ZVK6C2afwY8OSdRvOK0+ZLecUJYtj2MmjOt3Dm0w==", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/vision/node_modules/gcp-metadata": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", - "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", - "dependencies": { - "gaxios": "^6.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/vision/node_modules/google-auth-library": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.4.1.tgz", - "integrity": "sha512-Chs7cuzDuav8W/BXOoRgSXw4u0zxYtuqAHETDR5Q6dG1RwNwz7NUKjsDDHAsBV3KkiiJBtJqjbzy1XU1L41w1g==", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/@google-cloud/vision/node_modules/google-gax": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.0.5.tgz", @@ -6400,18 +6424,6 @@ "node": ">=14" } }, - "node_modules/@google-cloud/vision/node_modules/gtoken": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.0.1.tgz", - "integrity": "sha512-KcFVtoP1CVFtQu0aSk3AyAt2og66PFhZAlkUOuWKwzMLoulHXG5W5wE5xAnHb+yl3/wEFoqGW7/cDGMU8igDZQ==", - "dependencies": { - "gaxios": "^6.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@google-cloud/vision/node_modules/proto3-json-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.0.tgz", @@ -18785,16 +18797,29 @@ } }, "node_modules/gcp-metadata": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", - "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", - "optional": true, + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", "dependencies": { - "gaxios": "^5.0.0", + "gaxios": "^6.0.0", "json-bigint": "^1.0.0" }, "engines": { - "node": ">=12" + "node": ">=14" + } + }, + "node_modules/gcp-metadata/node_modules/gaxios": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.1.1.tgz", + "integrity": "sha512-bw8smrX+XlAoo9o1JAksBwX+hi/RG15J+NTSxmNPIclKC3ZVK6C2afwY8OSdRvOK0+ZLecUJYtj2MmjOt3Dm0w==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=14" } }, "node_modules/gensync": { @@ -19121,43 +19146,35 @@ } }, "node_modules/google-auth-library": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz", - "integrity": "sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==", - "optional": true, + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.4.2.tgz", + "integrity": "sha512-rTLO4gjhqqo3WvYKL5IdtlCvRqeQ4hxUx/p4lObobY2xotFW3bCQC+Qf1N51CYOfiqfMecdMwW9RIo7dFWYjqw==", "dependencies": { - "arrify": "^2.0.0", "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", - "fast-text-encoding": "^1.0.0", - "gaxios": "^5.0.0", - "gcp-metadata": "^5.3.0", - "gtoken": "^6.1.0", - "jws": "^4.0.0", - "lru-cache": "^6.0.0" + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" }, "engines": { - "node": ">=12" + "node": ">=14" } }, - "node_modules/google-auth-library/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "optional": true, + "node_modules/google-auth-library/node_modules/gaxios": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.1.1.tgz", + "integrity": "sha512-bw8smrX+XlAoo9o1JAksBwX+hi/RG15J+NTSxmNPIclKC3ZVK6C2afwY8OSdRvOK0+ZLecUJYtj2MmjOt3Dm0w==", "dependencies": { - "yallist": "^4.0.0" + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" }, "engines": { - "node": ">=10" + "node": ">=14" } }, - "node_modules/google-auth-library/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, "node_modules/google-gax": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.6.1.tgz", @@ -19201,6 +19218,65 @@ "node": "^8.13.0 || >=10.10.0" } }, + "node_modules/google-gax/node_modules/gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "optional": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-gax/node_modules/google-auth-library": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz", + "integrity": "sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.3.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-gax/node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "optional": true, + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/google-gax/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/google-gax/node_modules/protobufjs": { "version": "7.2.4", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", @@ -19225,6 +19301,12 @@ "node": ">=12.0.0" } }, + "node_modules/google-gax/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, "node_modules/google-p12-pem": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", @@ -19336,17 +19418,29 @@ "integrity": "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==" }, "node_modules/gtoken": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", - "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", - "optional": true, + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.0.1.tgz", + "integrity": "sha512-KcFVtoP1CVFtQu0aSk3AyAt2og66PFhZAlkUOuWKwzMLoulHXG5W5wE5xAnHb+yl3/wEFoqGW7/cDGMU8igDZQ==", "dependencies": { - "gaxios": "^5.0.1", - "google-p12-pem": "^4.0.0", + "gaxios": "^6.0.0", "jws": "^4.0.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" + } + }, + "node_modules/gtoken/node_modules/gaxios": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.1.1.tgz", + "integrity": "sha512-bw8smrX+XlAoo9o1JAksBwX+hi/RG15J+NTSxmNPIclKC3ZVK6C2afwY8OSdRvOK0+ZLecUJYtj2MmjOt3Dm0w==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=14" } }, "node_modules/guess-parser": { diff --git a/package.json b/package.json index 270c8dc74..f43f87fe5 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "firebase-admin": "^11.11.0", "fraction.js": "^4.3.7", "fs-extra": "^11.1.1", + "google-auth-library": "^9.4.2", "grip": "^1.5.0", "he": "^1.2.0", "https-proxy-agent": "^7.0.2", diff --git a/packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.component.html b/packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.component.html new file mode 100644 index 000000000..bed16b0cc --- /dev/null +++ b/packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.component.html @@ -0,0 +1,6 @@ +
+ + + {{ "components.signInWithGoogle.button" | translate }} + +
diff --git a/packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.component.scss b/packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.component.ts b/packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.component.ts new file mode 100644 index 000000000..0b3764286 --- /dev/null +++ b/packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.component.ts @@ -0,0 +1,64 @@ +import { + Component, + Input, + Output, + EventEmitter, + ElementRef, + ViewChild, +} from "@angular/core"; +import { TRPCService } from "../../services/trpc.service"; + +const getGoogleRef = () => { + return (window as any).google; +}; + +@Component({ + selector: "sign-in-with-google", + templateUrl: "sign-in-with-google.component.html", + styleUrls: ["./sign-in-with-google.component.scss"], +}) +export class SignInWithGoogleComponent { + @Output() signInComplete = new EventEmitter(); + + constructor(private trpcService: TRPCService) {} + + ngAfterViewInit() { + const googleScriptNodeId = "google-auth-script"; + const existingNode = document.getElementById(googleScriptNodeId); + if (!existingNode) { + const googleScriptNode = document.createElement("script"); + googleScriptNode.src = "https://accounts.google.com/gsi/client"; + googleScriptNode.async = true; + googleScriptNode.id = googleScriptNodeId; + googleScriptNode.addEventListener("load", () => { + this.initializeGoogleAccounts(); + }); + document.head.appendChild(googleScriptNode); + } + } + + async afterSignInComplete(args: any) { + const session = await this.trpcService.handle( + this.trpcService.trpc.users.signInWithGoogle.mutate(args), + ); + + if (session) { + this.signInComplete.emit(session); + } + } + + initializeGoogleAccounts() { + (window as any).ref = getGoogleRef()?.accounts.id.initialize({ + client_id: + "1064631313987-elks4csl9vdtes5j9b5l3savje7m3nhf.apps.googleusercontent.com", + context: "signin", + ux_mode: "popup", + callback: this.afterSignInComplete.bind(this), + auto_prompt: "false", + }); + } + + showGoogleAuthPrompt() { + getGoogleRef()?.accounts.id.prompt(); + } +} diff --git a/packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.module.ts b/packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.module.ts new file mode 100644 index 000000000..4cf094628 --- /dev/null +++ b/packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { RouterModule } from "@angular/router"; +import { IonicModule } from "@ionic/angular"; + +import { SignInWithGoogleComponent } from "./sign-in-with-google.component"; +import { GlobalModule } from "~/global.module"; + +@NgModule({ + declarations: [SignInWithGoogleComponent], + imports: [CommonModule, IonicModule, RouterModule, GlobalModule], + exports: [SignInWithGoogleComponent], +}) +export class SignInWithGoogleModule {} diff --git a/packages/frontend/src/app/pages/auth/auth.module.ts b/packages/frontend/src/app/pages/auth/auth.module.ts index 05c293807..88d1d7e21 100644 --- a/packages/frontend/src/app/pages/auth/auth.module.ts +++ b/packages/frontend/src/app/pages/auth/auth.module.ts @@ -10,6 +10,7 @@ import { TosClickwrapAgreementModule } from "~/components/tos-clickwrap-agreemen import { GlobalModule } from "~/global.module"; import { ToggleablePasswordDirective } from "../../directives/toggleable-password.directive"; +import { SignInWithGoogleModule } from "../../components/sign-in-with-google/sign-in-with-google.module"; @NgModule({ declarations: [AuthPage, ToggleablePasswordDirective], @@ -27,6 +28,7 @@ import { ToggleablePasswordDirective } from "../../directives/toggleable-passwor ReactiveFormsModule, LogoIconModule, TosClickwrapAgreementModule, + SignInWithGoogleModule, ], }) export class AuthPageModule {} diff --git a/packages/frontend/src/app/pages/auth/auth.page.html b/packages/frontend/src/app/pages/auth/auth.page.html index 5ec07dae6..44a3d4c26 100644 --- a/packages/frontend/src/app/pages/auth/auth.page.html +++ b/packages/frontend/src/app/pages/auth/auth.page.html @@ -99,6 +99,14 @@

{{ 'pages.auth.welcome.register' | translate }}

> +

OR

+ +
+ +
+
{ + if (!config.google.gsi.clientId || !config.google.gsi.clientSecret) { + throw new Error("GSI clientId or clientSecret missing"); + } + + const client = new OAuth2Client( + config.google.gsi.clientId, + config.google.gsi.clientSecret, + ); + const ticket = await client.verifyIdToken({ + idToken: input.credential, + audience: input.clientId, + }); + const payload = ticket.getPayload(); + if (!payload?.email) { + throw new TRPCError({ + message: "Invalid clientId or credential", + code: "BAD_REQUEST", + }); + } + + const user = await prisma.user.upsert({ + where: { + email: payload.email, + }, + create: { + name: payload.email.split("@")[0], + email: payload.email, + }, + update: { + lastLogin: new Date(), + }, + }); + + const session = await generateSession(user.id, SessionType.User); + + return session.token; + }); diff --git a/packages/trpc/src/services/config.ts b/packages/trpc/src/services/config.ts new file mode 100644 index 000000000..45013f151 --- /dev/null +++ b/packages/trpc/src/services/config.ts @@ -0,0 +1,32 @@ +enum Environment { + Prod = "production", + Selfhost = "selfhost", + Test = "test", + Development = "development", + All = "all", +} + +const getEnvString = (name: string, requiredEnvironments: Environment[]) => { + const value = process.env[name]; + + const isRequired = + requiredEnvironments.includes( + (process.env.NODE_ENV || "production") as Environment, + ) || requiredEnvironments.includes(Environment.All); + if (!value && isRequired) { + throw new Error(`Missing required environment variable: ${name}`); + } + + return value; +}; + +export const config = { + google: { + gsi: { + clientId: getEnvString("GOOGLE_GSI_CLIENT_ID", [Environment.Prod]), + clientSecret: getEnvString("GOOGLE_GSI_CLIENT_SECRET", [ + Environment.Prod, + ]), + }, + }, +}; diff --git a/packages/trpc/src/services/user/generateSession.ts b/packages/trpc/src/services/user/generateSession.ts new file mode 100644 index 000000000..5989c327a --- /dev/null +++ b/packages/trpc/src/services/user/generateSession.ts @@ -0,0 +1,31 @@ +import { Session } from "@prisma/client"; +import { prisma } from "@recipesage/prisma"; +import { randomBytes } from "node:crypto"; + +export enum SessionType { + User = "user", +} + +const SESSION_VALIDITY_LENGTH_DAYS = 30; + +/* + * Creates a session in the DB + */ +export const generateSession = ( + userId: string, + type: SessionType, +): Promise => { + const expires = new Date(); + expires.setDate(expires.getDate() + SESSION_VALIDITY_LENGTH_DAYS); + + const token = randomBytes(48).toString("hex"); + + return prisma.session.create({ + data: { + userId, + type, + token, + expires, + }, + }); +}; From 77261710b79f0170650028404169d3c7a23f58ab Mon Sep 17 00:00:00 2001 From: julianpoyourow Date: Sun, 28 Jan 2024 21:29:29 +0000 Subject: [PATCH 2/3] feat: use google approved sign-in button style --- packages/frontend/src/app/app.scss | 7 ++++ .../sign-in-with-google.component.html | 7 +--- .../sign-in-with-google.component.ts | 37 +++++++++++++++++-- .../src/app/pages/auth/auth.page.html | 11 +++++- .../src/app/pages/auth/auth.page.scss | 16 ++++++++ .../info-components/welcome/welcome.module.ts | 2 + packages/frontend/src/assets/i18n/en-us.json | 1 + .../src/environments/environment.prod.ts | 3 ++ .../src/environments/environment.selfhost.ts | 2 + .../frontend/src/environments/environment.ts | 3 ++ 10 files changed, 78 insertions(+), 11 deletions(-) diff --git a/packages/frontend/src/app/app.scss b/packages/frontend/src/app/app.scss index 30a6b2a9e..3688a8ee4 100644 --- a/packages/frontend/src/app/app.scss +++ b/packages/frontend/src/app/app.scss @@ -223,3 +223,10 @@ ion-button.button-outline { .language-select-alert .select-interface-option { text-transform: capitalize; } + +/* Must be added globally, since iframe is loaded outside of the context of Angular CSS encapsulation */ +.gsi-container iframe { + /* Center the "continue as XYZ" button */ + margin-left: auto !important; + margin-right: auto !important; +} diff --git a/packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.component.html b/packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.component.html index bed16b0cc..cc7ea0c30 100644 --- a/packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.component.html +++ b/packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.component.html @@ -1,6 +1 @@ -
- - - {{ "components.signInWithGoogle.button" | translate }} - -
+
diff --git a/packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.component.ts b/packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.component.ts index 0b3764286..c664b72f0 100644 --- a/packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.component.ts +++ b/packages/frontend/src/app/components/sign-in-with-google/sign-in-with-google.component.ts @@ -7,6 +7,10 @@ import { ViewChild, } from "@angular/core"; import { TRPCService } from "../../services/trpc.service"; +import { + GOOGLE_GSI_CLIENT_ID, + IS_SELFHOST, +} from "@recipesage/frontend/src/environments/environment"; const getGoogleRef = () => { return (window as any).google; @@ -18,11 +22,20 @@ const getGoogleRef = () => { styleUrls: ["./sign-in-with-google.component.scss"], }) export class SignInWithGoogleComponent { + // Can be use to hide the button and only use for prompting + @Input() showButton = true; + @Input() autoPrompt = false; + @Output() signInComplete = new EventEmitter(); + @ViewChild("googleButtonContainer", { static: true }) + googleButtonContainer!: ElementRef; + constructor(private trpcService: TRPCService) {} ngAfterViewInit() { + if (IS_SELFHOST) return; + const googleScriptNodeId = "google-auth-script"; const existingNode = document.getElementById(googleScriptNodeId); if (!existingNode) { @@ -32,8 +45,13 @@ export class SignInWithGoogleComponent { googleScriptNode.id = googleScriptNodeId; googleScriptNode.addEventListener("load", () => { this.initializeGoogleAccounts(); + if (this.showButton) this.renderGoogleButton(); + if (this.autoPrompt) this.showGoogleAuthPrompt(); }); document.head.appendChild(googleScriptNode); + } else { + if (this.showButton) this.renderGoogleButton(); + if (this.autoPrompt) this.showGoogleAuthPrompt(); } } @@ -48,9 +66,8 @@ export class SignInWithGoogleComponent { } initializeGoogleAccounts() { - (window as any).ref = getGoogleRef()?.accounts.id.initialize({ - client_id: - "1064631313987-elks4csl9vdtes5j9b5l3savje7m3nhf.apps.googleusercontent.com", + getGoogleRef()?.accounts.id.initialize({ + client_id: GOOGLE_GSI_CLIENT_ID, context: "signin", ux_mode: "popup", callback: this.afterSignInComplete.bind(this), @@ -61,4 +78,18 @@ export class SignInWithGoogleComponent { showGoogleAuthPrompt() { getGoogleRef()?.accounts.id.prompt(); } + + renderGoogleButton() { + getGoogleRef().accounts.id.renderButton( + this.googleButtonContainer.nativeElement, + { + type: "standard", + shape: "rectangular", + theme: "filled_black", + text: "continue_with", + size: "large", + logo_alignment: "left", + }, + ); + } } diff --git a/packages/frontend/src/app/pages/auth/auth.page.html b/packages/frontend/src/app/pages/auth/auth.page.html index 44a3d4c26..65cb3f800 100644 --- a/packages/frontend/src/app/pages/auth/auth.page.html +++ b/packages/frontend/src/app/pages/auth/auth.page.html @@ -99,9 +99,16 @@

{{ 'pages.auth.welcome.register' | translate }}

>
-

OR

+
+
+

{{ 'pages.auth.or' | translate }}

+
+
-
+
diff --git a/packages/frontend/src/app/pages/auth/auth.page.scss b/packages/frontend/src/app/pages/auth/auth.page.scss index 319408ed1..9c3d8b1d2 100644 --- a/packages/frontend/src/app/pages/auth/auth.page.scss +++ b/packages/frontend/src/app/pages/auth/auth.page.scss @@ -36,4 +36,20 @@ logo-icon { text-align: center; } + + .divider { + display: flex; + justify-content: center; + align-items: center; + + .divider-segment { + width: 200px; + border-bottom: 1px solid var(--ion-text-color); + } + + p { + margin-left: 20px; + margin-right: 20px; + } + } } diff --git a/packages/frontend/src/app/pages/info-components/welcome/welcome.module.ts b/packages/frontend/src/app/pages/info-components/welcome/welcome.module.ts index c0740d3b7..09b928363 100644 --- a/packages/frontend/src/app/pages/info-components/welcome/welcome.module.ts +++ b/packages/frontend/src/app/pages/info-components/welcome/welcome.module.ts @@ -6,6 +6,7 @@ import { RouterModule } from "@angular/router"; import { WelcomePage } from "./welcome.page"; import { GlobalModule } from "~/global.module"; +import { SignInWithGoogleModule } from "../../../components/sign-in-with-google/sign-in-with-google.module"; @NgModule({ declarations: [WelcomePage], @@ -19,6 +20,7 @@ import { GlobalModule } from "~/global.module"; }, ]), GlobalModule, + SignInWithGoogleModule, ], }) export class WelcomePageModule {} diff --git a/packages/frontend/src/assets/i18n/en-us.json b/packages/frontend/src/assets/i18n/en-us.json index 44705389c..1d82880ce 100644 --- a/packages/frontend/src/assets/i18n/en-us.json +++ b/packages/frontend/src/assets/i18n/en-us.json @@ -70,6 +70,7 @@ "pages.auth.button.forgotPassword": "Forgot password", "pages.auth.button.registerInstead": "Create an account instead", "pages.auth.button.loginInstead": "Log in instead", + "pages.auth.or": "OR", "pages.auth.error.invalidEmail": "Please enter a valid email address.", "pages.auth.error.noName": "Please enter a name (you can enter a nickname!)", "pages.auth.error.noPassword": "Please enter a password.", diff --git a/packages/frontend/src/environments/environment.prod.ts b/packages/frontend/src/environments/environment.prod.ts index 87b98ab08..8520262c1 100644 --- a/packages/frontend/src/environments/environment.prod.ts +++ b/packages/frontend/src/environments/environment.prod.ts @@ -12,3 +12,6 @@ export const API_BASE_URL = "https://api.recipesage.com/"; export const GRIP_WS_URL = "wss://grip.recipesage.com/ws"; export const SENTRY_SAMPLE_RATE = 1; + +export const GOOGLE_GSI_CLIENT_ID = + "1064631313987-elks4csl9vdtes5j9b5l3savje7m3nhf.apps.googleusercontent.com"; diff --git a/packages/frontend/src/environments/environment.selfhost.ts b/packages/frontend/src/environments/environment.selfhost.ts index f410ece48..5f2ed8864 100644 --- a/packages/frontend/src/environments/environment.selfhost.ts +++ b/packages/frontend/src/environments/environment.selfhost.ts @@ -18,3 +18,5 @@ const extraSlash = path.endsWith("/") ? "" : "/"; export const GRIP_WS_URL = `${wsProto}//${window.location.host}${path}${extraSlash}grip/ws`; export const SENTRY_SAMPLE_RATE = 0; + +export const GOOGLE_GSI_CLIENT_ID = null; diff --git a/packages/frontend/src/environments/environment.ts b/packages/frontend/src/environments/environment.ts index 8608f0cf2..88e61090e 100644 --- a/packages/frontend/src/environments/environment.ts +++ b/packages/frontend/src/environments/environment.ts @@ -17,6 +17,9 @@ export const GRIP_WS_URL = null; export const SENTRY_SAMPLE_RATE = 0; +export const GOOGLE_GSI_CLIENT_ID = + "1064631313987-elks4csl9vdtes5j9b5l3savje7m3nhf.apps.googleusercontent.com"; + /* * For easier debugging in development mode, you can import the following file * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. From 1ce556d5197b8026cd617ad63804b939e914aa92 Mon Sep 17 00:00:00 2001 From: julianpoyourow Date: Sun, 28 Jan 2024 21:32:48 +0000 Subject: [PATCH 3/3] fix: use color-scheme: light for gsi button to fix background --- packages/frontend/src/app/app.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend/src/app/app.scss b/packages/frontend/src/app/app.scss index 3688a8ee4..4869f6406 100644 --- a/packages/frontend/src/app/app.scss +++ b/packages/frontend/src/app/app.scss @@ -229,4 +229,5 @@ ion-button.button-outline { /* Center the "continue as XYZ" button */ margin-left: auto !important; margin-right: auto !important; + color-scheme: light; }