| Server IP : 65.108.144.40 / Your IP : 216.73.217.165 Web Server : Apache/2.4.52 (Ubuntu) System : Linux ubuntu-8gb-hel1-1 5.15.0-173-generic #183-Ubuntu SMP Fri Mar 6 13:29:34 UTC 2026 x86_64 User : dev ( 1000) PHP Version : 8.2.30 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : ON Directory : /home/dev/webapps/fivewishes-bridge/src/auth/ |
Upload File : |
import { Controller, Get, Query, Res } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import type { Response } from 'express';
import { AppError } from '../common/errors';
import { MultipassService } from '../multipass/multipass.service';
import { ShopifyService } from '../shopify/shopify.service';
import { KeycloakOidcService } from './keycloak-oidc.service';
import { PkceService } from './pkce.service';
import { StateService } from './state.service';
@Controller('auth')
export class AuthController {
constructor(
private readonly config: ConfigService,
private readonly keycloak: KeycloakOidcService,
private readonly stateService: StateService,
private readonly pkce: PkceService,
private readonly shopify: ShopifyService,
private readonly multipass: MultipassService,
) {}
@Get('login')
login(@Res() res: Response, @Query('return_to') returnTo?: string) {
const safeReturnTo = this.normalizeReturnTo(returnTo);
const { state, nonce } = this.stateService.createState({ return_to: safeReturnTo });
const { codeChallenge, codeChallengeMethod } = this.pkce.create(nonce);
const url = this.keycloak.buildAuthorizationUrl({
state,
nonce,
codeChallenge,
codeChallengeMethod,
});
return res.redirect(url);
}
@Get('forgot-password')
forgotPassword(
@Res() res: Response,
@Query('email') email?: string,
@Query('return_to') returnTo?: string,
) {
const safeReturnTo = this.normalizeReturnTo(returnTo);
const base = this.config.get<string>('BASE_URL');
if (!base) {
throw new AppError('Missing BASE_URL', { statusCode: 500 });
}
const redirectBack = new URL(`${base.replace(/\/+$/g, '')}/auth/login`);
if (safeReturnTo) redirectBack.searchParams.set('return_to', safeReturnTo);
const url = this.keycloak.buildResetPasswordUrl({
redirectUri: redirectBack.toString(),
loginHint: email,
});
return res.redirect(url);
}
@Get('callback')
async callback(
@Res() res: Response,
@Query('code') code?: string,
@Query('state') stateParam?: string,
@Query('error') error?: string,
@Query('error_description') errorDescription?: string,
) {
if (error) {
const msg = errorDescription ? `${error}: ${errorDescription}` : error;
throw new AppError(msg, { statusCode: 401, expose: true });
}
if (!code || !stateParam) {
throw new AppError('Missing code or state', { statusCode: 400, expose: true });
}
const state = this.stateService.verifyState(stateParam);
const codeVerifier = this.pkce.consumeVerifier(state.nonce);
if (!codeVerifier) {
throw new AppError('Missing PKCE verifier (session expired)', { statusCode: 400, expose: true });
}
const tokenResponse = await this.keycloak.exchangeCodeForTokens({
code,
codeVerifier,
});
if (!tokenResponse.id_token) {
throw new AppError('Missing id_token from Keycloak', { statusCode: 502 });
}
const profile = await this.keycloak.verifyAndExtractProfile({
idToken: tokenResponse.id_token,
expectedNonce: state.nonce,
accessToken: tokenResponse.access_token,
});
const customerId = await this.shopify.upsertCustomerFromKeycloak(profile);
const b2bEnabled = this.shopify.isB2bEnabled();
if (b2bEnabled && !profile.company_id) {
// eslint-disable-next-line no-console
console.warn('B2B enabled but Keycloak claim company_id is missing; skipping B2B linking.');
}
if (b2bEnabled && profile.company_id) {
// eslint-disable-next-line no-console
console.info(`B2B enabled; attempting to link to company ${profile.company_id}`);
if (this.config.get<boolean>('LOG_KEYCLOAK_CLAIMS')) {
// eslint-disable-next-line no-console
console.info('Keycloak roles (mapped)', { roles: profile.roles ?? [] });
}
try {
await this.shopify.ensureB2bAssignment({
customerId,
email: profile.email,
firstName: profile.first_name,
lastName: profile.last_name,
companyIdOrExternalId: profile.company_id,
companyLocationId: profile.company_location_id,
keycloakRoles: profile.roles,
});
} catch (err) {
// Non-fatal: still allow D2C Multipass login if B2B mapping fails.
// eslint-disable-next-line no-console
console.warn('B2B assignment failed; continuing with Multipass login.', err);
}
}
const token = this.multipass.createMultipassToken({
email: profile.email,
first_name: profile.first_name,
last_name: profile.last_name,
identifier: profile.external_id,
return_to: state.return_to ?? (profile.company_id ? '/account' : undefined),
});
const redirectUrl = this.shopify.buildMultipassRedirectUrl(token);
return res.redirect(redirectUrl);
}
private normalizeReturnTo(returnTo?: string): string | undefined {
if (!returnTo) return undefined;
// allow relative paths only
if (returnTo.startsWith('/') && !returnTo.startsWith('//')) return returnTo;
return undefined;
}
}