Integrate Oudelaa backend features, security, tests, and deployment updates

This commit is contained in:
boutmoun123
2026-07-26 16:58:53 +03:00
parent 1b24ca4294
commit 2fd5322ef7
178 changed files with 19068 additions and 2592 deletions
@@ -0,0 +1,120 @@
import { UnauthorizedException } from '@nestjs/common';
import { GoogleStrategy } from './google.strategy';
import { JwtRefreshStrategy } from './jwt-refresh.strategy';
import { JwtStrategy } from './jwt.strategy';
import { SuperAdminJwtStrategy } from './super-admin-jwt.strategy';
const config = (values: Record<string, string> = {}) => ({
get: jest.fn((key: string) => values[key] ?? 'test-secret'),
});
describe('authentication strategies', () => {
describe('JwtStrategy', () => {
it('accepts access tokens only for active users', async () => {
const repository = { findById: jest.fn().mockResolvedValue({ isDisabled: false }) };
const strategy = new JwtStrategy(
config({ 'jwt.accessSecret': 'access-secret' }) as any,
repository as any,
);
const payload = { sub: 'user-1', tokenType: 'access' } as any;
await expect(strategy.validate(payload)).resolves.toBe(payload);
expect(repository.findById).toHaveBeenCalledWith('user-1');
});
it('rejects wrong token purpose before database access', async () => {
const repository = { findById: jest.fn() };
const strategy = new JwtStrategy(config() as any, repository as any);
await expect(
strategy.validate({ sub: 'user-1', tokenType: 'refresh' } as any),
).rejects.toBeInstanceOf(UnauthorizedException);
expect(repository.findById).not.toHaveBeenCalled();
});
it.each([null, { isDisabled: true }])('rejects unavailable account %#', async (user) => {
const strategy = new JwtStrategy(
config() as any,
{ findById: jest.fn().mockResolvedValue(user) } as any,
);
await expect(
strategy.validate({ sub: 'user-1', tokenType: 'access' } as any),
).rejects.toBeInstanceOf(UnauthorizedException);
});
});
describe('purpose-bound stateless strategies', () => {
it('accepts only user refresh tokens in JwtRefreshStrategy', () => {
const strategy = new JwtRefreshStrategy(config() as any);
const payload = { sub: 'user-1', tokenType: 'refresh' } as any;
expect(strategy.validate(payload)).toBe(payload);
expect(() => strategy.validate({ tokenType: 'access' } as any)).toThrow(
UnauthorizedException,
);
});
it('accepts only superadmin access tokens', () => {
const strategy = new SuperAdminJwtStrategy(config() as any);
const payload = { sub: 'superadmin', tokenType: 'superadmin_access' } as any;
expect(strategy.validate(payload)).toBe(payload);
expect(() => strategy.validate({ tokenType: 'access' } as any)).toThrow(
UnauthorizedException,
);
});
});
describe('GoogleStrategy', () => {
it('normalizes profile data before authentication', () => {
const strategy = new GoogleStrategy(config() as any);
const done = jest.fn();
strategy.validate(
'access',
'refresh',
{
id: 'google-1',
displayName: 'Google Artist',
emails: [{ value: 'ARTIST@EXAMPLE.COM', verified: true }],
photos: [{ value: 'https://example.com/avatar.jpg' }],
} as any,
done,
);
expect(done).toHaveBeenCalledWith(null, {
googleId: 'google-1',
email: 'artist@example.com',
name: 'Google Artist',
avatar: 'https://example.com/avatar.jpg',
});
});
it('rejects profiles without an email', () => {
const strategy = new GoogleStrategy(config() as any);
const done = jest.fn();
strategy.validate('access', 'refresh', { id: 'google-1' } as any, done);
expect(done).toHaveBeenCalledWith(expect.any(UnauthorizedException));
});
it('provides a safe fallback display name', () => {
const strategy = new GoogleStrategy(config() as any);
const done = jest.fn();
strategy.validate(
'access',
'refresh',
{ id: 'google-1', emails: [{ value: 'artist@example.com' }] } as any,
done,
);
expect(done).toHaveBeenCalledWith(
null,
expect.objectContaining({ name: 'Google User', avatar: undefined }),
);
});
});
});
@@ -1,7 +1,6 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { Request } from 'express';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { JwtPayload } from '../../../common/interfaces/jwt-payload.interface';
@@ -11,12 +10,11 @@ export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh'
super({
jwtFromRequest: ExtractJwt.fromBodyField('refreshToken'),
ignoreExpiration: false,
secretOrKey: configService.get<string>('jwt.refreshSecret', { infer: true }),
passReqToCallback: true,
secretOrKey: configService.get<string>('jwt.refreshSecret', { infer: true }) ?? '',
});
}
validate(_: Request, payload: JwtPayload): JwtPayload {
validate(payload: JwtPayload): JwtPayload {
if (payload.tokenType !== 'refresh') {
throw new UnauthorizedException('Invalid token type');
}
+8 -3
View File
@@ -3,22 +3,27 @@ import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { JwtPayload } from '../../../common/interfaces/jwt-payload.interface';
import { UsersRepository } from '../../users/users.repository';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(configService: ConfigService) {
constructor(configService: ConfigService, private readonly usersRepository: UsersRepository) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('jwt.accessSecret', { infer: true }),
secretOrKey: configService.get<string>('jwt.accessSecret', { infer: true }) ?? '',
});
}
validate(payload: JwtPayload): JwtPayload {
async validate(payload: JwtPayload): Promise<JwtPayload> {
if (payload.tokenType !== 'access') {
throw new UnauthorizedException('Invalid token type');
}
const user = await this.usersRepository.findById(payload.sub);
if (!user || user.isDisabled) {
throw new UnauthorizedException('Account is unavailable');
}
return payload;
}
}
@@ -10,7 +10,7 @@ export class SuperAdminJwtStrategy extends PassportStrategy(Strategy, 'superadmi
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('superAdmin.accessSecret', { infer: true }),
secretOrKey: configService.get<string>('superAdmin.accessSecret', { infer: true }) ?? '',
});
}