feat: Passwordless auth — backend deployad, frontend uppdaterad
- Backend: Docker-container på port 8082 - Redis: Intern container (ingen exposed port) - Nginx-config: nginx-passwordless.conf (väntar på deploy) - Webb: API_BASE uppdaterad till api.quixzoom.com - Fix: datetime timezone-aware i approve-endpoint - Test: End-to-end flöde verifierat
This commit is contained in:
@@ -5,7 +5,7 @@ services:
|
||||
build: .
|
||||
container_name: quixzoom-passwordless
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "8082:8080"
|
||||
environment:
|
||||
- PORT=8080
|
||||
- HOST=0.0.0.0
|
||||
@@ -28,8 +28,7 @@ services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: quixzoom-auth-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
# No exposed port - internal only
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
networks:
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# quiXzoom Passwordless Auth — Nginx konfiguration
|
||||
# Lägg till i /etc/nginx/conf.d/ eller inkludera från huvudkonfig
|
||||
|
||||
upstream quixzoom_passwordless {
|
||||
server 127.0.0.1:8082;
|
||||
keepalive 16;
|
||||
}
|
||||
|
||||
# Passwordless auth endpoints
|
||||
location /v1/auth/passwordless {
|
||||
proxy_pass http://quixzoom_passwordless;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# CORS
|
||||
add_header Access-Control-Allow-Origin "https://quixzoom.se" always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
|
||||
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
|
||||
|
||||
# Handle preflight
|
||||
if ($request_method = OPTIONS) {
|
||||
add_header Access-Control-Allow-Origin "https://quixzoom.se";
|
||||
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
|
||||
add_header Access-Control-Allow-Headers "Content-Type, Authorization";
|
||||
add_header Access-Control-Max-Age 86400;
|
||||
return 204;
|
||||
}
|
||||
|
||||
# Rate limiting
|
||||
limit_req zone=general burst=20 nodelay;
|
||||
|
||||
# Timeouts
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_send_timeout 10s;
|
||||
proxy_read_timeout 10s;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import os
|
||||
import time
|
||||
import hashlib
|
||||
import secrets
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, HTTPException, Header, Request, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
@@ -229,11 +229,21 @@ async def approve_passwordless(
|
||||
|
||||
# Verify timestamp is recent (within 5 minutes)
|
||||
try:
|
||||
timestamp = datetime.fromisoformat(body.timestamp.replace('Z', '+00:00'))
|
||||
if datetime.utcnow() - timestamp > timedelta(minutes=5):
|
||||
# Parse timestamp and make it offset-aware
|
||||
timestamp_str = body.timestamp.replace('Z', '+00:00')
|
||||
timestamp = datetime.fromisoformat(timestamp_str)
|
||||
|
||||
# Ensure timestamp is offset-aware
|
||||
if timestamp.tzinfo is None:
|
||||
timestamp = timestamp.replace(tzinfo=timezone.utc)
|
||||
|
||||
# Get current UTC time as offset-aware
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
if now - timestamp > timedelta(minutes=5):
|
||||
raise HTTPException(status_code=403, detail="Timestamp too old")
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid timestamp format")
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid timestamp format: {str(e)}")
|
||||
|
||||
# TODO: Verify signature
|
||||
# expected_signature = hmac_sha256(session_id + request_token + timestamp, device_secret)
|
||||
|
||||
Reference in New Issue
Block a user