Skip to content

Commit

Permalink
Reformat
Browse files Browse the repository at this point in the history
  • Loading branch information
Ziedelth committed Apr 5, 2024
1 parent 053ac3d commit 897f30f
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 308 deletions.
274 changes: 68 additions & 206 deletions src/main/kotlin/fr/shikkanime/modules/Routing.kt

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions src/main/kotlin/fr/shikkanime/modules/SEOManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package fr.shikkanime.modules

import fr.shikkanime.entities.LinkObject
import fr.shikkanime.entities.enums.ConfigPropertyKey
import fr.shikkanime.services.caches.ConfigCacheService
import fr.shikkanime.services.caches.SimulcastCacheService
import fr.shikkanime.utils.Constant
import fr.shikkanime.utils.StringUtils

private const val ADMIN = "/admin"

fun setGlobalAttributes(
modelMap: MutableMap<String, Any?>,
controller: Any,
replacedPath: String,
title: String?
) {
val configCacheService = Constant.injector.getInstance(ConfigCacheService::class.java)
val simulcastCacheService = Constant.injector.getInstance(SimulcastCacheService::class.java)

modelMap["su"] = StringUtils
modelMap["links"] = getLinks(controller, replacedPath, simulcastCacheService)
modelMap["footerLinks"] = getFooterLinks(controller)
modelMap["title"] = getTitle(title)
modelMap["seoDescription"] = configCacheService.getValueAsString(ConfigPropertyKey.SEO_DESCRIPTION)
modelMap["googleSiteVerification"] =
configCacheService.getValueAsString(ConfigPropertyKey.GOOGLE_SITE_VERIFICATION_ID)
modelMap["currentSimulcast"] = simulcastCacheService.currentSimulcast
modelMap["analyticsDomain"] = configCacheService.getValueAsString(ConfigPropertyKey.ANALYTICS_DOMAIN)
modelMap["analyticsApi"] = configCacheService.getValueAsString(ConfigPropertyKey.ANALYTICS_API)
modelMap["analyticsScript"] = configCacheService.getValueAsString(ConfigPropertyKey.ANALYTICS_SCRIPT)
}

private fun getLinks(controller: Any, replacedPath: String, simulcastCacheService: SimulcastCacheService) =
LinkObject.list()
.filter { it.href.startsWith(ADMIN) == controller.javaClass.simpleName.startsWith("Admin") && !it.footer }
.map { link ->
link.href = link.href.replace("{currentSimulcast}", simulcastCacheService.currentSimulcast?.slug ?: "")
link.active = if (link.href == "/") replacedPath == link.href else replacedPath.startsWith(link.href)
link
}

private fun getFooterLinks(controller: Any) = LinkObject.list()
.filter { it.href.startsWith(ADMIN) == controller.javaClass.simpleName.startsWith("Admin") && it.footer }

private fun getTitle(title: String?): String =
title?.takeIf { it.contains(Constant.NAME) } ?: "$title - ${Constant.NAME}"
114 changes: 44 additions & 70 deletions src/main/kotlin/fr/shikkanime/modules/Security.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import fr.shikkanime.dtos.TokenDto
import fr.shikkanime.entities.enums.Role
import fr.shikkanime.services.caches.MemberCacheService
import fr.shikkanime.utils.Constant
import fr.shikkanime.utils.LoggerFactory
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
Expand All @@ -17,47 +16,15 @@ import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.sessions.*
import java.util.*
import java.util.logging.Level

private val logger = LoggerFactory.getLogger("Security")
private val memberCacheService = Constant.injector.getInstance(MemberCacheService::class.java)

fun Application.configureSecurity() {
val memberCacheService = Constant.injector.getInstance(MemberCacheService::class.java)

val jwtVerifier = JWT
.require(Algorithm.HMAC256(Constant.jwtSecret))
.withAudience(Constant.jwtAudience)
.withIssuer(Constant.jwtDomain)
.withClaimPresence("uuid")
.withClaimPresence("username")
.withClaimPresence("creationDateTime")
.withClaimPresence("roles")
.build()
val jwtVerifier = setupJWTVerifier()

authentication {
jwt {
realm = Constant.jwtRealm
verifier(jwtVerifier)
validate { credential ->
if (credential.payload.audience.contains(Constant.jwtAudience)) JWTPrincipal(credential.payload) else null
}
}

session<TokenDto>("auth-admin-session") {
validate { session ->
return@validate validationSession(jwtVerifier, session, memberCacheService)
}
challenge {
if (call.request.contentType() != ContentType.Text.Html) {
call.respond(
HttpStatusCode.Unauthorized,
MessageDto(MessageDto.Type.ERROR, "You are not authorized to access this page")
)
} else {
call.respondRedirect("/admin?error=2", permanent = true)
}
}
}
setupJWTAuthentication(jwtVerifier)
setupSessionAuthentication(jwtVerifier)
}

install(Sessions) {
Expand All @@ -68,42 +35,49 @@ fun Application.configureSecurity() {
}
}

private fun validationSession(
jwtVerifier: JWTVerifier,
session: TokenDto,
memberCacheService: MemberCacheService
): TokenDto? {
try {
val jwtPrincipal = jwtVerifier.verify(session.token)
val uuid = UUID.fromString(jwtPrincipal.getClaim("uuid").asString())
val username = jwtPrincipal.getClaim("username").asString()
val creationDateTime = jwtPrincipal.getClaim("creationDateTime").asString()
val roles = jwtPrincipal.getClaim("roles").asArray(Role::class.java)
val member = memberCacheService.find(uuid) ?: return null

if (member.username != username) {
logger.log(Level.SEVERE, "Error while validating session: username mismatch")
return null
}
private fun setupJWTVerifier(): JWTVerifier = JWT
.require(Algorithm.HMAC256(Constant.jwtSecret))
.withAudience(Constant.jwtAudience)
.withIssuer(Constant.jwtDomain)
.withClaimPresence("uuid")
.withClaimPresence("username")
.withClaimPresence("creationDateTime")
.withClaimPresence("roles")
.build()

if (!member.roles.toTypedArray().contentEquals(roles)) {
logger.log(Level.SEVERE, "Error while validating session: roles mismatch")
return null
}

if (member.creationDateTime.toString() != creationDateTime) {
logger.log(Level.SEVERE, "Error while validating session: creationDateTime mismatch")
return null
private fun AuthenticationConfig.setupJWTAuthentication(jwtVerifier: JWTVerifier) {
jwt {
realm = Constant.jwtRealm
verifier(jwtVerifier)
validate { credential ->
if (credential.payload.audience.contains(Constant.jwtAudience)) JWTPrincipal(credential.payload) else null
}
}
}

if (member.roles.none { it == Role.ADMIN }) {
logger.log(Level.SEVERE, "Error while validating session: role is not admin")
return null
private fun AuthenticationConfig.setupSessionAuthentication(jwtVerifier: JWTVerifier) {
session<TokenDto>("auth-admin-session") {
validate { session -> validationSession(jwtVerifier, session) }
challenge {
if (call.request.contentType() != ContentType.Text.Html) {
call.respond(
HttpStatusCode.Unauthorized,
MessageDto(MessageDto.Type.ERROR, "You are not authorized to access this page")
)
} else {
call.respondRedirect("/admin?error=2", permanent = true)
}
}

return session
} catch (e: Exception) {
logger.log(Level.SEVERE, "Error while validating session", e)
return null
}
}

private fun validationSession(jwtVerifier: JWTVerifier, session: TokenDto): TokenDto? {
val jwtPrincipal = jwtVerifier.verify(session.token) ?: return null
val member = memberCacheService.find(UUID.fromString(jwtPrincipal.getClaim("uuid").asString())) ?: return null

return if (member.username == jwtPrincipal.getClaim("username").asString() &&
member.roles.toTypedArray().contentEquals(jwtPrincipal.getClaim("roles").asArray(Role::class.java)) &&
member.creationDateTime.toString() == jwtPrincipal.getClaim("creationDateTime").asString() &&
member.roles.any { it == Role.ADMIN }
) session else null
}
80 changes: 80 additions & 0 deletions src/main/kotlin/fr/shikkanime/modules/SwaggerRouting.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package fr.shikkanime.modules

import fr.shikkanime.utils.routes.openapi.OpenAPI
import fr.shikkanime.utils.routes.param.PathParam
import fr.shikkanime.utils.routes.param.QueryParam
import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor
import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute
import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody
import io.ktor.http.*
import kotlin.reflect.KFunction
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.jvm.jvmErasure

fun swagger(
method: KFunction<*>,
routeTags: List<String>,
hiddenRoute: Boolean
): OpenApiRoute.() -> Unit {
val openApi = method.findAnnotation<OpenAPI>() ?: return {
tags = routeTags
hidden = hiddenRoute
}

return {
tags = routeTags
hidden = hiddenRoute || openApi.hidden
description = openApi.description
swaggerRequest(method)
swaggerResponse(openApi)
}
}

private fun OpenApiRoute.swaggerRequest(method: KFunction<*>) {
request {
method.parameters.filter { it.hasAnnotation<QueryParam>() || it.hasAnnotation<PathParam>() }
.forEach { parameter ->
val name = parameter.name!!
val type = parameter.type.jvmErasure

when {
parameter.hasAnnotation<QueryParam>() -> {
val qp = parameter.findAnnotation<QueryParam>()!!
queryParameter(name, type) {
description = qp.description
required = qp.required
}
}

parameter.hasAnnotation<PathParam>() -> {
val pp = parameter.findAnnotation<PathParam>()!!
pathParameter(name, type) {
description = pp.description
required = true
}
}
}
}
}
}

private fun OpenApiRoute.swaggerResponse(openApi: OpenAPI) {
response {
openApi.responses.forEach { response ->
HttpStatusCode.fromValue(response.status) to {
description = response.description
val block: OpenApiSimpleBody.() -> Unit = { mediaType(ContentType.parse(response.contentType)) }

when {
response.type.java.isArray -> body(
BodyTypeDescriptor.multipleOf(response.type.java.componentType.kotlin),
block
)

else -> body(response.type, block)
}
}
}
}
}
16 changes: 8 additions & 8 deletions src/main/kotlin/fr/shikkanime/services/AnimeService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import fr.shikkanime.dtos.WeeklyAnimeDto
import fr.shikkanime.dtos.WeeklyAnimesDto
import fr.shikkanime.dtos.animes.AnimeNoStatusDto
import fr.shikkanime.entities.Anime
import fr.shikkanime.entities.Episode
import fr.shikkanime.entities.SortParameter
import fr.shikkanime.entities.enums.CountryCode
import fr.shikkanime.repositories.AnimeRepository
import fr.shikkanime.utils.Constant
import fr.shikkanime.utils.MapCache
import fr.shikkanime.utils.StringUtils.capitalizeWords
import fr.shikkanime.utils.withUTC
Expand Down Expand Up @@ -52,10 +54,10 @@ class AnimeService : AbstractService<Anime, AnimeRepository>() {
fun findAllUUIDAndImage() = animeRepository.findAllUUIDAndImage()

fun getWeeklyAnimes(startOfWeekDay: LocalDate, countryCode: CountryCode): List<WeeklyAnimesDto> {
val start = ZonedDateTime.parse("${startOfWeekDay.minusDays(7)}T00:00:00Z")
val end = ZonedDateTime.parse("${startOfWeekDay.plusDays(7)}T23:59:59Z")
val start = startOfWeekDay.minusDays(7).atStartOfDay(Constant.utcZoneId)
val end = startOfWeekDay.plusDays(7).atTime(23, 59, 59).withUTC()
val list = episodeService.findAllByDateRange(countryCode, start, end)
val pattern = DateTimeFormatter.ofPattern("EEEE", Locale.of(countryCode.locale.split("-")[0], countryCode.locale.split("-")[1]))
val pattern = DateTimeFormatter.ofPattern("EEEE", Locale.forLanguageTag(countryCode.locale))

return startOfWeekDay.datesUntil(startOfWeekDay.plusDays(7)).toList().map { date ->
val dateTitle = date.format(pattern).capitalizeWords()
Expand All @@ -65,11 +67,9 @@ class AnimeService : AbstractService<Anime, AnimeRepository>() {
WeeklyAnimesDto(
dateTitle,
episodes.distinctBy { episode -> episode.anime?.uuid }.map { distinctEpisode ->
val platforms = episodes.mapNotNull { episode ->
if (episode.anime?.uuid == distinctEpisode.anime?.uuid)
episode.platform!!
else null
}.distinct()
val platforms = episodes.filter { it.anime?.uuid == distinctEpisode.anime?.uuid }
.mapNotNull(Episode::platform)
.distinct()

WeeklyAnimeDto(
AbstractConverter.convert(distinctEpisode.anime, AnimeNoStatusDto::class.java).toAnimeDto(),
Expand Down
5 changes: 5 additions & 0 deletions src/main/kotlin/fr/shikkanime/utils/Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package fr.shikkanime.utils

import com.mortennobel.imagescaling.ResampleOp
import java.awt.image.BufferedImage
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.ZonedDateTime

Expand All @@ -13,6 +14,10 @@ fun ZonedDateTime.withUTC(): ZonedDateTime {
return this.withZoneSameInstant(Constant.utcZoneId)
}

fun LocalDateTime.withUTC(): ZonedDateTime {
return this.atZone(Constant.utcZoneId)
}

fun LocalTime.isEqualOrAfter(other: LocalTime): Boolean {
return this == other || this.isAfter(other)
}
Expand Down
24 changes: 0 additions & 24 deletions src/main/kotlin/fr/shikkanime/utils/routes/Response.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,6 @@ open class Response(
val data: Any? = null,
val contentType: ContentType = ContentType.Application.Json,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Response) return false

if (status != other.status) return false
if (data != other.data) return false
if (session != other.session) return false
if (contentType != other.contentType) return false

return true
}

override fun hashCode(): Int {
var result = status.hashCode()
result = 31 * result + (data?.hashCode() ?: 0)
result = 31 * result + (session?.hashCode() ?: 0)
result = 31 * result + contentType.hashCode()
return result
}

override fun toString(): String {
return "Response(status=$status, data=$data, session=$session)"
}

companion object {
fun ok(data: Any? = null, session: TokenDto? = null): Response =
Response(HttpStatusCode.OK, data = data, session = session)
Expand Down

0 comments on commit 897f30f

Please sign in to comment.