paijo77 commited on
Commit
0711d6f
·
verified ·
1 Parent(s): eb4aafa

update app/routers/sources.py

Browse files
Files changed (1) hide show
  1. app/routers/sources.py +306 -0
app/routers/sources.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status, Request, Query
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+ from sqlalchemy import select, and_
4
+ from typing import List, Optional
5
+ from pydantic import BaseModel, HttpUrl
6
+
7
+ from app.database import get_db
8
+ from app.dependencies import get_current_user, require_user, require_admin
9
+ from app.db_models import User, ProxySource
10
+ from app.models import SourceType
11
+ from app.source_validator import source_validator, SourceValidationResult
12
+ from app.models import SourceConfig
13
+
14
+ router = APIRouter(prefix="/api/v1", tags=["sources"])
15
+
16
+ # Access limiter from app state via request
17
+ from slowapi import Limiter
18
+ from slowapi.util import get_remote_address
19
+
20
+ limiter = Limiter(key_func=get_remote_address)
21
+
22
+
23
+ class SourceCreate(BaseModel):
24
+ url: HttpUrl
25
+ type: SourceType
26
+ name: Optional[str] = None
27
+ description: Optional[str] = None
28
+ is_paid: bool = False
29
+
30
+
31
+ class SourceUpdate(BaseModel):
32
+ name: Optional[str] = None
33
+ description: Optional[str] = None
34
+ enabled: Optional[bool] = None
35
+ is_paid: Optional[bool] = None
36
+
37
+
38
+ class SourceResponse(BaseModel):
39
+ id: int
40
+ url: str
41
+ type: str
42
+ name: Optional[str]
43
+ description: Optional[str]
44
+ is_paid: bool
45
+ enabled: bool
46
+ validated: bool
47
+ validation_error: Optional[str]
48
+ total_scraped: int
49
+ success_rate: float
50
+ is_admin_source: bool
51
+ is_owner: bool = False
52
+
53
+ class Config:
54
+ from_attributes = True
55
+
56
+
57
+ class UserStats(BaseModel):
58
+ total_sources: int
59
+ active_sources: int
60
+ total_proxies_contributed: int
61
+ avg_success_rate: float
62
+
63
+
64
+ @router.get("/my-stats", response_model=UserStats)
65
+ @limiter.limit("60/minute")
66
+ async def get_my_stats(
67
+ request: Request,
68
+ current_user: User = Depends(require_user),
69
+ session: AsyncSession = Depends(get_db),
70
+ ):
71
+ result = await session.execute(
72
+ select(ProxySource).where(ProxySource.user_id == current_user.id)
73
+ )
74
+ sources = result.scalars().all()
75
+
76
+ total_sources = len(sources)
77
+ active_sources = sum(1 for s in sources if s.enabled)
78
+ total_proxies_contributed = sum(s.total_scraped for s in sources)
79
+
80
+ avg_success_rate = 0.0
81
+ if total_sources > 0:
82
+ avg_success_rate = sum(s.success_rate for s in sources) / total_sources
83
+
84
+ return UserStats(
85
+ total_sources=total_sources,
86
+ active_sources=active_sources,
87
+ total_proxies_contributed=total_proxies_contributed,
88
+ avg_success_rate=avg_success_rate,
89
+ )
90
+
91
+
92
+ @router.get("/my-sources", response_model=List[SourceResponse])
93
+ @limiter.limit("60/minute")
94
+ async def get_my_sources(
95
+ request: Request,
96
+ current_user: User = Depends(require_user),
97
+ session: AsyncSession = Depends(get_db),
98
+ ):
99
+ result = await session.execute(
100
+ select(ProxySource).where(ProxySource.user_id == current_user.id)
101
+ )
102
+ sources = result.scalars().all()
103
+
104
+ return [
105
+ SourceResponse(**{**source.__dict__, "is_owner": True}) for source in sources
106
+ ]
107
+
108
+
109
+ @router.post("/my-sources", response_model=dict, status_code=status.HTTP_201_CREATED)
110
+ @limiter.limit("10/hour")
111
+ async def create_source(
112
+ request: Request,
113
+ source_data: SourceCreate,
114
+ current_user: User = Depends(require_user),
115
+ session: AsyncSession = Depends(get_db),
116
+ ):
117
+ result = await session.execute(
118
+ select(ProxySource).where(ProxySource.url == str(source_data.url))
119
+ )
120
+ existing = result.scalar_one_or_none()
121
+
122
+ if existing:
123
+ raise HTTPException(
124
+ status_code=status.HTTP_409_CONFLICT,
125
+ detail="This source URL already exists in the database",
126
+ )
127
+
128
+ source_config = SourceConfig(
129
+ url=source_data.url, type=source_data.type, enabled=True
130
+ )
131
+
132
+ validation_result: SourceValidationResult = await source_validator.validate_source(
133
+ source_config
134
+ )
135
+
136
+ if not validation_result.valid:
137
+ raise HTTPException(
138
+ status_code=status.HTTP_400_BAD_REQUEST,
139
+ detail={
140
+ "error": "Source validation failed",
141
+ "reason": validation_result.error_message,
142
+ },
143
+ )
144
+
145
+ new_source = ProxySource(
146
+ user_id=current_user.id,
147
+ url=str(source_data.url),
148
+ type=source_data.type.value,
149
+ name=source_data.name or str(source_data.url).split("/")[-1],
150
+ description=source_data.description,
151
+ is_paid=source_data.is_paid,
152
+ enabled=True,
153
+ validated=True,
154
+ is_admin_source=False,
155
+ )
156
+
157
+ session.add(new_source)
158
+ await session.commit()
159
+ await session.refresh(new_source)
160
+
161
+ return {
162
+ "message": "Source created successfully",
163
+ "source_id": new_source.id,
164
+ "validation": {
165
+ "proxy_count": validation_result.proxy_count,
166
+ "sample_proxies": validation_result.sample_proxies,
167
+ },
168
+ }
169
+
170
+
171
+ @router.put("/my-sources/{source_id}", response_model=SourceResponse)
172
+ @limiter.limit("30/minute")
173
+ async def update_source(
174
+ request: Request,
175
+ source_id: int,
176
+ update_data: SourceUpdate,
177
+ current_user: User = Depends(require_user),
178
+ session: AsyncSession = Depends(get_db),
179
+ ):
180
+ result = await session.execute(
181
+ select(ProxySource).where(
182
+ and_(ProxySource.id == source_id, ProxySource.user_id == current_user.id)
183
+ )
184
+ )
185
+ source = result.scalar_one_or_none()
186
+
187
+ if not source:
188
+ raise HTTPException(
189
+ status_code=status.HTTP_404_NOT_FOUND,
190
+ detail="Source not found or you don't have permission to edit it",
191
+ )
192
+
193
+ if source.is_admin_source and current_user.role != "admin":
194
+ raise HTTPException(
195
+ status_code=status.HTTP_403_FORBIDDEN,
196
+ detail="Cannot edit admin-protected sources",
197
+ )
198
+
199
+ if update_data.name is not None:
200
+ source.name = update_data.name
201
+ if update_data.description is not None:
202
+ source.description = update_data.description
203
+ if update_data.enabled is not None:
204
+ source.enabled = update_data.enabled
205
+ if update_data.is_paid is not None:
206
+ source.is_paid = update_data.is_paid
207
+
208
+ await session.commit()
209
+ await session.refresh(source)
210
+
211
+ return SourceResponse(**{**source.__dict__, "is_owner": True})
212
+
213
+
214
+ @router.delete("/my-sources/{source_id}", status_code=status.HTTP_204_NO_CONTENT)
215
+ @limiter.limit("30/minute")
216
+ async def delete_source(
217
+ request: Request,
218
+ source_id: int,
219
+ current_user: User = Depends(require_user),
220
+ session: AsyncSession = Depends(get_db),
221
+ ):
222
+ result = await session.execute(
223
+ select(ProxySource).where(
224
+ and_(ProxySource.id == source_id, ProxySource.user_id == current_user.id)
225
+ )
226
+ )
227
+ source = result.scalar_one_or_none()
228
+
229
+ if not source:
230
+ raise HTTPException(
231
+ status_code=status.HTTP_404_NOT_FOUND,
232
+ detail="Source not found or you don't have permission to delete it",
233
+ )
234
+
235
+ if source.is_admin_source:
236
+ if current_user.role != "admin":
237
+ raise HTTPException(
238
+ status_code=status.HTTP_403_FORBIDDEN,
239
+ detail="Only admins can delete admin-protected sources",
240
+ )
241
+
242
+ await session.delete(source)
243
+ await session.commit()
244
+
245
+ return None
246
+
247
+
248
+ @router.get("/admin/sources", response_model=dict)
249
+ @limiter.limit("30/minute")
250
+ async def admin_get_all_sources(
251
+ request: Request,
252
+ limit: int = Query(50, ge=1, le=200),
253
+ offset: int = Query(0, ge=0),
254
+ current_user: User = Depends(require_admin),
255
+ session: AsyncSession = Depends(get_db),
256
+ ):
257
+ from sqlalchemy import func
258
+
259
+ total_result = await session.execute(select(func.count()).select_from(ProxySource))
260
+ total = total_result.scalar() or 0
261
+
262
+ result = await session.execute(
263
+ select(ProxySource)
264
+ .limit(limit)
265
+ .offset(offset)
266
+ .order_by(ProxySource.created_at.desc())
267
+ )
268
+ sources = result.scalars().all()
269
+
270
+ return {
271
+ "total": total,
272
+ "count": len(sources),
273
+ "offset": offset,
274
+ "limit": limit,
275
+ "sources": [
276
+ SourceResponse(
277
+ **{**source.__dict__, "is_owner": source.user_id == current_user.id}
278
+ )
279
+ for source in sources
280
+ ],
281
+ }
282
+
283
+
284
+ @router.post("/admin/sources/{source_id}/protect", response_model=SourceResponse)
285
+ async def admin_protect_source(
286
+ source_id: int,
287
+ current_user: User = Depends(require_admin),
288
+ session: AsyncSession = Depends(get_db),
289
+ ):
290
+ result = await session.execute(
291
+ select(ProxySource).where(ProxySource.id == source_id)
292
+ )
293
+ source = result.scalar_one_or_none()
294
+
295
+ if not source:
296
+ raise HTTPException(
297
+ status_code=status.HTTP_404_NOT_FOUND, detail="Source not found"
298
+ )
299
+
300
+ source.is_admin_source = True
301
+ await session.commit()
302
+ await session.refresh(source)
303
+
304
+ return SourceResponse(
305
+ **{**source.__dict__, "is_owner": source.user_id == current_user.id}
306
+ )