Add remember-me login and personalize sport presets
@@ -0,0 +1,8 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="18" cy="44" r="8" stroke="#8BE4FF" stroke-width="3"/>
|
||||
<circle cx="46" cy="44" r="8" stroke="#8BE4FF" stroke-width="3"/>
|
||||
<path d="M18 44L28 28H37L46 44" stroke="#EFF7FF" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M28 28L24 21H17" stroke="#7FF3BB" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M33 20H41" stroke="#7FF3BB" stroke-width="3" stroke-linecap="round"/>
|
||||
<path d="M31 28L38 36" stroke="#EFF7FF" stroke-width="3.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 636 B |
@@ -0,0 +1,8 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="34" cy="14" r="5" fill="#EFF7FF"/>
|
||||
<path d="M28 27L34 19L42 24" stroke="#8BE4FF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M34 19L31 33L40 39" stroke="#EFF7FF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31 33L20 39" stroke="#7FF3BB" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M40 39L47 51" stroke="#7FF3BB" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M17 50C22 46.5 27 45 32 45C37 45 42 46.5 47 50" stroke="#8BE4FF" stroke-width="3" stroke-linecap="round" opacity="0.78"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 681 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 14H28L24 27H34L21 50L25 36H16L20 14Z" fill="#EFF7FF"/>
|
||||
<path d="M39 18V46" stroke="#8BE4FF" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M47 22V42" stroke="#7FF3BB" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M55 26V38" stroke="#8BE4FF" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 416 B |
@@ -0,0 +1,8 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="32" cy="15" r="5" fill="#EFF7FF"/>
|
||||
<path d="M28 28L33 21L39 26" stroke="#8BE4FF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M33 21L30 34L24 45" stroke="#EFF7FF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M30 34L39 41L45 51" stroke="#7FF3BB" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18 51H50" stroke="#8BE4FF" stroke-width="3" stroke-linecap="round" opacity="0.75"/>
|
||||
<path d="M44 22V41" stroke="#7FF3BB" stroke-width="3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 671 B |
@@ -0,0 +1,8 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="10" y="24" width="8" height="16" rx="3" fill="#8BE4FF" fill-opacity="0.86"/>
|
||||
<rect x="18" y="27" width="6" height="10" rx="2" fill="#8BE4FF" fill-opacity="0.72"/>
|
||||
<rect x="40" y="27" width="6" height="10" rx="2" fill="#8BE4FF" fill-opacity="0.72"/>
|
||||
<rect x="46" y="24" width="8" height="16" rx="3" fill="#8BE4FF" fill-opacity="0.86"/>
|
||||
<rect x="24" y="29" width="16" height="6" rx="3" fill="#EFF7FF"/>
|
||||
<path d="M22 47C25 44.5 28.3 43.2 32 43.2C35.7 43.2 39 44.5 42 47" stroke="#7FF3BB" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 651 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="39" cy="20" r="5" fill="#EFF7FF"/>
|
||||
<path d="M25 28C29 23 36 22 43 24" stroke="#8BE4FF" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M27 35C31 31 36 30 42 31" stroke="#EFF7FF" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M11 43C15 40 18 40 22 43C26 46 29 46 33 43C37 40 40 40 44 43C48 46 51 46 55 43" stroke="#7FF3BB" stroke-width="3" stroke-linecap="round"/>
|
||||
<path d="M9 50C13 47 16 47 20 50C24 53 27 53 31 50C35 47 38 47 42 50C46 53 49 53 53 50" stroke="#8BE4FF" stroke-width="3" stroke-linecap="round" opacity="0.9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 657 B |
@@ -0,0 +1,8 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="32" cy="18" r="5" fill="#EFF7FF"/>
|
||||
<path d="M24 31C27 27.5 29.6 26 32 26C34.4 26 37 27.5 40 31" stroke="#8BE4FF" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M32 26V41" stroke="#EFF7FF" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M32 41L22 48" stroke="#7FF3BB" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M32 41L42 48" stroke="#7FF3BB" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M16 50H48" stroke="#8BE4FF" stroke-width="3" stroke-linecap="round" opacity="0.8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 618 B |
@@ -170,7 +170,9 @@ final class App
|
||||
redirect('/login');
|
||||
}
|
||||
|
||||
if (!$this->auth->attempt($username, $password)) {
|
||||
$remember = isset($_POST['remember_me']) && $_POST['remember_me'] === '1';
|
||||
|
||||
if (!$this->auth->attempt($username, $password, $remember)) {
|
||||
$this->throttle->hit($throttleKey);
|
||||
flash('error', 'Login fehlgeschlagen. Bitte prüfe Benutzername und Passwort.');
|
||||
redirect('/login');
|
||||
|
||||
@@ -16,7 +16,13 @@ final class Auth
|
||||
|
||||
$username = $_SESSION['user']['username'] ?? null;
|
||||
|
||||
return is_string($username) && $username !== '';
|
||||
$valid = is_string($username) && $username !== '';
|
||||
|
||||
if ($valid && !empty($_SESSION['remember_me'])) {
|
||||
setcookie(session_name(), session_id(), session_cookie_options_for(time() + remember_me_lifetime()));
|
||||
}
|
||||
|
||||
return $valid;
|
||||
}
|
||||
|
||||
public function user(): ?array
|
||||
@@ -28,7 +34,7 @@ final class Auth
|
||||
return $_SESSION['user'];
|
||||
}
|
||||
|
||||
public function attempt(string $username, string $password): bool
|
||||
public function attempt(string $username, string $password, bool $remember = false): bool
|
||||
{
|
||||
$user = $this->users->verify($username, $password);
|
||||
|
||||
@@ -36,12 +42,12 @@ final class Auth
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->login($user);
|
||||
$this->login($user, $remember);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function login(array $user): void
|
||||
public function login(array $user, bool $remember = false): void
|
||||
{
|
||||
if (!isset($user['username']) || !is_string($user['username']) || $user['username'] === '') {
|
||||
throw new RuntimeException('Der Benutzer konnte nicht angemeldet werden.');
|
||||
@@ -53,11 +59,20 @@ final class Auth
|
||||
'username' => $user['username'],
|
||||
'is_admin' => (bool) ($user['is_admin'] ?? false),
|
||||
];
|
||||
$_SESSION['remember_me'] = $remember;
|
||||
|
||||
if ($remember) {
|
||||
setcookie(session_name(), session_id(), session_cookie_options_for(time() + remember_me_lifetime()));
|
||||
} else {
|
||||
setcookie(session_name(), session_id(), session_cookie_options_for());
|
||||
}
|
||||
}
|
||||
|
||||
public function logout(): void
|
||||
{
|
||||
unset($_SESSION['user']);
|
||||
unset($_SESSION['remember_me']);
|
||||
setcookie(session_name(), '', session_cookie_options_for(time() - 3600));
|
||||
session_regenerate_id(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,22 +17,6 @@ final class Defaults
|
||||
],
|
||||
],
|
||||
'sport_types' => [
|
||||
[
|
||||
'id' => 'strength-home',
|
||||
'label' => 'Kraftsport (Keller)',
|
||||
'icon' => 'strength-home',
|
||||
'recovery_group' => 'kraftsport',
|
||||
'bonus_points' => 2,
|
||||
'allow_consecutive' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'strength-gym',
|
||||
'label' => 'Kraftsport (Gym)',
|
||||
'icon' => 'strength-gym',
|
||||
'recovery_group' => 'kraftsport',
|
||||
'bonus_points' => 2,
|
||||
'allow_consecutive' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'running',
|
||||
'label' => 'Joggen',
|
||||
@@ -41,14 +25,70 @@ final class Defaults
|
||||
'bonus_points' => 2,
|
||||
'allow_consecutive' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'cycling',
|
||||
'label' => 'Radfahren',
|
||||
'icon' => 'bike',
|
||||
'recovery_group' => 'radfahren',
|
||||
'bonus_points' => 2,
|
||||
'allow_consecutive' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'strength',
|
||||
'label' => 'Krafttraining',
|
||||
'icon' => 'strength',
|
||||
'recovery_group' => 'kraftsport',
|
||||
'bonus_points' => 2,
|
||||
'allow_consecutive' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'hiking',
|
||||
'label' => 'Wandern',
|
||||
'icon' => 'hike',
|
||||
'recovery_group' => 'wandern',
|
||||
'bonus_points' => 2,
|
||||
'allow_consecutive' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'swimming',
|
||||
'label' => 'Schwimmen',
|
||||
'icon' => 'swim',
|
||||
'recovery_group' => 'schwimmen',
|
||||
'bonus_points' => 2,
|
||||
'allow_consecutive' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'yoga',
|
||||
'label' => 'Yoga',
|
||||
'icon' => 'yoga',
|
||||
'recovery_group' => 'yoga',
|
||||
'bonus_points' => 2,
|
||||
'allow_consecutive' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'hiit-workout',
|
||||
'label' => 'HIIT / Workout',
|
||||
'icon' => 'hiit',
|
||||
'recovery_group' => 'hiit',
|
||||
'bonus_points' => 2,
|
||||
'allow_consecutive' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'rowing',
|
||||
'label' => 'Rudergerät',
|
||||
'label' => 'Rudern',
|
||||
'icon' => 'row',
|
||||
'recovery_group' => 'rudern',
|
||||
'bonus_points' => 2,
|
||||
'allow_consecutive' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'dance',
|
||||
'label' => 'Tanzen',
|
||||
'icon' => 'dance',
|
||||
'recovery_group' => 'tanzen',
|
||||
'bonus_points' => 2,
|
||||
'allow_consecutive' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'core',
|
||||
'label' => 'Core',
|
||||
|
||||
@@ -15,26 +15,12 @@ require __DIR__ . '/App.php';
|
||||
|
||||
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Berlin');
|
||||
|
||||
$forwardedProto = strtolower((string) ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''));
|
||||
$forwardedSsl = strtolower((string) ($_SERVER['HTTP_X_FORWARDED_SSL'] ?? ''));
|
||||
$isSecure = (
|
||||
(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|
||||
|| $forwardedProto === 'https'
|
||||
|| $forwardedSsl === 'on'
|
||||
);
|
||||
|
||||
ini_set('session.use_only_cookies', '1');
|
||||
ini_set('session.use_strict_mode', '1');
|
||||
ini_set('session.gc_maxlifetime', (string) remember_me_lifetime());
|
||||
|
||||
session_name('mood_session');
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 0,
|
||||
'path' => '/',
|
||||
'domain' => '',
|
||||
'secure' => $isSecure,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
session_set_cookie_params(session_cookie_params_for());
|
||||
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
|
||||
@@ -176,6 +176,47 @@ function mood_icon_path(string $sentiment): string
|
||||
return icon_path('mood-' . $sentiment);
|
||||
}
|
||||
|
||||
function request_is_secure(): bool
|
||||
{
|
||||
$forwardedProto = strtolower((string) ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''));
|
||||
$forwardedSsl = strtolower((string) ($_SERVER['HTTP_X_FORWARDED_SSL'] ?? ''));
|
||||
|
||||
return (
|
||||
(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|
||||
|| $forwardedProto === 'https'
|
||||
|| $forwardedSsl === 'on'
|
||||
);
|
||||
}
|
||||
|
||||
function remember_me_lifetime(): int
|
||||
{
|
||||
return 60 * 60 * 24 * 30;
|
||||
}
|
||||
|
||||
function session_cookie_params_for(int $lifetime = 0): array
|
||||
{
|
||||
return [
|
||||
'lifetime' => $lifetime,
|
||||
'path' => '/',
|
||||
'domain' => '',
|
||||
'secure' => request_is_secure(),
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
];
|
||||
}
|
||||
|
||||
function session_cookie_options_for(int $expires = 0): array
|
||||
{
|
||||
return [
|
||||
'expires' => $expires,
|
||||
'path' => '/',
|
||||
'domain' => '',
|
||||
'secure' => request_is_secure(),
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
];
|
||||
}
|
||||
|
||||
function sport_icon_path(string $icon): string
|
||||
{
|
||||
return icon_path('sport-' . $icon);
|
||||
@@ -184,11 +225,18 @@ function sport_icon_path(string $icon): string
|
||||
function sport_icon_options(): array
|
||||
{
|
||||
return [
|
||||
'strength-home' => 'Kraftsport daheim',
|
||||
'strength-gym' => 'Kraftsport im Gym',
|
||||
'strength' => 'Krafttraining',
|
||||
'bike' => 'Radfahren',
|
||||
'run' => 'Joggen',
|
||||
'hike' => 'Wandern',
|
||||
'swim' => 'Schwimmen',
|
||||
'yoga' => 'Yoga',
|
||||
'hiit' => 'HIIT / Workout',
|
||||
'row' => 'Rudergerät',
|
||||
'dance' => 'Tanzen',
|
||||
'core' => 'Core',
|
||||
'strength-home' => 'Krafttraining Zuhause',
|
||||
'strength-gym' => 'Krafttraining Auswärts',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
<input type="password" name="password" autocomplete="current-password" required>
|
||||
</label>
|
||||
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="remember_me" value="1">
|
||||
<span>Angemeldet bleiben</span>
|
||||
</label>
|
||||
|
||||
<button class="primary-button" type="submit">Anmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
<div class="section-head section-head--compact">
|
||||
<div>
|
||||
<h4>Sportarten und Bonuspunkte</h4>
|
||||
<p class="helper-text">Lege fest, welche Sportarten im Tracking auswählbar sind. Der Bonus gilt nur, wenn am Vortag keine gleiche Erholungsgruppe trainiert wurde.</p>
|
||||
<p class="helper-text">Lege fest, welche Sportarten nur für deinen eigenen Account im Tracking auswählbar sind. Wenn du lieber Varianten wie Krafttraining Zuhause oder Krafttraining Auswärts nutzen möchtest, kannst du sie hier einfach selbst anlegen.</p>
|
||||
</div>
|
||||
<button class="ghost-button" type="button" data-add-sport-type>Sportart hinzufügen</button>
|
||||
</div>
|
||||
@@ -123,7 +123,7 @@
|
||||
<div class="field-grid field-grid--four">
|
||||
<label>
|
||||
<span>Bezeichnung</span>
|
||||
<input type="text" value="" placeholder="z. B. Mobility" data-name-template="settings[sport_types][__INDEX__][label]">
|
||||
<input type="text" value="" placeholder="z. B. Krafttraining Zuhause" data-name-template="settings[sport_types][__INDEX__][label]">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
@@ -137,7 +137,7 @@
|
||||
|
||||
<label>
|
||||
<span>Erholungsgruppe</span>
|
||||
<input type="text" value="" placeholder="z. B. kraftsport" data-name-template="settings[sport_types][__INDEX__][recovery_group]">
|
||||
<input type="text" value="" placeholder="z. B. krafttraining" data-name-template="settings[sport_types][__INDEX__][recovery_group]">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
|
||||