9 Commits

Author SHA1 Message Date
AliceDarkest 4f5042dc43 Sicherheit: DB-Passwort rotieren und aus Secret laden
mindflux-ci / build (push) Successful in 28s
2026-07-12 21:43:02 +02:00
AliceDarkest 762812649c Merge: Admin-Passwort-Reset + Profilbild-Fix
mindflux-ci / build (push) Successful in 27s
2026-06-30 20:12:48 +02:00
AliceDarkest 6fd8f8ecc8 Fix: Admin-Passwort-Reset (Direkt-Button je Nutzer) + Profilbild beim Bearbeiten nicht mehr nullen
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 20:12:17 +02:00
AliceDarkest b4fbc972f2 Merge: Login case-insensitiv + Passwort-Wiederholfeld
mindflux-ci / build (push) Successful in 27s
2026-06-30 20:04:08 +02:00
AliceDarkest 21c594445f Fix: Login case-insensitiv (Registrierung speichert lowercase, Login war case-sensitiv) + Passwort-Wiederholfeld
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 20:03:46 +02:00
AliceDarkest 035d603504 Merge: Fix Dev-Lab Debugging-Pfad-Icon
mindflux-ci / build (push) Successful in 27s
2026-06-30 18:04:47 +02:00
AliceDarkest 08e5e2b089 Merge: Dev Lab – Entwicklerlabor (6 Mechaniken, 189 Aufgaben) + STYLEGUIDE.md
mindflux-ci / build (push) Successful in 26s
2026-06-30 16:32:45 +02:00
AliceDarkest f2db7f286b Merge: Syntax Saga – neues Spiel + 34 Seed-Missionen
mindflux-ci / build (push) Successful in 27s
2026-06-30 12:14:27 +02:00
AliceDarkest d255c7b4db Merge pull request 'Mobile-Fixes: Spiele scrollbar + FinalFlux-Hub Overlap/Menue' (#6) from fix/mobile-games-finalflux into main
mindflux-ci / build (push) Successful in 27s
Reviewed-on: #6
2026-06-29 18:13:28 +00:00
11 changed files with 95 additions and 18 deletions
+7
View File
@@ -22,8 +22,15 @@ jobs:
- name: Deploy
env:
MINDFLUX_DB_PASSWORD: ${{ secrets.MINDFLUX_DB_PASSWORD }}
MINDFLUX_ADMIN_PASSWORD: ${{ secrets.MINDFLUX_ADMIN_PASSWORD }}
MINDFLUX_ADMIN2_PASSWORD: ${{ secrets.MINDFLUX_ADMIN2_PASSWORD }}
run: |
test -n "$MINDFLUX_DB_PASSWORD" || { echo "MINDFLUX_DB_PASSWORD fehlt" >&2; exit 1; }
docker exec -i -e NEW_DB_PASSWORD="$MINDFLUX_DB_PASSWORD" postgres \
psql -v ON_ERROR_STOP=1 -U mindflux -d mindfluxdb <<'SQL'
\getenv new_db_password NEW_DB_PASSWORD
ALTER USER mindflux WITH PASSWORD :'new_db_password';
SQL
docker compose -f docker-compose.yml pull
docker compose -f docker-compose.yml up -d
+2 -2
View File
@@ -5,10 +5,10 @@ services:
restart: unless-stopped
environment:
# --- Datenbank: unverändert wie im bisherigen Deploy ---
# --- Datenbank: Passwort kommt aus Gitea-Secret (siehe Workflow), nie ins Repo ---
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/mindfluxdb
SPRING_DATASOURCE_USERNAME: mindflux
SPRING_DATASOURCE_PASSWORD: mindflux_pass
SPRING_DATASOURCE_PASSWORD: "${MINDFLUX_DB_PASSWORD:?MINDFLUX_DB_PASSWORD muss als Secret gesetzt sein}"
# --- App: lauscht intern auf 8080 (nginx -> mindflux:8080) ---
SERVER_PORT: "8080"
@@ -49,7 +49,7 @@ public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService(UserRepository userRepo) {
return username -> {
User user = userRepo.findByUsername(username)
User user = userRepo.findByUsernameIgnoreCase(username)
.orElseThrow(() -> new UsernameNotFoundException("Benutzer nicht gefunden"));
return org.springframework.security.core.userdetails.User
@@ -39,10 +39,17 @@ public class AdminUserController {
@PostMapping("/users/save")
public String saveUser(@ModelAttribute("user") User user) {
if (user.getId() != null && (user.getPassword() == null || user.getPassword().isBlank())) {
if (user.getId() != null) {
User existingUser = userService.findById(user.getId())
.orElseThrow(() -> new IllegalArgumentException("Ungültige User-ID: " + user.getId()));
user.setPassword(existingUser.getPassword());
// Passwort: leer = unverändert übernehmen, sonst neu verschlüsseln (Admin-Reset)
if (user.getPassword() == null || user.getPassword().isBlank()) {
user.setPassword(existingUser.getPassword());
} else {
user.setPassword(passwordEncoder.encode(user.getPassword()));
}
// Profilbild steht nicht im Formular -> bestehendes erhalten, nicht überschreiben
user.setProfilePicture(existingUser.getProfilePicture());
} else {
user.setPassword(passwordEncoder.encode(user.getPassword()));
}
@@ -51,6 +58,18 @@ public class AdminUserController {
return "redirect:/admin/users";
}
/** Direktes Zurücksetzen des Passworts eines Nutzers (Admin-Recovery, ohne E-Mail). */
@PostMapping("/users/{id}/reset-password")
public String resetPassword(@PathVariable Long id, @RequestParam String newPassword) {
if (newPassword != null && !newPassword.isBlank()) {
User user = userService.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Ungültige User-ID: " + id));
user.setPassword(passwordEncoder.encode(newPassword.trim()));
userService.save(user);
}
return "redirect:/admin/users";
}
@GetMapping("/users/edit/{id}")
public String editUserForm(@PathVariable Long id, Model model) {
User user = userService.findById(id).orElseThrow(() -> new IllegalArgumentException("Ungültige User-ID: " + id));
@@ -24,17 +24,24 @@ public class RegisterController {
}
@PostMapping("/register")
public String processRegistration(@ModelAttribute("user") User user, Model model) {
// Benutzername klein schreiben, um Doppelungen zu vermeiden
user.setUsername(user.getUsername().toLowerCase());
public String processRegistration(@ModelAttribute("user") User user,
@RequestParam(value = "confirmPassword", required = false) String confirmPassword) {
// Schreibweise des Benutzernamens beibehalten, nur trimmen
String username = user.getUsername() == null ? "" : user.getUsername().trim();
user.setUsername(username);
// Prüfen, ob Benutzername schon existiert
if (userService.findByUsername(user.getUsername()) != null) {
// Zurück zur kombinierten Login/Signup-Seite mit geöffnetem Signup-Panel
// Passwörter müssen übereinstimmen (Schutz vor Tippfehlern bei der Eingabe)
if (user.getPassword() == null || user.getPassword().isBlank()
|| !user.getPassword().equals(confirmPassword)) {
return "redirect:/?passwordMismatch";
}
// Benutzername schon vergeben? (case-insensitiv, damit "Alice" und "alice" nicht kollidieren)
if (username.isBlank() || userService.findByUsernameIgnoreCase(username) != null) {
return "redirect:/?registerError";
}
// Passwort verschlüsseln
// Passwort verschlüsseln und speichern
user.setPassword(passwordEncoder.encode(user.getPassword()));
user.setRole("USER"); // Standardrolle
userService.save(user);
@@ -9,6 +9,9 @@ public interface UserRepository extends JpaRepository<User, Long> {
// Benutzer anhand des Benutzernamens suchen
Optional<User> findByUsername(String username);
// Case-insensitive Suche: Login und Registrierung sollen unabhängig von Groß-/Kleinschreibung greifen
Optional<User> findByUsernameIgnoreCase(String username);
// Benutzer anhand des Benutzernamens löschen
void deleteByUsername(String username);
}
@@ -4,6 +4,7 @@ import com.example.mindflux.model.User;
public interface UserService {
User findByUsername(String username);
User findByUsernameIgnoreCase(String username);
void save(User user);
void deleteByUsername(String username); // <-- Nur die Methodensignatur, ohne {}
}
@@ -25,6 +25,11 @@ public class UserServiceImpl implements UserService {
return userRepository.findByUsername(username).orElse(null); // <-- Optional aufgelöst
}
@Override
public User findByUsernameIgnoreCase(String username) {
return userRepository.findByUsernameIgnoreCase(username).orElse(null);
}
@Override
public void save(User user) {
userRepository.save(user);
@@ -34,8 +34,9 @@
</div>
<div class="mf-field">
<label class="mf-label">Passwort</label>
<input class="mf-input" type="password" name="password" th:required="${user.id == null}" autocomplete="new-password"/>
<label class="mf-label" th:text="${user.id == null} ? 'Passwort' : 'Neues Passwort (leer lassen = unverändert)'">Passwort</label>
<input class="mf-input" type="password" name="password" th:required="${user.id == null}" autocomplete="new-password"
th:placeholder="${user.id == null} ? 'Passwort' : 'leer lassen = unverändert'"/>
</div>
<div class="mf-field">
@@ -43,9 +43,17 @@
<td th:text="${user.id}">1</td>
<td th:text="${user.username}">Max</td>
<td th:text="${user.email}">max@mindflux.de</td>
<td style="display:flex; gap:.5rem; flex-wrap:wrap;">
<td style="display:flex; gap:.5rem; flex-wrap:wrap; align-items:center;">
<a th:href="@{'/admin/users/edit/' + ${user.id}}" class="mf-btn outline">Bearbeiten</a>
<form th:action="@{'/admin/users/delete/' + ${user.id}}" method="post" style="display:inline;">
<form th:action="@{'/admin/users/' + ${user.id} + '/reset-password'}" method="post"
style="display:flex; gap:.35rem; align-items:center;"
onsubmit="return confirm('Passwort für diesen Nutzer wirklich neu setzen?');">
<input class="mf-input" type="text" name="newPassword" placeholder="Neues Passwort"
required minlength="4" style="width:150px; padding:.45rem .6rem;">
<button type="submit" class="mf-btn">PW zurücksetzen</button>
</form>
<form th:action="@{'/admin/users/delete/' + ${user.id}}" method="post" style="display:inline;"
onsubmit="return confirm('Benutzer wirklich löschen?');">
<button type="submit" class="mf-btn danger">Löschen</button>
</form>
</td>
+28 -2
View File
@@ -58,6 +58,7 @@
</div>
<div th:if="${param.registerError}" class="mf-alert err">Benutzername bereits vergeben.</div>
<div th:if="${param.passwordMismatch}" class="mf-alert err">Die Passwörter stimmen nicht überein. Bitte erneut eingeben.</div>
<form th:action="@{/register}" method="post">
<div class="mf-field">
@@ -72,6 +73,11 @@
<label class="mf-label" for="reg-password">Passwort</label>
<input id="reg-password" class="mf-input" type="password" name="password" placeholder="Passwort" required>
</div>
<div class="mf-field">
<label class="mf-label" for="reg-password2">Passwort wiederholen</label>
<input id="reg-password2" class="mf-input" type="password" name="confirmPassword" placeholder="Passwort wiederholen" required>
</div>
<p id="reg-pw-msg" class="mf-alert err" style="display:none">Die Passwörter stimmen nicht überein.</p>
<button class="neon-btn" type="submit"><span class="lock">🚀</span> Account erstellen</button>
</form>
@@ -128,10 +134,30 @@
el.addEventListener('click', function (e) { e.preventDefault(); c.classList.remove('active'); });
});
// Bei Registrierungsfehler direkt das Signup-Panel öffnen
if (new URLSearchParams(window.location.search).has('registerError')) {
// Bei Registrierungs-/Passwortfehler direkt das Signup-Panel öffnen
var qp = new URLSearchParams(window.location.search);
if (qp.has('registerError') || qp.has('passwordMismatch')) {
c.classList.add('active');
}
// Client-seitiger Passwort-Abgleich (sofortiges Feedback; der Server prüft zusätzlich)
var pw = document.getElementById('reg-password');
var pw2 = document.getElementById('reg-password2');
var pwMsg = document.getElementById('reg-pw-msg');
var signupForm = pw2 ? pw2.closest('form') : null;
function pwCheck() {
var mismatch = pw2.value.length > 0 && pw.value !== pw2.value;
if (pwMsg) pwMsg.style.display = mismatch ? 'block' : 'none';
pw2.setCustomValidity(pw.value !== pw2.value ? 'Passwörter stimmen nicht überein' : '');
return !mismatch;
}
if (pw && pw2) {
pw.addEventListener('input', pwCheck);
pw2.addEventListener('input', pwCheck);
if (signupForm) signupForm.addEventListener('submit', function (e) {
if (pw.value !== pw2.value) { e.preventDefault(); pwCheck(); c.classList.add('active'); }
});
}
})();
</script>