diff --git a/.env.development b/.env.development
index 2c2fc36..c01088e 100644
--- a/.env.development
+++ b/.env.development
@@ -20,8 +20,8 @@ SET_PASSWORD_TOKEN_EXPIRES_IN=86400
SET_SESSION=1
# DATABSES
-REDIS_URL="redis://localhost:6380"
-MONGO_DATABASE_URL="mongodb://root:example@localhost:27017/typescript-backend-toolkit?authSource=admin"
+REDIS_URL="redis://localhost:6388"
+MONGO_DATABASE_URL="mongodb://root:example@localhost:27028/typescript-backend-toolkit?authSource=admin"
# Mailgun Configuration (dummy values for development)
MAILGUN_API_KEY="dummy-key"
diff --git a/.env.sample b/.env.sample
index e35a7ad..3417369 100644
--- a/.env.sample
+++ b/.env.sample
@@ -33,8 +33,7 @@ SET_SESSION=0
# GOOGLE AUTH
GOOGLE_CLIENT_ID=""
-GOOGLE_CLIENT_SECRET=''
-GOOGLE_REDIRECT_URI = ''
+
# DATABSES
REDIS_URL=""
diff --git a/GoogleAuth.md b/GoogleAuth.md
new file mode 100644
index 0000000..61300ba
--- /dev/null
+++ b/GoogleAuth.md
@@ -0,0 +1,104 @@
+# Google Authentication Implementation Guide
+
+This document explains how to implement Google authentication in the Typescript Backend Toolkit using Google Identity Services.
+
+## Overview
+
+The implementation uses Google's newer Identity Services API, which provides a more secure and user-friendly authentication experience compared to the older OAuth 2.0 redirect flow. The key differences are:
+
+| Feature | Google Identity Services (New) | OAuth 2.0 Redirect Flow (Old) |
+| -------------------- | --------------------------------- | ------------------------------ |
+| User Experience | One-tap sign-in, popup-based flow | Full page redirects |
+| Security | JWT-based ID tokens | Authorization code flow |
+| Implementation | Client-side token generation | Server-side code exchange |
+| Session Disruption | Minimal - stays on same page | High - navigates away from app |
+| Cross-device Support | Better mobile/desktop integration | Requires more custom handling |
+
+## Prerequisites
+
+1. A Google Cloud Platform account
+2. A configured OAuth 2.0 Client ID
+3. Proper configuration of authorized JavaScript origins and redirect URIs
+
+## Environment Variables
+
+Add these variables to your `.env` file:
+
+```
+GOOGLE_CLIENT_ID=your-client-id-here
+```
+
+## Implementation Steps
+
+### 1. Frontend Implementation
+
+The frontend implementation uses Google's Identity Services library to handle the authentication flow:
+
+```html
+
+
+
+
+
+
+
+
+
+
+```
+
+## Security Considerations
+
+1. **Token Validation**: Always verify the ID token on your server using Google's libraries.
+2. **Email Verification**: Check that the email is verified (`email_verified` claim).
+3. **Expiration Time**: Verify the token hasn't expired (`exp` claim).
+4. **HTTPS**: Always use HTTPS for token transmission.
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Invalid Client ID**: Ensure your client ID is correctly configured in both frontend and backend.
+2. **CORS Issues**: Make sure your domain is listed in the authorized JavaScript origins.
+3. **Token Verification Failures**: Check that your server time is synchronized.
+4. **Missing Scopes**: Ensure you're requesting the necessary scopes (email, profile).
+
+## References
+
+- [Google Identity Services Documentation](https://developers.google.com/identity/gsi/web/guides/overview)
+- [Google OAuth 2.0 for Client-side Web Applications](https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow)
+- [Verifying Google ID Tokens](https://developers.google.com/identity/sign-in/web/backend-auth)
diff --git a/docker-compose.yml b/docker-compose.yml
index e725df1..e9ce60e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -3,7 +3,7 @@ services:
image: mongo:5.0.2
restart: 'unless-stopped'
ports:
- - '27017:27017'
+ - '27028:27017'
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
@@ -13,7 +13,7 @@ services:
redis:
image: redis:latest
ports:
- - 6380:6379
+ - 6388:6379
volumes:
- redis_ts_toolkit:/data
diff --git a/package.json b/package.json
index 35b0732..34dbdd6 100644
--- a/package.json
+++ b/package.json
@@ -69,6 +69,7 @@
"express": "^4.19.2",
"express-async-handler": "^1.2.0",
"express-session": "^1.18.0",
+ "google-auth-library": "^9.15.1",
"helmet": "^6.0.1",
"http-status-codes": "^2.3.0",
"ioredis": "^5.3.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 383fe70..9060df8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -71,6 +71,9 @@ importers:
express-session:
specifier: ^1.18.0
version: 1.18.1
+ google-auth-library:
+ specifier: ^9.15.1
+ version: 9.15.1
helmet:
specifier: ^6.0.1
version: 6.2.0
@@ -2090,6 +2093,10 @@ packages:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
+ agent-base@7.1.3:
+ resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==}
+ engines: {node: '>= 14'}
+
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@@ -2199,6 +2206,9 @@ packages:
resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==}
engines: {node: '>= 0.8'}
+ bignumber.js@9.3.0:
+ resolution: {integrity: sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==}
+
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
@@ -2806,6 +2816,9 @@ packages:
resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==}
engines: {node: '>= 0.10.0'}
+ extend@3.0.2:
+ resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
+
fast-copy@3.0.2:
resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==}
@@ -2936,6 +2949,14 @@ packages:
engines: {node: '>=10'}
deprecated: This package is no longer supported.
+ gaxios@6.7.1:
+ resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==}
+ engines: {node: '>=14'}
+
+ gcp-metadata@6.1.1:
+ resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==}
+ engines: {node: '>=14'}
+
generic-pool@3.9.0:
resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==}
engines: {node: '>= 4'}
@@ -3004,6 +3025,14 @@ packages:
resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
engines: {node: '>=10'}
+ google-auth-library@9.15.1:
+ resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==}
+ engines: {node: '>=14'}
+
+ google-logging-utils@0.0.2:
+ resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==}
+ engines: {node: '>=14'}
+
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
@@ -3011,6 +3040,10 @@ packages:
graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
+ gtoken@7.1.0:
+ resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==}
+ engines: {node: '>=14.0.0'}
+
has-bigints@1.1.0:
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
engines: {node: '>= 0.4'}
@@ -3069,6 +3102,10 @@ packages:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
+ https-proxy-agent@7.0.6:
+ resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
+ engines: {node: '>= 14'}
+
iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
@@ -3197,6 +3234,10 @@ packages:
resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
engines: {node: '>= 0.4'}
+ is-stream@2.0.1:
+ resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
+ engines: {node: '>=8'}
+
is-string@1.1.1:
resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
engines: {node: '>= 0.4'}
@@ -3271,6 +3312,9 @@ packages:
engines: {node: '>=6'}
hasBin: true
+ json-bigint@1.0.0:
+ resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
+
json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
@@ -3296,9 +3340,15 @@ packages:
jwa@1.4.1:
resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
+ jwa@2.0.1:
+ resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
+
jws@3.2.2:
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
+ jws@4.0.0:
+ resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==}
+
kareem@2.6.3:
resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==}
engines: {node: '>=12.0.0'}
@@ -6532,6 +6582,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ agent-base@7.1.3: {}
+
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
@@ -6654,6 +6706,8 @@ snapshots:
dependencies:
safe-buffer: 5.1.2
+ bignumber.js@9.3.0: {}
+
bl@4.1.0:
dependencies:
buffer: 5.7.1
@@ -7469,6 +7523,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ extend@3.0.2: {}
+
fast-copy@3.0.2: {}
fast-deep-equal@2.0.1: {}
@@ -7599,6 +7655,26 @@ snapshots:
strip-ansi: 6.0.1
wide-align: 1.1.5
+ gaxios@6.7.1:
+ dependencies:
+ extend: 3.0.2
+ https-proxy-agent: 7.0.6
+ is-stream: 2.0.1
+ node-fetch: 2.7.0
+ uuid: 9.0.1
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+
+ gcp-metadata@6.1.1:
+ dependencies:
+ gaxios: 6.7.1
+ google-logging-utils: 0.0.2
+ json-bigint: 1.0.0
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+
generic-pool@3.9.0: {}
gensync@1.0.0-beta.2: {}
@@ -7687,10 +7763,32 @@ snapshots:
merge2: 1.4.1
slash: 3.0.0
+ google-auth-library@9.15.1:
+ dependencies:
+ base64-js: 1.5.1
+ ecdsa-sig-formatter: 1.0.11
+ gaxios: 6.7.1
+ gcp-metadata: 6.1.1
+ gtoken: 7.1.0
+ jws: 4.0.0
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+
+ google-logging-utils@0.0.2: {}
+
gopd@1.2.0: {}
graphemer@1.4.0: {}
+ gtoken@7.1.0:
+ dependencies:
+ gaxios: 6.7.1
+ jws: 4.0.0
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+
has-bigints@1.1.0: {}
has-flag@4.0.0: {}
@@ -7753,6 +7851,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ https-proxy-agent@7.0.6:
+ dependencies:
+ agent-base: 7.1.3
+ debug: 4.4.0
+ transitivePeerDependencies:
+ - supports-color
+
iconv-lite@0.4.24:
dependencies:
safer-buffer: 2.1.2
@@ -7887,6 +7992,8 @@ snapshots:
dependencies:
call-bound: 1.0.3
+ is-stream@2.0.1: {}
+
is-string@1.1.1:
dependencies:
call-bound: 1.0.3
@@ -7960,6 +8067,10 @@ snapshots:
jsesc@3.1.0: {}
+ json-bigint@1.0.0:
+ dependencies:
+ bignumber.js: 9.3.0
+
json-buffer@3.0.1: {}
json-schema-traverse@0.4.1: {}
@@ -7991,11 +8102,22 @@ snapshots:
ecdsa-sig-formatter: 1.0.11
safe-buffer: 5.2.1
+ jwa@2.0.1:
+ dependencies:
+ buffer-equal-constant-time: 1.0.1
+ ecdsa-sig-formatter: 1.0.11
+ safe-buffer: 5.2.1
+
jws@3.2.2:
dependencies:
jwa: 1.4.1
safe-buffer: 5.2.1
+ jws@4.0.0:
+ dependencies:
+ jwa: 2.0.1
+ safe-buffer: 5.2.1
+
kareem@2.6.3: {}
keyv@4.5.4:
diff --git a/public/google-auth-test.html b/public/google-auth-test.html
new file mode 100644
index 0000000..9f08d44
--- /dev/null
+++ b/public/google-auth-test.html
@@ -0,0 +1,202 @@
+
+
+
+
+
+ Typescript Backend Toolkit - Google Auth Test
+
+
+
+
+
+
+
Typescript Backend Toolkit Google Auth Test
+
+
+
+
Google Identity Services (New)
+
Active
+
Uses the newer Google Identity Services API with improved security and UX:
+
+
+
+
+
+
Processing authentication...
+
+
+
+
+
+
+
+
Response:
+
Sign in to see the response...
+
+
+
+
+
+
diff --git a/src/config/config.service.ts b/src/config/config.service.ts
index 8c52967..4ca83a1 100644
--- a/src/config/config.service.ts
+++ b/src/config/config.service.ts
@@ -1,47 +1,48 @@
-import dotenv from "dotenv";
-import { z } from "zod";
+import dotenv from 'dotenv';
+import { z } from 'zod';
dotenv.config();
// Remove .optional() from requried schema properties
const configSchema = z.object({
- REDIS_URL: z.string().url(),
- PORT: z.string().regex(/^\d+$/).transform(Number),
- MONGO_DATABASE_URL: z.string().url(),
- SMTP_HOST: z.string().min(1).optional(),
- SMTP_PORT: z.string().regex(/^\d+$/).transform(Number).optional(),
- SMTP_USERNAME: z.string().email().optional(),
- EMAIL_FROM: z.string().email().optional(),
- SMTP_FROM: z.string().min(1).optional(),
- SMTP_PASSWORD: z.string().min(1).optional(),
- CLIENT_SIDE_URL: z.string().url(),
- JWT_SECRET: z.string().min(1),
- JWT_EXPIRES_IN: z.string().default("86400").transform(Number),
- SESSION_EXPIRES_IN: z.string().default("86400").transform(Number),
- PASSWORD_RESET_TOKEN_EXPIRES_IN: z.string().default("86400").transform(Number),
- SET_PASSWORD_TOKEN_EXPIRES_IN: z.string().default("86400").transform(Number),
- STATIC_OTP: z.enum(["1", "0"]).transform(Number).optional(),
- NODE_ENV: z
- .union([z.literal("production"), z.literal("development")])
- .default("development")
- .optional(),
- SET_SESSION: z
- .string()
- .transform((value) => !!Number(value))
- .optional(),
- GOOGLE_CLIENT_ID: z.string().optional(),
- GOOGLE_CLIENT_SECRET: z.string().optional(),
- GOOGLE_REDIRECT_URI: z.string().optional(),
- APP_NAME: z.string().default("API V1"),
- APP_VERSION: z.string().default("1.0.0"),
- // Mailgun configuration
- MAILGUN_API_KEY: z.string().min(1),
- MAILGUN_DOMAIN: z.string().min(1),
- MAILGUN_FROM_EMAIL: z.string().email(),
- ADMIN_EMAIL: z.string().email(),
- ADMIN_PASSWORD: z.string().min(1),
- OTP_VERIFICATION_ENABLED: z.string().transform((value) => !!Number(value)),
+ REDIS_URL: z.string().url(),
+ PORT: z.string().regex(/^\d+$/).transform(Number),
+ MONGO_DATABASE_URL: z.string().url(),
+ SMTP_HOST: z.string().min(1).optional(),
+ SMTP_PORT: z.string().regex(/^\d+$/).transform(Number).optional(),
+ SMTP_USERNAME: z.string().email().optional(),
+ EMAIL_FROM: z.string().email().optional(),
+ SMTP_FROM: z.string().min(1).optional(),
+ SMTP_PASSWORD: z.string().min(1).optional(),
+ CLIENT_SIDE_URL: z.string().url(),
+ JWT_SECRET: z.string().min(1),
+ JWT_EXPIRES_IN: z.string().default('86400').transform(Number),
+ SESSION_EXPIRES_IN: z.string().default('86400').transform(Number),
+ PASSWORD_RESET_TOKEN_EXPIRES_IN: z
+ .string()
+ .default('86400')
+ .transform(Number),
+ SET_PASSWORD_TOKEN_EXPIRES_IN: z.string().default('86400').transform(Number),
+ STATIC_OTP: z.enum(['1', '0']).transform(Number).optional(),
+ NODE_ENV: z
+ .union([z.literal('production'), z.literal('development')])
+ .default('development')
+ .optional(),
+ SET_SESSION: z
+ .string()
+ .transform((value) => !!Number(value))
+ .optional(),
+ GOOGLE_CLIENT_ID: z.string().optional(),
+ APP_NAME: z.string().default('API V1'),
+ APP_VERSION: z.string().default('1.0.0'),
+ // Mailgun configuration
+ MAILGUN_API_KEY: z.string().min(1),
+ MAILGUN_DOMAIN: z.string().min(1),
+ MAILGUN_FROM_EMAIL: z.string().email(),
+ ADMIN_EMAIL: z.string().email(),
+ ADMIN_PASSWORD: z.string().min(1),
+ OTP_VERIFICATION_ENABLED: z.string().transform((value) => !!Number(value)),
});
export type Config = z.infer;
diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts
index d0e1f31..78e1ae0 100644
--- a/src/modules/auth/auth.controller.ts
+++ b/src/modules/auth/auth.controller.ts
@@ -1,109 +1,106 @@
-import type { Request, Response } from "express";
-import config from "../../config/config.service";
-import type { GoogleCallbackQuery } from "../../types";
-import { successResponse } from "../../utils/api.utils";
-import type { JwtPayload } from "../../utils/auth.utils";
-import { AUTH_COOKIE_KEY, COOKIE_CONFIG } from "./auth.constants";
+import type { Request, Response } from 'express';
+import config from '../../config/config.service';
+import { successResponse } from '../../utils/api.utils';
+import type { JwtPayload } from '../../utils/auth.utils';
+import { AUTH_COOKIE_KEY, COOKIE_CONFIG } from './auth.constants';
import type {
- ChangePasswordSchemaType,
- ForgetPasswordSchemaType,
- LoginUserByEmailSchemaType,
- RegisterUserByEmailSchemaType,
- ResetPasswordSchemaType,
-} from "./auth.schema";
+ ChangePasswordSchemaType,
+ ForgetPasswordSchemaType,
+ GoogleTokenVerificationSchemaType,
+ LoginUserByEmailSchemaType,
+ RegisterUserByEmailSchemaType,
+ ResetPasswordSchemaType,
+} from './auth.schema';
import {
- changePassword,
- forgetPassword,
- googleLogin,
- loginUserByEmail,
- registerUserByEmail,
- resetPassword,
-} from "./auth.service";
+ changePassword,
+ forgetPassword,
+ loginUserByEmail,
+ registerUserByEmail,
+ resetPassword,
+ verifyGoogleToken,
+} from './auth.service';
export const handleResetPassword = async (
- req: Request,
- res: Response,
+ req: Request,
+ res: Response,
) => {
- await resetPassword(req.body);
+ await resetPassword(req.body);
- return successResponse(res, "Password successfully reset");
+ return successResponse(res, 'Password successfully reset');
};
export const handleForgetPassword = async (
- req: Request,
- res: Response,
+ req: Request,
+ res: Response,
) => {
- const user = await forgetPassword(req.body);
+ const user = await forgetPassword(req.body);
- return successResponse(res, "Code has been sent", { userId: user._id });
+ return successResponse(res, 'Code has been sent', { userId: user._id });
};
export const handleChangePassword = async (
- req: Request,
- res: Response,
+ req: Request,
+ res: Response,
) => {
- await changePassword((req.user as JwtPayload).sub, req.body);
+ await changePassword((req.user as JwtPayload).sub, req.body);
- return successResponse(res, "Password successfully changed");
+ return successResponse(res, 'Password successfully changed');
};
export const handleRegisterUser = async (
- req: Request,
- res: Response,
+ req: Request,
+ res: Response,
) => {
- const user = await registerUserByEmail(req.body);
+ const user = await registerUserByEmail(req.body);
- if (config.OTP_VERIFICATION_ENABLED) {
- return successResponse(res, "Please check your email for OTP", user);
- }
+ if (config.OTP_VERIFICATION_ENABLED) {
+ return successResponse(res, 'Please check your email for OTP', user);
+ }
- return successResponse(res, "User has been reigstered", user);
+ return successResponse(res, 'User has been reigstered', user);
};
export const handleLogout = async (_: Request, res: Response) => {
- res.cookie(AUTH_COOKIE_KEY, undefined, COOKIE_CONFIG);
+ res.cookie(AUTH_COOKIE_KEY, undefined, COOKIE_CONFIG);
- return successResponse(res, "Logout successful");
+ return successResponse(res, 'Logout successful');
};
export const handleLoginByEmail = async (
- req: Request,
- res: Response,
+ req: Request,
+ res: Response,
) => {
- const token = await loginUserByEmail(req.body);
- if (config.SET_SESSION) {
- res.cookie(AUTH_COOKIE_KEY, token, COOKIE_CONFIG);
- }
- return successResponse(res, "Login successful", { token: token });
+ const token = await loginUserByEmail(req.body);
+ if (config.SET_SESSION) {
+ res.cookie(AUTH_COOKIE_KEY, token, COOKIE_CONFIG);
+ }
+ return successResponse(res, 'Login successful', { token: token });
};
export const handleGetCurrentUser = async (req: Request, res: Response) => {
- const user = req.user;
+ const user = req.user;
- return successResponse(res, undefined, user);
+ return successResponse(res, undefined, user);
};
-export const handleGoogleLogin = async (_: Request, res: Response) => {
- if (!config.GOOGLE_CLIENT_ID || !config.GOOGLE_REDIRECT_URI) {
- throw new Error("Google credentials are not set");
- }
- const googleAuthURL = `https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=${config.GOOGLE_CLIENT_ID}&redirect_uri=${config.GOOGLE_REDIRECT_URI}&scope=email profile`;
-
- res.redirect(googleAuthURL);
-};
-export const handleGoogleCallback = async (
- req: Request,
- res: Response,
+export const handleGoogleTokenVerification = async (
+ req: Request,
+ res: Response,
) => {
- const user = await googleLogin(req.query);
- if (!user) throw new Error("Failed to login");
- res.cookie(
- AUTH_COOKIE_KEY,
- user.socialAccount?.[0]?.accessToken,
- COOKIE_CONFIG,
- );
-
- return successResponse(res, "Logged in successfully", {
- token: user.socialAccount?.[0]?.accessToken,
- });
+ const { user, token } = await verifyGoogleToken(req.body);
+
+ if (config.SET_SESSION) {
+ res.cookie(AUTH_COOKIE_KEY, token, COOKIE_CONFIG);
+ }
+
+ return successResponse(res, 'Google authentication successful', {
+ user: {
+ id: user._id,
+ email: user.email,
+ username: user.username,
+ avatar: user.avatar,
+ role: user.role,
+ },
+ token,
+ });
};
diff --git a/src/modules/auth/auth.router.ts b/src/modules/auth/auth.router.ts
index 5cf02d5..d983970 100644
--- a/src/modules/auth/auth.router.ts
+++ b/src/modules/auth/auth.router.ts
@@ -1,64 +1,66 @@
-import { canAccess } from "../../middlewares/can-access.middleware";
-import MagicRouter from "../../openapi/magic-router";
+import { canAccess } from '../../middlewares/can-access.middleware';
+import MagicRouter from '../../openapi/magic-router';
import {
- handleChangePassword,
- handleForgetPassword,
- handleGetCurrentUser,
- handleGoogleCallback,
- handleGoogleLogin,
- handleLoginByEmail,
- handleLogout,
- handleRegisterUser,
- handleResetPassword,
-} from "./auth.controller";
+ handleChangePassword,
+ handleForgetPassword,
+ handleGetCurrentUser,
+ handleGoogleTokenVerification,
+ handleLoginByEmail,
+ handleLogout,
+ handleRegisterUser,
+ handleResetPassword,
+} from './auth.controller';
import {
- changePasswordSchema,
- forgetPasswordSchema,
- loginUserByEmailSchema,
- registerUserByEmailSchema,
- resetPasswordSchema,
-} from "./auth.schema";
+ changePasswordSchema,
+ forgetPasswordSchema,
+ googleTokenVerificationSchema,
+ loginUserByEmailSchema,
+ registerUserByEmailSchema,
+ resetPasswordSchema,
+} from './auth.schema';
-export const AUTH_ROUTER_ROOT = "/auth";
+export const AUTH_ROUTER_ROOT = '/auth';
const authRouter = new MagicRouter(AUTH_ROUTER_ROOT);
authRouter.post(
- "/login/email",
- { requestType: { body: loginUserByEmailSchema } },
- handleLoginByEmail,
+ '/login/email',
+ { requestType: { body: loginUserByEmailSchema } },
+ handleLoginByEmail,
);
authRouter.post(
- "/register/email",
- { requestType: { body: registerUserByEmailSchema } },
- handleRegisterUser,
+ '/register/email',
+ { requestType: { body: registerUserByEmailSchema } },
+ handleRegisterUser,
);
-authRouter.post("/logout", {}, handleLogout);
+authRouter.post('/logout', {}, handleLogout);
-authRouter.get("/me", {}, canAccess(), handleGetCurrentUser);
+authRouter.get('/me', {}, canAccess(), handleGetCurrentUser);
authRouter.post(
- "/forget-password",
- { requestType: { body: forgetPasswordSchema } },
- handleForgetPassword,
+ '/forget-password',
+ { requestType: { body: forgetPasswordSchema } },
+ handleForgetPassword,
);
authRouter.post(
- "/change-password",
- { requestType: { body: changePasswordSchema } },
- canAccess(),
- handleChangePassword,
+ '/change-password',
+ { requestType: { body: changePasswordSchema } },
+ canAccess(),
+ handleChangePassword,
);
authRouter.post(
- "/reset-password",
- { requestType: { body: resetPasswordSchema } },
- handleResetPassword,
+ '/reset-password',
+ { requestType: { body: resetPasswordSchema } },
+ handleResetPassword,
);
-authRouter.get("/google", {}, handleGoogleLogin);
-authRouter.get("/google/callback", {}, handleGoogleCallback);
-
+authRouter.post(
+ '/google/token',
+ { requestType: { body: googleTokenVerificationSchema } },
+ handleGoogleTokenVerification,
+);
export default authRouter.getRouter();
diff --git a/src/modules/auth/auth.schema.ts b/src/modules/auth/auth.schema.ts
index dc74e17..c10a6e8 100644
--- a/src/modules/auth/auth.schema.ts
+++ b/src/modules/auth/auth.schema.ts
@@ -1,57 +1,65 @@
-import validator from "validator";
-import z from "zod";
-import { passwordValidationSchema } from "../../common/common.schema";
-import { baseCreateUser } from "../user/user.schema";
+import validator from 'validator';
+import z from 'zod';
+import { passwordValidationSchema } from '../../common/common.schema';
+import { baseCreateUser } from '../user/user.schema';
export const resetPasswordSchema = z.object({
- userId: z
- .string({ required_error: "userId is required" })
- .min(1)
- .refine((value) => validator.isMongoId(value), "userId must be valid"),
- code: z
- .string({ required_error: "code is required" })
- .min(4)
- .max(4)
- .refine((value) => validator.isAlphanumeric(value), "code must be valid"),
- password: passwordValidationSchema("Password"),
- confirmPassword: passwordValidationSchema("Confirm password"),
+ userId: z
+ .string({ required_error: 'userId is required' })
+ .min(1)
+ .refine((value) => validator.isMongoId(value), 'userId must be valid'),
+ code: z
+ .string({ required_error: 'code is required' })
+ .min(4)
+ .max(4)
+ .refine((value) => validator.isAlphanumeric(value), 'code must be valid'),
+ password: passwordValidationSchema('Password'),
+ confirmPassword: passwordValidationSchema('Confirm password'),
});
export const changePasswordSchema = z.object({
- currentPassword: passwordValidationSchema("Current password"),
- newPassword: passwordValidationSchema("New password"),
+ currentPassword: passwordValidationSchema('Current password'),
+ newPassword: passwordValidationSchema('New password'),
});
export const forgetPasswordSchema = z.object({
- email: z
- .string({ required_error: "Email is required" })
- .email("Email must be valid"),
+ email: z
+ .string({ required_error: 'Email is required' })
+ .email('Email must be valid'),
});
export const registerUserByEmailSchema = z
- .object({
- name: z.string({ required_error: "Name is required" }).min(1),
- confirmPassword: passwordValidationSchema("Confirm Password"),
- })
- .merge(baseCreateUser)
- .strict()
- .refine(({ password, confirmPassword }) => {
- if (password !== confirmPassword) {
- return false;
- }
-
- return true;
- }, "Password and confirm password must be same");
+ .object({
+ name: z.string({ required_error: 'Name is required' }).min(1),
+ confirmPassword: passwordValidationSchema('Confirm Password'),
+ })
+ .merge(baseCreateUser)
+ .strict()
+ .refine(({ password, confirmPassword }) => {
+ if (password !== confirmPassword) {
+ return false;
+ }
+
+ return true;
+ }, 'Password and confirm password must be same');
export const loginUserByEmailSchema = z.object({
- email: z
- .string({ required_error: "Email is required" })
- .email({ message: "Email is not valid" }),
- password: z.string().min(1, "Password is required"),
+ email: z
+ .string({ required_error: 'Email is required' })
+ .email({ message: 'Email is not valid' }),
+ password: z.string().min(1, 'Password is required'),
+});
+export const googleTokenVerificationSchema = z.object({
+ idToken: z
+ .string({ required_error: 'Google ID token is required' })
+ .min(1, 'Google ID token cannot be empty'),
});
+export type GoogleTokenVerificationSchemaType = z.infer<
+ typeof googleTokenVerificationSchema
+>;
export type RegisterUserByEmailSchemaType = z.infer<
- typeof registerUserByEmailSchema
+ typeof registerUserByEmailSchema
>;
export type LoginUserByEmailSchemaType = z.infer;
diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts
index 99ca864..90b2e0f 100644
--- a/src/modules/auth/auth.service.ts
+++ b/src/modules/auth/auth.service.ts
@@ -1,187 +1,186 @@
-import config from "../../config/config.service";
-import { ROLE_ENUM, type RoleType, SOCIAL_ACCOUNT_ENUM } from "../../enums";
-import type { GoogleCallbackQuery } from "../../types";
+import config from '../../config/config.service';
+import { ROLE_ENUM, type RoleType, SOCIAL_ACCOUNT_ENUM } from '../../enums';
import {
- type JwtPayload,
- compareHash,
- fetchGoogleTokens,
- generateOTP,
- getUserInfo,
- hashPassword,
- signToken,
-} from "../../utils/auth.utils";
-import { generateRandomNumbers } from "../../utils/common.utils";
-import type { UserType } from "../user/user.dto";
+ type JwtPayload,
+ compareHash,
+ generateOTP,
+ hashPassword,
+ signToken,
+ verifyGoogleIdToken,
+} from '../../utils/auth.utils';
+import { generateRandomNumbers } from '../../utils/common.utils';
+import type { UserType } from '../user/user.dto';
import {
- createUser,
- getUserByEmail,
- getUserById,
- updateUser,
-} from "../user/user.services";
+ createUser,
+ getUserByEmail,
+ getUserById,
+ updateUser,
+} from '../user/user.services';
import type {
- ChangePasswordSchemaType,
- ForgetPasswordSchemaType,
- LoginUserByEmailSchemaType,
- RegisterUserByEmailSchemaType,
- ResetPasswordSchemaType,
-} from "./auth.schema";
+ ChangePasswordSchemaType,
+ ForgetPasswordSchemaType,
+ GoogleTokenVerificationSchemaType,
+ LoginUserByEmailSchemaType,
+ RegisterUserByEmailSchemaType,
+ ResetPasswordSchemaType,
+} from './auth.schema';
export const resetPassword = async (payload: ResetPasswordSchemaType) => {
- const user = await getUserById(payload.userId);
+ const user = await getUserById(payload.userId);
- if (!user || user.passwordResetCode !== payload.code) {
- throw new Error("token is not valid or expired, please try again");
- }
+ if (!user || user.passwordResetCode !== payload.code) {
+ throw new Error('token is not valid or expired, please try again');
+ }
- if (payload.confirmPassword !== payload.password) {
- throw new Error("Password and confirm password must be same");
- }
+ if (payload.confirmPassword !== payload.password) {
+ throw new Error('Password and confirm password must be same');
+ }
- const hashedPassword = await hashPassword(payload.password);
+ const hashedPassword = await hashPassword(payload.password);
- await updateUser(payload.userId, {
- password: hashedPassword,
- passwordResetCode: null,
- });
+ await updateUser(payload.userId, {
+ password: hashedPassword,
+ passwordResetCode: null,
+ });
};
export const forgetPassword = async (
- payload: ForgetPasswordSchemaType,
+ payload: ForgetPasswordSchemaType,
): Promise => {
- const user = await getUserByEmail(payload.email);
+ const user = await getUserByEmail(payload.email);
- if (!user) {
- throw new Error("user doesn't exists");
- }
+ if (!user) {
+ throw new Error("user doesn't exists");
+ }
- const code = generateRandomNumbers(4);
+ const code = generateRandomNumbers(4);
- await updateUser(user._id, { passwordResetCode: code });
+ await updateUser(user._id, { passwordResetCode: code });
- return user;
+ return user;
};
export const changePassword = async (
- userId: string,
- payload: ChangePasswordSchemaType,
+ userId: string,
+ payload: ChangePasswordSchemaType,
): Promise => {
- const user = await getUserById(userId, "+password");
+ const user = await getUserById(userId, '+password');
- if (!user || !user.password) {
- throw new Error("User is not found");
- }
+ if (!user || !user.password) {
+ throw new Error('User is not found');
+ }
- const isCurrentPassowordCorrect = await compareHash(
- user.password,
- payload.currentPassword,
- );
+ const isCurrentPassowordCorrect = await compareHash(
+ user.password,
+ payload.currentPassword,
+ );
- if (!isCurrentPassowordCorrect) {
- throw new Error("current password is not valid");
- }
+ if (!isCurrentPassowordCorrect) {
+ throw new Error('current password is not valid');
+ }
- const hashedPassword = await hashPassword(payload.newPassword);
+ const hashedPassword = await hashPassword(payload.newPassword);
- await updateUser(userId, { password: hashedPassword });
+ await updateUser(userId, { password: hashedPassword });
};
export const registerUserByEmail = async (
- payload: RegisterUserByEmailSchemaType,
+ payload: RegisterUserByEmailSchemaType,
): Promise => {
- const userExistByEmail = await getUserByEmail(payload.email);
+ const userExistByEmail = await getUserByEmail(payload.email);
- if (userExistByEmail) {
- throw new Error("Account already exist with same email address");
- }
+ if (userExistByEmail) {
+ throw new Error('Account already exist with same email address');
+ }
- const { confirmPassword, ...rest } = payload;
+ const { confirmPassword, ...rest } = payload;
- const otp = config.OTP_VERIFICATION_ENABLED ? generateOTP() : null;
+ const otp = config.OTP_VERIFICATION_ENABLED ? generateOTP() : null;
- const user = await createUser(
- { ...rest, role: "DEFAULT_USER", otp },
- false,
- );
+ const user = await createUser({ ...rest, role: 'DEFAULT_USER', otp }, false);
- return user;
+ return user;
};
export const loginUserByEmail = async (
- payload: LoginUserByEmailSchemaType,
+ payload: LoginUserByEmailSchemaType,
): Promise => {
- const user = await getUserByEmail(payload.email, "+password");
+ const user = await getUserByEmail(payload.email, '+password');
- if (!user || !(await compareHash(String(user.password), payload.password))) {
- throw new Error("Invalid email or password");
- }
+ if (!user || !(await compareHash(String(user.password), payload.password))) {
+ throw new Error('Invalid email or password');
+ }
- const jwtPayload: JwtPayload = {
- sub: String(user._id),
- email: user?.email,
- phoneNo: user?.phoneNo,
- role: String(user.role) as RoleType,
- username: user.username,
- };
+ const jwtPayload: JwtPayload = {
+ sub: String(user._id),
+ email: user?.email,
+ phoneNo: user?.phoneNo,
+ role: String(user.role) as RoleType,
+ username: user.username,
+ };
- const token = await signToken(jwtPayload);
+ const token = await signToken(jwtPayload);
- return token;
+ return token;
};
-export const googleLogin = async (
- payload: GoogleCallbackQuery,
-): Promise => {
- const { code, error } = payload;
-
- if (error) {
- throw new Error(error);
- }
-
- if (!code) {
- throw new Error("Code Not Provided");
- }
- const tokenResponse = await fetchGoogleTokens({ code });
-
- const { access_token, refresh_token, expires_in } = tokenResponse;
-
- const userInfoResponse = await getUserInfo(access_token);
-
- const { id, email, name, picture } = userInfoResponse;
-
- const user = await getUserByEmail(email);
-
- if (!user) {
- const newUser = await createUser({
- email,
- username: name,
- avatar: picture,
- role: ROLE_ENUM.DEFAULT_USER,
- password: generateRandomNumbers(4),
- socialAccount: [
- {
- refreshToken: refresh_token,
- tokenExpiry: new Date(Date.now() + expires_in * 1000),
- accountType: SOCIAL_ACCOUNT_ENUM.GOOGLE,
- accessToken: access_token,
- accountID: id,
- },
- ],
- });
-
- return newUser;
- }
-
- const updatedUser = await updateUser(user._id, {
- socialAccount: [
- {
- refreshToken: refresh_token,
- tokenExpiry: new Date(Date.now() + expires_in * 1000),
- accountType: SOCIAL_ACCOUNT_ENUM.GOOGLE,
- accessToken: access_token,
- accountID: id,
- },
- ],
- });
-
- return updatedUser;
+export const verifyGoogleToken = async (
+ payload: GoogleTokenVerificationSchemaType,
+): Promise<{ user: UserType; token: string }> => {
+ const tokenPayload = await verifyGoogleIdToken(payload.idToken);
+
+ const { googleId, email, name, picture, emailVerified, tokenExpiry } =
+ tokenPayload;
+
+ if (!emailVerified) {
+ throw new Error('Google account email is not verified');
+ }
+
+ const existingUser = await getUserByEmail(email);
+
+ let user: UserType;
+
+ if (!existingUser) {
+ user = await createUser({
+ email,
+ username: name || email.split('@')[0],
+ avatar: picture,
+ role: ROLE_ENUM.DEFAULT_USER,
+ password: generateRandomNumbers(8),
+ socialAccount: [
+ {
+ refreshToken: '',
+ tokenExpiry: new Date(tokenExpiry * 1000),
+ accountType: SOCIAL_ACCOUNT_ENUM.GOOGLE,
+ accessToken: payload.idToken,
+ accountID: googleId,
+ },
+ ],
+ });
+ } else {
+ user = await updateUser(existingUser._id, {
+ ...(picture && { avatar: picture }),
+ socialAccount: [
+ {
+ refreshToken: '',
+ tokenExpiry: new Date(tokenExpiry * 1000),
+ accountType: SOCIAL_ACCOUNT_ENUM.GOOGLE,
+ accessToken: payload.idToken,
+ accountID: googleId,
+ },
+ ],
+ });
+ }
+
+ const jwtPayload: JwtPayload = {
+ sub: String(user._id),
+ email: user.email,
+ phoneNo: user.phoneNo,
+ role: String(user.role) as RoleType,
+ username: user.username,
+ };
+
+ const token = await signToken(jwtPayload);
+
+ return { user, token };
};
diff --git a/src/utils/auth.utils.ts b/src/utils/auth.utils.ts
index f1e09be..fab9a1d 100644
--- a/src/utils/auth.utils.ts
+++ b/src/utils/auth.utils.ts
@@ -1,152 +1,133 @@
-import crypto from "node:crypto";
-import argon2 from "argon2";
-import { JsonWebTokenError, sign, verify } from "jsonwebtoken";
-import config from "../config/config.service";
-import type { RoleType } from "../enums";
-import logger from "../lib/logger.service";
+import crypto from 'node:crypto';
+import argon2 from 'argon2';
+import { OAuth2Client } from 'google-auth-library';
+import { JsonWebTokenError, sign, verify } from 'jsonwebtoken';
+import config from '../config/config.service';
+import type { RoleType } from '../enums';
+import logger from '../lib/logger.service';
export interface GoogleTokenResponse {
- access_token: string;
- expires_in: number;
- id_token: string;
- refresh_token?: string;
- scope: string;
- token_type: string;
+ access_token: string;
+ expires_in: number;
+ id_token: string;
+ refresh_token?: string;
+ scope: string;
+ token_type: string;
}
export interface GoogleTokensRequestParams {
- code: string;
+ code: string;
}
export type JwtPayload = {
- sub: string;
- email?: string | null;
- phoneNo?: string | null;
- username: string;
- role: RoleType;
+ sub: string;
+ email?: string | null;
+ phoneNo?: string | null;
+ username: string;
+ role: RoleType;
};
export type PasswordResetTokenPayload = {
- email: string;
- userId: string;
+ email: string;
+ userId: string;
};
export type SetPasswordTokenPayload = {
- email: string;
- userId: string;
+ email: string;
+ userId: string;
};
+const client = new OAuth2Client(config.GOOGLE_CLIENT_ID);
export const hashPassword = async (password: string): Promise => {
- return argon2.hash(password);
+ return argon2.hash(password);
};
export const compareHash = async (
- hashed: string,
- plainPassword: string,
+ hashed: string,
+ plainPassword: string,
): Promise => {
- return argon2.verify(hashed, plainPassword);
+ return argon2.verify(hashed, plainPassword);
};
export const signToken = async (payload: JwtPayload): Promise => {
- return sign(payload, String(config.JWT_SECRET), {
- expiresIn: Number(config.JWT_EXPIRES_IN) * 1000,
- });
+ return sign(payload, String(config.JWT_SECRET), {
+ expiresIn: Number(config.JWT_EXPIRES_IN) * 1000,
+ });
};
export const signPasswordResetToken = async (
- payload: PasswordResetTokenPayload,
+ payload: PasswordResetTokenPayload,
) => {
- return sign(payload, String(config.JWT_SECRET), {
- expiresIn: config.PASSWORD_RESET_TOKEN_EXPIRES_IN * 1000,
- });
+ return sign(payload, String(config.JWT_SECRET), {
+ expiresIn: config.PASSWORD_RESET_TOKEN_EXPIRES_IN * 1000,
+ });
};
export const signSetPasswordToken = async (
- payload: SetPasswordTokenPayload,
+ payload: SetPasswordTokenPayload,
) => {
- return sign(payload, String(config.JWT_SECRET), {
- expiresIn: config.SET_PASSWORD_TOKEN_EXPIRES_IN,
- });
+ return sign(payload, String(config.JWT_SECRET), {
+ expiresIn: config.SET_PASSWORD_TOKEN_EXPIRES_IN,
+ });
};
export const verifyToken = async <
- T extends JwtPayload | PasswordResetTokenPayload | SetPasswordTokenPayload,
+ T extends JwtPayload | PasswordResetTokenPayload | SetPasswordTokenPayload,
>(
- token: string,
+ token: string,
): Promise => {
- try {
- return verify(token, String(config.JWT_SECRET)) as T;
- } catch (err) {
- if (err instanceof Error) {
- throw new Error(err.message);
- }
-
- if (err instanceof JsonWebTokenError) {
- throw new Error(err.message);
- }
-
- logger.error("verifyToken", { err });
- throw err;
- }
+ try {
+ return verify(token, String(config.JWT_SECRET)) as T;
+ } catch (err) {
+ if (err instanceof Error) {
+ throw new Error(err.message);
+ }
+
+ if (err instanceof JsonWebTokenError) {
+ throw new Error(err.message);
+ }
+
+ logger.error('verifyToken', { err });
+ throw err;
+ }
};
export const generateRandomPassword = (length = 16): string => {
- return crypto.randomBytes(length).toString("hex");
+ return crypto.randomBytes(length).toString('hex');
};
-export const fetchGoogleTokens = async (
- params: GoogleTokensRequestParams,
-): Promise => {
- if (
- !config.GOOGLE_CLIENT_ID ||
- !config.GOOGLE_CLIENT_SECRET ||
- !config.GOOGLE_REDIRECT_URI
- ) {
- throw new Error("Google credentials are not set");
- }
-
- const url = "https://oauth2.googleapis.com/token";
- const response = await fetch(url, {
- method: "POST",
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
- body: new URLSearchParams({
- code: params.code,
- client_id: config.GOOGLE_CLIENT_ID,
- client_secret: config.GOOGLE_CLIENT_SECRET,
- redirect_uri: config.GOOGLE_REDIRECT_URI,
- grant_type: "authorization_code",
- }),
- });
-
- if (!response.ok) {
- throw new Error("Failed to exchange code for tokens");
- }
-
- const data: GoogleTokenResponse = await response.json();
- return data;
-};
-export interface GoogleUserInfo {
- id: string;
- email: string;
- verified_email: boolean;
- name: string;
- given_name: string;
- family_name: string;
- picture: string;
- locale: string;
-}
-export const getUserInfo = async (accessToken: string) => {
- const userInfoResponse = await fetch(
- "https://www.googleapis.com/oauth2/v2/userinfo",
- {
- headers: { Authorization: `Bearer ${accessToken}` },
- },
- );
- if (!userInfoResponse.ok) {
- throw new Error("Error fetching user info");
- }
- return userInfoResponse.json();
+export const generateOTP = (length = 6): string => {
+ return crypto.randomBytes(length).toString('hex').slice(0, length);
};
-export const generateOTP = (length = 6): string => {
- return crypto.randomBytes(length).toString("hex").slice(0, length);
+export const verifyGoogleIdToken = async (idToken: string) => {
+ try {
+ const ticket = await client.verifyIdToken({
+ idToken,
+ audience: config.GOOGLE_CLIENT_ID,
+ });
+
+ const payload = ticket.getPayload();
+
+ if (!payload) {
+ throw new Error('Invalid token payload');
+ }
+
+ const { sub, email, name, picture, email_verified, exp } = payload;
+
+ if (!email) {
+ throw new Error('Email not found in token payload');
+ }
+
+ return {
+ googleId: sub,
+ email,
+ name: name || email.split('@')[0],
+ picture,
+ emailVerified: email_verified,
+ tokenExpiry: exp,
+ };
+ } catch (error) {
+ logger.error('Error verifying Google ID token', { error });
+ throw new Error('Failed to verify Google ID token');
+ }
};