File size: 20,245 Bytes
5844082
 
 
 
afa8e49
a0c6b50
5844082
a0c6b50
5844082
 
 
 
 
 
c6bd2df
5844082
420774d
 
 
c6bd2df
 
5844082
 
 
 
 
 
 
c6bd2df
 
 
 
 
 
5844082
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
037071f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5844082
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
037071f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c314dfe
 
5844082
c314dfe
edd6ece
c314dfe
037071f
 
a0c6b50
 
 
 
 
5844082
 
 
 
a0c6b50
5844082
a0c6b50
c314dfe
5844082
 
 
 
c314dfe
 
edd6ece
c314dfe
 
 
 
 
5844082
 
a0c6b50
c314dfe
5844082
a54bb56
5844082
a54bb56
a0c6b50
c314dfe
a0c6b50
5844082
 
c314dfe
 
5844082
 
037071f
 
 
c314dfe
5844082
 
c314dfe
 
 
5844082
 
 
 
 
 
a0c6b50
91cd81f
 
1aea8cd
 
 
 
 
 
 
 
c6bd2df
1aea8cd
 
037071f
 
 
1aea8cd
 
 
 
 
 
 
 
 
 
 
 
c6bd2df
1aea8cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c6bd2df
1aea8cd
 
8ff2562
 
 
 
 
c6bd2df
1aea8cd
 
8ff2562
c9b4649
 
 
 
 
 
 
8ff2562
c6bd2df
8ff2562
1aea8cd
 
8ff2562
1aea8cd
 
 
 
 
 
 
 
 
417d30a
31d0432
417d30a
420774d
417d30a
 
 
 
 
 
 
 
037071f
8a75325
417d30a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91cd81f
 
 
 
 
 
917d393
 
91cd81f
 
037071f
 
91cd81f
917d393
3da8a04
917d393
 
 
 
91cd81f
 
 
 
917d393
 
 
 
 
 
 
 
 
 
 
91cd81f
917d393
91cd81f
 
917d393
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91cd81f
 
 
 
917d393
91cd81f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
import os
import logging
import asyncio
import aiohttp
from pydantic import BaseModel
from fastapi import APIRouter, HTTPException, Header, Query
from functools import lru_cache
from typing import List, Dict, Any, Optional

router = APIRouter()

# Configuração do Supabase
SUPABASE_URL = "https://ussxqnifefkgkaumjann.supabase.co"
SUPABASE_KEY = os.getenv("SUPA_KEY")
SUPABASE_ROLE_KEY = os.getenv("SUPA_SERVICE_KEY")

class FeedDeleteRequest(BaseModel):
    feed_id: int
    
if not SUPABASE_KEY or not SUPABASE_ROLE_KEY:
    raise ValueError("❌ SUPA_KEY ou SUPA_SERVICE_KEY não foram definidos no ambiente!")

SUPABASE_HEADERS = {
    "apikey": SUPABASE_KEY,
    "Authorization": f"Bearer {SUPABASE_KEY}",
    "Content-Type": "application/json"
}

SUPABASE_ROLE_HEADERS = {
    "apikey": SUPABASE_ROLE_KEY,
    "Authorization": f"Bearer {SUPABASE_ROLE_KEY}",
    "Content-Type": "application/json"
}

# Configuração do logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Cache para reduzir chamadas repetidas
@lru_cache(maxsize=128)
def get_cached_admin_status(user_id: str) -> bool:
    """Obtém e armazena em cache se um usuário é admin"""
    import requests
    user_data_url = f"{SUPABASE_URL}/rest/v1/User?id=eq.{user_id}"
    response = requests.get(user_data_url, headers=SUPABASE_HEADERS)
    
    if response.status_code != 200 or not response.json():
        return False
    
    user_info = response.json()[0]
    return user_info.get("is_admin", False)

# Nova função para verificar permissões de usuário
async def get_user_permissions(user_id: str) -> Dict[str, bool]:
    """Obtém as permissões de um usuário"""
    user_data_url = f"{SUPABASE_URL}/rest/v1/User?id=eq.{user_id}&select=is_admin,view_users,manage_users"
    
    async with aiohttp.ClientSession() as session:
        async with session.get(user_data_url, headers=SUPABASE_HEADERS) as response:
            if response.status != 200 or not await response.json():
                return {"is_admin": False, "view_users": False, "manage_users": False}
            
            user_info = (await response.json())[0]
            return {
                "is_admin": user_info.get("is_admin", False),
                "view_users": user_info.get("view_users", False),
                "manage_users": user_info.get("manage_users", False)
            }

async def verify_admin_token(user_token: str) -> str:
    """Verifica se o token pertence a um administrador de forma assíncrona"""
    headers = {
        "Authorization": f"Bearer {user_token}",
        "apikey": SUPABASE_KEY,
        "Content-Type": "application/json"
    }
    
    async with aiohttp.ClientSession() as session:
        async with session.get(f"{SUPABASE_URL}/auth/v1/user", headers=headers) as response:
            if response.status != 200:
                raise HTTPException(status_code=401, detail="Token inválido ou expirado")
            
            user_data = await response.json()
            user_id = user_data.get("id")
            if not user_id:
                raise HTTPException(status_code=400, detail="ID do usuário não encontrado")
    
    is_admin = await asyncio.to_thread(get_cached_admin_status, user_id)
    
    if not is_admin:
        raise HTTPException(status_code=403, detail="Acesso negado: privilégios de administrador necessários")
    
    return user_id

# Nova função para verificar token e retornar ID do usuário e permissões
async def verify_token_with_permissions(user_token: str, required_permission: Optional[str] = None) -> Dict[str, Any]:
    """Verifica o token e retorna ID do usuário e suas permissões"""
    headers = {
        "Authorization": f"Bearer {user_token}",
        "apikey": SUPABASE_KEY,
        "Content-Type": "application/json"
    }
    
    async with aiohttp.ClientSession() as session:
        async with session.get(f"{SUPABASE_URL}/auth/v1/user", headers=headers) as response:
            if response.status != 200:
                raise HTTPException(status_code=401, detail="Token inválido ou expirado")
            
            user_data = await response.json()
            user_id = user_data.get("id")
            if not user_id:
                raise HTTPException(status_code=400, detail="ID do usuário não encontrado")
    
    # Obter permissões do usuário
    permissions = await get_user_permissions(user_id)
    
    # Verificar se é admin
    is_admin = permissions.get("is_admin", False)
    
    # Verificar permissão específica, se requisitada
    if required_permission:
        has_permission = permissions.get(required_permission, False)
        if not has_permission:
            raise HTTPException(
                status_code=403, 
                detail=f"Acesso negado: permissão '{required_permission}' necessária"
            )
    
    return {
        "user_id": user_id,
        "is_admin": is_admin,
        "permissions": permissions
    }

async def get_recent_users(limit: int = 50, search: Optional[str] = None, page: int = 0) -> Dict[str, Any]:
    """Obtém os usuários mais recentes da plataforma, com filtro opcional por nome e paginação"""
    try:
        offset = page * limit
        limit_plus_one = limit + 1
        
        # Adicionada condição para filtrar apenas usuários não-admin
        query = f"{SUPABASE_URL}/rest/v1/User?select=id,name,avatar,role,blurhash&is_admin=eq.false&order=created_at.desc&limit={limit_plus_one}&offset={offset}"
        
        if search:
            search_term = search.replace("'", "''")
            query += f"&name=ilike.*{search_term}*"
        
        headers = SUPABASE_HEADERS.copy()
        headers["Accept"] = "application/json; charset=utf-8"
        
        async with aiohttp.ClientSession() as session:
            async with session.get(query, headers=headers) as response:
                if response.status != 200:
                    logger.error(f"❌ Erro ao obter usuários: {response.status}")
                    return {"users": [], "has_next_page": False}
                
                text = await response.text(encoding='utf-8')
                import json
                users_data = json.loads(text)
        
        has_next_page = len(users_data) > limit
        users = users_data[:limit]
        
        return {
            "users": users,
            "has_next_page": has_next_page
        }
        
    except Exception as e:
        logger.error(f"❌ Erro ao obter usuários: {str(e)}")
        return {"users": [], "has_next_page": False}

@router.get("/admin/users")
async def get_recent_users_endpoint(
    user_token: str = Header(None, alias="User-key"),
    limit: int = Query(50, ge=1, le=100),
    page: int = Query(0, ge=0),
    search: Optional[str] = Query(None)
):
    """
    Endpoint para obter os usuários mais recentes da plataforma,
    com suporte a busca por nome e paginação.
    """
    try:
        # Verificar se o usuário tem permissão para visualizar usuários
        user_info = await verify_token_with_permissions(user_token, "view_users")
        
        result = await get_recent_users(limit, search, page)
        
        return {
            "users": result["users"],
            "count": len(result["users"]),
            "has_next_page": result["has_next_page"]
        }
        
    except HTTPException as he:
        raise he
        
    except Exception as e:
        logger.error(f"❌ Erro ao obter usuários: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

@router.post("/admin/update-user")
async def update_user(
    request: Dict[str, Any],
    user_token: str = Header(None, alias="User-key")
):
    """
    Endpoint para atualizar informações de um usuário específico.
    Atualmente suporta atualização de name e fee.
    Usa a chave de serviço para bypassar RLS do Supabase.
    """
    try:
        # Verificar se o usuário tem permissão para gerenciar usuários
        user_info = await verify_token_with_permissions(user_token, "manage_users")
        admin_id = user_info["user_id"]
        
        # Validar os parâmetros da requisição
        user_id = request.get("user_id")
        name = request.get("name")
        fee = request.get("fee")
        
        if not user_id:
            raise HTTPException(status_code=400, detail="ID do usuário é obrigatório")
        
        if name is None and fee is None:
            raise HTTPException(status_code=400, detail="Pelo menos um campo para atualização (name ou fee) deve ser fornecido")
        
        # Verificar se o usuário existe - usando os headers normais para consulta
        user_query = f"{SUPABASE_URL}/rest/v1/User?select=name,fee&id=eq.{user_id}"
        
        async with aiohttp.ClientSession() as session:
            async with session.get(user_query, headers=SUPABASE_HEADERS) as response:
                if response.status != 200:
                    logger.error(f"❌ Erro ao verificar usuário: {response.status}")
                    raise HTTPException(status_code=response.status, detail="Erro ao consultar usuário")
                
                user_data = await response.json()
                
                if not user_data:
                    raise HTTPException(status_code=404, detail="Usuário não encontrado")
                
                current_user = user_data[0]
                
                # Preparar os campos para atualização, apenas se forem diferentes
                update_fields = {}
                
                if name is not None and name != current_user.get("name"):
                    update_fields["name"] = name
                
                if fee is not None and isinstance(fee, int) and fee != current_user.get("fee"):
                    update_fields["fee"] = fee
                
                # Se não há campos para atualizar, retornar sem modificar
                if not update_fields:
                    return {"message": "Nenhuma alteração necessária", "user_id": user_id}
                
                # Atualizar o usuário no Supabase usando a chave de serviço para bypassar RLS
                update_url = f"{SUPABASE_URL}/rest/v1/User?id=eq.{user_id}"
                
                headers_with_prefer = SUPABASE_ROLE_HEADERS.copy()
                headers_with_prefer["Prefer"] = "return=representation"
                
                async with session.patch(update_url, json=update_fields, headers=headers_with_prefer) as update_response:
                    if update_response.status != 200:
                        logger.error(f"❌ Erro ao atualizar usuário: {update_response.status} - {await update_response.text()}")
                        raise HTTPException(status_code=update_response.status, detail="Erro ao atualizar usuário")
                    
                    updated_user_data = await update_response.json()
                    raw_user = updated_user_data[0] if updated_user_data else {}
                    updated_user = {
                        "name": raw_user.get("name"),
                        "avatar": raw_user.get("avatar"),
                        "role": raw_user.get("role"),
                        "fee": raw_user.get("fee")
                    }
                
                    logger.info(f"✅ Usuário {user_id} atualizado com sucesso por {admin_id}: {update_fields}")
                
                    return {
                        "message": "Usuário atualizado com sucesso",
                        "user": updated_user,
                        "updated_by": admin_id
                    }
    
    except HTTPException as he:
        raise he
    
    except Exception as e:
        logger.error(f"❌ Erro ao atualizar usuário: {str(e)}")
        raise HTTPException(status_code=500, detail="Erro interno do servidor")

@router.post("/admin/delete-feed")
async def delete_feed(
    request: FeedDeleteRequest,
    user_token: str = Header(None, alias="User-key")
):
    """
    Deleta um feed, seus portfolios relacionados e as imagens no bucket.
    """
    try:
        await verify_admin_token(user_token)

        feed_id = request.feed_id

        headers = SUPABASE_ROLE_HEADERS.copy()
        headers["Accept"] = "application/json"

        async with aiohttp.ClientSession() as session:
            # Buscar o feed pelo ID
            feed_query = f"{SUPABASE_URL}/rest/v1/Feeds?select=portfolios&limit=1&id=eq.{feed_id}"
            async with session.get(feed_query, headers=headers) as feed_response:
                if feed_response.status != 200:
                    raise HTTPException(status_code=feed_response.status, detail="Erro ao buscar feed")
                
                feed_data = await feed_response.json()
                if not feed_data:
                    raise HTTPException(status_code=404, detail="Feed não encontrado")
                
                portfolio_ids = feed_data[0].get("portfolios", [])
                if not isinstance(portfolio_ids, list):
                    import json
                    portfolio_ids = json.loads(portfolio_ids)  # Caso venha como string JSON

            # Buscar os portfolios para pegar os URLs das imagens
            image_urls = []
            if portfolio_ids:
                ids_str = ",".join([str(pid) for pid in portfolio_ids])
                portfolio_query = f"{SUPABASE_URL}/rest/v1/Portfolio?select=id,image_url&id=in.({ids_str})"
                async with session.get(portfolio_query, headers=headers) as portfolio_response:
                    if portfolio_response.status == 200:
                        portfolio_data = await portfolio_response.json()
                        image_urls = [item["image_url"] for item in portfolio_data if "image_url" in item]

            # Deletar os portfolios
            if portfolio_ids:
                delete_portfolios_url = f"{SUPABASE_URL}/rest/v1/Portfolio?id=in.({','.join(map(str, portfolio_ids))})"
                async with session.delete(delete_portfolios_url, headers=headers) as delete_response:
                    if delete_response.status != 204:
                        raise HTTPException(status_code=delete_response.status, detail="Erro ao deletar portfolios")

            # Deletar imagens do bucket Supabase Storage
            async def delete_image_from_storage(image_url: str):
                from urllib.parse import urlparse
                path = urlparse(image_url).path
                # Extraindo bucket e key
                parts = path.strip("/").split("/")
                if len(parts) < 3:
                    return
                bucket_id = parts[1]
                file_key = "/".join(parts[2:])
                delete_url = f"{SUPABASE_URL}/storage/v1/object/{bucket_id}/{file_key}"
                async with session.delete(delete_url, headers=headers) as delete_img_response:
                    if delete_img_response.status not in (200, 204):
                        logger.warning(f"❌ Falha ao deletar imagem: {file_key}")

            await asyncio.gather(*[delete_image_from_storage(url) for url in image_urls])

            # Deletar o feed
            delete_feed_url = f"{SUPABASE_URL}/rest/v1/Feeds?id=eq.{feed_id}"
            async with session.delete(delete_feed_url, headers=headers) as delete_feed_response:
                if delete_feed_response.status != 204:
                    raise HTTPException(status_code=delete_feed_response.status, detail="Erro ao deletar feed")

        logger.info(f"✅ Feed {feed_id} e portfolios relacionados deletados com sucesso.")
        return {"message": f"Feed {feed_id} e portfolios deletados com sucesso."}

    except HTTPException as he:
        raise he
    except Exception as e:
        logger.error(f"❌ Erro ao deletar feed: {str(e)}")
        raise HTTPException(status_code=500, detail="Erro interno do servidor")

@router.get("/admin/user")
async def get_user_name(
    user_id: str = Query(..., description="ID do usuário"),
    user_token: str = Header(None, alias="User-key")
):
    """
    Endpoint para obter informações de um usuário específico a partir do ID,
    incluindo seus feeds e uma imagem de portfólio para cada feed.
    """
    try:
        # Verificar se o usuário tem permissão para visualizar usuários
        user_info = await verify_token_with_permissions(user_token, "view_users")

        # Buscar informações básicas do usuário
        user_query = f"{SUPABASE_URL}/rest/v1/User?select=name,avatar,role,fee&id=eq.{user_id}"
        
        # Buscar feeds do usuário
        feeds_query = f"{SUPABASE_URL}/rest/v1/Feeds?select=id,portfolios,created_at,description,urls,user_id&user_id=eq.{user_id}&order=created_at.desc"
        
        headers = SUPABASE_HEADERS.copy()
        headers["Accept"] = "application/json; charset=utf-8"

        async with aiohttp.ClientSession() as session:
            # Fazer as requisições em paralelo
            user_task = session.get(user_query, headers=headers)
            feeds_task = session.get(feeds_query, headers=headers)
            
            async with user_task as user_response, feeds_task as feeds_response:
                if user_response.status != 200 or feeds_response.status != 200:
                    status = user_response.status if user_response.status != 200 else feeds_response.status
                    raise HTTPException(status_code=status, detail="Erro ao consultar o Supabase")
                
                user_data = await user_response.json()
                feeds_data = await feeds_response.json()
                
                if not user_data:
                    raise HTTPException(status_code=404, detail="Usuário não encontrado")
                
                # Processar feeds para buscar imagens de portfólio
                feeds_with_images = []
                for feed in feeds_data:
                    feed_info = {
                        "id": feed["id"],
                        "created_at": feed["created_at"],
                        "description": feed["description"],
                        "urls": feed["urls"],
                        "portfolios": feed["portfolios"]
                    }
                    
                    # Verificar se há portfolios associados
                    if feed["portfolios"] and len(feed["portfolios"]) > 0:
                        # Pegar o primeiro portfolio do array
                        first_portfolio_id = feed["portfolios"][0]
                        
                        # Buscar informações da imagem deste portfolio
                        portfolio_query = f"{SUPABASE_URL}/rest/v1/Portfolio?select=image_url,blurhash,width,height&id=eq.{first_portfolio_id}"
                        
                        async with session.get(portfolio_query, headers=headers) as portfolio_response:
                            if portfolio_response.status == 200:
                                portfolio_data = await portfolio_response.json()
                                
                                if portfolio_data and len(portfolio_data) > 0:
                                    feed_info["thumbnail"] = {
                                        "image_url": portfolio_data[0]["image_url"],
                                        "blurhash": portfolio_data[0]["blurhash"],
                                        "width": portfolio_data[0]["width"],
                                        "height": portfolio_data[0]["height"]
                                    }
                    
                    feeds_with_images.append(feed_info)
                
                # Construir a resposta completa
                response = {
                    "user": user_data[0],
                    "feeds": feeds_with_images,
                    "feeds_count": len(feeds_with_images)
                }
                
                return response

    except HTTPException as he:
        raise he
    except Exception as e:
        logger.error(f"❌ Erro ao buscar usuário e feeds: {str(e)}")
        raise HTTPException(status_code=500, detail="Erro interno do servidor")