Volle Mindflux-App ins Deploy-Repo übernehmen
Ersetzt das Hello-World-Gerüst durch die vollständige Anwendung (com.example.mindflux: 64 Klassen, Thymeleaf-Templates, Static-Assets, Flyway V1–V6). Deploy-Infrastruktur (.gitea, Dockerfile) bleibt erhalten. - pom: volle Abhängigkeiten (Security, Thymeleaf, JPA, Flyway), Spring Boot 3.4.4 / Java 17 - docker-compose: SPRING_DATASOURCE_* unverändert; App lauscht auf 8080 (nginx -> mindflux:8080); Uploads-Volume; Admin-Passwörter via Gitea-Secrets - Workflow: Deploy-Step injiziert MINDFLUX_ADMIN*_PASSWORD aus Secrets - .dockerignore ergänzt; Hello-World (com.mindflux) und Test-Datei 'main' entfernt Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
package com.example.mindflux;
|
||||
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class AppStartCheck implements CommandLineRunner {
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
System.out.println("✅ App läuft. Sicherheitsmodul sollte aktiv sein.");
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,10 +1,11 @@
|
||||
package com.mindflux;
|
||||
package com.example.mindflux;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class MindfluxApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MindfluxApplication.class, args);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.example.mindflux.config;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.repository.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
|
||||
@Configuration
|
||||
public class AdminSetup {
|
||||
|
||||
@Value("${MINDFLUX_ADMIN_USERNAME:admin}")
|
||||
private String adminUsername;
|
||||
|
||||
@Value("${MINDFLUX_ADMIN_EMAIL:admin@mindflux.de}")
|
||||
private String adminEmail;
|
||||
|
||||
@Value("${MINDFLUX_ADMIN_PASSWORD:}")
|
||||
private String adminPassword;
|
||||
|
||||
// Optionaler zweiter Bootstrap-Admin. Wird nur angelegt, wenn Username + Passwort gesetzt sind.
|
||||
@Value("${MINDFLUX_ADMIN2_USERNAME:}")
|
||||
private String admin2Username;
|
||||
|
||||
@Value("${MINDFLUX_ADMIN2_EMAIL:}")
|
||||
private String admin2Email;
|
||||
|
||||
@Value("${MINDFLUX_ADMIN2_PASSWORD:}")
|
||||
private String admin2Password;
|
||||
|
||||
@Bean
|
||||
public CommandLineRunner createAdmin(UserRepository userRepo, BCryptPasswordEncoder passwordEncoder) {
|
||||
return args -> {
|
||||
createAdminIfAbsent(userRepo, passwordEncoder, adminUsername, adminEmail, adminPassword);
|
||||
createAdminIfAbsent(userRepo, passwordEncoder, admin2Username, admin2Email, admin2Password);
|
||||
};
|
||||
}
|
||||
|
||||
private void createAdminIfAbsent(UserRepository userRepo, BCryptPasswordEncoder passwordEncoder,
|
||||
String username, String email, String password) {
|
||||
if (username == null || username.isBlank()) {
|
||||
return; // Admin-Slot nicht konfiguriert -> still überspringen.
|
||||
}
|
||||
if (password == null || password.isBlank()) {
|
||||
System.out.println("Kein Bootstrap-Admin erstellt: Passwort für '" + username + "' ist nicht gesetzt.");
|
||||
return;
|
||||
}
|
||||
if (userRepo.findByUsername(username).isEmpty()) {
|
||||
User admin = new User();
|
||||
admin.setUsername(username);
|
||||
admin.setEmail(email);
|
||||
admin.setPassword(passwordEncoder.encode(password));
|
||||
admin.setRole("ADMIN");
|
||||
userRepo.save(admin);
|
||||
System.out.println("Bootstrap-Admin erstellt: " + username);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.example.mindflux.config;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.repository.UserRepository;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.core.userdetails.*;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/", "/login", "/register", "/css/**", "/js/**", "/img/**").permitAll()
|
||||
.requestMatchers("/admin/**").hasRole("ADMIN") // NEU: Adminbereich absichern
|
||||
.requestMatchers("/api/users/**").hasRole("ADMIN")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(form -> form
|
||||
.loginPage("/") // login.html liegt auf "/"
|
||||
.loginProcessingUrl("/login")
|
||||
.defaultSuccessUrl("/dashboard", true)
|
||||
.failureUrl("/?error") // zeigt Loginfehler an
|
||||
.permitAll()
|
||||
)
|
||||
.logout(logout -> logout
|
||||
.logoutSuccessUrl("/?logout")
|
||||
.permitAll()
|
||||
)
|
||||
.csrf(csrf -> csrf.disable()); // optional deaktivieren bei Custom-Logik/API-Nutzung
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService(UserRepository userRepo) {
|
||||
return username -> {
|
||||
User user = userRepo.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("Benutzer nicht gefunden"));
|
||||
|
||||
return org.springframework.security.core.userdetails.User
|
||||
.withUsername(user.getUsername())
|
||||
.password(user.getPassword())
|
||||
.roles(user.getRole()) // Achtung: Nur ein String, z.B. "USER" oder "ADMIN"
|
||||
.build();
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BCryptPasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
|
||||
return config.getAuthenticationManager();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.example.mindflux.config;
|
||||
|
||||
import com.example.mindflux.model.TechtacularAnswer;
|
||||
import com.example.mindflux.model.TechtacularCategory;
|
||||
import com.example.mindflux.model.TechtacularQuestion;
|
||||
import com.example.mindflux.repository.TechtacularCategoryRepository;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Befüllt TechTacular einmalig (wenn leer) aus der kuratierten Inhaltsdatei
|
||||
* techtacular/content.json. Format: Jeopardy umgekehrt – pro Hinweis eine Aussage
|
||||
* ("clue") und der gesuchte Begriff ("solution", = richtige Antwort) plus Distraktoren.
|
||||
*/
|
||||
@Component
|
||||
public class TechtacularSeeder implements CommandLineRunner {
|
||||
|
||||
private final TechtacularCategoryRepository categoryRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public TechtacularSeeder(TechtacularCategoryRepository categoryRepository, ObjectMapper objectMapper) {
|
||||
this.categoryRepository = categoryRepository;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void run(String... args) {
|
||||
if (categoryRepository.count() > 0) {
|
||||
return; // schon befüllt
|
||||
}
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(new ClassPathResource("techtacular/content.json").getInputStream());
|
||||
JsonNode categories = root.get("categories");
|
||||
if (categories == null || !categories.isArray()) {
|
||||
System.out.println("⚠️ TechTacular-Seed: content.json ohne 'categories'.");
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> usedSlugs = new HashSet<>();
|
||||
int order = 1;
|
||||
int catCount = 0;
|
||||
int clueCount = 0;
|
||||
|
||||
for (JsonNode catNode : categories) {
|
||||
String title = catNode.path("title").asText("");
|
||||
JsonNode clues = catNode.get("clues");
|
||||
if (title.isBlank() || clues == null || !clues.isArray()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TechtacularCategory category =
|
||||
new TechtacularCategory(uniqueSlug(title, usedSlugs), trim(title, 200), order, true);
|
||||
|
||||
for (JsonNode clue : clues) {
|
||||
String clueText = clue.path("clue").asText("");
|
||||
String solution = clue.path("solution").asText("");
|
||||
JsonNode distractors = clue.get("distractors");
|
||||
if (clueText.isBlank() || solution.isBlank() || distractors == null || !distractors.isArray()) {
|
||||
continue;
|
||||
}
|
||||
int points = clue.path("points").asInt(100);
|
||||
String explanation = clue.hasNonNull("explanation") ? clue.get("explanation").asText() : null;
|
||||
|
||||
TechtacularQuestion question = new TechtacularQuestion(
|
||||
trim(clueText, 1000),
|
||||
(explanation == null || explanation.isBlank()) ? null : trim(explanation, 1000),
|
||||
points);
|
||||
|
||||
int sort = 0;
|
||||
question.addAnswer(new TechtacularAnswer(trim(solution, 500), true, sort++)); // richtige Antwort
|
||||
for (JsonNode d : distractors) {
|
||||
String dt = d.asText("");
|
||||
if (!dt.isBlank()) {
|
||||
question.addAnswer(new TechtacularAnswer(trim(dt, 500), false, sort++));
|
||||
}
|
||||
}
|
||||
|
||||
if (question.getAnswers().size() >= 2) {
|
||||
category.addQuestion(question);
|
||||
clueCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!category.getQuestions().isEmpty()) {
|
||||
categoryRepository.save(category);
|
||||
order++;
|
||||
catCount++;
|
||||
}
|
||||
}
|
||||
System.out.println("✅ TechTacular: " + catCount + " Kategorien / " + clueCount + " Hinweise geladen.");
|
||||
} catch (Exception e) {
|
||||
System.out.println("⚠️ TechTacular-Seed übersprungen: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String uniqueSlug(String title, Set<String> used) {
|
||||
String base = title.toLowerCase()
|
||||
.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
|
||||
.replaceAll("[^a-z0-9]+", "-")
|
||||
.replaceAll("(^-|-$)", "");
|
||||
if (base.isBlank()) {
|
||||
base = "kategorie";
|
||||
}
|
||||
String slug = base;
|
||||
int counter = 2;
|
||||
while (used.contains(slug) || categoryRepository.existsBySlug(slug)) {
|
||||
slug = base + "-" + counter++;
|
||||
}
|
||||
used.add(slug);
|
||||
return slug;
|
||||
}
|
||||
|
||||
private String trim(String value, int max) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return value.length() <= max ? value : value.substring(0, max);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.example.mindflux.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry
|
||||
.addResourceHandler("/uploads/**")
|
||||
.addResourceLocations("file:" + System.getProperty("user.dir") + "/uploads/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.Achievement;
|
||||
import com.example.mindflux.service.AchievementService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
public class AchievementController {
|
||||
|
||||
@Autowired
|
||||
private AchievementService achievementService;
|
||||
|
||||
@GetMapping("/profile/achievements")
|
||||
public String showAchievements(Model model, Principal principal) {
|
||||
String username = principal.getName();
|
||||
List<Achievement> achievements = achievementService.getAchievementsForUser(username);
|
||||
|
||||
// 🔥 Dummy-Daten nur für Tests!
|
||||
if (achievements == null || achievements.isEmpty()) {
|
||||
achievements = List.of(
|
||||
new Achievement("Hangman Hero", "Gewinne 10 Runden Hangman", "hangman.png", true),
|
||||
new Achievement("Memory Meister", "Erreiche 1000 Punkte im Memory", "memory.png", false),
|
||||
new Achievement("Quizkönig", "Beantworte 50 Fragen richtig", "quiz.png", true),
|
||||
new Achievement("Early Bird", "Lerne vor 7 Uhr morgens", "earlybird.png", false)
|
||||
);
|
||||
}
|
||||
|
||||
model.addAttribute("achievements", achievements);
|
||||
|
||||
return "achievements"; // lädt achievements.html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.service.MindMatchService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* Admin-Verwaltung der MindMatch-Decks und -Paare. Liegt unter /admin/** und ist
|
||||
* damit per SecurityConfig auf ROLE_ADMIN beschränkt.
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/admin/mindmatch")
|
||||
public class AdminMindMatchController {
|
||||
|
||||
private final MindMatchService mindMatchService;
|
||||
|
||||
public AdminMindMatchController(MindMatchService mindMatchService) {
|
||||
this.mindMatchService = mindMatchService;
|
||||
}
|
||||
|
||||
@GetMapping("")
|
||||
public String manage(Model model) {
|
||||
model.addAttribute("decks", mindMatchService.allDecksWithPairs());
|
||||
return "admin/mindmatch";
|
||||
}
|
||||
|
||||
@PostMapping("/decks")
|
||||
public String createDeck(@RequestParam String title) {
|
||||
mindMatchService.createDeck(title);
|
||||
return "redirect:/admin/mindmatch";
|
||||
}
|
||||
|
||||
@PostMapping("/decks/{deckId}/pairs")
|
||||
public String addPair(@PathVariable Long deckId,
|
||||
@RequestParam String term,
|
||||
@RequestParam String match) {
|
||||
mindMatchService.addPair(deckId, term, match);
|
||||
return "redirect:/admin/mindmatch";
|
||||
}
|
||||
|
||||
@PostMapping("/pairs/{pairId}/delete")
|
||||
public String deletePair(@PathVariable Long pairId) {
|
||||
mindMatchService.deletePair(pairId);
|
||||
return "redirect:/admin/mindmatch";
|
||||
}
|
||||
|
||||
@PostMapping("/decks/{deckId}/delete")
|
||||
public String deleteDeck(@PathVariable Long deckId) {
|
||||
mindMatchService.deleteDeck(deckId);
|
||||
return "redirect:/admin/mindmatch";
|
||||
}
|
||||
|
||||
@PostMapping("/decks/{deckId}/toggle")
|
||||
public String toggleDeck(@PathVariable Long deckId) {
|
||||
mindMatchService.toggleDeck(deckId);
|
||||
return "redirect:/admin/mindmatch";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.TechtacularCategory;
|
||||
import com.example.mindflux.service.TechtacularService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/** Admin-Verwaltung der TechTacular-Kategorien, -Fragen und -Antworten (ROLE_ADMIN via /admin/**). */
|
||||
@Controller
|
||||
@RequestMapping("/admin/techtacular")
|
||||
public class AdminTechtacularController {
|
||||
|
||||
private final TechtacularService techtacularService;
|
||||
|
||||
public AdminTechtacularController(TechtacularService techtacularService) {
|
||||
this.techtacularService = techtacularService;
|
||||
}
|
||||
|
||||
/** Übersicht aller Kategorien. */
|
||||
@GetMapping("")
|
||||
public String manage(Model model) {
|
||||
model.addAttribute("categories", techtacularService.categoryRows());
|
||||
return "admin/techtacular";
|
||||
}
|
||||
|
||||
/** Detailseite einer Kategorie mit ihren Fragen. */
|
||||
@GetMapping("/categories/{categoryId}")
|
||||
public String category(@PathVariable Long categoryId, Model model) {
|
||||
TechtacularCategory category = techtacularService.categoryWithQuestions(categoryId);
|
||||
if (category == null) {
|
||||
return "redirect:/admin/techtacular";
|
||||
}
|
||||
model.addAttribute("category", category);
|
||||
return "admin/techtacular-category";
|
||||
}
|
||||
|
||||
@PostMapping("/categories")
|
||||
public String createCategory(@RequestParam String title) {
|
||||
techtacularService.createCategory(title);
|
||||
return "redirect:/admin/techtacular";
|
||||
}
|
||||
|
||||
@PostMapping("/categories/{categoryId}/questions")
|
||||
public String addQuestion(@PathVariable Long categoryId,
|
||||
@RequestParam String text,
|
||||
@RequestParam(required = false) String explanation,
|
||||
@RequestParam(defaultValue = "100") int points,
|
||||
@RequestParam(required = false, defaultValue = "") String answer1,
|
||||
@RequestParam(required = false, defaultValue = "") String answer2,
|
||||
@RequestParam(required = false, defaultValue = "") String answer3,
|
||||
@RequestParam(required = false, defaultValue = "") String answer4,
|
||||
@RequestParam(defaultValue = "0") int correct) {
|
||||
List<String> answers = Arrays.asList(answer1, answer2, answer3, answer4);
|
||||
techtacularService.addQuestion(categoryId, text, explanation, points, answers, correct);
|
||||
return "redirect:/admin/techtacular/categories/" + categoryId;
|
||||
}
|
||||
|
||||
@PostMapping("/questions/{questionId}/delete")
|
||||
public String deleteQuestion(@PathVariable Long questionId, @RequestParam Long categoryId) {
|
||||
techtacularService.deleteQuestion(questionId);
|
||||
return "redirect:/admin/techtacular/categories/" + categoryId;
|
||||
}
|
||||
|
||||
@PostMapping("/categories/{categoryId}/delete")
|
||||
public String deleteCategory(@PathVariable Long categoryId) {
|
||||
techtacularService.deleteCategory(categoryId);
|
||||
return "redirect:/admin/techtacular";
|
||||
}
|
||||
|
||||
@PostMapping("/categories/{categoryId}/toggle")
|
||||
public String toggleCategory(@PathVariable Long categoryId) {
|
||||
techtacularService.toggleCategory(categoryId);
|
||||
return "redirect:/admin/techtacular";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.service.AdminUserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/admin")
|
||||
public class AdminUserController {
|
||||
|
||||
@Autowired
|
||||
private AdminUserService userService;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
// ⬅️ Neu: Dashboard-Startseite
|
||||
@GetMapping("")
|
||||
public String adminDashboard() {
|
||||
return "admin/dashboard"; // du brauchst: templates/admin/dashboard.html
|
||||
}
|
||||
|
||||
// ⬅️ Benutzerverwaltung
|
||||
@GetMapping("/users")
|
||||
public String listUsers(Model model) {
|
||||
model.addAttribute("users", userService.findAll());
|
||||
return "admin/users/list";
|
||||
}
|
||||
|
||||
@GetMapping("/users/new")
|
||||
public String createUserForm(Model model) {
|
||||
model.addAttribute("user", new User());
|
||||
return "admin/users/form";
|
||||
}
|
||||
|
||||
@PostMapping("/users/save")
|
||||
public String saveUser(@ModelAttribute("user") User user) {
|
||||
if (user.getId() != null && (user.getPassword() == null || user.getPassword().isBlank())) {
|
||||
User existingUser = userService.findById(user.getId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Ungültige User-ID: " + user.getId()));
|
||||
user.setPassword(existingUser.getPassword());
|
||||
} else {
|
||||
user.setPassword(passwordEncoder.encode(user.getPassword()));
|
||||
}
|
||||
|
||||
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));
|
||||
model.addAttribute("user", user);
|
||||
return "admin/users/form";
|
||||
}
|
||||
|
||||
@PostMapping("/users/delete/{id}")
|
||||
public String deleteUser(@PathVariable Long id) {
|
||||
userService.deleteById(id);
|
||||
return "redirect:/admin/users";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class DashboardController {
|
||||
|
||||
@GetMapping("/dashboard")
|
||||
public String showDashboard() {
|
||||
return "dashboard"; // lädt dashboard.html aus templates
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class DummyController {
|
||||
|
||||
@GetMapping("/comingsoon")
|
||||
public String comingSoon() {
|
||||
return "dummy"; // verweist auf dummy.html in src/main/resources/templates
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class FinalfluxController {
|
||||
|
||||
@GetMapping("/finalflux")
|
||||
public String showFinalfluxOverview() {
|
||||
return "finalflux"; // Lädt templates/finalflux.html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.dto.GameStats;
|
||||
import com.example.mindflux.dto.ScoreResult;
|
||||
import com.example.mindflux.dto.ScoreSubmission;
|
||||
import com.example.mindflux.service.GameScoreService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Generisches Score-API für alle Mindflux-Spiele. Der Spielname kommt aus der URL
|
||||
* und wird gegen eine Allowlist geprüft, damit keine beliebigen Spiele-Kennungen
|
||||
* in die Datenbank wandern.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/games")
|
||||
public class GameScoreController {
|
||||
|
||||
/** Bekannte Spiele (entsprechen den Dashboard-Routen). */
|
||||
private static final Set<String> KNOWN_GAMES = Set.of(
|
||||
"mindmatch", "techtacular", "syntax-saga", "escape-room",
|
||||
"service-clash", "bug-hunter", "netzwerker");
|
||||
|
||||
private final GameScoreService gameScoreService;
|
||||
|
||||
public GameScoreController(GameScoreService gameScoreService) {
|
||||
this.gameScoreService = gameScoreService;
|
||||
}
|
||||
|
||||
@PostMapping("/{game}/scores")
|
||||
public ResponseEntity<ScoreResult> submitScore(@PathVariable String game,
|
||||
@RequestBody ScoreSubmission submission,
|
||||
Principal principal) {
|
||||
if (!KNOWN_GAMES.contains(game)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(gameScoreService.record(principal.getName(), game, submission));
|
||||
}
|
||||
|
||||
@GetMapping("/{game}/stats")
|
||||
public ResponseEntity<GameStats> getStats(@PathVariable String game, Principal principal) {
|
||||
if (!KNOWN_GAMES.contains(game)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(gameScoreService.getStats(principal.getName(), game));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.dto.MindMatchDeckView;
|
||||
import com.example.mindflux.service.MindMatchService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Liefert die aktiven MindMatch-Decks (inkl. Paare) an die Spielseite. */
|
||||
@RestController
|
||||
@RequestMapping("/api/mindmatch")
|
||||
public class MindMatchApiController {
|
||||
|
||||
private final MindMatchService mindMatchService;
|
||||
|
||||
public MindMatchApiController(MindMatchService mindMatchService) {
|
||||
this.mindMatchService = mindMatchService;
|
||||
}
|
||||
|
||||
@GetMapping("/decks")
|
||||
public List<MindMatchDeckView> decks() {
|
||||
return mindMatchService.activeDecks();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class MindMatchController {
|
||||
|
||||
@GetMapping("/mindmatch")
|
||||
public String showMindMatch() {
|
||||
return "mindmatch"; // lädt templates/mindmatch.html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.service.UserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.Principal;
|
||||
import java.util.UUID;
|
||||
|
||||
@Controller
|
||||
public class ProfileController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private BCryptPasswordEncoder passwordEncoder;
|
||||
|
||||
private static final String UPLOAD_DIR = System.getProperty("user.dir") + "/uploads/profiles/";
|
||||
|
||||
@GetMapping("/profile")
|
||||
public String profile(Model model, Principal principal, @RequestParam(value = "success", required = false) Boolean success) {
|
||||
String username = principal.getName();
|
||||
User user = userService.findByUsername(username);
|
||||
|
||||
model.addAttribute("user", user);
|
||||
|
||||
if (Boolean.TRUE.equals(success)) {
|
||||
model.addAttribute("message", "Profil erfolgreich aktualisiert!");
|
||||
}
|
||||
|
||||
return "profile";
|
||||
}
|
||||
|
||||
@PostMapping("/profile/delete-picture")
|
||||
public String deleteProfilePicture(Principal principal) {
|
||||
String username = principal.getName();
|
||||
User existingUser = userService.findByUsername(username);
|
||||
|
||||
if (existingUser != null) {
|
||||
String profilePicture = existingUser.getProfilePicture();
|
||||
if (profilePicture != null && !profilePicture.trim().isEmpty()) {
|
||||
try {
|
||||
Path picturePath = Paths.get(System.getProperty("user.dir"), "uploads", "profiles", profilePicture);
|
||||
System.out.println("Versuche zu löschen: " + picturePath.toAbsolutePath());
|
||||
|
||||
if (Files.exists(picturePath)) {
|
||||
Files.delete(picturePath);
|
||||
System.out.println("Datei erfolgreich gelöscht: " + picturePath.getFileName());
|
||||
} else {
|
||||
System.out.println("Datei existiert nicht: " + picturePath.toAbsolutePath());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("Fehler beim Löschen der Datei: " + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
System.out.println("Kein Profilbild gesetzt, nichts zu löschen.");
|
||||
}
|
||||
|
||||
existingUser.setProfilePicture(null); // Wichtig: auch wenn Datei nicht existiert
|
||||
userService.save(existingUser);
|
||||
}
|
||||
|
||||
return "redirect:/profile?success=true";
|
||||
}
|
||||
|
||||
@GetMapping("/profile/knowledge")
|
||||
public String knowledgeCenter(Model model, Principal principal) {
|
||||
String username = principal.getName();
|
||||
User user = userService.findByUsername(username);
|
||||
|
||||
model.addAttribute("user", user);
|
||||
return "knowledge";
|
||||
}
|
||||
|
||||
@PostMapping("/profile/update")
|
||||
public String updateProfile(@RequestParam("email") String email,
|
||||
@RequestParam(value = "profilePicture", required = false) MultipartFile file,
|
||||
Principal principal) {
|
||||
String username = principal.getName();
|
||||
User existingUser = userService.findByUsername(username);
|
||||
|
||||
if (existingUser != null) {
|
||||
existingUser.setEmail(email); // Email aktualisieren
|
||||
|
||||
if (file != null && !file.isEmpty()) {
|
||||
try {
|
||||
// Altes Profilbild löschen (falls vorhanden)
|
||||
if (existingUser.getProfilePicture() != null && !existingUser.getProfilePicture().isEmpty()) {
|
||||
Path oldFilePath = Paths.get(System.getProperty("user.dir"), "uploads", "profiles", existingUser.getProfilePicture());
|
||||
if (Files.exists(oldFilePath)) {
|
||||
Files.delete(oldFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
// Neues Profilbild speichern
|
||||
String fileExtension = getFileExtension(file.getOriginalFilename());
|
||||
String fileName = UUID.randomUUID() + fileExtension;
|
||||
|
||||
Path uploadPath = Paths.get(UPLOAD_DIR);
|
||||
Files.createDirectories(uploadPath);
|
||||
|
||||
Path filePath = uploadPath.resolve(fileName);
|
||||
file.transferTo(filePath.toFile());
|
||||
|
||||
existingUser.setProfilePicture(fileName);
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
userService.save(existingUser);
|
||||
}
|
||||
|
||||
return "redirect:/profile?success=true";
|
||||
}
|
||||
|
||||
@PostMapping("/profile/change-password")
|
||||
public String changePassword(@RequestParam("oldPassword") String oldPassword,
|
||||
@RequestParam("newPassword") String newPassword,
|
||||
Principal principal,
|
||||
Model model) {
|
||||
String username = principal.getName();
|
||||
User user = userService.findByUsername(username);
|
||||
|
||||
if (user == null) {
|
||||
model.addAttribute("error", "Benutzer nicht gefunden.");
|
||||
return "profile";
|
||||
}
|
||||
|
||||
if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
|
||||
model.addAttribute("error", "Das aktuelle Passwort ist falsch.");
|
||||
return "profile";
|
||||
}
|
||||
|
||||
user.setPassword(passwordEncoder.encode(newPassword));
|
||||
userService.save(user);
|
||||
|
||||
model.addAttribute("message", "Passwort erfolgreich geändert.");
|
||||
return "profile";
|
||||
}
|
||||
|
||||
@PostMapping("/profile/delete")
|
||||
public String deleteProfile(Principal principal, Authentication authentication, HttpServletRequest request, HttpServletResponse response) {
|
||||
String username = principal.getName();
|
||||
userService.deleteByUsername(username);
|
||||
new SecurityContextLogoutHandler().logout(request, response, authentication);
|
||||
return "redirect:/?logout";
|
||||
}
|
||||
|
||||
// Hilfsmethode: Dateiendung extrahieren
|
||||
private String getFileExtension(String filename) {
|
||||
if (filename == null || filename.lastIndexOf(".") == -1) {
|
||||
return "";
|
||||
}
|
||||
return filename.substring(filename.lastIndexOf("."));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.dto.GameProgress;
|
||||
import com.example.mindflux.dto.ProgressData;
|
||||
import com.example.mindflux.service.ProgressService;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
public class ProgressController {
|
||||
|
||||
@Autowired
|
||||
private ProgressService progressService;
|
||||
|
||||
@GetMapping("/progress") // <-- hier geändert!
|
||||
public String showProgress(Model model, Principal principal) {
|
||||
String username = principal.getName(); // aktueller eingeloggter Benutzer
|
||||
|
||||
ProgressData progress = progressService.getOverallProgress(username);
|
||||
List<GameProgress> games = progressService.getGameProgress(username);
|
||||
|
||||
if (progress == null) {
|
||||
// Falls keine Fortschrittsdaten vorhanden sind, Dummy-Daten setzen
|
||||
model.addAttribute("correctAnswers", 0);
|
||||
model.addAttribute("currentXp", 0);
|
||||
model.addAttribute("xpForNextLevel", 1000);
|
||||
model.addAttribute("userLevel", 1);
|
||||
model.addAttribute("xpPercentage", 0);
|
||||
} else {
|
||||
model.addAttribute("correctAnswers", progress.getCorrectAnswers());
|
||||
model.addAttribute("currentXp", progress.getCurrentXp());
|
||||
model.addAttribute("xpForNextLevel", progress.getXpForNextLevel());
|
||||
model.addAttribute("userLevel", progress.getUserLevel());
|
||||
model.addAttribute("xpPercentage", progress.getProgressPercent());
|
||||
}
|
||||
|
||||
model.addAttribute("gameProgress", games != null ? games : List.of()); // leere Liste, falls null
|
||||
|
||||
return "progress"; // ruft progress.html auf
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.service.UserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Controller
|
||||
public class RegisterController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@GetMapping("/register")
|
||||
public String showRegistrationForm(Model model) {
|
||||
model.addAttribute("user", new User());
|
||||
return "register";
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
public String processRegistration(@ModelAttribute("user") User user, Model model) {
|
||||
// Benutzername klein schreiben, um Doppelungen zu vermeiden
|
||||
user.setUsername(user.getUsername().toLowerCase());
|
||||
|
||||
// Prüfen, ob Benutzername schon existiert
|
||||
if (userService.findByUsername(user.getUsername()) != null) {
|
||||
model.addAttribute("error", "Benutzername bereits vergeben.");
|
||||
return "register";
|
||||
}
|
||||
|
||||
// Passwort verschlüsseln
|
||||
user.setPassword(passwordEncoder.encode(user.getPassword()));
|
||||
user.setRole("USER"); // Standardrolle
|
||||
userService.save(user);
|
||||
|
||||
return "redirect:/?registerSuccess"; // zurück zum Login mit Erfolgsmeldung
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class SettingsController {
|
||||
|
||||
@GetMapping("/profile/settings")
|
||||
public String showSettingsPage() {
|
||||
return "settings"; // lädt templates/settings.html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class SozialesController {
|
||||
|
||||
@GetMapping("/soziales")
|
||||
public String soziales() {
|
||||
return "soziales"; // -> templates/soziales.html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.dto.*;
|
||||
import com.example.mindflux.service.TechtacularService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/** Spieler-API für TechTacular: Board laden, Antwort prüfen, Runde abschließen. */
|
||||
@RestController
|
||||
@RequestMapping("/api/techtacular")
|
||||
public class TechtacularApiController {
|
||||
|
||||
private final TechtacularService techtacularService;
|
||||
|
||||
public TechtacularApiController(TechtacularService techtacularService) {
|
||||
this.techtacularService = techtacularService;
|
||||
}
|
||||
|
||||
/** exclude = komma-separierte Frage-IDs der Vorrunde, die möglichst nicht erneut kommen sollen. */
|
||||
@GetMapping("/board")
|
||||
public TechtacularBoardView board(@RequestParam(required = false) String exclude) {
|
||||
return techtacularService.buildBoard(parseIds(exclude));
|
||||
}
|
||||
|
||||
private Set<Long> parseIds(String csv) {
|
||||
Set<Long> ids = new HashSet<>();
|
||||
if (csv != null && !csv.isBlank()) {
|
||||
for (String part : csv.split(",")) {
|
||||
try {
|
||||
ids.add(Long.parseLong(part.trim()));
|
||||
} catch (NumberFormatException ignored) {
|
||||
// ungültige IDs einfach überspringen
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
@PostMapping("/answer")
|
||||
public AnswerResult answer(@RequestBody AnswerRequest request) {
|
||||
return techtacularService.checkAnswer(request.getQuestionId(), request.getAnswerId());
|
||||
}
|
||||
|
||||
@PostMapping("/finish")
|
||||
public TechtacularResult finish(@RequestBody FinishRequest request, Principal principal) {
|
||||
return techtacularService.finish(principal.getName(), request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class TechtacularController {
|
||||
|
||||
@GetMapping("/techtacular")
|
||||
public String showTechtacular() {
|
||||
return "techtacular"; // lädt templates/techtacular.html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.repository.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
// 📥 User erstellen
|
||||
@PostMapping
|
||||
public User createUser(@RequestBody User user) {
|
||||
user.setPassword(passwordEncoder.encode(user.getPassword()));
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
// 📋 Alle User anzeigen
|
||||
@GetMapping
|
||||
public List<User> getAllUsers() {
|
||||
return userRepository.findAll();
|
||||
}
|
||||
|
||||
// 📍 Einzelnen User anzeigen
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<User> getUserById(@PathVariable Long id) {
|
||||
Optional<User> user = userRepository.findById(id);
|
||||
return user.map(ResponseEntity::ok)
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
// ✏️ User aktualisieren
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User updatedUser) {
|
||||
return userRepository.findById(id).map(user -> {
|
||||
user.setUsername(updatedUser.getUsername());
|
||||
if (updatedUser.getPassword() != null && !updatedUser.getPassword().isBlank()) {
|
||||
user.setPassword(passwordEncoder.encode(updatedUser.getPassword()));
|
||||
}
|
||||
user.setEmail(updatedUser.getEmail());
|
||||
user.setRole(updatedUser.getRole());
|
||||
return ResponseEntity.ok(userRepository.save(user));
|
||||
}).orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
// 🗑️ User löschen
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
|
||||
if (userRepository.existsById(id)) {
|
||||
userRepository.deleteById(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.repository.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/users")
|
||||
public class UserViewController {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
// Formular anzeigen
|
||||
@GetMapping("/form")
|
||||
public String showForm(Model model) {
|
||||
model.addAttribute("user", new User());
|
||||
return "user-form";
|
||||
}
|
||||
|
||||
// Benutzer speichern
|
||||
@PostMapping("/save")
|
||||
public String saveUser(@ModelAttribute User user) {
|
||||
userRepository.save(user);
|
||||
return "user-success"; // statt redirect
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Eine Zeile in der TechTacular-Kategorienübersicht (ohne alle Fragen zu laden). */
|
||||
public class AdminCategoryRow {
|
||||
private final Long id;
|
||||
private final String slug;
|
||||
private final String title;
|
||||
private final boolean active;
|
||||
private final long questionCount;
|
||||
|
||||
public AdminCategoryRow(Long id, String slug, String title, boolean active, long questionCount) {
|
||||
this.id = id;
|
||||
this.slug = slug;
|
||||
this.title = title;
|
||||
this.active = active;
|
||||
this.questionCount = questionCount;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public String getSlug() { return slug; }
|
||||
public String getTitle() { return title; }
|
||||
public boolean isActive() { return active; }
|
||||
public long getQuestionCount() { return questionCount; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Eine vom Spieler gewählte Antwort. */
|
||||
public class AnswerRequest {
|
||||
private Long questionId;
|
||||
private Long answerId;
|
||||
|
||||
public Long getQuestionId() { return questionId; }
|
||||
public void setQuestionId(Long questionId) { this.questionId = questionId; }
|
||||
|
||||
public Long getAnswerId() { return answerId; }
|
||||
public void setAnswerId(Long answerId) { this.answerId = answerId; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Serverseitige Bewertung einer einzelnen Antwort (instant feedback). */
|
||||
public class AnswerResult {
|
||||
private final boolean correct;
|
||||
private final Long correctAnswerId;
|
||||
private final String explanation;
|
||||
private final int points;
|
||||
|
||||
public AnswerResult(boolean correct, Long correctAnswerId, String explanation, int points) {
|
||||
this.correct = correct;
|
||||
this.correctAnswerId = correctAnswerId;
|
||||
this.explanation = explanation;
|
||||
this.points = points;
|
||||
}
|
||||
|
||||
public boolean isCorrect() { return correct; }
|
||||
public Long getCorrectAnswerId() { return correctAnswerId; }
|
||||
public String getExplanation() { return explanation; }
|
||||
public int getPoints() { return points; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Abschluss einer TechTacular-Runde: alle gewählten Antworten + Dauer. Score wird serverseitig recomputed. */
|
||||
public class FinishRequest {
|
||||
private int durationSeconds;
|
||||
private List<AnswerRequest> answers;
|
||||
|
||||
public int getDurationSeconds() { return durationSeconds; }
|
||||
public void setDurationSeconds(int durationSeconds) { this.durationSeconds = durationSeconds; }
|
||||
|
||||
public List<AnswerRequest> getAnswers() { return answers; }
|
||||
public void setAnswers(List<AnswerRequest> answers) { this.answers = answers; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public class GameProgress {
|
||||
private String name;
|
||||
private int progressPercent;
|
||||
private int highscore;
|
||||
private int plays;
|
||||
private LocalDate lastPlayed;
|
||||
|
||||
// Getter
|
||||
public String getName() { return name; }
|
||||
public int getProgressPercent() { return progressPercent; }
|
||||
public int getHighscore() { return highscore; }
|
||||
public int getPlays() { return plays; }
|
||||
public LocalDate getLastPlayed() { return lastPlayed; }
|
||||
|
||||
// Setter
|
||||
public void setName(String name) { this.name = name; }
|
||||
public void setProgressPercent(int progressPercent) { this.progressPercent = progressPercent; }
|
||||
public void setHighscore(int highscore) { this.highscore = highscore; }
|
||||
public void setPlays(int plays) { this.plays = plays; }
|
||||
public void setLastPlayed(LocalDate lastPlayed) { this.lastPlayed = lastPlayed; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/** Zusammengefasste Statistik eines Spielers für ein Spiel. */
|
||||
public class GameStats {
|
||||
private final int highscore;
|
||||
private final long plays;
|
||||
private final LocalDate lastPlayed;
|
||||
|
||||
public GameStats(int highscore, long plays, LocalDate lastPlayed) {
|
||||
this.highscore = highscore;
|
||||
this.plays = plays;
|
||||
this.lastPlayed = lastPlayed;
|
||||
}
|
||||
|
||||
public int getHighscore() { return highscore; }
|
||||
public long getPlays() { return plays; }
|
||||
public LocalDate getLastPlayed() { return lastPlayed; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Ein aktives Deck mit seinen Paaren fürs Spiel-API. */
|
||||
public class MindMatchDeckView {
|
||||
private final String slug;
|
||||
private final String title;
|
||||
private final List<MindMatchPairView> pairs;
|
||||
|
||||
public MindMatchDeckView(String slug, String title, List<MindMatchPairView> pairs) {
|
||||
this.slug = slug;
|
||||
this.title = title;
|
||||
this.pairs = pairs;
|
||||
}
|
||||
|
||||
public String getSlug() { return slug; }
|
||||
public String getTitle() { return title; }
|
||||
public List<MindMatchPairView> getPairs() { return pairs; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Ein Paar fürs Spiel-API. Feld heißt "match" (passend zur bisherigen JS-Logik). */
|
||||
public class MindMatchPairView {
|
||||
private final String term;
|
||||
private final String match;
|
||||
|
||||
public MindMatchPairView(String term, String match) {
|
||||
this.term = term;
|
||||
this.match = match;
|
||||
}
|
||||
|
||||
public String getTerm() { return term; }
|
||||
public String getMatch() { return match; }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
public class ProgressData {
|
||||
private int correctAnswers;
|
||||
private int currentXp;
|
||||
private int xpForNextLevel;
|
||||
private int userLevel;
|
||||
private int progressPercent;
|
||||
|
||||
public int getCorrectAnswers() {
|
||||
return correctAnswers;
|
||||
}
|
||||
|
||||
public void setCorrectAnswers(int correctAnswers) {
|
||||
this.correctAnswers = correctAnswers;
|
||||
}
|
||||
|
||||
public int getCurrentXp() {
|
||||
return currentXp;
|
||||
}
|
||||
|
||||
public void setCurrentXp(int currentXp) {
|
||||
this.currentXp = currentXp;
|
||||
}
|
||||
|
||||
public int getXpForNextLevel() {
|
||||
return xpForNextLevel;
|
||||
}
|
||||
|
||||
public void setXpForNextLevel(int xpForNextLevel) {
|
||||
this.xpForNextLevel = xpForNextLevel;
|
||||
}
|
||||
|
||||
public int getUserLevel() {
|
||||
return userLevel;
|
||||
}
|
||||
|
||||
public void setUserLevel(int userLevel) {
|
||||
this.userLevel = userLevel;
|
||||
}
|
||||
|
||||
public int getProgressPercent() {
|
||||
return progressPercent;
|
||||
}
|
||||
|
||||
public void setProgressPercent(int progressPercent) {
|
||||
this.progressPercent = progressPercent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Antwort nach dem Speichern eines Ergebnisses. */
|
||||
public class ScoreResult {
|
||||
private final int score;
|
||||
private final int highscore;
|
||||
private final boolean newHighscore;
|
||||
private final long plays;
|
||||
|
||||
public ScoreResult(int score, int highscore, boolean newHighscore, long plays) {
|
||||
this.score = score;
|
||||
this.highscore = highscore;
|
||||
this.newHighscore = newHighscore;
|
||||
this.plays = plays;
|
||||
}
|
||||
|
||||
public int getScore() { return score; }
|
||||
public int getHighscore() { return highscore; }
|
||||
public boolean isNewHighscore() { return newHighscore; }
|
||||
public long getPlays() { return plays; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/**
|
||||
* Was der Client nach einem abgeschlossenen Spiel sendet. Der Score wird
|
||||
* bewusst NICHT mitgeschickt, sondern serverseitig aus diesen Rohwerten
|
||||
* berechnet, damit ein manipulierter Client keinen Fantasie-Score speichern kann.
|
||||
*/
|
||||
public class ScoreSubmission {
|
||||
private String deck;
|
||||
private int pairs;
|
||||
private int moves;
|
||||
private int durationSeconds;
|
||||
|
||||
public String getDeck() { return deck; }
|
||||
public void setDeck(String deck) { this.deck = deck; }
|
||||
|
||||
public int getPairs() { return pairs; }
|
||||
public void setPairs(int pairs) { this.pairs = pairs; }
|
||||
|
||||
public int getMoves() { return moves; }
|
||||
public void setMoves(int moves) { this.moves = moves; }
|
||||
|
||||
public int getDurationSeconds() { return durationSeconds; }
|
||||
public void setDurationSeconds(int durationSeconds) { this.durationSeconds = durationSeconds; }
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Das Jeopardy-Board fürs Frontend: Spalten (Kategorien) mit Kacheln (Fragen).
|
||||
* Bewusst OHNE Information darüber, welche Antwort korrekt ist – geprüft wird serverseitig.
|
||||
*/
|
||||
public class TechtacularBoardView {
|
||||
|
||||
private final List<Category> categories;
|
||||
private final int maxScore;
|
||||
|
||||
public TechtacularBoardView(List<Category> categories, int maxScore) {
|
||||
this.categories = categories;
|
||||
this.maxScore = maxScore;
|
||||
}
|
||||
|
||||
public List<Category> getCategories() { return categories; }
|
||||
public int getMaxScore() { return maxScore; }
|
||||
|
||||
public static class Category {
|
||||
private final String title;
|
||||
private final List<Tile> tiles;
|
||||
|
||||
public Category(String title, List<Tile> tiles) {
|
||||
this.title = title;
|
||||
this.tiles = tiles;
|
||||
}
|
||||
|
||||
public String getTitle() { return title; }
|
||||
public List<Tile> getTiles() { return tiles; }
|
||||
}
|
||||
|
||||
public static class Tile {
|
||||
private final Long questionId;
|
||||
private final int points;
|
||||
private final String text;
|
||||
private final List<Option> answers;
|
||||
|
||||
public Tile(Long questionId, int points, String text, List<Option> answers) {
|
||||
this.questionId = questionId;
|
||||
this.points = points;
|
||||
this.text = text;
|
||||
this.answers = answers;
|
||||
}
|
||||
|
||||
public Long getQuestionId() { return questionId; }
|
||||
public int getPoints() { return points; }
|
||||
public String getText() { return text; }
|
||||
public List<Option> getAnswers() { return answers; }
|
||||
}
|
||||
|
||||
public static class Option {
|
||||
private final Long id;
|
||||
private final String text;
|
||||
|
||||
public Option(Long id, String text) {
|
||||
this.id = id;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public String getText() { return text; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Ergebnis einer abgeschlossenen TechTacular-Runde. */
|
||||
public class TechtacularResult {
|
||||
private final int score;
|
||||
private final int maxScore;
|
||||
private final int correctCount;
|
||||
private final int totalCount;
|
||||
private final int highscore;
|
||||
private final boolean newHighscore;
|
||||
private final long plays;
|
||||
|
||||
public TechtacularResult(int score, int maxScore, int correctCount, int totalCount,
|
||||
int highscore, boolean newHighscore, long plays) {
|
||||
this.score = score;
|
||||
this.maxScore = maxScore;
|
||||
this.correctCount = correctCount;
|
||||
this.totalCount = totalCount;
|
||||
this.highscore = highscore;
|
||||
this.newHighscore = newHighscore;
|
||||
this.plays = plays;
|
||||
}
|
||||
|
||||
public int getScore() { return score; }
|
||||
public int getMaxScore() { return maxScore; }
|
||||
public int getCorrectCount() { return correctCount; }
|
||||
public int getTotalCount() { return totalCount; }
|
||||
public int getHighscore() { return highscore; }
|
||||
public boolean isNewHighscore() { return newHighscore; }
|
||||
public long getPlays() { return plays; }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "achievements")
|
||||
public class Achievement {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String username;
|
||||
private String title;
|
||||
private String description;
|
||||
private String icon; // Dateiname des Icons (z.B. "star.png")
|
||||
private LocalDateTime dateAchieved;
|
||||
|
||||
// 🔥 Neuer Konstruktor für schnelle Dummy-Erstellung (ohne username/dateAchieved)
|
||||
public Achievement(String title, String description, String icon, boolean unlocked) {
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.icon = icon;
|
||||
this.dateAchieved = unlocked ? LocalDateTime.now() : null;
|
||||
}
|
||||
|
||||
// ⚡ Standard-Konstruktor (wichtig für JPA!)
|
||||
public Achievement() {
|
||||
}
|
||||
|
||||
// Getter und Setter
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
|
||||
public String getIcon() { return icon; }
|
||||
public void setIcon(String icon) { this.icon = icon; }
|
||||
|
||||
public LocalDateTime getDateAchieved() { return dateAchieved; }
|
||||
public void setDateAchieved(LocalDateTime dateAchieved) { this.dateAchieved = dateAchieved; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Ein einzelnes Spielergebnis. Bewusst generisch gehalten, damit alle
|
||||
* Mindflux-Spiele (MindMatch, TechTacular, ...) dieselbe Tabelle nutzen können.
|
||||
* Welches Spiel das Ergebnis erzeugt hat, steht in {@link #game}.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "game_scores")
|
||||
public class GameScore {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String username;
|
||||
|
||||
/** Spiel-Kennung, z.B. "mindmatch" (entspricht der Route im Dashboard). */
|
||||
private String game;
|
||||
|
||||
/** Optionales Deck / Level innerhalb des Spiels, z.B. "netzwerk". */
|
||||
private String deck;
|
||||
|
||||
private int score;
|
||||
|
||||
/** Anzahl der Zug-Versuche (kann je nach Spiel null sein). */
|
||||
private Integer moves;
|
||||
|
||||
@Column(name = "duration_seconds")
|
||||
private Integer durationSeconds;
|
||||
|
||||
@Column(name = "played_at")
|
||||
private LocalDateTime playedAt;
|
||||
|
||||
public GameScore() {
|
||||
}
|
||||
|
||||
public GameScore(String username, String game, String deck, int score,
|
||||
Integer moves, Integer durationSeconds, LocalDateTime playedAt) {
|
||||
this.username = username;
|
||||
this.game = game;
|
||||
this.deck = deck;
|
||||
this.score = score;
|
||||
this.moves = moves;
|
||||
this.durationSeconds = durationSeconds;
|
||||
this.playedAt = playedAt;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
|
||||
public String getGame() { return game; }
|
||||
public void setGame(String game) { this.game = game; }
|
||||
|
||||
public String getDeck() { return deck; }
|
||||
public void setDeck(String deck) { this.deck = deck; }
|
||||
|
||||
public int getScore() { return score; }
|
||||
public void setScore(int score) { this.score = score; }
|
||||
|
||||
public Integer getMoves() { return moves; }
|
||||
public void setMoves(Integer moves) { this.moves = moves; }
|
||||
|
||||
public Integer getDurationSeconds() { return durationSeconds; }
|
||||
public void setDurationSeconds(Integer durationSeconds) { this.durationSeconds = durationSeconds; }
|
||||
|
||||
public LocalDateTime getPlayedAt() { return playedAt; }
|
||||
public void setPlayedAt(LocalDateTime playedAt) { this.playedAt = playedAt; }
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Ein MindMatch-Deck (Kategorie) mit seinen Begriffspaaren. Von Admins pflegbar. */
|
||||
@Entity
|
||||
@Table(name = "mindmatch_decks")
|
||||
public class MindMatchDeck {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(unique = true)
|
||||
private String slug;
|
||||
|
||||
private String title;
|
||||
|
||||
@Column(name = "sort_order")
|
||||
private int sortOrder;
|
||||
|
||||
private boolean active = true;
|
||||
|
||||
@OneToMany(mappedBy = "deck", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@OrderBy("id ASC")
|
||||
private List<MindMatchPair> pairs = new ArrayList<>();
|
||||
|
||||
public MindMatchDeck() {
|
||||
}
|
||||
|
||||
public MindMatchDeck(String slug, String title, int sortOrder, boolean active) {
|
||||
this.slug = slug;
|
||||
this.title = title;
|
||||
this.sortOrder = sortOrder;
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
/** Fügt ein Paar hinzu und hält die Rückreferenz konsistent. */
|
||||
public void addPair(MindMatchPair pair) {
|
||||
pair.setDeck(this);
|
||||
this.pairs.add(pair);
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getSlug() { return slug; }
|
||||
public void setSlug(String slug) { this.slug = slug; }
|
||||
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
|
||||
public int getSortOrder() { return sortOrder; }
|
||||
public void setSortOrder(int sortOrder) { this.sortOrder = sortOrder; }
|
||||
|
||||
public boolean isActive() { return active; }
|
||||
public void setActive(boolean active) { this.active = active; }
|
||||
|
||||
public List<MindMatchPair> getPairs() { return pairs; }
|
||||
public void setPairs(List<MindMatchPair> pairs) { this.pairs = pairs; }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
/** Ein Begriffspaar innerhalb eines {@link MindMatchDeck}: Begriff + zugehörige Bedeutung. */
|
||||
@Entity
|
||||
@Table(name = "mindmatch_pairs")
|
||||
public class MindMatchPair {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "deck_id")
|
||||
private MindMatchDeck deck;
|
||||
|
||||
private String term;
|
||||
|
||||
/** Spalte heißt "match_text", da MATCH in SQL ein reserviertes Wort ist. */
|
||||
@Column(name = "match_text")
|
||||
private String matchText;
|
||||
|
||||
public MindMatchPair() {
|
||||
}
|
||||
|
||||
public MindMatchPair(String term, String matchText) {
|
||||
this.term = term;
|
||||
this.matchText = matchText;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public MindMatchDeck getDeck() { return deck; }
|
||||
public void setDeck(MindMatchDeck deck) { this.deck = deck; }
|
||||
|
||||
public String getTerm() { return term; }
|
||||
public void setTerm(String term) { this.term = term; }
|
||||
|
||||
public String getMatchText() { return matchText; }
|
||||
public void setMatchText(String matchText) { this.matchText = matchText; }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
/** Eine Antwortmöglichkeit zu einer {@link TechtacularQuestion}. */
|
||||
@Entity
|
||||
@Table(name = "techtacular_answers")
|
||||
public class TechtacularAnswer {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "question_id")
|
||||
private TechtacularQuestion question;
|
||||
|
||||
@Column(name = "answer_text")
|
||||
private String text;
|
||||
|
||||
private boolean correct;
|
||||
|
||||
@Column(name = "sort_order")
|
||||
private int sortOrder;
|
||||
|
||||
public TechtacularAnswer() {
|
||||
}
|
||||
|
||||
public TechtacularAnswer(String text, boolean correct, int sortOrder) {
|
||||
this.text = text;
|
||||
this.correct = correct;
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public TechtacularQuestion getQuestion() { return question; }
|
||||
public void setQuestion(TechtacularQuestion question) { this.question = question; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
|
||||
public boolean isCorrect() { return correct; }
|
||||
public void setCorrect(boolean correct) { this.correct = correct; }
|
||||
|
||||
public int getSortOrder() { return sortOrder; }
|
||||
public void setSortOrder(int sortOrder) { this.sortOrder = sortOrder; }
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Eine TechTacular-Kategorie (Spalte im Jeopardy-Board) mit ihren Fragen. */
|
||||
@Entity
|
||||
@Table(name = "techtacular_categories")
|
||||
public class TechtacularCategory {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(unique = true)
|
||||
private String slug;
|
||||
|
||||
private String title;
|
||||
|
||||
@Column(name = "sort_order")
|
||||
private int sortOrder;
|
||||
|
||||
private boolean active = true;
|
||||
|
||||
@OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@OrderBy("points ASC, id ASC")
|
||||
private List<TechtacularQuestion> questions = new ArrayList<>();
|
||||
|
||||
public TechtacularCategory() {
|
||||
}
|
||||
|
||||
public TechtacularCategory(String slug, String title, int sortOrder, boolean active) {
|
||||
this.slug = slug;
|
||||
this.title = title;
|
||||
this.sortOrder = sortOrder;
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
public void addQuestion(TechtacularQuestion question) {
|
||||
question.setCategory(this);
|
||||
this.questions.add(question);
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getSlug() { return slug; }
|
||||
public void setSlug(String slug) { this.slug = slug; }
|
||||
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
|
||||
public int getSortOrder() { return sortOrder; }
|
||||
public void setSortOrder(int sortOrder) { this.sortOrder = sortOrder; }
|
||||
|
||||
public boolean isActive() { return active; }
|
||||
public void setActive(boolean active) { this.active = active; }
|
||||
|
||||
public List<TechtacularQuestion> getQuestions() { return questions; }
|
||||
public void setQuestions(List<TechtacularQuestion> questions) { this.questions = questions; }
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Eine Frage mit ihren Antwortmöglichkeiten (genau eine ist korrekt) und Punktwert. */
|
||||
@Entity
|
||||
@Table(name = "techtacular_questions")
|
||||
public class TechtacularQuestion {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "category_id")
|
||||
private TechtacularCategory category;
|
||||
|
||||
@Column(name = "question_text")
|
||||
private String text;
|
||||
|
||||
private String explanation;
|
||||
|
||||
private int points;
|
||||
|
||||
@OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@OrderBy("sortOrder ASC, id ASC")
|
||||
private List<TechtacularAnswer> answers = new ArrayList<>();
|
||||
|
||||
public TechtacularQuestion() {
|
||||
}
|
||||
|
||||
public TechtacularQuestion(String text, String explanation, int points) {
|
||||
this.text = text;
|
||||
this.explanation = explanation;
|
||||
this.points = points;
|
||||
}
|
||||
|
||||
public void addAnswer(TechtacularAnswer answer) {
|
||||
answer.setQuestion(this);
|
||||
this.answers.add(answer);
|
||||
}
|
||||
|
||||
/** Liefert die als korrekt markierte Antwort (oder null). */
|
||||
public TechtacularAnswer correctAnswer() {
|
||||
return answers.stream().filter(TechtacularAnswer::isCorrect).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public TechtacularCategory getCategory() { return category; }
|
||||
public void setCategory(TechtacularCategory category) { this.category = category; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
|
||||
public String getExplanation() { return explanation; }
|
||||
public void setExplanation(String explanation) { this.explanation = explanation; }
|
||||
|
||||
public int getPoints() { return points; }
|
||||
public void setPoints(int points) { this.points = points; }
|
||||
|
||||
public List<TechtacularAnswer> getAnswers() { return answers; }
|
||||
public void setAnswers(List<TechtacularAnswer> answers) { this.answers = answers; }
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
public class User {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String username;
|
||||
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
|
||||
private String password;
|
||||
private String email;
|
||||
private String role;
|
||||
|
||||
@Column(name = "profile_picture")
|
||||
private String profilePicture;
|
||||
|
||||
public User() {}
|
||||
|
||||
public User(String username, String password, String email, String role) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.email = email;
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
// —————————————
|
||||
// Getter / Setter
|
||||
// —————————————
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String u) { this.username = u; }
|
||||
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String p) { this.password = p; }
|
||||
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String e) { this.email = e; }
|
||||
|
||||
public String getRole() { return role; }
|
||||
public void setRole(String r) { this.role = r; }
|
||||
|
||||
public String getProfilePicture() { return profilePicture; }
|
||||
public void setProfilePicture(String pictureUrl) { this.profilePicture = pictureUrl; }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.Achievement;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AchievementRepository extends JpaRepository<Achievement, Long> {
|
||||
List<Achievement> findByUsername(String username);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface AdminUserRepository extends JpaRepository<User, Long> {
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.GameScore;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface GameScoreRepository extends JpaRepository<GameScore, Long> {
|
||||
|
||||
/** Bestes Ergebnis eines Spielers in einem Spiel (Highscore). */
|
||||
Optional<GameScore> findTopByUsernameAndGameOrderByScoreDesc(String username, String game);
|
||||
|
||||
/** Zuletzt gespieltes Ergebnis eines Spielers in einem Spiel. */
|
||||
Optional<GameScore> findTopByUsernameAndGameOrderByPlayedAtDesc(String username, String game);
|
||||
|
||||
/** Wie oft ein Spieler ein Spiel abgeschlossen hat. */
|
||||
long countByUsernameAndGame(String username, String game);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.MindMatchDeck;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface MindMatchDeckRepository extends JpaRepository<MindMatchDeck, Long> {
|
||||
|
||||
List<MindMatchDeck> findAllByActiveTrueOrderBySortOrderAscIdAsc();
|
||||
|
||||
List<MindMatchDeck> findAllByOrderBySortOrderAscIdAsc();
|
||||
|
||||
Optional<MindMatchDeck> findBySlug(String slug);
|
||||
|
||||
boolean existsBySlug(String slug);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.MindMatchPair;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface MindMatchPairRepository extends JpaRepository<MindMatchPair, Long> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.TechtacularAnswer;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface TechtacularAnswerRepository extends JpaRepository<TechtacularAnswer, Long> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.TechtacularCategory;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface TechtacularCategoryRepository extends JpaRepository<TechtacularCategory, Long> {
|
||||
|
||||
List<TechtacularCategory> findAllByActiveTrueOrderBySortOrderAscIdAsc();
|
||||
|
||||
List<TechtacularCategory> findAllByOrderBySortOrderAscIdAsc();
|
||||
|
||||
boolean existsBySlug(String slug);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.TechtacularQuestion;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface TechtacularQuestionRepository extends JpaRepository<TechtacularQuestion, Long> {
|
||||
|
||||
long countByCategoryId(Long categoryId);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
|
||||
// Benutzer anhand des Benutzernamens suchen
|
||||
Optional<User> findByUsername(String username);
|
||||
|
||||
// Benutzer anhand des Benutzernamens löschen
|
||||
void deleteByUsername(String username);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.model.Achievement;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AchievementService {
|
||||
List<Achievement> getAchievementsForUser(String username);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.model.Achievement;
|
||||
import com.example.mindflux.repository.AchievementRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class AchievementServiceImpl implements AchievementService {
|
||||
|
||||
@Autowired
|
||||
private AchievementRepository achievementRepository;
|
||||
|
||||
@Override
|
||||
public List<Achievement> getAchievementsForUser(String username) {
|
||||
return achievementRepository.findByUsername(username);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.repository.AdminUserRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class AdminUserService {
|
||||
|
||||
@Autowired
|
||||
private AdminUserRepository userRepository;
|
||||
|
||||
public List<User> findAll() {
|
||||
return userRepository.findAll();
|
||||
}
|
||||
|
||||
public Optional<User> findById(Long id) {
|
||||
return userRepository.findById(id);
|
||||
}
|
||||
|
||||
public User save(User user) {
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
public void deleteById(Long id) {
|
||||
userRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.dto.GameStats;
|
||||
import com.example.mindflux.dto.ScoreResult;
|
||||
import com.example.mindflux.dto.ScoreSubmission;
|
||||
import com.example.mindflux.model.GameScore;
|
||||
import com.example.mindflux.repository.GameScoreRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Speichert Spielergebnisse und liefert Statistiken. Der Score wird hier
|
||||
* autoritativ berechnet – der Client schickt nur die Rohwerte.
|
||||
*/
|
||||
@Service
|
||||
public class GameScoreService {
|
||||
|
||||
private final GameScoreRepository repository;
|
||||
|
||||
public GameScoreService(GameScoreRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Punktezahl aus Genauigkeit und Tempo.
|
||||
* <ul>
|
||||
* <li>Genauigkeit: {@code 1000 * Paare / Züge} – 1000 Punkte bei perfektem Spiel
|
||||
* (jeder Aufdeck-Versuch ein Treffer), weniger bei vielen Fehlversuchen.</li>
|
||||
* <li>Zeitbonus: bis zu 500 Punkte, der nach 100 Sekunden auf 0 fällt.</li>
|
||||
* </ul>
|
||||
* Identisch in {@code mindmatch.js} gespiegelt, damit der Client sofort denselben
|
||||
* Wert anzeigt.
|
||||
*/
|
||||
public static int calculateScore(int pairs, int moves, int durationSeconds) {
|
||||
if (pairs <= 0) {
|
||||
return 0;
|
||||
}
|
||||
int safeMoves = Math.max(moves, pairs); // niemals weniger Züge als Paare
|
||||
int safeSeconds = Math.max(durationSeconds, 0);
|
||||
|
||||
int accuracyScore = Math.round(1000f * pairs / safeMoves);
|
||||
int timeBonus = Math.max(0, 500 - safeSeconds * 5);
|
||||
return accuracyScore + timeBonus;
|
||||
}
|
||||
|
||||
/** Speichert ein abgeschlossenes MindMatch-Spiel (Score aus Rohwerten berechnet). */
|
||||
public ScoreResult record(String username, String game, ScoreSubmission submission) {
|
||||
int moves = Math.max(submission.getMoves(), submission.getPairs());
|
||||
int seconds = Math.max(submission.getDurationSeconds(), 0);
|
||||
int score = calculateScore(submission.getPairs(), moves, seconds);
|
||||
return recordScore(username, game, submission.getDeck(), score, moves, seconds);
|
||||
}
|
||||
|
||||
/** Speichert ein bereits berechnetes Ergebnis (z.B. TechTacular) und liefert den Highscore-Stand. */
|
||||
public ScoreResult recordScore(String username, String game, String deck,
|
||||
int score, Integer moves, Integer durationSeconds) {
|
||||
GameScore previousBest = repository
|
||||
.findTopByUsernameAndGameOrderByScoreDesc(username, game)
|
||||
.orElse(null);
|
||||
|
||||
repository.save(new GameScore(
|
||||
username, game, deck, score, moves, durationSeconds, LocalDateTime.now()));
|
||||
|
||||
boolean newHighscore = previousBest == null || score > previousBest.getScore();
|
||||
int highscore = previousBest == null ? score : Math.max(previousBest.getScore(), score);
|
||||
long plays = repository.countByUsernameAndGame(username, game);
|
||||
|
||||
return new ScoreResult(score, highscore, newHighscore, plays);
|
||||
}
|
||||
|
||||
/** Aktuelle Statistik (Highscore, Anzahl Spiele, zuletzt gespielt) für ein Spiel. */
|
||||
public GameStats getStats(String username, String game) {
|
||||
int highscore = repository.findTopByUsernameAndGameOrderByScoreDesc(username, game)
|
||||
.map(GameScore::getScore)
|
||||
.orElse(0);
|
||||
long plays = repository.countByUsernameAndGame(username, game);
|
||||
var lastPlayed = repository.findTopByUsernameAndGameOrderByPlayedAtDesc(username, game)
|
||||
.map(score -> score.getPlayedAt().toLocalDate())
|
||||
.orElse(null);
|
||||
return new GameStats(highscore, plays, lastPlayed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.dto.MindMatchDeckView;
|
||||
import com.example.mindflux.dto.MindMatchPairView;
|
||||
import com.example.mindflux.model.MindMatchDeck;
|
||||
import com.example.mindflux.model.MindMatchPair;
|
||||
import com.example.mindflux.repository.MindMatchDeckRepository;
|
||||
import com.example.mindflux.repository.MindMatchPairRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class MindMatchService {
|
||||
|
||||
private final MindMatchDeckRepository deckRepository;
|
||||
private final MindMatchPairRepository pairRepository;
|
||||
|
||||
public MindMatchService(MindMatchDeckRepository deckRepository, MindMatchPairRepository pairRepository) {
|
||||
this.deckRepository = deckRepository;
|
||||
this.pairRepository = pairRepository;
|
||||
}
|
||||
|
||||
// ---------- Spieler ----------
|
||||
|
||||
/** Aktive Decks mit Paaren fürs Spiel (open-in-view=false -> hier initialisiert). */
|
||||
@Transactional(readOnly = true)
|
||||
public List<MindMatchDeckView> activeDecks() {
|
||||
return deckRepository.findAllByActiveTrueOrderBySortOrderAscIdAsc().stream()
|
||||
.map(deck -> new MindMatchDeckView(
|
||||
deck.getSlug(),
|
||||
deck.getTitle(),
|
||||
deck.getPairs().stream()
|
||||
.map(pair -> new MindMatchPairView(pair.getTerm(), pair.getMatchText()))
|
||||
.toList()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// ---------- Admin ----------
|
||||
|
||||
/** Alle Decks (auch inaktive) inkl. Paaren für die Verwaltung. */
|
||||
@Transactional(readOnly = true)
|
||||
public List<MindMatchDeck> allDecksWithPairs() {
|
||||
List<MindMatchDeck> decks = deckRepository.findAllByOrderBySortOrderAscIdAsc();
|
||||
decks.forEach(deck -> deck.getPairs().size()); // Lazy-Collection laden
|
||||
return decks;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void createDeck(String title) {
|
||||
if (title == null || title.isBlank()) {
|
||||
return;
|
||||
}
|
||||
String clean = title.trim();
|
||||
int order = (int) deckRepository.count() + 1;
|
||||
deckRepository.save(new MindMatchDeck(uniqueSlug(clean), clean, order, true));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void addPair(Long deckId, String term, String match) {
|
||||
if (term == null || term.isBlank() || match == null || match.isBlank()) {
|
||||
return;
|
||||
}
|
||||
deckRepository.findById(deckId).ifPresent(deck -> {
|
||||
deck.addPair(new MindMatchPair(term.trim(), match.trim()));
|
||||
deckRepository.save(deck);
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deletePair(Long pairId) {
|
||||
pairRepository.deleteById(pairId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteDeck(Long deckId) {
|
||||
deckRepository.deleteById(deckId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void toggleDeck(Long deckId) {
|
||||
deckRepository.findById(deckId).ifPresent(deck -> deck.setActive(!deck.isActive()));
|
||||
}
|
||||
|
||||
/** Erzeugt aus dem Titel einen URL-tauglichen, eindeutigen Slug. */
|
||||
private String uniqueSlug(String title) {
|
||||
String base = title.toLowerCase()
|
||||
.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
|
||||
.replaceAll("[^a-z0-9]+", "-")
|
||||
.replaceAll("(^-|-$)", "");
|
||||
if (base.isBlank()) {
|
||||
base = "deck";
|
||||
}
|
||||
String slug = base;
|
||||
int counter = 2;
|
||||
while (deckRepository.existsBySlug(slug)) {
|
||||
slug = base + "-" + counter++;
|
||||
}
|
||||
return slug;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.dto.GameProgress;
|
||||
import com.example.mindflux.dto.GameStats;
|
||||
import com.example.mindflux.dto.ProgressData;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class ProgressService {
|
||||
|
||||
/** Spiel-Kennung -> Anzeigename, in Dashboard-Reihenfolge. */
|
||||
private static final Map<String, String> GAMES = new LinkedHashMap<>();
|
||||
static {
|
||||
GAMES.put("mindmatch", "MindMatch");
|
||||
GAMES.put("techtacular", "TechTacular");
|
||||
GAMES.put("syntax-saga", "Syntax Saga");
|
||||
GAMES.put("escape-room", "Code Escape Room");
|
||||
GAMES.put("service-clash", "Service Clash");
|
||||
GAMES.put("bug-hunter", "Bug Hunter");
|
||||
GAMES.put("netzwerker", "Netzwerker");
|
||||
}
|
||||
|
||||
private final GameScoreService gameScoreService;
|
||||
|
||||
public ProgressService(GameScoreService gameScoreService) {
|
||||
this.gameScoreService = gameScoreService;
|
||||
}
|
||||
|
||||
public ProgressData getOverallProgress(String username) {
|
||||
// Dummy-Daten – später aus DB berechnen
|
||||
ProgressData data = new ProgressData();
|
||||
data.setCorrectAnswers(432);
|
||||
data.setCurrentXp(2400);
|
||||
data.setXpForNextLevel(3000);
|
||||
data.setUserLevel(5);
|
||||
data.setProgressPercent(78);
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Echter Spiel-Fortschritt pro Spiel aus der Datenbank (game_scores). */
|
||||
public List<GameProgress> getGameProgress(String username) {
|
||||
List<GameProgress> result = new ArrayList<>();
|
||||
for (Map.Entry<String, String> entry : GAMES.entrySet()) {
|
||||
GameStats stats = gameScoreService.getStats(username, entry.getKey());
|
||||
result.add(toGameProgress(entry.getValue(), stats));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private GameProgress toGameProgress(String displayName, GameStats stats) {
|
||||
GameProgress game = new GameProgress();
|
||||
game.setName(displayName);
|
||||
game.setHighscore(stats.getHighscore());
|
||||
game.setPlays((int) stats.getPlays());
|
||||
game.setLastPlayed(stats.getLastPlayed());
|
||||
// Fortschritt auf der Score-Skala (max ~1500 Punkte -> 100 %).
|
||||
game.setProgressPercent(Math.min(100, Math.round(stats.getHighscore() / 15.0f)));
|
||||
return game;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.dto.*;
|
||||
import com.example.mindflux.model.TechtacularAnswer;
|
||||
import com.example.mindflux.model.TechtacularCategory;
|
||||
import com.example.mindflux.model.TechtacularQuestion;
|
||||
import com.example.mindflux.repository.TechtacularCategoryRepository;
|
||||
import com.example.mindflux.repository.TechtacularQuestionRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class TechtacularService {
|
||||
|
||||
private static final String GAME = "techtacular";
|
||||
private static final int BOARD_CATEGORIES = 5;
|
||||
private static final int[] POINT_TIERS = {100, 200, 300, 400, 500};
|
||||
|
||||
private final TechtacularCategoryRepository categoryRepository;
|
||||
private final TechtacularQuestionRepository questionRepository;
|
||||
private final GameScoreService gameScoreService;
|
||||
private final Random random = new Random();
|
||||
|
||||
public TechtacularService(TechtacularCategoryRepository categoryRepository,
|
||||
TechtacularQuestionRepository questionRepository,
|
||||
GameScoreService gameScoreService) {
|
||||
this.categoryRepository = categoryRepository;
|
||||
this.questionRepository = questionRepository;
|
||||
this.gameScoreService = gameScoreService;
|
||||
}
|
||||
|
||||
// ---------- Spieler ----------
|
||||
|
||||
/**
|
||||
* Baut ein zufälliges Board: bis zu 5 Kategorien, je eine Frage pro Punktstufe (100–500).
|
||||
* Fragen aus {@code exclude} (z.B. die Vorrunde) werden möglichst vermieden, damit jede
|
||||
* Runde mit überwiegend neuen Fragen gefüllt wird.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public TechtacularBoardView buildBoard(Set<Long> exclude) {
|
||||
Set<Long> excluded = (exclude == null) ? Set.of() : exclude;
|
||||
|
||||
List<TechtacularCategory> usable = categoryRepository.findAllByActiveTrueOrderBySortOrderAscIdAsc()
|
||||
.stream().filter(c -> !c.getQuestions().isEmpty()).collect(Collectors.toList());
|
||||
Collections.shuffle(usable, random);
|
||||
List<TechtacularCategory> chosen = usable.stream().limit(BOARD_CATEGORIES).toList();
|
||||
|
||||
List<TechtacularBoardView.Category> columns = new ArrayList<>();
|
||||
int maxScore = 0;
|
||||
|
||||
for (TechtacularCategory category : chosen) {
|
||||
Map<Integer, List<TechtacularQuestion>> byPoints = category.getQuestions().stream()
|
||||
.collect(Collectors.groupingBy(TechtacularQuestion::getPoints));
|
||||
|
||||
List<TechtacularBoardView.Tile> tiles = new ArrayList<>();
|
||||
for (int value : POINT_TIERS) {
|
||||
List<TechtacularQuestion> pool = byPoints.get(value);
|
||||
if (pool == null || pool.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
// Bevorzugt Fragen, die nicht in der Vorrunde dran waren.
|
||||
List<TechtacularQuestion> fresh = pool.stream()
|
||||
.filter(q -> !excluded.contains(q.getId())).collect(Collectors.toList());
|
||||
List<TechtacularQuestion> pick = fresh.isEmpty() ? pool : fresh;
|
||||
TechtacularQuestion question = pick.get(random.nextInt(pick.size()));
|
||||
List<TechtacularBoardView.Option> options = question.getAnswers().stream()
|
||||
.map(a -> new TechtacularBoardView.Option(a.getId(), a.getText()))
|
||||
.collect(Collectors.toList());
|
||||
Collections.shuffle(options, random);
|
||||
tiles.add(new TechtacularBoardView.Tile(question.getId(), value, question.getText(), options));
|
||||
maxScore += value;
|
||||
}
|
||||
if (!tiles.isEmpty()) {
|
||||
columns.add(new TechtacularBoardView.Category(category.getTitle(), tiles));
|
||||
}
|
||||
}
|
||||
return new TechtacularBoardView(columns, maxScore);
|
||||
}
|
||||
|
||||
/** Bewertet eine einzelne Antwort serverseitig (Frontend kennt die richtige Antwort vorher nicht). */
|
||||
@Transactional(readOnly = true)
|
||||
public AnswerResult checkAnswer(Long questionId, Long answerId) {
|
||||
TechtacularQuestion question = questionRepository.findById(questionId).orElse(null);
|
||||
if (question == null) {
|
||||
return new AnswerResult(false, null, "", 0);
|
||||
}
|
||||
TechtacularAnswer correct = question.correctAnswer();
|
||||
Long correctId = correct != null ? correct.getId() : null;
|
||||
boolean isCorrect = correctId != null && correctId.equals(answerId);
|
||||
return new AnswerResult(isCorrect, correctId, question.getExplanation(), isCorrect ? question.getPoints() : 0);
|
||||
}
|
||||
|
||||
/** Schließt eine Runde ab: Score wird aus den tatsächlichen Antworten recomputed und gespeichert. */
|
||||
@Transactional
|
||||
public TechtacularResult finish(String username, FinishRequest request) {
|
||||
int score = 0;
|
||||
int maxScore = 0;
|
||||
int correctCount = 0;
|
||||
int total = 0;
|
||||
|
||||
if (request.getAnswers() != null) {
|
||||
for (AnswerRequest answer : request.getAnswers()) {
|
||||
if (answer.getQuestionId() == null) {
|
||||
continue;
|
||||
}
|
||||
TechtacularQuestion question = questionRepository.findById(answer.getQuestionId()).orElse(null);
|
||||
if (question == null) {
|
||||
continue;
|
||||
}
|
||||
total++;
|
||||
maxScore += question.getPoints();
|
||||
TechtacularAnswer correct = question.correctAnswer();
|
||||
if (correct != null && correct.getId().equals(answer.getAnswerId())) {
|
||||
score += question.getPoints();
|
||||
correctCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int seconds = Math.max(request.getDurationSeconds(), 0);
|
||||
ScoreResult saved = gameScoreService.recordScore(username, GAME, null, score, correctCount, seconds);
|
||||
return new TechtacularResult(score, maxScore, correctCount, total,
|
||||
saved.getHighscore(), saved.isNewHighscore(), saved.getPlays());
|
||||
}
|
||||
|
||||
// ---------- Admin ----------
|
||||
|
||||
/** Leichte Übersicht: Kategorien mit Fragenanzahl (lädt nicht alle Fragen). */
|
||||
@Transactional(readOnly = true)
|
||||
public List<AdminCategoryRow> categoryRows() {
|
||||
return categoryRepository.findAllByOrderBySortOrderAscIdAsc().stream()
|
||||
.map(c -> new AdminCategoryRow(c.getId(), c.getSlug(), c.getTitle(), c.isActive(),
|
||||
questionRepository.countByCategoryId(c.getId())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/** Eine Kategorie inkl. Fragen und Antworten (für die Detailseite). */
|
||||
@Transactional(readOnly = true)
|
||||
public TechtacularCategory categoryWithQuestions(Long id) {
|
||||
TechtacularCategory category = categoryRepository.findById(id).orElse(null);
|
||||
if (category != null) {
|
||||
category.getQuestions().forEach(q -> q.getAnswers().size()); // lazy init
|
||||
}
|
||||
return category;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void createCategory(String title) {
|
||||
if (title == null || title.isBlank()) {
|
||||
return;
|
||||
}
|
||||
String clean = title.trim();
|
||||
int order = (int) categoryRepository.count() + 1;
|
||||
categoryRepository.save(new TechtacularCategory(uniqueSlug(clean), clean, order, true));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void addQuestion(Long categoryId, String text, String explanation, int points,
|
||||
List<String> answers, int correctIndex) {
|
||||
if (text == null || text.isBlank() || answers == null) {
|
||||
return;
|
||||
}
|
||||
categoryRepository.findById(categoryId).ifPresent(category -> {
|
||||
TechtacularQuestion question = new TechtacularQuestion(
|
||||
text.trim(),
|
||||
(explanation == null || explanation.isBlank()) ? null : explanation.trim(),
|
||||
normalizePoints(points));
|
||||
|
||||
int sort = 0;
|
||||
boolean hasCorrect = false;
|
||||
for (int i = 0; i < answers.size(); i++) {
|
||||
String answerText = answers.get(i);
|
||||
if (answerText == null || answerText.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
boolean isCorrect = (i == correctIndex);
|
||||
hasCorrect = hasCorrect || isCorrect;
|
||||
question.addAnswer(new TechtacularAnswer(answerText.trim(), isCorrect, sort++));
|
||||
}
|
||||
|
||||
// Nur sinnvolle Fragen speichern: mind. 2 Antworten und genau die markierte ist korrekt.
|
||||
if (question.getAnswers().size() >= 2 && hasCorrect) {
|
||||
category.addQuestion(question);
|
||||
categoryRepository.save(category);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteQuestion(Long questionId) {
|
||||
questionRepository.deleteById(questionId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteCategory(Long categoryId) {
|
||||
categoryRepository.deleteById(categoryId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void toggleCategory(Long categoryId) {
|
||||
categoryRepository.findById(categoryId).ifPresent(c -> c.setActive(!c.isActive()));
|
||||
}
|
||||
|
||||
private int normalizePoints(int points) {
|
||||
int rounded = Math.round(points / 100f) * 100;
|
||||
return Math.max(100, Math.min(500, rounded));
|
||||
}
|
||||
|
||||
private String uniqueSlug(String title) {
|
||||
String base = title.toLowerCase()
|
||||
.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
|
||||
.replaceAll("[^a-z0-9]+", "-")
|
||||
.replaceAll("(^-|-$)", "");
|
||||
if (base.isBlank()) {
|
||||
base = "kategorie";
|
||||
}
|
||||
String slug = base;
|
||||
int counter = 2;
|
||||
while (categoryRepository.existsBySlug(slug)) {
|
||||
slug = base + "-" + counter++;
|
||||
}
|
||||
return slug;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
|
||||
public interface UserService {
|
||||
User findByUsername(String username);
|
||||
void save(User user);
|
||||
void deleteByUsername(String username); // <-- Nur die Methodensignatur, ohne {}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.repository.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class UserServiceImpl implements UserService {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Override
|
||||
public User findByUsername(String username) {
|
||||
return userRepository.findByUsername(username).orElse(null); // <-- Optional aufgelöst
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(User user) {
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByUsername(String username) {
|
||||
userRepository.deleteByUsername(username);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.mindflux;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
public class HelloController {
|
||||
|
||||
@GetMapping("/")
|
||||
public Map<String, String> hello() {
|
||||
return Map.of(
|
||||
"app", "mindflux",
|
||||
"status", "running",
|
||||
"message", "Hello from Spring Boot 🚀, automatic deploy is working. :-)"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user