From 63f93f335e830d5270e281a34f81434cfc0187b8 Mon Sep 17 00:00:00 2001 From: Musa Khalid <112591148+Mkalbani@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:12:41 +0000 Subject: [PATCH 1/2] Completed Google OAuth 2.0 integration --- drips/package-lock.json | 252 ++++++++++++++++++- drips/package.json | 5 + drips/src/app.module.ts | 5 +- drips/src/auth/auth.controller.ts | 28 +++ drips/src/auth/auth.module.ts | 27 ++ drips/src/auth/auth.service.ts | 23 ++ drips/src/auth/guards/google-auth.guard.ts | 5 + drips/src/auth/guards/jwt-auth.guard.ts | 5 + drips/src/auth/strategies/google.strategy.ts | 33 +++ drips/src/auth/strategies/jwt.strategy.ts | 27 ++ drips/src/config/config.service.ts | 24 ++ drips/src/config/env.validation.ts | 6 + drips/src/users/entities/user.entity.ts | 34 +++ drips/src/users/users.module.ts | 11 + drips/src/users/users.service.ts | 61 +++++ 15 files changed, 541 insertions(+), 5 deletions(-) create mode 100644 drips/src/auth/auth.controller.ts create mode 100644 drips/src/auth/auth.module.ts create mode 100644 drips/src/auth/auth.service.ts create mode 100644 drips/src/auth/guards/google-auth.guard.ts create mode 100644 drips/src/auth/guards/jwt-auth.guard.ts create mode 100644 drips/src/auth/strategies/google.strategy.ts create mode 100644 drips/src/auth/strategies/jwt.strategy.ts create mode 100644 drips/src/users/entities/user.entity.ts create mode 100644 drips/src/users/users.module.ts create mode 100644 drips/src/users/users.service.ts diff --git a/drips/package-lock.json b/drips/package-lock.json index 39586af..145e9a1 100644 --- a/drips/package-lock.json +++ b/drips/package-lock.json @@ -13,6 +13,8 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.2", + "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", "@nestjs/typeorm": "^11.0.0", "@types/joi": "^17.2.2", @@ -22,6 +24,9 @@ "class-validator": "^0.14.3", "ioredis": "^5.9.2", "joi": "^18.0.2", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "passport-jwt": "^4.0.1", "pg": "^8.17.2", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -2307,6 +2312,29 @@ } } }, + "node_modules/@nestjs/jwt": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.2.tgz", + "integrity": "sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.10", + "jsonwebtoken": "9.0.3" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, + "node_modules/@nestjs/passport": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz", + "integrity": "sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "passport": "^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, "node_modules/@nestjs/platform-express": { "version": "11.1.12", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.12.tgz", @@ -2886,6 +2914,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -2893,11 +2931,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.19.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", - "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -4110,6 +4153,15 @@ ], "license": "MIT" }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.9.17", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.17.tgz", @@ -4263,6 +4315,12 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -5032,6 +5090,15 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -7454,6 +7521,49 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", @@ -7563,12 +7673,48 @@ "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, "node_modules/lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", "license": "MIT" }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -7583,6 +7729,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -8001,6 +8153,12 @@ "node": ">=8" } }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -8190,6 +8348,75 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "license": "MIT", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -8266,6 +8493,11 @@ "node": ">=8" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/pg": { "version": "8.17.2", "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.2.tgz", @@ -8710,7 +8942,6 @@ "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz", "integrity": "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==", "license": "MIT", - "peer": true, "workspaces": [ "./packages/*" ], @@ -8899,7 +9130,6 @@ "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10273,6 +10503,12 @@ "node": ">=8" } }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, "node_modules/uint8array-extras": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", @@ -10289,7 +10525,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, "license": "MIT" }, "node_modules/universalify": { @@ -10393,6 +10628,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", diff --git a/drips/package.json b/drips/package.json index 94e4e25..688957d 100644 --- a/drips/package.json +++ b/drips/package.json @@ -24,6 +24,8 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.2", + "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", "@nestjs/typeorm": "^11.0.0", "@types/joi": "^17.2.2", @@ -33,6 +35,9 @@ "class-validator": "^0.14.3", "ioredis": "^5.9.2", "joi": "^18.0.2", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "passport-jwt": "^4.0.1", "pg": "^8.17.2", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", diff --git a/drips/src/app.module.ts b/drips/src/app.module.ts index 4ca8c30..4f860a1 100644 --- a/drips/src/app.module.ts +++ b/drips/src/app.module.ts @@ -6,6 +6,8 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AppConfigModule } from './config/config.module'; import { AppConfigService } from './config/config.service'; +import { AuthModule } from './auth/auth.module'; +import { UsersModule } from './users/users.module'; @Module({ imports: [ @@ -34,9 +36,10 @@ import { AppConfigService } from './config/config.service'; migrations: ['dist/migrations/*{.ts,.js}'], synchronize: configService.nodeEnv === 'development', logging: configService.nodeEnv === 'development', - // namingStrategy: new (require('typeorm').SnakeNamingStrategy)(), }), }), + UsersModule, + AuthModule, ], controllers: [AppController], providers: [AppService], diff --git a/drips/src/auth/auth.controller.ts b/drips/src/auth/auth.controller.ts new file mode 100644 index 0000000..8f47897 --- /dev/null +++ b/drips/src/auth/auth.controller.ts @@ -0,0 +1,28 @@ +import { Controller, Get, UseGuards, Req, Res } from '@nestjs/common'; +import { Response } from 'express'; +import { AuthService } from './auth.service'; +import { GoogleAuthGuard } from './guards/google-auth.guard'; + +@Controller('auth') +export class AuthController { + constructor(private authService: AuthService) {} + + @Get('google') + @UseGuards(GoogleAuthGuard) + async googleAuth() { + // Guard handles redirect to Google + } + + @Get('google/callback') + @UseGuards(GoogleAuthGuard) + async googleAuthCallback(@Req() req, @Res() res: Response) { + const user = req.user; + const { access_token, user: userPayload } = this.authService.issueToken(user); + + // Return JWT token and user info + return res.json({ + access_token, + user: userPayload, + }); + } +} diff --git a/drips/src/auth/auth.module.ts b/drips/src/auth/auth.module.ts new file mode 100644 index 0000000..d8c6341 --- /dev/null +++ b/drips/src/auth/auth.module.ts @@ -0,0 +1,27 @@ +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { AppConfigService } from '../config/config.service'; +import { UsersModule } from '../users/users.module'; +import { AuthService } from './auth.service'; +import { AuthController } from './auth.controller'; +import { GoogleStrategy } from './strategies/google.strategy'; +import { JwtStrategy } from './strategies/jwt.strategy'; + +@Module({ + imports: [ + PassportModule, + JwtModule.registerAsync({ + inject: [AppConfigService], + useFactory: (configService: AppConfigService) => ({ + secret: configService.jwtSecret, + signOptions: { expiresIn: configService.jwtExpiresIn }, + }), + }), + UsersModule, + ], + providers: [AuthService, GoogleStrategy, JwtStrategy], + controllers: [AuthController], + exports: [AuthService], +}) +export class AuthModule {} diff --git a/drips/src/auth/auth.service.ts b/drips/src/auth/auth.service.ts new file mode 100644 index 0000000..f421689 --- /dev/null +++ b/drips/src/auth/auth.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { User } from '../users/entities/user.entity'; + +@Injectable() +export class AuthService { + constructor(private jwtService: JwtService) {} + + issueToken(user: User): { access_token: string; user: Partial } { + const payload = { email: user.email, sub: user.id }; + return { + access_token: this.jwtService.sign(payload), + user: { + id: user.id, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + profilePicture: user.profilePicture, + authProvider: user.authProvider, + }, + }; + } +} diff --git a/drips/src/auth/guards/google-auth.guard.ts b/drips/src/auth/guards/google-auth.guard.ts new file mode 100644 index 0000000..4a2c87a --- /dev/null +++ b/drips/src/auth/guards/google-auth.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class GoogleAuthGuard extends AuthGuard('google') {} diff --git a/drips/src/auth/guards/jwt-auth.guard.ts b/drips/src/auth/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..2155290 --- /dev/null +++ b/drips/src/auth/guards/jwt-auth.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') {} diff --git a/drips/src/auth/strategies/google.strategy.ts b/drips/src/auth/strategies/google.strategy.ts new file mode 100644 index 0000000..c1a937c --- /dev/null +++ b/drips/src/auth/strategies/google.strategy.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy, VerifyCallback } from 'passport-google-oauth20'; +import { AppConfigService } from '../../config/config.service'; +import { UsersService } from '../../users/users.service'; + +@Injectable() +export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { + constructor( + configService: AppConfigService, + private usersService: UsersService, + ) { + super({ + clientID: configService.googleClientId, + clientSecret: configService.googleClientSecret, + callbackURL: configService.googleCallbackUrl, + }); + } + + async validate( + accessToken: string, + refreshToken: string, + profile: any, + done: VerifyCallback, + ) { + try { + const user = await this.usersService.createFromGoogle(profile); + done(null, user); + } catch (error) { + done(error); + } + } +} diff --git a/drips/src/auth/strategies/jwt.strategy.ts b/drips/src/auth/strategies/jwt.strategy.ts new file mode 100644 index 0000000..0221aa7 --- /dev/null +++ b/drips/src/auth/strategies/jwt.strategy.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy, ExtractJwt } from 'passport-jwt'; +import { AppConfigService } from '../../config/config.service'; +import { UsersService } from '../../users/users.service'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor( + configService: AppConfigService, + private usersService: UsersService, + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.jwtSecret, + }); + } + + async validate(payload: any) { + const user = await this.usersService.findById(payload.sub); + if (!user) { + return null; + } + return { ...payload, user }; + } +} diff --git a/drips/src/config/config.service.ts b/drips/src/config/config.service.ts index d1f3191..941cfd4 100644 --- a/drips/src/config/config.service.ts +++ b/drips/src/config/config.service.ts @@ -49,6 +49,30 @@ export class AppConfigService { return this.configService.get('REDIS_DB')!; } + get jwtSecret(): string { + return this.configService.get('JWT_SECRET')!; + } + + get jwtExpiresIn(): string { + return this.configService.get('JWT_EXPIRES_IN', '24h')!; + } + + get googleClientId(): string { + return this.configService.get('GOOGLE_CLIENT_ID')!; + } + + get googleClientSecret(): string { + return this.configService.get('GOOGLE_CLIENT_SECRET')!; + } + + get googleCallbackUrl(): string { + return this.configService.get('GOOGLE_CALLBACK_URL')!; + } + + get appUrl(): string { + return this.configService.get('APP_URL', 'http://localhost:3000')!; + } + get(key: string): any { return this.configService.get(key); } diff --git a/drips/src/config/env.validation.ts b/drips/src/config/env.validation.ts index 43fd24d..9b88c64 100644 --- a/drips/src/config/env.validation.ts +++ b/drips/src/config/env.validation.ts @@ -14,4 +14,10 @@ export const envValidationSchema = Joi.object({ REDIS_PORT: Joi.number().default(6379), REDIS_PASSWORD: Joi.string().allow('').optional(), REDIS_DB: Joi.number().default(0), + JWT_SECRET: Joi.string().required(), + JWT_EXPIRES_IN: Joi.string().default('24h'), + GOOGLE_CLIENT_ID: Joi.string().required(), + GOOGLE_CLIENT_SECRET: Joi.string().required(), + GOOGLE_CALLBACK_URL: Joi.string().required(), + APP_URL: Joi.string().default('http://localhost:3000'), }); diff --git a/drips/src/users/entities/user.entity.ts b/drips/src/users/entities/user.entity.ts new file mode 100644 index 0000000..f11777a --- /dev/null +++ b/drips/src/users/entities/user.entity.ts @@ -0,0 +1,34 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('users') +export class User { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', unique: true }) + email: string; + + @Column({ type: 'varchar', nullable: true }) + googleId: string; + + @Column({ type: 'varchar', nullable: true }) + password: string; + + @Column({ type: 'varchar' }) + firstName: string; + + @Column({ type: 'varchar', nullable: true }) + lastName: string; + + @Column({ type: 'varchar', nullable: true }) + profilePicture: string; + + @Column({ type: 'enum', enum: ['local', 'google', 'github'], default: 'local' }) + authProvider: 'local' | 'google' | 'github'; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/drips/src/users/users.module.ts b/drips/src/users/users.module.ts new file mode 100644 index 0000000..f8cc930 --- /dev/null +++ b/drips/src/users/users.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { User } from './entities/user.entity'; +import { UsersService } from './users.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([User])], + providers: [UsersService], + exports: [UsersService], +}) +export class UsersModule {} diff --git a/drips/src/users/users.service.ts b/drips/src/users/users.service.ts new file mode 100644 index 0000000..c1291bd --- /dev/null +++ b/drips/src/users/users.service.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { User } from './entities/user.entity'; + +@Injectable() +export class UsersService { + constructor( + @InjectRepository(User) + private usersRepository: Repository, + ) {} + + async findByEmail(email: string): Promise { + return this.usersRepository.findOne({ where: { email } }); + } + + async findByGoogleId(googleId: string): Promise { + return this.usersRepository.findOne({ where: { googleId } }); + } + + async findById(id: string): Promise { + return this.usersRepository.findOne({ where: { id } }); + } + + async createFromGoogle(profile: any): Promise { + const email = profile.emails[0].value; + const existingUser = await this.findByEmail(email); + + if (existingUser) { + // Link Google account to existing user + existingUser.googleId = profile.id; + existingUser.authProvider = 'google'; + if (!existingUser.profilePicture && profile.photos?.[0]) { + existingUser.profilePicture = profile.photos[0].value; + } + return this.usersRepository.save(existingUser); + } + + // Create new user + const user = this.usersRepository.create({ + email, + googleId: profile.id, + firstName: profile.name?.givenName || 'User', + lastName: profile.name?.familyName || '', + profilePicture: profile.photos?.[0]?.value, + authProvider: 'google', + }); + + return this.usersRepository.save(user); + } + + async create(userData: Partial): Promise { + const user = this.usersRepository.create(userData); + return this.usersRepository.save(user); + } + + async update(id: string, userData: Partial): Promise { + await this.usersRepository.update(id, userData); + return this.findById(id); + } +} From 980068e7af26b4dcb676dc17dd3206fc862a4e20 Mon Sep 17 00:00:00 2001 From: Musa Khalid <112591148+Mkalbani@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:56:29 +0000 Subject: [PATCH 2/2] docs: add integration notes --- drips/INTEGRATION_NOTES.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 drips/INTEGRATION_NOTES.md diff --git a/drips/INTEGRATION_NOTES.md b/drips/INTEGRATION_NOTES.md new file mode 100644 index 0000000..327e3f7 --- /dev/null +++ b/drips/INTEGRATION_NOTES.md @@ -0,0 +1 @@ +# Redis & Google OAuth Integration