fix(web): перепроверять статус сессии

This commit is contained in:
2026-10-27 19:15:15 +03:00
parent 520a61085a
commit fba648ea54
8 changed files with 393 additions and 15 deletions

View File

@@ -42,4 +42,5 @@ data class MeResponse(
val role: String,
val name: String,
val mustChangePassword: Boolean,
val active: Boolean = true,
)

View File

@@ -73,22 +73,25 @@ class AuthService(
}
private fun ensureLoginAllowed(account: Account) {
if (account.status != AccountStatus.ACTIVE) {
throw UnauthorizedException("Аккаунт архивирован")
}
if (account.role != LmsRole.STUDENT) return
val status = students.findById(account.id).map { it.status }.orElse(null)
if (status in setOf(StudentStatus.EXPELLED, StudentStatus.GRADUATED, StudentStatus.ARCHIVED)) {
throw UnauthorizedException("Доступ к системе закрыт")
}
ensureGroupOpen(account)
loginDeniedReason(account)?.let { throw UnauthorizedException(it) }
}
private fun ensureGroupOpen(account: Account) {
val groupStatus = enrollments.findByStudentAccountId(account.id)?.group?.status
if (groupStatus in setOf(GroupStatus.GRADUATED, GroupStatus.ARCHIVED)) {
throw UnauthorizedException("Группа закрыта")
/** Причина запрета входа или null, если вход разрешён. Используется и в me(). */
private fun loginDeniedReason(account: Account): String? {
if (account.status != AccountStatus.ACTIVE) {
return "Аккаунт архивирован"
}
if (account.role != LmsRole.STUDENT) return null
val status = students.findById(account.id).map { it.status }.orElse(null)
if (status in setOf(StudentStatus.EXPELLED, StudentStatus.GRADUATED, StudentStatus.ARCHIVED)) {
return "Доступ к системе закрыт"
}
return groupDeniedReason(account)
}
private fun groupDeniedReason(account: Account): String? {
val groupStatus = enrollments.findByStudentAccountId(account.id)?.group?.status
return if (groupStatus in setOf(GroupStatus.GRADUATED, GroupStatus.ARCHIVED)) "Группа закрыта" else null
}
@Transactional(readOnly = true)
@@ -103,6 +106,7 @@ class AuthService(
role = account.role.name,
name = displayName(account),
mustChangePassword = account.mustChangePassword,
active = loginDeniedReason(account) == null,
)
}

View File

@@ -0,0 +1,95 @@
package ru.ysu.lms.identity
import io.mockk.every
import io.mockk.mockk
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.oauth2.jwt.JwtEncoder
import ru.ysu.lms.identity.auth.AuthService
import ru.ysu.lms.identity.auth.LoginBackoff
import ru.ysu.lms.identity.domain.Account
import ru.ysu.lms.identity.domain.AccountStatus
import ru.ysu.lms.identity.domain.Enrollment
import ru.ysu.lms.identity.domain.GroupEntity
import ru.ysu.lms.identity.domain.GroupStatus
import ru.ysu.lms.identity.domain.Student
import ru.ysu.lms.identity.domain.StudentStatus
import ru.ysu.lms.identity.repo.AccountRepository
import ru.ysu.lms.identity.repo.EnrollmentRepository
import ru.ysu.lms.identity.repo.LoginAttemptRepository
import ru.ysu.lms.identity.repo.StudentRepository
import ru.ysu.lms.security.JwtProperties
import ru.ysu.lms.security.LmsRole
import java.time.Duration
import java.util.Optional
class AuthServiceMeActiveTest {
@Test
fun `me is active for active student`() {
val account = studentAccount()
val service = service(account, Student(accountId = account.id, lastName = "Иванов", firstName = "Иван"))
val me = service.me(account.id)
assertThat(me.active).isTrue()
assertThat(me.name).isEqualTo("Иванов Иван")
}
@Test
fun `me is inactive for expelled student`() {
val account = studentAccount()
val student =
Student(
accountId = account.id,
lastName = "Иванов",
firstName = "Иван",
status = StudentStatus.EXPELLED,
)
val service = service(account, student)
assertThat(service.me(account.id).active).isFalse()
}
@Test
fun `me is inactive for archived group`() {
val account = studentAccount()
val student = Student(accountId = account.id, lastName = "Иванов", firstName = "Иван")
val group = GroupEntity(name = "ИВТ-21", status = GroupStatus.ARCHIVED)
val service = service(account, student, Enrollment(group = group))
assertThat(service.me(account.id).active).isFalse()
}
@Test
fun `me is inactive for archived account`() {
val account = studentAccount().apply { status = AccountStatus.ARCHIVED }
val service = service(account, Student(accountId = account.id, lastName = "Иванов", firstName = "Иван"))
assertThat(service.me(account.id).active).isFalse()
}
private fun studentAccount(): Account = Account(login = "2025001", passwordHash = "hash", role = LmsRole.STUDENT)
private fun service(
account: Account,
student: Student,
enrollment: Enrollment? = null,
): AuthService {
val accounts = mockk<AccountRepository>()
every { accounts.findById(account.id) } returns Optional.of(account)
val students = mockk<StudentRepository>()
every { students.findById(account.id) } returns Optional.of(student)
val enrollments = mockk<EnrollmentRepository>()
every { enrollments.findByStudentAccountId(account.id) } returns enrollment
return AuthService(
accounts,
students,
enrollments,
mockk<PasswordEncoder>(),
mockk<JwtEncoder>(),
JwtProperties(accessTokenTtl = Duration.ofMinutes(10)),
LoginBackoff(mockk<LoginAttemptRepository>(relaxed = true)),
)
}
}