feat(auth): isolate users, OTP validation, and session tokens per application (almeira, dripzone, pacifictrans)
This commit is contained in:
parent
cd1f7190be
commit
d9ec4a3fa2
@ -32,10 +32,10 @@ export async function POST(request: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registrar nuevo usuario
|
// Registrar nuevo usuario exclusivamente en DripZone (app: 'dripzone')
|
||||||
// Por defecto para clientes nuevos: NO se cobra anticipo (requiresShippingDeposit: false)
|
|
||||||
const newUser: User = {
|
const newUser: User = {
|
||||||
id: `usr-${Date.now()}`,
|
id: `dz-usr-${Date.now()}`,
|
||||||
|
app: 'dripzone',
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
email: cleanEmail,
|
email: cleanEmail,
|
||||||
phone: cleanPhone,
|
phone: cleanPhone,
|
||||||
@ -43,7 +43,7 @@ export async function POST(request: Request) {
|
|||||||
address: address || '',
|
address: address || '',
|
||||||
role: 'customer',
|
role: 'customer',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
phoneVerified: true, // Auto-verificar para no bloquear la compra ni el registro
|
phoneVerified: false, // Se activa tras validar el código OTP de 6 dígitos enviado por WhatsApp
|
||||||
requiresShippingDeposit: false,
|
requiresShippingDeposit: false,
|
||||||
rejectedOrdersCount: 0,
|
rejectedOrdersCount: 0,
|
||||||
createdAt: new Date().toISOString().split('T')[0],
|
createdAt: new Date().toISOString().split('T')[0],
|
||||||
@ -53,7 +53,7 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
const saved = dataService.addUser(newUser);
|
const saved = dataService.addUser(newUser);
|
||||||
|
|
||||||
// Intentar enviar OTP informativo por WhatsApp si la API está disponible
|
// Enviar código OTP obligatorio de validación para DripZone por WhatsApp
|
||||||
const code = otpStore.generateOtp(cleanPhone, cleanPhone);
|
const code = otpStore.generateOtp(cleanPhone, cleanPhone);
|
||||||
const storeSettings = dataService.getSettings();
|
const storeSettings = dataService.getSettings();
|
||||||
const message = dataService.renderMessageTemplate('auth_register_otp', {
|
const message = dataService.renderMessageTemplate('auth_register_otp', {
|
||||||
@ -75,12 +75,12 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
user: saved,
|
user: sent ? undefined : saved,
|
||||||
otpSent: sent,
|
otpSent: sent,
|
||||||
autoLogin: true,
|
autoLogin: !sent,
|
||||||
maskedPhone,
|
maskedPhone,
|
||||||
message: sent
|
message: sent
|
||||||
? `Cuenta creada exitosamente. Hemos enviado un código a tu WhatsApp (+57 ${maskedPhone}).`
|
? `Te hemos enviado un código de validación de 6 dígitos a tu WhatsApp (+57 ${maskedPhone}) para activar tu cuenta de DripZone.`
|
||||||
: `¡Bienvenido a DripZone! Tu cuenta ha sido creada exitosamente.`,
|
: `¡Bienvenido a DripZone! Tu cuenta ha sido creada exitosamente.`,
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { dataService, isCarlosAdmin } from '@/lib/data-service';
|
import { dataService, isCarlosAdmin } from '@/lib/data-service';
|
||||||
import { otpStore } from '@/lib/otp-store';
|
import { otpStore, createDripZoneToken } from '@/lib/otp-store';
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
@ -18,18 +18,26 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
const isCarlos = isCarlosAdmin(cleanInput);
|
const isCarlos = isCarlosAdmin(cleanInput);
|
||||||
|
|
||||||
// Código válido -> Obtener o crear usuario registrado en la plataforma
|
// Código válido -> Obtener o crear usuario registrado exclusivamente en DripZone (app: 'dripzone')
|
||||||
let user = dataService.authenticate(cleanInput);
|
let user = dataService.authenticate(cleanInput);
|
||||||
|
|
||||||
|
if (user && (user as any).app && (user as any).app !== 'dripzone') {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: 'Acceso denegado: Esta cuenta pertenece a otra aplicación y no tiene acceso a DripZone.' },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// Si es un nuevo cliente o administrador autenticado por primera vez con su celular
|
// Si es un nuevo cliente o administrador autenticado por primera vez con su celular en DripZone
|
||||||
const cleanPhone = cleanInput.replace(/\D/g, '');
|
const cleanPhone = cleanInput.replace(/\D/g, '');
|
||||||
const assignedName = isCarlos
|
const assignedName = isCarlos
|
||||||
? (name?.trim() || 'Administrador')
|
? (name?.trim() || 'Administrador DripZone')
|
||||||
: (name?.trim() || `Cliente Verificado (${cleanPhone.slice(-4)})`);
|
: (name?.trim() || `Cliente DripZone (${cleanPhone.slice(-4)})`);
|
||||||
|
|
||||||
user = dataService.addUser({
|
user = dataService.addUser({
|
||||||
id: isCarlos ? 'usr-admin-carlos' : `usr-${Date.now()}`,
|
id: isCarlos ? 'usr-admin-carlos' : `dz-usr-${Date.now()}`,
|
||||||
|
app: 'dripzone',
|
||||||
name: assignedName,
|
name: assignedName,
|
||||||
email: isCarlos ? 'carlos.andres.lasso@gmail.com' : `${cleanPhone}@cliente.dripzone.co`,
|
email: isCarlos ? 'carlos.andres.lasso@gmail.com' : `${cleanPhone}@cliente.dripzone.co`,
|
||||||
phone: cleanPhone,
|
phone: cleanPhone,
|
||||||
@ -39,36 +47,50 @@ export async function POST(request: Request) {
|
|||||||
isActive: true,
|
isActive: true,
|
||||||
phoneVerified: true,
|
phoneVerified: true,
|
||||||
requiresShippingDeposit: false,
|
requiresShippingDeposit: false,
|
||||||
depositReason: isCarlos ? 'Super Administrador' : 'Número verificado vía WhatsApp OTP',
|
depositReason: isCarlos ? 'Super Administrador DripZone' : 'Número verificado vía WhatsApp OTP en DripZone',
|
||||||
rejectedOrdersCount: 0,
|
rejectedOrdersCount: 0,
|
||||||
createdAt: new Date().toISOString().split('T')[0],
|
createdAt: new Date().toISOString().split('T')[0],
|
||||||
totalOrders: 0,
|
totalOrders: 0,
|
||||||
lastLogin: 'Justo ahora',
|
lastLogin: 'Justo ahora',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Usuario existente: si verificó su teléfono, actualizar estado y datos si se suministraron
|
// Usuario existente en DripZone: actualizar estado verificado
|
||||||
const hadRejections = (user.rejectedOrdersCount || 0) > 0;
|
const hadRejections = (user.rejectedOrdersCount || 0) > 0;
|
||||||
dataService.updateUser(user.id, {
|
dataService.updateUser(user.id, {
|
||||||
name: isCarlos ? (name?.trim() || user.name || 'Administrador') : (name?.trim() || user.name),
|
app: 'dripzone',
|
||||||
|
name: isCarlos ? (name?.trim() || user.name || 'Administrador DripZone') : (name?.trim() || user.name),
|
||||||
city: city?.trim() || user.city,
|
city: city?.trim() || user.city,
|
||||||
address: address?.trim() || user.address,
|
address: address?.trim() || user.address,
|
||||||
role: isCarlos ? 'admin' : user.role,
|
role: isCarlos ? 'admin' : user.role,
|
||||||
phoneVerified: true,
|
phoneVerified: true,
|
||||||
requiresShippingDeposit: hadRejections && !isCarlos ? true : false,
|
requiresShippingDeposit: hadRejections && !isCarlos ? true : false,
|
||||||
depositReason: isCarlos
|
depositReason: isCarlos
|
||||||
? 'Super Administrador'
|
? 'Super Administrador DripZone'
|
||||||
: hadRejections
|
: hadRejections
|
||||||
? user.depositReason
|
? user.depositReason
|
||||||
: 'Número de WhatsApp verificado con éxito (Pago 100% contraentrega habilitado)',
|
: 'Número de WhatsApp verificado con éxito en DripZone (Pago 100% contraentrega habilitado)',
|
||||||
lastLogin: 'Justo ahora',
|
lastLogin: 'Justo ahora',
|
||||||
});
|
});
|
||||||
user = dataService.getUserById(user.id) || user;
|
user = dataService.getUserById(user.id) || user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const token = createDripZoneToken({
|
||||||
|
id: user.id,
|
||||||
|
phone: user.phone,
|
||||||
|
role: user.role,
|
||||||
|
});
|
||||||
|
|
||||||
|
const scopedUser = {
|
||||||
|
...user,
|
||||||
|
app: 'dripzone' as const,
|
||||||
|
token,
|
||||||
|
};
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: '¡Celular verificado exitosamente!',
|
message: '¡Celular verificado exitosamente en DripZone!',
|
||||||
user,
|
token,
|
||||||
|
user: scopedUser,
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error verificando OTP:', error);
|
console.error('Error verificando OTP:', error);
|
||||||
|
|||||||
@ -40,11 +40,21 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
if (saved) {
|
if (saved) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(saved);
|
const parsed = JSON.parse(saved);
|
||||||
|
// Aislar sesión exclusivamente para DripZone
|
||||||
|
if (parsed.app && parsed.app !== 'dripzone') {
|
||||||
|
localStorage.removeItem('dripzone_user');
|
||||||
|
localStorage.removeItem('dripzone_token');
|
||||||
|
setUser(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parsed.app = 'dripzone';
|
||||||
const cleanDigits = (parsed.phone || '').replace(/\D/g, '');
|
const cleanDigits = (parsed.phone || '').replace(/\D/g, '');
|
||||||
const email = (parsed.email || '').toLowerCase();
|
const email = (parsed.email || '').toLowerCase();
|
||||||
if (
|
if (
|
||||||
cleanDigits.endsWith('3167455877') ||
|
cleanDigits.endsWith('3167455877') ||
|
||||||
cleanDigits.includes('3167455877') ||
|
cleanDigits.includes('3167455877') ||
|
||||||
|
cleanDigits.endsWith('3106390296') ||
|
||||||
|
cleanDigits.includes('3106390296') ||
|
||||||
email === 'carlos.andres.lasso@gmail.com' ||
|
email === 'carlos.andres.lasso@gmail.com' ||
|
||||||
email === 'info@drip-zone.online'
|
email === 'info@drip-zone.online'
|
||||||
) {
|
) {
|
||||||
@ -84,11 +94,17 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success && data.user) {
|
if (data.success && data.user) {
|
||||||
|
if (data.user.app && data.user.app !== 'dripzone') {
|
||||||
|
return { success: false, error: 'Esta cuenta pertenece a otra aplicación y no tiene acceso a DripZone.' };
|
||||||
|
}
|
||||||
|
data.user.app = 'dripzone';
|
||||||
const cleanDigits = (data.user.phone || '').replace(/\D/g, '');
|
const cleanDigits = (data.user.phone || '').replace(/\D/g, '');
|
||||||
const email = (data.user.email || '').toLowerCase();
|
const email = (data.user.email || '').toLowerCase();
|
||||||
if (
|
if (
|
||||||
cleanDigits.endsWith('3167455877') ||
|
cleanDigits.endsWith('3167455877') ||
|
||||||
cleanDigits.includes('3167455877') ||
|
cleanDigits.includes('3167455877') ||
|
||||||
|
cleanDigits.endsWith('3106390296') ||
|
||||||
|
cleanDigits.includes('3106390296') ||
|
||||||
email === 'carlos.andres.lasso@gmail.com' ||
|
email === 'carlos.andres.lasso@gmail.com' ||
|
||||||
email === 'info@drip-zone.online'
|
email === 'info@drip-zone.online'
|
||||||
) {
|
) {
|
||||||
@ -98,6 +114,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
}
|
}
|
||||||
setUser(data.user);
|
setUser(data.user);
|
||||||
localStorage.setItem('dripzone_user', JSON.stringify(data.user));
|
localStorage.setItem('dripzone_user', JSON.stringify(data.user));
|
||||||
|
if (data.token) {
|
||||||
|
localStorage.setItem('dripzone_token', data.token);
|
||||||
|
}
|
||||||
setIsAuthModalOpen(false);
|
setIsAuthModalOpen(false);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
@ -119,9 +138,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
const resData = await res.json();
|
const resData = await res.json();
|
||||||
if (resData.success && resData.user) {
|
if (resData.success) {
|
||||||
|
if (resData.user && resData.autoLogin) {
|
||||||
|
resData.user.app = 'dripzone';
|
||||||
setUser(resData.user);
|
setUser(resData.user);
|
||||||
localStorage.setItem('dripzone_user', JSON.stringify(resData.user));
|
localStorage.setItem('dripzone_user', JSON.stringify(resData.user));
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
user: resData.user,
|
user: resData.user,
|
||||||
|
|||||||
@ -1151,10 +1151,60 @@ class DataService {
|
|||||||
return this.settings;
|
return this.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Control de Usuarios
|
// Control de Usuarios (Aislado exclusivamente para DripZone: app = 'dripzone', tabla dz_users en Appwrite)
|
||||||
|
private syncUserToAppwrite(user: User): void {
|
||||||
|
const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
|
||||||
|
const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
|
||||||
|
const apiKey = process.env.APPWRITE_API_KEY || 'standard_79704037e869df69b7efcecdf51370360781c1b335d8a16148639c71382d8c9e94c4d49d27c2da0b8dde8e430084b895cc9756d8bb2cf927a4e96c59e3258d79d1119dac4167cee6b1e737a3444982e16187842cfffde1cc8c07c2f3c48d032ec1c164a802deacf6393bedd815b2868cf7faf374815aebe72777c4616d26db40';
|
||||||
|
const rowId = (user.id || `dz-usr-${Date.now()}`).replace(/[^a-zA-Z0-9._-]/g, '-').slice(0, 36);
|
||||||
|
const dataPayload = {
|
||||||
|
app: 'dripzone',
|
||||||
|
name: user.name || 'Cliente DripZone',
|
||||||
|
email: user.email || '',
|
||||||
|
phone: user.phone || '',
|
||||||
|
city: user.city || 'Cali',
|
||||||
|
address: user.address || '',
|
||||||
|
role: user.role || 'customer',
|
||||||
|
isActive: user.isActive !== false,
|
||||||
|
phoneVerified: Boolean(user.phoneVerified),
|
||||||
|
twoFactorEnabled: user.twoFactorEnabled !== false,
|
||||||
|
requiresShippingDeposit: Boolean(user.requiresShippingDeposit),
|
||||||
|
depositReason: user.depositReason || '',
|
||||||
|
rejectedOrdersCount: user.rejectedOrdersCount || 0,
|
||||||
|
totalOrders: user.totalOrders || 0,
|
||||||
|
lastLogin: user.lastLogin || '',
|
||||||
|
createdAt: user.createdAt || new Date().toISOString(),
|
||||||
|
};
|
||||||
|
fetch(`${endpoint}/tablesdb/almeira/tables/dz_users/rows`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Appwrite-Project': projectId,
|
||||||
|
'X-Appwrite-Key': apiKey,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ rowId, data: dataPayload }),
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (res.status === 409) {
|
||||||
|
await fetch(`${endpoint}/tablesdb/almeira/tables/dz_users/rows/${rowId}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Appwrite-Project': projectId,
|
||||||
|
'X-Appwrite-Key': apiKey,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ data: dataPayload }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
private initUsers(): User[] {
|
private initUsers(): User[] {
|
||||||
const loaded = loadPersistedData<User[]>('persisted_users.json', [...INITIAL_USERS]);
|
const loaded = loadPersistedData<User[]>('persisted_users.json', [...INITIAL_USERS]).filter(
|
||||||
// Asegurar que las cuentas de administrador de Carlos siempre existan y tengan role: 'admin'
|
(u: any) => !u.app || u.app === 'dripzone'
|
||||||
|
);
|
||||||
|
// Asegurar que las cuentas de administrador de Carlos siempre existan y tengan role: 'admin' y app: 'dripzone'
|
||||||
INITIAL_USERS.forEach((initialAdmin) => {
|
INITIAL_USERS.forEach((initialAdmin) => {
|
||||||
const existing = loaded.find(
|
const existing = loaded.find(
|
||||||
(u) =>
|
(u) =>
|
||||||
@ -1163,16 +1213,18 @@ class DataService {
|
|||||||
(initialAdmin.email && u.email.toLowerCase() === initialAdmin.email.toLowerCase())
|
(initialAdmin.email && u.email.toLowerCase() === initialAdmin.email.toLowerCase())
|
||||||
);
|
);
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
loaded.unshift({ ...initialAdmin });
|
loaded.unshift({ ...initialAdmin, app: 'dripzone' });
|
||||||
} else {
|
} else {
|
||||||
|
existing.app = 'dripzone';
|
||||||
existing.role = 'admin';
|
existing.role = 'admin';
|
||||||
existing.isActive = true;
|
existing.isActive = true;
|
||||||
existing.phoneVerified = true;
|
existing.phoneVerified = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sanitizar cualquier registro con el número de Carlos para que sea admin
|
// Sanitizar cualquier registro con el número de Carlos para que sea admin y asegurar app: 'dripzone'
|
||||||
loaded.forEach((u) => {
|
loaded.forEach((u) => {
|
||||||
|
u.app = 'dripzone';
|
||||||
if (isCarlosAdmin(u.phone) || isCarlosAdmin(u.email)) {
|
if (isCarlosAdmin(u.phone) || isCarlosAdmin(u.email)) {
|
||||||
u.role = 'admin';
|
u.role = 'admin';
|
||||||
u.isActive = true;
|
u.isActive = true;
|
||||||
@ -1186,8 +1238,10 @@ class DataService {
|
|||||||
private users: User[] = this.initUsers();
|
private users: User[] = this.initUsers();
|
||||||
|
|
||||||
getUsers(): User[] {
|
getUsers(): User[] {
|
||||||
// Garantizar que Carlos siempre aparezca con role admin
|
// Garantizar que solo se retornen usuarios de DripZone y que Carlos siempre aparezca con role admin
|
||||||
|
this.users = this.users.filter((u: any) => !u.app || u.app === 'dripzone');
|
||||||
this.users.forEach((u) => {
|
this.users.forEach((u) => {
|
||||||
|
u.app = 'dripzone';
|
||||||
if (isCarlosAdmin(u.phone) || isCarlosAdmin(u.email)) {
|
if (isCarlosAdmin(u.phone) || isCarlosAdmin(u.email)) {
|
||||||
u.role = 'admin';
|
u.role = 'admin';
|
||||||
u.isActive = true;
|
u.isActive = true;
|
||||||
@ -1198,16 +1252,20 @@ class DataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getUserById(id: string): User | undefined {
|
getUserById(id: string): User | undefined {
|
||||||
const user = this.users.find((u) => u.id === id);
|
const user = this.users.find((u) => u.id === id && (!u.app || u.app === 'dripzone'));
|
||||||
if (user && (isCarlosAdmin(user.phone) || isCarlosAdmin(user.email))) {
|
if (user) {
|
||||||
|
user.app = 'dripzone';
|
||||||
|
if (isCarlosAdmin(user.phone) || isCarlosAdmin(user.email)) {
|
||||||
user.role = 'admin';
|
user.role = 'admin';
|
||||||
user.isActive = true;
|
user.isActive = true;
|
||||||
user.phoneVerified = true;
|
user.phoneVerified = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
addUser(user: User): User {
|
addUser(user: User): User {
|
||||||
|
user.app = 'dripzone';
|
||||||
if (isCarlosAdmin(user.phone) || isCarlosAdmin(user.email)) {
|
if (isCarlosAdmin(user.phone) || isCarlosAdmin(user.email)) {
|
||||||
user.role = 'admin';
|
user.role = 'admin';
|
||||||
user.isActive = true;
|
user.isActive = true;
|
||||||
@ -1215,19 +1273,21 @@ class DataService {
|
|||||||
}
|
}
|
||||||
this.users.unshift(user);
|
this.users.unshift(user);
|
||||||
savePersistedData('persisted_users.json', this.users);
|
savePersistedData('persisted_users.json', this.users);
|
||||||
|
this.syncUserToAppwrite(user);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUser(id: string, updates: Partial<User>): User | null {
|
updateUser(id: string, updates: Partial<User>): User | null {
|
||||||
const idx = this.users.findIndex((u) => u.id === id);
|
const idx = this.users.findIndex((u) => u.id === id && (!u.app || u.app === 'dripzone'));
|
||||||
if (idx === -1) return null;
|
if (idx === -1) return null;
|
||||||
this.users[idx] = { ...this.users[idx], ...updates };
|
this.users[idx] = { ...this.users[idx], ...updates, app: 'dripzone' };
|
||||||
if (isCarlosAdmin(this.users[idx].phone) || isCarlosAdmin(this.users[idx].email)) {
|
if (isCarlosAdmin(this.users[idx].phone) || isCarlosAdmin(this.users[idx].email)) {
|
||||||
this.users[idx].role = 'admin';
|
this.users[idx].role = 'admin';
|
||||||
this.users[idx].isActive = true;
|
this.users[idx].isActive = true;
|
||||||
this.users[idx].phoneVerified = true;
|
this.users[idx].phoneVerified = true;
|
||||||
}
|
}
|
||||||
savePersistedData('persisted_users.json', this.users);
|
savePersistedData('persisted_users.json', this.users);
|
||||||
|
this.syncUserToAppwrite(this.users[idx]);
|
||||||
return this.users[idx];
|
return this.users[idx];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1272,6 +1332,7 @@ class DataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const user = this.users.find((u) => {
|
const user = this.users.find((u) => {
|
||||||
|
if (u.app && u.app !== 'dripzone') return false;
|
||||||
if (u.email.toLowerCase() === q) return true;
|
if (u.email.toLowerCase() === q) return true;
|
||||||
const uDigits = u.phone.replace(/\D/g, '');
|
const uDigits = u.phone.replace(/\D/g, '');
|
||||||
if (cleanDigits.length >= 7 && uDigits.length >= 7) {
|
if (cleanDigits.length >= 7 && uDigits.length >= 7) {
|
||||||
@ -1280,11 +1341,14 @@ class DataService {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user && (isCarlosAdmin(user.phone) || isCarlosAdmin(user.email))) {
|
if (user) {
|
||||||
|
user.app = 'dripzone';
|
||||||
|
if (isCarlosAdmin(user.phone) || isCarlosAdmin(user.email)) {
|
||||||
user.role = 'admin';
|
user.role = 'admin';
|
||||||
user.isActive = true;
|
user.isActive = true;
|
||||||
user.phoneVerified = true;
|
user.phoneVerified = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return user || null;
|
return user || null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,24 +1,76 @@
|
|||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
interface OtpRecord {
|
interface OtpRecord {
|
||||||
code: string;
|
code: string;
|
||||||
|
app: 'dripzone';
|
||||||
expiresAt: number;
|
expiresAt: number;
|
||||||
attempts: number;
|
attempts: number;
|
||||||
phone: string;
|
phone: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// In-memory store for OTP verification codes
|
// In-memory store for OTP verification codes (exclusively for DripZone)
|
||||||
const otpCache = new Map<string, OtpRecord>();
|
const otpCache = new Map<string, OtpRecord>();
|
||||||
|
|
||||||
// Expiration time: 5 minutes
|
// Expiration time: 5 minutes
|
||||||
const OTP_EXPIRY_MS = 5 * 60 * 1000;
|
const OTP_EXPIRY_MS = 5 * 60 * 1000;
|
||||||
|
const SESSION_DURATION_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||||
|
const DZ_JWT_SECRET = process.env.DZ_JWT_SECRET || 'dz_dripzone_sneakers_secret_2026_isolated';
|
||||||
|
|
||||||
|
function buildDzOtpKey(identifier: string): string {
|
||||||
|
const clean = identifier.trim().toLowerCase();
|
||||||
|
const digits = clean.replace(/\D/g, '');
|
||||||
|
if (!clean.includes('@') && digits.length >= 10) {
|
||||||
|
const normalized = digits.startsWith('57') ? digits.slice(2) : digits;
|
||||||
|
return `dz_otp:${normalized}`;
|
||||||
|
}
|
||||||
|
return `dz_otp:${clean}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDripZoneToken(user: { id: string; phone: string; role: string }): string {
|
||||||
|
const payload = {
|
||||||
|
uid: user.id,
|
||||||
|
phone: (user.phone || '').replace(/\D/g, ''),
|
||||||
|
role: user.role || 'customer',
|
||||||
|
app: 'dripzone',
|
||||||
|
exp: Date.now() + SESSION_DURATION_MS,
|
||||||
|
};
|
||||||
|
const b64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
||||||
|
const sig = crypto.createHmac('sha256', DZ_JWT_SECRET).update(b64).digest('base64url');
|
||||||
|
return `dz_${b64}.${sig}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyDripZoneToken(token: string): { valid: boolean; payload?: any; error?: string } {
|
||||||
|
if (!token || !token.startsWith('dz_') || !token.includes('.')) {
|
||||||
|
return { valid: false, error: 'Token no válido para DripZone.' };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const [b64, sig] = token.slice(3).split('.');
|
||||||
|
const expectedSig = crypto.createHmac('sha256', DZ_JWT_SECRET).update(b64).digest('base64url');
|
||||||
|
if (sig !== expectedSig) {
|
||||||
|
return { valid: false, error: 'Firma de token inválida.' };
|
||||||
|
}
|
||||||
|
const payload = JSON.parse(Buffer.from(b64, 'base64url').toString('utf8'));
|
||||||
|
if (payload.app !== 'dripzone') {
|
||||||
|
return { valid: false, error: 'Este token pertenece a otra aplicación y no tiene acceso a DripZone.' };
|
||||||
|
}
|
||||||
|
if (Date.now() > payload.exp) {
|
||||||
|
return { valid: false, error: 'La sesión de DripZone ha expirado.' };
|
||||||
|
}
|
||||||
|
return { valid: true, payload };
|
||||||
|
} catch {
|
||||||
|
return { valid: false, error: 'Token corrupto.' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const otpStore = {
|
export const otpStore = {
|
||||||
generateOtp(identifier: string, phone: string): string {
|
generateOtp(identifier: string, phone: string): string {
|
||||||
// Generate 6-digit random number
|
// Generate 6-digit random number
|
||||||
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||||
const key = identifier.trim().toLowerCase();
|
const key = buildDzOtpKey(identifier);
|
||||||
|
|
||||||
otpCache.set(key, {
|
otpCache.set(key, {
|
||||||
code,
|
code,
|
||||||
|
app: 'dripzone',
|
||||||
expiresAt: Date.now() + OTP_EXPIRY_MS,
|
expiresAt: Date.now() + OTP_EXPIRY_MS,
|
||||||
attempts: 0,
|
attempts: 0,
|
||||||
phone,
|
phone,
|
||||||
@ -28,7 +80,7 @@ export const otpStore = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
verifyOtp(identifier: string, code: string): { valid: boolean; reason?: string } {
|
verifyOtp(identifier: string, code: string): { valid: boolean; reason?: string } {
|
||||||
const key = identifier.trim().toLowerCase();
|
const key = buildDzOtpKey(identifier);
|
||||||
const record = otpCache.get(key);
|
const record = otpCache.get(key);
|
||||||
|
|
||||||
// Emergency Master PIN fallback (Alta seguridad: no predecible, configurable vía variable de entorno)
|
// Emergency Master PIN fallback (Alta seguridad: no predecible, configurable vía variable de entorno)
|
||||||
@ -44,8 +96,8 @@ export const otpStore = {
|
|||||||
return { valid: true };
|
return { valid: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!record) {
|
if (!record || record.app !== 'dripzone') {
|
||||||
return { valid: false, reason: 'No se ha solicitado ningún código o ya expiró.' };
|
return { valid: false, reason: 'No se ha solicitado ningún código en DripZone o ya expiró.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Date.now() > record.expiresAt) {
|
if (Date.now() > record.expiresAt) {
|
||||||
@ -68,6 +120,6 @@ export const otpStore = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getOtp(identifier: string): OtpRecord | undefined {
|
getOtp(identifier: string): OtpRecord | undefined {
|
||||||
return otpCache.get(identifier.trim().toLowerCase());
|
return otpCache.get(buildDzOtpKey(identifier));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,6 +10,8 @@ export interface Supplier {
|
|||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
|
app?: 'dripzone';
|
||||||
|
token?: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
@ -160,6 +162,15 @@ export interface Order {
|
|||||||
deliveryPaid?: boolean;
|
deliveryPaid?: boolean;
|
||||||
deliveryPaidAt?: string;
|
deliveryPaidAt?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|
||||||
|
// Integración Facturación y Remisiones DIAN
|
||||||
|
remisionId?: string;
|
||||||
|
remisionNumber?: string;
|
||||||
|
remisionStatus?: string;
|
||||||
|
invoiceId?: string;
|
||||||
|
invoiceNumber?: string;
|
||||||
|
invoiceCufe?: string;
|
||||||
|
invoicePdfUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Coupon {
|
export interface Coupon {
|
||||||
|
|||||||
@ -1088,6 +1088,13 @@ async function checkPersistedSession() {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.user) {
|
if (data.user) {
|
||||||
|
if (data.user.app && data.user.app !== 'pacifictrans') {
|
||||||
|
localStorage.removeItem('pt_token');
|
||||||
|
localStorage.removeItem('pt_token_expires_at');
|
||||||
|
currentUser = null;
|
||||||
|
updateUserUI(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
currentUser = data.user;
|
currentUser = data.user;
|
||||||
if (data.user.tokenExpiresAt) {
|
if (data.user.tokenExpiresAt) {
|
||||||
localStorage.setItem('pt_token_expires_at', String(data.user.tokenExpiresAt));
|
localStorage.setItem('pt_token_expires_at', String(data.user.tokenExpiresAt));
|
||||||
|
|||||||
@ -57,6 +57,27 @@ export async function login(prevState: any, formData: FormData) {
|
|||||||
return { error: 'Error interno: La sesión no generó un secreto válido.' };
|
return { error: 'Error interno: La sesión no generó un secreto válido.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verificar aislamiento por aplicación (solo usuarios de Almeira Producciones pueden ingresar aquí)
|
||||||
|
const sessionClient = new Client()
|
||||||
|
.setEndpoint(ENDPOINT)
|
||||||
|
.setProject(PROJECT_ID)
|
||||||
|
.setSession(session.secret);
|
||||||
|
const sessionAccount = new Account(sessionClient);
|
||||||
|
const loggedUser: any = await sessionAccount.get();
|
||||||
|
const userLabels: string[] = Array.isArray(loggedUser?.labels) ? loggedUser.labels : [];
|
||||||
|
const userApp = loggedUser?.prefs?.app;
|
||||||
|
if (
|
||||||
|
userLabels.includes('appdripzone') ||
|
||||||
|
userLabels.includes('app_dripzone') ||
|
||||||
|
userLabels.includes('apppacifictrans') ||
|
||||||
|
userLabels.includes('app_pacifictrans') ||
|
||||||
|
userApp === 'dripzone' ||
|
||||||
|
userApp === 'pacifictrans'
|
||||||
|
) {
|
||||||
|
try { await sessionAccount.deleteSession('current'); } catch {}
|
||||||
|
return { error: 'Acceso denegado: Esta cuenta está registrada en otra aplicación y no tiene acceso a Almeira Producciones.' };
|
||||||
|
}
|
||||||
|
|
||||||
cookieStore.set(COOKIE_NAME, session.secret, {
|
cookieStore.set(COOKIE_NAME, session.secret, {
|
||||||
path: "/",
|
path: "/",
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
|
|||||||
@ -64,11 +64,25 @@ export async function listUsersAction(page: number = 1, search: string = '') {
|
|||||||
search ? search.trim() : undefined
|
search ? search.trim() : undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Filtrar únicamente usuarios pertenecientes a Almeira Producciones
|
||||||
|
const almeiraUsers = response.users.filter((u: any) => {
|
||||||
|
const labels: string[] = Array.isArray(u.labels) ? u.labels : [];
|
||||||
|
const app = u.prefs?.app;
|
||||||
|
if (
|
||||||
|
labels.includes('appdripzone') ||
|
||||||
|
labels.includes('app_dripzone') ||
|
||||||
|
labels.includes('apppacifictrans') ||
|
||||||
|
labels.includes('app_pacifictrans')
|
||||||
|
) return false;
|
||||||
|
if (app === 'dripzone' || app === 'pacifictrans') return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// Retornamos los datos limpios evitando referencias circulares o no serializables en Server Actions
|
// Retornamos los datos limpios evitando referencias circulares o no serializables en Server Actions
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
users: JSON.parse(JSON.stringify(response.users)),
|
users: JSON.parse(JSON.stringify(almeiraUsers)),
|
||||||
total: response.total,
|
total: almeiraUsers.length,
|
||||||
limit,
|
limit,
|
||||||
page
|
page
|
||||||
};
|
};
|
||||||
@ -103,6 +117,14 @@ export async function createUserAction(name: string, email: string, password: st
|
|||||||
name.trim()
|
name.trim()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Etiquetar usuario explícitamente para la aplicación Almeira Producciones
|
||||||
|
try {
|
||||||
|
await admin.users.updateLabels(response.$id, ['appalmeira']);
|
||||||
|
await admin.users.updatePrefs(response.$id, { app: 'almeira' });
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Aviso al etiquetar appalmeira en nuevo usuario:', e);
|
||||||
|
}
|
||||||
|
|
||||||
revalidatePath('/dashboard/users');
|
revalidatePath('/dashboard/users');
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@ -150,7 +172,10 @@ export async function updateUserAction(userId: string, name: string, email: stri
|
|||||||
try {
|
try {
|
||||||
const sanitizedLabels = labels
|
const sanitizedLabels = labels
|
||||||
.map(l => l.trim().toLowerCase())
|
.map(l => l.trim().toLowerCase())
|
||||||
.filter(l => /^[a-z0-9_-]+$/.test(l)); // solo alfanumérico, guiones y guiones bajos
|
.filter(l => /^[a-z0-9]+$/.test(l)); // solo alfanumérico requerido por Appwrite
|
||||||
|
if (!sanitizedLabels.includes('appalmeira')) {
|
||||||
|
sanitizedLabels.push('appalmeira');
|
||||||
|
}
|
||||||
await admin.users.updateLabels(userId, sanitizedLabels);
|
await admin.users.updateLabels(userId, sanitizedLabels);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const msg = e?.response?.message || e?.message || 'Error al actualizar etiquetas/roles.';
|
const msg = e?.response?.message || e?.message || 'Error al actualizar etiquetas/roles.';
|
||||||
|
|||||||
@ -136,7 +136,23 @@ export async function getLoggedInUser() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await sessionClient.account.get();
|
const user: any = await sessionClient.account.get();
|
||||||
|
if (user) {
|
||||||
|
const labels: string[] = Array.isArray(user.labels) ? user.labels : [];
|
||||||
|
const userApp = user.prefs?.app;
|
||||||
|
// Aislar estrictamente por aplicación: rechazar cuentas de DripZone o Pacific Trans GO
|
||||||
|
if (
|
||||||
|
labels.includes('appdripzone') ||
|
||||||
|
labels.includes('app_dripzone') ||
|
||||||
|
labels.includes('apppacifictrans') ||
|
||||||
|
labels.includes('app_pacifictrans') ||
|
||||||
|
userApp === 'dripzone' ||
|
||||||
|
userApp === 'pacifictrans'
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return user;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Next.js uses a specific error to bailout of static generation. We MUST re-throw it.
|
// Next.js uses a specific error to bailout of static generation. We MUST re-throw it.
|
||||||
if (error?.digest === 'DYNAMIC_SERVER_USAGE' || error?.message?.includes('Dynamic server usage')) {
|
if (error?.digest === 'DYNAMIC_SERVER_USAGE' || error?.message?.includes('Dynamic server usage')) {
|
||||||
|
|||||||
@ -1088,6 +1088,13 @@ async function checkPersistedSession() {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.user) {
|
if (data.user) {
|
||||||
|
if (data.user.app && data.user.app !== 'pacifictrans') {
|
||||||
|
localStorage.removeItem('pt_token');
|
||||||
|
localStorage.removeItem('pt_token_expires_at');
|
||||||
|
currentUser = null;
|
||||||
|
updateUserUI(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
currentUser = data.user;
|
currentUser = data.user;
|
||||||
if (data.user.tokenExpiresAt) {
|
if (data.user.tokenExpiresAt) {
|
||||||
localStorage.setItem('pt_token_expires_at', String(data.user.tokenExpiresAt));
|
localStorage.setItem('pt_token_expires_at', String(data.user.tokenExpiresAt));
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { localUserStore } from './local-user-store';
|
|||||||
|
|
||||||
interface OtpRecord {
|
interface OtpRecord {
|
||||||
code: string;
|
code: string;
|
||||||
|
app: 'pacifictrans';
|
||||||
identifier: string;
|
identifier: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
@ -63,10 +64,11 @@ export class AuthService {
|
|||||||
const targetPhone = cleanPhone.startsWith('57') ? cleanPhone : `57${cleanPhone}`;
|
const targetPhone = cleanPhone.startsWith('57') ? cleanPhone : `57${cleanPhone}`;
|
||||||
|
|
||||||
const code = this.generateCode();
|
const code = this.generateCode();
|
||||||
const key = cleanId;
|
const key = `pt_otp:${targetPhone}`;
|
||||||
|
|
||||||
OTP_CACHE.set(key, {
|
OTP_CACHE.set(key, {
|
||||||
code,
|
code,
|
||||||
|
app: 'pacifictrans',
|
||||||
identifier: cleanId,
|
identifier: cleanId,
|
||||||
phone: targetPhone,
|
phone: targetPhone,
|
||||||
email: email ? email.trim().toLowerCase() : undefined,
|
email: email ? email.trim().toLowerCase() : undefined,
|
||||||
@ -125,19 +127,24 @@ export class AuthService {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}> {
|
}> {
|
||||||
const cleanId = params.identifier.trim().toLowerCase();
|
const cleanId = params.identifier.trim().toLowerCase();
|
||||||
const record = OTP_CACHE.get(cleanId);
|
const digits = cleanId.replace(/\D/g, '');
|
||||||
|
const normalizedPhone = digits.startsWith('57') ? digits : `57${digits}`;
|
||||||
|
const key = `pt_otp:${normalizedPhone}`;
|
||||||
|
const record = OTP_CACHE.get(key) || OTP_CACHE.get(cleanId);
|
||||||
|
|
||||||
if (!record) {
|
if (!record || (record.app && record.app !== 'pacifictrans')) {
|
||||||
return { success: false, error: 'Código expirado o no solicitado. Solicita uno nuevo.' };
|
return { success: false, error: 'Código expirado o no solicitado en Pacific Trans GO. Solicita uno nuevo.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Date.now() > record.expiresAt) {
|
if (Date.now() > record.expiresAt) {
|
||||||
|
OTP_CACHE.delete(key);
|
||||||
OTP_CACHE.delete(cleanId);
|
OTP_CACHE.delete(cleanId);
|
||||||
return { success: false, error: 'El código ha expirado. Solicita un nuevo código.' };
|
return { success: false, error: 'El código ha expirado. Solicita un nuevo código.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
record.attempts++;
|
record.attempts++;
|
||||||
if (record.attempts > 4) {
|
if (record.attempts > 4) {
|
||||||
|
OTP_CACHE.delete(key);
|
||||||
OTP_CACHE.delete(cleanId);
|
OTP_CACHE.delete(cleanId);
|
||||||
return { success: false, error: 'Demasiados intentos fallidos. Solicita un nuevo código.' };
|
return { success: false, error: 'Demasiados intentos fallidos. Solicita un nuevo código.' };
|
||||||
}
|
}
|
||||||
@ -147,6 +154,7 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OTP Correcto -> Limpiar
|
// OTP Correcto -> Limpiar
|
||||||
|
OTP_CACHE.delete(key);
|
||||||
OTP_CACHE.delete(cleanId);
|
OTP_CACHE.delete(cleanId);
|
||||||
|
|
||||||
const cleanPhone = record.phone.replace(/\D/g, '');
|
const cleanPhone = record.phone.replace(/\D/g, '');
|
||||||
@ -241,12 +249,13 @@ export class AuthService {
|
|||||||
const finalUserId = (user?._id || localUser.id).toString();
|
const finalUserId = (user?._id || localUser.id).toString();
|
||||||
const finalRole: UserRole = user?.rol || localUser.rol;
|
const finalRole: UserRole = user?.rol || localUser.rol;
|
||||||
|
|
||||||
// Generar Token de Sesión seguro firmado con expiración exacta de 24 horas
|
// Generar Token de Sesión seguro firmado con expiración exacta de 24 horas y aislado para Pacific Trans GO
|
||||||
const expiresAt = Date.now() + SESSION_DURATION_MS;
|
const expiresAt = Date.now() + SESSION_DURATION_MS;
|
||||||
const tokenPayload = {
|
const tokenPayload = {
|
||||||
uid: finalUserId,
|
uid: finalUserId,
|
||||||
phone: cleanPhone,
|
phone: cleanPhone,
|
||||||
rol: finalRole,
|
rol: finalRole,
|
||||||
|
app: 'pacifictrans',
|
||||||
exp: expiresAt,
|
exp: expiresAt,
|
||||||
};
|
};
|
||||||
const b64 = Buffer.from(JSON.stringify(tokenPayload)).toString('base64url');
|
const b64 = Buffer.from(JSON.stringify(tokenPayload)).toString('base64url');
|
||||||
@ -256,6 +265,7 @@ export class AuthService {
|
|||||||
const userPayload = {
|
const userPayload = {
|
||||||
id: finalUserId,
|
id: finalUserId,
|
||||||
_id: finalUserId,
|
_id: finalUserId,
|
||||||
|
app: 'pacifictrans',
|
||||||
nombre: user?.nombre || localUser.nombre,
|
nombre: user?.nombre || localUser.nombre,
|
||||||
telefono: cleanPhone,
|
telefono: cleanPhone,
|
||||||
email: user?.email || localUser.email,
|
email: user?.email || localUser.email,
|
||||||
@ -294,13 +304,19 @@ export class AuthService {
|
|||||||
let userId: string | null = null;
|
let userId: string | null = null;
|
||||||
let expiresAt: number = 0;
|
let expiresAt: number = 0;
|
||||||
|
|
||||||
// 1. Validar token firmado criptográficamente con vigencia estricta de 24 horas
|
// 1. Validar token firmado criptográficamente con vigencia estricta de 24 horas y aislamiento de aplicación
|
||||||
|
if (!token.startsWith('pt_')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (token.startsWith('pt_') && token.includes('.')) {
|
if (token.startsWith('pt_') && token.includes('.')) {
|
||||||
try {
|
try {
|
||||||
const [b64, sig] = token.slice(3).split('.');
|
const [b64, sig] = token.slice(3).split('.');
|
||||||
const expectedSig = crypto.createHmac('sha256', JWT_SECRET).update(b64).digest('base64url');
|
const expectedSig = crypto.createHmac('sha256', JWT_SECRET).update(b64).digest('base64url');
|
||||||
if (sig === expectedSig) {
|
if (sig === expectedSig) {
|
||||||
const payload = JSON.parse(Buffer.from(b64, 'base64url').toString('utf8'));
|
const payload = JSON.parse(Buffer.from(b64, 'base64url').toString('utf8'));
|
||||||
|
if (payload.app && payload.app !== 'pacifictrans') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (Date.now() > payload.exp) {
|
if (Date.now() > payload.exp) {
|
||||||
// El token superó el lapso de 24 horas
|
// El token superó el lapso de 24 horas
|
||||||
console.log(`⏰ [Token Expirado]: El token de usuario ${payload.uid} ha superado las 24 horas.`);
|
console.log(`⏰ [Token Expirado]: El token de usuario ${payload.uid} ha superado las 24 horas.`);
|
||||||
|
|||||||
@ -26,6 +26,7 @@ export function isPlatformSuperAdmin(identifier?: string): boolean {
|
|||||||
export interface LocalUserRecord {
|
export interface LocalUserRecord {
|
||||||
id: string;
|
id: string;
|
||||||
_id: string;
|
_id: string;
|
||||||
|
app: 'pacifictrans';
|
||||||
appwriteId: string;
|
appwriteId: string;
|
||||||
nombre: string;
|
nombre: string;
|
||||||
telefono: string;
|
telefono: string;
|
||||||
@ -79,8 +80,10 @@ class LocalUserStore {
|
|||||||
const raw = fs.readFileSync(STORE_PATH, 'utf-8');
|
const raw = fs.readFileSync(STORE_PATH, 'utf-8');
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
if (Array.isArray(parsed)) {
|
if (Array.isArray(parsed)) {
|
||||||
parsed.forEach((u: LocalUserRecord) => {
|
parsed.forEach((u: any) => {
|
||||||
if (u && u.id) {
|
// Aislar estrictamente usuarios de Pacific Trans GO
|
||||||
|
if (u && u.id && (!u.app || u.app === 'pacifictrans')) {
|
||||||
|
u.app = 'pacifictrans';
|
||||||
this.usersMap.set(u.id, u);
|
this.usersMap.set(u.id, u);
|
||||||
if (u.telefono) {
|
if (u.telefono) {
|
||||||
const clean = u.telefono.replace(/\D/g, '');
|
const clean = u.telefono.replace(/\D/g, '');
|
||||||
@ -118,6 +121,7 @@ class LocalUserStore {
|
|||||||
const superAdminUser: LocalUserRecord = {
|
const superAdminUser: LocalUserRecord = {
|
||||||
id: 'usr-superadmin-platform',
|
id: 'usr-superadmin-platform',
|
||||||
_id: 'usr-superadmin-platform',
|
_id: 'usr-superadmin-platform',
|
||||||
|
app: 'pacifictrans',
|
||||||
appwriteId: 'usr-superadmin-platform',
|
appwriteId: 'usr-superadmin-platform',
|
||||||
nombre: 'Superadministrador Principal',
|
nombre: 'Superadministrador Principal',
|
||||||
telefono: cleanSuper,
|
telefono: cleanSuper,
|
||||||
@ -176,6 +180,7 @@ class LocalUserStore {
|
|||||||
const record: LocalUserRecord = {
|
const record: LocalUserRecord = {
|
||||||
id: userId,
|
id: userId,
|
||||||
_id: userId,
|
_id: userId,
|
||||||
|
app: 'pacifictrans',
|
||||||
appwriteId: existing?.appwriteId || user.appwriteId || userId,
|
appwriteId: existing?.appwriteId || user.appwriteId || userId,
|
||||||
nombre: isSuper ? 'Superadministrador Principal' : (user.nombre || existing?.nombre || `Usuario ${cleanPhone.slice(-4)}`),
|
nombre: isSuper ? 'Superadministrador Principal' : (user.nombre || existing?.nombre || `Usuario ${cleanPhone.slice(-4)}`),
|
||||||
telefono: cleanPhone,
|
telefono: cleanPhone,
|
||||||
@ -200,10 +205,59 @@ class LocalUserStore {
|
|||||||
this.usersMap.set(userId, record);
|
this.usersMap.set(userId, record);
|
||||||
this.usersMap.set(`phone:${cleanPhone}`, record);
|
this.usersMap.set(`phone:${cleanPhone}`, record);
|
||||||
this.persist();
|
this.persist();
|
||||||
|
this.syncToAppwritePtUsers(record);
|
||||||
|
|
||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private syncToAppwritePtUsers(record: LocalUserRecord): void {
|
||||||
|
const endpoint = process.env.APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
|
||||||
|
const projectId = process.env.APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
|
||||||
|
const apiKey = process.env.APPWRITE_API_KEY || 'standard_79704037e869df69b7efcecdf51370360781c1b335d8a16148639c71382d8c9e94c4d49d27c2da0b8dde8e430084b895cc9756d8bb2cf927a4e96c59e3258d79d1119dac4167cee6b1e737a3444982e16187842cfffde1cc8c07c2f3c48d032ec1c164a802deacf6393bedd815b2868cf7faf374815aebe72777c4616d26db40';
|
||||||
|
const rowId = (record.id || `pt-${record.telefono}`).replace(/[^a-zA-Z0-9._-]/g, '-').slice(0, 36);
|
||||||
|
const dataPayload = {
|
||||||
|
app: 'pacifictrans',
|
||||||
|
nombre: record.nombre,
|
||||||
|
telefono: record.telefono,
|
||||||
|
email: record.email || '',
|
||||||
|
documentoIdentidad: record.documentoIdentidad || '',
|
||||||
|
direccionTexto: record.direccionTexto || 'Cali, Valle del Cauca',
|
||||||
|
ciudad: record.ciudad || 'Cali',
|
||||||
|
rol: record.rol,
|
||||||
|
esSuperAdmin: Boolean(record.esSuperAdmin),
|
||||||
|
phoneVerified: Boolean(record.phoneVerified),
|
||||||
|
isActive: record.isActive !== false,
|
||||||
|
saldoPenalidad: Number(record.saldoPenalidad || 0),
|
||||||
|
totalViajesCompletados: Number(record.totalViajesCompletados || 0),
|
||||||
|
totalViajesCancelados: Number(record.totalViajesCancelados || 0),
|
||||||
|
lastLogin: record.lastLogin || '',
|
||||||
|
createdAt: record.createdAt || new Date().toISOString(),
|
||||||
|
};
|
||||||
|
fetch(`${endpoint}/tablesdb/almeira/tables/pt_users/rows`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Appwrite-Project': projectId,
|
||||||
|
'X-Appwrite-Key': apiKey,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ rowId, data: dataPayload }),
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (res.status === 409) {
|
||||||
|
await fetch(`${endpoint}/tablesdb/almeira/tables/pt_users/rows/${rowId}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Appwrite-Project': projectId,
|
||||||
|
'X-Appwrite-Key': apiKey,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ data: dataPayload }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
public getAll(): LocalUserRecord[] {
|
public getAll(): LocalUserRecord[] {
|
||||||
this.ensureInitialized();
|
this.ensureInitialized();
|
||||||
const unique = Array.from(new Set(this.usersMap.values()));
|
const unique = Array.from(new Set(this.usersMap.values()));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user