Admin: users page with search and full cascade delete (identity + progress + files)

This commit is contained in:
2026-10-02 11:53:01 +03:00
parent b94538cb7e
commit c0cf11a5a3
14 changed files with 370 additions and 1 deletions

View File

@@ -262,6 +262,63 @@ class GroupService(
return GroupDetail(group.id.toString(), group.name, group.taskDurationMin, rows)
}
/** Список всех пользователей (студентов) с фильтром по подстроке. */
@Transactional(readOnly = true)
fun users(query: String?): List<AdminUser> {
val q = query?.trim()?.lowercase().orEmpty()
val enrollByStudent =
enrollments
.findAll()
.mapNotNull { e ->
val sid = e.student?.accountId
val group = e.group
if (sid != null && group != null) sid to (group.id to group.name) else null
}.toMap()
return students
.findAll()
.mapNotNull { s ->
val account = s.account ?: return@mapNotNull null
val groupInfo = s.accountId?.let { enrollByStudent[it] }
AdminUser(
id = account.id.toString(),
login = account.login,
role = account.role.name,
studentNo = s.studentNo,
fullName = s.fullName,
basis = s.basis,
groupId = groupInfo?.first?.toString(),
groupName = groupInfo?.second,
)
}.filter { user ->
q.isEmpty() ||
user.fullName.lowercase().contains(q) ||
user.login.lowercase().contains(q) ||
user.studentNo.lowercase().contains(q) ||
(user.groupName?.lowercase()?.contains(q) == true)
}.sortedWith(compareBy({ it.fullName.lowercase() }, { it.studentNo }))
}
/** Удаляет студента из identity: зачисление, профиль, аккаунт. */
@Transactional
fun deleteUser(
accountId: UUID,
currentAccountId: UUID,
): DeleteUserResult {
if (accountId == currentAccountId) {
throw BadRequestException("Нельзя удалить собственный аккаунт")
}
val account = accounts.findById(accountId).orElseThrow { NotFoundException("Пользователь не найден") }
if (account.role != LmsRole.STUDENT) {
throw BadRequestException("Удалять можно только студенческие аккаунты")
}
val student = students.findById(accountId).orElse(null)
val fullName = student?.fullName.orEmpty()
enrollments.findByStudentAccountId(accountId)?.let { enrollments.delete(it) }
if (student != null) students.delete(student)
accounts.delete(account)
return DeleteUserResult(accountId.toString(), account.login, fullName)
}
@Transactional
fun resetPassword(studentAccountId: UUID): ResetPasswordResponse {
val student =

View File

@@ -67,6 +67,24 @@ data class StudentCandidate(
val currentGroupName: String?,
)
/** Пользователь для страницы управления: аккаунт + профиль студента. */
data class AdminUser(
val id: String,
val login: String,
val role: String,
val studentNo: String,
val fullName: String,
val basis: String,
val groupId: String?,
val groupName: String?,
)
data class DeleteUserResult(
val id: String,
val login: String,
val fullName: String,
)
class ExportFile(
val bytes: ByteArray,
val filename: String,

View File

@@ -1,9 +1,14 @@
package ru.ysu.lms.identity.roster
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.util.UUID
@@ -17,4 +22,17 @@ class StudentController(
fun resetPassword(
@PathVariable id: UUID,
): ResetPasswordResponse = groupService.resetPassword(id)
/** Поиск по всем пользователям (для страницы управления). */
@GetMapping
fun list(
@RequestParam(required = false, name = "q") query: String?,
): List<AdminUser> = groupService.users(query)
/** Полное удаление пользователя из identity. */
@DeleteMapping("/{id}")
fun delete(
@PathVariable id: UUID,
@AuthenticationPrincipal jwt: Jwt,
): DeleteUserResult = groupService.deleteUser(id, UUID.fromString(jwt.subject))
}