Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import edu.ntnu.idi.idatt.backend.iam.domain.UserRole
import edu.ntnu.idi.idatt.backend.iam.domain.UserStatus
import edu.ntnu.idi.idatt.backend.iam.infrastructure.RefreshTokenRepository
import edu.ntnu.idi.idatt.backend.iam.infrastructure.UserRepository
import edu.ntnu.idi.idatt.backend.moderation.domain.ClassroomBannedWord
import edu.ntnu.idi.idatt.backend.moderation.infrastructure.ClassroomBannedWordRepository
import edu.ntnu.idi.idatt.backend.notebook.domain.NotebookEntry
import edu.ntnu.idi.idatt.backend.notebook.domain.NotebookSourceType
import edu.ntnu.idi.idatt.backend.notebook.infrastructure.NotebookEntryRepository
Expand Down Expand Up @@ -59,6 +61,7 @@ class DevDataBootstrap(
private val refreshTokenRepository: RefreshTokenRepository,
private val pupilGameProgressRepository: PupilGameProgressRepository,
private val notebookEntryRepository: NotebookEntryRepository,
private val classroomBannedWordRepository: ClassroomBannedWordRepository,
private val passwordEncoder: PasswordEncoder,
private val mapStopBootstrap: MapStopBootstrap,
private val medalBootstrap: MedalBootstrap,
Expand Down Expand Up @@ -117,6 +120,8 @@ class DevDataBootstrap(
seedPupilProfile(pupilAda, "Ada", sampleClassroom)
seedPupilProfile(pupilNoah, "Noah", sampleClassroom)
seedPupilProfile(pupilSofia, "Sofia", sampleClassroom)
seedClassroomBannedWord(sampleClassroom, teacher, "faen")
seedClassroomBannedWord(sampleClassroom, teacher, "fuck")
seedPupilAvatar(pupilAda)
seedPupilAvatar(pupilNoah)
seedPupilAvatar(pupilSofia)
Expand Down Expand Up @@ -420,6 +425,25 @@ class DevDataBootstrap(
)
}

private fun seedClassroomBannedWord(
classroom: Classroom,
teacher: User,
word: String,
) {
val classroomId = requireNotNull(classroom.id) { "Seed classroom must have an id" }
if (classroomBannedWordRepository.existsByClassroomIdAndWord(classroomId, word)) {
return
}

classroomBannedWordRepository.save(
ClassroomBannedWord(
classroom = classroom,
createdByTeacherUserId = requireNotNull(teacher.id) { "Teacher seed user must have an id" },
word = word,
),
)
}

private fun seedNotebookEntries(
classroom: Classroom,
pupilAda: User,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package edu.ntnu.idi.idatt.backend.moderation.api

import edu.ntnu.idi.idatt.backend.moderation.application.CreateClassroomBannedWordCommand
import edu.ntnu.idi.idatt.backend.moderation.application.UpdateClassroomBannedWordCommand
import edu.ntnu.idi.idatt.backend.moderation.domain.ClassroomBannedWord
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size
import java.time.Instant

/**
* Request payload for creating or updating one classroom banned word.
*
* @property word teacher-supplied word or phrase to persist
*/
data class ClassroomBannedWordRequest(
@field:NotBlank
@field:Size(max = 100)
val word: String,
)

/**
* API response representing one persisted classroom banned word.
*
* @property id database identifier
* @property classroomId owning classroom
* @property word normalized blocked word or phrase
* @property createdByTeacherUserId teacher who originally created the row
* @property createdAt timestamp when the row was created
* @property updatedAt timestamp when the row was last updated
*/
data class ClassroomBannedWordResponse(
val id: Long,
val classroomId: Long,
val word: String,
val createdByTeacherUserId: Long,
val createdAt: Instant?,
val updatedAt: Instant?,
)

/**
* Maps one API request payload into an application-layer create command.
*/
fun ClassroomBannedWordRequest.toCreateCommand(classroomId: Long): CreateClassroomBannedWordCommand =
CreateClassroomBannedWordCommand(
classroomId = classroomId,
word = word,
)

/**
* Maps one API request payload into an application-layer update command.
*/
fun ClassroomBannedWordRequest.toUpdateCommand(
classroomId: Long,
bannedWordId: Long,
): UpdateClassroomBannedWordCommand =
UpdateClassroomBannedWordCommand(
classroomId = classroomId,
bannedWordId = bannedWordId,
word = word,
)

/**
* Maps one persisted banned-word entity into its API response shape.
*/
fun ClassroomBannedWord.toResponse(): ClassroomBannedWordResponse =
ClassroomBannedWordResponse(
id = requireNotNull(id) { "Banned word is missing an id" },
classroomId = requireNotNull(classroom.id) { "Classroom is missing an id" },
word = word,
createdByTeacherUserId = createdByTeacherUserId,
createdAt = createdAt,
updatedAt = updatedAt,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package edu.ntnu.idi.idatt.backend.moderation.api

import edu.ntnu.idi.idatt.backend.iam.security.CurrentUser
import edu.ntnu.idi.idatt.backend.moderation.application.ClassroomModerationService
import jakarta.validation.Valid
import org.springframework.http.HttpStatus
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.core.annotation.AuthenticationPrincipal
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.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController

/**
* Teacher-facing moderation endpoints for managing classroom banned words.
*
* @property classroomModerationService application service handling banned-word persistence and
* access validation
*/
@RestController
@RequestMapping("/api/v1/teacher/classrooms/{classroomId}/moderation/banned-words")
@PreAuthorize("hasAnyRole('TEACHER', 'ADMIN')")
class ClassroomModerationController(
private val classroomModerationService: ClassroomModerationService,
) {
/**
* Returns all banned words configured for the classroom.
*/
@GetMapping
fun listBannedWords(
@AuthenticationPrincipal currentUser: CurrentUser,
@PathVariable classroomId: Long,
): List<ClassroomBannedWordResponse> =
classroomModerationService
.listBannedWords(currentUser.subject, classroomId)
.map { it.toResponse() }

/**
* Creates one new banned word for the classroom.
*/
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun createBannedWord(
@AuthenticationPrincipal currentUser: CurrentUser,
@PathVariable classroomId: Long,
@Valid @RequestBody request: ClassroomBannedWordRequest,
): ClassroomBannedWordResponse =
classroomModerationService
.createBannedWord(currentUser.subject, request.toCreateCommand(classroomId))
.toResponse()

/**
* Updates one existing banned word for the classroom.
*/
@PutMapping("/{bannedWordId}")
fun updateBannedWord(
@AuthenticationPrincipal currentUser: CurrentUser,
@PathVariable classroomId: Long,
@PathVariable bannedWordId: Long,
@Valid @RequestBody request: ClassroomBannedWordRequest,
): ClassroomBannedWordResponse =
classroomModerationService
.updateBannedWord(currentUser.subject, request.toUpdateCommand(classroomId, bannedWordId))
.toResponse()

/**
* Deletes one existing banned word for the classroom.
*/
@DeleteMapping("/{bannedWordId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun deleteBannedWord(
@AuthenticationPrincipal currentUser: CurrentUser,
@PathVariable classroomId: Long,
@PathVariable bannedWordId: Long,
) {
classroomModerationService.deleteBannedWord(currentUser.subject, classroomId, bannedWordId)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package edu.ntnu.idi.idatt.backend.moderation.api

import edu.ntnu.idi.idatt.backend.iam.application.ClassroomUserResolver
import edu.ntnu.idi.idatt.backend.iam.application.CurrentUserResolver
import edu.ntnu.idi.idatt.backend.iam.security.CurrentUser
import edu.ntnu.idi.idatt.backend.moderation.application.ClassroomModerationService
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

/**
* Classroom-member-facing read endpoint for moderation reference data.
*
* Pupils use this endpoint to fetch the current classroom banned-word list for immediate client
* validation, while the backend remains the source of truth for actual submission blocking.
*
* @property classroomModerationService application service used to load persisted banned words
* @property currentUserResolver shared resolver for the authenticated caller
* @property classroomUserResolver classroom membership guard
*/
@RestController
@RequestMapping("/api/v1/classrooms/{classroomId}/moderation/banned-words")
class ClassroomModerationReferenceController(
private val classroomModerationService: ClassroomModerationService,
private val currentUserResolver: CurrentUserResolver,
private val classroomUserResolver: ClassroomUserResolver,
) {
/**
* Returns the banned words for one classroom after verifying that the caller is a member.
*/
@GetMapping
fun listBannedWordsForClassroomMember(
@AuthenticationPrincipal currentUser: CurrentUser,
@PathVariable classroomId: Long,
): List<ClassroomBannedWordResponse> {
val user = currentUserResolver.requireUser(currentUser.subject)
classroomUserResolver.requireClassroomMember(user, classroomId)

return classroomModerationService
.listBannedWordsForClassroom(classroomId)
.map { it.toResponse() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package edu.ntnu.idi.idatt.backend.moderation.application

/**
* Command representing the intent to create one classroom-scoped banned word.
*
* @property classroomId classroom that owns the moderation rule
* @property word teacher-supplied word or phrase to block
*/
data class CreateClassroomBannedWordCommand(
val classroomId: Long,
val word: String,
)

/**
* Command representing the intent to update one existing classroom-scoped banned word.
*
* @property classroomId classroom that owns the moderation rule
* @property bannedWordId identifier of the banned-word row being edited
* @property word replacement word or phrase to persist
*/
data class UpdateClassroomBannedWordCommand(
val classroomId: Long,
val bannedWordId: Long,
val word: String,
)
Loading