diff --git a/Dockerfile b/Dockerfile index 015e771e..b7844aa9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,16 @@ FROM mcr.microsoft.com/playwright:v1.43.1-jammy -ARG version=21.0.2.13-1 +ARG version=21.0.3.9-1 ENV LANG C.UTF-8 ENV JAVA_HOME /usr/lib/jvm/java-21-amazon-corretto ENV TZ Europe/Paris # Install Java and necessary packages -RUN set -eux; \ - apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates gnupg software-properties-common fontconfig java-common tzdata libopencv-dev fonts-dejavu \ +RUN set -eux \ + && apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates gnupg software-properties-common fontconfig java-common tzdata libopencv-dev fonts-dejavu \ && curl -fL https://apt.corretto.aws/corretto.key | apt-key add - \ && add-apt-repository 'deb https://apt.corretto.aws stable main' \ && mkdir -p /usr/share/man/man1 || true \ - && apt-get update; apt-get install -y java-21-amazon-corretto-jdk=1:"$version" \ + && apt-get update; apt-get install -y java-21-amazon-corretto-jdk=1:$version \ && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false curl gnupg software-properties-common \ && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* \ && ln -snf /usr/share/zoneinfo/"$TZ" /etc/localtime && echo "$TZ" > /etc/timezone && dpkg-reconfigure --frontend noninteractive tzdata diff --git a/docker-compose.yml b/docker-compose.yml index fa3c5cd5..33a07131 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,5 +23,11 @@ services: DATABASE_URL: jdbc:postgresql://shikkanime-db:5432/shikkanime DATABASE_USERNAME: postgres DATABASE_PASSWORD: "mysecretpassword" + JWT_SECRET: "mysecretkey" + JWT_DOMAIN: "http://localhost:37100" + JWT_REALM: "Access to '/admin'" + JWT_AUDIENCE: "http://localhost:37100/admin" + BASE_URL: "http://localhost:37100" + API_URL: "http://localhost:37100/api" volumes: - ./data:/app/data/ \ No newline at end of file diff --git a/src/main/kotlin/fr/shikkanime/controllers/api/AnimeController.kt b/src/main/kotlin/fr/shikkanime/controllers/api/AnimeController.kt index b54ef53a..6afcef10 100644 --- a/src/main/kotlin/fr/shikkanime/controllers/api/AnimeController.kt +++ b/src/main/kotlin/fr/shikkanime/controllers/api/AnimeController.kt @@ -89,17 +89,17 @@ class AnimeController : HasPageableRoute() { @Path("/{uuid}") @Get - @AdminSessionAuthenticated + @JWTAuthenticated @OpenAPI(hidden = true) private fun animeDetails( @PathParam("uuid") uuid: UUID, ): Response { - return Response.ok(animeService.find(uuid)) + return Response.ok(AbstractConverter.convert(animeService.find(uuid), AnimeDto::class.java)) } @Path("/{uuid}") @Put - @AdminSessionAuthenticated + @JWTAuthenticated @OpenAPI(hidden = true) private fun updateAnime(@PathParam("uuid") uuid: UUID, @BodyParam animeDto: AnimeDto): Response { val updated = animeService.update(uuid, animeDto) @@ -108,7 +108,7 @@ class AnimeController : HasPageableRoute() { @Path("/{uuid}") @Delete - @AdminSessionAuthenticated + @JWTAuthenticated @OpenAPI(hidden = true) private fun deleteAnime(@PathParam("uuid") uuid: UUID): Response { animeService.delete(animeService.find(uuid) ?: return Response.notFound()) diff --git a/src/main/kotlin/fr/shikkanime/controllers/api/ConfigController.kt b/src/main/kotlin/fr/shikkanime/controllers/api/ConfigController.kt index 98472f87..6cb6dda8 100644 --- a/src/main/kotlin/fr/shikkanime/controllers/api/ConfigController.kt +++ b/src/main/kotlin/fr/shikkanime/controllers/api/ConfigController.kt @@ -5,8 +5,8 @@ import fr.shikkanime.converters.AbstractConverter import fr.shikkanime.dtos.ConfigDto import fr.shikkanime.services.ConfigService import fr.shikkanime.utils.Constant -import fr.shikkanime.utils.routes.AdminSessionAuthenticated import fr.shikkanime.utils.routes.Controller +import fr.shikkanime.utils.routes.JWTAuthenticated import fr.shikkanime.utils.routes.Path import fr.shikkanime.utils.routes.Response import fr.shikkanime.utils.routes.method.Get @@ -24,7 +24,7 @@ class ConfigController { @Path @Get - @AdminSessionAuthenticated + @JWTAuthenticated @OpenAPI(hidden = true) private fun getConfigs( @QueryParam("name") nameParam: String?, @@ -40,7 +40,7 @@ class ConfigController { @Path("/{uuid}") @Put - @AdminSessionAuthenticated + @JWTAuthenticated @OpenAPI(hidden = true) private fun updateConfig(@PathParam("uuid") uuid: UUID, @BodyParam configDto: ConfigDto): Response { configService.update(uuid, configDto) diff --git a/src/main/kotlin/fr/shikkanime/controllers/api/EpisodeMappingController.kt b/src/main/kotlin/fr/shikkanime/controllers/api/EpisodeMappingController.kt index 991badc3..692ce969 100644 --- a/src/main/kotlin/fr/shikkanime/controllers/api/EpisodeMappingController.kt +++ b/src/main/kotlin/fr/shikkanime/controllers/api/EpisodeMappingController.kt @@ -73,15 +73,15 @@ class EpisodeMappingController : HasPageableRoute() { @Path("/{uuid}") @Get - @AdminSessionAuthenticated + @JWTAuthenticated @OpenAPI(hidden = true) private fun read(@PathParam("uuid") uuid: UUID): Response { - return Response.ok(episodeMappingService.find(uuid)) + return Response.ok(AbstractConverter.convert(episodeMappingService.find(uuid), EpisodeMappingDto::class.java)) } @Path("/{uuid}") @Put - @AdminSessionAuthenticated + @JWTAuthenticated @OpenAPI(hidden = true) private fun updateEpisode( @PathParam("uuid") uuid: UUID, @@ -93,7 +93,7 @@ class EpisodeMappingController : HasPageableRoute() { @Path("/{uuid}") @Delete - @AdminSessionAuthenticated + @JWTAuthenticated @OpenAPI(hidden = true) private fun deleteEpisode(@PathParam("uuid") uuid: UUID): Response { episodeMappingService.delete(episodeMappingService.find(uuid) ?: return Response.notFound()) diff --git a/src/main/kotlin/fr/shikkanime/modules/Routing.kt b/src/main/kotlin/fr/shikkanime/modules/Routing.kt index f28b57e0..f23e5153 100644 --- a/src/main/kotlin/fr/shikkanime/modules/Routing.kt +++ b/src/main/kotlin/fr/shikkanime/modules/Routing.kt @@ -78,10 +78,11 @@ private fun setSecurityHeaders(call: ApplicationCall, configCacheService: Config context.response.header( "Content-Security-Policy", - "default-src 'self'; font-src 'self';" + - "style-src 'self' 'unsafe-inline' 'unsafe-eval';" + - "script-src 'self' 'unsafe-inline' 'unsafe-eval';" + - "img-src data: 'self' ${Constant.apiUrl} ${Constant.baseUrl};" + + "default-src 'self';" + + "style-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net;" + + "font-src 'self' https://cdn.jsdelivr.net; " + + "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net;" + + "img-src data: 'self' 'unsafe-inline' 'unsafe-eval' ${Constant.apiUrl} ${Constant.baseUrl};" + "connect-src 'self' ${Constant.apiUrl} ${configCacheService.getValueAsString(ConfigPropertyKey.ANALYTICS_API) ?: ""};" ) @@ -199,6 +200,8 @@ private suspend fun handleTemplateResponse( val map = response.data as Map // NOSONAR val modelMap = (map["model"] as Map).toMutableMap() // NOSONAR setGlobalAttributes(modelMap, controller, replacedPath, map["title"] as String?) + call.principal()?.token?.let { modelMap["token"] = it } + call.respond(response.status, FreeMarkerContent(map["template"] as String, modelMap, "", response.contentType)) } diff --git a/src/main/kotlin/fr/shikkanime/modules/Security.kt b/src/main/kotlin/fr/shikkanime/modules/Security.kt index 3628650e..2353587e 100644 --- a/src/main/kotlin/fr/shikkanime/modules/Security.kt +++ b/src/main/kotlin/fr/shikkanime/modules/Security.kt @@ -46,12 +46,18 @@ private fun setupJWTVerifier(): JWTVerifier = JWT .build() private fun AuthenticationConfig.setupJWTAuthentication(jwtVerifier: JWTVerifier) { - jwt { + jwt("auth-jwt") { realm = Constant.jwtRealm verifier(jwtVerifier) validate { credential -> if (credential.payload.audience.contains(Constant.jwtAudience)) JWTPrincipal(credential.payload) else null } + challenge { _, _ -> + call.respond( + HttpStatusCode.Unauthorized, + MessageDto(MessageDto.Type.ERROR, "You are not authorized to access this page") + ) + } } } diff --git a/src/main/resources/assets/js/main.js b/src/main/resources/assets/js/main.js index c916112f..ad0647d1 100644 --- a/src/main/resources/assets/js/main.js +++ b/src/main/resources/assets/js/main.js @@ -9,8 +9,9 @@ function copyToClipboard(content) { } async function callApi(url, options = {}) { - const {abortSignal, method = 'GET', body = null} = options; + const {abortSignal, method = 'GET', authorization = null, body = null} = options; const headers = {'Content-Type': 'application/json'}; + if (authorization) headers['Authorization'] = 'Bearer ' + authorization; const fetchOptions = {headers, signal: abortSignal, method}; if (method !== 'GET') fetchOptions.body = JSON.stringify(body); diff --git a/src/main/resources/templates/_freemarker_implicit.ftl b/src/main/resources/templates/_freemarker_implicit.ftl index b0782f33..f7768501 100644 --- a/src/main/resources/templates/_freemarker_implicit.ftl +++ b/src/main/resources/templates/_freemarker_implicit.ftl @@ -15,6 +15,7 @@ [#-- @ftlvariable name="selectedSimulcast" type="fr.shikkanime.dtos.SimulcastDto" --] [#-- @ftlvariable name="googleSiteVerification" type="java.lang.String" --] [#-- @ftlvariable name="seoDescription" type="java.lang.String" --] +[#-- @ftlvariable name="token" type="java.lang.String" --] [#-- @ftlvariable name="currentSimulcast" type="fr.shikkanime.dtos.SimulcastDto" --] [#-- @ftlvariable name="footerLinks" type="kotlin.collections.AbstractList" --] [#-- @ftlvariable name="seoLinks" type="kotlin.collections.AbstractList" --] diff --git a/src/main/resources/templates/admin/animes/edit.ftl b/src/main/resources/templates/admin/animes/edit.ftl index 47e1e583..e063ffb3 100644 --- a/src/main/resources/templates/admin/animes/edit.ftl +++ b/src/main/resources/templates/admin/animes/edit.ftl @@ -208,7 +208,7 @@ async function loadAnime() { const uuid = getUuid(); - return await callApi('/api/v1/animes/' + uuid); + return await callApi('/api/v1/animes/' + uuid, {authorization: '${token}'}); } async function updateAnime(anime) { @@ -216,6 +216,7 @@ await callApi('/api/v1/animes/' + uuid, { method: 'PUT', + authorization: '${token}', body: anime }) .then(() => { @@ -234,7 +235,8 @@ const uuid = getUuid(); await callApi('/api/v1/animes/' + uuid, { - method: 'DELETE' + method: 'DELETE', + authorization: '${token}' }).catch(() => { const toastEl = document.getElementById('errorToast'); const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastEl) diff --git a/src/main/resources/templates/admin/configs.ftl b/src/main/resources/templates/admin/configs.ftl index 1984893f..7849b04e 100644 --- a/src/main/resources/templates/admin/configs.ftl +++ b/src/main/resources/templates/admin/configs.ftl @@ -73,12 +73,13 @@ params = '?name=' + name; } - return await callApi('/api/config' + params); + return await callApi('/api/config' + params, {authorization: '${token}'}); } async function updateConfig(config) { await callApi('/api/config/' + config.uuid, { method: 'PUT', + authorization: '${token}', body: config }) .then(() => { diff --git a/src/main/resources/templates/admin/episodes/edit.ftl b/src/main/resources/templates/admin/episodes/edit.ftl index 9c020a35..65ae6f58 100644 --- a/src/main/resources/templates/admin/episodes/edit.ftl +++ b/src/main/resources/templates/admin/episodes/edit.ftl @@ -217,7 +217,7 @@ async function loadEpisode() { const uuid = getUuid(); - return await callApi('/api/v1/episode-mappings/' + uuid); + return await callApi('/api/v1/episode-mappings/' + uuid, {authorization: '${token}'}); } async function updateEpisode(episode) { @@ -225,6 +225,7 @@ await callApi('/api/v1/episode-mappings/' + uuid, { method: 'PUT', + authorization: '${token}', body: episode }) .then(() => { @@ -243,7 +244,8 @@ const uuid = getUuid(); await callApi('/api/v1/episode-mappings/' + uuid, { - method: 'DELETE' + method: 'DELETE', + authorization: '${token}' }).catch(() => { const toastEl = document.getElementById('errorToast'); const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastEl) diff --git a/src/main/resources/templates/site/_layout.ftl b/src/main/resources/templates/site/_layout.ftl index 9a4f47f8..dbdd86e6 100644 --- a/src/main/resources/templates/site/_layout.ftl +++ b/src/main/resources/templates/site/_layout.ftl @@ -51,8 +51,8 @@ - - + + <#if (analyticsDomain?? && analyticsDomain?length != 0) && (analyticsApi?? && analyticsApi?length != 0) && (analyticsScript?? && analyticsScript?length != 0)> diff --git a/src/main/resources/templates/site/catalog.ftl b/src/main/resources/templates/site/catalog.ftl index 85fee1e7..8564c116 100644 --- a/src/main/resources/templates/site/catalog.ftl +++ b/src/main/resources/templates/site/catalog.ftl @@ -20,7 +20,7 @@ -
+
<#list animes as anime> <@animeComponent.display anime=anime /> diff --git a/src/main/resources/templates/site/home.ftl b/src/main/resources/templates/site/home.ftl index 217b048e..32e5a0a3 100644 --- a/src/main/resources/templates/site/home.ftl +++ b/src/main/resources/templates/site/home.ftl @@ -45,7 +45,7 @@
<#if animes?? && animes?size != 0> -
+
<#list animes as anime> <@animeComponent.display anime=anime /> diff --git a/src/main/resources/templates/site/search.ftl b/src/main/resources/templates/site/search.ftl index d40fd992..d40b1710 100644 --- a/src/main/resources/templates/site/search.ftl +++ b/src/main/resources/templates/site/search.ftl @@ -7,7 +7,7 @@ placeholder="Rechercher" autofocus @input="animes = (await search($event.target.value)).data">
-
+