ЛАБ 12

Вэб Програмчлал

3.1 Лабораторийн зорилго

Энэ лабораторид та:

  1. Spring Boot Web + Thymeleaf төсөл үүсгэх
  2. Full CRUD (Оюутан) бүтээх
  3. Form Validation нэмэх
  4. Session-аар Login/Logout хийх
  5. Filter/Interceptor-аар Auth шалгах

Хэл: Java 17+ / Spring Boot 3.x | Template: Thymeleaf | DB: H2 (in-memory)


3.2 Лаб 1: Төслийн бүтэц үүсгэх

Алхам 1: Dependencies (pom.xml)

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

Алхам 2: application.yml

server:
  port: 8080
  servlet:
    session:
      timeout: 30m

spring:
  datasource:
    url: jdbc:h2:mem:studentdb
    driver-class-name: org.h2.Driver
    username: sa
    password:
  h2:
    console:
      enabled: true      # http://localhost:8080/h2-console
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true
  thymeleaf:
    cache: false          # Хөгжүүлэлтэд cache унтраах

Алхам 3: Entity

@Entity
@Table(name = "students")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 50)
    private String name;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private Double gpa;

    // Constructors
    public Student() {}

    public Student(String name, String email, Double gpa) {
        this.name = name;
        this.email = email;
        this.gpa = gpa;
    }

    // Getters & Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public Double getGpa() { return gpa; }
    public void setGpa(Double gpa) { this.gpa = gpa; }
}

Алхам 4: Repository

@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
    boolean existsByEmail(String email);
    List<Student> findByNameContainingIgnoreCase(String name);
}

3.3 Лаб 2: Service + Form + Controller (Full CRUD)

Алхам 1: StudentForm (Validation)

public class StudentForm {

    private Long id;

    @NotBlank(message = "Нэр хоосон байж болохгүй")
    @Size(min = 2, max = 50, message = "Нэр 2-50 тэмдэгт байх ёстой")
    private String name;

    @NotBlank(message = "Email хоосон байж болохгүй")
    @Email(message = "Email формат буруу байна")
    private String email;

    @NotNull(message = "GPA оруулна уу")
    @DecimalMin(value = "0.0", message = "GPA 0.0-аас багагүй")
    @DecimalMax(value = "4.0", message = "GPA 4.0-аас ихгүй")
    private Double gpa;

    // Getters & Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public Double getGpa() { return gpa; }
    public void setGpa(Double gpa) { this.gpa = gpa; }
}

Алхам 2: StudentService

@Service
public class StudentService {

    private final StudentRepository repository;

    public StudentService(StudentRepository repository) {
        this.repository = repository;
    }

    public List<Student> findAll() {
        return repository.findAll();
    }

    public Student findById(Long id) {
        return repository.findById(id)
            .orElseThrow(() -> new StudentNotFoundException("Оюутан олдсонгүй: ID=" + id));
    }

    public Student create(StudentForm form) {
        Student student = new Student(form.getName(), form.getEmail(), form.getGpa());
        return repository.save(student);
    }

    public Student update(Long id, StudentForm form) {
        Student student = findById(id);
        student.setName(form.getName());
        student.setEmail(form.getEmail());
        student.setGpa(form.getGpa());
        return repository.save(student);
    }

    public void delete(Long id) {
        Student student = findById(id);
        repository.delete(student);
    }

    public StudentForm toForm(Student student) {
        StudentForm form = new StudentForm();
        form.setId(student.getId());
        form.setName(student.getName());
        form.setEmail(student.getEmail());
        form.setGpa(student.getGpa());
        return form;
    }

    public List<Student> search(String keyword) {
        return repository.findByNameContainingIgnoreCase(keyword);
    }
}

Алхам 3: StudentController (Full CRUD)

@Controller
@RequestMapping("/students")
public class StudentController {

    private final StudentService studentService;

    public StudentController(StudentService studentService) {
        this.studentService = studentService;
    }

    // LIST — Жагсаалт
    @GetMapping
    public String list(@RequestParam(required = false) String search, Model model) {
        List<Student> students;
        if (search != null && !search.isBlank()) {
            students = studentService.search(search);
            model.addAttribute("search", search);
        } else {
            students = studentService.findAll();
        }
        model.addAttribute("students", students);
        return "students/list";
    }

    // NEW — Form харуулах
    @GetMapping("/new")
    public String showCreateForm(Model model) {
        model.addAttribute("student", new StudentForm());
        model.addAttribute("editMode", false);
        return "students/form";
    }

    // CREATE — Хадгалах
    @PostMapping
    public String create(@Valid @ModelAttribute("student") StudentForm form,
                         BindingResult result, Model model,
                         RedirectAttributes redirectAttributes) {
        if (result.hasErrors()) {
            model.addAttribute("editMode", false);
            return "students/form";
        }
        studentService.create(form);
        redirectAttributes.addFlashAttribute("message", "Оюутан амжилттай нэмэгдлээ!");
        return "redirect:/students";
    }

    // EDIT — Form + одоогийн утга
    @GetMapping("/edit/{id}")
    public String showEditForm(@PathVariable Long id, Model model) {
        Student student = studentService.findById(id);
        model.addAttribute("student", studentService.toForm(student));
        model.addAttribute("editMode", true);
        return "students/form";
    }

    // UPDATE — Шинэчлэх
    @PostMapping("/edit/{id}")
    public String update(@PathVariable Long id,
                         @Valid @ModelAttribute("student") StudentForm form,
                         BindingResult result, Model model,
                         RedirectAttributes redirectAttributes) {
        if (result.hasErrors()) {
            model.addAttribute("editMode", true);
            return "students/form";
        }
        studentService.update(id, form);
        redirectAttributes.addFlashAttribute("message", "Оюутан амжилттай шинэчлэгдлээ!");
        return "redirect:/students";
    }

    // DELETE — Устгах
    @GetMapping("/delete/{id}")
    public String delete(@PathVariable Long id, RedirectAttributes redirectAttributes) {
        studentService.delete(id);
        redirectAttributes.addFlashAttribute("message", "Оюутан амжилттай устгагдлаа!");
        return "redirect:/students";
    }
}

3.4 Лаб 3: Thymeleaf Templates

templates/students/list.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Оюутнууд</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        th { background-color: #4CAF50; color: white; }
        tr:hover { background-color: #f5f5f5; }
        .alert { padding: 10px; margin: 10px 0; border-radius: 4px; }
        .alert-success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
        .btn { padding: 6px 12px; text-decoration: none; border-radius: 4px; }
        .btn-primary { background-color: #007bff; color: white; }
        .btn-warning { background-color: #ffc107; color: black; }
        .btn-danger { background-color: #dc3545; color: white; }
        .search-box { margin: 10px 0; }
    </style>
</head>
<body>
    <h1>Оюутнуудын жагсаалт</h1>

    <!-- Амжилтын мэдэгдэл -->
    <div th:if="${message}" class="alert alert-success" th:text="${message}"></div>

    <!-- Хайлт -->
    <div class="search-box">
        <form th:action="@{/students}" method="GET">
            <input type="text" name="search" placeholder="Нэрээр хайх..."
                   th:value="${search}">
            <button type="submit" class="btn btn-primary">Хайх</button>
            <a th:href="@{/students}" class="btn">Цэвэрлэх</a>
        </form>
    </div>

    <a th:href="@{/students/new}" class="btn btn-primary">+ Шинэ оюутан</a>

    <table th:if="${!#lists.isEmpty(students)}" style="margin-top: 10px;">
        <thead>
            <tr>
                <th>ID</th>
                <th>Нэр</th>
                <th>Email</th>
                <th>GPA</th>
                <th>Үйлдэл</th>
            </tr>
        </thead>
        <tbody>
            <tr th:each="student : ${students}">
                <td th:text="${student.id}"></td>
                <td th:text="${student.name}"></td>
                <td th:text="${student.email}"></td>
                <td th:text="${student.gpa}"></td>
                <td>
                    <a th:href="@{/students/edit/{id}(id=${student.id})}" class="btn btn-warning">Засах</a>
                    <a th:href="@{/students/delete/{id}(id=${student.id})}" class="btn btn-danger"
                       onclick="return confirm('Устгахдаа итгэлтэй байна уу?')">Устгах</a>
                </td>
            </tr>
        </tbody>
    </table>

    <p th:if="${#lists.isEmpty(students)}">Оюутан бүртгэгдээгүй байна.</p>

    <p style="margin-top: 20px;">Нийт: <strong th:text="${students.size()}">0</strong> оюутан</p>
</body>
</html>

templates/students/form.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="${editMode} ? 'Оюутан засах' : 'Шинэ оюутан'">Form</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .form-group { margin: 10px 0; }
        label { display: block; font-weight: bold; margin-bottom: 4px; }
        input { padding: 8px; width: 300px; border: 1px solid #ccc; border-radius: 4px; }
        input.is-invalid { border-color: #dc3545; }
        .error { color: #dc3545; font-size: 0.85em; }
        .btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; }
        .btn-primary { background-color: #007bff; color: white; }
        .btn-secondary { background-color: #6c757d; color: white; text-decoration: none; padding: 8px 16px; border-radius: 4px; }
    </style>
</head>
<body>
    <h1 th:text="${editMode} ? 'Оюутан засах' : 'Шинэ оюутан нэмэх'">Form</h1>

    <form th:action="${editMode} ? @{/students/edit/{id}(id=${student.id})} : @{/students}"
          th:object="${student}" method="POST">

        <div class="form-group">
            <label for="name">Нэр:</label>
            <input type="text" id="name" th:field="*{name}"
                   th:classappend="${#fields.hasErrors('name')} ? 'is-invalid'">
            <div th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="error"></div>
        </div>

        <div class="form-group">
            <label for="email">Email:</label>
            <input type="email" id="email" th:field="*{email}"
                   th:classappend="${#fields.hasErrors('email')} ? 'is-invalid'">
            <div th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="error"></div>
        </div>

        <div class="form-group">
            <label for="gpa">GPA:</label>
            <input type="number" step="0.1" id="gpa" th:field="*{gpa}"
                   th:classappend="${#fields.hasErrors('gpa')} ? 'is-invalid'">
            <div th:if="${#fields.hasErrors('gpa')}" th:errors="*{gpa}" class="error"></div>
        </div>

        <div class="form-group" style="margin-top: 15px;">
            <button type="submit" class="btn btn-primary"
                    th:text="${editMode} ? 'Шинэчлэх' : 'Нэмэх'">Submit</button>
            <a th:href="@{/students}" class="btn-secondary">Буцах</a>
        </div>
    </form>
</body>
</html>

3.5 Лаб 4: Session Login/Logout

User Entity + Repository

@Entity
@Table(name = "users")
public class AppUser {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String role;  // ADMIN, USER

    // Constructors, Getters, Setters
    public AppUser() {}
    public AppUser(String email, String password, String name, String role) {
        this.email = email;
        this.password = password;
        this.name = name;
        this.role = role;
    }
    public Long getId() { return id; }
    public String getEmail() { return email; }
    public String getPassword() { return password; }
    public String getName() { return name; }
    public String getRole() { return role; }
}

@Repository
public interface UserRepository extends JpaRepository<AppUser, Long> {
    Optional<AppUser> findByEmail(String email);
}

AuthController

@Controller
public class AuthController {

    private final UserRepository userRepository;

    public AuthController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @GetMapping("/login")
    public String loginPage() {
        return "auth/login";
    }

    @PostMapping("/login")
    public String login(@RequestParam String email,
                        @RequestParam String password,
                        HttpSession session,
                        RedirectAttributes redirectAttributes) {
        Optional<AppUser> userOpt = userRepository.findByEmail(email);

        if (userOpt.isPresent() && userOpt.get().getPassword().equals(password)) {
            AppUser user = userOpt.get();
            session.setAttribute("currentUser", user);
            return "redirect:/students";
        }

        redirectAttributes.addFlashAttribute("error", "Email эсвэл нууц үг буруу!");
        return "redirect:/login";
    }

    @GetMapping("/logout")
    public String logout(HttpSession session) {
        session.invalidate();
        return "redirect:/login";
    }
}

templates/auth/login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Нэвтрэх</title>
    <style>
        body { font-family: Arial; display: flex; justify-content: center; margin-top: 100px; }
        .login-box { width: 350px; padding: 30px; border: 1px solid #ddd; border-radius: 8px; }
        input { width: 100%; padding: 10px; margin: 8px 0; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; }
        button { width: 100%; padding: 10px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
        .error { color: #dc3545; margin-bottom: 10px; }
    </style>
</head>
<body>
    <div class="login-box">
        <h2>Нэвтрэх</h2>
        <div th:if="${error}" class="error" th:text="${error}"></div>
        <form th:action="@{/login}" method="POST">
            <input type="email" name="email" placeholder="Email" required>
            <input type="password" name="password" placeholder="Нууц үг" required>
            <button type="submit">Нэвтрэх</button>
        </form>
    </div>
</body>
</html>

3.6 Лаб 5: Auth Interceptor + Exception Handler

AuthInterceptor

@Component
public class AuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        HttpSession session = request.getSession(false);
        if (session == null || session.getAttribute("currentUser") == null) {
            response.sendRedirect("/login");
            return false;
        }
        return true;
    }
}

WebConfig — Interceptor бүртгэх

@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final AuthInterceptor authInterceptor;

    public WebConfig(AuthInterceptor authInterceptor) {
        this.authInterceptor = authInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/login", "/logout", "/css/**", "/js/**", "/h2-console/**");
    }
}

StudentNotFoundException + Handler

public class StudentNotFoundException extends RuntimeException {
    public StudentNotFoundException(String message) {
        super(message);
    }
}

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(StudentNotFoundException.class)
    public String handleNotFound(StudentNotFoundException ex, Model model) {
        model.addAttribute("error", ex.getMessage());
        return "error/404";
    }

    @ExceptionHandler(Exception.class)
    public String handleGeneral(Exception ex, Model model) {
        model.addAttribute("error", "Серверийн алдаа: " + ex.getMessage());
        return "error/500";
    }
}

DataInitializer — Анхны өгөгдөл

@Component
public class DataInitializer implements CommandLineRunner {

    private final UserRepository userRepository;
    private final StudentRepository studentRepository;

    public DataInitializer(UserRepository userRepository, StudentRepository studentRepository) {
        this.userRepository = userRepository;
        this.studentRepository = studentRepository;
    }

    @Override
    public void run(String... args) {
        // Хэрэглэгч
        userRepository.save(new AppUser("admin@test.com", "admin123", "Админ", "ADMIN"));
        userRepository.save(new AppUser("user@test.com", "user123", "Хэрэглэгч", "USER"));

        // Оюутнууд
        studentRepository.save(new Student("Бат", "bat@test.com", 3.5));
        studentRepository.save(new Student("Сараа", "saraa@test.com", 3.8));
        studentRepository.save(new Student("Дорж", "dorj@test.com", 2.9));
    }
}

Тест хийх:

# Апп ажиллуулах
./mvnw spring-boot:run

# Browser-д нээх
# http://localhost:8080/login
# Email: admin@test.com, Password: admin123

# Оюутнуудын жагсаалт
# http://localhost:8080/students

# CRUD тест:
# + Шинэ оюутан → Нэмэх → Жагсаалтад харагдана
# Засах → Утга өөрчлөх → Шинэчлэгдэнэ
# Устгах → Confirm → Устгагдана
# Хайх → Нэрээр хайна

# H2 Console
# http://localhost:8080/h2-console
# JDBC URL: jdbc:h2:mem:studentdb