Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { JwtPayload } from "jsonwebtoken";
import { AuthService } from "../../../auth/auth.service";
import AuthModule from '../modules/auth.module';

describe('AuthService.createTestingSessionTokens() test suite', () => {
let authService: AuthService;

beforeEach(async () => {
authService = await AuthModule.getAuthService();
});

it('Should return access and refresh tokens and expiration dates', async () => {
const payload = {
profile_id: '12345',
player_id: '12345',
tokenVersion: 0,
};

const result = await authService.createTestingSessionTokens(payload);

const accessToken = authService['jwtService'].decode(
result.accessToken,
) as JwtPayload;

const refreshToken = authService['jwtService'].decode(
result.refreshToken,
) as JwtPayload;

expect(accessToken.profile_id).toBe(payload.profile_id);
expect(accessToken.player_id).toBe(payload.player_id);

expect(refreshToken.profile_id).toBe(payload.profile_id);
expect(refreshToken.player_id).toBe(payload.player_id);
expect(refreshToken.tokenVersion).toBe(payload.tokenVersion);
expect(refreshToken.type).toBe('refresh');

expect(result.tokenExpires).toBe(accessToken.exp);
expect(result.refreshTokenExpires).toBe(refreshToken.exp);
});
});
38 changes: 22 additions & 16 deletions src/__tests__/auth/box/BoxAuthService/signIn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,17 @@ describe('BoxAuthService.signIn() test suite', () => {

await boxAuthService.signIn(validUsername, validPassword);

const expectedPayload = {
profile_id: existingAdminProfile._id.toString(),
player_id: createdPlayer._id.toString(),
box_id: existingBox._id.toString(),
groupAdmin: true,
};

expect(signAsyncSpy).toHaveBeenCalledWith(expectedPayload);
expect(signAsyncSpy).toHaveBeenCalledWith(
{
profile_id: existingAdminProfile._id.toString(),
player_id: createdPlayer._id.toString(),
box_id: existingBox._id.toString(),
groupAdmin: true,
tokenVersion: 0,
type: 'refresh',
},
{ expiresIn: '2d' },
);
});

it('Should call signAsync() with payload containing groupAdmin set to true if the profile is group admin', async () => {
Expand All @@ -144,14 +147,17 @@ describe('BoxAuthService.signIn() test suite', () => {

await boxAuthService.signIn(validUsername, validPassword);

const expectedPayload = {
profile_id: existingAdminProfile._id.toString(),
player_id: createdPlayer._id.toString(),
box_id: existingBox._id.toString(),
groupAdmin: true,
};

expect(signAsyncSpy).toHaveBeenCalledWith(expectedPayload);
expect(signAsyncSpy).toHaveBeenCalledWith(
{
profile_id: existingAdminProfile._id.toString(),
player_id: createdPlayer._id.toString(),
box_id: existingBox._id.toString(),
groupAdmin: true,
tokenVersion: 0,
type: 'refresh',
},
{ expiresIn: '2d' },
);
});

it('Should not return clan data if player is not in any clan', async () => {
Expand Down
2 changes: 2 additions & 0 deletions src/__tests__/auth/modules/authCommonModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ProfileSchema } from '../../../profile/profile.schema';
import { PlayerSchema } from '../../../player/schemas/player.schema';
import { ClanSchema } from '../../../clan/clan.schema';
import { BoxTestingSessionGuard } from '../../../box/auth/boxTestingSession.guard';
import { ProfileModule } from '../../../profile/profile.module';

export default class AuthCommonModule {
private constructor() {}
Expand All @@ -39,6 +40,7 @@ export default class AuthCommonModule {
{ name: ModelName.BOX, schema: BoxSchema },
{ name: ModelName.GROUP_ADMIN, schema: GroupAdminSchema },
]),
ProfileModule,
],
providers: [
AuthService,
Expand Down
39 changes: 22 additions & 17 deletions src/__tests__/box/auth/BoxAuthHandler/getGroupAdminToken.test.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,42 @@
import ProfileBuilderFactory from '../../../../__tests__/profile/data/profileBuilderFactory';
import BoxAuthHandler from '../../../../box/auth/BoxAuthHandler';
import BoxBuilderFactory from '../../data/boxBuilderFactory';
import BoxModule from '../../modules/box.module';
import { JwtService } from '@nestjs/jwt';
import ProfileModule from '../../../../__tests__/profile/modules/profile.module';

describe('BoxAuthHandler.getGroupAdminToken() test suite', () => {
let boxAuthHandler: BoxAuthHandler;
const boxUserBuilder = BoxBuilderFactory.getBuilder('BoxUser');
const boxAdminUser = boxUserBuilder.setGroupAdmin(true).build();

const profileModel = ProfileModule.getProfileModel();
const profileBuilder = ProfileBuilderFactory.getBuilder('Profile');
const profile = profileBuilder.set_id(boxAdminUser.profile_id).build();

beforeEach(async () => {
boxAuthHandler = await BoxModule.getBoxAuthHandler();
});

it('Should call the JWTService.signAsync() with correct params', async () => {
const jwtMethod = jest
.spyOn(JwtService.prototype, 'signAsync')
.mockResolvedValueOnce('token');

await boxAuthHandler.getGroupAdminToken(boxAdminUser);
it('Should return access and refresh tokens and expiration times', async () => {
await profileModel.create(profile);

const { clan_id: _clan_id, ...clanIdOmittedUser } = boxAdminUser;
const [result, errors] = await boxAuthHandler.getGroupAdminToken(boxAdminUser);

expect(jwtMethod).toHaveBeenCalledTimes(1);
expect(jwtMethod).toHaveBeenCalledWith(clanIdOmittedUser);
expect(errors).toBeNull();
expect(result).toEqual(
expect.objectContaining({
accessToken: expect.any(String),
tokenExpires: expect.any(Number),
refreshToken: expect.any(String),
refreshTokenExpires: expect.any(Number),
}),
);
});

it('Should return token from the JWTService.signAsync() response', async () => {
const jwtToken = 'returned-token';
jest
.spyOn(JwtService.prototype, 'signAsync')
.mockResolvedValueOnce(jwtToken);
it('Should return NOT_FOUND error when profile does not exist', async () => {
const [result, errors] = await boxAuthHandler.getGroupAdminToken(boxAdminUser);

const returnedToken = await boxAuthHandler.getGroupAdminToken(boxAdminUser);
expect(returnedToken).toBe(jwtToken);
expect(result).toBeNull();
expect(errors).toContainSE_NOT_FOUND();
});
});
2 changes: 2 additions & 0 deletions src/__tests__/box/modules/boxCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { envVars } from '../../../common/service/envHandler/envVars';
import UniqueFieldGenerator from '../../../box/util/UniqueFieldGenerator';
import { ItemSchema } from '../../../clanInventory/item/item.schema';
import { GameSchema } from '../../../gameData/game.schema';
import { AuthModule } from '../../../auth/auth.module';

export default class BoxCommonModule {
private constructor() {}
Expand Down Expand Up @@ -67,6 +68,7 @@ export default class BoxCommonModule {
JwtModule,
DailyTasksModule,
ChatModule,
AuthModule,
],
providers: [
BoxService,
Expand Down
6 changes: 4 additions & 2 deletions src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Module } from '@nestjs/common';
import { forwardRef, Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { RequestHelperModule } from '../requestHelper/requestHelper.module';
Expand All @@ -13,6 +13,7 @@ import { MongooseModule } from '@nestjs/mongoose';
import { PlayerSchema } from '../player/schemas/player.schema';
import { ProfileSchema } from '../profile/profile.schema';
import { ClanSchema } from '../clan/clan.schema';
import { ProfileModule } from '../profile/profile.module';

@Module({
imports: [
Expand All @@ -29,9 +30,10 @@ import { ClanSchema } from '../clan/clan.schema';
{ name: ModelName.BOX, schema: BoxSchema },
{ name: ModelName.GROUP_ADMIN, schema: GroupAdminSchema },
]),
forwardRef(() => ProfileModule),
],
providers: [AuthService, BoxAuthService, AuthServiceProvider],
controllers: [AuthController],
exports: [AuthService],
exports: [AuthService, BoxAuthService],
})
export class AuthModule {}
41 changes: 41 additions & 0 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import { InjectModel } from '@nestjs/mongoose';
import { Player } from '../player/schemas/player.schema';
import { Profile } from '../profile/profile.schema';
import { Clan } from '../clan/clan.schema';
import { envVars } from '../common/service/envHandler/envVars';
import { StringValue } from 'ms';
import { TokensDto } from './dto/tokens.dto';
import { TokenPayload } from './dto/tokenPayload.dto';

@Injectable()
export class AuthService {
Expand Down Expand Up @@ -174,4 +178,41 @@ export class AuthService {
];
}
}

/**
* Create Access and Refresh tokens
*
* Extracts the expiration time in Unix timestamp format
*
* @param payload User info used to create tokens
* @returns Access and Refresh tokens + expiration dates if successful
*/
public async createTestingSessionTokens(
payload: TokenPayload,
):Promise<TokensDto> {
const expiresIn = (envVars.JWT_EXPIRES ?? '30d') as StringValue;

const refreshToken = await this.jwtService.signAsync(
{...payload, type: 'refresh'},
{ expiresIn },
);
const decodedRefreshToken = this.jwtService.decode(refreshToken);
const refreshTokenExpires = decodedRefreshToken?.exp;

const { tokenVersion, ...accessPayload } = payload;

const accessToken = await this.jwtService.signAsync(
accessPayload,
{ expiresIn },
);
const decodedAccessToken = this.jwtService.decode(accessToken);
const tokenExpires = decodedAccessToken?.exp;

return {
accessToken,
tokenExpires,
refreshToken,
refreshTokenExpires,
};
}
}
9 changes: 3 additions & 6 deletions src/auth/box/BoxAuthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,10 @@ export default class BoxAuthService extends AuthService {
player_id: player?._id,
box_id,
groupAdmin,
tokenVersion: profile.tokenVersion ?? 0,
};

const accessToken = await this.jwt.signAsync(payload);
const decodedAccessToken: any = this.jwt.decode(accessToken);
// Extract the expiration time in Unix timestamp format
const tokenExpires = decodedAccessToken?.exp;
const tokens = await this.createTestingSessionTokens(payload);

profile['Player'] = player;

Expand All @@ -108,8 +106,7 @@ export default class BoxAuthService extends AuthService {

return {
...serializedProfile,
accessToken,
tokenExpires,
...tokens,
};
};
}
16 changes: 16 additions & 0 deletions src/auth/dto/signInResponse.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,22 @@ export class SignInResponseDto extends ProfileDto {
@Expose()
tokenExpires: number;

/**
* Refresh token to create new access token
*
* @example "eyJhbGciOiJIUzI1NiIsInR..."
*/
@Expose()
refreshToken: string;

/**
* Refresh token expiration date
*
* @example 1735689600
*/
@Expose()
refreshTokenExpires: number;

/**
* Whether the profile has configured a security question.
*
Expand Down
51 changes: 51 additions & 0 deletions src/auth/dto/tokenPayload.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Expose } from 'class-transformer';
import AddType from '../../common/base/decorator/AddType.decorator';

@AddType('TokenPayload')
export class TokenPayload {
/**
* Profile Id
*
* @example "67fe4e2d8a54d4cc39266a43"
*/
@Expose()
profile_id: string;

/**
* Player Id
*
* @example "67fe4e2d8a54d4cc39266a43"
*/
@Expose()
player_id: string;

/**
* Id of Box session
*/
@Expose()
box_id?: string;

/**
* Whether user is GroupAdmin or not
*
* @example false
*/
@Expose()
groupAdmin?: boolean;

/**
* Token version
*
* @example 0
*/
@Expose()
tokenVersion: number;

/**
* Clan Id
*
* @example "67fe4e2d8a54d4cc39266a43"
*/
@Expose()
clan_id?: string;
}
Loading
Loading