Initial commit: Hedena Gold monorepo.
First commit of the Hedena Gold product (separate from gold-bank): branding, package names, and deploy defaults aligned to the new brand; legacy Henza trees stay local-only via .gitignore.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import { envOr } from '../../common/env';
|
||||
import { AdminSessionService } from '../sessions/admin-session.service';
|
||||
import { expandWithParents } from './acl.constants';
|
||||
import { withSuperAdminRids } from './super-admin';
|
||||
|
||||
export interface JwtPayload {
|
||||
sub: string;
|
||||
mobile: string;
|
||||
/**
|
||||
* Present only on tokens issued while session tracking is on. It identifies the
|
||||
* AdminSession row, which is what makes revoking a token before it expires
|
||||
* possible. A token without it predates tracking and stays valid until it
|
||||
* expires, so switching the feature on does not sign everyone out.
|
||||
*/
|
||||
jti?: string;
|
||||
/**
|
||||
* Set on APP tokens only ('customer' or 'sales-expert'). An admin-panel
|
||||
* token has no `typ`. Checked in validate() below — see the note there.
|
||||
*/
|
||||
typ?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* App token types, which must NEVER authenticate an admin-panel route.
|
||||
*
|
||||
* This matters because a sales expert's app token carries their AdminUser id
|
||||
* in `sub` (D21). The two token families are signed with different secrets, so
|
||||
* normally the signature alone stops it — but `JWT_CUSTOMER_SECRET` falls back
|
||||
* to `JWT_SECRET` when unset, and in that configuration an expert's app token
|
||||
* WOULD verify here and be handed full admin-panel permissions. This check
|
||||
* closes that regardless of how the secrets are configured.
|
||||
*/
|
||||
const APP_TOKEN_TYPES = new Set(['customer', 'sales-expert']);
|
||||
|
||||
/**
|
||||
* The authenticated principal attached to `req.user`.
|
||||
*
|
||||
* `permissions` is already expanded with ancestors, so guards can test a rid with
|
||||
* a plain `includes()` — the same shape the legacy frontend held in its Vuex store
|
||||
* after checkParent().
|
||||
*/
|
||||
export interface AuthenticatedAdmin {
|
||||
id: string;
|
||||
fullName: string;
|
||||
mobile: string;
|
||||
role: string;
|
||||
userTypeId: number | null;
|
||||
branchId: string | null;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly sessions: AdminSessionService,
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: envOr('JWT_SECRET', 'dev-only-insecure-secret'),
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: JwtPayload): Promise<AuthenticatedAdmin> {
|
||||
// An app token is never an admin-panel token, whatever it is signed with.
|
||||
if (payload?.typ && APP_TOKEN_TYPES.has(payload.typ)) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
// Revoked centrally (IAM back-channel logout) or locally? While tracking is
|
||||
// off this returns true without touching the database, so the request costs
|
||||
// exactly what it did before.
|
||||
if (!(await this.sessions.isActive(payload.jti))) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
const user = await this.prisma.adminUser.findUnique({
|
||||
where: { id: payload.sub },
|
||||
include: { userType: { include: { permissions: true } } },
|
||||
});
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
// Permissions are re-read on every request rather than baked into the token,
|
||||
// so a permission change takes effect immediately instead of at next login.
|
||||
const rids = withSuperAdminRids(user.mobile, user.userType?.permissions.map((p) => p.rid) ?? []);
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
fullName: user.fullName,
|
||||
mobile: user.mobile,
|
||||
role: user.role,
|
||||
userTypeId: user.userTypeId,
|
||||
branchId: user.branchId,
|
||||
permissions: expandWithParents(rids),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user