feat(auth): implement Google and Zalo OAuth backend strategies
Add complete OAuth2 authentication flow for Google and Zalo providers: - OAuthService: handles account linking (by email/phone), new user creation for OAuth-only accounts, and JWT token generation - GoogleOAuthStrategy: passport-google-oauth20 integration - ZaloOAuthStrategy: custom OAuth2 implementation using Zalo's API (authorization URL generation, code exchange, user info fetch) - OAuthController: redirect and callback endpoints for both providers with httpOnly cookie-based token management - Unit tests for OAuthService (7 tests), GoogleOAuthStrategy (4 tests), and ZaloOAuthStrategy (7 tests) - OAuth env vars added to .env.example and env-validation warnings Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Strategy, type Profile, type VerifyCallback } from 'passport-google-oauth20';
|
||||
import { type OAuthService, type OAuthUserProfile } from '../services/oauth.service';
|
||||
|
||||
@Injectable()
|
||||
export class GoogleOAuthStrategy extends PassportStrategy(Strategy, 'google') {
|
||||
constructor(private readonly oauthService: OAuthService) {
|
||||
const clientID = process.env['GOOGLE_CLIENT_ID'];
|
||||
const clientSecret = process.env['GOOGLE_CLIENT_SECRET'];
|
||||
const callbackURL = process.env['GOOGLE_CALLBACK_URL'] ?? '/auth/google/callback';
|
||||
|
||||
if (!clientID || !clientSecret) {
|
||||
throw new Error('GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables are required');
|
||||
}
|
||||
|
||||
super({
|
||||
clientID,
|
||||
clientSecret,
|
||||
callbackURL,
|
||||
scope: ['email', 'profile'],
|
||||
});
|
||||
}
|
||||
|
||||
async validate(
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
profile: Profile,
|
||||
done: VerifyCallback,
|
||||
): Promise<void> {
|
||||
const email = profile.emails?.[0]?.value;
|
||||
const photo = profile.photos?.[0]?.value;
|
||||
const fullName = profile.displayName || [profile.name?.givenName, profile.name?.familyName].filter(Boolean).join(' ') || 'Google User';
|
||||
|
||||
const oauthProfile: OAuthUserProfile = {
|
||||
provider: 'GOOGLE',
|
||||
providerUserId: profile.id,
|
||||
email,
|
||||
fullName,
|
||||
avatarUrl: photo,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
rawProfile: profile._json as Record<string, unknown>,
|
||||
};
|
||||
|
||||
try {
|
||||
const tokens = await this.oauthService.authenticateOAuth(oauthProfile);
|
||||
done(null, tokens);
|
||||
} catch (err) {
|
||||
done(err as Error, undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user