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
67 changes: 36 additions & 31 deletions src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule } from '../config/config.module';
import { UsersModule } from '../users/users.module';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { WalletAuthService } from './services/wallet-auth.service';
import { WalletAuthController } from './controllers/wallet-auth.controller';
import { WalletAuthGuard } from './guards/wallet-auth.guard';
import { ConfigService } from '../config/config.service';
import { RedisModule } from '../common/module/redis/redis.module';

@Module({
imports: [
ConfigModule,
UsersModule,
RedisModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.jwtSecret,
signOptions: { expiresIn: '1h' },
}),
inject: [ConfigService],
}),
],
controllers: [AuthController, WalletAuthController],
providers: [AuthService, WalletAuthService, WalletAuthGuard],
exports: [AuthService, WalletAuthService, WalletAuthGuard],
})
export class AuthModule {}

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule } from '../config/config.module';
import { UsersModule } from '../users/users.module';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { WalletAuthService } from './services/wallet-auth.service';
import { WalletAuthController } from './controllers/wallet-auth.controller';
import { WalletAuthGuard } from './guards/wallet-auth.guard';
import { ConfigService } from '../config/config.service';
import { RedisModule } from '../common/module/redis/redis.module';
import { TokenAuthStrategy } from './strategies/token-auth.strategy';
import { TokenAuthController } from './controllers/token-auth.controller';
import { BlockchainModule } from '../blockchain/blockchain.module';

@Module({
imports: [
ConfigModule,
UsersModule,
RedisModule,
BlockchainModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.jwtSecret,
signOptions: { expiresIn: '1h' },
}),
inject: [ConfigService],
}),
],
controllers: [AuthController, WalletAuthController, TokenAuthController],
providers: [AuthService, WalletAuthService, WalletAuthGuard, TokenAuthStrategy],
exports: [AuthService, WalletAuthService, WalletAuthGuard],
})
export class AuthModule {}
56 changes: 56 additions & 0 deletions src/auth/controllers/token-auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Controller, Post, Body, UseGuards, Get, Request } from '@nestjs/common';
import { TokenAuthDto } from '../dto/token-auth.dto';
import { TokenAuthGuard } from '../guards/token-auth.guard';
import { BlockchainService } from '../../blockchain/services/blockchain.service';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';

@Controller('auth/token')
export class TokenAuthController {
constructor(private readonly blockchainService: BlockchainService) {}

@Post('login')
@UseGuards(TokenAuthGuard)
async login(@Request() req, @Body() tokenAuthDto: TokenAuthDto) {
// The user is already validated by the TokenAuthGuard
const user = req.user;

// Get token balance for the user
const tokenBalance = await this.blockchainService.getTokenBalance(user.walletAddress);

// Get voting power for the user
const votingPower = await this.blockchainService.getVotingPower(user.walletAddress);

return {
user: {
id: user.id,
username: user.username,
walletAddress: user.walletAddress,
},
tokenBalance,
votingPower,
// Note: JWT token would be provided by the standard auth service
};
}

@Get('profile')
@UseGuards(JwtAuthGuard)
async getProfile(@Request() req) {
const user = req.user;

// Get token balance for the user
const tokenBalance = await this.blockchainService.getTokenBalance(user.walletAddress);

// Get voting power for the user
const votingPower = await this.blockchainService.getVotingPower(user.walletAddress);

return {
user: {
id: user.id,
username: user.username,
walletAddress: user.walletAddress,
},
tokenBalance,
votingPower,
};
}
}
15 changes: 15 additions & 0 deletions src/auth/dto/token-auth.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { IsNotEmpty, IsString } from 'class-validator';

export class TokenAuthDto {
@IsNotEmpty()
@IsString()
walletAddress: string;

@IsNotEmpty()
@IsString()
signature: string;

@IsNotEmpty()
@IsString()
message: string;
}
5 changes: 5 additions & 0 deletions src/auth/guards/token-auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class TokenAuthGuard extends AuthGuard('token-auth') {}
247 changes: 247 additions & 0 deletions src/auth/strategies/token-auth.strategy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UnauthorizedException, BadRequestException } from '@nestjs/common';
import { TokenAuthStrategy } from './token-auth.strategy';
import { BlockchainService } from '../../blockchain/services/blockchain.service';
import { UsersService } from '../../users/users.service';
import { ConfigService } from '@nestjs/config';

describe('TokenAuthStrategy', () => {
let strategy: TokenAuthStrategy;
let blockchainService: BlockchainService;
let usersService: UsersService;
let configService: ConfigService;

const mockBlockchainService = {
verifySignature: jest.fn(),
getTokenBalance: jest.fn(),
};

const mockUsersService = {
findByWalletAddress: jest.fn(),
createWithWalletAddress: jest.fn(),
updateLastLogin: jest.fn(),
};

const mockConfigService = {
get: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
TokenAuthStrategy,
{
provide: BlockchainService,
useValue: mockBlockchainService,
},
{
provide: UsersService,
useValue: mockUsersService,
},
{
provide: ConfigService,
useValue: mockConfigService,
},
],
}).compile();

strategy = module.get<TokenAuthStrategy>(TokenAuthStrategy);
blockchainService = module.get<BlockchainService>(BlockchainService);
usersService = module.get<UsersService>(UsersService);
configService = module.get<ConfigService>(ConfigService);

// Default config values
mockConfigService.get.mockImplementation((key) => {
const config = {
AUTH_MIN_TOKEN_BALANCE: 1,
AUTH_MESSAGE_PREFIX: 'StarkPulse Authentication:',
AUTH_MESSAGE_EXPIRATION: 300,
};
return config[key];
});
});

afterEach(() => {
jest.clearAllMocks();
});

it('should be defined', () => {
expect(strategy).toBeDefined();
});

describe('validate', () => {
const mockRequest = {
body: {
walletAddress: '0x1234567890abcdef',
signature: '0xsignature',
message: 'StarkPulse Authentication:1634567890:randomnonce',
},
};

const mockUser = {
id: 'user-id',
walletAddress: '0x1234567890abcdef',
status: 'ACTIVE',
};

beforeEach(() => {
// Mock Date.now to return a fixed timestamp for testing
jest.spyOn(Date, 'now').mockImplementation(() => 1634567990000); // 100 seconds after the message timestamp
});

it('should authenticate a user with valid credentials', async () => {
mockBlockchainService.verifySignature.mockResolvedValue(true);
mockUsersService.findByWalletAddress.mockResolvedValue(mockUser);
mockBlockchainService.getTokenBalance.mockResolvedValue(10);

const result = await strategy.validate(mockRequest as any);

expect(result).toEqual(mockUser);
expect(mockBlockchainService.verifySignature).toHaveBeenCalledWith(
mockRequest.body.walletAddress,
mockRequest.body.message,
mockRequest.body.signature,
);
expect(mockUsersService.updateLastLogin).toHaveBeenCalledWith(mockUser.id);
});

it('should create a new user if not found', async () => {
mockBlockchainService.verifySignature.mockResolvedValue(true);
mockUsersService.findByWalletAddress.mockResolvedValue(null);
mockUsersService.createWithWalletAddress.mockResolvedValue(mockUser);
mockBlockchainService.getTokenBalance.mockResolvedValue(10);

const result = await strategy.validate(mockRequest as any);

expect(result).toEqual(mockUser);
expect(mockUsersService.createWithWalletAddress).toHaveBeenCalledWith(
mockRequest.body.walletAddress,
);
});

it('should throw UnauthorizedException if missing credentials', async () => {
const invalidRequest = {
body: {
walletAddress: '0x1234567890abcdef',
// Missing signature and message
},
};

await expect(strategy.validate(invalidRequest as any)).rejects.toThrow(
UnauthorizedException,
);
});

it('should throw UnauthorizedException if signature is invalid', async () => {
mockBlockchainService.verifySignature.mockResolvedValue(false);

await expect(strategy.validate(mockRequest as any)).rejects.toThrow(
UnauthorizedException,
);
});

it('should throw UnauthorizedException if token balance is insufficient', async () => {
mockBlockchainService.verifySignature.mockResolvedValue(true);
mockUsersService.findByWalletAddress.mockResolvedValue(mockUser);
mockBlockchainService.getTokenBalance.mockResolvedValue(0);

await expect(strategy.validate(mockRequest as any)).rejects.toThrow(
UnauthorizedException,
);
expect(mockBlockchainService.getTokenBalance).toHaveBeenCalledWith(
mockRequest.body.walletAddress,
);
});

it('should throw UnauthorizedException if user is banned', async () => {
const bannedUser = { ...mockUser, status: 'BANNED' };
mockBlockchainService.verifySignature.mockResolvedValue(true);
mockUsersService.findByWalletAddress.mockResolvedValue(bannedUser);
mockBlockchainService.getTokenBalance.mockResolvedValue(10);

await expect(strategy.validate(mockRequest as any)).rejects.toThrow(
UnauthorizedException,
);
});

it('should throw BadRequestException if message format is invalid', async () => {
const invalidMessageRequest = {
body: {
walletAddress: '0x1234567890abcdef',
signature: '0xsignature',
message: 'InvalidPrefix:1634567890:randomnonce',
},
};

await expect(strategy.validate(invalidMessageRequest as any)).rejects.toThrow(
BadRequestException,
);
});

it('should throw BadRequestException if message has expired', async () => {
// Mock Date.now to return a timestamp far in the future
jest.spyOn(Date, 'now').mockImplementation(() => 1634567890000 + 600000); // 600 seconds after the message timestamp

await expect(strategy.validate(mockRequest as any)).rejects.toThrow(
BadRequestException,
);
});

it('should throw BadRequestException if timestamp is invalid', async () => {
const invalidTimestampRequest = {
body: {
walletAddress: '0x1234567890abcdef',
signature: '0xsignature',
message: 'StarkPulse Authentication:invalid:randomnonce',
},
};

await expect(strategy.validate(invalidTimestampRequest as any)).rejects.toThrow(
BadRequestException,
);
});

it('should use configurable minimum token balance', async () => {
mockConfigService.get.mockImplementation((key) => {
const config = {
AUTH_MIN_TOKEN_BALANCE: 5,
AUTH_MESSAGE_PREFIX: 'StarkPulse Authentication:',
AUTH_MESSAGE_EXPIRATION: 300,
};
return config[key];
});

// Re-initialize strategy to use new config
const module: TestingModule = await Test.createTestingModule({
providers: [
TokenAuthStrategy,
{
provide: BlockchainService,
useValue: mockBlockchainService,
},
{
provide: UsersService,
useValue: mockUsersService,
},
{
provide: ConfigService,
useValue: mockConfigService,
},
],
}).compile();

const configuredStrategy = module.get<TokenAuthStrategy>(TokenAuthStrategy);

mockBlockchainService.verifySignature.mockResolvedValue(true);
mockUsersService.findByWalletAddress.mockResolvedValue(mockUser);
mockBlockchainService.getTokenBalance.mockResolvedValue(3);

await expect(configuredStrategy.validate(mockRequest as any)).rejects.toThrow(
UnauthorizedException,
);
expect(mockBlockchainService.getTokenBalance).toHaveBeenCalledWith(
mockRequest.body.walletAddress,
);
});
});
});
Loading
Loading