Marketplace móvil para negocios locales mexicanos. Estructura inicial: - apps/backend (NestJS API) - apps/frontend (React Web) - apps/mobile (Expo/React Native) - apps/mcp-server (Claude MCP Server) - apps/whatsapp-service (WhatsApp Business API) - database/ (PostgreSQL DDL) - docs/ (Documentación) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
192 lines
7.7 KiB
JavaScript
192 lines
7.7 KiB
JavaScript
"use strict";
|
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
};
|
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
};
|
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.AuthService = void 0;
|
|
const common_1 = require("@nestjs/common");
|
|
const typeorm_1 = require("@nestjs/typeorm");
|
|
const typeorm_2 = require("typeorm");
|
|
const jwt_1 = require("@nestjs/jwt");
|
|
const config_1 = require("@nestjs/config");
|
|
const bcrypt = require("bcrypt");
|
|
const tenant_entity_1 = require("./entities/tenant.entity");
|
|
const user_entity_1 = require("./entities/user.entity");
|
|
let AuthService = class AuthService {
|
|
constructor(tenantRepository, userRepository, jwtService, configService) {
|
|
this.tenantRepository = tenantRepository;
|
|
this.userRepository = userRepository;
|
|
this.jwtService = jwtService;
|
|
this.configService = configService;
|
|
}
|
|
generateSlug(name) {
|
|
return name
|
|
.toLowerCase()
|
|
.normalize('NFD')
|
|
.replace(/[\u0300-\u036f]/g, '')
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/(^-|-$)/g, '')
|
|
.substring(0, 45) + '-' + Date.now().toString(36).slice(-4);
|
|
}
|
|
async register(dto) {
|
|
const existingTenant = await this.tenantRepository.findOne({
|
|
where: { phone: dto.phone },
|
|
});
|
|
if (existingTenant) {
|
|
throw new common_1.ConflictException('Este teléfono ya está registrado');
|
|
}
|
|
const pinHash = await bcrypt.hash(dto.pin, 10);
|
|
const slug = this.generateSlug(dto.name);
|
|
const tenant = this.tenantRepository.create({
|
|
name: dto.name,
|
|
slug,
|
|
businessType: dto.businessType,
|
|
phone: dto.phone,
|
|
email: dto.email,
|
|
address: dto.address,
|
|
city: dto.city,
|
|
whatsappNumber: dto.whatsapp || dto.phone,
|
|
subscriptionStatus: 'trial',
|
|
status: 'active',
|
|
});
|
|
const savedTenant = await this.tenantRepository.save(tenant);
|
|
const user = this.userRepository.create({
|
|
tenantId: savedTenant.id,
|
|
phone: dto.phone,
|
|
name: dto.ownerName,
|
|
pinHash,
|
|
role: 'owner',
|
|
status: 'active',
|
|
});
|
|
const savedUser = await this.userRepository.save(user);
|
|
return this.generateTokens(savedUser, savedTenant);
|
|
}
|
|
async login(dto) {
|
|
const user = await this.userRepository.findOne({
|
|
where: { phone: dto.phone },
|
|
});
|
|
if (!user) {
|
|
throw new common_1.UnauthorizedException('Teléfono o PIN incorrectos');
|
|
}
|
|
const tenant = await this.tenantRepository.findOne({
|
|
where: { id: user.tenantId },
|
|
});
|
|
if (!tenant) {
|
|
throw new common_1.UnauthorizedException('Teléfono o PIN incorrectos');
|
|
}
|
|
if (tenant.subscriptionStatus === 'cancelled') {
|
|
throw new common_1.UnauthorizedException('Tu suscripción ha sido cancelada');
|
|
}
|
|
if (tenant.subscriptionStatus === 'suspended' || tenant.status === 'suspended') {
|
|
throw new common_1.UnauthorizedException('Tu cuenta está suspendida. Contacta soporte.');
|
|
}
|
|
if (user.status !== 'active') {
|
|
throw new common_1.UnauthorizedException('Tu cuenta está inactiva');
|
|
}
|
|
if (user.lockedUntil && user.lockedUntil > new Date()) {
|
|
throw new common_1.UnauthorizedException('Cuenta bloqueada temporalmente. Intenta más tarde.');
|
|
}
|
|
const isValidPin = await bcrypt.compare(dto.pin, user.pinHash);
|
|
if (!isValidPin) {
|
|
user.failedAttempts = (user.failedAttempts || 0) + 1;
|
|
if (user.failedAttempts >= 5) {
|
|
user.lockedUntil = new Date(Date.now() + 15 * 60 * 1000);
|
|
}
|
|
await this.userRepository.save(user);
|
|
throw new common_1.UnauthorizedException('Teléfono o PIN incorrectos');
|
|
}
|
|
user.failedAttempts = 0;
|
|
user.lockedUntil = null;
|
|
user.lastLoginAt = new Date();
|
|
await this.userRepository.save(user);
|
|
return this.generateTokens(user, tenant);
|
|
}
|
|
async refreshToken(refreshToken) {
|
|
try {
|
|
const payload = this.jwtService.verify(refreshToken, {
|
|
secret: this.configService.get('JWT_SECRET'),
|
|
});
|
|
const user = await this.userRepository.findOne({
|
|
where: { id: payload.sub },
|
|
});
|
|
if (!user) {
|
|
throw new common_1.UnauthorizedException('Token inválido');
|
|
}
|
|
const tenant = await this.tenantRepository.findOne({
|
|
where: { id: user.tenantId },
|
|
});
|
|
if (!tenant) {
|
|
throw new common_1.UnauthorizedException('Token inválido');
|
|
}
|
|
return this.generateTokens(user, tenant);
|
|
}
|
|
catch {
|
|
throw new common_1.UnauthorizedException('Token inválido o expirado');
|
|
}
|
|
}
|
|
async changePin(userId, currentPin, newPin) {
|
|
const user = await this.userRepository.findOne({
|
|
where: { id: userId },
|
|
});
|
|
if (!user) {
|
|
throw new common_1.BadRequestException('Usuario no encontrado');
|
|
}
|
|
const isValidPin = await bcrypt.compare(currentPin, user.pinHash);
|
|
if (!isValidPin) {
|
|
throw new common_1.UnauthorizedException('PIN actual incorrecto');
|
|
}
|
|
user.pinHash = await bcrypt.hash(newPin, 10);
|
|
await this.userRepository.save(user);
|
|
}
|
|
generateTokens(user, tenant) {
|
|
const payload = {
|
|
sub: user.id,
|
|
tenantId: tenant.id,
|
|
phone: user.phone,
|
|
role: user.role,
|
|
};
|
|
const accessToken = this.jwtService.sign(payload, {
|
|
expiresIn: this.configService.get('JWT_EXPIRES_IN', '24h'),
|
|
});
|
|
const refreshToken = this.jwtService.sign(payload, {
|
|
expiresIn: this.configService.get('JWT_REFRESH_EXPIRES_IN', '7d'),
|
|
});
|
|
return {
|
|
accessToken,
|
|
refreshToken,
|
|
user: {
|
|
id: user.id,
|
|
name: user.name,
|
|
role: user.role,
|
|
phone: user.phone,
|
|
},
|
|
tenant: {
|
|
id: tenant.id,
|
|
name: tenant.name,
|
|
slug: tenant.slug,
|
|
businessType: tenant.businessType,
|
|
subscriptionStatus: tenant.subscriptionStatus,
|
|
},
|
|
};
|
|
}
|
|
};
|
|
exports.AuthService = AuthService;
|
|
exports.AuthService = AuthService = __decorate([
|
|
(0, common_1.Injectable)(),
|
|
__param(0, (0, typeorm_1.InjectRepository)(tenant_entity_1.Tenant)),
|
|
__param(1, (0, typeorm_1.InjectRepository)(user_entity_1.User)),
|
|
__metadata("design:paramtypes", [typeorm_2.Repository,
|
|
typeorm_2.Repository,
|
|
jwt_1.JwtService,
|
|
config_1.ConfigService])
|
|
], AuthService);
|
|
//# sourceMappingURL=auth.service.js.map
|