P1 complete: real انطلق maps + Nabeh WhatsApp OTP + chat + ratings + cancel(both) + fraud detection
- maps: real map-saas route/geocode/reverse/places (x-api-key per country) + straight-line fallback - auth: Redis-backed OTP + Nabeh WhatsApp send (dev mode keeps fixed 1234 bypass) - chat: per-trip messages (participant-guarded) + socket broadcast - ratings: post-trip rating + driver avg recompute + no double-rate - cancel: from rider or driver + stage-based cancel fee + notify both - fraud: cancel-abuse (soft/hard block via Redis), arrived-far-from-pickup, completed-too-fast → fraud_flags - migration InitP1b (chat_messages, ratings, fraud_flags, trips.cancelled_by/cancel_fee) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
|
||||
import { Inject, Injectable, Logger, UnauthorizedException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import Redis from 'ioredis';
|
||||
import { REDIS } from '../../common/redis/redis.module';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { User } from '../users/entities/user.entity';
|
||||
import { TenantsService } from '../tenants/tenants.service';
|
||||
import { Tenant } from '../../database/entities/tenant.entity';
|
||||
import { NabehService } from '../../integrations/nabeh/nabeh.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@@ -14,44 +18,78 @@ export class AuthService {
|
||||
private jwtService: JwtService,
|
||||
private config: ConfigService,
|
||||
private tenantsService: TenantsService,
|
||||
private nabeh: NabehService,
|
||||
@Inject(REDIS) private readonly redis: Redis,
|
||||
) {}
|
||||
|
||||
/** يحوّل الـ slug القادم من الهيدر إلى UUID المستأجر (tenant_id). */
|
||||
private async resolveTenantId(tenantSlugOrId: string): Promise<string> {
|
||||
const tenant = await this.tenantsService.resolve(tenantSlugOrId);
|
||||
private async resolveTenant(slugOrId: string): Promise<Tenant> {
|
||||
const tenant = await this.tenantsService.resolve(slugOrId);
|
||||
if (!tenant) throw new UnauthorizedException('Unknown tenant');
|
||||
return tenant.id;
|
||||
return tenant;
|
||||
}
|
||||
|
||||
private get devCode(): string {
|
||||
// رمز التطوير الثابت — يُستبدل بمحوّل SMS في P2 (راجع docs/07).
|
||||
return '1234';
|
||||
private get devMode(): boolean {
|
||||
return this.config.get<boolean>('auth.otpDevMode') !== false;
|
||||
}
|
||||
|
||||
async sendOtp(tenantId: string, phone: string) {
|
||||
// وضع تطوير: الرمز ثابت ويُطبع باللوغ بلا مزوّد SMS.
|
||||
this.logger.log(`OTP for tenant=${tenantId} phone=${phone} => ${this.devCode} (dev)`);
|
||||
return {
|
||||
success: true,
|
||||
message: 'OTP sent',
|
||||
dev_code: this.config.get('auth.otpDevMode') ? this.devCode : undefined,
|
||||
};
|
||||
private otpKey(tenantId: string, phone: string): string {
|
||||
return `otp:${tenantId}:${phone}`;
|
||||
}
|
||||
|
||||
private genCode(): string {
|
||||
if (this.devMode) return '1234';
|
||||
const len = this.config.get<number>('auth.otpLength') ?? 4;
|
||||
let c = '';
|
||||
for (let i = 0; i < len; i++) c += Math.floor(Math.random() * 10);
|
||||
return c;
|
||||
}
|
||||
|
||||
/** يصيغ الهاتف لصيغة دولية بلا + (مثل 962790000000) حسب دولة المستأجر. */
|
||||
private formatPhone(phone: string, countryPack: string): string {
|
||||
const codes = this.config.get<Record<string, string>>('callingCodes') ?? {};
|
||||
const cc = codes[countryPack] ?? '962';
|
||||
let p = phone.replace(/\D/g, '');
|
||||
if (p.startsWith('00')) p = p.slice(2);
|
||||
if (p.startsWith('0')) p = cc + p.slice(1);
|
||||
else if (!p.startsWith(cc)) p = cc + p;
|
||||
return p;
|
||||
}
|
||||
|
||||
async sendOtp(tenantSlug: string, phone: string) {
|
||||
const tenant = await this.resolveTenant(tenantSlug);
|
||||
const code = this.genCode();
|
||||
const ttl = this.config.get<number>('auth.otpTtl') ?? 300;
|
||||
await this.redis.set(this.otpKey(tenant.id, phone), code, 'EX', ttl);
|
||||
|
||||
if (this.devMode) {
|
||||
this.logger.log(`OTP (dev) tenant=${tenant.slug} phone=${phone} => ${code}`);
|
||||
return { success: true, message: 'OTP sent (dev)', dev_code: code };
|
||||
}
|
||||
|
||||
// إرسال حقيقي عبر واتساب (Nabeh)
|
||||
const intl = this.formatPhone(phone, tenant.countryPack);
|
||||
await this.nabeh.sendOtp(intl, code);
|
||||
return { success: true, message: 'OTP sent via WhatsApp' };
|
||||
}
|
||||
|
||||
async verifyOtp(tenantSlug: string, phone: string, code: string) {
|
||||
if (code !== this.devCode) {
|
||||
throw new UnauthorizedException('Invalid OTP code');
|
||||
const tenant = await this.resolveTenant(tenantSlug);
|
||||
|
||||
// في وضع التطوير: الرمز الثابت 1234 يمرّ دائماً (تسهيل الاختبار).
|
||||
const devBypass = this.devMode && code === '1234';
|
||||
if (!devBypass) {
|
||||
const stored = await this.redis.get(this.otpKey(tenant.id, phone));
|
||||
if (!stored || stored !== code) {
|
||||
throw new UnauthorizedException('Invalid or expired OTP code');
|
||||
}
|
||||
await this.redis.del(this.otpKey(tenant.id, phone));
|
||||
}
|
||||
|
||||
// الهيدر يحمل slug — نحوّله لـ UUID قبل أي استعلام على tenant_id.
|
||||
const tenantId = await this.resolveTenantId(tenantSlug);
|
||||
|
||||
let user = await this.usersService.findByPhone(tenantId, phone);
|
||||
let user = await this.usersService.findByPhone(tenant.id, phone);
|
||||
if (!user) {
|
||||
user = await this.usersService.create(tenantId, phone);
|
||||
user = await this.usersService.create(tenant.id, phone);
|
||||
}
|
||||
|
||||
return this.issueTokens(user, tenantId);
|
||||
return this.issueTokens(user, tenant.id);
|
||||
}
|
||||
|
||||
async refresh(refreshToken: string) {
|
||||
@@ -69,7 +107,6 @@ export class AuthService {
|
||||
return this.issueTokens(user, user.tenant_id);
|
||||
}
|
||||
|
||||
/** يصدر access + refresh معاً. */
|
||||
private issueTokens(user: User, tenantId: string) {
|
||||
const base = {
|
||||
sub: user.id,
|
||||
|
||||
Reference in New Issue
Block a user