From 8336c5a9b2d8086a6caea5fac9716be48ff634dc Mon Sep 17 00:00:00 2001
From: Vladislav Kopnin
Date: Fri, 11 Sep 2026 21:55:08 +0300
Subject: [PATCH] Moscow-time date display, CSRF error UX, session cookie
hardening
- DateUtils: format ISO instants as dd.MM.yyyy HH:mm in Europe/Moscow
- admin/lab page and student dashboard show deadlines in MSK time
- CSRF failure redirects to /login with friendly message instead of 403
- session tracking-modes=cookie (no ;jsessionid in URLs) (previously set)
- web rebuild with all dialog/status-toggle fixes verified end-to-end
---
.../main/kotlin/ru/ysu/lms/web/DateUtils.kt | 21 +++++++++++++++++++
.../ru/ysu/lms/web/admin/AdminController.kt | 2 ++
.../main/kotlin/ru/ysu/lms/web/api/ApiDtos.kt | 2 ++
.../ru/ysu/lms/web/auth/AuthPageController.kt | 4 ++++
.../ru/ysu/lms/web/config/CsrfInterceptor.kt | 15 ++++++++++++-
.../ysu/lms/web/student/StudentController.kt | 6 +++++-
web/src/main/resources/application.yml | 1 +
.../main/resources/templates/admin/lab.html | 4 ++--
.../templates/student/dashboard.html | 4 ++--
9 files changed, 53 insertions(+), 6 deletions(-)
create mode 100644 web/src/main/kotlin/ru/ysu/lms/web/DateUtils.kt
diff --git a/web/src/main/kotlin/ru/ysu/lms/web/DateUtils.kt b/web/src/main/kotlin/ru/ysu/lms/web/DateUtils.kt
new file mode 100644
index 0000000..b3a2bb1
--- /dev/null
+++ b/web/src/main/kotlin/ru/ysu/lms/web/DateUtils.kt
@@ -0,0 +1,21 @@
+package ru.ysu.lms.web
+
+import java.time.Instant
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+
+object DateUtils {
+ private val MSK = ZoneId.of("Europe/Moscow")
+ private val FORMAT = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
+
+ fun formatMsk(value: String?): String? =
+ value?.let {
+ try {
+ val zdt = ZonedDateTime.ofInstant(Instant.parse(it), MSK)
+ zdt.format(FORMAT)
+ } catch (_: Exception) {
+ it
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminController.kt b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminController.kt
index ffaf518..f72ffd0 100644
--- a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminController.kt
+++ b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminController.kt
@@ -283,6 +283,8 @@ class AdminController(
model.addAttribute("lab", lab)
model.addAttribute("groups", groupSections)
model.addAttribute("descriptionHtml", markdown.render(lab.descriptionMd))
+ model.addAttribute("deadlineMsk", ru.ysu.lms.web.DateUtils.formatMsk(lab.deadlineAt))
+ model.addAttribute("defenseMsk", ru.ysu.lms.web.DateUtils.formatMsk(lab.defenseAt))
model.addAttribute("deadlineLocal", lab.deadlineAt?.take(16) ?: "")
model.addAttribute("defenseLocal", lab.defenseAt?.take(16) ?: "")
return "admin/lab"
diff --git a/web/src/main/kotlin/ru/ysu/lms/web/api/ApiDtos.kt b/web/src/main/kotlin/ru/ysu/lms/web/api/ApiDtos.kt
index bb6bc39..11698ee 100644
--- a/web/src/main/kotlin/ru/ysu/lms/web/api/ApiDtos.kt
+++ b/web/src/main/kotlin/ru/ysu/lms/web/api/ApiDtos.kt
@@ -153,6 +153,8 @@ data class MeAssignmentDto(
val pointsMax: Any? = null,
) {
var statementHtml: String = ""
+ var deadlineMsk: String? = null
+ var defenseMsk: String? = null
}
@JsonIgnoreProperties(ignoreUnknown = true)
diff --git a/web/src/main/kotlin/ru/ysu/lms/web/auth/AuthPageController.kt b/web/src/main/kotlin/ru/ysu/lms/web/auth/AuthPageController.kt
index 460ff1f..e9294eb 100644
--- a/web/src/main/kotlin/ru/ysu/lms/web/auth/AuthPageController.kt
+++ b/web/src/main/kotlin/ru/ysu/lms/web/auth/AuthPageController.kt
@@ -27,6 +27,10 @@ class AuthPageController(private val api: ApiClient) {
if (currentUser(session) != null) {
return "redirect:/"
}
+ (session.getAttribute("csrfError") as? String)?.let {
+ session.removeAttribute("csrfError")
+ model.addAttribute("error", it)
+ }
model.addAttribute("adminExists", api.adminExists())
return "login"
}
diff --git a/web/src/main/kotlin/ru/ysu/lms/web/config/CsrfInterceptor.kt b/web/src/main/kotlin/ru/ysu/lms/web/config/CsrfInterceptor.kt
index 8d8e504..323855a 100644
--- a/web/src/main/kotlin/ru/ysu/lms/web/config/CsrfInterceptor.kt
+++ b/web/src/main/kotlin/ru/ysu/lms/web/config/CsrfInterceptor.kt
@@ -29,13 +29,26 @@ class CsrfInterceptor : HandlerInterceptor {
if (request.method.equals("POST", ignoreCase = true)) {
val sent = request.getParameter(FIELD)
if (sent != token) {
- response.sendError(HttpServletResponse.SC_FORBIDDEN, "CSRF token mismatch")
+ request.getSession().setAttribute("csrfError", "CSRF-токен устарел. Попробуйте ещё раз.")
+ response.sendRedirect("/login")
return false
}
}
return true
}
+ override fun afterCompletion(
+ request: HttpServletRequest,
+ response: HttpServletResponse,
+ handler: Any,
+ ex: Exception?,
+ ) {
+ // после успешного POST-запроса чистим флаг ошибки (если был)
+ if (request.method.equals("POST", ignoreCase = true)) {
+ request.getSession().removeAttribute("csrfError")
+ }
+ }
+
companion object {
const val ATTR = "csrfToken"
const val FIELD = "_csrf"
diff --git a/web/src/main/kotlin/ru/ysu/lms/web/student/StudentController.kt b/web/src/main/kotlin/ru/ysu/lms/web/student/StudentController.kt
index 862eacf..4cc6cb1 100644
--- a/web/src/main/kotlin/ru/ysu/lms/web/student/StudentController.kt
+++ b/web/src/main/kotlin/ru/ysu/lms/web/student/StudentController.kt
@@ -21,7 +21,11 @@ class StudentController(
fun dashboard(session: HttpSession, model: Model): String {
val token = session.getAttribute(SessionKeys.TOKEN) as? String ?: return "redirect:/login"
val assignments = api.meAssignments(token).map {
- it.apply { statementHtml = markdown.render(statementMd) }
+ it.apply {
+ statementHtml = markdown.render(statementMd)
+ deadlineMsk = ru.ysu.lms.web.DateUtils.formatMsk(deadlineAt)
+ defenseMsk = ru.ysu.lms.web.DateUtils.formatMsk(defenseAt)
+ }
}
model.addAttribute("assignments", assignments)
return "student/dashboard"
diff --git a/web/src/main/resources/application.yml b/web/src/main/resources/application.yml
index 3343281..df7fd0c 100644
--- a/web/src/main/resources/application.yml
+++ b/web/src/main/resources/application.yml
@@ -5,6 +5,7 @@ server:
cookie:
http-only: true
same-site: lax
+ tracking-modes: cookie
spring:
application:
diff --git a/web/src/main/resources/templates/admin/lab.html b/web/src/main/resources/templates/admin/lab.html
index bcf4293..8f27e34 100644
--- a/web/src/main/resources/templates/admin/lab.html
+++ b/web/src/main/resources/templates/admin/lab.html
@@ -13,8 +13,8 @@
- · Срок:
- · Защита:
+ · Срок:
+ · Защита:
· Максимум: б.
diff --git a/web/src/main/resources/templates/student/dashboard.html b/web/src/main/resources/templates/student/dashboard.html
index a3a5382..af1c7f4 100644
--- a/web/src/main/resources/templates/student/dashboard.html
+++ b/web/src/main/resources/templates/student/dashboard.html
@@ -20,8 +20,8 @@
—
- —
- —
+ —
+ —