Sistema limpio

This commit is contained in:
2026-03-10 13:46:30 -06:00
parent 6bc5145b9c
commit 3f7b166767
21 changed files with 687 additions and 117 deletions

104
backend/auth_backup.ts Normal file
View File

@@ -0,0 +1,104 @@
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();

6
backend/check_lines.py Normal file
View File

@@ -0,0 +1,6 @@
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()}")

14
backend/check_user.py Normal file
View File

@@ -0,0 +1,14 @@
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())

16
backend/fix_response.py Normal file
View File

@@ -0,0 +1,16 @@
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")

18
backend/fix_syntax.py Normal file
View File

@@ -0,0 +1,18 @@
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
backend/fix_users.py Normal file
View File

@@ -0,0 +1,28 @@
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")

60
backend/fix_users2.py Normal file
View File

@@ -0,0 +1,60 @@
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")

36
backend/fix_users3.py Normal file
View File

@@ -0,0 +1,36 @@
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")

38
backend/fix_users4.py Normal file
View File

@@ -0,0 +1,38 @@
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")

11
backend/show_context.py Normal file
View File

@@ -0,0 +1,11 @@
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()}")

6
check_lines.py Normal file
View File

@@ -0,0 +1,6 @@
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()}")

14
check_user.py Normal file
View File

@@ -0,0 +1,14 @@
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())

16
fix_response.py Normal file
View File

@@ -0,0 +1,16 @@
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")

18
fix_syntax.py Normal file
View File

@@ -0,0 +1,18 @@
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 Normal file
View File

@@ -0,0 +1,28 @@
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")

60
fix_users2.py Normal file
View File

@@ -0,0 +1,60 @@
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")

36
fix_users3.py Normal file
View File

@@ -0,0 +1,36 @@
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")

38
fix_users4.py Normal file
View File

@@ -0,0 +1,38 @@
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")

View File

@@ -0,0 +1,104 @@
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();

View File

@@ -1,23 +1,22 @@
import { writable } from 'svelte/store';
import type { Writable } from 'svelte/store'; import type { Writable } from 'svelte/store';
import { writable } from 'svelte/store';
// Types export interface User {
export interface InternalUser {
id: string; id: string;
email: string; email: string;
first_name: string; first_name: string;
last_name: string; last_name: string;
role: 'ADMIN' | 'SUPPORT_MANAGER' | 'AGENT' | 'AUDITOR'; tenant_id: string;
tenant_slug: string;
role: 'CLIENT_ADMIN' | 'CLIENT_USER';
is_active: boolean; is_active: boolean;
is_two_factor_enabled: boolean; is_two_factor_enabled: boolean;
created_at: string; created_at: string;
tenant_id: string;
} }
export interface AuthState { export interface AuthState {
user: InternalUser | null; user: User | null;
token: string | null; token: string | null;
refreshToken: string | null;
isAuthenticated: boolean; isAuthenticated: boolean;
isLoading: boolean; isLoading: boolean;
} }
@@ -25,181 +24,90 @@ export interface AuthState {
export interface LoginRequest { export interface LoginRequest {
email: string; email: string;
password: string; password: string;
tenant_slug: string; tenant_slug?: string;
totp_code?: string; totp_code?: string;
} }
export interface LoginResponse { export interface LoginResponse {
access_token: string; access_token: string;
refresh_token: string;
token_type: string; token_type: string;
expires_in: number; expires_in: number;
user: InternalUser; user: User;
} }
export interface RefreshTokenRequest {
refresh_token: string;
}
export interface TokenResponse {
access_token: string;
token_type: string;
expires_in: number;
}
// Initial state
const initialState: AuthState = { const initialState: AuthState = {
user: null, user: null,
token: null, token: null,
refreshToken: null,
isAuthenticated: false, isAuthenticated: false,
isLoading: false isLoading: false
}; };
// Create auth store
function createAuthStore() { function createAuthStore() {
const { subscribe, set, update } = writable<AuthState>(initialState); const { subscribe, set, update }: Writable<AuthState> = writable(initialState);
// Track current state for uso interno (evita dependencias circulares)
let _state = initialState; let _state = initialState;
subscribe(s => { _state = s; }); subscribe(s => { _state = s; });
return { return {
subscribe, subscribe,
// Rehidrata sesión desde cookie HttpOnly (no toca localStorage)
init: async () => { init: async () => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
try { try {
const response = await fetch('/api/v1/auth/me', { const response = await fetch('/api/v1/auth/me', {
credentials: 'include', credentials: 'include',
headers: { 'X-App': 'internal' } headers: { 'X-App': 'client' }
}); });
if (response.ok) { if (response.ok) {
const user = await response.json(); const user = await response.json();
set({ set({ user, token: null, isAuthenticated: true, isLoading: false });
user,
token: null,
refreshToken: null,
isAuthenticated: true,
isLoading: false
});
}
// 401/400 es esperado cuando no hay sesión activa — no es un error
} catch (error) {
// Ignorar errores de red en init
} }
} catch (error) {}
} }
}, },
// Login
login: async (credentials: LoginRequest): Promise<void> => { login: async (credentials: LoginRequest): Promise<void> => {
update(state => ({ ...state, isLoading: true })); update(state => ({ ...state, isLoading: true }));
try { try {
const response = await fetch('/api/v1/auth/login', { const response = await fetch('/api/v1/auth/login', {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-Tenant-Slug': credentials.tenant_slug, 'X-App': 'client',
}, },
body: JSON.stringify(credentials) body: JSON.stringify(credentials)
}); });
if (!response.ok) { if (!response.ok) {
const error = await response.json(); const error = await response.json();
throw new Error(error.detail || 'Login failed'); throw new Error(error.detail || 'Login failed');
} }
const data: LoginResponse = await response.json(); const data: LoginResponse = await response.json();
set({ user: data.user, token: data.access_token, isAuthenticated: true, isLoading: false });
set({
user: data.user,
token: data.access_token,
refreshToken: data.refresh_token,
isAuthenticated: true,
isLoading: false
});
} catch (error) { } catch (error) {
update(state => ({ ...state, isLoading: false })); update(state => ({ ...state, isLoading: false }));
throw error; throw error;
} }
}, },
// Refresh Session
refreshSession: async (): Promise<void> => {
const currentRefreshToken = _state.refreshToken;
if (!currentRefreshToken) {
throw new Error("No refresh token available");
}
update(state => ({ ...state, isLoading: true }));
try {
const response = await fetch('/api/v1/auth/refresh', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ refresh_token: currentRefreshToken })
});
if (!response.ok) {
// If refresh fails, logout
if (response.status === 401 || response.status === 403) {
auth.logout();
}
const error = await response.json();
throw new Error(error.detail || 'Refresh failed');
}
const data: TokenResponse = await response.json();
update(state => ({
...state,
token: data.access_token,
isLoading: false
}));
} catch (error) {
update(state => ({ ...state, isLoading: false }));
throw error;
}
},
// Logout
logout: async () => { logout: async () => {
// Llamar al backend para que borre la cookie HttpOnly
try { try {
const token = _state.token;
const slug = _state.user?.tenant_slug || _state.user?.tenant_id || '';
await fetch('/api/v1/auth/logout', { await fetch('/api/v1/auth/logout', {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
headers: { 'X-App': 'internal' } headers: {
'X-App': 'client',
'X-Tenant-Slug': slug,
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
}); });
} catch { /* ignorar errores de red */ } } catch {}
set(initialState); set(initialState);
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.location.href = '/login'; window.location.href = '/login';
} }
}, },
updateUser: (user: User) => { update(state => ({ ...state, user })); },
// Update user data setUser: (user: User) => { set({ user, token: null, isAuthenticated: true, isLoading: false }); },
updateUser: (user: InternalUser) => { setLoading: (isLoading: boolean) => { update(state => ({ ...state, isLoading })); }
update(state => ({ ...state, user }));
},
// Set user from SSR pre-load (no fetch required)
setUser: (user: InternalUser) => {
set({ user, token: null, refreshToken: null, isAuthenticated: true, isLoading: false });
},
// Set loading state
setLoading: (isLoading: boolean) => {
update(state => ({ ...state, isLoading }));
}
}; };
} }

11
show_context.py Normal file
View File

@@ -0,0 +1,11 @@
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()}")