Add OnlineQuiz app
This commit is contained in:
28
OnlineQuiz/build.gradle
Normal file
28
OnlineQuiz/build.gradle
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import org.springframework.boot.gradle.plugin.SpringBootPlugin
|
||||||
|
|
||||||
|
apply plugin: 'java'
|
||||||
|
apply plugin: 'org.springframework.boot'
|
||||||
|
apply plugin: 'io.spring.dependency-management'
|
||||||
|
|
||||||
|
description = 'Secure Online Quiz Application'
|
||||||
|
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyManagement {
|
||||||
|
imports {
|
||||||
|
mavenBom SpringBootPlugin.BOM_COORDINATES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.app.quiz;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class OnlineQuizApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(OnlineQuizApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.app.quiz.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class WebSecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
http.authorizeHttpRequests(request -> request
|
||||||
|
.requestMatchers("/login","/register").permitAll()
|
||||||
|
.requestMatchers("/home").hasAnyRole("USER", "ADMIN")
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.formLogin(form -> form
|
||||||
|
.loginPage("/login").permitAll()
|
||||||
|
.defaultSuccessUrl("/home")
|
||||||
|
)
|
||||||
|
.logout(Customizer.withDefaults());
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public AuthenticationManager authenticationManager() {
|
||||||
|
return authentication ->
|
||||||
|
new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), authentication.getCredentials());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
package com.app.quiz.controller;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.web.authentication.WebAuthenticationDetails;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import com.app.quiz.service.QuizUserDetailsService;
|
||||||
|
import com.app.quiz.model.Question;
|
||||||
|
import com.app.quiz.service.QuestionsService;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class QuizController {
|
||||||
|
|
||||||
|
private final QuizUserDetailsService userDetailsService;
|
||||||
|
private final QuestionsService questionsService;
|
||||||
|
private final AuthenticationManager authenticationManager;
|
||||||
|
|
||||||
|
public QuizController(QuizUserDetailsService userDetailsService, AuthenticationManager authenticationManager, QuestionsService questionsService) {
|
||||||
|
this.userDetailsService = userDetailsService;
|
||||||
|
this.authenticationManager = authenticationManager;
|
||||||
|
this.questionsService = questionsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/home")
|
||||||
|
public String homepage(Model model) {
|
||||||
|
// Get the authenticated user's details
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
// Get the username
|
||||||
|
String username = authentication.getName();
|
||||||
|
model.addAttribute("username", username);
|
||||||
|
|
||||||
|
// Get the user's role
|
||||||
|
String role = authentication.getAuthorities().stream()
|
||||||
|
.map(GrantedAuthority::getAuthority)
|
||||||
|
.findFirst()
|
||||||
|
.orElse("ROLE_USER"); // Default role if no authority is found
|
||||||
|
|
||||||
|
// Redirect to the appropriate page based on the role
|
||||||
|
if (role.equals("ROLE_ADMIN")) {
|
||||||
|
// Fetch the latest quizzes from the service
|
||||||
|
List<Question> quizzes = questionsService.loadQuizzes();
|
||||||
|
|
||||||
|
// Add the quizzes to the model
|
||||||
|
model.addAttribute("quizzes", quizzes);
|
||||||
|
return "QuizList"; // Return the QuizList.html template
|
||||||
|
} else {
|
||||||
|
// Fetch the latest quizzes from the service
|
||||||
|
List<Question> quizzes = questionsService.loadQuizzes();
|
||||||
|
|
||||||
|
// Add the quizzes to the model
|
||||||
|
model.addAttribute("quizzes", quizzes);
|
||||||
|
return "Quiz"; // Return the Quiz.html template
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/login")
|
||||||
|
public String login() {
|
||||||
|
return "login"; // Returns the login.html template
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/register")
|
||||||
|
public String register() {
|
||||||
|
return "register"; // Returns the register.html template
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST endpoint to handle user registration and auto-login
|
||||||
|
@PostMapping("/register")
|
||||||
|
public String registerUser(
|
||||||
|
@RequestParam String username, // Username from the form
|
||||||
|
@RequestParam String password, // Password from the form
|
||||||
|
@RequestParam String email, // Email from the form
|
||||||
|
@RequestParam String role, // Role from the form,
|
||||||
|
HttpServletRequest request
|
||||||
|
) {
|
||||||
|
// Register the user by storing their details in the HashMap
|
||||||
|
try {
|
||||||
|
userDetailsService.registerUser(username, password, email, role);
|
||||||
|
} catch (Exception userExistsAlready) {
|
||||||
|
// Redirect to the /register endpoint
|
||||||
|
return "redirect:/register?error";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate the user
|
||||||
|
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password);
|
||||||
|
authToken.setDetails(new WebAuthenticationDetails(request));
|
||||||
|
Authentication authentication = authenticationManager.authenticate(authToken);
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
|
||||||
|
// Redirect to the /login endpoint
|
||||||
|
return "redirect:/login?success";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/addQuiz")
|
||||||
|
public String showAddQuizForm(Model model) {
|
||||||
|
model.addAttribute("quiz", new Question()); // Add a new Quiz object to the model
|
||||||
|
return "addQuiz"; // Return the addQuiz.html template
|
||||||
|
}
|
||||||
|
@PostMapping("/addQuiz")
|
||||||
|
public String addQuiz(@ModelAttribute Question quiz, Model model, Authentication authentication) {
|
||||||
|
// Get the user's role
|
||||||
|
String role = authentication.getAuthorities().stream()
|
||||||
|
.map(GrantedAuthority::getAuthority)
|
||||||
|
.findFirst()
|
||||||
|
.orElse("ROLE_USER"); // Default role if no authority is found
|
||||||
|
|
||||||
|
// Redirect to the appropriate page based on the role
|
||||||
|
if (role.equals("ROLE_ADMIN")) {
|
||||||
|
quiz.setId(questionsService.getNextId());
|
||||||
|
// Add the quiz to the service
|
||||||
|
questionsService.addQuestion(quiz);
|
||||||
|
|
||||||
|
// Add a success message to the model
|
||||||
|
model.addAttribute("success", "Quiz added successfully!");
|
||||||
|
|
||||||
|
// Redirect to the quiz list page
|
||||||
|
return "redirect:/home";
|
||||||
|
} else {
|
||||||
|
// Add an error message to the model
|
||||||
|
model.addAttribute("error", "You do not have permission to add a quiz.");
|
||||||
|
|
||||||
|
// Redirect to the add quiz page
|
||||||
|
return "redirect:/addQuiz?error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display the edit quiz page
|
||||||
|
@GetMapping("/editQuiz/{id}")
|
||||||
|
public String showEditQuizForm(@PathVariable("id") int id, Model model) {
|
||||||
|
// Find the quiz by ID
|
||||||
|
Question quiz = questionsService.getQuestionById(id);
|
||||||
|
|
||||||
|
// Add the quiz to the model
|
||||||
|
model.addAttribute("quiz", quiz);
|
||||||
|
|
||||||
|
// Return the editQuiz.html template
|
||||||
|
return "editQuiz";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/editQuestion")
|
||||||
|
public String editQuestion(@ModelAttribute("quiz") Question quiz) {
|
||||||
|
// Get the authenticated user's details
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
// Get the user's role
|
||||||
|
String role = authentication.getAuthorities().stream()
|
||||||
|
.map(GrantedAuthority::getAuthority)
|
||||||
|
.findFirst()
|
||||||
|
.orElse("ROLE_USER"); // Default role if no authority is found
|
||||||
|
|
||||||
|
// Redirect to the appropriate page based on the role
|
||||||
|
if (role.equals("ROLE_ADMIN")) {
|
||||||
|
// Update the quiz in the service
|
||||||
|
questionsService.editQuestion(quiz);
|
||||||
|
// Redirect to the quiz list page
|
||||||
|
return "redirect:/home";
|
||||||
|
} else {
|
||||||
|
// Redirect to the quiz page
|
||||||
|
return "redirect:/home";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/deleteQuiz/{id}")
|
||||||
|
public String deleteQuiz(@PathVariable("id") int id, Model model) {
|
||||||
|
// Get the authenticated user's details
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
// Get the user's role
|
||||||
|
String role = authentication.getAuthorities().stream()
|
||||||
|
.map(GrantedAuthority::getAuthority)
|
||||||
|
.findFirst()
|
||||||
|
.orElse("ROLE_USER"); // Default role if no authority is found
|
||||||
|
|
||||||
|
// Redirect to the appropriate page based on the role
|
||||||
|
if (role.equals("ROLE_ADMIN")) {
|
||||||
|
// Delete the quiz by ID
|
||||||
|
questionsService.deleteQuestion(id);
|
||||||
|
return "redirect:/home"; // Redirect to the quiz list page
|
||||||
|
} else {
|
||||||
|
return "redirect:/home"; // Redirect to the home page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@PostMapping("/submitQuiz")
|
||||||
|
public String evaluateQuiz(@RequestParam Map<String, String> allParams, Model model) {
|
||||||
|
int correctAnswers = 0;
|
||||||
|
List<String> userAnswers = new ArrayList<>();
|
||||||
|
List<Question> quizzes = questionsService.loadQuizzes();
|
||||||
|
|
||||||
|
// Iterate through the quizzes and compare answers
|
||||||
|
for (int i = 0; i < quizzes.size(); i++) {
|
||||||
|
String userAnswer = allParams.get("answer" + i); // Get the answer for question i
|
||||||
|
userAnswers.add(userAnswer); // Store user's answer
|
||||||
|
if (quizzes.get(i).getCorrectAnswer().equals(userAnswer)) {
|
||||||
|
correctAnswers++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add data to the model
|
||||||
|
model.addAttribute("quizzes", quizzes);
|
||||||
|
model.addAttribute("userAnswers", userAnswers);
|
||||||
|
model.addAttribute("correctAnswers", correctAnswers);
|
||||||
|
model.addAttribute("totalQuestions", quizzes.size());
|
||||||
|
|
||||||
|
// Return the result template
|
||||||
|
return "result";
|
||||||
|
}
|
||||||
|
}
|
||||||
75
OnlineQuiz/src/main/java/com/app/quiz/model/Question.java
Normal file
75
OnlineQuiz/src/main/java/com/app/quiz/model/Question.java
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package com.app.quiz.model;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class Question {
|
||||||
|
private Integer id;
|
||||||
|
private String questionText;
|
||||||
|
private ArrayList<String> options; // Keep options as ArrayList<String>
|
||||||
|
private String correctAnswer;
|
||||||
|
|
||||||
|
// No-argument constructor (required for Thymeleaf binding)
|
||||||
|
public Question() {
|
||||||
|
this.options = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor with parameters
|
||||||
|
public Question(Integer id, String questionText, ArrayList<String> options, String correctAnswer) {
|
||||||
|
this.id = id;
|
||||||
|
this.questionText = questionText;
|
||||||
|
this.options = options;
|
||||||
|
this.correctAnswer = correctAnswer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public Integer getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Integer id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getQuestionText() {
|
||||||
|
return questionText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setQuestionText(String questionText) {
|
||||||
|
this.questionText = questionText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArrayList<String> getOptions() {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOptions(ArrayList<String> options) {
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to get options as a comma-separated string
|
||||||
|
public String getOptionsAsString() {
|
||||||
|
return String.join(",", options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to set options from a comma-separated string
|
||||||
|
public void setOptionsFromString(String optionsString) {
|
||||||
|
this.options = new ArrayList<>(Arrays.asList(optionsString.split(",")));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCorrectAnswer() {
|
||||||
|
return correctAnswer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCorrectAnswer(String correctAnswer) {
|
||||||
|
this.correctAnswer = correctAnswer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return ("ID: " + id +
|
||||||
|
"\nQuestion: " + questionText +
|
||||||
|
"\nOptions: " + options +
|
||||||
|
"\nCorrect Answer: " + correctAnswer);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
OnlineQuiz/src/main/java/com/app/quiz/model/User.java
Normal file
56
OnlineQuiz/src/main/java/com/app/quiz/model/User.java
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package com.app.quiz.model;
|
||||||
|
|
||||||
|
public class User {
|
||||||
|
private String username;
|
||||||
|
private String email;
|
||||||
|
private String password;
|
||||||
|
private String role;
|
||||||
|
|
||||||
|
public User(String username, String email, String password, String role) {
|
||||||
|
this.username = username;
|
||||||
|
this.email = email;
|
||||||
|
this.password = password;
|
||||||
|
this.role = role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username) {
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmail() {
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEmail(String email) {
|
||||||
|
this.email = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassword(String password) {
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRole() {
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRole(String role) {
|
||||||
|
this.role = role;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "User{" +
|
||||||
|
"username='" + username + '\'' +
|
||||||
|
", email='" + email + '\'' +
|
||||||
|
", role='" + role + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.app.quiz.service;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.app.quiz.model.Question;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class QuestionsService {
|
||||||
|
|
||||||
|
private final Map<Integer, Question> questions = new HashMap<>();
|
||||||
|
private int nextId = 1;
|
||||||
|
|
||||||
|
public List<Question> loadQuizzes() {
|
||||||
|
return List.copyOf(questions.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Question getQuestionById(int id) {
|
||||||
|
return questions.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNextId() {
|
||||||
|
return nextId++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean addQuestion(Question Question) {
|
||||||
|
Integer QuestionId = Question.getId();
|
||||||
|
|
||||||
|
if (questions.containsKey(QuestionId)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
questions.put(QuestionId, Question);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean editQuestion(Question Question) {
|
||||||
|
Integer QuestionId = Question.getId();
|
||||||
|
|
||||||
|
if (questions.containsKey(QuestionId)) {
|
||||||
|
questions.put(QuestionId, Question);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean deleteQuestion(int id) {
|
||||||
|
Integer QuestionId = Integer.valueOf(id);
|
||||||
|
if (questions.containsKey(QuestionId)) {
|
||||||
|
questions.remove(QuestionId);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int submitQuestion(ArrayList<Question> list) {
|
||||||
|
int result = 0;
|
||||||
|
for (Question Question: list){
|
||||||
|
Question QuestionInList = questions.get(Question.getId());
|
||||||
|
if(QuestionInList.getCorrectAnswer().equals(Question.getCorrectAnswer())) {
|
||||||
|
result++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.app.quiz.service;
|
||||||
|
|
||||||
|
import com.app.quiz.model.User;
|
||||||
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class QuizUserDetailsService implements UserDetailsService {
|
||||||
|
|
||||||
|
private final List<User> users = new ArrayList<>();
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
public QuizUserDetailsService(PasswordEncoder passwordEncoder) {
|
||||||
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
|
Optional<User> user = users.stream().filter(u -> u.getUsername().equals(username)).findFirst();
|
||||||
|
if (user.isEmpty()) {
|
||||||
|
throw new UsernameNotFoundException("User %s not found".formatted(username));
|
||||||
|
}
|
||||||
|
return org.springframework.security.core.userdetails.User
|
||||||
|
.withUsername(user.get().getUsername())
|
||||||
|
.password(user.get().getPassword())
|
||||||
|
.roles(user.get().getRole())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerUser(String username, String password, String email, String role) {
|
||||||
|
if (users.stream().anyMatch(u -> u.getUsername().equals(username))) {
|
||||||
|
throw new BadCredentialsException("Username already exists");
|
||||||
|
}
|
||||||
|
users.add(new User(username, email, passwordEncoder.encode(password), role));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
9
OnlineQuiz/src/main/resources/application.properties
Normal file
9
OnlineQuiz/src/main/resources/application.properties
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
spring.application.name=Online Quiz
|
||||||
|
|
||||||
|
# Server port
|
||||||
|
server.port=8080
|
||||||
|
|
||||||
|
# Thymeleaf configuration
|
||||||
|
spring.thymeleaf.cache=false
|
||||||
|
spring.thymeleaf.prefix=classpath:/templates/
|
||||||
|
spring.thymeleaf.suffix=.html
|
||||||
82
OnlineQuiz/src/main/resources/templates/Quiz.html
Normal file
82
OnlineQuiz/src/main/resources/templates/Quiz.html
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Quiz</title>
|
||||||
|
<style>
|
||||||
|
.quiz-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
/* Header styles */
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.options {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
.submit-button {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.submit-button:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logout button styles */
|
||||||
|
.logout-button {
|
||||||
|
background-color: #2196F3; /* Blue background */
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-button:hover {
|
||||||
|
background-color: #1976D2; /* Darker blue on hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header section with Logout Button -->
|
||||||
|
<div class="header">
|
||||||
|
<h1>Quiz Questions</h1>
|
||||||
|
<form th:action="@{/logout}" method="post">
|
||||||
|
<button type="submit" class="logout-button">Logout</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<form th:action="@{/submitQuiz}" method="post">
|
||||||
|
<div th:each="quiz, iterStat : ${quizzes}" class="quiz-container">
|
||||||
|
<div class="question" th:text="${'Question ' + (iterStat.index + 1) + ': ' + quiz.questionText}"></div>
|
||||||
|
<div class="options">
|
||||||
|
<ul>
|
||||||
|
<li th:each="option, optStat : ${quiz.options}">
|
||||||
|
<input type="radio" th:name="${'answer' + iterStat.index}" th:value="${option}" th:id="${'option' + iterStat.index + '-' + optStat.index}" required>
|
||||||
|
<label th:for="${'option' + iterStat.index + '-' + optStat.index}" th:text="${option}"></label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="submit-button">Submit Answers</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
124
OnlineQuiz/src/main/resources/templates/QuizList.html
Normal file
124
OnlineQuiz/src/main/resources/templates/QuizList.html
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Quiz List</title>
|
||||||
|
<style>
|
||||||
|
/* General styles */
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header styles */
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Quiz container styles */
|
||||||
|
.quiz-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.correct-answer {
|
||||||
|
color: green;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button styles */
|
||||||
|
.add-quiz-button, .edit-quiz-button, .delete-quiz-button {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-quiz-button {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-quiz-button {
|
||||||
|
background-color: #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-quiz-button {
|
||||||
|
background-color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-quiz-button:hover, .edit-quiz-button:hover, .delete-quiz-button:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logout button styles */
|
||||||
|
.logout-button {
|
||||||
|
background-color: #2196F3; /* Blue background */
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-button:hover {
|
||||||
|
background-color: #1976D2; /* Darker blue on hover */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header section with Logout Button -->
|
||||||
|
<div class="header">
|
||||||
|
<h1>Quiz Questions</h1>
|
||||||
|
<form th:action="@{/logout}" method="post">
|
||||||
|
<button type="submit" class="logout-button">Logout</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Quiz Button -->
|
||||||
|
<div style="padding: 20px;">
|
||||||
|
<a th:href="@{/addQuiz}">
|
||||||
|
<button class="add-quiz-button">Add Quiz</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quiz List -->
|
||||||
|
<div style="padding: 20px;">
|
||||||
|
<div th:each="quiz : ${quizzes}" class="quiz-container">
|
||||||
|
<div class="question" th:text="${'Question: ' + quiz.questionText}"></div>
|
||||||
|
<div class="options">
|
||||||
|
<ul>
|
||||||
|
<li th:each="option : ${quiz.options}" th:text="${option}"></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="correct-answer" th:text="${'Correct Answer: ' + quiz.correctAnswer}"></div>
|
||||||
|
<!-- Edit Button -->
|
||||||
|
<a th:href="@{/editQuiz/{id}(id=${quiz.id})}">
|
||||||
|
<button class="edit-quiz-button">Edit Quiz</button>
|
||||||
|
</a>
|
||||||
|
<!-- Delete Button -->
|
||||||
|
<a th:href="@{/deleteQuiz/{id}(id=${quiz.id})}">
|
||||||
|
<button class="delete-quiz-button">Delete Quiz</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
123
OnlineQuiz/src/main/resources/templates/addQuiz.html
Normal file
123
OnlineQuiz/src/main/resources/templates/addQuiz.html
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Add Quiz</title>
|
||||||
|
<style>
|
||||||
|
/* General styles */
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form container */
|
||||||
|
.form-container {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
|
||||||
|
max-width: 500px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form header */
|
||||||
|
.form-header {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form group (label + input) */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
border-color: #2196F3;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 5px rgba(33, 150, 243, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error messages */
|
||||||
|
.error-message {
|
||||||
|
color: #f44336;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Submit button */
|
||||||
|
.submit-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #2196F3;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:hover {
|
||||||
|
background-color: #1976D2;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="form-container">
|
||||||
|
<!-- Form header -->
|
||||||
|
<div class="form-header">Add a New Quiz</div>
|
||||||
|
|
||||||
|
<!-- Form -->
|
||||||
|
<form th:action="@{/addQuiz}" method="post" th:object="${quiz}">
|
||||||
|
<!-- Question -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="questionText">Question:</label>
|
||||||
|
<input type="text" id="questionText" th:field="*{questionText}" required>
|
||||||
|
<span th:if="${#fields.hasErrors('questionText')}" th:errors="*{questionText}" class="error-message"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Options -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="options">Options (comma-separated):</label>
|
||||||
|
<input type="text" id="options" th:field="*{options}" required>
|
||||||
|
<span th:if="${#fields.hasErrors('options')}" th:errors="*{options}" class="error-message"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Correct Answer -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="correctAnswer">Correct Answer:</label>
|
||||||
|
<input type="text" id="correctAnswer" th:field="*{correctAnswer}" required>
|
||||||
|
<span th:if="${#fields.hasErrors('correctAnswer')}" th:errors="*{correctAnswer}" class="error-message"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit button -->
|
||||||
|
<button type="submit" class="submit-button">Add Quiz</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
125
OnlineQuiz/src/main/resources/templates/editQuiz.html
Normal file
125
OnlineQuiz/src/main/resources/templates/editQuiz.html
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Edit Quiz</title>
|
||||||
|
<style>
|
||||||
|
/* General styles */
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form container */
|
||||||
|
.form-container {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
|
||||||
|
max-width: 500px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form header */
|
||||||
|
.form-header {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form group (label + input) */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
border-color: #2196F3;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 5px rgba(33, 150, 243, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error messages */
|
||||||
|
.error-message {
|
||||||
|
color: #f44336;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Submit button */
|
||||||
|
.submit-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #2196F3;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:hover {
|
||||||
|
background-color: #1976D2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="form-container">
|
||||||
|
<!-- Form header -->
|
||||||
|
<div class="form-header">Edit Quiz</div>
|
||||||
|
|
||||||
|
<!-- Form -->
|
||||||
|
<form th:action="@{/editQuestion}" method="post" th:object="${quiz}">
|
||||||
|
<!-- Hidden field for quiz ID -->
|
||||||
|
<input type="hidden" th:field="*{id}" />
|
||||||
|
|
||||||
|
<!-- Question -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="questionText">Question:</label>
|
||||||
|
<input type="text" id="questionText" th:field="*{questionText}" required>
|
||||||
|
<span th:if="${#fields.hasErrors('questionText')}" th:errors="*{questionText}" class="error-message"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Options -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="options">Options (comma-separated):</label>
|
||||||
|
<input type="text" id="options" th:value="${quiz.getOptionsAsString()}" name="options" required>
|
||||||
|
<span th:if="${#fields.hasErrors('options')}" th:errors="*{options}" class="error-message"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Correct Answer -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="correctAnswer">Correct Answer:</label>
|
||||||
|
<input type="text" id="correctAnswer" th:field="*{correctAnswer}" required>
|
||||||
|
<span th:if="${#fields.hasErrors('correctAnswer')}" th:errors="*{correctAnswer}" class="error-message"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit button -->
|
||||||
|
<button type="submit" class="submit-button">Update Quiz</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
111
OnlineQuiz/src/main/resources/templates/login.html
Normal file
111
OnlineQuiz/src/main/resources/templates/login.html
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login Page</title>
|
||||||
|
<style>
|
||||||
|
/* Center the body content */
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the login container */
|
||||||
|
.login-container {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 300px;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center; /* Center items horizontally */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the form elements */
|
||||||
|
.login-container h2 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
max-width: 250px; /* Limit the width of inputs */
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #2d2db0;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
max-width: 250px; /* Limit the width of the button */
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container button:hover {
|
||||||
|
background-color: #2d2db0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error and Success Messages */
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
color: green;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Link to Registration Page */
|
||||||
|
.login-container p {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container a {
|
||||||
|
color: #2d2db0;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<h2>Login</h2>
|
||||||
|
<!-- Login Form -->
|
||||||
|
<form th:action="@{/login}" method="post" class="form-group">
|
||||||
|
<!-- Username -->
|
||||||
|
<input type="text" name="username" placeholder="Username" required>
|
||||||
|
<!-- Password -->
|
||||||
|
<input type="password" name="password" placeholder="Password" required>
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
<!-- Error Message -->
|
||||||
|
<div th:if="${param.error}" class="error">
|
||||||
|
Invalid username or password.
|
||||||
|
</div>
|
||||||
|
<!-- Success Message -->
|
||||||
|
<div th:if="${param.success}" class="success">
|
||||||
|
User registered successfully!
|
||||||
|
</div>
|
||||||
|
<!-- Link to Registration Page -->
|
||||||
|
<p>Don't have an account? <a href="/register">Register here</a></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
97
OnlineQuiz/src/main/resources/templates/register.html
Normal file
97
OnlineQuiz/src/main/resources/templates/register.html
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Registration Page</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.registration-container {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 300px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.registration-container h2 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.registration-container input, .registration-container select {
|
||||||
|
width: 80%;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registration-container select {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 10px center;
|
||||||
|
background-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registration-container button {
|
||||||
|
width: 80%;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #2d2db0;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.registration-container button:hover {
|
||||||
|
background-color: #2d2db0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="registration-container">
|
||||||
|
<h2>Register</h2>
|
||||||
|
<form th:action="@{/register}" method="post" class="form-group">
|
||||||
|
<!-- Username -->
|
||||||
|
<input type="text" name="username" placeholder="Username" required>
|
||||||
|
|
||||||
|
<!-- Password -->
|
||||||
|
<input type="password" name="password" placeholder="Password" required>
|
||||||
|
|
||||||
|
<!-- Email -->
|
||||||
|
<input type="email" name="email" placeholder="Email" required>
|
||||||
|
|
||||||
|
<!-- Role Dropdown -->
|
||||||
|
<select name="role" required>
|
||||||
|
<option value="" disabled selected>Select your role</option>
|
||||||
|
<option value="ADMIN">Admin</option>
|
||||||
|
<option value="USER">User</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<button type="submit">Register</button>
|
||||||
|
</form>
|
||||||
|
<!-- Error Message -->
|
||||||
|
<div th:if="${param.error}" class="error">
|
||||||
|
Invalid username.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Already have an account? <a href="/login">Login here</a></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
131
OnlineQuiz/src/main/resources/templates/result.html
Normal file
131
OnlineQuiz/src/main/resources/templates/result.html
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Quiz Result</title>
|
||||||
|
<style>
|
||||||
|
/* General styles */
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header styles */
|
||||||
|
.header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 10px 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px; /* Space between buttons */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button styles */
|
||||||
|
.nav-button {
|
||||||
|
background-color: #2196F3; /* Blue background */
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none; /* Remove underline for links */
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button:hover {
|
||||||
|
background-color: #1976D2; /* Darker blue on hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Result container styles */
|
||||||
|
.result-container {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
max-width: 600px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
margin: 80px auto 20px; /* Add margin to avoid overlap with header */
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-header {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4CAF50;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-details {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-details h3 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-details ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-details li {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.correct {
|
||||||
|
color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.incorrect {
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header with Home and Logout Buttons -->
|
||||||
|
<div class="header">
|
||||||
|
<a th:href="@{/home}">
|
||||||
|
<button class="nav-button">Home</button>
|
||||||
|
</a>
|
||||||
|
<form th:action="@{/logout}" method="post">
|
||||||
|
<button type="submit" class="nav-button">Logout</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Result Container -->
|
||||||
|
<div class="result-container">
|
||||||
|
<!-- Result Header -->
|
||||||
|
<div class="result-header" th:text="${'You scored ' + correctAnswers + ' out of ' + totalQuestions + '!'}"></div>
|
||||||
|
|
||||||
|
<!-- Result Details -->
|
||||||
|
<div class="result-details">
|
||||||
|
<h3>Details:</h3>
|
||||||
|
<ul>
|
||||||
|
<!-- Iterate through quizzes and display correct/incorrect answers -->
|
||||||
|
<li th:each="quiz, iterStat : ${quizzes}">
|
||||||
|
<div>
|
||||||
|
<strong th:text="${'Question ' + (iterStat.index + 1) + ': ' + quiz.questionText}"></strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Your Answer: </span>
|
||||||
|
<span th:class="${quiz.correctAnswer == userAnswers[iterStat.index] ? 'correct' : 'incorrect'}"
|
||||||
|
th:text="${userAnswers[iterStat.index]}"></span>
|
||||||
|
</div>
|
||||||
|
<div th:if="${quiz.correctAnswer != userAnswers[iterStat.index]}">
|
||||||
|
<span>Correct Answer: </span>
|
||||||
|
<span class="correct" th:text="${quiz.correctAnswer}"></span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
plugins {
|
||||||
|
id 'org.springframework.boot' version '3.5.7' apply false
|
||||||
|
id 'io.spring.dependency-management' version '1.1.7' apply false
|
||||||
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = 'me.ahlroos'
|
group = 'me.ahlroos'
|
||||||
version = '1.0-SNAPSHOT'
|
version = '1.0-SNAPSHOT'
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
4
gradlew
vendored
4
gradlew
vendored
@@ -45,7 +45,7 @@
|
|||||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
#
|
#
|
||||||
# The "traditional" practice of packing multiple parameters into a
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
# space-separated string is a well documented source of bugs and security
|
# space-separated string is a well documented source of bugs and config
|
||||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
# options in "$@", and eventually passing that to Java.
|
# options in "$@", and eventually passing that to Java.
|
||||||
#
|
#
|
||||||
@@ -248,4 +248,4 @@ eval "set -- $(
|
|||||||
tr '\n' ' '
|
tr '\n' ' '
|
||||||
)" '"$@"'
|
)" '"$@"'
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
exec "$JAVACMD" "$@"
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
rootProject.name = 'java-developer-course'
|
rootProject.name = 'java-developer-course'
|
||||||
include 'PetCareScheduler'
|
include 'PetCareScheduler'
|
||||||
include 'Portfolio'
|
include 'Portfolio'
|
||||||
|
include 'OnlineQuiz'
|
||||||
Reference in New Issue
Block a user