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 f7a0dc9d3b
commit 7a781fea33
14 changed files with 370 additions and 1 deletions

View File

@@ -0,0 +1,24 @@
package ru.ysu.lms.progress
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.util.UUID
@RestController
@RequestMapping("/api/v1/purge")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
class StudentPurgeController(
private val service: StudentPurgeService,
) {
/** Полное удаление данных студента из прогресс-сервиса (включая файлы сдач). */
@DeleteMapping("/students/{studentId}")
fun purgeStudent(
@PathVariable studentId: UUID,
): Map<String, Boolean> {
service.purge(studentId)
return mapOf("ok" to true)
}
}

View File

@@ -0,0 +1,44 @@
package ru.ysu.lms.progress
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import ru.ysu.lms.progress.repo.DefenseSessionRepository
import ru.ysu.lms.progress.repo.GradeCommentRepository
import ru.ysu.lms.progress.repo.GradePublicationStudentRepository
import ru.ysu.lms.progress.repo.LabGradeRepository
import ru.ysu.lms.progress.repo.StudentLabLevelRepository
import ru.ysu.lms.progress.repo.SubmissionRepository
import ru.ysu.lms.progress.repo.VariantAssignmentRepository
import java.util.UUID
@Suppress("LongParameterList")
@Service
class StudentPurgeService(
private val submissions: SubmissionRepository,
private val grades: LabGradeRepository,
private val gradeComments: GradeCommentRepository,
private val defenseSessions: DefenseSessionRepository,
private val assignments: VariantAssignmentRepository,
private val levels: StudentLabLevelRepository,
private val publicationsSt: GradePublicationStudentRepository,
private val storage: SubmissionStorage,
) {
/** Удаляет все данные студента в прогресс-сервисе: сдач, оценки, комментарии, защиты. */
@Transactional
fun purgeDb(studentId: UUID) {
submissions.findByStudentIdOrderByCreatedAtDesc(studentId).also { submissions.deleteAll(it) }
grades.findByStudentId(studentId).also { grades.deleteAll(it) }
gradeComments.findByStudentId(studentId).also { gradeComments.deleteAll(it) }
defenseSessions.findByStudentId(studentId).also { defenseSessions.deleteAll(it) }
assignments.findByStudentId(studentId).also { assignments.deleteAll(it) }
levels.findByStudentId(studentId).also { levels.deleteAll(it) }
publicationsSt.findByStudentId(studentId).also { publicationsSt.deleteAll(it) }
}
fun purgeFiles(studentId: UUID) = storage.deleteStudentFiles(studentId)
fun purge(studentId: UUID) {
purgeDb(studentId)
purgeFiles(studentId)
}
}

View File

@@ -78,6 +78,22 @@ class SubmissionStorage(
seq: Int,
): Path = rootDir.resolve(studentId.toString()).resolve(labId.toString()).resolve(seq.toString())
/** Удаляет всю папку сдач студента (data/submissions/<studentId>/...). */
fun deleteStudentFiles(studentId: UUID) {
val dir = rootDir.resolve(studentId.toString()).normalize()
require(dir != rootDir && dir.startsWith(rootDir)) { "path escape blocked" }
if (!Files.exists(dir)) return
Files
.walk(dir)
.use { walk ->
walk
.sorted(Comparator.reverseOrder())
.forEach { p ->
runCatching { Files.delete(p) }
}
}
}
private fun sanitize(name: String): String {
val base = name.substringAfterLast('/').substringAfterLast('\\').ifBlank { "file" }
val clean = base.replace(Regex("[^A-Za-z0-9._\\-\\p{IsCyrillic}]"), "_").take(120)

View File

@@ -21,4 +21,6 @@ interface GradePublicationStudentRepository : JpaRepository<GradePublicationStud
labId: UUID,
studentId: UUID,
): Boolean
fun findByStudentId(studentId: UUID): List<GradePublicationStudent>
}