Skip to content

feat: implement Google OAuth authentication for customers#17

Open
OmarElprolosy66 wants to merge 1 commit intomainfrom
feature/12-google-auth
Open

feat: implement Google OAuth authentication for customers#17
OmarElprolosy66 wants to merge 1 commit intomainfrom
feature/12-google-auth

Conversation

@OmarElprolosy66
Copy link
Collaborator

  • Add Google OAuth 2.0 authentication flow using passport-google-oauth20
  • Implement account creation and linking for existing email addresses
  • Generate custom JWT tokens (access & refresh) for OAuth users
  • Add GoogleCustomerStrategy with environment variable validation
  • Create findOrCreateByGoogle method in CustomerService
  • Add googleLogin method in CustomerAuthService
  • Implement OAuth endpoints (initiate & callback) with proper redirect
  • Add comprehensive Swagger documentation for OAuth endpoints
  • Fix response handling to prevent headers already sent error
  • Register GoogleCustomerStrategy in CustomerAuthModule

- Add Google OAuth 2.0 authentication flow using passport-google-oauth20
- Implement account creation and linking for existing email addresses
- Generate custom JWT tokens (access & refresh) for OAuth users
- Add GoogleCustomerStrategy with environment variable validation
- Create findOrCreateByGoogle method in CustomerService
- Add googleLogin method in CustomerAuthService
- Implement OAuth endpoints (initiate & callback) with proper redirect
- Add comprehensive Swagger documentation for OAuth endpoints
- Fix response handling to prevent headers already sent error
- Register GoogleCustomerStrategy in CustomerAuthModule
Copilot AI review requested due to automatic review settings January 22, 2026 15:12
@OmarElprolosy66 OmarElprolosy66 self-assigned this Jan 22, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements Google OAuth 2.0 authentication for customers, integrates it with the existing JWT-based auth and Prisma customer model, and updates docs/tests around category and admin/employee routes.

Changes:

  • Add Google OAuth 2.0 strategy for customers, a googleLogin flow that issues access/refresh JWTs, and OAuth init/callback endpoints with redirect handling.
  • Extend the Customer model and DTO to use the PROVIDER enum with a default, add a migration, and wire up the new Google strategy into the customer auth module.
  • Adjust category deletion routes and tests (removing /admin from the path) and clarify Swagger summaries for admin/employee-only product and payment endpoints, plus add OAuth env vars and dependencies.

Reviewed changes

Copilot reviewed 17 out of 19 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
backend/test/category.e2e-spec.ts Updates category DELETE e2e tests to match the new /api/categories/:id admin-only route and expected 403/400/204 behaviors.
backend/src/product/product.controller.ts Refines Swagger @ApiOperation summaries to explicitly indicate admin/employee-only operations for product CRUD and related variant/image routes.
backend/src/payment/payment.controller.ts Clarifies the Swagger summary on the cash-on-delivery endpoint to indicate admin/employee usage.
backend/src/customer/dto/create-cusotmer.dto.ts Switches the provider field from a free-form string to the PROVIDER enum and updates the Swagger example accordingly.
backend/src/customer/customer.service.ts Adds findOrCreateByGoogle to locate, link, or create customers based on Google profile data, setting provider, providerId, and status for OAuth accounts.
backend/src/category/category.controller.ts Changes the admin-only DELETE route from admin/:id to :id and updates Swagger summaries for create/update to highlight admin/employee access.
backend/src/auth/user-auth/strategies/jwt.user.strategy.ts Simplifies JWT secret configuration by using configService.getOrThrow('JWT_USER_SECRET') in the user JWT strategy.
backend/src/auth/customer-auth/strategies/jwt.customer.strategy.ts Uses configService.getOrThrow('JWT_CUSTOMER_SECRET') for the customer JWT strategy secret instead of manual null checks.
backend/src/auth/customer-auth/strategies/google.customer.strategy.ts Introduces a Google Passport strategy that validates profiles and maps Google user data into a normalized object for downstream auth.
backend/src/auth/customer-auth/customer-auth.service.ts Implements googleLogin to integrate Google profiles with customers, enforce status checks, issue customer JWTs, and store hashed refresh tokens.
backend/src/auth/customer-auth/customer-auth.module.ts Registers GoogleCustomerStrategy alongside the existing customer auth service and JWT strategy.
backend/src/auth/customer-auth/customer-auth.controller.ts Adds /customers/auth/google and /customers/auth/google/callback endpoints to initiate and handle Google OAuth, set the refresh token cookie, and redirect with an access token.
backend/public/uploads/products/1768921287553-2e1f99a2-406a-4669-9f93-8ac803fa0b46.png Adds a product image asset to the public uploads directory.
backend/prisma/schema.prisma Sets a default of LOCAL for the nullable Customer.provider enum and defines the PROVIDER and Status enums used by customer auth.
backend/prisma/migrations/20260120130403_fix_customer_auth_provider_default_value/migration.sql Alters the customers.provider column to default to 'LOCAL' at the database level.
backend/package.json Adds passport-google-oauth20, its type definitions, and clinic as a dev dependency to support Google OAuth and profiling.
backend/.gitignore Ignores the .clinic directory created by the Clinic profiling tool.
backend/.env.example Documents required Google OAuth environment variables (GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_CALLBACK_URL).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +486 to +538
// Try to find existing customer by provider ID first
let customer = await this.prisma.customer.findFirst({
where: {
provider: 'GOOGLE',
providerId: profile.providerId,
},
});

if (customer) {
this.logger.log(
`Existing Google customer found: ${customer.email} (ID: ${customer.id})`,
CustomerService.name,
);
return customer;
}

// Check if customer exists with same email but different provider
customer = await this.prisma.customer.findUnique({
where: { email: profile.email },
});

if (customer) {
// Link Google account to existing customer
this.logger.log(
`Linking Google account to existing customer: ${customer.email} (ID: ${customer.id})`,
CustomerService.name,
);

customer = await this.prisma.customer.update({
where: { id: customer.id },
data: {
provider: 'GOOGLE',
providerId: profile.providerId,
},
});

return customer;
}

// Create new customer
this.logger.log(
`Creating new Google customer: ${profile.email}`,
CustomerService.name,
);

customer = await this.prisma.customer.create({
data: {
email: profile.email,
name: profile.name,
provider: 'GOOGLE',
providerId: profile.providerId,
status: 'ACTIVE',
passwordHash: null, // No password for OAuth users
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findOrCreateByGoogle is using raw string literals for Prisma enum fields (provider: 'GOOGLE' and status: 'ACTIVE') both in the query filter and in the create/update payload. This will not type-check against the generated Prisma types (which expect PROVIDER and Status enum values) and couples the implementation to the exact string values. Import the PROVIDER and Status enums from @prisma/client and use those constants for both the where filter and the data assignments to keep the method type-safe and aligned with the schema enums.

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +24
const { id, emails, displayName } = profile;
const email = emails && emails.length > 0 ? emails[0].value : null;

Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

email is derived as emails && emails.length > 0 ? emails[0].value : null, but downstream the Google login flow (findOrCreateByGoogle and the Prisma Customer model) assumes a non-null, unique email (the schema defines email String @unique and findOrCreateByGoogle calls findUnique({ where: { email: profile.email } }) and create({ data: { email: profile.email, ... } })). If Google does not return an email, this will lead to an invalid where clause and a failing create on a non-null column at runtime; consider failing validation early here (e.g., throwing an explicit error) when email is missing instead of returning a profile with email: null.

Copilot uses AI. Check for mistakes.
Comment on lines +102 to +113
// Set refresh token as httpOnly cookie
res.cookie('refresh_token', refresh_token, {
httpOnly: true,
secure: this.configService.get('NODE_ENV') === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
path: '/api/customers/auth'
});

// Redirect to frontend with access token
const frontendUrl = this.configService.get('FRONTEND_URL') || 'http://localhost:3000';
const redirectUrl = `${frontendUrl}/auth/google/callback?access_token=${result.access_token}`;
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Google OAuth callback currently redirects to the frontend with the JWT access token in the query string (.../auth/google/callback?access_token=...). Putting bearer tokens in URLs is insecure because they can leak via browser history, referrer headers, and logs; it would be safer to deliver the access token via an httpOnly cookie (similar to the refresh token) or another mechanism that does not place the token in the URL.

Suggested change
// Set refresh token as httpOnly cookie
res.cookie('refresh_token', refresh_token, {
httpOnly: true,
secure: this.configService.get('NODE_ENV') === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
path: '/api/customers/auth'
});
// Redirect to frontend with access token
const frontendUrl = this.configService.get('FRONTEND_URL') || 'http://localhost:3000';
const redirectUrl = `${frontendUrl}/auth/google/callback?access_token=${result.access_token}`;
const isProduction = this.configService.get('NODE_ENV') === 'production';
// Set refresh token as httpOnly cookie
res.cookie('refresh_token', refresh_token, {
httpOnly: true,
secure: isProduction,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
path: '/api/customers/auth'
});
// Optionally set access token as httpOnly cookie to avoid exposing it in the URL
if (result.access_token) {
res.cookie('access_token', result.access_token, {
httpOnly: true,
secure: isProduction,
sameSite: 'strict',
// Adjust maxAge if needed to match access token lifetime
maxAge: 15 * 60 * 1000, // 15 minutes
path: '/api/customers/auth'
});
}
// Redirect to frontend without including access token in the URL
const frontendUrl = this.configService.get('FRONTEND_URL') || 'http://localhost:3000';
const redirectUrl = `${frontendUrl}/auth/google/callback`;

Copilot uses AI. Check for mistakes.
Comment on lines +252 to +303
async googleLogin(profile: { providerId: string; email: string; name: string }, device?: string, ip?: string): Promise<ResponseAuthCustomerDto & { refresh_token: string }> {
this.logger.debug(`Google login attempt for email: ${profile.email}`, CustomerAuthService.name);

// Find or create customer by Google profile
const customer = await this.customerService.findOrCreateByGoogle(profile);

if (customer.status === Status.INACTIVE) {
this.logger.warn(`Google login failed. Account is inactive: ${customer.email}`, CustomerAuthService.name);
throw new UnauthorizedException('Account is inactive');
}

this.logger.log(`Google customer authenticated successfully: ${customer.email} (ID: ${customer.id})`, CustomerAuthService.name);

// Generate JWT tokens (same as regular login)
const payload = {
sub: customer.id,
email: customer.email,
};

const access_token = await this.jwtService.signAsync(payload, {
secret: this.configService.get<string>('JWT_CUSTOMER_SECRET')!,
expiresIn: this.configService.get<string>('JWT_CUSTOMER_EXPIRES_IN') ?? '15m' as any,
});

const refresh_token = await this.jwtService.signAsync(
{
...payload,
jti: randomUUID(),
},
{
secret: this.configService.get<string>('JWT_CUSTOMER_REFRESH_SECRET')!,
expiresIn: this.configService.get<string>('JWT_CUSTOMER_REFRESH_EXPIRES_IN') ?? '7d' as any,
}
);

// Store refresh token
await this.customerService.storeRefreshToken(
customer.id,
await argon2.hash(refresh_token),
device,
ip
);

return {
name: customer.name,
email: customer.email,
phone: customer.phone ?? '',
status: customer.status ?? Status.ACTIVE,
access_token,
refresh_token,
};
}
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The newly added Google OAuth flow (GET /customers/auth/google, GET /customers/auth/google/callback, and CustomerAuthService.googleLogin) is not covered by the existing e2e tests, even though other customer auth flows (signup, login, refresh, logout, logout-all) are thoroughly tested in customer.e2e-spec.ts. Adding e2e tests for a successful Google login, inactive-account rejection, and basic error paths would help ensure this new authentication path remains reliable.

Copilot uses AI. Check for mistakes.
@@ -68,12 +69,12 @@ export class CreateCustomerDto {

@ApiProperty({
description: 'OAuth provider name (e.g., google, facebook)',
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description for the provider field still says OAuth provider name (e.g., google, facebook), but the type has been tightened to the PROVIDER enum, which currently only defines GOOGLE and LOCAL in schema.prisma. To avoid confusing API consumers, update this description and example to reflect the actual allowed enum values (e.g., GOOGLE, LOCAL) or expand the enum if you intend to support additional providers like Facebook.

Suggested change
description: 'OAuth provider name (e.g., google, facebook)',
description: 'Authentication provider (PROVIDER enum: GOOGLE, LOCAL)',

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants