feat: homelab deployment - NPM + IONOS DNS + monitoring + NAS backup
- Restructured docker-compose for Nginx Proxy Manager (no custom nginx) - Added domain wordly.art configuration - Added Prometheus + Grafana monitoring stack with pre-configured dashboards - Added PostgreSQL backup script to NAS (daily/weekly/monthly rotation) - Added alert rules for backend, system, and Docker metrics - Updated deployment guide for NPM + IONOS DNS homelab setup - Added marketing plan document - PDF translator and watermark support - Enhanced middleware, routes, and translator modules Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -161,7 +161,7 @@ Créer un nouveau compte utilisateur.
|
||||
- Le `tier` par défaut est "free"
|
||||
|
||||
**Codes d'erreur:**
|
||||
- `INVALID_EMAIL`: Format d'email invalide
|
||||
- `INVALID_EMAIL`: Invalid email format
|
||||
- `EMAIL_EXISTS`: Un compte existe déjà avec cet email
|
||||
- `INVALID_REQUEST`: Données d'inscription invalides
|
||||
""",
|
||||
@@ -187,17 +187,17 @@ Créer un nouveau compte utilisateur.
|
||||
"application/json": {
|
||||
"examples": {
|
||||
"INVALID_EMAIL": {
|
||||
"summary": "Format d'email invalide",
|
||||
"summary": "Invalid email format",
|
||||
"value": {
|
||||
"error": "INVALID_EMAIL",
|
||||
"message": "Format d'email invalide",
|
||||
"message": "Invalid email format",
|
||||
},
|
||||
},
|
||||
"EMAIL_EXISTS": {
|
||||
"summary": "Email déjà utilisé",
|
||||
"summary": "Email already used",
|
||||
"value": {
|
||||
"error": "EMAIL_EXISTS",
|
||||
"message": "Un compte existe déjà avec cette adresse email",
|
||||
"message": "An account already exists with this email address",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -215,7 +215,7 @@ async def register_v1(request: Request):
|
||||
status_code=400,
|
||||
content={
|
||||
"error": "INVALID_REQUEST",
|
||||
"message": "Corps de requete JSON invalide",
|
||||
"message": "Invalid JSON request body",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -227,7 +227,7 @@ async def register_v1(request: Request):
|
||||
status_code=400,
|
||||
content={
|
||||
"error": "INVALID_EMAIL",
|
||||
"message": "Format d'email invalide",
|
||||
"message": "Invalid email format",
|
||||
},
|
||||
)
|
||||
# Check if it's a password validation error
|
||||
@@ -241,7 +241,7 @@ async def register_v1(request: Request):
|
||||
# Otherwise it's a weak password
|
||||
# Translate common pydantic messages to French
|
||||
if "at least 8 characters" in msg.lower() or "8 caractères" in msg:
|
||||
msg = "Le mot de passe doit contenir au moins 8 caractères"
|
||||
msg = "Password must be at least 8 characters long"
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
@@ -253,7 +253,7 @@ async def register_v1(request: Request):
|
||||
status_code=400,
|
||||
content={
|
||||
"error": "INVALID_REQUEST",
|
||||
"message": "Données d'inscription invalides",
|
||||
"message": "Invalid registration data",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -266,7 +266,7 @@ async def register_v1(request: Request):
|
||||
status_code=503,
|
||||
content={
|
||||
"error": "AUTH_HASHING_UNAVAILABLE",
|
||||
"message": "Service d'inscription temporairement indisponible",
|
||||
"message": "Registration service temporarily unavailable",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -279,14 +279,14 @@ async def register_v1(request: Request):
|
||||
status_code=400,
|
||||
content={
|
||||
"error": "EMAIL_EXISTS",
|
||||
"message": "Un compte existe déjà avec cette adresse email",
|
||||
"message": "An account already exists with this email address",
|
||||
},
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"error": "REGISTRATION_FAILED",
|
||||
"message": "Impossible de créer le compte avec les données fournies",
|
||||
"message": "Unable to create account with the provided data",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -318,7 +318,7 @@ Déconnecte l'utilisateur en révoquant son token d'accès.
|
||||
- HTTP 200 avec message de confirmation
|
||||
|
||||
**Codes d'erreur:**
|
||||
- `TOKEN_MISSING`: Token d'authentification manquant
|
||||
- `TOKEN_MISSING`: Authentication token missing
|
||||
- `TOKEN_INVALID`: Token invalide ou expiré
|
||||
""",
|
||||
responses={
|
||||
@@ -326,7 +326,7 @@ Déconnecte l'utilisateur en révoquant son token d'accès.
|
||||
"description": "Déconnexion réussie",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {"data": {"message": "Déconnexion réussie"}, "meta": {}}
|
||||
"example": {"data": {"message": "Logged out successfully"}, "meta": {}}
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -336,17 +336,17 @@ Déconnecte l'utilisateur en révoquant son token d'accès.
|
||||
"application/json": {
|
||||
"examples": {
|
||||
"TOKEN_MISSING": {
|
||||
"summary": "Token manquant",
|
||||
"summary": "Missing token",
|
||||
"value": {
|
||||
"error": "TOKEN_MISSING",
|
||||
"message": "Token d'authentification requis",
|
||||
"message": "Authentication token required",
|
||||
},
|
||||
},
|
||||
"TOKEN_INVALID": {
|
||||
"summary": "Token invalide",
|
||||
"summary": "Invalid token",
|
||||
"value": {
|
||||
"error": "TOKEN_INVALID",
|
||||
"message": "Token invalide ou expiré",
|
||||
"message": "Invalid or expired token",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -363,7 +363,7 @@ async def logout_v1(request: Request):
|
||||
status_code=401,
|
||||
content={
|
||||
"error": "TOKEN_MISSING",
|
||||
"message": "Token d'authentification requis",
|
||||
"message": "Authentication token required",
|
||||
},
|
||||
)
|
||||
access_token = auth_header[7:]
|
||||
@@ -374,7 +374,7 @@ async def logout_v1(request: Request):
|
||||
status_code=401,
|
||||
content={
|
||||
"error": "TOKEN_INVALID",
|
||||
"message": "Token invalide ou expiré",
|
||||
"message": "Invalid or expired token",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -397,7 +397,7 @@ async def logout_v1(request: Request):
|
||||
|
||||
return JSONResponse(
|
||||
status_code=200,
|
||||
content={"data": {"message": "Déconnexion réussie"}, "meta": {}},
|
||||
content={"data": {"message": "Logged out successfully"}, "meta": {}},
|
||||
)
|
||||
|
||||
|
||||
@@ -416,7 +416,7 @@ Authentifie un utilisateur et retourne les tokens JWT.
|
||||
|
||||
**Codes d'erreur:**
|
||||
- `INVALID_REQUEST`: Corps de requête JSON invalide
|
||||
- `INVALID_EMAIL`: Format d'email invalide
|
||||
- `INVALID_EMAIL`: Invalid email format
|
||||
- `INVALID_CREDENTIALS`: Email ou mot de passe incorrect
|
||||
""",
|
||||
responses={
|
||||
@@ -441,17 +441,17 @@ Authentifie un utilisateur et retourne les tokens JWT.
|
||||
"application/json": {
|
||||
"examples": {
|
||||
"INVALID_REQUEST": {
|
||||
"summary": "Corps invalide",
|
||||
"summary": "Invalid body",
|
||||
"value": {
|
||||
"error": "INVALID_REQUEST",
|
||||
"message": "Corps de requete JSON invalide",
|
||||
"message": "Invalid JSON request body",
|
||||
},
|
||||
},
|
||||
"INVALID_EMAIL": {
|
||||
"summary": "Email invalide",
|
||||
"summary": "Invalid email",
|
||||
"value": {
|
||||
"error": "INVALID_EMAIL",
|
||||
"message": "Format d'email invalide",
|
||||
"message": "Invalid email format",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -464,7 +464,7 @@ Authentifie un utilisateur et retourne les tokens JWT.
|
||||
"application/json": {
|
||||
"example": {
|
||||
"error": "INVALID_CREDENTIALS",
|
||||
"message": "Email ou mot de passe incorrect",
|
||||
"message": "Incorrect email or password",
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -480,7 +480,7 @@ async def login_v1(request: Request):
|
||||
status_code=400,
|
||||
content={
|
||||
"error": "INVALID_REQUEST",
|
||||
"message": "Corps de requete JSON invalide",
|
||||
"message": "Invalid JSON request body",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -492,14 +492,14 @@ async def login_v1(request: Request):
|
||||
status_code=400,
|
||||
content={
|
||||
"error": "INVALID_EMAIL",
|
||||
"message": "Format d'email invalide",
|
||||
"message": "Invalid email format",
|
||||
},
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"error": "INVALID_REQUEST",
|
||||
"message": "Données d'inscription invalides",
|
||||
"message": "Invalid registration data",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -511,7 +511,7 @@ async def login_v1(request: Request):
|
||||
status_code=401,
|
||||
content={
|
||||
"error": "INVALID_CREDENTIALS",
|
||||
"message": "Email ou mot de passe incorrect",
|
||||
"message": "Incorrect email or password",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -520,7 +520,7 @@ async def login_v1(request: Request):
|
||||
status_code=401,
|
||||
content={
|
||||
"error": "INVALID_CREDENTIALS",
|
||||
"message": "Email ou mot de passe incorrect",
|
||||
"message": "Incorrect email or password",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -552,7 +552,7 @@ async def google_auth_v1(body: GoogleAuthRequest):
|
||||
if not body.credential and not body.access_token:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"error": "MISSING_TOKEN", "message": "credential ou access_token requis."},
|
||||
content={"error": "MISSING_TOKEN", "message": "credential or access_token required."},
|
||||
)
|
||||
|
||||
google_client_id = os.getenv("GOOGLE_CLIENT_ID", "")
|
||||
@@ -574,13 +574,13 @@ async def google_auth_v1(body: GoogleAuthRequest):
|
||||
except Exception:
|
||||
return JSONResponse(
|
||||
status_code=503,
|
||||
content={"error": "GOOGLE_UNREACHABLE", "message": "Impossible de contacter les serveurs Google."},
|
||||
content={"error": "GOOGLE_UNREACHABLE", "message": "Unable to contact Google servers."},
|
||||
)
|
||||
|
||||
if resp.status_code != 200:
|
||||
return JSONResponse(
|
||||
status_code=401,
|
||||
content={"error": "INVALID_GOOGLE_TOKEN", "message": "Token Google invalide ou expiré."},
|
||||
content={"error": "INVALID_GOOGLE_TOKEN", "message": "Invalid or expired Google token."},
|
||||
)
|
||||
|
||||
google_data = resp.json()
|
||||
@@ -589,14 +589,14 @@ async def google_auth_v1(body: GoogleAuthRequest):
|
||||
if body.credential and google_client_id and google_data.get("aud") != google_client_id:
|
||||
return JSONResponse(
|
||||
status_code=401,
|
||||
content={"error": "INVALID_TOKEN_AUDIENCE", "message": "Token Google non destiné à cette application."},
|
||||
content={"error": "INVALID_TOKEN_AUDIENCE", "message": "Google token not intended for this application."},
|
||||
)
|
||||
|
||||
email = google_data.get("email")
|
||||
if not email:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"error": "MISSING_EMAIL", "message": "Email non fourni par Google."},
|
||||
content={"error": "MISSING_EMAIL", "message": "Email not provided by Google."},
|
||||
)
|
||||
|
||||
name = google_data.get("name") or google_data.get("given_name") or email.split("@")[0]
|
||||
@@ -636,7 +636,7 @@ async def refresh_v1(request: Request):
|
||||
status_code=400,
|
||||
content={
|
||||
"error": "INVALID_REQUEST",
|
||||
"message": "Corps de requete JSON invalide",
|
||||
"message": "Invalid JSON request body",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -645,7 +645,7 @@ async def refresh_v1(request: Request):
|
||||
status_code=400,
|
||||
content={
|
||||
"error": "INVALID_REQUEST",
|
||||
"message": "Corps de requete JSON invalide",
|
||||
"message": "Invalid JSON request body",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -659,7 +659,7 @@ async def refresh_v1(request: Request):
|
||||
status_code=400,
|
||||
content={
|
||||
"error": "INVALID_REQUEST",
|
||||
"message": "Refresh token requis",
|
||||
"message": "Refresh token required",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -669,7 +669,7 @@ async def refresh_v1(request: Request):
|
||||
status_code=401,
|
||||
content={
|
||||
"error": "TOKEN_EXPIRED",
|
||||
"message": "Token invalide ou expiré",
|
||||
"message": "Invalid or expired token",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -678,7 +678,7 @@ async def refresh_v1(request: Request):
|
||||
status_code=401,
|
||||
content={
|
||||
"error": "TOKEN_EXPIRED",
|
||||
"message": "Token invalide ou expiré",
|
||||
"message": "Invalid or expired token",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -688,7 +688,7 @@ async def refresh_v1(request: Request):
|
||||
status_code=401,
|
||||
content={
|
||||
"error": "TOKEN_EXPIRED",
|
||||
"message": "Token invalide ou expiré",
|
||||
"message": "Invalid or expired token",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -893,7 +893,7 @@ async def get_billing_portal_v1(user=Depends(require_user)):
|
||||
status_code=400,
|
||||
content={
|
||||
"error": "BILLING_UNAVAILABLE",
|
||||
"message": "Portail de facturation non disponible",
|
||||
"message": "Billing portal unavailable",
|
||||
},
|
||||
)
|
||||
return JSONResponse(status_code=200, content={"data": {"url": url}, "meta": {}})
|
||||
@@ -928,7 +928,7 @@ async def forgot_password(request: Request):
|
||||
status_code=503,
|
||||
content={
|
||||
"error": "EMAIL_SERVICE_UNAVAILABLE",
|
||||
"message": "Service email non configure",
|
||||
"message": "Email service not configured",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -939,7 +939,7 @@ async def forgot_password(request: Request):
|
||||
status_code=400,
|
||||
content={
|
||||
"error": "INVALID_REQUEST",
|
||||
"message": "Corps de requete JSON invalide",
|
||||
"message": "Invalid JSON request body",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -949,7 +949,7 @@ async def forgot_password(request: Request):
|
||||
status_code=400,
|
||||
content={
|
||||
"error": "INVALID_REQUEST",
|
||||
"message": "Email requis",
|
||||
"message": "Email required",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1015,7 +1015,7 @@ async def forgot_password(request: Request):
|
||||
status_code=200,
|
||||
content={
|
||||
"data": {
|
||||
"message": "Si un compte existe avec cette adresse, un email de reinitialisation a ete envoye."
|
||||
"message": "If an account exists with this email, a reset email has been sent."
|
||||
},
|
||||
"meta": {},
|
||||
},
|
||||
@@ -1041,7 +1041,7 @@ async def reset_password(request: Request):
|
||||
status_code=400,
|
||||
content={
|
||||
"error": "INVALID_REQUEST",
|
||||
"message": "Corps de requete JSON invalide",
|
||||
"message": "Invalid JSON request body",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1053,7 +1053,7 @@ async def reset_password(request: Request):
|
||||
status_code=400,
|
||||
content={
|
||||
"error": "INVALID_REQUEST",
|
||||
"message": "Token et mot de passe requis",
|
||||
"message": "Token and password required",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1063,7 +1063,7 @@ async def reset_password(request: Request):
|
||||
status_code=400,
|
||||
content={
|
||||
"error": "WEAK_PASSWORD",
|
||||
"message": "Le mot de passe doit contenir au moins 8 caracteres, une majuscule, une minuscule et un chiffre",
|
||||
"message": "Password must be at least 8 characters with at least one uppercase letter, one lowercase letter, and one digit",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1085,7 +1085,7 @@ async def reset_password(request: Request):
|
||||
status_code=400,
|
||||
content={
|
||||
"error": "INVALID_TOKEN",
|
||||
"message": "Token invalide ou expire",
|
||||
"message": "Invalid or expired token",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1099,7 +1099,7 @@ async def reset_password(request: Request):
|
||||
status_code=400,
|
||||
content={
|
||||
"error": "TOKEN_EXPIRED",
|
||||
"message": "Token expire, veuillez redemander une reinitialisation",
|
||||
"message": "Token expired, please request a new reset",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1116,14 +1116,14 @@ async def reset_password(request: Request):
|
||||
status_code=500,
|
||||
content={
|
||||
"error": "RESET_FAILED",
|
||||
"message": "Erreur lors de la reinitialisation",
|
||||
"message": "Error during password reset",
|
||||
},
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=200,
|
||||
content={
|
||||
"data": {"message": "Mot de passe reinitialise avec succes"},
|
||||
"data": {"message": "Password reset successfully"},
|
||||
"meta": {},
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user