Adm global
This commit is contained in:
@@ -92,7 +92,7 @@ async def get_current_user(
|
|||||||
# Roles globales (is_global) pueden operar en cualquier tenant → omitir chequeo.
|
# Roles globales (is_global) pueden operar en cualquier tenant → omitir chequeo.
|
||||||
# Roles de cliente (is_client) deben coincidir con su propio tenant.
|
# Roles de cliente (is_client) deben coincidir con su propio tenant.
|
||||||
request_tenant_id = getattr(getattr(request, "state", None), "tenant_id", None)
|
request_tenant_id = getattr(getattr(request, "state", None), "tenant_id", None)
|
||||||
if request_tenant_id and user.role.is_client and str(user.tenant_id) != str(request_tenant_id):
|
if request_tenant_id and not user.role.is_global and str(user.tenant_id) != str(request_tenant_id):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Tenant header does not match authenticated user",
|
detail="Tenant header does not match authenticated user",
|
||||||
|
|||||||
@@ -481,6 +481,7 @@ async def get_current_user(
|
|||||||
"role": user.role.value if hasattr(user.role, 'value') else user.role,
|
"role": user.role.value if hasattr(user.role, 'value') else user.role,
|
||||||
"tenant_id": str(user.tenant_id),
|
"tenant_id": str(user.tenant_id),
|
||||||
"tenant_name": user.tenant.name if user.tenant else None,
|
"tenant_name": user.tenant.name if user.tenant else None,
|
||||||
|
"tenant_slug": user.tenant.slug if user.tenant else None,
|
||||||
"is_active": user.is_active,
|
"is_active": user.is_active,
|
||||||
"is_two_factor_enabled": user.totp_secret is not None,
|
"is_two_factor_enabled": user.totp_secret is not None,
|
||||||
"last_login": user.last_login.isoformat() if user.last_login else None,
|
"last_login": user.last_login.isoformat() if user.last_login else None,
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ async def create_tenant(
|
|||||||
if result.scalar_one_or_none():
|
if result.scalar_one_or_none():
|
||||||
raise HTTPException(status_code=400, detail="Tenant slug already exists")
|
raise HTTPException(status_code=400, detail="Tenant slug already exists")
|
||||||
|
|
||||||
db_tenant = Tenant(**tenant.model_dump())
|
data = tenant.model_dump()
|
||||||
|
data['slug'] = data['slug'].lower().strip()
|
||||||
|
db_tenant = Tenant(**data)
|
||||||
db.add(db_tenant)
|
db.add(db_tenant)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(db_tenant)
|
await db.refresh(db_tenant)
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
import type { Writable } from 'svelte/store';
|
|
||||||
import { writable } from 'svelte/store';
|
|
||||||
export interface User {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
first_name: string;
|
|
||||||
last_name: string;
|
|
||||||
tenant_id: string;
|
|
||||||
role: 'CLIENT_ADMIN' | 'CLIENT_USER';
|
|
||||||
is_active: boolean;
|
|
||||||
is_two_factor_enabled: boolean;
|
|
||||||
created_at: string;
|
|
||||||
}
|
|
||||||
export interface AuthState {
|
|
||||||
user: User | null;
|
|
||||||
token: string | null;
|
|
||||||
isAuthenticated: boolean;
|
|
||||||
isLoading: boolean;
|
|
||||||
}
|
|
||||||
export interface LoginRequest {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
tenant_slug: string;
|
|
||||||
totp_code?: string;
|
|
||||||
}
|
|
||||||
export interface LoginResponse {
|
|
||||||
access_token: string;
|
|
||||||
token_type: string;
|
|
||||||
expires_in: number;
|
|
||||||
user: User;
|
|
||||||
}
|
|
||||||
const initialState: AuthState = {
|
|
||||||
user: null,
|
|
||||||
token: null,
|
|
||||||
isAuthenticated: false,
|
|
||||||
isLoading: false
|
|
||||||
};
|
|
||||||
function createAuthStore() {
|
|
||||||
const { subscribe, set, update }: Writable<AuthState> = writable(initialState);
|
|
||||||
let _state = initialState;
|
|
||||||
subscribe(s => { _state = s; });
|
|
||||||
return {
|
|
||||||
subscribe,
|
|
||||||
init: async () => {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/v1/auth/me', {
|
|
||||||
credentials: 'include',
|
|
||||||
headers: { 'X-App': 'client', 'X-Tenant-Slug': 'aduanasoft' }
|
|
||||||
});
|
|
||||||
if (response.ok) {
|
|
||||||
const user = await response.json();
|
|
||||||
set({ user, token: null, isAuthenticated: true, isLoading: false });
|
|
||||||
}
|
|
||||||
} catch (error) {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
login: async (credentials: LoginRequest): Promise<void> => {
|
|
||||||
update(state => ({ ...state, isLoading: true }));
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/v1/auth/login', {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Tenant-Slug': credentials.tenant_slug,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(credentials)
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json();
|
|
||||||
throw new Error(error.detail || 'Login failed');
|
|
||||||
}
|
|
||||||
const data: LoginResponse = await response.json();
|
|
||||||
set({ user: data.user, token: data.access_token, isAuthenticated: true, isLoading: false });
|
|
||||||
} catch (error) {
|
|
||||||
update(state => ({ ...state, isLoading: false }));
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
logout: async () => {
|
|
||||||
try {
|
|
||||||
const token = _state.token;
|
|
||||||
await fetch('/api/v1/auth/logout', {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
|
||||||
'X-App': 'client',
|
|
||||||
'X-Tenant-Slug': 'aduanasoft',
|
|
||||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch {}
|
|
||||||
set(initialState);
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.location.href = '/login';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateUser: (user: User) => { update(state => ({ ...state, user })); },
|
|
||||||
setUser: (user: User) => { set({ user, token: null, isAuthenticated: true, isLoading: false }); },
|
|
||||||
setLoading: (isLoading: boolean) => { update(state => ({ ...state, isLoading })); }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export const auth = createAuthStore();
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
with open("/app/app/api/v1/endpoints/users.py", "r") as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if "tenant_id == current_user.tenant_id" in line:
|
|
||||||
print(f"Linea {i+1}: {line.rstrip()}")
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from app.core.database import AsyncSessionLocal
|
|
||||||
from app.models.user import User
|
|
||||||
from sqlalchemy import select
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
async def check():
|
|
||||||
async with AsyncSessionLocal() as db:
|
|
||||||
result = await db.execute(select(User))
|
|
||||||
users = result.scalars().all()
|
|
||||||
for u in users:
|
|
||||||
print(f"ID: {u.id} | Email: {u.email} | Tenant: {u.tenant_id} | Rol: {u.role}")
|
|
||||||
|
|
||||||
asyncio.run(check())
|
|
||||||
19
backend/debug_tenant.py
Normal file
19
backend/debug_tenant.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import asyncio
|
||||||
|
from app.core.database import AsyncSessionLocal
|
||||||
|
from app.models.user import User
|
||||||
|
from app.models.tenant import Tenant
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
async def check():
|
||||||
|
async with AsyncSessionLocal() as db:
|
||||||
|
result = await db.execute(
|
||||||
|
select(User, Tenant).join(Tenant).where(User.email == 'javier@ventas.com')
|
||||||
|
)
|
||||||
|
user, tenant = result.one()
|
||||||
|
print(f"user.tenant_id: {user.tenant_id}")
|
||||||
|
print(f"tenant.id: {tenant.id}")
|
||||||
|
print(f"tenant.slug: {tenant.slug}")
|
||||||
|
print(f"user.role: {user.role}")
|
||||||
|
print(f"role.is_client: {user.role.is_client}")
|
||||||
|
|
||||||
|
asyncio.run(check())
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
with open("/app/app/api/v1/endpoints/auth.py", "r") as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
# Linea 286 (0-indexed 285): "tenant_id": str(user.tenant_id),
|
|
||||||
# Agregar tenant_slug despues de tenant_id
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if '"tenant_id": str(user.tenant_id),' in line:
|
|
||||||
indent = " "
|
|
||||||
new_line = indent + '"tenant_slug": tenant.slug if tenant else str(user.tenant_id),\n'
|
|
||||||
lines.insert(i + 1, new_line)
|
|
||||||
print(f"OK: tenant_slug agregado en linea {i+2}")
|
|
||||||
break
|
|
||||||
|
|
||||||
with open("/app/app/api/v1/endpoints/auth.py", "w") as f:
|
|
||||||
f.writelines(lines)
|
|
||||||
print("Listo")
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
with open("/app/app/api/v1/endpoints/users.py") as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
new_block = [
|
|
||||||
" if current_user.role.value == 'ADMIN':\n",
|
|
||||||
" query = select(User).where(User.id == user_id)\n",
|
|
||||||
" else:\n",
|
|
||||||
" query = select(User).where(\n",
|
|
||||||
" User.id == user_id,\n",
|
|
||||||
" User.tenant_id == current_user.tenant_id\n",
|
|
||||||
" )\n",
|
|
||||||
]
|
|
||||||
|
|
||||||
lines[150:161] = new_block
|
|
||||||
|
|
||||||
with open("/app/app/api/v1/endpoints/users.py", "w") as f:
|
|
||||||
f.writelines(lines)
|
|
||||||
print("Listo")
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
with open("/app/app/api/v1/endpoints/users.py", "r") as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
old1 = " User.tenant_id == current_user.tenant_id # " + "\u2705" + " Seguridad multi-tenant"
|
|
||||||
new1 = """ from app.models.user import UserRole as _UserRole
|
|
||||||
if current_user.role == _UserRole.ADMIN:
|
|
||||||
query = select(User).where(User.id == user_id)
|
|
||||||
else:
|
|
||||||
query = select(User).where(
|
|
||||||
User.id == user_id,
|
|
||||||
User.tenant_id == current_user.tenant_id
|
|
||||||
)"""
|
|
||||||
|
|
||||||
if old1 in content:
|
|
||||||
content = content.replace(old1, new1)
|
|
||||||
print("OK bloque 1")
|
|
||||||
else:
|
|
||||||
print("SKIP bloque 1 - buscando alternativa")
|
|
||||||
old1b = " User.tenant_id == current_user.tenant_id\n )\n result = await db.execute(query)\n if not user:"
|
|
||||||
new1b = " User.tenant_id == current_user.tenant_id\n )\n result = await db.execute(query)\n if not user:"
|
|
||||||
print("Lineas con tenant_id encontradas:")
|
|
||||||
for i, line in enumerate(content.split("\n")):
|
|
||||||
if "tenant_id == current_user.tenant_id" in line:
|
|
||||||
print(f" Linea {i}: {line}")
|
|
||||||
|
|
||||||
with open("/app/app/api/v1/endpoints/users.py", "w") as f:
|
|
||||||
f.write(content)
|
|
||||||
print("Listo")
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
with open("/app/app/api/v1/endpoints/users.py", "r") as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
from app.models.user import UserRole as _UserRole
|
|
||||||
|
|
||||||
# Reemplazar el patron comun de query con filtro de tenant
|
|
||||||
# por una version que permite a ADMIN ver todos los tenants
|
|
||||||
|
|
||||||
old_get_user = """ query = select(User).where(
|
|
||||||
User.id == user_id,
|
|
||||||
User.tenant_id == current_user.tenant_id
|
|
||||||
)
|
|
||||||
result = await db.execute(query)
|
|
||||||
user = result.scalar_one_or_none()
|
|
||||||
if not user:"""
|
|
||||||
|
|
||||||
new_get_user = """ if current_user.role.value == "ADMIN":
|
|
||||||
query = select(User).where(User.id == user_id)
|
|
||||||
else:
|
|
||||||
query = select(User).where(
|
|
||||||
User.id == user_id,
|
|
||||||
User.tenant_id == current_user.tenant_id
|
|
||||||
)
|
|
||||||
result = await db.execute(query)
|
|
||||||
user = result.scalar_one_or_none()
|
|
||||||
if not user:"""
|
|
||||||
|
|
||||||
old_update_user = """ query = select(User).where(
|
|
||||||
User.id == user_id,
|
|
||||||
User.tenant_id == current_user.tenant_id
|
|
||||||
)
|
|
||||||
result = await db.execute(query)
|
|
||||||
db_user = result.scalar_one_or_none()
|
|
||||||
if not db_user:"""
|
|
||||||
|
|
||||||
new_update_user = """ if current_user.role.value == "ADMIN":
|
|
||||||
query = select(User).where(User.id == user_id)
|
|
||||||
else:
|
|
||||||
query = select(User).where(
|
|
||||||
User.id == user_id,
|
|
||||||
User.tenant_id == current_user.tenant_id
|
|
||||||
)
|
|
||||||
result = await db.execute(query)
|
|
||||||
db_user = result.scalar_one_or_none()
|
|
||||||
if not db_user:"""
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
for old, new in [(old_get_user, new_get_user), (old_update_user, new_update_user)]:
|
|
||||||
occurrences = content.count(old)
|
|
||||||
if occurrences > 0:
|
|
||||||
content = content.replace(old, new)
|
|
||||||
count += occurrences
|
|
||||||
print(f"OK: {occurrences} ocurrencia(s) reemplazada(s)")
|
|
||||||
else:
|
|
||||||
print(f"SKIP: bloque no encontrado")
|
|
||||||
|
|
||||||
with open("/app/app/api/v1/endpoints/users.py", "w") as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
print(f"Total: {count} reemplazos aplicados")
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
with open("/app/app/api/v1/endpoints/users.py", "r") as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
old1 = """ # Buscar usuario
|
|
||||||
query = select(User).where(
|
|
||||||
User.id == user_id,
|
|
||||||
User.tenant_id == current_user.tenant_id
|
|
||||||
)
|
|
||||||
result = await db.execute(query)
|
|
||||||
db_user = result.scalar_one_or_none()
|
|
||||||
|
|
||||||
if not db_user:"""
|
|
||||||
|
|
||||||
new1 = """ # Buscar usuario - ADMIN global puede editar cualquier tenant
|
|
||||||
if current_user.role.value == "ADMIN":
|
|
||||||
query = select(User).where(User.id == user_id)
|
|
||||||
else:
|
|
||||||
query = select(User).where(
|
|
||||||
User.id == user_id,
|
|
||||||
User.tenant_id == current_user.tenant_id
|
|
||||||
)
|
|
||||||
result = await db.execute(query)
|
|
||||||
db_user = result.scalar_one_or_none()
|
|
||||||
|
|
||||||
if not db_user:"""
|
|
||||||
|
|
||||||
count = content.count(old1)
|
|
||||||
if count > 0:
|
|
||||||
content = content.replace(old1, new1)
|
|
||||||
print(f"OK: {count} bloques reemplazados")
|
|
||||||
else:
|
|
||||||
print("ERROR: bloque no encontrado")
|
|
||||||
|
|
||||||
with open("/app/app/api/v1/endpoints/users.py", "w") as f:
|
|
||||||
f.write(content)
|
|
||||||
print("Listo")
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
with open("/app/app/api/v1/endpoints/users.py", "r") as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
admin_check = [
|
|
||||||
" # Buscar usuario - ADMIN global puede editar cualquier tenant\n",
|
|
||||||
" if current_user.role.value == \"ADMIN\":\n",
|
|
||||||
" query = select(User).where(User.id == user_id)\n",
|
|
||||||
" else:\n",
|
|
||||||
" query = select(User).where(\n",
|
|
||||||
" User.id == user_id,\n",
|
|
||||||
" User.tenant_id == current_user.tenant_id\n",
|
|
||||||
" )\n",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Reemplazar bloques en lineas 198, 305, 382 (0-indexed: 197, 304, 381)
|
|
||||||
replaced = 0
|
|
||||||
new_lines = lines[:]
|
|
||||||
i = 0
|
|
||||||
while i < len(new_lines):
|
|
||||||
if (new_lines[i].strip() == "# Buscar usuario" and
|
|
||||||
i+1 < len(new_lines) and "select(User).where(" in new_lines[i+1] and
|
|
||||||
i+2 < len(new_lines) and "User.id == user_id," in new_lines[i+2] and
|
|
||||||
i+3 < len(new_lines) and "User.tenant_id == current_user.tenant_id" in new_lines[i+3]):
|
|
||||||
|
|
||||||
indent = " "
|
|
||||||
new_block = admin_check[:]
|
|
||||||
# Remove old 4 lines of query block (comment + query 4 lines)
|
|
||||||
new_lines[i:i+5] = new_block
|
|
||||||
replaced += 1
|
|
||||||
i += len(new_block)
|
|
||||||
else:
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
print(f"Reemplazos realizados: {replaced}")
|
|
||||||
|
|
||||||
with open("/app/app/api/v1/endpoints/users.py", "w") as f:
|
|
||||||
f.writelines(new_lines)
|
|
||||||
print("Listo")
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
with open("/app/app/api/v1/endpoints/users.py", "r") as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
# Mostrar contexto alrededor de lineas con tenant_id
|
|
||||||
targets = [51, 92, 159, 200, 307, 384]
|
|
||||||
for t in targets:
|
|
||||||
print(f"\n=== Linea {t} ===")
|
|
||||||
start = max(0, t-5)
|
|
||||||
end = min(len(lines), t+5)
|
|
||||||
for i in range(start, end):
|
|
||||||
print(f"{i+1}: {lines[i].rstrip()}")
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
with open("/app/app/api/v1/endpoints/users.py", "r") as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if "tenant_id == current_user.tenant_id" in line:
|
|
||||||
print(f"Linea {i+1}: {line.rstrip()}")
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from app.core.database import AsyncSessionLocal
|
|
||||||
from app.models.user import User
|
|
||||||
from sqlalchemy import select
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
async def check():
|
|
||||||
async with AsyncSessionLocal() as db:
|
|
||||||
result = await db.execute(select(User))
|
|
||||||
users = result.scalars().all()
|
|
||||||
for u in users:
|
|
||||||
print(f"ID: {u.id} | Email: {u.email} | Tenant: {u.tenant_id} | Rol: {u.role}")
|
|
||||||
|
|
||||||
asyncio.run(check())
|
|
||||||
19
debug_tenant.py
Normal file
19
debug_tenant.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import asyncio
|
||||||
|
from app.core.database import AsyncSessionLocal
|
||||||
|
from app.models.user import User
|
||||||
|
from app.models.tenant import Tenant
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
async def check():
|
||||||
|
async with AsyncSessionLocal() as db:
|
||||||
|
result = await db.execute(
|
||||||
|
select(User, Tenant).join(Tenant).where(User.email == 'javier@ventas.com')
|
||||||
|
)
|
||||||
|
user, tenant = result.one()
|
||||||
|
print(f"user.tenant_id: {user.tenant_id}")
|
||||||
|
print(f"tenant.id: {tenant.id}")
|
||||||
|
print(f"tenant.slug: {tenant.slug}")
|
||||||
|
print(f"user.role: {user.role}")
|
||||||
|
print(f"role.is_client: {user.role.is_client}")
|
||||||
|
|
||||||
|
asyncio.run(check())
|
||||||
67
fix_login.py
67
fix_login.py
@@ -1,67 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
with open("/app/app/api/v1/endpoints/auth.py", "r") as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
old = ''' # 1. Validar tenant
|
|
||||||
tenant_result = await db.execute(
|
|
||||||
select(Tenant).where(Tenant.slug == login_data.tenant_slug)
|
|
||||||
)
|
|
||||||
tenant = tenant_result.scalar_one_or_none()
|
|
||||||
if tenant is None:
|
|
||||||
logger.warning(
|
|
||||||
"Login failed - tenant not found",
|
|
||||||
email=login_data.email,
|
|
||||||
tenant_slug=login_data.tenant_slug,
|
|
||||||
)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="Tenant not found",
|
|
||||||
)'''
|
|
||||||
|
|
||||||
new = ''' # 1. Validar tenant - por slug si viene, sino detectar por email
|
|
||||||
if login_data.tenant_slug:
|
|
||||||
tenant_result = await db.execute(
|
|
||||||
select(Tenant).where(Tenant.slug == login_data.tenant_slug)
|
|
||||||
)
|
|
||||||
tenant = tenant_result.scalar_one_or_none()
|
|
||||||
if tenant is None:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="Tenant not found",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
tenant = None'''
|
|
||||||
|
|
||||||
if old in content:
|
|
||||||
content = content.replace(old, new)
|
|
||||||
print("OK: bloque tenant reemplazado")
|
|
||||||
else:
|
|
||||||
print("ERROR: bloque no encontrado")
|
|
||||||
|
|
||||||
# Tambien actualizar la query de usuario para usar tenant o no
|
|
||||||
old2 = ''' # 2. Buscar usuario en base de datos (aislado por tenant)
|
|
||||||
query = select(User).where(
|
|
||||||
User.email == login_data.email,
|
|
||||||
User.tenant_id == tenant.id,
|
|
||||||
)'''
|
|
||||||
|
|
||||||
new2 = ''' # 2. Buscar usuario - filtrar por tenant si se detecto, sino buscar por email
|
|
||||||
if tenant:
|
|
||||||
query = select(User).where(
|
|
||||||
User.email == login_data.email,
|
|
||||||
User.tenant_id == tenant.id,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
query = select(User).where(User.email == login_data.email)'''
|
|
||||||
|
|
||||||
if old2 in content:
|
|
||||||
content = content.replace(old2, new2)
|
|
||||||
print("OK: bloque query reemplazado")
|
|
||||||
else:
|
|
||||||
print("ERROR: bloque query no encontrado")
|
|
||||||
|
|
||||||
with open("/app/app/api/v1/endpoints/auth.py", "w") as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
print("Listo")
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
with open("/app/app/api/v1/endpoints/auth.py", "r") as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
old = ''' # Rate limiting (best-effort): by (tenant,email) to slow brute force.
|
|
||||||
ident_key = None
|
|
||||||
if settings.RATE_LIMIT_ENABLED and not settings.TESTING:
|
|
||||||
email_norm = login_data.email.strip().lower()
|
|
||||||
ident_key = cache_key("rl", "login", "id", str(tenant.id), email_norm)'''
|
|
||||||
|
|
||||||
new = ''' # Rate limiting (best-effort): by (tenant,email) to slow brute force.
|
|
||||||
ident_key = None
|
|
||||||
if settings.RATE_LIMIT_ENABLED and not settings.TESTING and tenant:
|
|
||||||
email_norm = login_data.email.strip().lower()
|
|
||||||
ident_key = cache_key("rl", "login", "id", str(tenant.id), email_norm)'''
|
|
||||||
|
|
||||||
if old in content:
|
|
||||||
content = content.replace(old, new)
|
|
||||||
print("OK: rate limiting fix aplicado")
|
|
||||||
else:
|
|
||||||
print("ERROR: bloque no encontrado")
|
|
||||||
|
|
||||||
with open("/app/app/api/v1/endpoints/auth.py", "w") as f:
|
|
||||||
f.write(content)
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
with open("/app/app/api/v1/endpoints/auth.py", "r") as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
# Linea 286 (0-indexed 285): "tenant_id": str(user.tenant_id),
|
|
||||||
# Agregar tenant_slug despues de tenant_id
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if '"tenant_id": str(user.tenant_id),' in line:
|
|
||||||
indent = " "
|
|
||||||
new_line = indent + '"tenant_slug": tenant.slug if tenant else str(user.tenant_id),\n'
|
|
||||||
lines.insert(i + 1, new_line)
|
|
||||||
print(f"OK: tenant_slug agregado en linea {i+2}")
|
|
||||||
break
|
|
||||||
|
|
||||||
with open("/app/app/api/v1/endpoints/auth.py", "w") as f:
|
|
||||||
f.writelines(lines)
|
|
||||||
print("Listo")
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
with open("/app/app/api/v1/endpoints/users.py") as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
new_block = [
|
|
||||||
" if current_user.role.value == 'ADMIN':\n",
|
|
||||||
" query = select(User).where(User.id == user_id)\n",
|
|
||||||
" else:\n",
|
|
||||||
" query = select(User).where(\n",
|
|
||||||
" User.id == user_id,\n",
|
|
||||||
" User.tenant_id == current_user.tenant_id\n",
|
|
||||||
" )\n",
|
|
||||||
]
|
|
||||||
|
|
||||||
lines[150:161] = new_block
|
|
||||||
|
|
||||||
with open("/app/app/api/v1/endpoints/users.py", "w") as f:
|
|
||||||
f.writelines(lines)
|
|
||||||
print("Listo")
|
|
||||||
28
fix_users.py
28
fix_users.py
@@ -1,28 +0,0 @@
|
|||||||
with open("/app/app/api/v1/endpoints/users.py", "r") as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
old1 = " User.tenant_id == current_user.tenant_id # " + "\u2705" + " Seguridad multi-tenant"
|
|
||||||
new1 = """ from app.models.user import UserRole as _UserRole
|
|
||||||
if current_user.role == _UserRole.ADMIN:
|
|
||||||
query = select(User).where(User.id == user_id)
|
|
||||||
else:
|
|
||||||
query = select(User).where(
|
|
||||||
User.id == user_id,
|
|
||||||
User.tenant_id == current_user.tenant_id
|
|
||||||
)"""
|
|
||||||
|
|
||||||
if old1 in content:
|
|
||||||
content = content.replace(old1, new1)
|
|
||||||
print("OK bloque 1")
|
|
||||||
else:
|
|
||||||
print("SKIP bloque 1 - buscando alternativa")
|
|
||||||
old1b = " User.tenant_id == current_user.tenant_id\n )\n result = await db.execute(query)\n if not user:"
|
|
||||||
new1b = " User.tenant_id == current_user.tenant_id\n )\n result = await db.execute(query)\n if not user:"
|
|
||||||
print("Lineas con tenant_id encontradas:")
|
|
||||||
for i, line in enumerate(content.split("\n")):
|
|
||||||
if "tenant_id == current_user.tenant_id" in line:
|
|
||||||
print(f" Linea {i}: {line}")
|
|
||||||
|
|
||||||
with open("/app/app/api/v1/endpoints/users.py", "w") as f:
|
|
||||||
f.write(content)
|
|
||||||
print("Listo")
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
with open("/app/app/api/v1/endpoints/users.py", "r") as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
from app.models.user import UserRole as _UserRole
|
|
||||||
|
|
||||||
# Reemplazar el patron comun de query con filtro de tenant
|
|
||||||
# por una version que permite a ADMIN ver todos los tenants
|
|
||||||
|
|
||||||
old_get_user = """ query = select(User).where(
|
|
||||||
User.id == user_id,
|
|
||||||
User.tenant_id == current_user.tenant_id
|
|
||||||
)
|
|
||||||
result = await db.execute(query)
|
|
||||||
user = result.scalar_one_or_none()
|
|
||||||
if not user:"""
|
|
||||||
|
|
||||||
new_get_user = """ if current_user.role.value == "ADMIN":
|
|
||||||
query = select(User).where(User.id == user_id)
|
|
||||||
else:
|
|
||||||
query = select(User).where(
|
|
||||||
User.id == user_id,
|
|
||||||
User.tenant_id == current_user.tenant_id
|
|
||||||
)
|
|
||||||
result = await db.execute(query)
|
|
||||||
user = result.scalar_one_or_none()
|
|
||||||
if not user:"""
|
|
||||||
|
|
||||||
old_update_user = """ query = select(User).where(
|
|
||||||
User.id == user_id,
|
|
||||||
User.tenant_id == current_user.tenant_id
|
|
||||||
)
|
|
||||||
result = await db.execute(query)
|
|
||||||
db_user = result.scalar_one_or_none()
|
|
||||||
if not db_user:"""
|
|
||||||
|
|
||||||
new_update_user = """ if current_user.role.value == "ADMIN":
|
|
||||||
query = select(User).where(User.id == user_id)
|
|
||||||
else:
|
|
||||||
query = select(User).where(
|
|
||||||
User.id == user_id,
|
|
||||||
User.tenant_id == current_user.tenant_id
|
|
||||||
)
|
|
||||||
result = await db.execute(query)
|
|
||||||
db_user = result.scalar_one_or_none()
|
|
||||||
if not db_user:"""
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
for old, new in [(old_get_user, new_get_user), (old_update_user, new_update_user)]:
|
|
||||||
occurrences = content.count(old)
|
|
||||||
if occurrences > 0:
|
|
||||||
content = content.replace(old, new)
|
|
||||||
count += occurrences
|
|
||||||
print(f"OK: {occurrences} ocurrencia(s) reemplazada(s)")
|
|
||||||
else:
|
|
||||||
print(f"SKIP: bloque no encontrado")
|
|
||||||
|
|
||||||
with open("/app/app/api/v1/endpoints/users.py", "w") as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
print(f"Total: {count} reemplazos aplicados")
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
with open("/app/app/api/v1/endpoints/users.py", "r") as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
old1 = """ # Buscar usuario
|
|
||||||
query = select(User).where(
|
|
||||||
User.id == user_id,
|
|
||||||
User.tenant_id == current_user.tenant_id
|
|
||||||
)
|
|
||||||
result = await db.execute(query)
|
|
||||||
db_user = result.scalar_one_or_none()
|
|
||||||
|
|
||||||
if not db_user:"""
|
|
||||||
|
|
||||||
new1 = """ # Buscar usuario - ADMIN global puede editar cualquier tenant
|
|
||||||
if current_user.role.value == "ADMIN":
|
|
||||||
query = select(User).where(User.id == user_id)
|
|
||||||
else:
|
|
||||||
query = select(User).where(
|
|
||||||
User.id == user_id,
|
|
||||||
User.tenant_id == current_user.tenant_id
|
|
||||||
)
|
|
||||||
result = await db.execute(query)
|
|
||||||
db_user = result.scalar_one_or_none()
|
|
||||||
|
|
||||||
if not db_user:"""
|
|
||||||
|
|
||||||
count = content.count(old1)
|
|
||||||
if count > 0:
|
|
||||||
content = content.replace(old1, new1)
|
|
||||||
print(f"OK: {count} bloques reemplazados")
|
|
||||||
else:
|
|
||||||
print("ERROR: bloque no encontrado")
|
|
||||||
|
|
||||||
with open("/app/app/api/v1/endpoints/users.py", "w") as f:
|
|
||||||
f.write(content)
|
|
||||||
print("Listo")
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
with open("/app/app/api/v1/endpoints/users.py", "r") as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
admin_check = [
|
|
||||||
" # Buscar usuario - ADMIN global puede editar cualquier tenant\n",
|
|
||||||
" if current_user.role.value == \"ADMIN\":\n",
|
|
||||||
" query = select(User).where(User.id == user_id)\n",
|
|
||||||
" else:\n",
|
|
||||||
" query = select(User).where(\n",
|
|
||||||
" User.id == user_id,\n",
|
|
||||||
" User.tenant_id == current_user.tenant_id\n",
|
|
||||||
" )\n",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Reemplazar bloques en lineas 198, 305, 382 (0-indexed: 197, 304, 381)
|
|
||||||
replaced = 0
|
|
||||||
new_lines = lines[:]
|
|
||||||
i = 0
|
|
||||||
while i < len(new_lines):
|
|
||||||
if (new_lines[i].strip() == "# Buscar usuario" and
|
|
||||||
i+1 < len(new_lines) and "select(User).where(" in new_lines[i+1] and
|
|
||||||
i+2 < len(new_lines) and "User.id == user_id," in new_lines[i+2] and
|
|
||||||
i+3 < len(new_lines) and "User.tenant_id == current_user.tenant_id" in new_lines[i+3]):
|
|
||||||
|
|
||||||
indent = " "
|
|
||||||
new_block = admin_check[:]
|
|
||||||
# Remove old 4 lines of query block (comment + query 4 lines)
|
|
||||||
new_lines[i:i+5] = new_block
|
|
||||||
replaced += 1
|
|
||||||
i += len(new_block)
|
|
||||||
else:
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
print(f"Reemplazos realizados: {replaced}")
|
|
||||||
|
|
||||||
with open("/app/app/api/v1/endpoints/users.py", "w") as f:
|
|
||||||
f.writelines(new_lines)
|
|
||||||
print("Listo")
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Handle } from '@sveltejs/kit';
|
import type { Handle } from '@sveltejs/kit';
|
||||||
export const handle: Handle = async ({ event, resolve }) => {
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
// No restaurar sesión en la página de login
|
// No restaurar sesión en la página de login
|
||||||
if (event.url.pathname === '/login') {
|
if (event.url.pathname === '/login') {
|
||||||
event.locals.user = null;
|
event.locals.user = null;
|
||||||
return resolve(event);
|
return resolve(event);
|
||||||
@@ -17,7 +17,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`,
|
'Authorization': `Bearer ${token}`,
|
||||||
'X-App': 'client',
|
'X-App': 'client',
|
||||||
'X-Tenant-Slug': 'aduanasoft'
|
'X-Tenant-Slug': ''
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
|||||||
@@ -25,14 +25,12 @@ async function request<T>(endpoint: string, options: RequestOptions = {}): Promi
|
|||||||
if (authState.token) {
|
if (authState.token) {
|
||||||
headers.set('Authorization', `Bearer ${authState.token}`);
|
headers.set('Authorization', `Bearer ${authState.token}`);
|
||||||
}
|
}
|
||||||
if (authState.user?.tenant_id && !headers.has('X-Tenant-ID')) {
|
|
||||||
headers.set('X-Tenant-ID', authState.user.tenant_id);
|
|
||||||
}
|
|
||||||
if (!headers.has('Content-Type')) {
|
if (!headers.has('Content-Type')) {
|
||||||
headers.set('Content-Type', 'application/json');
|
headers.set('Content-Type', 'application/json');
|
||||||
}
|
}
|
||||||
headers.set('X-App', 'client');
|
headers.set('X-App', 'client');
|
||||||
const slug = get(authStore)?.user?.tenant_slug || get(authStore)?.user?.tenant_id || '';
|
const slug = get(auth)?.user?.tenant_slug || get(auth)?.user?.tenant_id || '';
|
||||||
headers.set('X-Tenant-Slug', slug);
|
headers.set('X-Tenant-Slug', slug);
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@@ -67,11 +65,9 @@ async function downloadFile(endpoint: string, filename: string): Promise<void> {
|
|||||||
if (authState.token) {
|
if (authState.token) {
|
||||||
headers.set('Authorization', `Bearer ${authState.token}`);
|
headers.set('Authorization', `Bearer ${authState.token}`);
|
||||||
}
|
}
|
||||||
if (authState.user?.tenant_id) {
|
|
||||||
headers.set('X-Tenant-ID', authState.user.tenant_id);
|
|
||||||
}
|
}
|
||||||
headers.set('X-App', 'client');
|
headers.set('X-App', 'client');
|
||||||
const slug = get(authStore)?.user?.tenant_slug || get(authStore)?.user?.tenant_id || '';
|
const slug = get(auth)?.user?.tenant_slug || get(auth)?.user?.tenant_id || '';
|
||||||
headers.set('X-Tenant-Slug', slug);
|
headers.set('X-Tenant-Slug', slug);
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE}${endpoint}`, {
|
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
totp_code: totpCode || undefined
|
totp_code: totpCode || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success('¡Bienvenido! Has iniciado sesión correctamente');
|
toast.success('¡Bienvenido! Has iniciado sesión correctamente');
|
||||||
goto('/');
|
goto('/');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Login error:', error);
|
console.error('Login error:', error);
|
||||||
@@ -46,9 +46,9 @@
|
|||||||
// Check if 2FA is required
|
// Check if 2FA is required
|
||||||
if (error.message.includes('two-factor') || error.message.includes('2FA')) {
|
if (error.message.includes('two-factor') || error.message.includes('2FA')) {
|
||||||
showTwoFactor = true;
|
showTwoFactor = true;
|
||||||
errorMessage = 'Introduce el código de tu aplicación de autenticación';
|
errorMessage = 'Introduce el código de tu aplicación de autenticación';
|
||||||
} else {
|
} else {
|
||||||
errorMessage = error.message || 'Error al iniciar sesión';
|
errorMessage = error.message || 'Error al iniciar sesión';
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -96,8 +96,8 @@
|
|||||||
de Servicios de TI
|
de Servicios de TI
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-lg text-blue-100/90 font-light max-w-lg leading-relaxed drop-shadow-md">
|
<p class="text-lg text-blue-100/90 font-light max-w-lg leading-relaxed drop-shadow-md">
|
||||||
Portal de atención a clientes. Genere tickets de soporte técnico para nuestros sistemas y
|
Portal de atención a clientes. Genere tickets de soporte técnico para nuestros sistemas y
|
||||||
reciba asistencia especializada para garantizar la continuidad de su operación.
|
reciba asistencia especializada para garantizar la continuidad de su operación.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@
|
|||||||
<!-- Email Input -->
|
<!-- Email Input -->
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="email" class="block text-sm font-semibold text-gray-700"
|
<label for="email" class="block text-sm font-semibold text-gray-700"
|
||||||
>Correo Electrónico</label
|
>Correo Electrónico</label
|
||||||
>
|
>
|
||||||
<div class="relative group">
|
<div class="relative group">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
<!-- Password Input -->
|
<!-- Password Input -->
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="password" class="block text-sm font-semibold text-gray-700"
|
<label for="password" class="block text-sm font-semibold text-gray-700"
|
||||||
>Contraseña</label
|
>Contraseña</label
|
||||||
>
|
>
|
||||||
<div class="relative group">
|
<div class="relative group">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
@@ -176,7 +176,7 @@
|
|||||||
bind:value={password}
|
bind:value={password}
|
||||||
on:keydown={handleKeyDown}
|
on:keydown={handleKeyDown}
|
||||||
class="block w-full pl-10 pr-10 py-3 bg-[#fff9c4]/0 hover:bg-gray-50 focus:bg-white border text-gray-900 border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent transition-all duration-200 sm:text-sm"
|
class="block w-full pl-10 pr-10 py-3 bg-[#fff9c4]/0 hover:bg-gray-50 focus:bg-white border text-gray-900 border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent transition-all duration-200 sm:text-sm"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
required
|
required
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
@@ -187,7 +187,7 @@
|
|||||||
bind:value={password}
|
bind:value={password}
|
||||||
on:keydown={handleKeyDown}
|
on:keydown={handleKeyDown}
|
||||||
class="block w-full pl-10 pr-10 py-3 bg-[#fff9c4]/0 hover:bg-gray-50 focus:bg-white border text-gray-900 border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent transition-all duration-200 sm:text-sm"
|
class="block w-full pl-10 pr-10 py-3 bg-[#fff9c4]/0 hover:bg-gray-50 focus:bg-white border text-gray-900 border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent transition-all duration-200 sm:text-sm"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
required
|
required
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
@@ -221,7 +221,7 @@
|
|||||||
class="text-sm font-medium text-blue-600 hover:text-blue-500 bg-transparent border-none p-0 cursor-pointer"
|
class="text-sm font-medium text-blue-600 hover:text-blue-500 bg-transparent border-none p-0 cursor-pointer"
|
||||||
on:click={() => goto('/forgot-password')}
|
on:click={() => goto('/forgot-password')}
|
||||||
>
|
>
|
||||||
Olvidé mi clave
|
Olvidé mi clave
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,9 +229,9 @@
|
|||||||
<!-- 2FA Input -->
|
<!-- 2FA Input -->
|
||||||
<div class="space-y-4 animate-slide-up">
|
<div class="space-y-4 animate-slide-up">
|
||||||
<label for="code" class="block text-sm font-medium text-gray-700 text-center"
|
<label for="code" class="block text-sm font-medium text-gray-700 text-center"
|
||||||
>Código de Verificación (2FA)</label
|
>Código de Verificación (2FA)</label
|
||||||
>
|
>
|
||||||
<p class="text-xs text-center text-gray-500 mb-4">Ingrese el código de 6 dÃgitos</p>
|
<p class="text-xs text-center text-gray-500 mb-4">Ingrese el código de 6 dígitos</p>
|
||||||
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
@@ -268,7 +268,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-8 text-center text-xs text-gray-400">
|
<div class="mt-8 text-center text-xs text-gray-400">
|
||||||
© 2026 Aduanasoft. Acceso exclusivo autorizado.
|
© 2026 Aduanasoft. Acceso exclusivo autorizado.
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
with open("/app/app/api/v1/endpoints/users.py", "r") as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
# Mostrar contexto alrededor de lineas con tenant_id
|
|
||||||
targets = [51, 92, 159, 200, 307, 384]
|
|
||||||
for t in targets:
|
|
||||||
print(f"\n=== Linea {t} ===")
|
|
||||||
start = max(0, t-5)
|
|
||||||
end = min(len(lines), t+5)
|
|
||||||
for i in range(start, end):
|
|
||||||
print(f"{i+1}: {lines[i].rstrip()}")
|
|
||||||
Reference in New Issue
Block a user