mirror of
https://github.com/gotson/komga.git
synced 2025-01-09 04:08:00 +08:00
feat(api): restrict content according to user's restrictions
This commit is contained in:
parent
2621500666
commit
b0d6314ec9
@ -0,0 +1,35 @@
|
||||
package org.gotson.komga.domain.model
|
||||
|
||||
sealed class ContentRestriction {
|
||||
sealed class AgeRestriction(val age: Int) : ContentRestriction() {
|
||||
/**
|
||||
* Allow only content that has an age rating equal to or under the provided [age]
|
||||
*
|
||||
* @param[age] the age rating to allow
|
||||
*/
|
||||
class AllowOnlyUnder(age: Int) : AgeRestriction(age)
|
||||
|
||||
/**
|
||||
* Exclude content that has an age rating equal to or over the provided [age]
|
||||
*
|
||||
* @param[age] the age rating to exclude
|
||||
*/
|
||||
class ExcludeOver(age: Int) : AgeRestriction(age)
|
||||
}
|
||||
|
||||
sealed class LabelsRestriction(val labels: Collection<String>) : ContentRestriction() {
|
||||
/**
|
||||
* Allow only content that has at least one of the provided sharing [labels]
|
||||
*
|
||||
* @param[labels] a set of sharing labels to allow access to
|
||||
*/
|
||||
class AllowOnly(labels: Set<String>) : LabelsRestriction(labels)
|
||||
|
||||
/**
|
||||
* Exclude content that has at least one of the provided sharing [labels]
|
||||
*
|
||||
* @param[labels] a set of sharing labels to exclude
|
||||
*/
|
||||
class Exclude(labels: Set<String>) : LabelsRestriction(labels)
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@ data class KomgaUser(
|
||||
val rolePageStreaming: Boolean = true,
|
||||
val sharedLibrariesIds: Set<String> = emptySet(),
|
||||
val sharedAllLibraries: Boolean = true,
|
||||
val restrictions: Set<ContentRestriction> = emptySet(),
|
||||
val id: String = TsidCreator.getTsid256().toString(),
|
||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||
override val lastModifiedDate: LocalDateTime = createdDate,
|
||||
@ -66,6 +67,18 @@ data class KomgaUser(
|
||||
return sharedAllLibraries || sharedLibrariesIds.any { it == library.id }
|
||||
}
|
||||
|
||||
fun isContentRestricted(ageRating: Int? = null, sharingLabels: Set<String> = emptySet()): Boolean {
|
||||
restrictions.forEach { restriction ->
|
||||
when (restriction) {
|
||||
is ContentRestriction.AgeRestriction.AllowOnlyUnder -> if (ageRating == null || ageRating > restriction.age) return true
|
||||
is ContentRestriction.AgeRestriction.ExcludeOver -> ageRating?.let { if (it >= restriction.age) return true }
|
||||
is ContentRestriction.LabelsRestriction.AllowOnly -> if (restriction.labels.intersect(sharingLabels).isEmpty()) return true
|
||||
is ContentRestriction.LabelsRestriction.Exclude -> if (restriction.labels.intersect(sharingLabels).isNotEmpty()) return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "KomgaUser(email='$email', roleAdmin=$roleAdmin, roleFileDownload=$roleFileDownload, rolePageStreaming=$rolePageStreaming, sharedLibrariesIds=$sharedLibrariesIds, sharedAllLibraries=$sharedAllLibraries, id=$id, createdDate=$createdDate, lastModifiedDate=$lastModifiedDate)"
|
||||
}
|
||||
|
@ -1,31 +1,29 @@
|
||||
package org.gotson.komga.domain.persistence
|
||||
|
||||
import org.gotson.komga.domain.model.ContentRestriction
|
||||
import org.gotson.komga.domain.model.ReadList
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
|
||||
interface ReadListRepository {
|
||||
fun findByIdOrNull(readListId: String): ReadList?
|
||||
|
||||
fun findAll(search: String? = null, pageable: Pageable): Page<ReadList>
|
||||
/**
|
||||
* Find one ReadList by [readListId],
|
||||
* optionally with only bookIds filtered by the provided [filterOnLibraryIds] it not null.
|
||||
*/
|
||||
fun findByIdOrNull(readListId: String, filterOnLibraryIds: Collection<String>? = null, restrictions: Set<ContentRestriction> = emptySet()): ReadList?
|
||||
|
||||
/**
|
||||
* Find one ReadList by readListId,
|
||||
* optionally with only bookIds filtered by the provided filterOnLibraryIds.
|
||||
* Find all ReadList
|
||||
* optionally with at least one Book belonging to the provided [belongsToLibraryIds] if not null,
|
||||
* optionally with only bookIds filtered by the provided [filterOnLibraryIds] if not null.
|
||||
*/
|
||||
fun findByIdOrNull(readListId: String, filterOnLibraryIds: Collection<String>?): ReadList?
|
||||
fun findAll(belongsToLibraryIds: Collection<String>? = null, filterOnLibraryIds: Collection<String>? = null, search: String? = null, pageable: Pageable, restrictions: Set<ContentRestriction> = emptySet()): Page<ReadList>
|
||||
|
||||
/**
|
||||
* Find all ReadList with at least one Book belonging to the provided belongsToLibraryIds,
|
||||
* optionally with only bookIds filtered by the provided filterOnLibraryIds.
|
||||
* Find all ReadList that contains the provided [containsBookId],
|
||||
* optionally with only bookIds filtered by the provided [filterOnLibraryIds] if not null.
|
||||
*/
|
||||
fun findAllByLibraryIds(belongsToLibraryIds: Collection<String>, filterOnLibraryIds: Collection<String>?, search: String? = null, pageable: Pageable): Page<ReadList>
|
||||
|
||||
/**
|
||||
* Find all ReadList that contains the provided containsBookId,
|
||||
* optionally with only bookIds filtered by the provided filterOnLibraryIds.
|
||||
*/
|
||||
fun findAllContainingBookId(containsBookId: String, filterOnLibraryIds: Collection<String>?): Collection<ReadList>
|
||||
fun findAllContainingBookId(containsBookId: String, filterOnLibraryIds: Collection<String>?, restrictions: Set<ContentRestriction> = emptySet()): Collection<ReadList>
|
||||
|
||||
fun findAllEmpty(): Collection<ReadList>
|
||||
|
||||
|
@ -1,31 +1,29 @@
|
||||
package org.gotson.komga.domain.persistence
|
||||
|
||||
import org.gotson.komga.domain.model.ContentRestriction
|
||||
import org.gotson.komga.domain.model.SeriesCollection
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
|
||||
interface SeriesCollectionRepository {
|
||||
fun findByIdOrNull(collectionId: String): SeriesCollection?
|
||||
|
||||
fun findAll(search: String? = null, pageable: Pageable): Page<SeriesCollection>
|
||||
/**
|
||||
* Find one SeriesCollection by [collectionId],
|
||||
* optionally with only seriesId filtered by the provided [filterOnLibraryIds] if not null.
|
||||
*/
|
||||
fun findByIdOrNull(collectionId: String, filterOnLibraryIds: Collection<String>? = null, restrictions: Set<ContentRestriction> = emptySet()): SeriesCollection?
|
||||
|
||||
/**
|
||||
* Find one SeriesCollection by collectionId,
|
||||
* optionally with only seriesId filtered by the provided filterOnLibraryIds.
|
||||
* Find all SeriesCollection
|
||||
* optionally with at least one Series belonging to the provided [belongsToLibraryIds] if not null,
|
||||
* optionally with only seriesId filtered by the provided [filterOnLibraryIds] if not null.
|
||||
*/
|
||||
fun findByIdOrNull(collectionId: String, filterOnLibraryIds: Collection<String>?): SeriesCollection?
|
||||
fun findAll(belongsToLibraryIds: Collection<String>? = null, filterOnLibraryIds: Collection<String>? = null, search: String? = null, pageable: Pageable, restrictions: Set<ContentRestriction> = emptySet()): Page<SeriesCollection>
|
||||
|
||||
/**
|
||||
* Find all SeriesCollection with at least one Series belonging to the provided belongsToLibraryIds,
|
||||
* optionally with only seriesId filtered by the provided filterOnLibraryIds.
|
||||
* Find all SeriesCollection that contains the provided [containsSeriesId],
|
||||
* optionally with only seriesId filtered by the provided [filterOnLibraryIds] if not null.
|
||||
*/
|
||||
fun findAllByLibraryIds(belongsToLibraryIds: Collection<String>, filterOnLibraryIds: Collection<String>?, search: String? = null, pageable: Pageable): Page<SeriesCollection>
|
||||
|
||||
/**
|
||||
* Find all SeriesCollection that contains the provided containsSeriesId,
|
||||
* optionally with only seriesId filtered by the provided filterOnLibraryIds.
|
||||
*/
|
||||
fun findAllContainingSeriesId(containsSeriesId: String, filterOnLibraryIds: Collection<String>?): Collection<SeriesCollection>
|
||||
fun findAllContainingSeriesId(containsSeriesId: String, filterOnLibraryIds: Collection<String>?, restrictions: Set<ContentRestriction> = emptySet()): Collection<SeriesCollection>
|
||||
|
||||
fun findAllEmpty(): Collection<SeriesCollection>
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.gotson.komga.infrastructure.jooq
|
||||
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.ContentRestriction
|
||||
import org.gotson.komga.domain.model.ReadStatus
|
||||
import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource
|
||||
import org.gotson.komga.infrastructure.search.LuceneEntity
|
||||
@ -77,8 +78,8 @@ class BookDtoDao(
|
||||
"readList.number" to rlb.NUMBER,
|
||||
)
|
||||
|
||||
override fun findAll(search: BookSearchWithReadProgress, userId: String, pageable: Pageable): Page<BookDto> {
|
||||
val conditions = search.toCondition()
|
||||
override fun findAll(search: BookSearchWithReadProgress, userId: String, pageable: Pageable, restrictions: Set<ContentRestriction>): Page<BookDto> {
|
||||
val conditions = search.toCondition().and(restrictions.toCondition())
|
||||
|
||||
return findAll(conditions, userId, pageable, search.toJoinConditions(), null, search.searchTerm)
|
||||
}
|
||||
@ -106,20 +107,21 @@ class BookDtoDao(
|
||||
val bookIds = luceneHelper.searchEntitiesIds(searchTerm, LuceneEntity.Book)
|
||||
val searchCondition = b.ID.inOrNoCondition(bookIds)
|
||||
|
||||
val count = dsl.selectDistinct(b.ID)
|
||||
.from(b)
|
||||
.leftJoin(m).on(b.ID.eq(m.BOOK_ID))
|
||||
.leftJoin(d).on(b.ID.eq(d.BOOK_ID))
|
||||
.leftJoin(r).on(b.ID.eq(r.BOOK_ID)).and(readProgressCondition(userId))
|
||||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.apply { if (joinConditions.tag) leftJoin(bt).on(b.ID.eq(bt.BOOK_ID)) }
|
||||
.apply { if (joinConditions.selectReadListNumber) leftJoin(rlb).on(b.ID.eq(rlb.BOOK_ID)) }
|
||||
.apply { if (joinConditions.author) leftJoin(a).on(b.ID.eq(a.BOOK_ID)) }
|
||||
.where(conditions)
|
||||
.and(searchCondition)
|
||||
.groupBy(b.ID)
|
||||
.fetch()
|
||||
.size
|
||||
val count = dsl.fetchCount(
|
||||
dsl.selectDistinct(b.ID)
|
||||
.from(b)
|
||||
.leftJoin(m).on(b.ID.eq(m.BOOK_ID))
|
||||
.leftJoin(d).on(b.ID.eq(d.BOOK_ID))
|
||||
.leftJoin(r).on(b.ID.eq(r.BOOK_ID)).and(readProgressCondition(userId))
|
||||
.leftJoin(sd).on(b.SERIES_ID.eq(sd.SERIES_ID))
|
||||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.apply { if (joinConditions.tag) leftJoin(bt).on(b.ID.eq(bt.BOOK_ID)) }
|
||||
.apply { if (joinConditions.selectReadListNumber) leftJoin(rlb).on(b.ID.eq(rlb.BOOK_ID)) }
|
||||
.apply { if (joinConditions.author) leftJoin(a).on(b.ID.eq(a.BOOK_ID)) }
|
||||
.where(conditions)
|
||||
.and(searchCondition)
|
||||
.groupBy(b.ID),
|
||||
)
|
||||
|
||||
val orderBy =
|
||||
pageable.sort.mapNotNull {
|
||||
@ -172,12 +174,14 @@ class BookDtoDao(
|
||||
): BookDto? =
|
||||
findSiblingReadList(readListId, bookId, userId, filterOnLibraryIds, next = true)
|
||||
|
||||
override fun findAllOnDeck(userId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<BookDto> {
|
||||
override fun findAllOnDeck(userId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable, restrictions: Set<ContentRestriction>): Page<BookDto> {
|
||||
val seriesIds = dsl.select(s.ID)
|
||||
.from(s)
|
||||
.leftJoin(b).on(s.ID.eq(b.SERIES_ID))
|
||||
.leftJoin(r).on(b.ID.eq(r.BOOK_ID)).and(readProgressCondition(userId))
|
||||
.apply { filterOnLibraryIds?.let { where(s.LIBRARY_ID.`in`(it)) } }
|
||||
.leftJoin(sd).on(b.SERIES_ID.eq(sd.SERIES_ID))
|
||||
.where(restrictions.toCondition())
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.groupBy(s.ID)
|
||||
.having(countUnread.ge(inline(1.toBigDecimal())))
|
||||
.and(countRead.ge(inline(1.toBigDecimal())))
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.gotson.komga.infrastructure.jooq
|
||||
|
||||
import org.gotson.komga.domain.model.ContentRestriction
|
||||
import org.gotson.komga.domain.model.ReadList
|
||||
import org.gotson.komga.domain.persistence.ReadListRepository
|
||||
import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource
|
||||
@ -32,32 +33,42 @@ class ReadListDao(
|
||||
private val rl = Tables.READLIST
|
||||
private val rlb = Tables.READLIST_BOOK
|
||||
private val b = Tables.BOOK
|
||||
private val sd = Tables.SERIES_METADATA
|
||||
|
||||
private val sorts = mapOf(
|
||||
"name" to rl.NAME.collate(SqliteUdfDataSource.collationUnicode3),
|
||||
)
|
||||
|
||||
override fun findByIdOrNull(readListId: String): ReadList? =
|
||||
selectBase()
|
||||
.where(rl.ID.eq(readListId))
|
||||
.fetchAndMap(null)
|
||||
.firstOrNull()
|
||||
|
||||
override fun findByIdOrNull(readListId: String, filterOnLibraryIds: Collection<String>?): ReadList? =
|
||||
selectBase()
|
||||
override fun findByIdOrNull(readListId: String, filterOnLibraryIds: Collection<String>?, restrictions: Set<ContentRestriction>): ReadList? =
|
||||
selectBase(restrictions.isNotEmpty())
|
||||
.where(rl.ID.eq(readListId))
|
||||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.fetchAndMap(filterOnLibraryIds)
|
||||
.apply { if (restrictions.isNotEmpty()) and(restrictions.toCondition()) }
|
||||
.fetchAndMap(filterOnLibraryIds, restrictions)
|
||||
.firstOrNull()
|
||||
|
||||
override fun findAll(search: String?, pageable: Pageable): Page<ReadList> {
|
||||
override fun findAll(belongsToLibraryIds: Collection<String>?, filterOnLibraryIds: Collection<String>?, search: String?, pageable: Pageable, restrictions: Set<ContentRestriction>): Page<ReadList> {
|
||||
val readListIds = luceneHelper.searchEntitiesIds(search, LuceneEntity.ReadList)
|
||||
val searchCondition = rl.ID.inOrNoCondition(readListIds)
|
||||
|
||||
val count = dsl.selectCount()
|
||||
.from(rl)
|
||||
.where(searchCondition)
|
||||
.fetchOne(0, Long::class.java) ?: 0
|
||||
val conditions = searchCondition
|
||||
.and(b.LIBRARY_ID.inOrNoCondition(belongsToLibraryIds))
|
||||
.and(b.LIBRARY_ID.inOrNoCondition(filterOnLibraryIds))
|
||||
.and(restrictions.toCondition())
|
||||
|
||||
val queryIds =
|
||||
if (belongsToLibraryIds == null && filterOnLibraryIds == null && restrictions.isEmpty()) null
|
||||
else
|
||||
dsl.selectDistinct(rl.ID)
|
||||
.from(rl)
|
||||
.leftJoin(rlb).on(rl.ID.eq(rlb.READLIST_ID))
|
||||
.leftJoin(b).on(rlb.BOOK_ID.eq(b.ID))
|
||||
.apply { if (restrictions.isNotEmpty()) leftJoin(sd).on(sd.SERIES_ID.eq(b.SERIES_ID)) }
|
||||
.where(conditions)
|
||||
|
||||
val count =
|
||||
if (queryIds != null) dsl.fetchCount(queryIds)
|
||||
else dsl.fetchCount(rl, searchCondition)
|
||||
|
||||
val orderBy =
|
||||
pageable.sort.mapNotNull {
|
||||
@ -65,50 +76,12 @@ class ReadListDao(
|
||||
else it.toSortField(sorts)
|
||||
}
|
||||
|
||||
val items = selectBase()
|
||||
.where(searchCondition)
|
||||
.orderBy(orderBy)
|
||||
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
|
||||
.fetchAndMap(null)
|
||||
|
||||
val pageSort = if (orderBy.isNotEmpty()) pageable.sort else Sort.unsorted()
|
||||
return PageImpl(
|
||||
items,
|
||||
if (pageable.isPaged) PageRequest.of(pageable.pageNumber, pageable.pageSize, pageSort)
|
||||
else PageRequest.of(0, maxOf(count.toInt(), 20), pageSort),
|
||||
count,
|
||||
)
|
||||
}
|
||||
|
||||
override fun findAllByLibraryIds(belongsToLibraryIds: Collection<String>, filterOnLibraryIds: Collection<String>?, search: String?, pageable: Pageable): Page<ReadList> {
|
||||
val readListIds = luceneHelper.searchEntitiesIds(search, LuceneEntity.ReadList)
|
||||
val searchCondition = rl.ID.inOrNoCondition(readListIds)
|
||||
|
||||
val conditions = b.LIBRARY_ID.`in`(belongsToLibraryIds)
|
||||
.and(searchCondition)
|
||||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
|
||||
val ids = dsl.selectDistinct(rl.ID)
|
||||
.from(rl)
|
||||
.leftJoin(rlb).on(rl.ID.eq(rlb.READLIST_ID))
|
||||
.leftJoin(b).on(rlb.BOOK_ID.eq(b.ID))
|
||||
val items = selectBase(restrictions.isNotEmpty())
|
||||
.where(conditions)
|
||||
.fetch(0, String::class.java)
|
||||
|
||||
val count = ids.size
|
||||
|
||||
val orderBy =
|
||||
pageable.sort.mapNotNull {
|
||||
if (it.property == "relevance" && !readListIds.isNullOrEmpty()) rl.ID.sortByValues(readListIds, it.isAscending)
|
||||
else it.toSortField(sorts)
|
||||
}
|
||||
|
||||
val items = selectBase()
|
||||
.where(rl.ID.`in`(ids))
|
||||
.and(conditions)
|
||||
.apply { if (queryIds != null) and(rl.ID.`in`(queryIds)) }
|
||||
.orderBy(orderBy)
|
||||
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
|
||||
.fetchAndMap(filterOnLibraryIds)
|
||||
.fetchAndMap(filterOnLibraryIds, restrictions)
|
||||
|
||||
val pageSort = if (orderBy.isNotEmpty()) pageable.sort else Sort.unsorted()
|
||||
return PageImpl(
|
||||
@ -119,17 +92,19 @@ class ReadListDao(
|
||||
)
|
||||
}
|
||||
|
||||
override fun findAllContainingBookId(containsBookId: String, filterOnLibraryIds: Collection<String>?): Collection<ReadList> {
|
||||
val ids = dsl.select(rl.ID)
|
||||
override fun findAllContainingBookId(containsBookId: String, filterOnLibraryIds: Collection<String>?, restrictions: Set<ContentRestriction>): Collection<ReadList> {
|
||||
val queryIds = dsl.select(rl.ID)
|
||||
.from(rl)
|
||||
.leftJoin(rlb).on(rl.ID.eq(rlb.READLIST_ID))
|
||||
.apply { if (restrictions.isNotEmpty()) leftJoin(b).on(rlb.BOOK_ID.eq(b.ID)).leftJoin(sd).on(sd.SERIES_ID.eq(b.SERIES_ID)) }
|
||||
.where(rlb.BOOK_ID.eq(containsBookId))
|
||||
.fetch(0, String::class.java)
|
||||
.apply { if (restrictions.isNotEmpty()) and(restrictions.toCondition()) }
|
||||
|
||||
return selectBase()
|
||||
.where(rl.ID.`in`(ids))
|
||||
return selectBase(restrictions.isNotEmpty())
|
||||
.where(rl.ID.`in`(queryIds))
|
||||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.fetchAndMap(filterOnLibraryIds)
|
||||
.apply { if (restrictions.isNotEmpty()) and(restrictions.toCondition()) }
|
||||
.fetchAndMap(filterOnLibraryIds, restrictions)
|
||||
}
|
||||
|
||||
override fun findAllEmpty(): Collection<ReadList> =
|
||||
@ -150,20 +125,23 @@ class ReadListDao(
|
||||
.fetchAndMap(null)
|
||||
.firstOrNull()
|
||||
|
||||
private fun selectBase() =
|
||||
private fun selectBase(joinOnSeriesMetadata: Boolean = false) =
|
||||
dsl.selectDistinct(*rl.fields())
|
||||
.from(rl)
|
||||
.leftJoin(rlb).on(rl.ID.eq(rlb.READLIST_ID))
|
||||
.leftJoin(b).on(rlb.BOOK_ID.eq(b.ID))
|
||||
.apply { if (joinOnSeriesMetadata) leftJoin(sd).on(sd.SERIES_ID.eq(b.SERIES_ID)) }
|
||||
|
||||
private fun ResultQuery<Record>.fetchAndMap(filterOnLibraryIds: Collection<String>?): List<ReadList> =
|
||||
private fun ResultQuery<Record>.fetchAndMap(filterOnLibraryIds: Collection<String>?, restrictions: Set<ContentRestriction> = emptySet()): List<ReadList> =
|
||||
fetchInto(rl)
|
||||
.map { rr ->
|
||||
val bookIds = dsl.select(*rlb.fields())
|
||||
.from(rlb)
|
||||
.leftJoin(b).on(rlb.BOOK_ID.eq(b.ID))
|
||||
.apply { if (restrictions.isNotEmpty()) leftJoin(sd).on(sd.SERIES_ID.eq(b.SERIES_ID)) }
|
||||
.where(rlb.READLIST_ID.eq(rr.id))
|
||||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.apply { if (restrictions.isNotEmpty()) and(restrictions.toCondition()) }
|
||||
.orderBy(rlb.NUMBER.asc())
|
||||
.fetchInto(rlb)
|
||||
.mapNotNull { it.number to it.bookId }
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.gotson.komga.infrastructure.jooq
|
||||
|
||||
import org.gotson.komga.domain.model.ContentRestriction
|
||||
import org.gotson.komga.domain.model.SeriesCollection
|
||||
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
|
||||
import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource
|
||||
@ -31,32 +32,42 @@ class SeriesCollectionDao(
|
||||
private val c = Tables.COLLECTION
|
||||
private val cs = Tables.COLLECTION_SERIES
|
||||
private val s = Tables.SERIES
|
||||
private val sd = Tables.SERIES_METADATA
|
||||
|
||||
private val sorts = mapOf(
|
||||
"name" to c.NAME.collate(SqliteUdfDataSource.collationUnicode3),
|
||||
)
|
||||
|
||||
override fun findByIdOrNull(collectionId: String): SeriesCollection? =
|
||||
selectBase()
|
||||
.where(c.ID.eq(collectionId))
|
||||
.fetchAndMap(null)
|
||||
.firstOrNull()
|
||||
|
||||
override fun findByIdOrNull(collectionId: String, filterOnLibraryIds: Collection<String>?): SeriesCollection? =
|
||||
selectBase()
|
||||
override fun findByIdOrNull(collectionId: String, filterOnLibraryIds: Collection<String>?, restrictions: Set<ContentRestriction>): SeriesCollection? =
|
||||
selectBase(restrictions.isNotEmpty())
|
||||
.where(c.ID.eq(collectionId))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.fetchAndMap(filterOnLibraryIds)
|
||||
.apply { if (restrictions.isNotEmpty()) and(restrictions.toCondition()) }
|
||||
.fetchAndMap(filterOnLibraryIds, restrictions)
|
||||
.firstOrNull()
|
||||
|
||||
override fun findAll(search: String?, pageable: Pageable): Page<SeriesCollection> {
|
||||
override fun findAll(belongsToLibraryIds: Collection<String>?, filterOnLibraryIds: Collection<String>?, search: String?, pageable: Pageable, restrictions: Set<ContentRestriction>): Page<SeriesCollection> {
|
||||
val collectionIds = luceneHelper.searchEntitiesIds(search, LuceneEntity.Collection)
|
||||
val searchCondition = c.ID.inOrNoCondition(collectionIds)
|
||||
|
||||
val count = dsl.selectCount()
|
||||
.from(c)
|
||||
.where(searchCondition)
|
||||
.fetchOne(0, Long::class.java) ?: 0
|
||||
val conditions = searchCondition
|
||||
.and(s.LIBRARY_ID.inOrNoCondition(belongsToLibraryIds))
|
||||
.and(s.LIBRARY_ID.inOrNoCondition(filterOnLibraryIds))
|
||||
.and(restrictions.toCondition())
|
||||
|
||||
val queryIds =
|
||||
if (belongsToLibraryIds == null && filterOnLibraryIds == null && restrictions.isEmpty()) null
|
||||
else
|
||||
dsl.selectDistinct(c.ID)
|
||||
.from(c)
|
||||
.leftJoin(cs).on(c.ID.eq(cs.COLLECTION_ID))
|
||||
.leftJoin(s).on(cs.SERIES_ID.eq(s.ID))
|
||||
.leftJoin(sd).on(cs.SERIES_ID.eq(sd.SERIES_ID))
|
||||
.where(conditions)
|
||||
|
||||
val count =
|
||||
if (queryIds != null) dsl.fetchCount(queryIds)
|
||||
else dsl.fetchCount(c, searchCondition)
|
||||
|
||||
val orderBy =
|
||||
pageable.sort.mapNotNull {
|
||||
@ -64,50 +75,12 @@ class SeriesCollectionDao(
|
||||
else it.toSortField(sorts)
|
||||
}
|
||||
|
||||
val items = selectBase()
|
||||
.where(searchCondition)
|
||||
.orderBy(orderBy)
|
||||
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
|
||||
.fetchAndMap(null)
|
||||
|
||||
val pageSort = if (orderBy.isNotEmpty()) pageable.sort else Sort.unsorted()
|
||||
return PageImpl(
|
||||
items,
|
||||
if (pageable.isPaged) PageRequest.of(pageable.pageNumber, pageable.pageSize, pageSort)
|
||||
else PageRequest.of(0, maxOf(count.toInt(), 20), pageSort),
|
||||
count,
|
||||
)
|
||||
}
|
||||
|
||||
override fun findAllByLibraryIds(belongsToLibraryIds: Collection<String>, filterOnLibraryIds: Collection<String>?, search: String?, pageable: Pageable): Page<SeriesCollection> {
|
||||
val collectionIds = luceneHelper.searchEntitiesIds(search, LuceneEntity.Collection)
|
||||
val searchCondition = c.ID.inOrNoCondition(collectionIds)
|
||||
|
||||
val conditions = s.LIBRARY_ID.`in`(belongsToLibraryIds)
|
||||
.and(searchCondition)
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
|
||||
val ids = dsl.selectDistinct(c.ID)
|
||||
.from(c)
|
||||
.leftJoin(cs).on(c.ID.eq(cs.COLLECTION_ID))
|
||||
.leftJoin(s).on(cs.SERIES_ID.eq(s.ID))
|
||||
val items = selectBase(restrictions.isNotEmpty())
|
||||
.where(conditions)
|
||||
.fetch(0, String::class.java)
|
||||
|
||||
val count = ids.size
|
||||
|
||||
val orderBy =
|
||||
pageable.sort.mapNotNull {
|
||||
if (it.property == "relevance" && !collectionIds.isNullOrEmpty()) c.ID.sortByValues(collectionIds, it.isAscending)
|
||||
else it.toSortField(sorts)
|
||||
}
|
||||
|
||||
val items = selectBase()
|
||||
.where(c.ID.`in`(ids))
|
||||
.and(conditions)
|
||||
.apply { if (queryIds != null) and(c.ID.`in`(queryIds)) }
|
||||
.orderBy(orderBy)
|
||||
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
|
||||
.fetchAndMap(filterOnLibraryIds)
|
||||
.fetchAndMap(filterOnLibraryIds, restrictions)
|
||||
|
||||
val pageSort = if (orderBy.isNotEmpty()) pageable.sort else Sort.unsorted()
|
||||
return PageImpl(
|
||||
@ -118,17 +91,19 @@ class SeriesCollectionDao(
|
||||
)
|
||||
}
|
||||
|
||||
override fun findAllContainingSeriesId(containsSeriesId: String, filterOnLibraryIds: Collection<String>?): Collection<SeriesCollection> {
|
||||
val ids = dsl.select(c.ID)
|
||||
override fun findAllContainingSeriesId(containsSeriesId: String, filterOnLibraryIds: Collection<String>?, restrictions: Set<ContentRestriction>): Collection<SeriesCollection> {
|
||||
val queryIds = dsl.select(c.ID)
|
||||
.from(c)
|
||||
.leftJoin(cs).on(c.ID.eq(cs.COLLECTION_ID))
|
||||
.apply { if (restrictions.isNotEmpty()) leftJoin(sd).on(cs.SERIES_ID.eq(sd.SERIES_ID)) }
|
||||
.where(cs.SERIES_ID.eq(containsSeriesId))
|
||||
.fetch(0, String::class.java)
|
||||
.apply { if (restrictions.isNotEmpty()) and(restrictions.toCondition()) }
|
||||
|
||||
return selectBase()
|
||||
.where(c.ID.`in`(ids))
|
||||
return selectBase(restrictions.isNotEmpty())
|
||||
.where(c.ID.`in`(queryIds))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.fetchAndMap(filterOnLibraryIds)
|
||||
.apply { if (restrictions.isNotEmpty()) and(restrictions.toCondition()) }
|
||||
.fetchAndMap(filterOnLibraryIds, restrictions)
|
||||
}
|
||||
|
||||
override fun findAllEmpty(): Collection<SeriesCollection> =
|
||||
@ -149,20 +124,23 @@ class SeriesCollectionDao(
|
||||
.fetchAndMap(null)
|
||||
.firstOrNull()
|
||||
|
||||
private fun selectBase() =
|
||||
private fun selectBase(joinOnSeriesMetadata: Boolean = false) =
|
||||
dsl.selectDistinct(*c.fields())
|
||||
.from(c)
|
||||
.leftJoin(cs).on(c.ID.eq(cs.COLLECTION_ID))
|
||||
.leftJoin(s).on(cs.SERIES_ID.eq(s.ID))
|
||||
.apply { if (joinOnSeriesMetadata) leftJoin(sd).on(cs.SERIES_ID.eq(sd.SERIES_ID)) }
|
||||
|
||||
private fun ResultQuery<Record>.fetchAndMap(filterOnLibraryIds: Collection<String>?): List<SeriesCollection> =
|
||||
private fun ResultQuery<Record>.fetchAndMap(filterOnLibraryIds: Collection<String>?, restrictions: Set<ContentRestriction> = emptySet()): List<SeriesCollection> =
|
||||
fetchInto(c)
|
||||
.map { cr ->
|
||||
val seriesIds = dsl.select(*cs.fields())
|
||||
.from(cs)
|
||||
.leftJoin(s).on(cs.SERIES_ID.eq(s.ID))
|
||||
.apply { if (restrictions.isNotEmpty()) leftJoin(sd).on(cs.SERIES_ID.eq(sd.SERIES_ID)) }
|
||||
.where(cs.COLLECTION_ID.eq(cr.id))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.apply { if (restrictions.isNotEmpty()) and(restrictions.toCondition()) }
|
||||
.orderBy(cs.NUMBER.asc())
|
||||
.fetchInto(cs)
|
||||
.mapNotNull { it.seriesId }
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.gotson.komga.infrastructure.jooq
|
||||
|
||||
import mu.KotlinLogging
|
||||
import org.gotson.komga.domain.model.ContentRestriction
|
||||
import org.gotson.komga.domain.model.ReadStatus
|
||||
import org.gotson.komga.domain.model.SeriesSearch
|
||||
import org.gotson.komga.domain.model.SeriesSearchWithReadProgress
|
||||
@ -37,7 +38,6 @@ import java.net.URL
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
const val BOOKS_COUNT = "booksCount"
|
||||
const val BOOKS_UNREAD_COUNT = "booksUnreadCount"
|
||||
const val BOOKS_IN_PROGRESS_COUNT = "booksInProgressCount"
|
||||
const val BOOKS_READ_COUNT = "booksReadCount"
|
||||
@ -77,8 +77,8 @@ class SeriesDtoDao(
|
||||
"booksCount" to s.BOOK_COUNT,
|
||||
)
|
||||
|
||||
override fun findAll(search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable): Page<SeriesDto> {
|
||||
val conditions = search.toCondition()
|
||||
override fun findAll(search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable, restrictions: Set<ContentRestriction>): Page<SeriesDto> {
|
||||
val conditions = search.toCondition().and(restrictions.toCondition())
|
||||
|
||||
return findAll(conditions, userId, pageable, search.toJoinConditions(), search.searchTerm)
|
||||
}
|
||||
@ -88,8 +88,9 @@ class SeriesDtoDao(
|
||||
search: SeriesSearchWithReadProgress,
|
||||
userId: String,
|
||||
pageable: Pageable,
|
||||
restrictions: Set<ContentRestriction>,
|
||||
): Page<SeriesDto> {
|
||||
val conditions = search.toCondition().and(cs.COLLECTION_ID.eq(collectionId))
|
||||
val conditions = search.toCondition().and(restrictions.toCondition()).and(cs.COLLECTION_ID.eq(collectionId))
|
||||
val joinConditions = search.toJoinConditions().copy(selectCollectionNumber = true, collection = true)
|
||||
|
||||
return findAll(conditions, userId, pageable, joinConditions, search.searchTerm)
|
||||
@ -98,16 +99,18 @@ class SeriesDtoDao(
|
||||
override fun findAllRecentlyUpdated(
|
||||
search: SeriesSearchWithReadProgress,
|
||||
userId: String,
|
||||
restrictions: Set<ContentRestriction>,
|
||||
pageable: Pageable,
|
||||
): Page<SeriesDto> {
|
||||
val conditions = search.toCondition()
|
||||
.and(s.CREATED_DATE.ne(s.LAST_MODIFIED_DATE))
|
||||
.and(restrictions.toCondition())
|
||||
.and(s.CREATED_DATE.notEqual(s.LAST_MODIFIED_DATE))
|
||||
|
||||
return findAll(conditions, userId, pageable, search.toJoinConditions(), search.searchTerm)
|
||||
}
|
||||
|
||||
override fun countByFirstCharacter(search: SeriesSearchWithReadProgress, userId: String): List<GroupCountDto> {
|
||||
val conditions = search.toCondition()
|
||||
override fun countByFirstCharacter(search: SeriesSearchWithReadProgress, userId: String, restrictions: Set<ContentRestriction>): List<GroupCountDto> {
|
||||
val conditions = search.toCondition().and(restrictions.toCondition())
|
||||
val joinConditions = search.toJoinConditions()
|
||||
val seriesIds = luceneHelper.searchEntitiesIds(search.searchTerm, LuceneEntity.Series)
|
||||
val searchCondition = s.ID.inOrNoCondition(seriesIds)
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.gotson.komga.infrastructure.jooq
|
||||
|
||||
import org.gotson.komga.domain.model.ContentRestriction
|
||||
import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource
|
||||
import org.gotson.komga.jooq.Tables
|
||||
import org.jooq.Condition
|
||||
@ -34,7 +35,7 @@ fun Field<String>.sortByValues(values: List<String>, asc: Boolean = true): Field
|
||||
return c.otherwise(Int.MAX_VALUE)
|
||||
}
|
||||
|
||||
fun Field<String>.inOrNoCondition(list: List<String>?): Condition =
|
||||
fun Field<String>.inOrNoCondition(list: Collection<String>?): Condition =
|
||||
when {
|
||||
list == null -> DSL.noCondition()
|
||||
list.isEmpty() -> DSL.falseCondition()
|
||||
@ -63,3 +64,16 @@ fun DSLContext.insertTempStrings(batchSize: Int, collection: Collection<String>)
|
||||
}
|
||||
|
||||
fun DSLContext.selectTempStrings() = this.select(Tables.TEMP_STRING_LIST.STRING).from(Tables.TEMP_STRING_LIST)
|
||||
|
||||
fun Set<ContentRestriction>.toCondition(): Condition =
|
||||
this.fold(DSL.noCondition()) { accumulator, restriction ->
|
||||
accumulator.and(
|
||||
when (restriction) {
|
||||
is ContentRestriction.AgeRestriction.AllowOnlyUnder -> Tables.SERIES_METADATA.AGE_RATING.lessOrEqual(restriction.age)
|
||||
is ContentRestriction.AgeRestriction.ExcludeOver -> Tables.SERIES_METADATA.AGE_RATING.isNull.or(Tables.SERIES_METADATA.AGE_RATING.lessThan(restriction.age))
|
||||
// TODO: add conditions for sharing labels
|
||||
is ContentRestriction.LabelsRestriction.AllowOnly -> DSL.noCondition()
|
||||
is ContentRestriction.LabelsRestriction.Exclude -> DSL.noCondition()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,16 @@
|
||||
package org.gotson.komga.interfaces.api
|
||||
|
||||
import org.gotson.komga.domain.model.KomgaUser
|
||||
import org.gotson.komga.interfaces.api.rest.dto.SeriesDto
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
|
||||
/**
|
||||
* Convenience function to check for content restriction.
|
||||
*
|
||||
* @throws[ResponseStatusException] if the user cannot access the content
|
||||
*/
|
||||
fun KomgaUser.checkContentRestriction(series: SeriesDto) {
|
||||
if (!canAccessLibrary(series.libraryId)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
if (isContentRestricted(ageRating = series.metadata.ageRating)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
}
|
@ -18,6 +18,7 @@ import org.gotson.komga.domain.persistence.SeriesCollectionRepository
|
||||
import org.gotson.komga.infrastructure.jooq.toCurrentTimeZone
|
||||
import org.gotson.komga.infrastructure.security.KomgaPrincipal
|
||||
import org.gotson.komga.infrastructure.swagger.PageAsQueryParam
|
||||
import org.gotson.komga.interfaces.api.checkContentRestriction
|
||||
import org.gotson.komga.interfaces.api.opds.dto.OpdsAuthor
|
||||
import org.gotson.komga.interfaces.api.opds.dto.OpdsEntryAcquisition
|
||||
import org.gotson.komga.interfaces.api.opds.dto.OpdsEntryNavigation
|
||||
@ -225,6 +226,7 @@ class OpdsController(
|
||||
principal.user.id,
|
||||
principal.user.getAuthorizedLibraryIds(null),
|
||||
page,
|
||||
principal.user.restrictions,
|
||||
)
|
||||
|
||||
val builder = uriBuilder(ROUTE_ON_DECK)
|
||||
@ -263,6 +265,7 @@ class OpdsController(
|
||||
bookSearch,
|
||||
principal.user.id,
|
||||
pageable,
|
||||
principal.user.restrictions,
|
||||
)
|
||||
|
||||
val builder = uriBuilder(ROUTE_ON_DECK)
|
||||
@ -301,7 +304,7 @@ class OpdsController(
|
||||
deleted = false,
|
||||
)
|
||||
|
||||
val seriesPage = seriesDtoRepository.findAll(seriesSearch, principal.user.id, pageable)
|
||||
val seriesPage = seriesDtoRepository.findAll(seriesSearch, principal.user.id, pageable, principal.user.restrictions)
|
||||
|
||||
val builder = uriBuilder(ROUTE_SERIES_ALL)
|
||||
.queryParamIfPresent("search", Optional.ofNullable(searchTerm))
|
||||
@ -335,7 +338,7 @@ class OpdsController(
|
||||
deleted = false,
|
||||
)
|
||||
|
||||
val seriesPage = seriesDtoRepository.findAll(seriesSearch, principal.user.id, pageable)
|
||||
val seriesPage = seriesDtoRepository.findAll(seriesSearch, principal.user.id, pageable, principal.user.restrictions)
|
||||
|
||||
val uriBuilder = uriBuilder(ROUTE_SERIES_LATEST)
|
||||
|
||||
@ -372,7 +375,7 @@ class OpdsController(
|
||||
)
|
||||
val pageable = PageRequest.of(page.pageNumber, page.pageSize, Sort.by(Sort.Order.desc("createdDate")))
|
||||
|
||||
val bookPage = bookDtoRepository.findAll(bookSearch, principal.user.id, pageable)
|
||||
val bookPage = bookDtoRepository.findAll(bookSearch, principal.user.id, pageable, principal.user.restrictions)
|
||||
|
||||
val uriBuilder = uriBuilder(ROUTE_BOOKS_LATEST)
|
||||
|
||||
@ -420,12 +423,7 @@ class OpdsController(
|
||||
@Parameter(hidden = true) page: Pageable,
|
||||
): OpdsFeed {
|
||||
val pageable = PageRequest.of(page.pageNumber, page.pageSize, Sort.by(Sort.Order.asc("name")))
|
||||
val collections =
|
||||
if (principal.user.sharedAllLibraries) {
|
||||
collectionRepository.findAll(pageable = pageable)
|
||||
} else {
|
||||
collectionRepository.findAllByLibraryIds(principal.user.sharedLibrariesIds, principal.user.sharedLibrariesIds, pageable = pageable)
|
||||
}
|
||||
val collections = collectionRepository.findAll(principal.user.getAuthorizedLibraryIds(null), principal.user.getAuthorizedLibraryIds(null), pageable = pageable)
|
||||
|
||||
val uriBuilder = uriBuilder(ROUTE_COLLECTIONS_ALL)
|
||||
|
||||
@ -450,12 +448,7 @@ class OpdsController(
|
||||
@Parameter(hidden = true) page: Pageable,
|
||||
): OpdsFeed {
|
||||
val pageable = PageRequest.of(page.pageNumber, page.pageSize, Sort.by(Sort.Order.asc("name")))
|
||||
val readLists =
|
||||
if (principal.user.sharedAllLibraries) {
|
||||
readListRepository.findAll(pageable = pageable)
|
||||
} else {
|
||||
readListRepository.findAllByLibraryIds(principal.user.sharedLibrariesIds, principal.user.sharedLibrariesIds, pageable = pageable)
|
||||
}
|
||||
val readLists = readListRepository.findAll(principal.user.getAuthorizedLibraryIds(null), principal.user.getAuthorizedLibraryIds(null), pageable = pageable)
|
||||
|
||||
val uriBuilder = uriBuilder(ROUTE_READLISTS_ALL)
|
||||
|
||||
@ -514,7 +507,7 @@ class OpdsController(
|
||||
@Parameter(hidden = true) page: Pageable,
|
||||
): OpdsFeed =
|
||||
seriesDtoRepository.findByIdOrNull(id, principal.user.id)?.let { series ->
|
||||
if (!principal.user.canAccessLibrary(series.libraryId)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
principal.user.checkContentRestriction(series)
|
||||
|
||||
val bookSearch = BookSearchWithReadProgress(
|
||||
seriesIds = listOf(id),
|
||||
@ -559,7 +552,7 @@ class OpdsController(
|
||||
|
||||
val pageable = PageRequest.of(page.pageNumber, page.pageSize, Sort.by(Sort.Order.asc("metadata.titleSort")))
|
||||
|
||||
val entries = seriesDtoRepository.findAll(seriesSearch, principal.user.id, pageable)
|
||||
val entries = seriesDtoRepository.findAll(seriesSearch, principal.user.id, pageable, principal.user.restrictions)
|
||||
.map { it.toOpdsEntry() }
|
||||
|
||||
val uriBuilder = uriBuilder("libraries/$id")
|
||||
@ -597,7 +590,7 @@ class OpdsController(
|
||||
deleted = false,
|
||||
)
|
||||
|
||||
val entries = seriesDtoRepository.findAllByCollectionId(collection.id, seriesSearch, principal.user.id, pageable)
|
||||
val entries = seriesDtoRepository.findAllByCollectionId(collection.id, seriesSearch, principal.user.id, pageable, principal.user.restrictions)
|
||||
.map { seriesDto ->
|
||||
val index = if (shouldEnforceSort(userAgent)) collection.seriesIds.indexOf(seriesDto.id) + 1 else null
|
||||
seriesDto.toOpdsEntry(index)
|
||||
|
@ -1,12 +1,13 @@
|
||||
package org.gotson.komga.interfaces.api.persistence
|
||||
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.ContentRestriction
|
||||
import org.gotson.komga.interfaces.api.rest.dto.BookDto
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
|
||||
interface BookDtoRepository {
|
||||
fun findAll(search: BookSearchWithReadProgress, userId: String, pageable: Pageable): Page<BookDto>
|
||||
fun findAll(search: BookSearchWithReadProgress, userId: String, pageable: Pageable, restrictions: Set<ContentRestriction> = emptySet()): Page<BookDto>
|
||||
|
||||
/**
|
||||
* Find books that are part of a readlist, optionally filtered by library
|
||||
@ -38,7 +39,7 @@ interface BookDtoRepository {
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): BookDto?
|
||||
|
||||
fun findAllOnDeck(userId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<BookDto>
|
||||
fun findAllOnDeck(userId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable, restrictions: Set<ContentRestriction> = emptySet()): Page<BookDto>
|
||||
|
||||
fun findAllDuplicates(userId: String, pageable: Pageable): Page<BookDto>
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.gotson.komga.interfaces.api.persistence
|
||||
|
||||
import org.gotson.komga.domain.model.ContentRestriction
|
||||
import org.gotson.komga.domain.model.SeriesSearchWithReadProgress
|
||||
import org.gotson.komga.interfaces.api.rest.dto.GroupCountDto
|
||||
import org.gotson.komga.interfaces.api.rest.dto.SeriesDto
|
||||
@ -9,9 +10,9 @@ import org.springframework.data.domain.Pageable
|
||||
interface SeriesDtoRepository {
|
||||
fun findByIdOrNull(seriesId: String, userId: String): SeriesDto?
|
||||
|
||||
fun findAll(search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable): Page<SeriesDto>
|
||||
fun findAllByCollectionId(collectionId: String, search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable): Page<SeriesDto>
|
||||
fun findAllRecentlyUpdated(search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable): Page<SeriesDto>
|
||||
fun findAll(search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable, restrictions: Set<ContentRestriction> = emptySet()): Page<SeriesDto>
|
||||
fun findAllByCollectionId(collectionId: String, search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable, restrictions: Set<ContentRestriction> = emptySet()): Page<SeriesDto>
|
||||
fun findAllRecentlyUpdated(search: SeriesSearchWithReadProgress, userId: String, restrictions: Set<ContentRestriction>, pageable: Pageable): Page<SeriesDto>
|
||||
|
||||
fun countByFirstCharacter(search: SeriesSearchWithReadProgress, userId: String): List<GroupCountDto>
|
||||
fun countByFirstCharacter(search: SeriesSearchWithReadProgress, userId: String, restrictions: Set<ContentRestriction>): List<GroupCountDto>
|
||||
}
|
||||
|
@ -11,9 +11,11 @@ import org.gotson.komga.application.events.EventPublisher
|
||||
import org.gotson.komga.application.tasks.HIGHEST_PRIORITY
|
||||
import org.gotson.komga.application.tasks.HIGH_PRIORITY
|
||||
import org.gotson.komga.application.tasks.TaskEmitter
|
||||
import org.gotson.komga.domain.model.Book
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.DomainEvent
|
||||
import org.gotson.komga.domain.model.ImageConversionException
|
||||
import org.gotson.komga.domain.model.KomgaUser
|
||||
import org.gotson.komga.domain.model.MarkSelectedPreference
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.MediaNotReadyException
|
||||
@ -26,6 +28,7 @@ import org.gotson.komga.domain.persistence.BookMetadataRepository
|
||||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
import org.gotson.komga.domain.persistence.MediaRepository
|
||||
import org.gotson.komga.domain.persistence.ReadListRepository
|
||||
import org.gotson.komga.domain.persistence.SeriesMetadataRepository
|
||||
import org.gotson.komga.domain.persistence.ThumbnailBookRepository
|
||||
import org.gotson.komga.domain.service.BookLifecycle
|
||||
import org.gotson.komga.infrastructure.image.ImageType
|
||||
@ -93,6 +96,7 @@ class BookController(
|
||||
private val bookLifecycle: BookLifecycle,
|
||||
private val bookRepository: BookRepository,
|
||||
private val bookMetadataRepository: BookMetadataRepository,
|
||||
private val seriesMetadataRepository: SeriesMetadataRepository,
|
||||
private val mediaRepository: MediaRepository,
|
||||
private val bookDtoRepository: BookDtoRepository,
|
||||
private val readListRepository: ReadListRepository,
|
||||
@ -138,7 +142,7 @@ class BookController(
|
||||
tags = tags,
|
||||
)
|
||||
|
||||
return bookDtoRepository.findAll(bookSearch, principal.user.id, pageRequest)
|
||||
return bookDtoRepository.findAll(bookSearch, principal.user.id, pageRequest, principal.user.restrictions)
|
||||
.map { it.restrictUrl(!principal.user.roleAdmin) }
|
||||
}
|
||||
|
||||
@ -166,6 +170,7 @@ class BookController(
|
||||
),
|
||||
principal.user.id,
|
||||
pageRequest,
|
||||
principal.user.restrictions,
|
||||
).map { it.restrictUrl(!principal.user.roleAdmin) }
|
||||
}
|
||||
|
||||
@ -181,6 +186,7 @@ class BookController(
|
||||
principal.user.id,
|
||||
principal.user.getAuthorizedLibraryIds(libraryIds),
|
||||
page,
|
||||
principal.user.restrictions,
|
||||
).map { it.restrictUrl(!principal.user.roleAdmin) }
|
||||
|
||||
@PageableAsQueryParam
|
||||
@ -214,7 +220,8 @@ class BookController(
|
||||
@PathVariable bookId: String,
|
||||
): BookDto =
|
||||
bookDtoRepository.findByIdOrNull(bookId, principal.user.id)?.let {
|
||||
if (!principal.user.canAccessLibrary(it.libraryId)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
principal.user.checkContentRestriction(it)
|
||||
|
||||
it.restrictUrl(!principal.user.roleAdmin)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
@ -223,9 +230,7 @@ class BookController(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable bookId: String,
|
||||
): BookDto {
|
||||
bookRepository.getLibraryIdOrNull(bookId)?.let {
|
||||
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
principal.user.checkContentRestriction(bookId)
|
||||
|
||||
return bookDtoRepository.findPreviousInSeriesOrNull(bookId, principal.user.id)
|
||||
?.restrictUrl(!principal.user.roleAdmin)
|
||||
@ -237,9 +242,7 @@ class BookController(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable bookId: String,
|
||||
): BookDto {
|
||||
bookRepository.getLibraryIdOrNull(bookId)?.let {
|
||||
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
principal.user.checkContentRestriction(bookId)
|
||||
|
||||
return bookDtoRepository.findNextInSeriesOrNull(bookId, principal.user.id)
|
||||
?.restrictUrl(!principal.user.roleAdmin)
|
||||
@ -251,11 +254,9 @@ class BookController(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable(name = "bookId") bookId: String,
|
||||
): List<ReadListDto> {
|
||||
bookRepository.getLibraryIdOrNull(bookId)?.let {
|
||||
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
principal.user.checkContentRestriction(bookId)
|
||||
|
||||
return readListRepository.findAllContainingBookId(bookId, principal.user.getAuthorizedLibraryIds(null))
|
||||
return readListRepository.findAllContainingBookId(bookId, principal.user.getAuthorizedLibraryIds(null), principal.user.restrictions)
|
||||
.map { it.toDto() }
|
||||
}
|
||||
|
||||
@ -271,9 +272,7 @@ class BookController(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable bookId: String,
|
||||
): ByteArray {
|
||||
bookRepository.getLibraryIdOrNull(bookId)?.let {
|
||||
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
principal.user.checkContentRestriction(bookId)
|
||||
|
||||
return bookLifecycle.getThumbnailBytes(bookId) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
@ -285,9 +284,7 @@ class BookController(
|
||||
@PathVariable(name = "bookId") bookId: String,
|
||||
@PathVariable(name = "thumbnailId") thumbnailId: String,
|
||||
): ByteArray {
|
||||
bookRepository.getLibraryIdOrNull(bookId)?.let {
|
||||
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
principal.user.checkContentRestriction(bookId)
|
||||
|
||||
return bookLifecycle.getThumbnailBytesByThumbnailId(thumbnailId)
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
@ -298,9 +295,7 @@ class BookController(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable(name = "bookId") bookId: String,
|
||||
): Collection<ThumbnailBookDto> {
|
||||
bookRepository.getLibraryIdOrNull(bookId)?.let {
|
||||
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
principal.user.checkContentRestriction(bookId)
|
||||
|
||||
return thumbnailBookRepository.findAllByBookId(bookId)
|
||||
.map { it.toDto() }
|
||||
@ -316,7 +311,6 @@ class BookController(
|
||||
@RequestParam("selected") selected: Boolean = true,
|
||||
) {
|
||||
val book = bookRepository.findByIdOrNull(bookId) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
|
||||
if (!contentDetector.isImage(file.inputStream.buffered().use { contentDetector.detectMediaType(it) }))
|
||||
throw ResponseStatusException(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
|
||||
@ -340,10 +334,6 @@ class BookController(
|
||||
@PathVariable(name = "bookId") bookId: String,
|
||||
@PathVariable(name = "thumbnailId") thumbnailId: String,
|
||||
) {
|
||||
bookRepository.findByIdOrNull(bookId)?.let { book ->
|
||||
if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
thumbnailBookRepository.findByIdOrNull(thumbnailId)?.let {
|
||||
thumbnailBookRepository.markSelected(it)
|
||||
eventPublisher.publishEvent(DomainEvent.ThumbnailBookAdded(it.copy(selected = true)))
|
||||
@ -358,10 +348,6 @@ class BookController(
|
||||
@PathVariable(name = "bookId") bookId: String,
|
||||
@PathVariable(name = "thumbnailId") thumbnailId: String,
|
||||
) {
|
||||
bookRepository.findByIdOrNull(bookId)?.let { book ->
|
||||
if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
thumbnailBookRepository.findByIdOrNull(thumbnailId)?.let {
|
||||
try {
|
||||
bookLifecycle.deleteThumbnailForBook(it)
|
||||
@ -386,7 +372,7 @@ class BookController(
|
||||
@PathVariable bookId: String,
|
||||
): ResponseEntity<StreamingResponseBody> =
|
||||
bookRepository.findByIdOrNull(bookId)?.let { book ->
|
||||
if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
principal.user.checkContentRestriction(book)
|
||||
try {
|
||||
val media = mediaRepository.findById(book.id)
|
||||
with(FileSystemResource(book.path)) {
|
||||
@ -421,7 +407,7 @@ class BookController(
|
||||
@PathVariable bookId: String,
|
||||
): List<PageDto> =
|
||||
bookRepository.findByIdOrNull(bookId)?.let { book ->
|
||||
if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
principal.user.checkContentRestriction(book)
|
||||
|
||||
val media = mediaRepository.findById(book.id)
|
||||
when (media.status) {
|
||||
@ -482,7 +468,9 @@ class BookController(
|
||||
.setNotModified(media)
|
||||
.body(ByteArray(0))
|
||||
}
|
||||
if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
|
||||
principal.user.checkContentRestriction(book)
|
||||
|
||||
try {
|
||||
val convertFormat = when (convertTo?.lowercase()) {
|
||||
"jpeg" -> ImageType.JPEG
|
||||
@ -539,7 +527,9 @@ class BookController(
|
||||
.setNotModified(media)
|
||||
.body(ByteArray(0))
|
||||
}
|
||||
if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
|
||||
principal.user.checkContentRestriction(book)
|
||||
|
||||
try {
|
||||
val pageContent = bookLifecycle.getBookPage(book, pageNumber, resizeTo = 300)
|
||||
|
||||
@ -626,7 +616,7 @@ class BookController(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
) {
|
||||
bookRepository.findByIdOrNull(bookId)?.let { book ->
|
||||
if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
principal.user.checkContentRestriction(book)
|
||||
|
||||
try {
|
||||
if (readProgress.completed != null && readProgress.completed)
|
||||
@ -647,7 +637,7 @@ class BookController(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
) {
|
||||
bookRepository.findByIdOrNull(bookId)?.let { book ->
|
||||
if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
principal.user.checkContentRestriction(book)
|
||||
|
||||
bookLifecycle.deleteReadProgress(book, principal.user)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
@ -692,4 +682,49 @@ class BookController(
|
||||
|
||||
private fun getBookLastModified(media: Media) =
|
||||
media.lastModifiedDate.toInstant(ZoneOffset.UTC).toEpochMilli()
|
||||
|
||||
/**
|
||||
* Convenience function to check for content restriction.
|
||||
* This will retrieve data from repositories if needed.
|
||||
*
|
||||
* @throws[ResponseStatusException] if the user cannot access the content
|
||||
*/
|
||||
private fun KomgaUser.checkContentRestriction(book: BookDto) {
|
||||
if (!canAccessLibrary(book.libraryId)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
if (restrictions.isNotEmpty()) seriesMetadataRepository.findById(book.seriesId).let {
|
||||
if (isContentRestricted(ageRating = it.ageRating)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to check for content restriction.
|
||||
* This will retrieve data from repositories if needed.
|
||||
*
|
||||
* @throws[ResponseStatusException] if the user cannot access the content
|
||||
*/
|
||||
private fun KomgaUser.checkContentRestriction(book: Book) {
|
||||
if (!canAccessLibrary(book.libraryId)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
if (restrictions.isNotEmpty()) seriesMetadataRepository.findById(book.seriesId).let {
|
||||
if (isContentRestricted(ageRating = it.ageRating)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to check for content restriction.
|
||||
* This will retrieve data from repositories if needed.
|
||||
*
|
||||
* @throws[ResponseStatusException] if the user cannot access the content
|
||||
*/
|
||||
private fun KomgaUser.checkContentRestriction(bookId: String) {
|
||||
if (!sharedAllLibraries) {
|
||||
bookRepository.getLibraryIdOrNull(bookId)?.let {
|
||||
if (!canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
if (restrictions.isNotEmpty()) bookRepository.getSeriesIdOrNull(bookId)?.let { seriesId ->
|
||||
seriesMetadataRepository.findById(seriesId).let {
|
||||
if (isContentRestricted(ageRating = it.ageRating)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
}
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
}
|
||||
|
@ -114,30 +114,8 @@ class ReadListController(
|
||||
sort,
|
||||
)
|
||||
|
||||
return when {
|
||||
principal.user.sharedAllLibraries && libraryIds == null -> readListRepository.findAll(
|
||||
searchTerm,
|
||||
pageable = pageRequest,
|
||||
)
|
||||
principal.user.sharedAllLibraries && libraryIds != null -> readListRepository.findAllByLibraryIds(
|
||||
libraryIds,
|
||||
null,
|
||||
searchTerm,
|
||||
pageable = pageRequest,
|
||||
)
|
||||
!principal.user.sharedAllLibraries && libraryIds != null -> readListRepository.findAllByLibraryIds(
|
||||
libraryIds,
|
||||
principal.user.sharedLibrariesIds,
|
||||
searchTerm,
|
||||
pageable = pageRequest,
|
||||
)
|
||||
else -> readListRepository.findAllByLibraryIds(
|
||||
principal.user.sharedLibrariesIds,
|
||||
principal.user.sharedLibrariesIds,
|
||||
searchTerm,
|
||||
pageable = pageRequest,
|
||||
)
|
||||
}.map { it.toDto() }
|
||||
return readListRepository.findAll(principal.user.getAuthorizedLibraryIds(libraryIds), principal.user.getAuthorizedLibraryIds(null), searchTerm, pageRequest, principal.user.restrictions)
|
||||
.map { it.toDto() }
|
||||
}
|
||||
|
||||
@GetMapping("{id}")
|
||||
@ -145,7 +123,7 @@ class ReadListController(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable id: String,
|
||||
): ReadListDto =
|
||||
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))
|
||||
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null), principal.user.restrictions)
|
||||
?.toDto()
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
@ -155,7 +133,7 @@ class ReadListController(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable id: String,
|
||||
): ResponseEntity<ByteArray> {
|
||||
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let {
|
||||
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null), principal.user.restrictions)?.let {
|
||||
return ResponseEntity.ok()
|
||||
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePrivate())
|
||||
.body(readListLifecycle.getThumbnailBytes(it))
|
||||
@ -169,7 +147,7 @@ class ReadListController(
|
||||
@PathVariable(name = "id") id: String,
|
||||
@PathVariable(name = "thumbnailId") thumbnailId: String,
|
||||
): ByteArray {
|
||||
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let {
|
||||
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null), principal.user.restrictions)?.let {
|
||||
return readListLifecycle.getThumbnailBytes(thumbnailId)
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
@ -180,7 +158,7 @@ class ReadListController(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable(name = "id") id: String,
|
||||
): Collection<ThumbnailReadListDto> {
|
||||
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let {
|
||||
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null), principal.user.restrictions)?.let {
|
||||
return thumbnailReadListRepository.findAllByReadListId(id).map { it.toDto() }
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
@ -194,7 +172,7 @@ class ReadListController(
|
||||
@RequestParam("file") file: MultipartFile,
|
||||
@RequestParam("selected") selected: Boolean = true,
|
||||
) {
|
||||
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let { readList ->
|
||||
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null), principal.user.restrictions)?.let { readList ->
|
||||
|
||||
if (!contentDetector.isImage(file.inputStream.buffered().use { contentDetector.detectMediaType(it) }))
|
||||
throw ResponseStatusException(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
|
||||
@ -218,7 +196,7 @@ class ReadListController(
|
||||
@PathVariable(name = "id") id: String,
|
||||
@PathVariable(name = "thumbnailId") thumbnailId: String,
|
||||
) {
|
||||
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let {
|
||||
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null), principal.user.restrictions)?.let {
|
||||
thumbnailReadListRepository.findByIdOrNull(thumbnailId)?.let {
|
||||
readListLifecycle.markSelectedThumbnail(it)
|
||||
eventPublisher.publishEvent(DomainEvent.ThumbnailReadListAdded(it.copy(selected = true)))
|
||||
@ -234,7 +212,7 @@ class ReadListController(
|
||||
@PathVariable(name = "id") id: String,
|
||||
@PathVariable(name = "thumbnailId") thumbnailId: String,
|
||||
) {
|
||||
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let {
|
||||
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null), principal.user.restrictions)?.let {
|
||||
thumbnailReadListRepository.findByIdOrNull(thumbnailId)?.let {
|
||||
readListLifecycle.deleteThumbnail(it)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
@ -93,12 +93,8 @@ class SeriesCollectionController(
|
||||
sort,
|
||||
)
|
||||
|
||||
return when {
|
||||
principal.user.sharedAllLibraries && libraryIds == null -> collectionRepository.findAll(searchTerm, pageable = pageRequest)
|
||||
principal.user.sharedAllLibraries && libraryIds != null -> collectionRepository.findAllByLibraryIds(libraryIds, null, searchTerm, pageable = pageRequest)
|
||||
!principal.user.sharedAllLibraries && libraryIds != null -> collectionRepository.findAllByLibraryIds(libraryIds, principal.user.sharedLibrariesIds, searchTerm, pageable = pageRequest)
|
||||
else -> collectionRepository.findAllByLibraryIds(principal.user.sharedLibrariesIds, principal.user.sharedLibrariesIds, searchTerm, pageable = pageRequest)
|
||||
}.map { it.toDto() }
|
||||
return collectionRepository.findAll(principal.user.getAuthorizedLibraryIds(libraryIds), principal.user.getAuthorizedLibraryIds(null), searchTerm, pageRequest, principal.user.restrictions)
|
||||
.map { it.toDto() }
|
||||
}
|
||||
|
||||
@GetMapping("{id}")
|
||||
@ -106,7 +102,7 @@ class SeriesCollectionController(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable id: String,
|
||||
): CollectionDto =
|
||||
collectionRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))
|
||||
collectionRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null), principal.user.restrictions)
|
||||
?.toDto()
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
@ -116,7 +112,7 @@ class SeriesCollectionController(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable id: String,
|
||||
): ResponseEntity<ByteArray> {
|
||||
collectionRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let {
|
||||
collectionRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null), principal.user.restrictions)?.let {
|
||||
return ResponseEntity.ok()
|
||||
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePrivate())
|
||||
.body(collectionLifecycle.getThumbnailBytes(it, principal.user.id))
|
||||
@ -130,7 +126,7 @@ class SeriesCollectionController(
|
||||
@PathVariable(name = "id") id: String,
|
||||
@PathVariable(name = "thumbnailId") thumbnailId: String,
|
||||
): ByteArray {
|
||||
collectionRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let {
|
||||
collectionRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null), principal.user.restrictions)?.let {
|
||||
return collectionLifecycle.getThumbnailBytes(thumbnailId)
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
@ -141,7 +137,7 @@ class SeriesCollectionController(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable(name = "id") id: String,
|
||||
): Collection<ThumbnailSeriesCollectionDto> {
|
||||
collectionRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let {
|
||||
collectionRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null), principal.user.restrictions)?.let {
|
||||
return thumbnailSeriesCollectionRepository.findAllByCollectionId(id).map { it.toDto() }
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
@ -272,7 +268,7 @@ class SeriesCollectionController(
|
||||
@Parameter(hidden = true) @Authors authors: List<Author>?,
|
||||
@Parameter(hidden = true) page: Pageable,
|
||||
): Page<SeriesDto> =
|
||||
collectionRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let { collection ->
|
||||
collectionRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null), principal.user.restrictions)?.let { collection ->
|
||||
val sort =
|
||||
if (collection.ordered) Sort.by(Sort.Order.asc("collection.number"))
|
||||
else Sort.by(Sort.Order.asc("metadata.titleSort"))
|
||||
@ -300,7 +296,7 @@ class SeriesCollectionController(
|
||||
authors = authors,
|
||||
)
|
||||
|
||||
seriesDtoRepository.findAllByCollectionId(collection.id, seriesSearch, principal.user.id, pageRequest)
|
||||
seriesDtoRepository.findAllByCollectionId(collection.id, seriesSearch, principal.user.id, pageRequest, principal.user.restrictions)
|
||||
.map { it.restrictUrl(!principal.user.roleAdmin) }
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import org.gotson.komga.application.tasks.TaskEmitter
|
||||
import org.gotson.komga.domain.model.Author
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.DomainEvent
|
||||
import org.gotson.komga.domain.model.KomgaUser
|
||||
import org.gotson.komga.domain.model.MarkSelectedPreference
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.MediaType.ZIP
|
||||
@ -43,6 +44,7 @@ import org.gotson.komga.infrastructure.swagger.PageableAsQueryParam
|
||||
import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam
|
||||
import org.gotson.komga.infrastructure.web.Authors
|
||||
import org.gotson.komga.infrastructure.web.DelimitedPair
|
||||
import org.gotson.komga.interfaces.api.checkContentRestriction
|
||||
import org.gotson.komga.interfaces.api.persistence.BookDtoRepository
|
||||
import org.gotson.komga.interfaces.api.persistence.ReadProgressDtoRepository
|
||||
import org.gotson.komga.interfaces.api.persistence.SeriesDtoRepository
|
||||
@ -176,7 +178,7 @@ class SeriesController(
|
||||
authors = authors,
|
||||
)
|
||||
|
||||
return seriesDtoRepository.findAll(seriesSearch, principal.user.id, pageRequest)
|
||||
return seriesDtoRepository.findAll(seriesSearch, principal.user.id, pageRequest, principal.user.restrictions)
|
||||
.map { it.restrictUrl(!principal.user.roleAdmin) }
|
||||
}
|
||||
|
||||
@ -231,7 +233,7 @@ class SeriesController(
|
||||
authors = authors,
|
||||
)
|
||||
|
||||
return seriesDtoRepository.countByFirstCharacter(seriesSearch, principal.user.id)
|
||||
return seriesDtoRepository.countByFirstCharacter(seriesSearch, principal.user.id, principal.user.restrictions)
|
||||
}
|
||||
|
||||
@Operation(description = "Return recently added or updated series.")
|
||||
@ -261,6 +263,7 @@ class SeriesController(
|
||||
),
|
||||
principal.user.id,
|
||||
pageRequest,
|
||||
principal.user.restrictions,
|
||||
).map { it.restrictUrl(!principal.user.roleAdmin) }
|
||||
}
|
||||
|
||||
@ -291,6 +294,7 @@ class SeriesController(
|
||||
),
|
||||
principal.user.id,
|
||||
pageRequest,
|
||||
principal.user.restrictions,
|
||||
).map { it.restrictUrl(!principal.user.roleAdmin) }
|
||||
}
|
||||
|
||||
@ -320,6 +324,7 @@ class SeriesController(
|
||||
deleted = deleted,
|
||||
),
|
||||
principal.user.id,
|
||||
principal.user.restrictions,
|
||||
pageRequest,
|
||||
).map { it.restrictUrl(!principal.user.roleAdmin) }
|
||||
}
|
||||
@ -330,7 +335,7 @@ class SeriesController(
|
||||
@PathVariable(name = "seriesId") id: String,
|
||||
): SeriesDto =
|
||||
seriesDtoRepository.findByIdOrNull(id, principal.user.id)?.let {
|
||||
if (!principal.user.canAccessLibrary(it.libraryId)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
principal.user.checkContentRestriction(it)
|
||||
it.restrictUrl(!principal.user.roleAdmin)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
@ -340,9 +345,7 @@ class SeriesController(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable(name = "seriesId") seriesId: String,
|
||||
): ByteArray {
|
||||
seriesRepository.getLibraryId(seriesId)?.let {
|
||||
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
principal.user.checkContentRestriction(seriesId)
|
||||
|
||||
return seriesLifecycle.getThumbnailBytes(seriesId, principal.user.id)
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
@ -355,9 +358,7 @@ class SeriesController(
|
||||
@PathVariable(name = "seriesId") seriesId: String,
|
||||
@PathVariable(name = "thumbnailId") thumbnailId: String,
|
||||
): ByteArray {
|
||||
seriesRepository.getLibraryId(seriesId)?.let {
|
||||
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
principal.user.checkContentRestriction(seriesId)
|
||||
|
||||
return seriesLifecycle.getThumbnailBytesByThumbnailId(thumbnailId)
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
@ -368,9 +369,7 @@ class SeriesController(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable(name = "seriesId") seriesId: String,
|
||||
): Collection<SeriesThumbnailDto> {
|
||||
seriesRepository.getLibraryId(seriesId)?.let {
|
||||
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
principal.user.checkContentRestriction(seriesId)
|
||||
|
||||
return thumbnailsSeriesRepository.findAllBySeriesId(seriesId)
|
||||
.map { it.toDto() }
|
||||
@ -443,9 +442,8 @@ class SeriesController(
|
||||
@Parameter(hidden = true) @Authors authors: List<Author>?,
|
||||
@Parameter(hidden = true) page: Pageable,
|
||||
): Page<BookDto> {
|
||||
seriesRepository.getLibraryId(seriesId)?.let {
|
||||
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
principal.user.checkContentRestriction(seriesId)
|
||||
|
||||
val sort =
|
||||
if (page.sort.isSorted) page.sort
|
||||
else Sort.by(Sort.Order.asc("metadata.numberSort"))
|
||||
@ -477,11 +475,9 @@ class SeriesController(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable(name = "seriesId") seriesId: String,
|
||||
): List<CollectionDto> {
|
||||
seriesRepository.getLibraryId(seriesId)?.let {
|
||||
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
principal.user.checkContentRestriction(seriesId)
|
||||
|
||||
return collectionRepository.findAllContainingSeriesId(seriesId, principal.user.getAuthorizedLibraryIds(null))
|
||||
return collectionRepository.findAllContainingSeriesId(seriesId, principal.user.getAuthorizedLibraryIds(null), principal.user.restrictions)
|
||||
.map { it.toDto() }
|
||||
}
|
||||
|
||||
@ -557,9 +553,7 @@ class SeriesController(
|
||||
@PathVariable seriesId: String,
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
) {
|
||||
seriesRepository.getLibraryId(seriesId)?.let {
|
||||
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
principal.user.checkContentRestriction(seriesId)
|
||||
|
||||
seriesLifecycle.markReadProgressCompleted(seriesId, principal.user)
|
||||
}
|
||||
@ -571,9 +565,7 @@ class SeriesController(
|
||||
@PathVariable seriesId: String,
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
) {
|
||||
seriesRepository.getLibraryId(seriesId)?.let {
|
||||
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
principal.user.checkContentRestriction(seriesId)
|
||||
|
||||
seriesLifecycle.deleteReadProgress(seriesId, principal.user)
|
||||
}
|
||||
@ -583,21 +575,21 @@ class SeriesController(
|
||||
fun getReadProgressTachiyomi(
|
||||
@PathVariable seriesId: String,
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
): TachiyomiReadProgressDto =
|
||||
seriesRepository.getLibraryId(seriesId)?.let {
|
||||
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
return readProgressDtoRepository.findProgressBySeries(seriesId, principal.user.id)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
): TachiyomiReadProgressDto {
|
||||
principal.user.checkContentRestriction(seriesId)
|
||||
|
||||
return readProgressDtoRepository.findProgressBySeries(seriesId, principal.user.id)
|
||||
}
|
||||
|
||||
@GetMapping("v2/series/{seriesId}/read-progress/tachiyomi")
|
||||
fun getReadProgressTachiyomiV2(
|
||||
@PathVariable seriesId: String,
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
): TachiyomiReadProgressV2Dto =
|
||||
seriesRepository.getLibraryId(seriesId)?.let {
|
||||
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
return readProgressDtoRepository.findProgressV2BySeries(seriesId, principal.user.id)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
): TachiyomiReadProgressV2Dto {
|
||||
principal.user.checkContentRestriction(seriesId)
|
||||
|
||||
return readProgressDtoRepository.findProgressV2BySeries(seriesId, principal.user.id)
|
||||
}
|
||||
|
||||
@Deprecated("Use v2 for proper handling of chapter number with numberSort")
|
||||
@PutMapping("v1/series/{seriesId}/read-progress/tachiyomi")
|
||||
@ -607,9 +599,7 @@ class SeriesController(
|
||||
@Valid @RequestBody readProgress: TachiyomiReadProgressUpdateDto,
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
) {
|
||||
seriesRepository.getLibraryId(seriesId)?.let {
|
||||
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
principal.user.checkContentRestriction(seriesId)
|
||||
|
||||
bookDtoRepository.findAll(
|
||||
BookSearchWithReadProgress(seriesIds = listOf(seriesId)),
|
||||
@ -629,9 +619,7 @@ class SeriesController(
|
||||
@RequestBody readProgress: TachiyomiReadProgressUpdateV2Dto,
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
) {
|
||||
seriesRepository.getLibraryId(seriesId)?.let {
|
||||
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
principal.user.checkContentRestriction(seriesId)
|
||||
|
||||
bookDtoRepository.findAll(
|
||||
BookSearchWithReadProgress(seriesIds = listOf(seriesId)),
|
||||
@ -650,9 +638,7 @@ class SeriesController(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable seriesId: String,
|
||||
): ResponseEntity<StreamingResponseBody> {
|
||||
seriesRepository.getLibraryId(seriesId)?.let {
|
||||
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
principal.user.checkContentRestriction(seriesId)
|
||||
|
||||
val books = bookRepository.findAllBySeriesId(seriesId)
|
||||
|
||||
@ -700,4 +686,21 @@ class SeriesController(
|
||||
priority = HIGHEST_PRIORITY,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to check for content restriction.
|
||||
* This will retrieve data from repositories if needed.
|
||||
*
|
||||
* @throws[ResponseStatusException] if the user cannot access the content
|
||||
*/
|
||||
private fun KomgaUser.checkContentRestriction(seriesId: String) {
|
||||
if (!sharedAllLibraries) {
|
||||
seriesRepository.getLibraryId(seriesId)?.let {
|
||||
if (!canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
if (restrictions.isNotEmpty()) seriesMetadataRepository.findById(seriesId).let {
|
||||
if (isContentRestricted(ageRating = it.ageRating)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,73 @@
|
||||
package org.gotson.komga.domain.model
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.gotson.komga.domain.model.ContentRestriction.AgeRestriction
|
||||
import org.gotson.komga.domain.model.ContentRestriction.LabelsRestriction
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class KomgaUserTest {
|
||||
|
||||
val defaultUser = KomgaUser("user@example.org", "aPassword", false)
|
||||
|
||||
@Nested
|
||||
inner class ContentRestriction {
|
||||
|
||||
@Test
|
||||
fun `given user with age AllowOnlyUnder restriction when checking for content restriction then it is accurate`() {
|
||||
val user = defaultUser.copy(restrictions = setOf(AgeRestriction.AllowOnlyUnder(5)))
|
||||
|
||||
assertThat(user.isContentRestricted(ageRating = 3)).isFalse
|
||||
assertThat(user.isContentRestricted(ageRating = 5)).isFalse
|
||||
assertThat(user.isContentRestricted(ageRating = 8)).isTrue
|
||||
assertThat(user.isContentRestricted(ageRating = null)).isTrue
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given user with age ExcludeOver restriction when checking for content restriction then it is accurate`() {
|
||||
val user = defaultUser.copy(restrictions = setOf(AgeRestriction.ExcludeOver(16)))
|
||||
|
||||
assertThat(user.isContentRestricted(ageRating = 10)).isFalse
|
||||
assertThat(user.isContentRestricted(ageRating = null)).isFalse
|
||||
assertThat(user.isContentRestricted(ageRating = 16)).isTrue
|
||||
assertThat(user.isContentRestricted(ageRating = 18)).isTrue
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given user with sharing label AllowOnly restriction when checking for content restriction then it is accurate`() {
|
||||
val user = defaultUser.copy(restrictions = setOf(LabelsRestriction.AllowOnly(setOf("allow", "this"))))
|
||||
|
||||
assertThat(user.isContentRestricted(sharingLabels = setOf("allow"))).isFalse
|
||||
assertThat(user.isContentRestricted(sharingLabels = setOf("this"))).isFalse
|
||||
assertThat(user.isContentRestricted(sharingLabels = setOf("allow", "this"))).isFalse
|
||||
assertThat(user.isContentRestricted(sharingLabels = setOf("other"))).isTrue
|
||||
assertThat(user.isContentRestricted(sharingLabels = emptySet())).isTrue
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given user with sharing label Exclude restriction when checking for content restriction then it is accurate`() {
|
||||
val user = defaultUser.copy(restrictions = setOf(LabelsRestriction.Exclude(setOf("exclude", "this"))))
|
||||
|
||||
assertThat(user.isContentRestricted(sharingLabels = emptySet())).isFalse
|
||||
assertThat(user.isContentRestricted(sharingLabels = setOf("allow"))).isFalse
|
||||
assertThat(user.isContentRestricted(sharingLabels = setOf("other", "this"))).isTrue
|
||||
assertThat(user.isContentRestricted(sharingLabels = setOf("this"))).isTrue
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given user with both sharing label AllowOnly and Exclude restriction when checking for content restriction then it is accurate`() {
|
||||
val user = defaultUser.copy(
|
||||
restrictions = setOf(
|
||||
LabelsRestriction.AllowOnly(setOf("allow", "both")),
|
||||
LabelsRestriction.Exclude(setOf("exclude", "both")),
|
||||
)
|
||||
)
|
||||
|
||||
assertThat(user.isContentRestricted(sharingLabels = setOf("allow"))).isFalse
|
||||
assertThat(user.isContentRestricted(sharingLabels = setOf("allow", "other"))).isFalse
|
||||
assertThat(user.isContentRestricted(sharingLabels = setOf("allow", "both"))).isTrue
|
||||
assertThat(user.isContentRestricted(sharingLabels = setOf("exclude"))).isTrue
|
||||
assertThat(user.isContentRestricted(sharingLabels = emptySet())).isTrue
|
||||
}
|
||||
}
|
||||
}
|
@ -1628,8 +1628,8 @@ class LibraryContentLifecycleTest(
|
||||
libraryContentLifecycle.emptyTrash(library)
|
||||
|
||||
// then
|
||||
val collections = collectionRepository.findAll(null, Pageable.unpaged())
|
||||
val readLists = readListRepository.findAll(null, Pageable.unpaged())
|
||||
val collections = collectionRepository.findAll(pageable = Pageable.unpaged())
|
||||
val readLists = readListRepository.findAll(pageable = Pageable.unpaged())
|
||||
|
||||
assertThat(collections.content).isEmpty()
|
||||
assertThat(readLists.content).isEmpty()
|
||||
|
@ -180,11 +180,11 @@ class ReadListDaoTest(
|
||||
)
|
||||
|
||||
// when
|
||||
val foundLibrary1Filtered = readListDao.findAllByLibraryIds(listOf(library.id), listOf(library.id), pageable = Pageable.unpaged()).content
|
||||
val foundLibrary1Unfiltered = readListDao.findAllByLibraryIds(listOf(library.id), null, pageable = Pageable.unpaged()).content
|
||||
val foundLibrary2Filtered = readListDao.findAllByLibraryIds(listOf(library2.id), listOf(library2.id), pageable = Pageable.unpaged()).content
|
||||
val foundLibrary2Unfiltered = readListDao.findAllByLibraryIds(listOf(library2.id), null, pageable = Pageable.unpaged()).content
|
||||
val foundBothUnfiltered = readListDao.findAllByLibraryIds(listOf(library.id, library2.id), null, pageable = Pageable.unpaged()).content
|
||||
val foundLibrary1Filtered = readListDao.findAll(listOf(library.id), listOf(library.id), pageable = Pageable.unpaged()).content
|
||||
val foundLibrary1Unfiltered = readListDao.findAll(listOf(library.id), null, pageable = Pageable.unpaged()).content
|
||||
val foundLibrary2Filtered = readListDao.findAll(listOf(library2.id), listOf(library2.id), pageable = Pageable.unpaged()).content
|
||||
val foundLibrary2Unfiltered = readListDao.findAll(listOf(library2.id), null, pageable = Pageable.unpaged()).content
|
||||
val foundBothUnfiltered = readListDao.findAll(listOf(library.id, library2.id), null, pageable = Pageable.unpaged()).content
|
||||
|
||||
// then
|
||||
assertThat(foundLibrary1Filtered).hasSize(2)
|
||||
|
@ -166,11 +166,11 @@ class SeriesCollectionDaoTest(
|
||||
)
|
||||
|
||||
// when
|
||||
val foundLibrary1Filtered = collectionDao.findAllByLibraryIds(listOf(library.id), listOf(library.id), pageable = Pageable.unpaged()).content
|
||||
val foundLibrary1Unfiltered = collectionDao.findAllByLibraryIds(listOf(library.id), null, pageable = Pageable.unpaged()).content
|
||||
val foundLibrary2Filtered = collectionDao.findAllByLibraryIds(listOf(library2.id), listOf(library2.id), pageable = Pageable.unpaged()).content
|
||||
val foundLibrary2Unfiltered = collectionDao.findAllByLibraryIds(listOf(library2.id), null, pageable = Pageable.unpaged()).content
|
||||
val foundBothUnfiltered = collectionDao.findAllByLibraryIds(listOf(library.id, library2.id), null, pageable = Pageable.unpaged()).content
|
||||
val foundLibrary1Filtered = collectionDao.findAll(listOf(library.id), listOf(library.id), pageable = Pageable.unpaged()).content
|
||||
val foundLibrary1Unfiltered = collectionDao.findAll(listOf(library.id), null, pageable = Pageable.unpaged()).content
|
||||
val foundLibrary2Filtered = collectionDao.findAll(listOf(library2.id), listOf(library2.id), pageable = Pageable.unpaged()).content
|
||||
val foundLibrary2Unfiltered = collectionDao.findAll(listOf(library2.id), null, pageable = Pageable.unpaged()).content
|
||||
val foundBothUnfiltered = collectionDao.findAll(listOf(library.id, library2.id), null, pageable = Pageable.unpaged()).content
|
||||
|
||||
// then
|
||||
assertThat(foundLibrary1Filtered).hasSize(2)
|
||||
|
@ -105,6 +105,117 @@ class OpdsControllerTest(
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class ContentRestriction {
|
||||
@Test
|
||||
@WithMockCustomUser(allowAgeUnder = 10)
|
||||
fun `given user only allowed content with specific age rating when getting series then only gets series that satisfies this criteria`() {
|
||||
val series10 = makeSeries(name = "series_10", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
seriesMetadataRepository.findById(series.id).let {
|
||||
seriesMetadataRepository.update(it.copy(ageRating = 10))
|
||||
}
|
||||
}
|
||||
|
||||
val series5 = makeSeries(name = "series_5", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
seriesMetadataRepository.findById(series.id).let {
|
||||
seriesMetadataRepository.update(it.copy(ageRating = 5))
|
||||
}
|
||||
}
|
||||
|
||||
val series15 = makeSeries(name = "series_15", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
seriesMetadataRepository.findById(series.id).let {
|
||||
seriesMetadataRepository.update(it.copy(ageRating = 15))
|
||||
}
|
||||
}
|
||||
|
||||
val series = makeSeries(name = "series_no", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
}
|
||||
|
||||
mockMvc.get("/opds/v1.2/series/${series5.id}").andExpect { status { isOk() } }
|
||||
mockMvc.get("/opds/v1.2/series/${series10.id}").andExpect { status { isOk() } }
|
||||
mockMvc.get("/opds/v1.2/series/${series15.id}").andExpect { status { isForbidden() } }
|
||||
mockMvc.get("/opds/v1.2/series/${series.id}").andExpect { status { isForbidden() } }
|
||||
|
||||
mockMvc.get("/opds/v1.2/series")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
xpath("/feed/entry/id") { nodeCount(2) }
|
||||
xpath("/feed/entry[1]/id") { string(series10.id) }
|
||||
xpath("/feed/entry[2]/id") { string(series5.id) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockCustomUser(excludeAgeOver = 16)
|
||||
fun `given user disallowed content with specific age rating when getting series then only gets series that satisfies this criteria`() {
|
||||
val series10 = makeSeries(name = "series_10", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
seriesMetadataRepository.findById(series.id).let {
|
||||
seriesMetadataRepository.update(it.copy(ageRating = 10))
|
||||
}
|
||||
}
|
||||
|
||||
val series18 = makeSeries(name = "series_18", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
seriesMetadataRepository.findById(series.id).let {
|
||||
seriesMetadataRepository.update(it.copy(ageRating = 18))
|
||||
}
|
||||
}
|
||||
|
||||
val series16 = makeSeries(name = "series_16", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
seriesMetadataRepository.findById(series.id).let {
|
||||
seriesMetadataRepository.update(it.copy(ageRating = 16))
|
||||
}
|
||||
}
|
||||
|
||||
val series = makeSeries(name = "series_no", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
}
|
||||
|
||||
mockMvc.get("/opds/v1.2/series/${series.id}").andExpect { status { isOk() } }
|
||||
mockMvc.get("/opds/v1.2/series/${series10.id}").andExpect { status { isOk() } }
|
||||
mockMvc.get("/opds/v1.2/series/${series16.id}").andExpect { status { isForbidden() } }
|
||||
mockMvc.get("/opds/v1.2/series/${series18.id}").andExpect { status { isForbidden() } }
|
||||
|
||||
mockMvc.get("/opds/v1.2/series")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
xpath("/feed/entry/id") { nodeCount(2) }
|
||||
xpath("/feed/entry[1]/id") { string(series10.id) }
|
||||
xpath("/feed/entry[2]/id") { string(series.id) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class SeriesSort {
|
||||
@Test
|
||||
|
@ -17,6 +17,7 @@ import org.gotson.komga.domain.persistence.BookRepository
|
||||
import org.gotson.komga.domain.persistence.KomgaUserRepository
|
||||
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||
import org.gotson.komga.domain.persistence.MediaRepository
|
||||
import org.gotson.komga.domain.persistence.SeriesMetadataRepository
|
||||
import org.gotson.komga.domain.persistence.SeriesRepository
|
||||
import org.gotson.komga.domain.service.BookLifecycle
|
||||
import org.gotson.komga.domain.service.KomgaUserLifecycle
|
||||
@ -56,6 +57,7 @@ import kotlin.random.Random
|
||||
@AutoConfigureMockMvc(printOnlyOnFailure = false)
|
||||
class BookControllerTest(
|
||||
@Autowired private val seriesRepository: SeriesRepository,
|
||||
@Autowired private val seriesMetadataRepository: SeriesMetadataRepository,
|
||||
@Autowired private val seriesLifecycle: SeriesLifecycle,
|
||||
@Autowired private val mediaRepository: MediaRepository,
|
||||
@Autowired private val bookMetadataRepository: BookMetadataRepository,
|
||||
@ -124,6 +126,125 @@ class BookControllerTest(
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class RestrictedContent {
|
||||
@Test
|
||||
@WithMockCustomUser(allowAgeUnder = 10)
|
||||
fun `given user only allowed content with specific age rating when getting series then only gets books that satisfies this criteria`() {
|
||||
val book10 = makeBook("book_10", libraryId = library.id)
|
||||
makeSeries(name = "series_10", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(book10)
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
seriesMetadataRepository.findById(series.id).let {
|
||||
seriesMetadataRepository.update(it.copy(ageRating = 10))
|
||||
}
|
||||
}
|
||||
|
||||
val book5 = makeBook("book_5", libraryId = library.id)
|
||||
makeSeries(name = "series_5", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(book5)
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
seriesMetadataRepository.findById(series.id).let {
|
||||
seriesMetadataRepository.update(it.copy(ageRating = 5))
|
||||
}
|
||||
}
|
||||
|
||||
val book15 = makeBook("book_15", libraryId = library.id)
|
||||
makeSeries(name = "series_15", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(book15)
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
seriesMetadataRepository.findById(series.id).let {
|
||||
seriesMetadataRepository.update(it.copy(ageRating = 15))
|
||||
}
|
||||
}
|
||||
|
||||
val book = makeBook("book", libraryId = library.id)
|
||||
makeSeries(name = "series_no", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(book)
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
}
|
||||
|
||||
mockMvc.get("/api/v1/books/${book5.id}").andExpect { status { isOk() } }
|
||||
mockMvc.get("/api/v1/books/${book10.id}").andExpect { status { isOk() } }
|
||||
mockMvc.get("/api/v1/books/${book15.id}").andExpect { status { isForbidden() } }
|
||||
mockMvc.get("/api/v1/books/${book.id}").andExpect { status { isForbidden() } }
|
||||
|
||||
mockMvc.get("/api/v1/books")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.content.length()") { value(2) }
|
||||
jsonPath("$.content[0].name") { value(book10.name) }
|
||||
jsonPath("$.content[1].name") { value(book5.name) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockCustomUser(excludeAgeOver = 10)
|
||||
fun `given user disallowed content with specific age rating when getting series then only gets books that satisfies this criteria`() {
|
||||
val book10 = makeBook("book_10", libraryId = library.id)
|
||||
makeSeries(name = "series_10", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(book10)
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
seriesMetadataRepository.findById(series.id).let {
|
||||
seriesMetadataRepository.update(it.copy(ageRating = 10))
|
||||
}
|
||||
}
|
||||
|
||||
val book5 = makeBook("book_5", libraryId = library.id)
|
||||
makeSeries(name = "series_5", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(book5)
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
seriesMetadataRepository.findById(series.id).let {
|
||||
seriesMetadataRepository.update(it.copy(ageRating = 5))
|
||||
}
|
||||
}
|
||||
|
||||
val book15 = makeBook("book_15", libraryId = library.id)
|
||||
makeSeries(name = "series_15", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(book15)
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
seriesMetadataRepository.findById(series.id).let {
|
||||
seriesMetadataRepository.update(it.copy(ageRating = 15))
|
||||
}
|
||||
}
|
||||
|
||||
val book = makeBook("book", libraryId = library.id)
|
||||
makeSeries(name = "series_no", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(book)
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
}
|
||||
|
||||
mockMvc.get("/api/v1/books/${book5.id}").andExpect { status { isOk() } }
|
||||
mockMvc.get("/api/v1/books/${book.id}").andExpect { status { isOk() } }
|
||||
mockMvc.get("/api/v1/books/${book10.id}").andExpect { status { isForbidden() } }
|
||||
mockMvc.get("/api/v1/books/${book15.id}").andExpect { status { isForbidden() } }
|
||||
|
||||
mockMvc.get("/api/v1/books")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.content.length()") { value(2) }
|
||||
jsonPath("$.content[0].name") { value(book.name) }
|
||||
jsonPath("$.content[1].name") { value(book5.name) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class UserWithoutLibraryAccess {
|
||||
@Test
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.gotson.komga.interfaces.api.rest
|
||||
|
||||
import org.gotson.komga.domain.model.ContentRestriction
|
||||
import org.gotson.komga.domain.model.KomgaUser
|
||||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||
import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD
|
||||
@ -20,6 +21,10 @@ annotation class WithMockCustomUser(
|
||||
val sharedAllLibraries: Boolean = true,
|
||||
val sharedLibraries: Array<String> = [],
|
||||
val id: String = "0",
|
||||
val allowAgeUnder: Int = -1,
|
||||
val excludeAgeOver: Int = -1,
|
||||
val allowLabels: Array<String> = [],
|
||||
val excludeLabels: Array<String> = [],
|
||||
)
|
||||
|
||||
class WithMockCustomUserSecurityContextFactory : WithSecurityContextFactory<WithMockCustomUser> {
|
||||
@ -35,6 +40,16 @@ class WithMockCustomUserSecurityContextFactory : WithSecurityContextFactory<With
|
||||
rolePageStreaming = customUser.roles.contains(ROLE_PAGE_STREAMING),
|
||||
sharedAllLibraries = customUser.sharedAllLibraries,
|
||||
sharedLibrariesIds = customUser.sharedLibraries.toSet(),
|
||||
restrictions = buildSet {
|
||||
if (customUser.allowAgeUnder >= 0) add(ContentRestriction.AgeRestriction.AllowOnlyUnder(customUser.allowAgeUnder))
|
||||
if (customUser.excludeAgeOver >= 0) add(ContentRestriction.AgeRestriction.ExcludeOver(customUser.excludeAgeOver))
|
||||
if (customUser.allowLabels.isNotEmpty()) {
|
||||
add(ContentRestriction.LabelsRestriction.AllowOnly(customUser.allowLabels.toSet()))
|
||||
}
|
||||
if (customUser.excludeLabels.isNotEmpty()) {
|
||||
add(ContentRestriction.LabelsRestriction.Exclude(customUser.excludeLabels.toSet()))
|
||||
}
|
||||
},
|
||||
id = customUser.id,
|
||||
),
|
||||
)
|
||||
|
@ -8,6 +8,7 @@ import org.gotson.komga.domain.model.makeLibrary
|
||||
import org.gotson.komga.domain.model.makeSeries
|
||||
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||
import org.gotson.komga.domain.persistence.ReadListRepository
|
||||
import org.gotson.komga.domain.persistence.SeriesMetadataRepository
|
||||
import org.gotson.komga.domain.service.LibraryLifecycle
|
||||
import org.gotson.komga.domain.service.ReadListLifecycle
|
||||
import org.gotson.komga.domain.service.SeriesLifecycle
|
||||
@ -39,6 +40,7 @@ class ReadListControllerTest(
|
||||
@Autowired private val libraryLifecycle: LibraryLifecycle,
|
||||
@Autowired private val libraryRepository: LibraryRepository,
|
||||
@Autowired private val seriesLifecycle: SeriesLifecycle,
|
||||
@Autowired private val seriesMetadataRepository: SeriesMetadataRepository,
|
||||
) {
|
||||
|
||||
private val library1 = makeLibrary("Library1", id = "1")
|
||||
@ -170,6 +172,88 @@ class ReadListControllerTest(
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class ContentRestriction {
|
||||
@Test
|
||||
@WithMockCustomUser(allowAgeUnder = 10)
|
||||
fun `given user only allowed content with specific age rating when getting collections then only get collections that satisfies this criteria`() {
|
||||
val book10 = makeBook("book_10", libraryId = library1.id)
|
||||
val series10 = makeSeries(name = "series_10", libraryId = library1.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(book10)
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
seriesMetadataRepository.findById(series.id).let {
|
||||
seriesMetadataRepository.update(it.copy(ageRating = 10))
|
||||
}
|
||||
}
|
||||
|
||||
val book = makeBook("book", libraryId = library1.id)
|
||||
val series = makeSeries(name = "series_no", libraryId = library1.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(book)
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
}
|
||||
|
||||
val rlAllowed = readListLifecycle.addReadList(
|
||||
ReadList(
|
||||
name = "Allowed",
|
||||
bookIds = listOf(book10.id).toIndexedMap(),
|
||||
),
|
||||
)
|
||||
|
||||
val rlFiltered = readListLifecycle.addReadList(
|
||||
ReadList(
|
||||
name = "Filtered",
|
||||
bookIds = listOf(book10.id, book.id).toIndexedMap(),
|
||||
),
|
||||
)
|
||||
|
||||
val rlDenied = readListLifecycle.addReadList(
|
||||
ReadList(
|
||||
name = "Denied",
|
||||
bookIds = listOf(book.id).toIndexedMap(),
|
||||
),
|
||||
)
|
||||
|
||||
mockMvc.get("/api/v1/readlists")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.totalElements") { value(2) }
|
||||
jsonPath("$.content[?(@.name == '${rlAllowed.name}')].filtered") { value(false) }
|
||||
jsonPath("$.content[?(@.name == '${rlFiltered.name}')].filtered") { value(true) }
|
||||
}
|
||||
|
||||
mockMvc.get("/api/v1/readlists/${rlAllowed.id}")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.bookIds.length()") { value(1) }
|
||||
jsonPath("$.filtered") { value(false) }
|
||||
}
|
||||
|
||||
mockMvc.get("/api/v1/readlists/${rlFiltered.id}")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.bookIds.length()") { value(1) }
|
||||
jsonPath("$.filtered") { value(true) }
|
||||
}
|
||||
|
||||
mockMvc.get("/api/v1/readlists/${rlDenied.id}")
|
||||
.andExpect {
|
||||
status { isNotFound() }
|
||||
}
|
||||
|
||||
mockMvc.get("/api/v1/books/${book10.id}/readlists")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.length()") { value(2) }
|
||||
jsonPath("$[?(@.name == '${rlAllowed.name}')].filtered") { value(false) }
|
||||
jsonPath("$[?(@.name == '${rlFiltered.name}')].filtered") { value(true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class GetBooksAndFilter {
|
||||
@Test
|
||||
|
@ -3,10 +3,12 @@ package org.gotson.komga.interfaces.api.rest
|
||||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||
import org.gotson.komga.domain.model.Series
|
||||
import org.gotson.komga.domain.model.SeriesCollection
|
||||
import org.gotson.komga.domain.model.makeBook
|
||||
import org.gotson.komga.domain.model.makeLibrary
|
||||
import org.gotson.komga.domain.model.makeSeries
|
||||
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
|
||||
import org.gotson.komga.domain.persistence.SeriesMetadataRepository
|
||||
import org.gotson.komga.domain.service.LibraryLifecycle
|
||||
import org.gotson.komga.domain.service.SeriesCollectionLifecycle
|
||||
import org.gotson.komga.domain.service.SeriesLifecycle
|
||||
@ -37,6 +39,7 @@ class SeriesCollectionControllerTest(
|
||||
@Autowired private val libraryLifecycle: LibraryLifecycle,
|
||||
@Autowired private val libraryRepository: LibraryRepository,
|
||||
@Autowired private val seriesLifecycle: SeriesLifecycle,
|
||||
@Autowired private val seriesMetadataRepository: SeriesMetadataRepository,
|
||||
) {
|
||||
|
||||
private val library1 = makeLibrary("Library1", id = "1")
|
||||
@ -165,6 +168,86 @@ class SeriesCollectionControllerTest(
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class ContentRestriction {
|
||||
@Test
|
||||
@WithMockCustomUser(allowAgeUnder = 10)
|
||||
fun `given user only allowed content with specific age rating when getting collections then only get collections that satisfies this criteria`() {
|
||||
val series10 = makeSeries(name = "series_10", libraryId = library1.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library1.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
seriesMetadataRepository.findById(series.id).let {
|
||||
seriesMetadataRepository.update(it.copy(ageRating = 10))
|
||||
}
|
||||
}
|
||||
|
||||
val series = makeSeries(name = "series_no", libraryId = library1.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library1.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
}
|
||||
|
||||
val colAllowed = collectionLifecycle.addCollection(
|
||||
SeriesCollection(
|
||||
name = "Allowed",
|
||||
seriesIds = listOf(series10.id),
|
||||
),
|
||||
)
|
||||
|
||||
val colFiltered = collectionLifecycle.addCollection(
|
||||
SeriesCollection(
|
||||
name = "Filtered",
|
||||
seriesIds = listOf(series10.id, series.id),
|
||||
),
|
||||
)
|
||||
|
||||
val colDenied = collectionLifecycle.addCollection(
|
||||
SeriesCollection(
|
||||
name = "Denied",
|
||||
seriesIds = listOf(series.id),
|
||||
),
|
||||
)
|
||||
|
||||
mockMvc.get("/api/v1/collections")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.totalElements") { value(2) }
|
||||
jsonPath("$.content[?(@.name == '${colAllowed.name}')].filtered") { value(false) }
|
||||
jsonPath("$.content[?(@.name == '${colFiltered.name}')].filtered") { value(true) }
|
||||
}
|
||||
|
||||
mockMvc.get("/api/v1/collections/${colAllowed.id}")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.seriesIds.length()") { value(1) }
|
||||
jsonPath("$.filtered") { value(false) }
|
||||
}
|
||||
|
||||
mockMvc.get("/api/v1/collections/${colFiltered.id}")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.seriesIds.length()") { value(1) }
|
||||
jsonPath("$.filtered") { value(true) }
|
||||
}
|
||||
|
||||
mockMvc.get("/api/v1/collections/${colDenied.id}")
|
||||
.andExpect {
|
||||
status { isNotFound() }
|
||||
}
|
||||
|
||||
mockMvc.get("/api/v1/series/${series10.id}/collections")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.length()") { value(2) }
|
||||
jsonPath("$[?(@.name == '${colAllowed.name}')].filtered") { value(false) }
|
||||
jsonPath("$[?(@.name == '${colFiltered.name}')].filtered") { value(true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class Creation {
|
||||
@Test
|
||||
|
@ -243,6 +243,117 @@ class SeriesControllerTest(
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class ContentRestrictedUser {
|
||||
@Test
|
||||
@WithMockCustomUser(allowAgeUnder = 10)
|
||||
fun `given user only allowed content with specific age rating when getting series then only gets series that satisfies this criteria`() {
|
||||
val series10 = makeSeries(name = "series_10", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
seriesMetadataRepository.findById(series.id).let {
|
||||
seriesMetadataRepository.update(it.copy(ageRating = 10))
|
||||
}
|
||||
}
|
||||
|
||||
val series5 = makeSeries(name = "series_5", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
seriesMetadataRepository.findById(series.id).let {
|
||||
seriesMetadataRepository.update(it.copy(ageRating = 5))
|
||||
}
|
||||
}
|
||||
|
||||
val series15 = makeSeries(name = "series_15", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
seriesMetadataRepository.findById(series.id).let {
|
||||
seriesMetadataRepository.update(it.copy(ageRating = 15))
|
||||
}
|
||||
}
|
||||
|
||||
val series = makeSeries(name = "series_no", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
}
|
||||
|
||||
mockMvc.get("/api/v1/series/${series5.id}").andExpect { status { isOk() } }
|
||||
mockMvc.get("/api/v1/series/${series10.id}").andExpect { status { isOk() } }
|
||||
mockMvc.get("/api/v1/series/${series15.id}").andExpect { status { isForbidden() } }
|
||||
mockMvc.get("/api/v1/series/${series.id}").andExpect { status { isForbidden() } }
|
||||
|
||||
mockMvc.get("/api/v1/series")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.content.length()") { value(2) }
|
||||
jsonPath("$.content[0].name") { value("series_10") }
|
||||
jsonPath("$.content[1].name") { value("series_5") }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockCustomUser(excludeAgeOver = 16)
|
||||
fun `given user disallowed content with specific age rating when getting series then only gets series that satisfies this criteria`() {
|
||||
val series10 = makeSeries(name = "series_10", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
seriesMetadataRepository.findById(series.id).let {
|
||||
seriesMetadataRepository.update(it.copy(ageRating = 10))
|
||||
}
|
||||
}
|
||||
|
||||
val series18 = makeSeries(name = "series_18", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
seriesMetadataRepository.findById(series.id).let {
|
||||
seriesMetadataRepository.update(it.copy(ageRating = 18))
|
||||
}
|
||||
}
|
||||
|
||||
val series16 = makeSeries(name = "series_16", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
seriesMetadataRepository.findById(series.id).let {
|
||||
seriesMetadataRepository.update(it.copy(ageRating = 16))
|
||||
}
|
||||
}
|
||||
|
||||
val series = makeSeries(name = "series_no", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
}
|
||||
|
||||
mockMvc.get("/api/v1/series/${series.id}").andExpect { status { isOk() } }
|
||||
mockMvc.get("/api/v1/series/${series10.id}").andExpect { status { isOk() } }
|
||||
mockMvc.get("/api/v1/series/${series16.id}").andExpect { status { isForbidden() } }
|
||||
mockMvc.get("/api/v1/series/${series18.id}").andExpect { status { isForbidden() } }
|
||||
|
||||
mockMvc.get("/api/v1/series")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.content.length()") { value(2) }
|
||||
jsonPath("$.content[0].name") { value("series_10") }
|
||||
jsonPath("$.content[1].name") { value("series_no") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class UserWithoutLibraryAccess {
|
||||
@Test
|
||||
|
Loading…
Reference in New Issue
Block a user