Compare commits

..

8 Commits

Author SHA1 Message Date
herel dd9bdf84f3 fix: update dependencies (1.4.1) 2026-07-01 16:09:24 +02:00
herel 80f4b4047c chore(deploy): 512Mi ram 2026-07-01 15:56:08 +02:00
herel cc64d126be fix(api): Add upper bound validation to num_exercises
The /generate and /generate-operations endpoints accepted num_exercises
without an upper bound, allowing DoS via excessively large values.
Add Field(le=100) to both ExerciseRequest and OperationExerciseRequest
Pydantic models to cap exercises at 100.

Fixes vuln-0001
2026-07-01 15:07:10 +02:00
herel e4db258bc5 fix: update version 2025-12-10 11:38:16 +01:00
herel 449adc60e0 feat: dark mode and python 3.14 2025-12-10 11:36:40 +01:00
herel d68dc8411f chore: speedup readyness 2025-10-15 17:25:08 +02:00
herel 60f776c56d chore: service internal traffic policy 2025-10-15 17:24:13 +02:00
herel 7228dc4ec6 chore: add scale-to-zero overlay 2025-10-15 17:11:30 +02:00
10 changed files with 299 additions and 39 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
# Use Python 3.13 slim image as base # Use Python 3.13 slim image as base
FROM python:3.13-slim AS builder FROM python:3.14-slim AS builder
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app
@@ -17,7 +17,7 @@ RUN --mount=type=cache,id=pip,target=/root/.cache/pip \
--target=/app/site-packages \ --target=/app/site-packages \
-r requirements.txt -r requirements.txt
FROM python:3.13-slim FROM python:3.14-slim
COPY --from=builder /app/site-packages /app/site-packages COPY --from=builder /app/site-packages /app/site-packages
+4 -4
View File
@@ -8,7 +8,7 @@ from typing import List
from fastapi import FastAPI, Request, Form from fastapi import FastAPI, Request, Form
from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from pydantic import BaseModel from pydantic import BaseModel, Field
from fpdf import FPDF from fpdf import FPDF
import logging import logging
@@ -68,13 +68,13 @@ class MathExercisesPDF(FPDF):
class ExerciseRequest(BaseModel): class ExerciseRequest(BaseModel):
min_table: int min_table: int
max_table: int max_table: int
num_exercises: int = 15 num_exercises: int = Field(15, ge=1, le=100)
multiplication_only: bool = False multiplication_only: bool = False
max_first_operand: int = 12 # New parameter for the maximum value of the first operand max_first_operand: int = Field(12, ge=1, le=12) # New parameter for the maximum value of the first operand
class OperationExerciseRequest(BaseModel): class OperationExerciseRequest(BaseModel):
num_exercises: int = 20 num_exercises: int = Field(20, ge=1, le=100)
def generate_exercises( def generate_exercises(
+168 -2
View File
@@ -5,17 +5,42 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Générateur d'Exercices de Mathématiques</title> <title>Générateur d'Exercices de Mathématiques</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<style> <style>
:root {
--bs-body-bg: #ffffff;
--bs-body-color: #212529;
--bs-border-color: #dee2e6;
--bs-secondary-bg: #f8f9fa;
--bs-tertiary-bg: #ffffff;
}
[data-bs-theme="dark"] {
--bs-body-bg: #121212;
--bs-body-color: #e9ecef;
--bs-border-color: #495057;
--bs-secondary-bg: #212529;
--bs-tertiary-bg: #343a40;
}
body {
background-color: var(--bs-body-bg);
color: var(--bs-body-color);
transition: background-color 0.3s ease, color 0.3s ease;
}
.card { .card {
box-shadow: 0 4px 8px rgba(0,0,0,0.1); box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transition: 0.3s; transition: 0.3s;
margin-bottom: 20px; margin-bottom: 20px;
background-color: var(--bs-tertiary-bg);
border-color: var(--bs-border-color);
} }
.card:hover { .card:hover {
box-shadow: 0 8px 16px rgba(0,0,0,0.2); box-shadow: 0 8px 16px rgba(0,0,0,0.2);
} }
.exercise-result { .exercise-result {
background-color: #f8f9fa; background-color: var(--bs-secondary-bg);
border-radius: 5px; border-radius: 5px;
padding: 15px; padding: 15px;
margin-top: 20px; margin-top: 20px;
@@ -25,9 +50,81 @@
height: 3rem; height: 3rem;
margin-right: 0.75rem; margin-right: 0.75rem;
} }
.dark-mode-toggle {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
border-radius: 50%;
width: 50px;
height: 50px;
border: none;
background-color: var(--bs-tertiary-bg);
color: var(--bs-body-color);
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
.dark-mode-toggle:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
[data-bs-theme="dark"] .bg-light {
background-color: var(--bs-secondary-bg) !important;
}
[data-bs-theme="dark"] .text-muted {
color: var(--bs-body-color) !important;
opacity: 0.7;
}
[data-bs-theme="dark"] .table {
--bs-table-bg: var(--bs-tertiary-bg);
--bs-table-striped-bg: var(--bs-secondary-bg);
--bs-table-hover-bg: var(--bs-border-color);
}
[data-bs-theme="dark"] .alert {
border-color: var(--bs-border-color);
}
[data-bs-theme="dark"] .form-control,
[data-bs-theme="dark"] .form-select {
background-color: var(--bs-tertiary-bg);
border-color: var(--bs-border-color);
color: var(--bs-body-color);
}
[data-bs-theme="dark"] .form-control:focus,
[data-bs-theme="dark"] .form-select:focus {
background-color: var(--bs-tertiary-bg);
border-color: var(--bs-primary);
color: var(--bs-body-color);
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
[data-bs-theme="dark"] .form-check-input {
background-color: var(--bs-tertiary-bg);
border-color: var(--bs-border-color);
}
[data-bs-theme="dark"] .form-check-input:checked {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
}
.transition-all {
transition: all 0.3s ease;
}
</style> </style>
</head> </head>
<body> <body>
<!-- Dark Mode Toggle Button -->
<button class="dark-mode-toggle" id="darkModeToggle" title="Basculer le mode sombre" aria-label="Basculer le mode sombre">
<i class="bi bi-moon-fill" id="darkModeIcon"></i>
</button>
<div class="container mt-5 mb-5"> <div class="container mt-5 mb-5">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-lg-8"> <div class="col-lg-8">
@@ -251,7 +348,7 @@
</div> </div>
</div> </div>
<footer class="bg-light text-center text-muted py-4 mt-5"> <footer class="bg-light text-center text-muted py-4 mt-5 transition-all">
<div class="container"> <div class="container">
<p>Générateur d'Exercices de Mathématiques &copy; 2025</p> <p>Générateur d'Exercices de Mathématiques &copy; 2025</p>
</div> </div>
@@ -259,6 +356,72 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
<script> <script>
// Dark Mode Theme Management
class ThemeManager {
constructor() {
this.init();
}
init() {
// Load saved theme or detect system preference
const savedTheme = localStorage.getItem('theme');
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
const theme = savedTheme || systemTheme;
this.setTheme(theme);
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) {
this.setTheme(e.matches ? 'dark' : 'light');
}
});
// Setup toggle button
this.setupToggleButton();
}
setTheme(theme) {
document.documentElement.setAttribute('data-bs-theme', theme);
localStorage.setItem('theme', theme);
this.updateToggleIcon(theme);
}
toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-bs-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
this.setTheme(newTheme);
}
updateToggleIcon(theme) {
const icon = document.getElementById('darkModeIcon');
if (theme === 'dark') {
icon.className = 'bi bi-sun-fill';
} else {
icon.className = 'bi bi-moon-fill';
}
}
setupToggleButton() {
const toggleButton = document.getElementById('darkModeToggle');
toggleButton.addEventListener('click', () => {
this.toggleTheme();
this.animateToggle();
});
}
animateToggle() {
const button = document.getElementById('darkModeToggle');
button.style.transform = 'scale(0.9)';
setTimeout(() => {
button.style.transform = '';
}, 150);
}
}
// Initialize theme manager
const themeManager = new ThemeManager();
// Current page for pagination // Current page for pagination
let currentPage = 1; let currentPage = 1;
const pageSize = 10; const pageSize = 10;
@@ -485,8 +648,11 @@
// Load PDF list on page load // Load PDF list on page load
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Ensure theme is initialized first
setTimeout(() => {
restoreFormValues(); restoreFormValues();
loadPdfList(currentPage); loadPdfList(currentPage);
}, 100);
}); });
// Add event listeners to save form values on change // Add event listeners to save form values on change
+3 -3
View File
@@ -41,7 +41,7 @@ spec:
memory: "64Mi" memory: "64Mi"
cpu: "250m" cpu: "250m"
limits: limits:
memory: "128Mi" memory: "512Mi"
cpu: "500m" cpu: "500m"
# Liveness probe # Liveness probe
livenessProbe: livenessProbe:
@@ -57,8 +57,8 @@ spec:
httpGet: httpGet:
path: /health path: /health
port: 8000 port: 8000
initialDelaySeconds: 5 initialDelaySeconds: 2
periodSeconds: 10 periodSeconds: 3
timeoutSeconds: 3 timeoutSeconds: 3
failureThreshold: 3 failureThreshold: 3
# Environment variables from ConfigMap # Environment variables from ConfigMap
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: keda-add-ons-http-interceptor-proxy
name: keda-add-ons-http-interceptor-proxy
spec:
externalName: keda-add-ons-http-interceptor-proxy.keda
selector:
app: keda-add-ons-http-interceptor-proxy
type: ExternalName
status:
loadBalancer: {}
@@ -0,0 +1,24 @@
kind: HTTPScaledObject
apiVersion: http.keda.sh/v1alpha1
metadata:
name: math-exercises
spec:
hosts:
- "math-tables.cl1.parano.ch"
pathPrefixes:
- /
scaleTargetRef:
name: math-exercises-app
kind: Deployment
apiVersion: apps/v1
service: math-exercises-service
portName: http
replicas:
min: 0
max: 3
scaledownPeriod: 60
scalingMetric:
requestRate:
granularity: 1s
targetValue: 100
window: 1m
@@ -0,0 +1,51 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: math-tables
resources:
- ../production
- external-service.yaml
- http-scaled-object.yaml
patches:
- patch: |
[
{
"op": "remove",
"path": "/spec/replicas"
}
]
target:
group: apps
version: v1
kind: Deployment
name: math-exercises-app
- path: remove-pdb.yaml
- patch: |
[
{
"op": "replace",
"path": "/spec/rules/0/http/paths/0/backend/service/port/number",
"value": 8080
},
{
"op": "replace",
"path": "/spec/rules/0/http/paths/0/backend/service/name",
"value": "keda-add-ons-http-interceptor-proxy"
}
]
target:
group: networking.k8s.io
version: v1
kind: Ingress
name: math-exercises-ingress
- patch: |
[
{
"op": "replace",
"path": "/spec/internalTrafficPolicy",
"value": "Cluster"
}
]
target:
version: v1
kind: Service
name: math-exercises-service
@@ -0,0 +1,5 @@
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: math-exercises-pdb
$patch: delete
@@ -13,7 +13,7 @@ resources:
images: images:
- name: math-exercises - name: math-exercises
newName: harbor.cl1.parano.ch/library/math-exercice newName: harbor.cl1.parano.ch/library/math-exercice
newTag: 1.2.0 newTag: 1.4.1
# Production-specific labels # Production-specific labels
+25 -25
View File
@@ -1,32 +1,32 @@
annotated-doc==0.0.4
annotated-types==0.7.0 annotated-types==0.7.0
anyio==4.10.0 anyio==4.14.1
boto3==1.26.160 boto3==1.43.38
boto3-stubs==1.40.23 boto3-stubs==1.43.38
botocore==1.29.165 botocore==1.43.38
botocore-stubs==1.40.23 botocore-stubs==1.43.14
click==8.2.1 click==8.4.2
defusedxml==0.7.1 defusedxml==0.7.1
fastapi==0.116.1 fastapi==0.138.2
fonttools==4.59.2 fonttools==4.63.0
fpdf2==2.8.4 fpdf2==2.8.7
h11==0.16.0 h11==0.16.0
idna==3.10 idna==3.18
Jinja2==3.1.4 Jinja2==3.1.6
jmespath==1.0.1 jmespath==1.1.0
MarkupSafe==3.0.2 MarkupSafe==3.0.3
pillow==11.3.0 pillow==12.3.0
pydantic==2.11.7 pydantic==2.13.4
pydantic_core==2.33.2 pydantic_core==2.46.4
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
python-multipart==0.0.9 python-multipart==0.0.32
s3transfer==0.6.2 s3transfer==0.19.0
six==1.17.0 six==1.17.0
sniffio==1.3.1 sniffio==1.3.1
starlette==0.47.3 starlette==0.46.2
types-awscrt==0.27.6 types-awscrt==0.34.1
types-fpdf2==2.8.4.20250822 types-s3transfer==0.16.0
types-s3transfer==0.13.1 typing-inspection==0.4.2
typing-inspection==0.4.1
typing_extensions==4.15.0 typing_extensions==4.15.0
urllib3==1.26.20 urllib3==2.7.0
uvicorn==0.30.6 uvicorn==0.49.0