Context
Authentication is the gateway to all protected features. This issue implements user registration and login using username/password credentials. On registration, a Stellar wallet is automatically generated in the background. JWT access + refresh token strategy is used.
📄 Reference docs:
backend/docs/standards/security.md — password hashing, JWT RS256, token flow
backend/docs/standards/dtos.md — DTO validation
backend/docs/standards/endpoints.md — route conventions
backend/docs/standards/error-handling.md — error codes
Objective
Implement the AuthModule with:
POST /api/v1/auth/register — create user + auto-generate Stellar wallet
POST /api/v1/auth/login — validate credentials, issue access + refresh tokens
What to implement
DTOs
src/modules/auth/dto/register.dto.ts
export class RegisterDto {
@IsString() @IsNotEmpty() @MaxLength(50) @Matches(/^[a-z0-9_]+$/) username: string;
@IsString() @IsNotEmpty() @MaxLength(100) firstName: string;
@IsString() @IsNotEmpty() @MaxLength(100) lastName: string;
@IsEmail() email: string;
@IsString() @MinLength(8) @MaxLength(72) password: string;
@IsString() @IsNotEmpty() confirmPassword: string; // validated in service
}
src/modules/auth/dto/login.dto.ts
export class LoginDto {
@IsString() @IsNotEmpty() username: string;
@IsString() @IsNotEmpty() password: string;
}
src/modules/auth/dto/auth-tokens.dto.ts
export class AuthTokensDto {
accessToken: string;
refreshToken: string;
}
AuthService — src/modules/auth/auth.service.ts
register(dto: RegisterDto): Promise
- Validate
dto.password === dto.confirmPassword → throw AppException('PASSWORDS_DO_NOT_MATCH', ..., 400) if not
- Check username uniqueness →
AppException('USERNAME_TAKEN', ..., 409)
- Check email uniqueness →
AppException('EMAIL_TAKEN', ..., 409)
- Hash password:
bcrypt.hash(dto.password, 12)
- Insert user into
users table via UsersRepository
- Call
WalletsService.generateAndSave(userId) — fire and forget (do not await, use .catch(logger.error))
- Return void (HTTP 201, no tokens — user must login separately)
login(dto: LoginDto): Promise
- Find user by username →
AppException('INVALID_CREDENTIALS', ..., 401) if not found (do not distinguish between "user not found" and "wrong password")
bcrypt.compare(dto.password, user.passwordHash) → same AppException('INVALID_CREDENTIALS', ..., 401) if mismatch
- Generate access token: RS256 JWT, payload
{ sub: user.id, username: user.username }, TTL 15 min
- Generate refresh token:
crypto.randomBytes(64).toString('hex')
- Hash refresh token with
bcrypt.hash(rawRefreshToken, 10) and store in refresh_tokens
- Return
{ accessToken, refreshToken } (raw values — not hashed)
AuthController — src/modules/auth/auth.controller.ts
@ApiTags('auth')
@Controller('auth')
export class AuthController {
@Post('register')
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Register a new user' })
@ApiResponse({ status: 201, description: 'User created successfully' })
@ApiResponse({ status: 409, description: 'USERNAME_TAKEN or EMAIL_TAKEN' })
@ApiResponse({ status: 400, description: 'VALIDATION_ERROR or PASSWORDS_DO_NOT_MATCH' })
register(@Body() dto: RegisterDto): Promise<void> { ... }
@Post('login')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Login and receive JWT tokens' })
@ApiResponse({ status: 200, type: AuthTokensDto })
@ApiResponse({ status: 401, description: 'INVALID_CREDENTIALS' })
login(@Body() dto: LoginDto): Promise<AuthTokensDto> { ... }
}
UsersRepository — src/modules/users/users.repository.ts
Methods needed for this issue:
findByUsername(username: string): Promise<User | null>
findByEmail(email: string): Promise<User | null>
create(payload): Promise<User> — inserts into users table
- Snake_case to camelCase mapping (see
backend/docs/standards/database.md)
AuthRepository — src/modules/auth/auth.repository.ts
Methods:
saveRefreshToken({ userId, tokenHash, expiresAt }): Promise<void>
findRefreshToken(tokenHash: string): Promise<RefreshToken | null>
revokeRefreshToken(id: string): Promise<void>
Packages to install
npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt
npm install -D @types/bcrypt @types/passport-jwt
Swagger documentation required
Every endpoint must have:
@ApiTags('auth')
@ApiOperation({ summary: '...' })
@ApiBody({ type: RegisterDto | LoginDto })
@ApiResponse for every possible HTTP status (200/201, 400, 401, 409)
- DTOs decorated with
@ApiProperty() on every field with example values
Example:
@ApiProperty({ example: 'juan_perez', description: 'Unique username, lowercase alphanumeric and underscores' })
username: string;
Acceptance Criteria
Unit Tests required
src/modules/auth/auth.service.spec.ts
register: throws PASSWORDS_DO_NOT_MATCH when passwords differ
register: throws USERNAME_TAKEN when usersRepository.findByUsername returns a user
register: throws EMAIL_TAKEN when usersRepository.findByEmail returns a user
register: calls bcrypt.hash with cost 12 before saving
register: calls walletsService.generateAndSave fire-and-forget after user creation
login: throws INVALID_CREDENTIALS when user not found
login: throws INVALID_CREDENTIALS when bcrypt.compare returns false
login: returns { accessToken, refreshToken } on valid credentials
login: access token payload contains { sub: user.id, username: user.username }
Complexity: High
Depends on: setup issue, database/migrations issues
Context
Authentication is the gateway to all protected features. This issue implements user registration and login using username/password credentials. On registration, a Stellar wallet is automatically generated in the background. JWT access + refresh token strategy is used.
Objective
Implement the
AuthModulewith:POST /api/v1/auth/register— create user + auto-generate Stellar walletPOST /api/v1/auth/login— validate credentials, issue access + refresh tokensWhat to implement
DTOs
src/modules/auth/dto/register.dto.tssrc/modules/auth/dto/login.dto.tssrc/modules/auth/dto/auth-tokens.dto.tsAuthService —
src/modules/auth/auth.service.tsregister(dto: RegisterDto): Promise
dto.password === dto.confirmPassword→ throwAppException('PASSWORDS_DO_NOT_MATCH', ..., 400)if notAppException('USERNAME_TAKEN', ..., 409)AppException('EMAIL_TAKEN', ..., 409)bcrypt.hash(dto.password, 12)userstable viaUsersRepositoryWalletsService.generateAndSave(userId)— fire and forget (do not await, use.catch(logger.error))login(dto: LoginDto): Promise
AppException('INVALID_CREDENTIALS', ..., 401)if not found (do not distinguish between "user not found" and "wrong password")bcrypt.compare(dto.password, user.passwordHash)→ sameAppException('INVALID_CREDENTIALS', ..., 401)if mismatch{ sub: user.id, username: user.username }, TTL 15 mincrypto.randomBytes(64).toString('hex')bcrypt.hash(rawRefreshToken, 10)and store inrefresh_tokens{ accessToken, refreshToken }(raw values — not hashed)AuthController —
src/modules/auth/auth.controller.tsUsersRepository —
src/modules/users/users.repository.tsMethods needed for this issue:
findByUsername(username: string): Promise<User | null>findByEmail(email: string): Promise<User | null>create(payload): Promise<User>— inserts intouserstablebackend/docs/standards/database.md)AuthRepository —
src/modules/auth/auth.repository.tsMethods:
saveRefreshToken({ userId, tokenHash, expiresAt }): Promise<void>findRefreshToken(tokenHash: string): Promise<RefreshToken | null>revokeRefreshToken(id: string): Promise<void>Packages to install
Swagger documentation required
Every endpoint must have:
@ApiTags('auth')@ApiOperation({ summary: '...' })@ApiBody({ type: RegisterDto | LoginDto })@ApiResponsefor every possible HTTP status (200/201, 400, 401, 409)@ApiProperty()on every field withexamplevaluesExample:
Acceptance Criteria
POST /api/v1/auth/registerwith valid data returns 201POST /api/v1/auth/registerwith duplicate username returns 409 with codeUSERNAME_TAKENPOST /api/v1/auth/registerwith mismatched passwords returns 400 with codePASSWORDS_DO_NOT_MATCHPOST /api/v1/auth/loginwith valid credentials returns{ accessToken, refreshToken }POST /api/v1/auth/loginwith wrong password returns 401 with codeINVALID_CREDENTIALSPOST /api/v1/auth/loginwith unknown username returns 401 with codeINVALID_CREDENTIALS(same error — no user enumeration)users.password_hashstarts with$2b$authtagUnit Tests required
src/modules/auth/auth.service.spec.tsregister: throwsPASSWORDS_DO_NOT_MATCHwhen passwords differregister: throwsUSERNAME_TAKENwhenusersRepository.findByUsernamereturns a userregister: throwsEMAIL_TAKENwhenusersRepository.findByEmailreturns a userregister: callsbcrypt.hashwith cost 12 before savingregister: callswalletsService.generateAndSavefire-and-forget after user creationlogin: throwsINVALID_CREDENTIALSwhen user not foundlogin: throwsINVALID_CREDENTIALSwhen bcrypt.compare returns falselogin: returns{ accessToken, refreshToken }on valid credentialslogin: access token payload contains{ sub: user.id, username: user.username }Complexity: High
Depends on: setup issue, database/migrations issues