Files
tehran_tala/apps/api/src/modules/customer-auth/customer-jwt.strategy.ts
T

132 lines
4.7 KiB
TypeScript
Raw Normal View History

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { AdminRole } from '@prisma/client';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PrismaService } from '../../prisma/prisma.service';
import { customerJwtSecret } from './customer-auth.service';
import {
CUSTOMER_TOKEN_TYPE,
EXPERT_TOKEN_TYPE,
type AppJwtPayload,
type AuthenticatedCustomer,
} from './app-principal';
2026-09-25 19:04:48 +03:30
import { AppSessionService } from '../sessions/app-session.service';
export {
CUSTOMER_TOKEN_TYPE,
EXPERT_TOKEN_TYPE,
type AuthenticatedCustomer,
} from './app-principal';
/** Kept for the existing imports; the payload now covers both principals. */
export type CustomerJwtPayload = AppJwtPayload;
/**
* The app principal, kept entirely separate from JwtStrategy (which reads
* AdminUser for the admin PANEL). The reference had the same split: `auth:api`
* for admins and `auth('api-customer')` for the app.
*
* This strategy accepts TWO token types and resolves both to one shape:
*
* typ 'customer' -> the Customer row named by `sub`
* typ 'sales-expert' -> the AdminUser named by `sub`, plus their trading
* Customer row, which becomes `id`
*
* Because `id` is always a Customer id, every route already written for a
* customer works unchanged for an expert — which is exactly the reference's
* behaviour, where an agent was a customer with extra screens.
*
* An expert's app token is NOT an admin-panel token: its `typ` is
* 'sales-expert', while JwtStrategy requires the admin type, so it is refused
* on every admin route.
*/
@Injectable()
export class CustomerJwtStrategy extends PassportStrategy(Strategy, 'customer-jwt') {
2026-09-25 19:04:48 +03:30
constructor(
private readonly prisma: PrismaService,
private readonly sessions: AppSessionService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: customerJwtSecret(),
});
}
async validate(payload: AppJwtPayload): Promise<AuthenticatedCustomer> {
2026-09-25 19:04:48 +03:30
// نشست باطل‌شده (خروج سراسری از IdP یا ابطال محلی). در حالت خاموشِ ردیابی،
// بدون هیچ کوئری true برمی‌گردد ⇒ هزینه و رفتار دقیقاً مثل قبل.
if (!(await this.sessions.isActive(payload?.jti))) {
throw new UnauthorizedException();
}
if (payload?.typ === EXPERT_TOKEN_TYPE) {
return this.validateExpert(payload);
}
// A token that is not explicitly a customer token is refused, whatever
// else it carries.
if (payload?.typ !== CUSTOMER_TOKEN_TYPE) {
throw new UnauthorizedException();
}
// Re-read on every request, so deactivating or archiving a customer takes
// effect immediately instead of at token expiry.
const customer = await this.prisma.customer.findUnique({
where: { id: payload.sub },
select: { id: true, mobile: true, isActive: true, isArchived: true },
});
if (!customer || !customer.isActive || customer.isArchived) {
throw new UnauthorizedException();
}
return { id: customer.id, mobile: customer.mobile, type: CUSTOMER_TOKEN_TYPE };
}
/**
* A sales expert. `sub` is their AdminUser id, so the role and active flag
* are re-checked on every request: demoting an expert or deactivating them
* locks them out immediately rather than at token expiry.
*
* The trading Customer row is resolved here rather than stored in the token,
* so a token issued before that row existed keeps working.
*/
private async validateExpert(payload: AppJwtPayload): Promise<AuthenticatedCustomer> {
const admin = await this.prisma.adminUser.findUnique({
where: { id: payload.sub },
select: {
id: true,
mobile: true,
role: true,
isActive: true,
salesExpertProfile: {
select: {
tradingCustomerId: true,
tradingCustomer: { select: { id: true, isActive: true, isArchived: true } },
},
},
},
});
if (!admin || !admin.isActive || admin.role !== AdminRole.SALES_EXPERT) {
throw new UnauthorizedException();
}
const trading = admin.salesExpertProfile?.tradingCustomer;
if (!trading || !trading.isActive || trading.isArchived) {
// The account is created at sign-in; if it is missing or has been
// deactivated, the session cannot act. Refused rather than silently
// degraded, so an expert never places an order against nothing.
throw new UnauthorizedException();
}
return {
id: trading.id,
mobile: admin.mobile,
type: EXPERT_TOKEN_TYPE,
adminUserId: admin.id,
};
}
}