Compare commits
5 Commits
v1.20.0
...
3f7b166767
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f7b166767 | |||
| 6bc5145b9c | |||
| 597286fff0 | |||
| e141701567 | |||
| 16b3dc5d47 |
@@ -284,6 +284,7 @@ async def login(
|
||||
"last_name": user.last_name,
|
||||
"role": user.role,
|
||||
"tenant_id": str(user.tenant_id),
|
||||
"tenant_slug": tenant.slug if tenant else str(user.tenant_id),
|
||||
"is_active": user.is_active,
|
||||
"is_two_factor_enabled": user.totp_enabled or False,
|
||||
"created_at": user.created_at.isoformat() if user.created_at else None
|
||||
|
||||
@@ -148,9 +148,12 @@ async def read_user(
|
||||
|
||||
✅ Implementa multi-tenancy: solo permite acceso a usuarios del propio 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 # ✅ Seguridad multi-tenant
|
||||
User.tenant_id == current_user.tenant_id
|
||||
)
|
||||
result = await db.execute(query)
|
||||
user = result.scalar_one_or_none()
|
||||
@@ -187,7 +190,10 @@ async def update_user(
|
||||
detail="You don't have permission to update users"
|
||||
)
|
||||
|
||||
# Buscar usuario
|
||||
# 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
|
||||
@@ -294,7 +300,10 @@ async def delete_user(
|
||||
detail="You cannot delete yourself"
|
||||
)
|
||||
|
||||
# Buscar usuario
|
||||
# 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
|
||||
@@ -371,7 +380,10 @@ async def activate_user(
|
||||
detail="You don't have permission to activate users"
|
||||
)
|
||||
|
||||
# Buscar usuario
|
||||
# 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
|
||||
|
||||
104
backend/auth_backup.ts
Normal file
104
backend/auth_backup.ts
Normal 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
6
backend/check_lines.py
Normal 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
14
backend/check_user.py
Normal 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
16
backend/fix_response.py
Normal 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
18
backend/fix_syntax.py
Normal 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
28
backend/fix_users.py
Normal 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
60
backend/fix_users2.py
Normal 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
36
backend/fix_users3.py
Normal 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
38
backend/fix_users4.py
Normal 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
11
backend/show_context.py
Normal 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
6
check_lines.py
Normal 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
14
check_user.py
Normal 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())
|
||||
@@ -13,9 +13,9 @@ services:
|
||||
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./db/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
|
||||
#- ./db/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
|
||||
ports:
|
||||
- "5432:5432"
|
||||
- "5433:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-servicemanager}"]
|
||||
interval: 10s
|
||||
@@ -215,7 +215,7 @@ services:
|
||||
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- uploads_data:/var/www/uploads:ro
|
||||
ports:
|
||||
- "80:80"
|
||||
- "8088:80"
|
||||
depends_on:
|
||||
- backend
|
||||
- frontend-client
|
||||
|
||||
16
fix_response.py
Normal file
16
fix_response.py
Normal 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
18
fix_syntax.py
Normal 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
28
fix_users.py
Normal 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
60
fix_users2.py
Normal 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
36
fix_users3.py
Normal 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
38
fix_users4.py
Normal 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")
|
||||
@@ -1,14 +1,32 @@
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
const cookie = event.request.headers.get('cookie') ?? '';
|
||||
if (cookie) {
|
||||
// No restaurar sesión en la página de login
|
||||
if (event.url.pathname === '/login') {
|
||||
event.locals.user = null;
|
||||
return resolve(event);
|
||||
}
|
||||
|
||||
const cookieHeader = event.request.headers.get('cookie') ?? '';
|
||||
const cookieMatch = cookieHeader.match(/(?:client_access_token|internal_access_token)=([^;]+)/);
|
||||
const token = cookieMatch?.[1];
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
const apiUrl = process.env.PUBLIC_API_URL ?? 'http://backend:8000';
|
||||
const response = await fetch(`${apiUrl}/v1/auth/me`, {
|
||||
headers: { cookie, 'X-App': 'client' }
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'X-App': 'client',
|
||||
'X-Tenant-Slug': 'aduanasoft'
|
||||
}
|
||||
});
|
||||
event.locals.user = response.ok ? await response.json() : null;
|
||||
if (response.ok) {
|
||||
event.locals.user = await response.json();
|
||||
} else {
|
||||
event.locals.user = null;
|
||||
event.cookies.delete('client_access_token', { path: '/' });
|
||||
event.cookies.delete('internal_access_token', { path: '/' });
|
||||
}
|
||||
} catch {
|
||||
event.locals.user = null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
// Types
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
@@ -13,131 +11,94 @@ export interface User {
|
||||
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;
|
||||
}
|
||||
|
||||
// Initial state
|
||||
const initialState: AuthState = {
|
||||
user: null,
|
||||
token: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false
|
||||
};
|
||||
|
||||
// Create auth store
|
||||
function createAuthStore() {
|
||||
const { subscribe, set, update }: Writable<AuthState> = writable(initialState);
|
||||
|
||||
let _state = initialState;
|
||||
subscribe(s => { _state = s; });
|
||||
return {
|
||||
subscribe,
|
||||
|
||||
// Rehidrata sesión desde cookie HttpOnly (no toca localStorage)
|
||||
init: async () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const response = await fetch('/api/v1/auth/me', {
|
||||
credentials: 'include',
|
||||
headers: { 'X-App': 'client' }
|
||||
headers: { 'X-App': 'client', 'X-Tenant-Slug': 'aduanasoft' }
|
||||
});
|
||||
if (response.ok) {
|
||||
const user = await response.json();
|
||||
set({
|
||||
user,
|
||||
token: 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
|
||||
set({ user, token: null, isAuthenticated: true, isLoading: false });
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
},
|
||||
|
||||
// Login
|
||||
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
|
||||
});
|
||||
set({ user: data.user, token: data.access_token, isAuthenticated: true, isLoading: false });
|
||||
} catch (error) {
|
||||
update(state => ({ ...state, isLoading: false }));
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Logout
|
||||
logout: async () => {
|
||||
// Llamar al backend para que borre la cookie HttpOnly
|
||||
try {
|
||||
const token = _state.token;
|
||||
await fetch('/api/v1/auth/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'X-App': 'client' }
|
||||
headers: {
|
||||
'X-App': 'client',
|
||||
'X-Tenant-Slug': 'aduanasoft',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||
}
|
||||
});
|
||||
} catch { /* ignorar errores de red */ }
|
||||
} catch {}
|
||||
set(initialState);
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
},
|
||||
|
||||
// Update user data
|
||||
updateUser: (user: User) => {
|
||||
update(state => ({ ...state, user }));
|
||||
},
|
||||
|
||||
// Set user from SSR pre-load (no fetch required)
|
||||
setUser: (user: User) => {
|
||||
set({ user, token: null, isAuthenticated: true, isLoading: false });
|
||||
},
|
||||
|
||||
// Set loading state
|
||||
setLoading: (isLoading: boolean) => {
|
||||
update(state => ({ ...state, isLoading }));
|
||||
}
|
||||
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();
|
||||
104
frontend-client/src/lib/stores/auth.ts.bak
Normal file
104
frontend-client/src/lib/stores/auth.ts.bak
Normal 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();
|
||||
@@ -1,13 +1,7 @@
|
||||
/**
|
||||
* Cliente HTTP centralizado para frontend-client.
|
||||
* Usa cookies HttpOnly (client_access_token) como fuente primaria de auth,
|
||||
* con Bearer token como complemento cuando está disponible en memoria.
|
||||
*/
|
||||
import { auth } from '$lib/stores/auth';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
const API_BASE = '/api/v1';
|
||||
|
||||
interface RequestOptions extends RequestInit {
|
||||
params?: Record<string, string>;
|
||||
}
|
||||
@@ -20,7 +14,6 @@ async function request<T>(endpoint: string, options: RequestOptions = {}): Promi
|
||||
const filteredParams = Object.entries(params)
|
||||
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
|
||||
|
||||
if (Object.keys(filteredParams).length > 0) {
|
||||
url += `?${new URLSearchParams(filteredParams).toString()}`;
|
||||
}
|
||||
@@ -29,7 +22,6 @@ async function request<T>(endpoint: string, options: RequestOptions = {}): Promi
|
||||
const authState = get(auth);
|
||||
const headers = new Headers(init.headers);
|
||||
|
||||
// Bearer header cuando el token está en memoria (sesión activa sin reload)
|
||||
if (authState.token) {
|
||||
headers.set('Authorization', `Bearer ${authState.token}`);
|
||||
}
|
||||
@@ -39,8 +31,9 @@ async function request<T>(endpoint: string, options: RequestOptions = {}): Promi
|
||||
if (!headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
// Identifica este frontend para que el backend use client_access_token
|
||||
headers.set('X-App', 'client');
|
||||
const slug = get(authStore)?.user?.tenant_slug || get(authStore)?.user?.tenant_id || '';
|
||||
headers.set('X-Tenant-Slug', slug);
|
||||
|
||||
const response = await fetch(url, {
|
||||
...init,
|
||||
@@ -78,6 +71,8 @@ async function downloadFile(endpoint: string, filename: string): Promise<void> {
|
||||
headers.set('X-Tenant-ID', authState.user.tenant_id);
|
||||
}
|
||||
headers.set('X-App', 'client');
|
||||
const slug = get(authStore)?.user?.tenant_slug || get(authStore)?.user?.tenant_id || '';
|
||||
headers.set('X-Tenant-Slug', slug);
|
||||
|
||||
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||
method: 'GET',
|
||||
@@ -108,19 +103,14 @@ async function downloadFile(endpoint: string, filename: string): Promise<void> {
|
||||
export const api = {
|
||||
get: <T>(endpoint: string, params?: Record<string, string>) =>
|
||||
request<T>(endpoint, { method: 'GET', params }),
|
||||
|
||||
post: <T>(endpoint: string, body?: any) =>
|
||||
request<T>(endpoint, { method: 'POST', body: body !== undefined ? JSON.stringify(body) : undefined }),
|
||||
|
||||
put: <T>(endpoint: string, body?: any) =>
|
||||
request<T>(endpoint, { method: 'PUT', body: body !== undefined ? JSON.stringify(body) : undefined }),
|
||||
|
||||
patch: <T>(endpoint: string, body?: any) =>
|
||||
request<T>(endpoint, { method: 'PATCH', body: body !== undefined ? JSON.stringify(body) : undefined }),
|
||||
|
||||
delete: <T>(endpoint: string) =>
|
||||
request<T>(endpoint, { method: 'DELETE' }),
|
||||
|
||||
downloadFile: (endpoint: string, filename: string) =>
|
||||
downloadFile(endpoint, filename)
|
||||
};
|
||||
@@ -38,7 +38,7 @@
|
||||
totp_code: totpCode || undefined
|
||||
});
|
||||
|
||||
toast.success('¡Bienvenido! Has iniciado sesión correctamente');
|
||||
toast.success('¡Bienvenido! Has iniciado sesión correctamente');
|
||||
goto('/');
|
||||
} catch (error: any) {
|
||||
console.error('Login error:', error);
|
||||
@@ -46,9 +46,9 @@
|
||||
// Check if 2FA is required
|
||||
if (error.message.includes('two-factor') || error.message.includes('2FA')) {
|
||||
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 {
|
||||
errorMessage = error.message || 'Error al iniciar sesión';
|
||||
errorMessage = error.message || 'Error al iniciar sesión';
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
} finally {
|
||||
@@ -96,8 +96,8 @@
|
||||
de Servicios de TI
|
||||
</h2>
|
||||
<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
|
||||
reciba asistencia especializada para garantizar la continuidad de su operación.
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
<!-- Email Input -->
|
||||
<div class="space-y-1.5">
|
||||
<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="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
@@ -160,7 +160,7 @@
|
||||
<!-- Password Input -->
|
||||
<div class="space-y-1.5">
|
||||
<label for="password" class="block text-sm font-semibold text-gray-700"
|
||||
>Contraseña</label
|
||||
>Contraseña</label
|
||||
>
|
||||
<div class="relative group">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
@@ -176,7 +176,7 @@
|
||||
bind:value={password}
|
||||
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"
|
||||
placeholder="••••••••"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
@@ -187,7 +187,7 @@
|
||||
bind:value={password}
|
||||
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"
|
||||
placeholder="••••••••"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
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"
|
||||
on:click={() => goto('/forgot-password')}
|
||||
>
|
||||
Olvidé mi clave
|
||||
Olvidé mi clave
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -229,9 +229,9 @@
|
||||
<!-- 2FA Input -->
|
||||
<div class="space-y-4 animate-slide-up">
|
||||
<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="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
@@ -268,7 +268,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-8 text-center text-xs text-gray-400">
|
||||
© 2026 Aduanasoft. Acceso exclusivo autorizado.
|
||||
© 2026 Aduanasoft. Acceso exclusivo autorizado.
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
server: {
|
||||
@@ -10,19 +9,17 @@ export default defineConfig({
|
||||
usePolling: true,
|
||||
interval: 500
|
||||
},
|
||||
// HMR: el browser llega al contenedor en el mismo puerto 3000
|
||||
hmr: {
|
||||
host: 'localhost',
|
||||
clientPort: parseInt(process.env.HMR_CLIENT_PORT || '3000')
|
||||
},
|
||||
// Permitir que Vite sirva archivos del filesystem del contenedor
|
||||
fs: {
|
||||
allow: ['/app', '.'],
|
||||
strict: false
|
||||
},
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: process.env.PUBLIC_API_URL || 'http://localhost:8000',
|
||||
target: process.env.PUBLIC_API_URL || 'http://backend:8000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
// Types
|
||||
export interface InternalUser {
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
first_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_two_factor_enabled: boolean;
|
||||
created_at: string;
|
||||
tenant_id: string;
|
||||
}
|
||||
|
||||
export interface AuthState {
|
||||
user: InternalUser | null;
|
||||
user: User | null;
|
||||
token: string | null;
|
||||
refreshToken: string | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
@@ -25,180 +24,90 @@ export interface AuthState {
|
||||
export interface LoginRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
tenant_slug: string;
|
||||
tenant_slug?: string;
|
||||
totp_code?: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
token_type: string;
|
||||
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 = {
|
||||
user: null,
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false
|
||||
};
|
||||
|
||||
// Create auth store
|
||||
function createAuthStore() {
|
||||
const { subscribe, set, update } = writable<AuthState>(initialState);
|
||||
|
||||
// Track current state for uso interno (evita dependencias circulares)
|
||||
const { subscribe, set, update }: Writable<AuthState> = writable(initialState);
|
||||
let _state = initialState;
|
||||
subscribe(s => { _state = s; });
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
||||
// Rehidrata sesión desde cookie HttpOnly (no toca localStorage)
|
||||
init: async () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const response = await fetch('/api/v1/auth/me', {
|
||||
credentials: 'include',
|
||||
headers: { 'X-App': 'internal' }
|
||||
headers: { 'X-App': 'client' }
|
||||
});
|
||||
if (response.ok) {
|
||||
const user = await response.json();
|
||||
set({
|
||||
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
|
||||
set({ user, token: null, isAuthenticated: true, isLoading: false });
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
},
|
||||
|
||||
// Login
|
||||
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-App': 'client',
|
||||
},
|
||||
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,
|
||||
refreshToken: data.refresh_token,
|
||||
isAuthenticated: true,
|
||||
isLoading: false
|
||||
});
|
||||
set({ user: data.user, token: data.access_token, isAuthenticated: true, isLoading: false });
|
||||
} catch (error) {
|
||||
update(state => ({ ...state, isLoading: false }));
|
||||
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 () => {
|
||||
// Llamar al backend para que borre la cookie HttpOnly
|
||||
try {
|
||||
const token = _state.token;
|
||||
const slug = _state.user?.tenant_slug || _state.user?.tenant_id || '';
|
||||
await fetch('/api/v1/auth/logout', {
|
||||
method: 'POST',
|
||||
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);
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
},
|
||||
|
||||
// Update user data
|
||||
updateUser: (user: InternalUser) => {
|
||||
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 }));
|
||||
}
|
||||
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 })); }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@
|
||||
<label
|
||||
for="code"
|
||||
class="block text-xs font-semibold uppercase tracking-wider text-blue-800 dark:text-blue-300 mb-2 text-center"
|
||||
>Verificación de Seguridad</label
|
||||
>Verificación de Seguridad</label
|
||||
>
|
||||
<div class="relative">
|
||||
<input
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
server: {
|
||||
// Puerto: dentro del contenedor siempre 3000; Docker mapea 3001:3000 al host
|
||||
port: parseInt(process.env.PORT || '3001'),
|
||||
host: '0.0.0.0',
|
||||
watch: {
|
||||
usePolling: true,
|
||||
interval: 500
|
||||
},
|
||||
// HMR: el browser llega al contenedor a través del puerto 3001 del host
|
||||
hmr: {
|
||||
host: 'localhost',
|
||||
clientPort: parseInt(process.env.HMR_CLIENT_PORT || '3001')
|
||||
},
|
||||
// Permitir que Vite sirva archivos del filesystem del contenedor
|
||||
fs: {
|
||||
allow: ['/app', '.'],
|
||||
strict: false
|
||||
},
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: process.env.PUBLIC_API_URL || 'http://localhost:8000',
|
||||
target: process.env.PUBLIC_API_URL || 'http://backend:8000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
|
||||
BIN
migrations.sql
Normal file
BIN
migrations.sql
Normal file
Binary file not shown.
11
show_context.py
Normal file
11
show_context.py
Normal 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()}")
|
||||
Reference in New Issue
Block a user