add : UserAPI
This commit is contained in:
115
docs/users-api.md
Normal file
115
docs/users-api.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Users API
|
||||
|
||||
Base URL: `/api`
|
||||
|
||||
All responses follow the standard response wrapper.
|
||||
|
||||
These endpoints require:
|
||||
|
||||
- Bearer token
|
||||
- Authenticated user
|
||||
|
||||
## Current Scope
|
||||
|
||||
This first phase covers:
|
||||
|
||||
- current user profile
|
||||
- dashboard summary
|
||||
- wallet summary
|
||||
- wallet transaction list
|
||||
- active session list and revoke
|
||||
|
||||
Orders, addresses, favorites, and support tickets are not implemented yet in this phase.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### `GET /api/users/me`
|
||||
|
||||
Returns current user profile:
|
||||
|
||||
- `id`
|
||||
- `phone`
|
||||
- `username`
|
||||
- `fullName`
|
||||
- `isVerified`
|
||||
- `role`
|
||||
- `wallet`
|
||||
- `loyaltyProfile`
|
||||
- `createdAt`
|
||||
- `updatedAt`
|
||||
|
||||
### `PATCH /api/users/me`
|
||||
|
||||
Updates current user profile.
|
||||
|
||||
Request body:
|
||||
|
||||
```json
|
||||
{
|
||||
"fullName": "Ali Rezaei",
|
||||
"username": "alirezaei"
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `username` must be unique
|
||||
- `username` allows letters, numbers, underscore, dot, and hyphen
|
||||
|
||||
### `GET /api/users/me/dashboard`
|
||||
|
||||
Returns dashboard summary for the authenticated user.
|
||||
|
||||
Response includes:
|
||||
|
||||
- `profile`
|
||||
- `stats.walletBalance`
|
||||
- `stats.walletTransactionsCount`
|
||||
- `stats.activeSessionsCount`
|
||||
- `stats.ordersCount`
|
||||
- `stats.favoritesCount`
|
||||
- `stats.ticketsCount`
|
||||
- `loyalty`
|
||||
|
||||
Current placeholder values:
|
||||
|
||||
- `ordersCount = 0`
|
||||
- `favoritesCount = 0`
|
||||
- `ticketsCount = 0`
|
||||
|
||||
These fields are placeholders until those modules are implemented.
|
||||
|
||||
### `GET /api/users/me/wallet`
|
||||
|
||||
Returns:
|
||||
|
||||
- wallet balance
|
||||
- transaction count
|
||||
- latest 10 wallet transactions
|
||||
|
||||
### `GET /api/users/me/wallet/transactions`
|
||||
|
||||
Query params:
|
||||
|
||||
- `page`
|
||||
- `limit`
|
||||
- `type`: `deposit | withdraw`
|
||||
|
||||
### `GET /api/users/me/sessions`
|
||||
|
||||
Returns active sessions for the current user.
|
||||
|
||||
Each item includes:
|
||||
|
||||
- `id`
|
||||
- `deviceInfo`
|
||||
- `ipAddress`
|
||||
- `expiresAt`
|
||||
- `createdAt`
|
||||
- `updatedAt`
|
||||
|
||||
Session metadata is captured during register, login, OTP verify, and refresh.
|
||||
|
||||
### `DELETE /api/users/me/sessions/:sessionId`
|
||||
|
||||
Revokes one active session for the current user.
|
||||
@@ -37,29 +37,32 @@ export class AuthController {
|
||||
@Post('register/password')
|
||||
@ApiOperation({ summary: 'Register with mobile, username, and password' })
|
||||
@ApiBody({ type: RegisterPasswordDto })
|
||||
registerWithPassword(@Body() dto: RegisterPasswordDto) {
|
||||
return this.authService.registerWithPassword(dto);
|
||||
registerWithPassword(
|
||||
@Body() dto: RegisterPasswordDto,
|
||||
@Req() request: Request,
|
||||
) {
|
||||
return this.authService.registerWithPassword(dto, request);
|
||||
}
|
||||
|
||||
@Post('login/password')
|
||||
@ApiOperation({ summary: 'Login with username and password' })
|
||||
@ApiBody({ type: LoginPasswordDto })
|
||||
loginWithPassword(@Body() dto: LoginPasswordDto) {
|
||||
return this.authService.loginWithPassword(dto);
|
||||
loginWithPassword(@Body() dto: LoginPasswordDto, @Req() request: Request) {
|
||||
return this.authService.loginWithPassword(dto, request);
|
||||
}
|
||||
|
||||
@Post('otp/verify')
|
||||
@ApiOperation({ summary: 'Verify OTP code for mobile auth flow' })
|
||||
@ApiBody({ type: VerifyOtpDto })
|
||||
verifyOtp(@Body() dto: VerifyOtpDto) {
|
||||
return this.authService.verifyOtp(dto.phone, dto.otp);
|
||||
verifyOtp(@Body() dto: VerifyOtpDto, @Req() request: Request) {
|
||||
return this.authService.verifyOtp(dto.phone, dto.otp, request);
|
||||
}
|
||||
|
||||
@Post('refresh')
|
||||
@ApiOperation({ summary: 'Refresh JWT access token' })
|
||||
@ApiBody({ type: RefreshTokenDto })
|
||||
refresh(@Body() dto: RefreshTokenDto) {
|
||||
return this.authService.refreshToken(dto.refreshToken);
|
||||
refresh(@Body() dto: RefreshTokenDto, @Req() request: Request) {
|
||||
return this.authService.refreshToken(dto.refreshToken, request);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ConfigService } from '@nestjs/config';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { Request } from 'express';
|
||||
import { StringValue } from 'ms';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { AuthOtp } from './entities/auth-otp.entity';
|
||||
@@ -59,7 +60,7 @@ export class AuthService {
|
||||
};
|
||||
}
|
||||
|
||||
async registerWithPassword(dto: RegisterPasswordDto) {
|
||||
async registerWithPassword(dto: RegisterPasswordDto, request?: Request) {
|
||||
const existingPhone = await this.usersService.findByPhone(dto.phone);
|
||||
if (existingPhone) {
|
||||
throw new BadRequestException('Phone already exists');
|
||||
@@ -79,12 +80,12 @@ export class AuthService {
|
||||
role: UserRole.USER,
|
||||
});
|
||||
const tokens = await this.issueTokens(savedUser);
|
||||
await this.storeRefreshToken(savedUser, tokens.refreshToken);
|
||||
await this.storeRefreshToken(savedUser, tokens.refreshToken, request);
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
async loginWithPassword(dto: LoginPasswordDto) {
|
||||
async loginWithPassword(dto: LoginPasswordDto, request?: Request) {
|
||||
const user = await this.usersService.findByUsername(dto.username);
|
||||
if (!user?.passwordHash) {
|
||||
throw new UnauthorizedException('Invalid username or password');
|
||||
@@ -96,12 +97,12 @@ export class AuthService {
|
||||
}
|
||||
|
||||
const tokens = await this.issueTokens(user);
|
||||
await this.storeRefreshToken(user, tokens.refreshToken);
|
||||
await this.storeRefreshToken(user, tokens.refreshToken, request);
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
async verifyOtp(phone: string, otp: string) {
|
||||
async verifyOtp(phone: string, otp: string, request?: Request) {
|
||||
const user = await this.usersService.findByPhone(phone);
|
||||
const otpRecord = await this.authOtpsRepository.findOne({
|
||||
where: { phone, purpose: 'login', usedAt: IsNull() },
|
||||
@@ -131,12 +132,12 @@ export class AuthService {
|
||||
]);
|
||||
|
||||
const tokens = await this.issueTokens(user);
|
||||
await this.storeRefreshToken(user, tokens.refreshToken);
|
||||
await this.storeRefreshToken(user, tokens.refreshToken, request);
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string) {
|
||||
async refreshToken(refreshToken: string, request?: Request) {
|
||||
const payload = await this.jwtService.verifyAsync<JwtPayload>(refreshToken, {
|
||||
secret: this.configService.getOrThrow<string>('jwt.secret'),
|
||||
});
|
||||
@@ -169,7 +170,7 @@ export class AuthService {
|
||||
await this.userSessionsRepository.save(validSession);
|
||||
|
||||
const tokens = await this.issueTokens(user);
|
||||
await this.storeRefreshToken(user, tokens.refreshToken);
|
||||
await this.storeRefreshToken(user, tokens.refreshToken, request);
|
||||
|
||||
return tokens;
|
||||
}
|
||||
@@ -227,12 +228,18 @@ export class AuthService {
|
||||
};
|
||||
}
|
||||
|
||||
private async storeRefreshToken(user: User, refreshToken: string) {
|
||||
private async storeRefreshToken(
|
||||
user: User,
|
||||
refreshToken: string,
|
||||
request?: Request,
|
||||
) {
|
||||
const refreshTtl = this.configService.getOrThrow<StringValue>('jwt.refreshTtl');
|
||||
const session = this.userSessionsRepository.create({
|
||||
user,
|
||||
refreshTokenHash: await bcrypt.hash(refreshToken, 10),
|
||||
expiresAt: new Date(Date.now() + this.parseDurationToMs(refreshTtl)),
|
||||
deviceInfo: this.resolveDeviceInfo(request),
|
||||
ipAddress: this.resolveIpAddress(request),
|
||||
});
|
||||
await this.userSessionsRepository.save(session);
|
||||
}
|
||||
@@ -301,4 +308,20 @@ export class AuthService {
|
||||
|
||||
return amount * unitMap[unit];
|
||||
}
|
||||
|
||||
private resolveDeviceInfo(request?: Request) {
|
||||
const userAgent = request?.get('user-agent')?.trim();
|
||||
|
||||
return userAgent ? userAgent.slice(0, 255) : null;
|
||||
}
|
||||
|
||||
private resolveIpAddress(request?: Request) {
|
||||
const forwardedFor = request?.headers['x-forwarded-for'];
|
||||
const forwardedIp = Array.isArray(forwardedFor)
|
||||
? forwardedFor[0]
|
||||
: forwardedFor?.split(',')[0];
|
||||
const ip = forwardedIp?.trim() || request?.ip || null;
|
||||
|
||||
return ip ? ip.slice(0, 64) : null;
|
||||
}
|
||||
}
|
||||
|
||||
25
src/modules/users/dto/list-wallet-transactions.dto.ts
Normal file
25
src/modules/users/dto/list-wallet-transactions.dto.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsIn, IsInt, IsOptional, IsString, Min } from 'class-validator';
|
||||
|
||||
export class ListWalletTransactionsDto {
|
||||
@ApiPropertyOptional({ example: 1, default: 1 })
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => Number(value))
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number = 1;
|
||||
|
||||
@ApiPropertyOptional({ example: 20, default: 20 })
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => Number(value))
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
limit?: number = 20;
|
||||
|
||||
@ApiPropertyOptional({ example: 'deposit' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsIn(['deposit', 'withdraw'])
|
||||
type?: string;
|
||||
}
|
||||
20
src/modules/users/dto/update-profile.dto.ts
Normal file
20
src/modules/users/dto/update-profile.dto.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsOptional, IsString, Length, Matches } from 'class-validator';
|
||||
|
||||
export class UpdateProfileDto {
|
||||
@ApiPropertyOptional({ example: 'Ali Rezaei' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Length(2, 150)
|
||||
fullName?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'alirezaei' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Length(3, 50)
|
||||
@Matches(/^[a-zA-Z0-9_.-]+$/, {
|
||||
message:
|
||||
'username can only contain letters, numbers, underscore, dot, and hyphen',
|
||||
})
|
||||
username?: string;
|
||||
}
|
||||
81
src/modules/users/users.controller.ts
Normal file
81
src/modules/users/users.controller.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Query,
|
||||
Req,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { JwtPayload } from '../auth/interfaces/jwt-payload.interface';
|
||||
import { ListWalletTransactionsDto } from './dto/list-wallet-transactions.dto';
|
||||
import { UpdateProfileDto } from './dto/update-profile.dto';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@ApiTags('Users')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Get('me')
|
||||
@ApiOperation({ summary: 'Get current authenticated user profile' })
|
||||
getMe(@Req() request: Request & { user: JwtPayload }) {
|
||||
return this.usersService.getProfile(request.user.sub);
|
||||
}
|
||||
|
||||
@Patch('me')
|
||||
@ApiOperation({ summary: 'Update current authenticated user profile' })
|
||||
updateMe(
|
||||
@Req() request: Request & { user: JwtPayload },
|
||||
@Body() dto: UpdateProfileDto,
|
||||
) {
|
||||
return this.usersService.updateProfile(request.user.sub, dto);
|
||||
}
|
||||
|
||||
@Get('me/dashboard')
|
||||
@ApiOperation({ summary: 'Get dashboard summary for current user' })
|
||||
getDashboard(@Req() request: Request & { user: JwtPayload }) {
|
||||
return this.usersService.getDashboard(request.user.sub);
|
||||
}
|
||||
|
||||
@Get('me/wallet')
|
||||
@ApiOperation({ summary: 'Get wallet summary for current user' })
|
||||
getWallet(@Req() request: Request & { user: JwtPayload }) {
|
||||
return this.usersService.getWalletSummary(request.user.sub);
|
||||
}
|
||||
|
||||
@Get('me/wallet/transactions')
|
||||
@ApiOperation({ summary: 'List wallet transactions for current user' })
|
||||
getWalletTransactions(
|
||||
@Req() request: Request & { user: JwtPayload },
|
||||
@Query() query: ListWalletTransactionsDto,
|
||||
) {
|
||||
return this.usersService.getWalletTransactions(request.user.sub, query);
|
||||
}
|
||||
|
||||
@Get('me/sessions')
|
||||
@ApiOperation({ summary: 'List active sessions for current user' })
|
||||
getSessions(@Req() request: Request & { user: JwtPayload }) {
|
||||
return this.usersService.getActiveSessions(request.user.sub);
|
||||
}
|
||||
|
||||
@Delete('me/sessions/:sessionId')
|
||||
@ApiOperation({ summary: 'Revoke one active session for current user' })
|
||||
revokeSession(
|
||||
@Req() request: Request & { user: JwtPayload },
|
||||
@Param('sessionId') sessionId: string,
|
||||
) {
|
||||
return this.usersService.revokeSession(request.user.sub, sessionId);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UserSession } from '../auth/entities/user-session.entity';
|
||||
import { LoyaltyProfile } from './entities/loyalty-profile.entity';
|
||||
import { User } from './entities/user.entity';
|
||||
import { UserLevelHistory } from './entities/user-level-history.entity';
|
||||
import { WalletTransaction } from './entities/wallet-transaction.entity';
|
||||
import { Wallet } from './entities/wallet.entity';
|
||||
import { UsersController } from './users.controller';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@Module({
|
||||
@@ -15,8 +17,10 @@ import { UsersService } from './users.service';
|
||||
WalletTransaction,
|
||||
LoyaltyProfile,
|
||||
UserLevelHistory,
|
||||
UserSession,
|
||||
]),
|
||||
],
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
})
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { UserSession } from '../auth/entities/user-session.entity';
|
||||
import { ListWalletTransactionsDto } from './dto/list-wallet-transactions.dto';
|
||||
import { UpdateProfileDto } from './dto/update-profile.dto';
|
||||
import { LoyaltyProfile } from './entities/loyalty-profile.entity';
|
||||
import { User } from './entities/user.entity';
|
||||
import { UserLevelHistory } from './entities/user-level-history.entity';
|
||||
import { WalletTransaction } from './entities/wallet-transaction.entity';
|
||||
import { Wallet } from './entities/wallet.entity';
|
||||
import { UserRole } from './enums/user-role.enum';
|
||||
import { UserLevel } from './enums/user-level.enum';
|
||||
@@ -19,6 +27,10 @@ export class UsersService {
|
||||
private readonly loyaltyProfilesRepository: Repository<LoyaltyProfile>,
|
||||
@InjectRepository(UserLevelHistory)
|
||||
private readonly userLevelHistoriesRepository: Repository<UserLevelHistory>,
|
||||
@InjectRepository(WalletTransaction)
|
||||
private readonly walletTransactionsRepository: Repository<WalletTransaction>,
|
||||
@InjectRepository(UserSession)
|
||||
private readonly userSessionsRepository: Repository<UserSession>,
|
||||
) {}
|
||||
|
||||
findByPhone(phone: string) {
|
||||
@@ -91,4 +103,222 @@ export class UsersService {
|
||||
async save(user: User) {
|
||||
return this.usersRepository.save(user);
|
||||
}
|
||||
|
||||
async getProfile(userId: string) {
|
||||
const user = await this.requireUser(userId);
|
||||
|
||||
return this.mapProfile(user);
|
||||
}
|
||||
|
||||
async updateProfile(userId: string, dto: UpdateProfileDto) {
|
||||
const user = await this.requireUser(userId);
|
||||
|
||||
if (dto.username && dto.username !== user.username) {
|
||||
const existingUser = await this.findByUsername(dto.username);
|
||||
if (existingUser && existingUser.id !== user.id) {
|
||||
throw new BadRequestException('Username already exists');
|
||||
}
|
||||
user.username = dto.username;
|
||||
}
|
||||
|
||||
if (dto.fullName) {
|
||||
user.fullName = dto.fullName;
|
||||
}
|
||||
|
||||
const savedUser = await this.usersRepository.save(user);
|
||||
|
||||
return this.mapProfile(savedUser);
|
||||
}
|
||||
|
||||
async getDashboard(userId: string) {
|
||||
const user = await this.requireUser(userId);
|
||||
const [activeSessionsCount, walletTransactionsCount, loyaltyHistory] =
|
||||
await Promise.all([
|
||||
this.userSessionsRepository.count({
|
||||
where: {
|
||||
user: { id: user.id },
|
||||
revokedAt: IsNull(),
|
||||
},
|
||||
}),
|
||||
this.walletTransactionsRepository.count({
|
||||
where: {
|
||||
wallet: { id: user.wallet.id },
|
||||
},
|
||||
}),
|
||||
this.userLevelHistoriesRepository.find({
|
||||
where: { loyaltyProfile: { id: user.loyaltyProfile.id } },
|
||||
order: { createdAt: 'DESC' },
|
||||
take: 5,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
profile: this.mapProfile(user),
|
||||
stats: {
|
||||
walletBalance: user.wallet.balance,
|
||||
walletTransactionsCount,
|
||||
activeSessionsCount,
|
||||
ordersCount: 0,
|
||||
favoritesCount: 0,
|
||||
ticketsCount: 0,
|
||||
},
|
||||
loyalty: {
|
||||
currentLevel: user.loyaltyProfile.currentLevel,
|
||||
totalSpent: user.loyaltyProfile.totalSpent,
|
||||
history: loyaltyHistory.map((item) => ({
|
||||
id: item.id,
|
||||
level: item.level,
|
||||
reason: item.reason,
|
||||
createdAt: item.createdAt,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getWalletSummary(userId: string) {
|
||||
const user = await this.requireUser(userId);
|
||||
const [recentTransactions, transactionsCount] = await Promise.all([
|
||||
this.walletTransactionsRepository.find({
|
||||
where: { wallet: { id: user.wallet.id } },
|
||||
order: { createdAt: 'DESC' },
|
||||
take: 10,
|
||||
}),
|
||||
this.walletTransactionsRepository.count({
|
||||
where: { wallet: { id: user.wallet.id } },
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
wallet: {
|
||||
id: user.wallet.id,
|
||||
balance: user.wallet.balance,
|
||||
transactionsCount,
|
||||
updatedAt: user.wallet.updatedAt,
|
||||
},
|
||||
recentTransactions: recentTransactions.map((item) =>
|
||||
this.mapWalletTransaction(item),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async getWalletTransactions(userId: string, query: ListWalletTransactionsDto) {
|
||||
const user = await this.requireUser(userId);
|
||||
const page = query.page ?? 1;
|
||||
const limit = query.limit ?? 20;
|
||||
|
||||
const qb = this.walletTransactionsRepository
|
||||
.createQueryBuilder('transaction')
|
||||
.innerJoin('transaction.wallet', 'wallet')
|
||||
.where('wallet.id = :walletId', { walletId: user.wallet.id })
|
||||
.orderBy('transaction.created_at', 'DESC')
|
||||
.skip((page - 1) * limit)
|
||||
.take(limit);
|
||||
|
||||
if (query.type) {
|
||||
qb.andWhere('transaction.type = :type', { type: query.type });
|
||||
}
|
||||
|
||||
const [items, total] = await qb.getManyAndCount();
|
||||
|
||||
return {
|
||||
items: items.map((item) => this.mapWalletTransaction(item)),
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit) || 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getActiveSessions(userId: string) {
|
||||
const user = await this.requireUser(userId);
|
||||
const sessions = await this.userSessionsRepository.find({
|
||||
where: {
|
||||
user: { id: user.id },
|
||||
revokedAt: IsNull(),
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
return {
|
||||
items: sessions.map((session) => ({
|
||||
id: session.id,
|
||||
deviceInfo: session.deviceInfo,
|
||||
ipAddress: session.ipAddress,
|
||||
expiresAt: session.expiresAt,
|
||||
createdAt: session.createdAt,
|
||||
updatedAt: session.updatedAt,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async revokeSession(userId: string, sessionId: string) {
|
||||
const session = await this.userSessionsRepository.findOne({
|
||||
where: {
|
||||
id: sessionId,
|
||||
user: { id: userId },
|
||||
revokedAt: IsNull(),
|
||||
},
|
||||
relations: { user: true },
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
|
||||
session.revokedAt = new Date();
|
||||
await this.userSessionsRepository.save(session);
|
||||
|
||||
return {
|
||||
message: 'Session revoked successfully',
|
||||
sessionId: session.id,
|
||||
};
|
||||
}
|
||||
|
||||
private async requireUser(userId: string) {
|
||||
const user = await this.findById(userId);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
private mapProfile(user: User) {
|
||||
return {
|
||||
id: user.id,
|
||||
phone: user.phone,
|
||||
username: user.username,
|
||||
fullName: user.fullName,
|
||||
isVerified: user.isVerified,
|
||||
role: user.role,
|
||||
wallet: user.wallet
|
||||
? {
|
||||
id: user.wallet.id,
|
||||
balance: user.wallet.balance,
|
||||
}
|
||||
: null,
|
||||
loyaltyProfile: user.loyaltyProfile
|
||||
? {
|
||||
id: user.loyaltyProfile.id,
|
||||
currentLevel: user.loyaltyProfile.currentLevel,
|
||||
totalSpent: user.loyaltyProfile.totalSpent,
|
||||
}
|
||||
: null,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
private mapWalletTransaction(transaction: WalletTransaction) {
|
||||
return {
|
||||
id: transaction.id,
|
||||
type: transaction.type,
|
||||
amount: transaction.amount,
|
||||
description: transaction.description,
|
||||
createdAt: transaction.createdAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user