mirror of
https://git.meshkee.com/Meshkee/backend.git
synced 2026-08-11 22:30:59 +04:30
Add passwordless OTP login and SMS password reset.
Expose login-otp and reset-password so dashboards can finish forgot-password and one-time SMS sign-in, and sync website API docs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
f4295e780c
commit
08f901306c
+10
-4
@@ -1,7 +1,7 @@
|
|||||||
# Meshkee CMS API — Project Context
|
# Meshkee CMS API — Project Context
|
||||||
|
|
||||||
> Living reference for developers and AI assistants working on this codebase.
|
> Living reference for developers and AI assistants working on this codebase.
|
||||||
> Last updated: August 4, 2026
|
> Last updated: August 5, 2026
|
||||||
|
|
||||||
## What This Project Is
|
## What This Project Is
|
||||||
|
|
||||||
@@ -238,9 +238,11 @@ All routes are prefixed with `/api/v1`.
|
|||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| POST | `/auth/register` | Customer registration by domain |
|
| POST | `/auth/register` | Customer registration by domain |
|
||||||
| POST | `/auth/login` | Cell + password |
|
| POST | `/auth/login` | Cell + password |
|
||||||
|
| POST | `/auth/login-otp` | Passwordless login with SMS OTP |
|
||||||
|
| POST | `/auth/reset-password` | Reset password with SMS OTP |
|
||||||
| POST | `/auth/refresh` | Refresh token |
|
| POST | `/auth/refresh` | Refresh token |
|
||||||
| POST | `/auth/send-otp` | Send OTP (Redis-backed) |
|
| POST | `/auth/send-otp` | Send OTP (Redis-backed) |
|
||||||
| POST | `/auth/verify-otp` | Verify OTP |
|
| POST | `/auth/verify-otp` | Verify OTP (marks cell verified; no tokens) |
|
||||||
| GET | `/tenants/:host` | Resolve business from domain |
|
| GET | `/tenants/:host` | Resolve business from domain |
|
||||||
| GET | `/tenants/:host/store-specials` | Active store specials |
|
| GET | `/tenants/:host/store-specials` | Active store specials |
|
||||||
| GET | `/tenants/:host/website/category-groups` | Homepage category rows |
|
| GET | `/tenants/:host/website/category-groups` | Homepage category rows |
|
||||||
@@ -361,6 +363,10 @@ Each resource typically has: `read`, `create`, `update`, `delete` (+ `publish` f
|
|||||||
|
|
||||||
- Registration resolves tenant by `domain` → creates/links user → assigns `customer` role
|
- Registration resolves tenant by `domain` → creates/links user → assigns `customer` role
|
||||||
- OTP stored in Redis (`otp:{cellNumber}`), 5-min TTL; disabled when `SMS_ENABLED=false`
|
- OTP stored in Redis (`otp:{cellNumber}`), 5-min TTL; disabled when `SMS_ENABLED=false`
|
||||||
|
- `POST /auth/send-otp` → SMS code; `POST /auth/verify-otp` marks `cellVerifiedAt` (no tokens)
|
||||||
|
- `POST /auth/login-otp` → passwordless login (consumes OTP, verifies cell, returns tokens)
|
||||||
|
- `POST /auth/reset-password` → forgot password (OTP + `newPassword`, verifies cell)
|
||||||
|
- Password login (`POST /auth/login`) rejects unverified cells when SMS is enabled
|
||||||
- JWT payload: `sub`, `cellNumber`, `roles`, `dashboard`, `type`
|
- JWT payload: `sub`, `cellNumber`, `roles`, `dashboard`, `type`
|
||||||
- SMS provider: Gama (`sms.igama.ir`) SendQuick via service shortcode (`SMS_GAMA_*`)
|
- SMS provider: Gama (`sms.igama.ir`) SendQuick via service shortcode (`SMS_GAMA_*`)
|
||||||
- Partner gateway (external sites like Balout): `POST /api/v1/public/sms/send` with `X-Api-Key` + body `{ domain, to, message }`; partners configured in `SMS_PARTNERS` (`domain:apiKey` pairs). Rate limits: 30/partner/min and 5/destination/min. Not part of storefront website-api docs.
|
- Partner gateway (external sites like Balout): `POST /api/v1/public/sms/send` with `X-Api-Key` + body `{ domain, to, message }`; partners configured in `SMS_PARTNERS` (`domain:apiKey` pairs). Rate limits: 30/partner/min and 5/destination/min. Not part of storefront website-api docs.
|
||||||
@@ -552,7 +558,7 @@ See `.env.example` for the full list. Key groups:
|
|||||||
|
|
||||||
### Implemented
|
### Implemented
|
||||||
|
|
||||||
- Multi-tenant auth (register, login, OTP, profile)
|
- Multi-tenant auth (register, login, passwordless OTP login, reset password via SMS, profile)
|
||||||
- Super admin: users, businesses, domains, system business categories
|
- Super admin: users, businesses, domains, system business categories
|
||||||
- Super admin: selective migrate-from-old + purge-data (portfolio categories + portfolios; oversized images resized to max 1280×1280; purge removes portfolios + images)
|
- Super admin: selective migrate-from-old + purge-data (portfolio categories + portfolios; oversized images resized to max 1280×1280; purge removes portfolios + images)
|
||||||
- Business team management
|
- Business team management
|
||||||
@@ -566,6 +572,7 @@ See `.env.example` for the full list. Key groups:
|
|||||||
- Category variations & technical forms
|
- Category variations & technical forms
|
||||||
- Tenant resolution by domain
|
- Tenant resolution by domain
|
||||||
- RBAC with granular permissions
|
- RBAC with granular permissions
|
||||||
|
- Auth SMS OTP via Gama SendQuick (`send-otp`, `verify-otp`, `login-otp`, `reset-password`)
|
||||||
- Partner SMS gateway (`POST /public/sms/send`) + Gama SendQuick integration
|
- Partner SMS gateway (`POST /public/sms/send`) + Gama SendQuick integration
|
||||||
|
|
||||||
### Planned / partial
|
### Planned / partial
|
||||||
@@ -575,7 +582,6 @@ See `.env.example` for the full list. Key groups:
|
|||||||
| Blogs | Yes | Yes | No | No model |
|
| Blogs | Yes | Yes | No | No model |
|
||||||
| Portfolios | Yes | Yes | Partial (migrate-from-old) | Yes |
|
| Portfolios | Yes | Yes | Partial (migrate-from-old) | Yes |
|
||||||
| Customer dashboard | Partial | No | Register only | Yes |
|
| Customer dashboard | Partial | No | Register only | Yes |
|
||||||
| SMS provider | — | — | Stub | — |
|
|
||||||
| Store checkout (cart, orders) | Yes | Yes | Yes | Yes |
|
| Store checkout (cart, orders) | Yes | Yes | Yes | Yes |
|
||||||
| Customer favorites | — | `favorites.*` seeded | No | No |
|
| Customer favorites | — | `favorites.*` seeded | No | No |
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ You are building a **Meshkee business website (storefront)**. You must use the M
|
|||||||
1. `GET /tenants/{domain}` → branding + `businessId`
|
1. `GET /tenants/{domain}` → branding + `businessId`
|
||||||
2. Homepage: business-info, sliders, category-groups, brand-groups, store-specials
|
2. Homepage: business-info, sliders, category-groups, brand-groups, store-specials
|
||||||
3. Catalog: categories, products, store-items
|
3. Catalog: categories, products, store-items
|
||||||
4. Auth: register/login → store tokens
|
4. Auth: register/login → store tokens. Optional: `POST /auth/send-otp` then `POST /auth/login-otp` (passwordless) or `POST /auth/reset-password` (forgot password). `POST /auth/verify-otp` only marks the cell verified (no tokens).
|
||||||
5. Cart checkout with `addressId` or inline `shippingAddress` + `payment`
|
5. Cart checkout with `addressId` or inline `shippingAddress` + `payment`
|
||||||
|
|
||||||
If OpenAPI and this brief conflict, **OpenAPI wins**.
|
If OpenAPI and this brief conflict, **OpenAPI wins**.
|
||||||
|
|||||||
@@ -200,6 +200,55 @@
|
|||||||
"url": "{{baseUrl}}/auth/login"
|
"url": "{{baseUrl}}/auth/login"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Login with OTP",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"if (pm.response.code === 200) {",
|
||||||
|
" const json = pm.response.json();",
|
||||||
|
" pm.collectionVariables.set('accessToken', json.accessToken);",
|
||||||
|
" pm.collectionVariables.set('refreshToken', json.refreshToken);",
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"cellNumber\": \"+98XXXXXXXXXX\",\n \"code\": \"123456\"\n}"
|
||||||
|
},
|
||||||
|
"url": "{{baseUrl}}/auth/login-otp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Reset password (SMS OTP)",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"cellNumber\": \"+98XXXXXXXXXX\",\n \"code\": \"123456\",\n \"newPassword\": \"newpassword123\"\n}"
|
||||||
|
},
|
||||||
|
"url": "{{baseUrl}}/auth/reset-password"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Me (current user)",
|
"name": "Me (current user)",
|
||||||
"request": {
|
"request": {
|
||||||
|
|||||||
@@ -527,6 +527,51 @@
|
|||||||
"responses": { "200": { "description": "{ user, accessToken, refreshToken }" } }
|
"responses": { "200": { "description": "{ user, accessToken, refreshToken }" } }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/auth/login-otp": {
|
||||||
|
"post": {
|
||||||
|
"tags": ["Auth"],
|
||||||
|
"summary": "Passwordless login with SMS OTP",
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["cellNumber", "code"],
|
||||||
|
"properties": {
|
||||||
|
"cellNumber": { "type": "string" },
|
||||||
|
"code": { "type": "string", "minLength": 6, "maxLength": 6 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": { "200": { "description": "{ user, accessToken, refreshToken }" } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/reset-password": {
|
||||||
|
"post": {
|
||||||
|
"tags": ["Auth"],
|
||||||
|
"summary": "Reset password with SMS OTP",
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["cellNumber", "code", "newPassword"],
|
||||||
|
"properties": {
|
||||||
|
"cellNumber": { "type": "string" },
|
||||||
|
"code": { "type": "string", "minLength": 6, "maxLength": 6 },
|
||||||
|
"newPassword": { "type": "string", "minLength": 8 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": { "200": { "description": "{ message }" } }
|
||||||
|
}
|
||||||
|
},
|
||||||
"/auth/refresh": {
|
"/auth/refresh": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": ["Auth"],
|
"tags": ["Auth"],
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ import { AuthService } from './auth.service';
|
|||||||
import { CurrentUser } from './decorators/current-user.decorator';
|
import { CurrentUser } from './decorators/current-user.decorator';
|
||||||
import { ChangePasswordDto } from './dto/change-password.dto';
|
import { ChangePasswordDto } from './dto/change-password.dto';
|
||||||
import { LoginDto } from './dto/login.dto';
|
import { LoginDto } from './dto/login.dto';
|
||||||
|
import { LoginOtpDto } from './dto/login-otp.dto';
|
||||||
import { RefreshTokenDto } from './dto/refresh-token.dto';
|
import { RefreshTokenDto } from './dto/refresh-token.dto';
|
||||||
import { RegisterDto } from './dto/register.dto';
|
import { RegisterDto } from './dto/register.dto';
|
||||||
|
import { ResetPasswordDto } from './dto/reset-password.dto';
|
||||||
import { SendOtpDto } from './dto/send-otp.dto';
|
import { SendOtpDto } from './dto/send-otp.dto';
|
||||||
import { UpdateProfileDto } from './dto/update-profile.dto';
|
import { UpdateProfileDto } from './dto/update-profile.dto';
|
||||||
import { UpsertUserAddressDto } from './dto/upsert-user-address.dto';
|
import { UpsertUserAddressDto } from './dto/upsert-user-address.dto';
|
||||||
@@ -39,6 +41,20 @@ export class AuthController {
|
|||||||
return this.authService.login(dto);
|
return this.authService.login(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('login-otp')
|
||||||
|
loginWithOtp(@Body() dto: LoginOtpDto) {
|
||||||
|
return this.authService.loginWithOtp(dto.cellNumber, dto.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('reset-password')
|
||||||
|
resetPassword(@Body() dto: ResetPasswordDto) {
|
||||||
|
return this.authService.resetPassword(
|
||||||
|
dto.cellNumber,
|
||||||
|
dto.code,
|
||||||
|
dto.newPassword,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Post('refresh')
|
@Post('refresh')
|
||||||
refresh(@Body() dto: RefreshTokenDto) {
|
refresh(@Body() dto: RefreshTokenDto) {
|
||||||
return this.authService.refresh(dto.refreshToken);
|
return this.authService.refresh(dto.refreshToken);
|
||||||
|
|||||||
+66
-35
@@ -307,27 +307,75 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async verifyOtp(cellNumber: string, code: string) {
|
async verifyOtp(cellNumber: string, code: string) {
|
||||||
if (!this.sms.isEnabled()) {
|
const user = await this.consumeOtp(cellNumber, code);
|
||||||
const user = await this.prisma.user.findUnique({
|
const smsEnabled = this.sms.isEnabled();
|
||||||
where: { cellNumber },
|
|
||||||
|
if (!user.cellVerifiedAt) {
|
||||||
|
await this.prisma.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: { cellVerifiedAt: new Date() },
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!user) {
|
return {
|
||||||
throw new UnauthorizedException('Cell number is not registered');
|
enabled: smsEnabled,
|
||||||
}
|
verified: true,
|
||||||
|
message: smsEnabled
|
||||||
|
? 'Cell number verified successfully'
|
||||||
|
: 'SMS verification is disabled — cell number marked as verified.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!user.cellVerifiedAt) {
|
async loginWithOtp(cellNumber: string, code: string) {
|
||||||
await this.prisma.user.update({
|
const user = await this.consumeOtp(cellNumber, code);
|
||||||
where: { id: user.id },
|
|
||||||
data: { cellVerifiedAt: new Date() },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
await this.prisma.user.update({
|
||||||
enabled: false,
|
where: { id: user.id },
|
||||||
verified: true,
|
data: {
|
||||||
message: 'SMS verification is disabled — cell number marked as verified.',
|
cellVerifiedAt: user.cellVerifiedAt ?? new Date(),
|
||||||
};
|
lastLoginAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const authUser = await this.getAuthUser(user.id);
|
||||||
|
const tokens = await this.issueTokens(authUser);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Login successful',
|
||||||
|
user: this.serializeUser(authUser),
|
||||||
|
...tokens,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetPassword(cellNumber: string, code: string, newPassword: string) {
|
||||||
|
const user = await this.consumeOtp(cellNumber, code);
|
||||||
|
const passwordHash = await bcrypt.hash(newPassword, 10);
|
||||||
|
|
||||||
|
await this.prisma.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: {
|
||||||
|
passwordHash,
|
||||||
|
cellVerifiedAt: user.cellVerifiedAt ?? new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Password reset successfully. You can now sign in with your new password.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Validates OTP (or skips when SMS is disabled) and returns the active user. */
|
||||||
|
private async consumeOtp(cellNumber: string, code: string) {
|
||||||
|
const user = await this.prisma.user.findUnique({
|
||||||
|
where: { cellNumber },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user || !user.isActive) {
|
||||||
|
throw new UnauthorizedException('Cell number is not registered');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.sms.isEnabled()) {
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
const storedCode = await this.redis.getOtp(cellNumber);
|
const storedCode = await this.redis.getOtp(cellNumber);
|
||||||
@@ -335,25 +383,8 @@ export class AuthService {
|
|||||||
throw new UnauthorizedException('Invalid or expired verification code');
|
throw new UnauthorizedException('Invalid or expired verification code');
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.prisma.user.findUnique({
|
|
||||||
where: { cellNumber },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new UnauthorizedException('Cell number is not registered');
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.prisma.user.update({
|
|
||||||
where: { id: user.id },
|
|
||||||
data: { cellVerifiedAt: new Date() },
|
|
||||||
});
|
|
||||||
await this.redis.deleteOtp(cellNumber);
|
await this.redis.deleteOtp(cellNumber);
|
||||||
|
return user;
|
||||||
return {
|
|
||||||
enabled: true,
|
|
||||||
verified: true,
|
|
||||||
message: 'Cell number verified successfully',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAuthUser(userId: bigint): Promise<AuthUser> {
|
private async getAuthUser(userId: bigint): Promise<AuthUser> {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { IsString, Length, Matches } from 'class-validator';
|
||||||
|
|
||||||
|
export class LoginOtpDto {
|
||||||
|
@IsString()
|
||||||
|
@Matches(/^\+[1-9]\d{6,14}$/, {
|
||||||
|
message: 'cellNumber must be in E.164 format (e.g. +989121234567)',
|
||||||
|
})
|
||||||
|
cellNumber!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Length(6, 6)
|
||||||
|
@Matches(/^\d{6}$/, { message: 'code must be a 6-digit number' })
|
||||||
|
code!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { IsString, Length, Matches, MinLength } from 'class-validator';
|
||||||
|
|
||||||
|
export class ResetPasswordDto {
|
||||||
|
@IsString()
|
||||||
|
@Matches(/^\+[1-9]\d{6,14}$/, {
|
||||||
|
message: 'cellNumber must be in E.164 format (e.g. +989121234567)',
|
||||||
|
})
|
||||||
|
cellNumber!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Length(6, 6)
|
||||||
|
@Matches(/^\d{6}$/, { message: 'code must be a 6-digit number' })
|
||||||
|
code!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@MinLength(8, { message: 'newPassword must be at least 8 characters' })
|
||||||
|
newPassword!: string;
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ You are building a **Meshkee business website (storefront)**. You must use the M
|
|||||||
1. `GET /tenants/{domain}` → branding + `businessId`
|
1. `GET /tenants/{domain}` → branding + `businessId`
|
||||||
2. Homepage: business-info, sliders, category-groups, brand-groups, store-specials
|
2. Homepage: business-info, sliders, category-groups, brand-groups, store-specials
|
||||||
3. Catalog: categories, products, store-items
|
3. Catalog: categories, products, store-items
|
||||||
4. Auth: register/login → store tokens
|
4. Auth: register/login → store tokens. Optional: `POST /auth/send-otp` then `POST /auth/login-otp` (passwordless) or `POST /auth/reset-password` (forgot password). `POST /auth/verify-otp` only marks the cell verified (no tokens).
|
||||||
5. Cart checkout with `addressId` or inline `shippingAddress` + `payment`
|
5. Cart checkout with `addressId` or inline `shippingAddress` + `payment`
|
||||||
|
|
||||||
If OpenAPI and this brief conflict, **OpenAPI wins**.
|
If OpenAPI and this brief conflict, **OpenAPI wins**.
|
||||||
|
|||||||
@@ -200,6 +200,55 @@
|
|||||||
"url": "{{baseUrl}}/auth/login"
|
"url": "{{baseUrl}}/auth/login"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Login with OTP",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"if (pm.response.code === 200) {",
|
||||||
|
" const json = pm.response.json();",
|
||||||
|
" pm.collectionVariables.set('accessToken', json.accessToken);",
|
||||||
|
" pm.collectionVariables.set('refreshToken', json.refreshToken);",
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"cellNumber\": \"+98XXXXXXXXXX\",\n \"code\": \"123456\"\n}"
|
||||||
|
},
|
||||||
|
"url": "{{baseUrl}}/auth/login-otp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Reset password (SMS OTP)",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"cellNumber\": \"+98XXXXXXXXXX\",\n \"code\": \"123456\",\n \"newPassword\": \"newpassword123\"\n}"
|
||||||
|
},
|
||||||
|
"url": "{{baseUrl}}/auth/reset-password"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Me (current user)",
|
"name": "Me (current user)",
|
||||||
"request": {
|
"request": {
|
||||||
|
|||||||
@@ -527,6 +527,51 @@
|
|||||||
"responses": { "200": { "description": "{ user, accessToken, refreshToken }" } }
|
"responses": { "200": { "description": "{ user, accessToken, refreshToken }" } }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/auth/login-otp": {
|
||||||
|
"post": {
|
||||||
|
"tags": ["Auth"],
|
||||||
|
"summary": "Passwordless login with SMS OTP",
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["cellNumber", "code"],
|
||||||
|
"properties": {
|
||||||
|
"cellNumber": { "type": "string" },
|
||||||
|
"code": { "type": "string", "minLength": 6, "maxLength": 6 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": { "200": { "description": "{ user, accessToken, refreshToken }" } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/reset-password": {
|
||||||
|
"post": {
|
||||||
|
"tags": ["Auth"],
|
||||||
|
"summary": "Reset password with SMS OTP",
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["cellNumber", "code", "newPassword"],
|
||||||
|
"properties": {
|
||||||
|
"cellNumber": { "type": "string" },
|
||||||
|
"code": { "type": "string", "minLength": 6, "maxLength": 6 },
|
||||||
|
"newPassword": { "type": "string", "minLength": 8 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": { "200": { "description": "{ message }" } }
|
||||||
|
}
|
||||||
|
},
|
||||||
"/auth/refresh": {
|
"/auth/refresh": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": ["Auth"],
|
"tags": ["Auth"],
|
||||||
|
|||||||
Reference in New Issue
Block a user