@@ -22,6 +22,7 @@ from .fonts import fonts, stylesheet
|
||||
from .models.api import UploadResponse, RenderStatus, ErrorEnvelope
|
||||
from .models.schematic import Document
|
||||
from .uploads import read_schematic
|
||||
from .job_types import job_kind
|
||||
|
||||
FRONTEND = Path(__file__).resolve().parents[2] / 'frontend'
|
||||
ID = re.compile(r'^[a-f0-9]{48}$')
|
||||
@@ -54,7 +55,7 @@ class UploadLimit:
|
||||
def __init__(self, app): self.app = app
|
||||
async def __call__(self, scope, receive, send):
|
||||
if scope['type'] != 'http': return await self.app(scope, receive, send)
|
||||
file_count = 2 if scope['path'] in ('/api/compare','/api/v1/comparisons') else 1
|
||||
file_count = 2 if scope['path'] in ('/api/compare','/api/v1/comparisons','/api/v1/pcbs/comparisons') else 1
|
||||
limit = file_count * settings().max_upload_bytes + 1024 * 1024
|
||||
received = 0
|
||||
async def bounded_receive():
|
||||
@@ -71,7 +72,7 @@ app.add_middleware(UploadLimit)
|
||||
@app.middleware('http')
|
||||
async def security(request, call_next):
|
||||
# Reject unauthorized POST bodies before the multipart parser reads them.
|
||||
if request.method == 'POST' and request.url.path in ('/api/v1/schematics/render','/api/compare','/api/v1/comparisons'):
|
||||
if request.method == 'POST' and request.url.path in ('/api/v1/schematics/render','/api/compare','/api/v1/comparisons','/api/v1/pcbs/render','/api/v1/pcbs/comparisons'):
|
||||
if settings().api_key and not authenticated(request.headers.get('x-api-key')):
|
||||
return JSONResponse({'success': False, 'error': {'code': 'unauthorized', 'message': 'Provide a valid X-API-Key header'}}, 401)
|
||||
response = await call_next(request)
|
||||
@@ -109,10 +110,11 @@ def health():
|
||||
with sessions()() as db: db.execute(text('SELECT 1'))
|
||||
return {'status': 'ok'}
|
||||
|
||||
def authorize(identifier, key, token, *, comparison=False):
|
||||
def authorize(identifier, key, token, *, comparison=False, resource_kind=None):
|
||||
if not ID.fullmatch(identifier): error('not_found', 'Schematic not found', 404)
|
||||
with sessions()() as db: job = db.get(Job, identifier)
|
||||
if job is None or job.input_key.endswith('/comparison.json') != comparison:
|
||||
expected_kind = resource_kind or ('schematic_comparison' if comparison else 'schematic_render')
|
||||
if job is None or job_kind(job.input_key) != expected_kind:
|
||||
error('not_found', 'Schematic not found', 404)
|
||||
# Private existence and expiry are not disclosed to unauthenticated callers.
|
||||
if job.access == 'private' and not (authenticated(key) or (token and hmac.compare_digest(job.token_hash, digest(token)))):
|
||||
@@ -201,7 +203,7 @@ def index(): return FileResponse(FRONTEND / 'index.html')
|
||||
|
||||
@app.get('/assets/{name}', include_in_schema=False)
|
||||
def asset(name: str):
|
||||
if name not in {'viewer.js','viewer.css','upload.js','upload.css','camera.js','compare.js','compare.css','compare-upload.js','interactions.js'}: error('not_found', 'Asset not found', 404)
|
||||
if name not in {'viewer.js','viewer.css','upload.js','upload.css','camera.js','compare.js','compare.css','compare-upload.js','interactions.js','pcb.js','pcb.css','pcb-layers.js'}: error('not_found', 'Asset not found', 404)
|
||||
return FileResponse(FRONTEND / name)
|
||||
|
||||
@app.get('/docs', include_in_schema=False)
|
||||
@@ -224,3 +226,5 @@ async def database_error(request, exc):
|
||||
# Comparison jobs share the existing queue, access policy and error envelope.
|
||||
from .compare_api import register_comparison_routes
|
||||
register_comparison_routes(app, authorize=authorize, require_api=require_api, error=error)
|
||||
from .pcb_api import register_pcb_routes
|
||||
register_pcb_routes(app, authorize=authorize, require_api=require_api, error=error)
|
||||
|
||||
مرجع در شماره جدید
مسدود کردن کاربر