update
This commit is contained in:
143
OTP.md
Normal file
143
OTP.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# OTP API Documentation
|
||||
|
||||
## Overview
|
||||
The OTP system handles two main flows:
|
||||
1. **Login OTP**: Passwordless login using phone number
|
||||
2. **Verification OTP**: Phone number verification during registration
|
||||
|
||||
Base URL: `/server/auth`
|
||||
|
||||
## Login OTP Flow
|
||||
|
||||
### 1. Generate Login OTP
|
||||
**Endpoint:** `POST /auth/send-otp`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"phone": "1234567890"
|
||||
}
|
||||
```
|
||||
|
||||
**Success Response (200):**
|
||||
```json
|
||||
{
|
||||
"message": "OTP sent successfully"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `400 Bad Request`: Phone number is required
|
||||
- `400 Bad Request`: Admin users should use regular login, not OTP
|
||||
- `400 Bad Request`: Please verify your phone number first (for unverified users)
|
||||
|
||||
### 2. Verify Login OTP
|
||||
**Endpoint:** `POST /auth/verify-otp`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"phone": "1234567890",
|
||||
"otp": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
**Success Response (200):**
|
||||
```json
|
||||
{
|
||||
"message": "OTP verified successfully"
|
||||
}
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Sets JWT token in HTTP-only cookie
|
||||
- Returns different user roles (USER, VIEWER, etc.)
|
||||
- If user exists: logs in with actual user data
|
||||
- If user doesn't exist: creates VIEWER session
|
||||
|
||||
**Error Responses:**
|
||||
- `400 Bad Request`: Phone number and OTP are required
|
||||
- `400 Bad Request`: Invalid or expired OTP
|
||||
|
||||
## Phone Verification Flow
|
||||
|
||||
### 1. Send Verification OTP
|
||||
**Endpoint:** `POST /auth/send-verification-otp`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"phone": "1234567890"
|
||||
}
|
||||
```
|
||||
|
||||
**Success Response (200):**
|
||||
```json
|
||||
{
|
||||
"message": "Verification OTP sent to your phone"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `400 Bad Request`: Phone number is required
|
||||
- `400 Bad Request`: User not found
|
||||
- `400 Bad Request`: Phone number is already verified
|
||||
|
||||
### 2. Verify Account
|
||||
**Endpoint:** `POST /auth/verify-account`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"phone": "1234567890",
|
||||
"otp": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
**Success Response (200):**
|
||||
```json
|
||||
{
|
||||
"message": "Phone number verified successfully",
|
||||
"user": {
|
||||
"id": "user-uuid",
|
||||
"name": "User Name",
|
||||
"phone_number": "1234567890",
|
||||
"role": "USER",
|
||||
"phone_verified": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `400 Bad Request`: Phone number and OTP are required
|
||||
- `400 Bad Request`: Invalid or expired verification OTP
|
||||
|
||||
## Important Notes
|
||||
|
||||
### OTP Specifications
|
||||
- **Length**: 6 digits
|
||||
- **Login OTP Expiry**: 5 minutes
|
||||
- **Verification OTP Expiry**: 10 minutes
|
||||
- **Format**: Numeric only (100000-999999)
|
||||
|
||||
### Security Features
|
||||
- Only one active OTP per phone/purpose at a time
|
||||
- Previous OTPs are automatically deactivated when new ones are generated
|
||||
- OTPs are single-use (marked as used after verification)
|
||||
- Expired OTPs are automatically rejected
|
||||
|
||||
### User Roles & Permissions
|
||||
- **SUPER_ADMIN, EDITOR, REVIEWER, VIEWER**: Cannot use login OTP (must use regular login)
|
||||
- **USER**: Can use login OTP only after phone verification
|
||||
- **Unregistered numbers**: Get VIEWER role after OTP login
|
||||
|
||||
### SMS Integration
|
||||
- Uses SMS service for actual delivery
|
||||
- Falls back to console logging if SMS fails
|
||||
- Returns success even if SMS delivery fails (for development)
|
||||
|
||||
### Cookie Management
|
||||
- JWT tokens stored in HTTP-only cookies
|
||||
- Cookie settings: `httpOnly: true, sameSite: 'strict', secure: true`
|
||||
- Automatic cookie setting on successful OTP verification
|
||||
- **Phone verification automatically updates JWT cookie** with new `phone_verified: true` status
|
||||
@@ -39,7 +39,7 @@ export class AuthController {
|
||||
path: '/',
|
||||
});
|
||||
|
||||
return { response: 'ok', user };
|
||||
return { response: 'ok', user, };
|
||||
}
|
||||
|
||||
@Post('logout')
|
||||
@@ -117,49 +117,36 @@ export class AuthController {
|
||||
|
||||
@Public()
|
||||
@Post('send-verification-otp')
|
||||
async sendVerificationOtp(
|
||||
@Body('emailOrPhone') emailOrPhone: string,
|
||||
@Body('type') type: 'email' | 'phone',
|
||||
) {
|
||||
if (!emailOrPhone || !type) {
|
||||
throw new BadRequestException('Email/Phone and type are required');
|
||||
async sendVerificationOtp(@Body('phone') phone: string) {
|
||||
if (!phone) {
|
||||
throw new BadRequestException('Phone number is required');
|
||||
}
|
||||
|
||||
if (type !== 'email' && type !== 'phone') {
|
||||
throw new BadRequestException('Type must be either "email" or "phone"');
|
||||
}
|
||||
|
||||
return this.authService.sendVerificationOtp(emailOrPhone, type);
|
||||
|
||||
return this.authService.sendVerificationOtp(phone);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('verify-account')
|
||||
async verifyAccount(
|
||||
@Body('emailOrPhone') emailOrPhone: string,
|
||||
@Body('otp') otp: string,
|
||||
@Body('type') type: 'email' | 'phone',
|
||||
) {
|
||||
if (!emailOrPhone || !otp || !type) {
|
||||
throw new BadRequestException('Email/Phone, OTP, and type are required');
|
||||
}
|
||||
|
||||
if (type !== 'email' && type !== 'phone') {
|
||||
throw new BadRequestException('Type must be either "email" or "phone"');
|
||||
}
|
||||
|
||||
return this.authService.verifyAccount(emailOrPhone, otp, type);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('auto-verify-phone')
|
||||
async autoVerifyPhone(
|
||||
@Body('phone') phone: string,
|
||||
@Body('otp') otp: string,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
) {
|
||||
if (!phone || !otp) {
|
||||
throw new BadRequestException('Phone number and OTP are required');
|
||||
}
|
||||
|
||||
return this.authService.autoVerifyPhone(phone, otp);
|
||||
|
||||
const result = await this.authService.verifyAccount(phone, otp);
|
||||
|
||||
if (result.token) {
|
||||
res.cookie('token', result.token, {
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
secure: true,
|
||||
path: '/',
|
||||
});
|
||||
}
|
||||
|
||||
return { message: result.message, user: result.user };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,15 +28,11 @@ export class AuthService {
|
||||
const user =
|
||||
await this.userService.findByPhoneOrEmailWithPassword(emailOrPhone);
|
||||
if (user && (await bcrypt.compare(password, user.password))) {
|
||||
// Check if user account is verified (only for USER role)
|
||||
if (user.role === 'USER') {
|
||||
if (!user.phone_verified) {
|
||||
throw new UnauthorizedException('Please verify your phone number.');
|
||||
}
|
||||
// if (!user.email_verified) {
|
||||
// throw new UnauthorizedException('Please verify your email address.');
|
||||
// }
|
||||
}
|
||||
// if (user.role === 'USER') {
|
||||
// if (!user.phone_verified) {
|
||||
// throw new UnauthorizedException('Please verify your phone number.');
|
||||
// }
|
||||
// }
|
||||
|
||||
const { password, ...result } = user;
|
||||
return result;
|
||||
@@ -45,17 +41,15 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async validateViewer(phone: string) {
|
||||
// For temporary viewer users, do not check DB, just accept any phone and return a minimal user-like object
|
||||
return { id: null, name: null, phone_number: phone, role: 'VIEWER' };
|
||||
}
|
||||
|
||||
async login(user: any): Promise<string> {
|
||||
// If user is a temporary viewer (no id), issue a token with minimal payload
|
||||
if (user.role === 'VIEWER' && !user.id) {
|
||||
const payload = { sub: null, role: 'VIEWER', phone: user.phone_number };
|
||||
return this.jwtService.sign(payload);
|
||||
}
|
||||
const payload = { name: user.name, sub: user.id, role: user.role };
|
||||
const payload = { name: user.name, sub: user.id, role: user.role, phone_verified: user.phone_verified };
|
||||
return this.jwtService.sign(payload);
|
||||
}
|
||||
|
||||
@@ -64,30 +58,24 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async generateOtp(phone: string): Promise<{ message: string }> {
|
||||
// Check if user exists and their role
|
||||
const user = await this.userService.findByPhone(phone);
|
||||
|
||||
// If user is admin, reject OTP request
|
||||
if (
|
||||
user &&
|
||||
(user.role === 'SUPER_ADMIN' ||
|
||||
user.role === 'EDITOR' ||
|
||||
user.role === 'REVIEWER' ||
|
||||
user.role === 'VIEWER')
|
||||
user.role === 'REVIEWER')
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
'Admin users should use regular login, not OTP',
|
||||
);
|
||||
}
|
||||
|
||||
// If user exists and has an account, they should verify first or use password login
|
||||
if (user && user.role === 'USER') {
|
||||
if (!user.phone_verified) {
|
||||
throw new BadRequestException(
|
||||
'Please verify your phone number first.',
|
||||
);
|
||||
}
|
||||
}
|
||||
// if (user && user.role === 'USER') {
|
||||
// if (!user.phone_verified) {
|
||||
// throw new BadRequestException('Please verify your phone number first.');
|
||||
// }
|
||||
// }
|
||||
|
||||
// Deactivate any existing active LOGIN OTPs for this phone number
|
||||
await this.otpRepository.update(
|
||||
@@ -157,6 +145,7 @@ export class AuthService {
|
||||
name: otp.user.name,
|
||||
phone_number: otp.user.phone_number,
|
||||
role: otp.user.role,
|
||||
phone_verified: otp.user.phone_verified,
|
||||
};
|
||||
} else {
|
||||
user = { id: null, name: null, phone_number: phone, role: 'VIEWER' };
|
||||
@@ -167,81 +156,59 @@ export class AuthService {
|
||||
return { message: 'OTP verified successfully', token };
|
||||
}
|
||||
|
||||
async sendVerificationOtp(
|
||||
emailOrPhone: string,
|
||||
type: 'email' | 'phone',
|
||||
): Promise<{ message: string }> {
|
||||
const user =
|
||||
await this.userService.findByPhoneOrEmailWithPassword(emailOrPhone);
|
||||
async sendVerificationOtp(phone: string): Promise<{ message: string }> {
|
||||
const user = await this.userService.findByPhone(phone);
|
||||
|
||||
if (!user) {
|
||||
throw new BadRequestException('User not found');
|
||||
}
|
||||
|
||||
if (type === 'phone' && user.phone_verified) {
|
||||
if (user.phone_verified) {
|
||||
throw new BadRequestException('Phone number is already verified');
|
||||
}
|
||||
|
||||
if (type === 'email' && user.email_verified) {
|
||||
throw new BadRequestException('Email is already verified');
|
||||
}
|
||||
|
||||
const purpose =
|
||||
type === 'email' ? 'EMAIL_VERIFICATION' : 'PHONE_VERIFICATION';
|
||||
const contact = type === 'email' ? user.email : user.phone_number;
|
||||
|
||||
// Deactivate any existing verification OTPs
|
||||
await this.otpRepository.update(
|
||||
{ phone_number: contact, purpose, isActive: true },
|
||||
{ phone_number: phone, purpose: 'PHONE_VERIFICATION', isActive: true },
|
||||
{ isActive: false },
|
||||
);
|
||||
|
||||
const otpCode = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setMinutes(expiresAt.getMinutes() + 10); // 10 minutes for verification
|
||||
expiresAt.setMinutes(expiresAt.getMinutes() + 10);
|
||||
|
||||
const otp = this.otpRepository.create({
|
||||
phone_number: contact,
|
||||
phone_number: phone,
|
||||
otp_code: otpCode,
|
||||
expires_at: expiresAt,
|
||||
is_verified: false,
|
||||
isActive: true,
|
||||
purpose,
|
||||
purpose: 'PHONE_VERIFICATION',
|
||||
user,
|
||||
});
|
||||
|
||||
await this.otpRepository.save(otp);
|
||||
|
||||
if (type === 'phone') {
|
||||
const smsSent = await this.smsService.sendOtp(contact, otpCode);
|
||||
if (!smsSent) {
|
||||
console.log(`SMS failed, Verification OTP for ${contact}: ${otpCode}`);
|
||||
return {
|
||||
message:
|
||||
'Verification OTP generated but SMS delivery may have failed',
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// For email, log to console (you can implement email service later)
|
||||
console.log(`Email verification OTP for ${contact}: ${otpCode}`);
|
||||
const smsSent = await this.smsService.sendOtp(phone, otpCode);
|
||||
if (!smsSent) {
|
||||
console.log(`SMS failed, Verification OTP for ${phone}: ${otpCode}`);
|
||||
return {
|
||||
message: 'Verification OTP generated but SMS delivery may have failed',
|
||||
};
|
||||
}
|
||||
|
||||
return { message: `Verification OTP sent to your ${type}` };
|
||||
return { message: 'Verification OTP sent to your phone' };
|
||||
}
|
||||
|
||||
async verifyAccount(
|
||||
emailOrPhone: string,
|
||||
phone: string,
|
||||
otpCode: string,
|
||||
type: 'email' | 'phone',
|
||||
): Promise<{ message: string }> {
|
||||
const purpose =
|
||||
type === 'email' ? 'EMAIL_VERIFICATION' : 'PHONE_VERIFICATION';
|
||||
|
||||
): Promise<{ message: string; user?: any; token?: string }> {
|
||||
const otp = await this.otpRepository.findOne({
|
||||
where: {
|
||||
phone_number: emailOrPhone,
|
||||
phone_number: phone,
|
||||
otp_code: otpCode,
|
||||
purpose,
|
||||
purpose: 'PHONE_VERIFICATION',
|
||||
isActive: true,
|
||||
is_verified: false,
|
||||
expires_at: MoreThan(new Date()),
|
||||
@@ -260,64 +227,24 @@ export class AuthService {
|
||||
});
|
||||
|
||||
// Update user verification status
|
||||
const updateData =
|
||||
type === 'email' ? { email_verified: true } : { phone_verified: true };
|
||||
await this.userService.updateUser(otp.user.id, { phone_verified: true });
|
||||
|
||||
await this.userService.updateUser(otp.user.id, updateData);
|
||||
|
||||
return {
|
||||
message: `${type === 'email' ? 'Email' : 'Phone number'} verified successfully`,
|
||||
// Get updated user data and generate new token
|
||||
const updatedUser = await this.userService.findOneById(otp.user.id);
|
||||
const userData = {
|
||||
id: updatedUser.id,
|
||||
name: updatedUser.name,
|
||||
phone_number: updatedUser.phone_number,
|
||||
role: updatedUser.role,
|
||||
phone_verified: true,
|
||||
};
|
||||
}
|
||||
|
||||
async autoVerifyPhone(phone: string, otpCode: string): Promise<{ message: string; user?: any }> {
|
||||
const user = await this.userService.findByPhone(phone);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('No account found with this phone number');
|
||||
}
|
||||
|
||||
if (user.phone_verified) {
|
||||
throw new BadRequestException('Phone number already verified');
|
||||
}
|
||||
|
||||
// Find the OTP
|
||||
const otp = await this.otpRepository.findOne({
|
||||
where: {
|
||||
phone_number: phone,
|
||||
otp_code: otpCode,
|
||||
purpose: 'PHONE_VERIFICATION',
|
||||
isActive: true,
|
||||
is_verified: false,
|
||||
expires_at: MoreThan(new Date()),
|
||||
},
|
||||
relations: ['user'],
|
||||
});
|
||||
|
||||
if (!otp) {
|
||||
throw new BadRequestException('Invalid or expired OTP');
|
||||
}
|
||||
|
||||
// Mark OTP as used
|
||||
await this.otpRepository.update(otp.id, {
|
||||
is_verified: true,
|
||||
isActive: false,
|
||||
});
|
||||
|
||||
// Update user phone verification status
|
||||
await this.userService.updateUser(user.id, { phone_verified: true });
|
||||
const token = await this.login(userData);
|
||||
|
||||
return {
|
||||
message: 'Phone number verified successfully',
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
phone_number: user.phone_number,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
phone_verified: true,
|
||||
email_verified: user.email_verified,
|
||||
}
|
||||
user: userData,
|
||||
token,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export class CreateUserAndCustomerDto {
|
||||
city: string;
|
||||
@IsNotEmpty()
|
||||
city_id: string;
|
||||
@IsNotEmpty()
|
||||
@IsOptional()
|
||||
proof: string; // image id
|
||||
@IsNotEmpty()
|
||||
gender: GENDER;
|
||||
|
||||
@@ -31,6 +31,7 @@ export class Customer extends BaseEntity {
|
||||
@OneToOne(() => Image, (image) => image.customer, {
|
||||
eager: true,
|
||||
cascade: true,
|
||||
nullable:true,
|
||||
})
|
||||
@JoinColumn()
|
||||
proof: Image;
|
||||
|
||||
@@ -56,10 +56,14 @@ export class UserService {
|
||||
// Optionally create admin/customer profile if specified by role
|
||||
if (user.role === Role.USER) {
|
||||
await this.customerRepo.save(this.customerRepo.create({ user: newUser }));
|
||||
|
||||
|
||||
// Send verification instructions (don't send actual OTPs during registration)
|
||||
console.log(`User registered: ${newUser.email} / ${newUser.phone_number}`);
|
||||
console.log('Please use /auth/send-verification-otp to verify your account');
|
||||
console.log(
|
||||
`User registered: ${newUser.email} / ${newUser.phone_number}`,
|
||||
);
|
||||
console.log(
|
||||
'Please use /auth/send-verification-otp to verify your account',
|
||||
);
|
||||
} else {
|
||||
await this.adminRepo.save(this.adminRepo.create({ user: newUser }));
|
||||
}
|
||||
@@ -74,9 +78,9 @@ export class UserService {
|
||||
if (await this.userRepo.findOne({ where: { email: dto.email } }))
|
||||
throw new ConflictException('Email already registered');
|
||||
|
||||
if (!(await this.imageService.exists(dto.proof))) {
|
||||
throw new BadRequestException('Proof image does not exist');
|
||||
}
|
||||
// if (!(await this.imageService.exists(dto.proof))) {
|
||||
// throw new BadRequestException('Proof image does not exist');
|
||||
// }
|
||||
|
||||
await this.userRepo.manager.transaction(async (manager) => {
|
||||
// Create user
|
||||
@@ -90,9 +94,12 @@ export class UserService {
|
||||
role: Role.USER,
|
||||
isActive: true,
|
||||
});
|
||||
console.log('user', user);
|
||||
const savedUser = await manager.save(User, user);
|
||||
|
||||
const proof = await this.imageService.confirmImage(dto.proof);
|
||||
let proof;
|
||||
if (dto.proof) {
|
||||
proof = await this.imageService.confirmImage(dto.proof);
|
||||
}
|
||||
|
||||
// Create customer and link image
|
||||
const customer = manager.create(Customer, {
|
||||
@@ -104,10 +111,9 @@ export class UserService {
|
||||
city_id: dto.city_id,
|
||||
gender: dto.gender,
|
||||
user: savedUser,
|
||||
proof,
|
||||
proof: proof ?? undefined,
|
||||
});
|
||||
await manager.save(Customer, customer);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user