added erd and api-concept doc files and fixed issue for deployment.
This commit is contained in:
@@ -5,15 +5,10 @@ class LoginAttempt(db.Model):
|
|||||||
__tablename__ = "login_attempts"
|
__tablename__ = "login_attempts"
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
ip_address = db.Column(db.String(45), nullable=False)
|
ip_address = db.Column(db.String(45), unique=True, nullable=False)
|
||||||
username = db.Column(db.String(80), nullable=False)
|
|
||||||
failed_attempts = db.Column(db.Integer, nullable=False, default=0, server_default=db.text("0"))
|
failed_attempts = db.Column(db.Integer, nullable=False, default=0, server_default=db.text("0"))
|
||||||
locked_until = db.Column(db.DateTime)
|
locked_until = db.Column(db.DateTime)
|
||||||
last_failed_at = db.Column(db.DateTime)
|
last_failed_at = db.Column(db.DateTime)
|
||||||
|
|
||||||
__table_args__ = (
|
|
||||||
db.UniqueConstraint("ip_address", "username", name="uq_login_attempt_ip_username"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<LoginAttempt {self.ip_address}:{self.username}>"
|
return f"<LoginAttempt {self.ip_address}>"
|
||||||
|
|||||||
+24
-46
@@ -30,8 +30,8 @@ login_bp = Blueprint("login", __name__)
|
|||||||
# constants
|
# constants
|
||||||
MIN_PASSWORD_LENGTH = 8
|
MIN_PASSWORD_LENGTH = 8
|
||||||
MAX_PASSWORD_LENGTH = 128
|
MAX_PASSWORD_LENGTH = 128
|
||||||
MAX_LOGIN_FAILURES = 8
|
MAX_LOGIN_FAILURES = 4
|
||||||
LOGIN_LOCK_MINUTES = 15
|
LOGIN_LOCK_MINUTES = 30
|
||||||
TWO_FACTOR_CODE_DIGITS = 6
|
TWO_FACTOR_CODE_DIGITS = 6
|
||||||
TWO_FACTOR_TIME_STEP_SECONDS = 30
|
TWO_FACTOR_TIME_STEP_SECONDS = 30
|
||||||
TWO_FACTOR_VALID_WINDOW = 1
|
TWO_FACTOR_VALID_WINDOW = 1
|
||||||
@@ -90,26 +90,13 @@ def get_request_ip():
|
|||||||
return (request.remote_addr or "unknown")[:45]
|
return (request.remote_addr or "unknown")[:45]
|
||||||
|
|
||||||
|
|
||||||
def login_attempt_buckets(username):
|
def get_or_create_login_attempt():
|
||||||
"""Return the lockout buckets tracked for this login attempt.
|
"""Fetch or create the failed-login counter for the current IP address."""
|
||||||
|
|
||||||
The app tracks failures both per username+IP and per IP-only bucket to
|
|
||||||
slow down brute-force attempts against many usernames from the same client.
|
|
||||||
"""
|
|
||||||
normalized_username = normalize_username(username).lower() or "__empty__"
|
|
||||||
ip_address = get_request_ip()
|
ip_address = get_request_ip()
|
||||||
return [
|
attempt = LoginAttempt.query.filter_by(ip_address=ip_address).first()
|
||||||
(ip_address, normalized_username),
|
|
||||||
(ip_address, "__ip__"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def get_or_create_attempt(ip_address, bucket_key):
|
|
||||||
"""Fetch an existing login-attempt record or create a new one."""
|
|
||||||
attempt = LoginAttempt.query.filter_by(ip_address=ip_address, username=bucket_key).first()
|
|
||||||
|
|
||||||
if attempt is None:
|
if attempt is None:
|
||||||
attempt = LoginAttempt(ip_address=ip_address, username=bucket_key)
|
attempt = LoginAttempt(ip_address=ip_address)
|
||||||
db.session.add(attempt)
|
db.session.add(attempt)
|
||||||
|
|
||||||
return attempt
|
return attempt
|
||||||
@@ -122,49 +109,38 @@ def clear_expired_lock(attempt, now):
|
|||||||
attempt.locked_until = None
|
attempt.locked_until = None
|
||||||
|
|
||||||
|
|
||||||
def get_active_lock(username):
|
def get_active_lock():
|
||||||
"""Return the latest active lockout time for this username/IP combination."""
|
"""Return the active lockout time for the current IP address."""
|
||||||
now = utcnow()
|
now = utcnow()
|
||||||
active_until = None
|
attempt = LoginAttempt.query.filter_by(ip_address=get_request_ip()).first()
|
||||||
|
|
||||||
for ip_address, bucket_key in login_attempt_buckets(username):
|
|
||||||
attempt = LoginAttempt.query.filter_by(ip_address=ip_address, username=bucket_key).first()
|
|
||||||
|
|
||||||
if not attempt:
|
if not attempt:
|
||||||
continue
|
return None
|
||||||
|
|
||||||
clear_expired_lock(attempt, now)
|
clear_expired_lock(attempt, now)
|
||||||
|
|
||||||
if attempt.locked_until and (active_until is None or attempt.locked_until > active_until):
|
|
||||||
active_until = attempt.locked_until
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return active_until
|
return attempt.locked_until
|
||||||
|
|
||||||
|
|
||||||
def record_failed_login(username):
|
def record_failed_login():
|
||||||
"""Increase the failure counter and start a temporary lock when needed."""
|
"""Increase the failed-login counter for this IP and lock it if needed."""
|
||||||
now = utcnow()
|
now = utcnow()
|
||||||
locked_until = None
|
attempt = get_or_create_login_attempt()
|
||||||
|
|
||||||
for ip_address, bucket_key in login_attempt_buckets(username):
|
|
||||||
attempt = get_or_create_attempt(ip_address, bucket_key)
|
|
||||||
clear_expired_lock(attempt, now)
|
clear_expired_lock(attempt, now)
|
||||||
attempt.failed_attempts = (attempt.failed_attempts or 0) + 1
|
attempt.failed_attempts = (attempt.failed_attempts or 0) + 1
|
||||||
attempt.last_failed_at = now
|
attempt.last_failed_at = now
|
||||||
|
|
||||||
if attempt.failed_attempts >= MAX_LOGIN_FAILURES:
|
if attempt.failed_attempts >= MAX_LOGIN_FAILURES:
|
||||||
attempt.locked_until = now + timedelta(minutes=LOGIN_LOCK_MINUTES)
|
attempt.locked_until = now + timedelta(minutes=LOGIN_LOCK_MINUTES)
|
||||||
locked_until = attempt.locked_until
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return locked_until
|
return attempt.locked_until
|
||||||
|
|
||||||
|
|
||||||
def clear_login_attempts(username):
|
def clear_login_attempts():
|
||||||
"""Remove tracked failed attempts after a successful login."""
|
"""Remove failed-login attempts for this IP after a successful login."""
|
||||||
for ip_address, bucket_key in login_attempt_buckets(username):
|
attempt = LoginAttempt.query.filter_by(ip_address=get_request_ip()).first()
|
||||||
attempt = LoginAttempt.query.filter_by(ip_address=ip_address, username=bucket_key).first()
|
|
||||||
|
|
||||||
if attempt:
|
if attempt:
|
||||||
db.session.delete(attempt)
|
db.session.delete(attempt)
|
||||||
@@ -345,8 +321,8 @@ def login():
|
|||||||
if not username or not password:
|
if not username or not password:
|
||||||
return render_template("login.html", error="Username and password are required.", username=username)
|
return render_template("login.html", error="Username and password are required.", username=username)
|
||||||
|
|
||||||
# Stop early if this user or IP bucket is currently locked.
|
# Stop early if this IP address is currently locked.
|
||||||
locked_until = get_active_lock(username)
|
locked_until = get_active_lock()
|
||||||
|
|
||||||
if locked_until and locked_until > utcnow():
|
if locked_until and locked_until > utcnow():
|
||||||
return render_template(
|
return render_template(
|
||||||
@@ -362,7 +338,7 @@ def login():
|
|||||||
password_matches = check_password_hash(password_hash, password)
|
password_matches = check_password_hash(password_hash, password)
|
||||||
|
|
||||||
if not user or not password_matches:
|
if not user or not password_matches:
|
||||||
locked_until = record_failed_login(username)
|
locked_until = record_failed_login()
|
||||||
error_message = "Invalid username or password."
|
error_message = "Invalid username or password."
|
||||||
|
|
||||||
if locked_until:
|
if locked_until:
|
||||||
@@ -372,7 +348,7 @@ def login():
|
|||||||
|
|
||||||
return render_template("login.html", error=error_message, username=username)
|
return render_template("login.html", error=error_message, username=username)
|
||||||
|
|
||||||
clear_login_attempts(username)
|
clear_login_attempts()
|
||||||
|
|
||||||
if user.two_factor_enabled:
|
if user.two_factor_enabled:
|
||||||
# Delay the full login until the authenticator code has been verified.
|
# Delay the full login until the authenticator code has been verified.
|
||||||
@@ -392,6 +368,7 @@ def login():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
login_user(user)
|
login_user(user)
|
||||||
clear_pending_two_factor()
|
clear_pending_two_factor()
|
||||||
|
session["start_dashboard_with_default_mode"] = True
|
||||||
return redirect(url_for("main.dashboard"))
|
return redirect(url_for("main.dashboard"))
|
||||||
|
|
||||||
return render_template("login.html")
|
return render_template("login.html")
|
||||||
@@ -453,6 +430,7 @@ def verify_two_factor():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
login_user(user)
|
login_user(user)
|
||||||
flash("Two-factor authentication successful.", "success")
|
flash("Two-factor authentication successful.", "success")
|
||||||
|
session["start_dashboard_with_default_mode"] = True
|
||||||
return redirect(url_for("main.dashboard"))
|
return redirect(url_for("main.dashboard"))
|
||||||
|
|
||||||
return render_template("two_factor_verify.html")
|
return render_template("two_factor_verify.html")
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import json
|
|||||||
import random
|
import random
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from flask import Blueprint, jsonify, render_template, request
|
from flask import Blueprint, jsonify, render_template, request, session
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
|
|
||||||
from .. import db
|
from .. import db
|
||||||
@@ -308,7 +308,12 @@ def home():
|
|||||||
@login_required
|
@login_required
|
||||||
def dashboard():
|
def dashboard():
|
||||||
"""Render the logged-in dashboard."""
|
"""Render the logged-in dashboard."""
|
||||||
return render_template("dashboard.html", user=current_user)
|
force_default_dashboard_mode = bool(session.pop("start_dashboard_with_default_mode", False))
|
||||||
|
return render_template(
|
||||||
|
"dashboard.html",
|
||||||
|
user=current_user,
|
||||||
|
force_default_dashboard_mode=force_default_dashboard_mode,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@main_bp.route("/privacy_policy")
|
@main_bp.route("/privacy_policy")
|
||||||
|
|||||||
+24
-11
@@ -5,11 +5,12 @@ const MAX_BOARD_SIZE = 13;
|
|||||||
const EMPTY_CELL = 0;
|
const EMPTY_CELL = 0;
|
||||||
const OCCUPIED_CELL = 1;
|
const OCCUPIED_CELL = 1;
|
||||||
const GAME_STORAGE_KEY = "eight-queens-dashboard-session-v1";
|
const GAME_STORAGE_KEY = "eight-queens-dashboard-session-v1";
|
||||||
|
const FORCE_DEFAULT_MODE = document.body?.dataset.forceDefaultMode === "true";
|
||||||
|
|
||||||
const AppLanguage = window.AppLanguage;
|
const AppLanguage = window.AppLanguage;
|
||||||
|
|
||||||
// Mutable game and solver state
|
// Mutable game and solver state
|
||||||
let mode = "queens";
|
let mode = "rooks";
|
||||||
let boardSize = DEFAULT_BOARD_SIZE;
|
let boardSize = DEFAULT_BOARD_SIZE;
|
||||||
let pendingBoardSize = DEFAULT_BOARD_SIZE;
|
let pendingBoardSize = DEFAULT_BOARD_SIZE;
|
||||||
let board = createEmptyBoard(boardSize);
|
let board = createEmptyBoard(boardSize);
|
||||||
@@ -1328,13 +1329,13 @@ function renderTimeHistory(savePoints) {
|
|||||||
|
|
||||||
async function refreshSavePoints() {
|
async function refreshSavePoints() {
|
||||||
const queryString = getSavePointFilters();
|
const queryString = getSavePointFilters();
|
||||||
const response = await fetch(`/save_points${queryString ? `?${queryString}` : ""}`);
|
const response = await fetch(`save_points${queryString ? `?${queryString}` : ""}`);
|
||||||
const savePoints = await response.json();
|
const savePoints = await response.json();
|
||||||
renderSavePoints(savePoints);
|
renderSavePoints(savePoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshTimeHistory() {
|
async function refreshTimeHistory() {
|
||||||
const response = await fetch("/save_points?sort=updated");
|
const response = await fetch("save_points?sort=updated");
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return;
|
return;
|
||||||
@@ -1466,7 +1467,7 @@ function startSolverPlayback(stepImmediately = true) {
|
|||||||
// requestId prevents older async responses from overwriting a newer solver run
|
// requestId prevents older async responses from overwriting a newer solver run
|
||||||
// when the user changes size, mode, or solver action quickly.
|
// when the user changes size, mode, or solver action quickly.
|
||||||
async function prepareSolutionTrace() {
|
async function prepareSolutionTrace() {
|
||||||
const endpoint = mode === "rooks" ? "/solve_rooks_trace" : "/solve_trace";
|
const endpoint = mode === "rooks" ? "solve_rooks_trace" : "solve_trace";
|
||||||
const requestId = ++solverRequestId;
|
const requestId = ++solverRequestId;
|
||||||
|
|
||||||
toggleSidebar(false);
|
toggleSidebar(false);
|
||||||
@@ -1612,7 +1613,7 @@ async function instantSolve() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = mode === "rooks" ? "/solve_rooks" : "/solve";
|
const endpoint = mode === "rooks" ? "solve_rooks" : "solve";
|
||||||
const requestId = ++solverRequestId;
|
const requestId = ++solverRequestId;
|
||||||
|
|
||||||
toggleSidebar(false);
|
toggleSidebar(false);
|
||||||
@@ -1665,7 +1666,7 @@ async function instantSolve() {
|
|||||||
// Save point persistence actions
|
// Save point persistence actions
|
||||||
|
|
||||||
async function saveGame() {
|
async function saveGame() {
|
||||||
const response = await fetch("/save", {
|
const response = await fetch("save", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@@ -1697,13 +1698,13 @@ async function saveGame() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadGame() {
|
async function loadGame() {
|
||||||
const response = await fetch("/load");
|
const response = await fetch("load");
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
applyLoadedGame(data);
|
applyLoadedGame(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSavePoint(savePointId) {
|
async function loadSavePoint(savePointId) {
|
||||||
const response = await fetch(`/load/${savePointId}`);
|
const response = await fetch(`load/${savePointId}`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
setStatus(t("loadError"), "red");
|
setStatus(t("loadError"), "red");
|
||||||
@@ -1718,7 +1719,7 @@ async function loadSavePoint(savePointId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function toggleSavePointFavorite(savePointId) {
|
async function toggleSavePointFavorite(savePointId) {
|
||||||
const response = await fetch(`/save_points/${savePointId}/favorite`, {
|
const response = await fetch(`save_points/${savePointId}/favorite`, {
|
||||||
method: "POST"
|
method: "POST"
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1739,7 +1740,7 @@ async function deleteSavePoint(savePointId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`/save_points/${savePointId}`, {
|
const response = await fetch(`save_points/${savePointId}`, {
|
||||||
method: "DELETE"
|
method: "DELETE"
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1790,6 +1791,14 @@ function restoreSessionFromStorage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearStoredSession() {
|
||||||
|
try {
|
||||||
|
window.localStorage.removeItem(GAME_STORAGE_KEY);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore unavailable storage so startup can continue.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Boot order: translate UI, restore any local session, then load history data.
|
// Boot order: translate UI, restore any local session, then load history data.
|
||||||
async function initializeDashboard() {
|
async function initializeDashboard() {
|
||||||
pendingBoardSize = boardSize;
|
pendingBoardSize = boardSize;
|
||||||
@@ -1799,7 +1808,11 @@ async function initializeDashboard() {
|
|||||||
updateSettingsTrigger(false);
|
updateSettingsTrigger(false);
|
||||||
setSolverMessage(t("solverReady", { modeLabel: getModeLabel(mode) }));
|
setSolverMessage(t("solverReady", { modeLabel: getModeLabel(mode) }));
|
||||||
|
|
||||||
if (!restoreSessionFromStorage()) {
|
if (FORCE_DEFAULT_MODE) {
|
||||||
|
clearStoredSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FORCE_DEFAULT_MODE || !restoreSessionFromStorage()) {
|
||||||
resetProgress();
|
resetProgress();
|
||||||
drawBoard();
|
drawBoard();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/footer.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/footer.css') }}">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body data-force-default-mode="{{ 'true' if force_default_dashboard_mode else 'false' }}">
|
||||||
<nav class="dashboard-nav">
|
<nav class="dashboard-nav">
|
||||||
<a class="brand" href="{{ url_for('main.home') }}">
|
<a class="brand" href="{{ url_for('main.home') }}">
|
||||||
<span class="brand-mark">♛</span>
|
<span class="brand-mark">♛</span>
|
||||||
|
|||||||
+2
-4
@@ -10,12 +10,10 @@ CREATE TABLE users (
|
|||||||
|
|
||||||
CREATE TABLE login_attempts (
|
CREATE TABLE login_attempts (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
ip_address VARCHAR(45) NOT NULL,
|
ip_address VARCHAR(45) NOT NULL UNIQUE,
|
||||||
username VARCHAR(80) NOT NULL,
|
|
||||||
failed_attempts INTEGER NOT NULL DEFAULT 0,
|
failed_attempts INTEGER NOT NULL DEFAULT 0,
|
||||||
locked_until DATETIME,
|
locked_until DATETIME,
|
||||||
last_failed_at DATETIME,
|
last_failed_at DATETIME
|
||||||
CONSTRAINT uq_login_attempt_ip_username UNIQUE (ip_address, username)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE game_state (
|
CREATE TABLE game_state (
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,86 @@
|
|||||||
|
# REST-API-Konzept
|
||||||
|
|
||||||
|
Dieses API-Konzept beschreibt die wichtigsten Schnittstellen der Anwendung zum Acht-Damen-und-Türme-Problem. Die API wird vom Dashboard verwendet, um Spielbretter zu prüfen, Loesungen zu berechnen und Spielstände zu speichern oder zu laden.
|
||||||
|
|
||||||
|
## Allgemeine Angaben
|
||||||
|
|
||||||
|
| Punkt | Beschreibung |
|
||||||
|
|---|---|
|
||||||
|
| Datenformat | JSON |
|
||||||
|
| Authentifizierung | Session-Cookie nach Login |
|
||||||
|
| Geschützte Endpunkte | Speicherstand-Funktionen benoetigen einen angemeldeten Benutzer |
|
||||||
|
| Brettgroesse | 4 bis 13 Felder pro Seite |
|
||||||
|
| Standard-Brettgroesse | 8 x 8 |
|
||||||
|
| Spielmodi | `queens` für Damen, `rooks` für Türme |
|
||||||
|
|
||||||
|
## Solver-API
|
||||||
|
|
||||||
|
| Methode | Pfad | Zweck | Parameter / Body | Authentifizierung |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| POST | `/check` | Prüft, ob eine Figur auf ein bestimmtes Feld gesetzt werden darf. | JSON: `board`, `row`, `col`, `mode` | Nein |
|
||||||
|
| GET | `/solve` | Gibt Loesungen für das Damenproblem zurück. | Query: `size` | Nein |
|
||||||
|
| GET | `/solve_trace` | Gibt eine Damen-Loesung Schritt für Schritt zurück. | Query: `size` | Nein |
|
||||||
|
| GET | `/solve_rooks` | Gibt eine gültige Turm-Loesung zurück. | Query: `size` | Nein |
|
||||||
|
| GET | `/solve_rooks_trace` | Gibt eine Turm-Loesung Schritt für Schritt zurück. | Query: `size` | Nein |
|
||||||
|
|
||||||
|
## Speicherstand-API
|
||||||
|
|
||||||
|
| Methode | Pfad | Zweck | Parameter / Body | Authentifizierung |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| POST | `/save` | Speichert den aktuellen Spielstand des angemeldeten Benutzers. | JSON: Spielbrett, Modus, Name, Notiz, Favorit, Schritte, Zeit, geloest | Ja |
|
||||||
|
| GET | `/save_points` | Listet alle gespeicherten Spielstände des angemeldeten Benutzers auf. | Optional: `mode`, `board_size`, `favorites_only`, `sort` | Ja |
|
||||||
|
| GET | `/load` | Lädt den neusten gespeicherten Spielstand. | Keine | Ja |
|
||||||
|
| GET | `/load/{id}` | Lädt einen bestimmten gespeicherten Spielstand. | Pfadparameter: `id` | Ja |
|
||||||
|
| POST | `/save_points/{id}/favorite` | Schaltet den Favoritenstatus eines Speicherstandes um. | Pfadparameter: `id` | Ja |
|
||||||
|
| DELETE | `/save_points/{id}` | Loescht einen bestimmten Speicherstand. | Pfadparameter: `id` | Ja |
|
||||||
|
|
||||||
|
## Login- und Seitenrouten
|
||||||
|
|
||||||
|
| Methode | Pfad | Zweck | Typ |
|
||||||
|
|---|---|---|---|
|
||||||
|
| GET / POST | `/register` | Neues Benutzerkonto erstellen. | Formularroute |
|
||||||
|
| GET / POST | `/login` | Benutzer anmelden. | Formularroute |
|
||||||
|
| GET / POST | `/login/verify` | Zwei-Faktor-Code prüfen. | Formularroute |
|
||||||
|
| GET / POST | `/login/settings` | Passwort und Zwei-Faktor-Authentifizierung verwalten. | Formularroute |
|
||||||
|
| POST | `/logout` | Benutzer abmelden. | Formularroute |
|
||||||
|
| GET | `/` | Startseite anzeigen. | Seitenroute |
|
||||||
|
| GET | `/dashboard` | Dashboard anzeigen. | Seitenroute |
|
||||||
|
| GET | `/privacy_policy` | Datenschutzerklärung anzeigen. | Seitenroute |
|
||||||
|
| GET | `/imprint` | Impressum anzeigen. | Seitenroute |
|
||||||
|
|
||||||
|
## Wichtige Request-Daten
|
||||||
|
|
||||||
|
| Feld | Verwendung | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `board` | `/check`, `/save` | Zweidimensionale Matrix des Spielbretts. `0` bedeutet leer, `1` bedeutet Figur. |
|
||||||
|
| `row` | `/check` | Zeile des zu prüfenden Feldes. |
|
||||||
|
| `col` | `/check` | Spalte des zu prüfenden Feldes. |
|
||||||
|
| `mode` | `/check`, `/save`, `/save_points` | Spielmodus: `queens` oder `rooks`. |
|
||||||
|
| `size` | Solver-Endpunkte | Gewünschte Brettgroesse. |
|
||||||
|
| `boardSize` | `/save` | Groesse des gespeicherten Spielbretts. |
|
||||||
|
| `saveName` | `/save` | Name des Speicherstandes. |
|
||||||
|
| `saveNote` | `/save` | Optionale Notiz zum Speicherstand. |
|
||||||
|
| `isFavorite` | `/save` | Gibt an, ob der Speicherstand als Favorit markiert ist. |
|
||||||
|
| `stepCount` | `/save` | Anzahl der ausgeführten Schritte. |
|
||||||
|
| `elapsedSeconds` | `/save` | Vergangene Spielzeit in Sekunden. |
|
||||||
|
| `isSolved` | `/save` | Gibt an, ob das Spiel geloest wurde. |
|
||||||
|
|
||||||
|
## Wichtige Response-Daten
|
||||||
|
|
||||||
|
| Feld | Verwendung | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `valid` | `/check` | Gibt zurück, ob der Zug erlaubt ist. |
|
||||||
|
| `status` | `/save`, `/save_points/{id}` | Statusmeldung, zum Beispiel `saved` oder `deleted`. |
|
||||||
|
| `save_point` | `/save` | Neu gespeicherter Spielstand. |
|
||||||
|
| `steps` | Trace-Endpunkte | Liste der einzelnen Loesungsschritte. |
|
||||||
|
| `solved` | Trace-Endpunkte | Gibt an, ob eine Loesung gefunden wurde. |
|
||||||
|
| `error` | Fehlerfälle | Fehlermeldung, zum Beispiel wenn ein Speicherstand nicht gefunden wurde. |
|
||||||
|
|
||||||
|
## Fehlerfälle
|
||||||
|
|
||||||
|
| Situation | Beispielantwort | HTTP-Status |
|
||||||
|
|---|---|---|
|
||||||
|
| Speicherstand existiert nicht oder gehoert einem anderen Benutzer. | `{ "error": "Save point not found" }` | 404 |
|
||||||
|
| Benutzer ist nicht angemeldet und ruft geschützte Route auf. | Weiterleitung zur Login-Seite | 302 |
|
||||||
|
| Ungültige Brettgroesse wird übergeben. | Wert wird auf erlaubten Bereich begrenzt. | 200 |
|
||||||
|
|
||||||
Reference in New Issue
Block a user