skailan-core
Version:
Servicio de autenticación y multitenancy para Skailan.
308 lines • 11.9 kB
JavaScript
import { TrialService } from "../services/TrialService.js";
import { ProductService } from "../../product/domain/services/ProductService.js";
export class PaymentAccessMiddleware {
static instance;
prisma;
trialService;
productService;
constructor(prisma) {
this.prisma = prisma;
this.trialService = TrialService.getInstance(prisma);
this.productService = ProductService.getInstance(prisma);
}
static getInstance(prisma) {
if (!PaymentAccessMiddleware.instance) {
PaymentAccessMiddleware.instance = new PaymentAccessMiddleware(prisma);
}
return PaymentAccessMiddleware.instance;
}
/**
* Middleware principal para controlar acceso basado en pagos
*/
checkPaymentAccess(options = {}) {
return async (req, res, next) => {
try {
// Verificar que el usuario esté autenticado
if (!req.user) {
return res.status(401).json({
error: "Usuario no autenticado",
code: "UNAUTHORIZED",
});
}
// Obtener la organización del usuario
const membership = await this.prisma.membership.findFirst({
where: {
userId: req.user.id,
status: "ACTIVE",
},
include: {
organization: true,
},
});
if (!membership) {
return res.status(403).json({
error: "Usuario no pertenece a ninguna organización activa",
code: "NO_ORGANIZATION",
});
}
req.organization = membership.organization;
// Verificar si la organización está bloqueada
if (membership.organization.isBlocked) {
return res.status(403).json({
error: "Organización bloqueada por falta de pago",
code: "ORGANIZATION_BLOCKED",
details: {
reason: "payment_required",
trialEnd: membership.organization.trialEnd,
},
});
}
// Si solo requiere trial o pago activo
if (options.requireTrialOrPayment) {
const canAccess = await this.trialService.canAccess(membership.organization.id);
if (!canAccess) {
return res.status(403).json({
error: "Acceso denegado: trial expirado y sin pago activo",
code: "ACCESS_DENIED",
details: {
reason: "trial_expired_no_payment",
trialEnd: membership.organization.trialEnd,
},
});
}
}
// Si requiere pago activo específicamente
if (options.requireActivePayment) {
const hasActiveSubscription = await this.hasActiveSubscription(membership.organization.id);
if (!hasActiveSubscription) {
if (options.redirectToPayment) {
return res.status(402).json({
error: "Pago requerido",
code: "PAYMENT_REQUIRED",
redirect: "/billing",
});
}
else {
return res.status(403).json({
error: "Pago activo requerido",
code: "ACTIVE_PAYMENT_REQUIRED",
});
}
}
}
// Verificar características específicas
if (options.allowedFeatures && options.allowedFeatures.length > 0) {
const hasFeature = await this.checkFeatures(membership.organization.id, options.allowedFeatures);
if (!hasFeature) {
return res.status(403).json({
error: "Característica no disponible en el plan actual",
code: "FEATURE_NOT_AVAILABLE",
details: {
requiredFeatures: options.allowedFeatures,
},
});
}
}
// Verificar límites de usuarios si es necesario
if (options.checkUserLimits) {
const userLimitExceeded = await this.checkUserLimits(membership.organization.id);
if (userLimitExceeded) {
return res.status(403).json({
error: "Límite de usuarios excedido",
code: "USER_LIMIT_EXCEEDED",
details: {
currentUsers: userLimitExceeded.current,
maxUsers: userLimitExceeded.max,
},
});
}
}
next();
}
catch (error) {
console.error("Error en PaymentAccessMiddleware:", error);
return res.status(500).json({
error: "Error interno del servidor",
code: "INTERNAL_ERROR",
});
}
};
}
/**
* Middleware para verificar trial activo
*/
checkTrialAccess() {
return async (req, res, next) => {
try {
if (!req.organization) {
return res.status(400).json({
error: "Organización no encontrada en el request",
code: "NO_ORGANIZATION",
});
}
const trialStatus = await this.trialService.getTrialStatus(req.organization.id);
if (!trialStatus.isActive) {
return res.status(403).json({
error: "Trial no activo",
code: "TRIAL_NOT_ACTIVE",
details: {
trialStatus,
},
});
}
// Agregar información del trial al request
req.trialStatus = trialStatus;
next();
}
catch (error) {
console.error("Error en checkTrialAccess:", error);
return res.status(500).json({
error: "Error interno del servidor",
code: "INTERNAL_ERROR",
});
}
};
}
/**
* Middleware para verificar suscripción activa
*/
checkActiveSubscription() {
return async (req, res, next) => {
try {
if (!req.organization) {
return res.status(400).json({
error: "Organización no encontrada en el request",
code: "NO_ORGANIZATION",
});
}
const hasActiveSubscription = await this.hasActiveSubscription(req.organization.id);
if (!hasActiveSubscription) {
return res.status(403).json({
error: "Suscripción activa requerida",
code: "ACTIVE_SUBSCRIPTION_REQUIRED",
});
}
next();
}
catch (error) {
console.error("Error en checkActiveSubscription:", error);
return res.status(500).json({
error: "Error interno del servidor",
code: "INTERNAL_ERROR",
});
}
};
}
/**
* Middleware para verificar características específicas
*/
checkFeatureAccess(featureCode) {
return async (req, res, next) => {
try {
if (!req.organization) {
return res.status(400).json({
error: "Organización no encontrada en el request",
code: "NO_ORGANIZATION",
});
}
const hasFeature = await this.checkFeatures(req.organization.id, [
featureCode,
]);
if (!hasFeature) {
return res.status(403).json({
error: `Característica '${featureCode}' no disponible`,
code: "FEATURE_NOT_AVAILABLE",
details: {
requiredFeature: featureCode,
},
});
}
next();
}
catch (error) {
console.error("Error en checkFeatureAccess:", error);
return res.status(500).json({
error: "Error interno del servidor",
code: "INTERNAL_ERROR",
});
}
};
}
/**
* Verifica si la organización tiene una suscripción activa
*/
async hasActiveSubscription(organizationId) {
const subscription = await this.prisma.subscription.findFirst({
where: {
organizationId,
status: "ACTIVE",
},
});
return !!subscription;
}
/**
* Verifica si la organización tiene acceso a las características especificadas
*/
async checkFeatures(organizationId, featureCodes) {
// Obtener la suscripción activa
const subscription = await this.prisma.subscription.findFirst({
where: {
organizationId,
status: "ACTIVE",
},
include: {
plan: {
include: {
planFeatures: {
include: {
feature: true,
},
},
},
},
},
});
if (!subscription) {
return false;
}
// Verificar cada característica requerida
for (const featureCode of featureCodes) {
const hasFeature = subscription.plan.planFeatures.some((pf) => pf.feature.code === featureCode && pf.feature.isActive);
if (!hasFeature) {
return false;
}
}
return true;
}
/**
* Verifica los límites de usuarios
*/
async checkUserLimits(organizationId) {
const subscription = await this.prisma.subscription.findFirst({
where: {
organizationId,
status: "ACTIVE",
},
include: {
plan: true,
},
});
if (!subscription || !subscription.plan.maxUsers) {
return null; // Sin límite
}
const currentUsers = await this.prisma.membership.count({
where: {
organizationId,
status: "ACTIVE",
},
});
if (currentUsers >= subscription.plan.maxUsers) {
return {
current: currentUsers,
max: subscription.plan.maxUsers,
};
}
return null;
}
}
//# sourceMappingURL=PaymentAccessMiddleware.js.map