diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 06d3ded3..d2ba3b56 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,115 +7,39 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ "17", "21" ] + database: [ "h2", "postgresql", "mysql", "mariadb" ] steps: + # Checkout + - name: Configure git to checkout as-is + run: git config --global core.autocrlf false + - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 # Setup - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Set up Java - uses: actions/setup-java@v4 + - name: Set up GraalVM + uses: graalvm/setup-graalvm@v1 with: - java-version: ${{ matrix.java }} - distribution: "temurin" + java-version: "21" + distribution: "graalvm" cache: "maven" # Build & Test - name: Build with Maven - run: mvn --batch-mode --no-transfer-progress install - env: - TEST_DATABASE: h2 - - - name: Run integration tests on PostgreSQL - run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify - env: - TEST_DATABASE: postgresql - - - name: Run integration tests on MySQL - run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify - env: - TEST_DATABASE: mysql - - - name: Run integration tests on MariaDB - run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify - env: - TEST_DATABASE: mariadb - - - name: Run integration tests with Redis cache enabled - run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify - env: - TEST_DATABASE: h2 - REDIS: true + run: mvn --batch-mode --no-transfer-progress install -Pnative -D"quarkus.datasource.db-kind"=${{ matrix.database }} # Upload artifacts - - name: Upload JAR - uses: actions/upload-artifact@v4 - if: ${{ matrix.java == '17' }} - with: - name: commafeed.jar - path: commafeed-server/target/commafeed.jar - - - name: Upload Playwright artifacts - if: failure() + - name: Upload cross-platform app uses: actions/upload-artifact@v4 with: - name: playwright-artifacts - path: | - **/target/playwright-artifacts/ + name: commafeed-${{ matrix.database }}-jvm + path: commafeed-server/target/commafeed-*.zip - # Docker - - name: Login to Container Registry - uses: docker/login-action@v3 - if: ${{ matrix.java == '17' && (github.ref_type == 'tag' || github.ref_name == 'master') }} + - name: Upload native executable + uses: actions/upload-artifact@v4 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Docker build and push tag - uses: docker/build-push-action@v6 - if: ${{ matrix.java == '17' && github.ref_type == 'tag' }} - with: - context: . - push: true - platforms: linux/amd64,linux/arm64/v8 - tags: | - athou/commafeed:latest - athou/commafeed:${{ github.ref_name }} - - - name: Docker build and push master - uses: docker/build-push-action@v6 - if: ${{ matrix.java == '17' && github.ref_name == 'master' }} - with: - context: . - push: true - platforms: linux/amd64,linux/arm64/v8 - tags: athou/commafeed:master - - # Create GitHub release after Docker image has been published - - name: Extract Changelog Entry - uses: mindsers/changelog-reader-action@v2 - if: ${{ matrix.java == '17' && github.ref_type == 'tag' }} - id: changelog_reader - with: - version: ${{ github.ref_name }} - - - name: Create GitHub release - uses: softprops/action-gh-release@v2 - if: ${{ matrix.java == '17' && github.ref_type == 'tag' }} - with: - name: CommaFeed ${{ github.ref_name }} - body: ${{ steps.changelog_reader.outputs.changes }} - draft: false - prerelease: false - files: | - commafeed-server/target/commafeed.jar - commafeed-server/config.yml.example + name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }} + path: commafeed-server/target/commafeed-*-runner diff --git a/commafeed-client/pom.xml b/commafeed-client/pom.xml index 7049bbff..c55058fb 100644 --- a/commafeed-client/pom.xml +++ b/commafeed-client/pom.xml @@ -80,7 +80,7 @@ copy-resources - ${project.build.directory}/classes/assets + ${project.build.directory}/classes/META-INF/resources dist diff --git a/commafeed-client/src/app/client.ts b/commafeed-client/src/app/client.ts index 3e9a7cd1..e34759ed 100644 --- a/commafeed-client/src/app/client.ts +++ b/commafeed-client/src/app/client.ts @@ -81,7 +81,17 @@ export const client = { }, }, user: { - login: async (req: LoginRequest) => await axiosInstance.post("user/login", req), + login: async (req: LoginRequest) => { + const formData = new URLSearchParams() + formData.append("j_username", req.name) + formData.append("j_password", req.password) + return await axiosInstance.post("j_security_check", formData, { + baseURL: ".", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }) + }, register: async (req: RegistrationRequest) => await axiosInstance.post("user/register", req), passwordReset: async (req: PasswordResetRequest) => await axiosInstance.post("user/passwordReset", req), getSettings: async () => await axiosInstance.get("user/settings"), diff --git a/commafeed-client/src/pages/auth/RegistrationPage.tsx b/commafeed-client/src/pages/auth/RegistrationPage.tsx index a5570687..23dd8fc0 100644 --- a/commafeed-client/src/pages/auth/RegistrationPage.tsx +++ b/commafeed-client/src/pages/auth/RegistrationPage.tsx @@ -24,12 +24,18 @@ export function RegistrationPage() { }, }) - const register = useAsyncCallback(client.user.register, { + const login = useAsyncCallback(client.user.login, { onSuccess: () => { dispatch(redirectToRootCategory()) }, }) + const register = useAsyncCallback(client.user.register, { + onSuccess: () => { + login.execute(form.values) + }, + }) + return ( @@ -50,6 +56,12 @@ export function RegistrationPage() { )} + {login.error && ( + + + + )} +
@@ -68,7 +80,7 @@ export function RegistrationPage() { size="md" required /> -
diff --git a/commafeed-client/vite.config.ts b/commafeed-client/vite.config.ts index eaba0d76..4abeea41 100644 --- a/commafeed-client/vite.config.ts +++ b/commafeed-client/vite.config.ts @@ -32,6 +32,7 @@ export default defineConfig(env => ({ "/openapi.json": "http://localhost:8083", "/custom_css.css": "http://localhost:8083", "/custom_js.js": "http://localhost:8083", + "/j_security_check": "http://localhost:8083", "/logout": "http://localhost:8083", }, }, diff --git a/commafeed-server/TODO.md b/commafeed-server/TODO.md new file mode 100644 index 00000000..3a14ea57 --- /dev/null +++ b/commafeed-server/TODO.md @@ -0,0 +1,30 @@ +TODO +---- + +MVP: + +- quarkus mailer for smtp + - https://quarkus.io/guides/mailer +- cookie duration too short + - https://github.com/quarkusio/quarkus/issues/42463 + +- update dockerfile +- update github actions (build and copy outside target each database artifact) +- update readme +- update release notes (+ mention h2 migration has been removed, upgrade to last 4.x is required) + +Nice to have: + +- find a better way to scan rome classes +- remove suppresswarnings "deprecation" +- remove rest assured or use only rest assured +- rename "servlets" since they are now rest endpoints +- warnings hibernate on startup +- OPML encoding is not handled correctly +- remove Timers metrics page + +native-image +------------- + +- https://www.graalvm.org/latest/reference-manual/native-image/dynamic-features/Resources/ +- https://github.com/rometools/rome/pull/636/files \ No newline at end of file diff --git a/commafeed-server/config.dev.yml b/commafeed-server/config.dev.yml deleted file mode 100644 index 600490ea..00000000 --- a/commafeed-server/config.dev.yml +++ /dev/null @@ -1,157 +0,0 @@ -# CommaFeed settings -# ------------------ -app: - # url used to access commafeed - publicUrl: http://localhost:8082/ - - # whether to expose a robots.txt file that disallows web crawlers and search engine indexers - hideFromWebCrawlers: true - - # whether to allow user registrations - allowRegistrations: true - - # whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char) - strictPasswordPolicy: true - - # create a demo account the first time the app starts - createDemoAccount: true - - # put your google analytics tracking code here - googleAnalyticsTrackingCode: - - # put your google server key (used for youtube favicon fetching) - googleAuthKey: - - # number of http threads - backgroundThreads: 3 - - # number of database updating threads - databaseUpdateThreads: 1 - - # rows to delete per query while cleaning up old entries - databaseCleanupBatchSize: 100 - - # settings for sending emails (password recovery) - smtpHost: localhost - smtpPort: 25 - smtpTls: false - smtpUserName: user - smtpPassword: pass - smtpFromAddress: - - # Graphite Metric settings - # Allows those who use Graphite to have CommaFeed send metrics for graphing (time in seconds) - graphiteEnabled: false - graphitePrefix: "test.commafeed" - graphiteHost: "localhost" - graphitePort: 2003 - graphiteInterval: 60 - - # whether this commafeed instance has a lot of feeds to refresh - # leave this to false in almost all cases - heavyLoad: false - - # minimum amount of time commafeed will wait before refreshing the same feed - refreshIntervalMinutes: 5 - - # if enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser - # useful if commafeed is usually accessed through a restricting proxy - imageProxyEnabled: true - - # database query timeout (in milliseconds), 0 to disable - queryTimeout: 0 - - # time to keep unread statuses (in days), 0 to disable - keepStatusDays: 0 - - # entries to keep per feed, old entries will be deleted, 0 to disable - maxFeedCapacity: 500 - - # entries older than this will be deleted, 0 to disable - maxEntriesAgeDays: 365 - - # limit the number of feeds a user can subscribe to, 0 to disable - maxFeedsPerUser: 0 - - # don't parse feeds that are too large to prevent memory issues - maxFeedResponseSize: 5M - - # cache service to use, possible values are 'noop' and 'redis' - cache: noop - - # announcement string displayed on the main page - announcement: - - # user-agent string that will be used by the http client, leave empty for the default one - userAgent: - - # enable websocket connection so the server can notify the web client that there are new entries for your feeds - websocketEnabled: true - - # interval at which the client will send a ping message on the websocket to keep the connection alive - websocketPingInterval: 15m - - # if websocket is disabled or the connection is lost, the client will reload the feed tree at this interval - treeReloadInterval: 30s - -# Database connection -# ------------------- -# for MariaDB -# driverClass is org.mariadb.jdbc.Driver -# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC -# -# for MySQL -# driverClass is com.mysql.cj.jdbc.Driver -# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC -# -# for PostgreSQL -# driverClass is org.postgresql.Driver -# url is jdbc:postgresql://localhost:5432/commafeed - -database: - driverClass: org.h2.Driver - url: jdbc:h2:./target/commafeed - user: sa - password: sa - properties: - charSet: UTF-8 - validationQuery: "/* CommaFeed Health Check */ SELECT 1" - -server: - applicationConnectors: - - type: http - port: 8083 - adminConnectors: - - type: http - port: 8084 - -logging: - level: INFO - loggers: - com.commafeed: DEBUG - liquibase: INFO - org.hibernate.SQL: INFO # or ALL for sql debugging - org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN - appenders: - - type: console - - type: file - currentLogFilename: log/commafeed.log - threshold: ALL - archive: true - archivedLogFilenamePattern: log/commafeed-%d.log - archivedFileCount: 5 - timeZone: UTC - -# Redis pool configuration -# (only used if app.cache is 'redis') -# ----------------------------------- -redis: - host: localhost - port: 6379 - # username is only required when using ACLs - username: - password: - timeout: 2000 - database: 0 - maxTotal: 500 - \ No newline at end of file diff --git a/commafeed-server/config.yml.example b/commafeed-server/config.yml.example deleted file mode 100644 index 8568b50d..00000000 --- a/commafeed-server/config.yml.example +++ /dev/null @@ -1,158 +0,0 @@ -# CommaFeed settings -# ------------------ -app: - # url used to access commafeed - publicUrl: http://localhost:8082/ - - # whether to expose a robots.txt file that disallows web crawlers and search engine indexers - hideFromWebCrawlers: true - - # whether to allow user registrations - allowRegistrations: false - - # whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char) - strictPasswordPolicy: true - - # create a demo account the first time the app starts - createDemoAccount: false - - # put your google analytics tracking code here - googleAnalyticsTrackingCode: - - # put your google server key (used for youtube favicon fetching) - googleAuthKey: - - # number of http threads - backgroundThreads: 3 - - # number of database updating threads - databaseUpdateThreads: 1 - - # rows to delete per query while cleaning up old entries - databaseCleanupBatchSize: 100 - - # settings for sending emails (password recovery) - smtpHost: - smtpPort: - smtpTls: false - smtpUserName: - smtpPassword: - smtpFromAddress: - - # Graphite Metric settings - # Allows those who use Graphite to have CommaFeed send metrics for graphing (time in seconds) - graphiteEnabled: false - graphitePrefix: "test.commafeed" - graphiteHost: "localhost" - graphitePort: 2003 - graphiteInterval: 60 - - # whether this commafeed instance has a lot of feeds to refresh - # leave this to false in almost all cases - heavyLoad: false - - # minimum amount of time commafeed will wait before refreshing the same feed - refreshIntervalMinutes: 5 - - # if enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser - # useful if commafeed is usually accessed through a restricting proxy - imageProxyEnabled: false - - # database query timeout (in milliseconds), 0 to disable - queryTimeout: 0 - - # time to keep unread statuses (in days), 0 to disable - keepStatusDays: 0 - - # entries to keep per feed, old entries will be deleted, 0 to disable - maxFeedCapacity: 500 - - # entries older than this will be deleted, 0 to disable - maxEntriesAgeDays: 365 - - # limit the number of feeds a user can subscribe to, 0 to disable - maxFeedsPerUser: 0 - - # don't parse feeds that are too large to prevent memory issues - maxFeedResponseSize: 5M - - # cache service to use, possible values are 'noop' and 'redis' - cache: noop - - # announcement string displayed on the main page - announcement: - - # user-agent string that will be used by the http client, leave empty for the default one - userAgent: - - # enable websocket connection so the server can notify the web client that there are new entries for your feeds - websocketEnabled: true - - # interval at which the client will send a ping message on the websocket to keep the connection alive - websocketPingInterval: 15m - - # if websocket is disabled or the connection is lost, the client will reload the feed tree at this interval - treeReloadInterval: 30s - -# Database connection -# ------------------- -# for MariaDB -# driverClass is org.mariadb.jdbc.Driver -# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC -# -# for MySQL -# driverClass is com.mysql.cj.jdbc.Driver -# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC -# -# for PostgreSQL -# driverClass is org.postgresql.Driver -# url is jdbc:postgresql://localhost:5432/commafeed - -database: - driverClass: org.h2.Driver - url: jdbc:h2:/commafeed/data/db;DEFRAG_ALWAYS=TRUE - user: sa - password: sa - properties: - charSet: UTF-8 - validationQuery: "/* CommaFeed Health Check */ SELECT 1" - minSize: 1 - maxSize: 50 - maxConnectionAge: 30m - -server: - applicationConnectors: - - type: http - port: 8082 - adminConnectors: [ ] - requestLog: - appenders: [ ] - -logging: - level: ERROR - loggers: - com.commafeed: INFO - liquibase: INFO - io.dropwizard.server.ServerFactory: INFO - appenders: - - type: console - - type: file - currentLogFilename: log/commafeed.log - threshold: ALL - archive: true - archivedLogFilenamePattern: log/commafeed-%d.log - archivedFileCount: 5 - timeZone: UTC - -# Redis pool configuration -# (only used if app.cache is 'redis') -# ----------------------------------- -redis: - host: localhost - port: 6379 - # username is only required when using ACLs - username: - password: - timeout: 2000 - database: 0 - maxTotal: 500 diff --git a/commafeed-server/docker-compose.dev.yml b/commafeed-server/docker-compose.dev.yml index 9fdfb1a6..29b9c6f8 100644 --- a/commafeed-server/docker-compose.dev.yml +++ b/commafeed-server/docker-compose.dev.yml @@ -4,7 +4,7 @@ services: mysql: image: mariadb environment: - - MYSQL_ROOT_PASSWORD=root + - MYSQL_ROOT_PASSWORD=commafeed - MYSQL_DATABASE=commafeed ports: - "3306:3306" @@ -12,8 +12,8 @@ services: postgresql: image: postgres environment: - - POSTGRES_USER=root - - POSTGRES_PASSWORD=root + - POSTGRES_USER=commafeed + - POSTGRES_PASSWORD=commafeed - POSTGRES_DB=commafeed ports: - "5432:5432" diff --git a/commafeed-server/pom.xml b/commafeed-server/pom.xml index 8522cfad..4ab11efd 100644 --- a/commafeed-server/pom.xml +++ b/commafeed-server/pom.xml @@ -12,28 +12,20 @@ CommaFeed Server - 7.0.0 + 3.13.1 6.6 2.1.0 - 1.78.1 + 3.1.8 - 1.20.1 - - 16.4 - - 9.0.1 - - 11.4.3 - - 7.4.0 + h2 - io.dropwizard - dropwizard-dependencies - 4.0.7 + io.quarkus.platform + quarkus-bom + ${quarkus.version} pom import @@ -41,26 +33,67 @@ - commafeed - - - src/test/resources - false - - - src/test/resources - - docker-images.properties - - true - - - + + + kr.motd.maven + os-maven-plugin + 1.7.1 + + + + io.quarkus.platform + quarkus-maven-plugin + ${quarkus.version} + true + + + + build + generate-code + generate-code-tests + native-image-agent + + + + commafeed-${project.version} + + -${quarkus.datasource.db-kind}-${os.detected.name}-${os.detected.arch}-runner + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.7.1 + + + package + + single + + + commafeed-${project.version}-${quarkus.datasource.db-kind} + false + + src/main/assembly/zip-quarkus-app.xml + + + + + org.apache.maven.plugins maven-surefire-plugin 3.3.1 + + + org.jboss.logmanager.LogManager + + org.apache.maven.plugins @@ -74,6 +107,13 @@ + + + ${project.build.directory}/${project.build.finalName}-runner + + org.jboss.logmanager.LogManager + + io.github.git-commit-id @@ -94,63 +134,13 @@ false - - org.apache.maven.plugins - maven-shade-plugin - 3.6.0 - - - org.kordamp.shade - maven-shade-ext-transformers - 1.4.0 - - - - false - - - *:* - - module-info.class - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - - - - package - - shade - - - - - - com.commafeed.CommaFeedApplication - - - - rome.properties - - append - - - - - - io.swagger.core.v3 swagger-maven-plugin-jakarta 2.2.22 - ${project.build.directory}/classes/assets + ${project.build.directory}/classes/META-INF/resources JSONANDYAML com.commafeed.frontend.resource @@ -168,18 +158,6 @@ - - org.apache.maven.plugins - maven-jar-plugin - 3.4.2 - - - - true - - - - org.apache.maven.plugins maven-checkstyle-plugin @@ -247,60 +225,65 @@ provided - org.slf4j - slf4j-api - - - org.slf4j - jcl-over-slf4j + org.kohsuke.metainf-services + metainf-services + 1.11 + provided - com.google.inject - guice - ${guice.version} + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-security + + + io.quarkus + quarkus-hibernate-validator + + + io.quarkus + quarkus-rest + + + io.quarkus + quarkus-rest-jackson + + + io.quarkus + quarkus-websockets + + + io.quarkus + quarkus-hibernate-orm + + + io.quarkus + quarkus-liquibase + + + io.quarkus + quarkus-jdbc-h2 + + + io.quarkus + quarkus-jdbc-mysql + + + io.quarkus + quarkus-jdbc-mariadb + + + io.quarkus + quarkus-jdbc-postgresql - - io.dropwizard - dropwizard-core - - - io.dropwizard - dropwizard-unix-socket - - - io.dropwizard - dropwizard-hibernate - - - io.dropwizard - dropwizard-migrations - - - io.dropwizard - dropwizard-assets - - - io.dropwizard - dropwizard-forms - - - io.dropwizard.metrics - metrics-graphite - io.dropwizard.metrics metrics-json - - - io.whitfin - dropwizard-environment-substitutor - 1.1.1 - - - org.eclipse.jetty.websocket - websocket-jakarta-server + 4.2.26 @@ -357,7 +340,6 @@ com.sun.mail jakarta.mail - 2.0.1 @@ -409,7 +391,15 @@ org.apache.httpcomponents.client5 httpclient5 + 5.3.1 + + + org.brotli + dec + 0.1.2 + + io.github.hakky54 sslcontext-kickstart-for-apache5 @@ -423,35 +413,8 @@ - com.h2database - h2 - 2.3.232 - - - com.manticore-projects.tools - h2migrationtool - 1.7 - - - - com.mysql - mysql-connector-j - 9.0.0 - - - org.mariadb.jdbc - mariadb-java-client - 3.4.1 - - - org.postgresql - postgresql - 42.7.3 - - - - org.junit.jupiter - junit-jupiter-engine + io.quarkus + quarkus-junit5 test @@ -464,39 +427,41 @@ mockito-junit-jupiter test - org.mock-server mockserver-junit-jupiter 5.15.0 test - - - org.bouncycastle - bcprov-jdk18on - ${bouncycastle.version} + org.glassfish.jersey.core + jersey-client + ${jersey.version} test - org.bouncycastle - bcpkix-jdk18on - ${bouncycastle.version} + org.glassfish.jersey.media + jersey-media-multipart + ${jersey.version} + test + + + org.glassfish.jersey.media + jersey-media-json-jackson + ${jersey.version} + test + + + io.rest-assured + rest-assured test - com.icegreen greenmail-junit5 2.0.1 test - - io.dropwizard - dropwizard-testing - test - org.awaitility awaitility @@ -508,30 +473,19 @@ 1.46.0 test - - - org.testcontainers - testcontainers - ${testcontainers.version} - test - - - org.testcontainers - postgresql - ${testcontainers.version} - test - - - org.testcontainers - mysql - ${testcontainers.version} - test - - - org.testcontainers - mariadb - ${testcontainers.version} - test - + + + + native + + + native + + + + true + + + diff --git a/commafeed-server/src/main/assembly/zip-quarkus-app.xml b/commafeed-server/src/main/assembly/zip-quarkus-app.xml new file mode 100644 index 00000000..e35ab082 --- /dev/null +++ b/commafeed-server/src/main/assembly/zip-quarkus-app.xml @@ -0,0 +1,21 @@ + + + zip-quarkus-app + + true + commafeed-${project.version}-${quarkus.datasource.db-kind} + + + zip + + + + ${project.build.directory}/quarkus-app + / + + **/* + + + + \ No newline at end of file diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java index 4bd7183f..222b6548 100644 --- a/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java @@ -1,277 +1,44 @@ package com.commafeed; -import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; import java.time.Instant; -import java.util.EnumSet; -import java.util.Set; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import org.eclipse.jetty.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer; -import org.hibernate.cfg.AvailableSettings; - -import com.codahale.metrics.json.MetricsModule; -import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.feed.FeedRefreshEngine; -import com.commafeed.backend.model.AbstractModel; -import com.commafeed.backend.model.Feed; -import com.commafeed.backend.model.FeedCategory; -import com.commafeed.backend.model.FeedEntry; -import com.commafeed.backend.model.FeedEntryContent; -import com.commafeed.backend.model.FeedEntryStatus; -import com.commafeed.backend.model.FeedEntryTag; -import com.commafeed.backend.model.FeedSubscription; -import com.commafeed.backend.model.User; -import com.commafeed.backend.model.UserRole; -import com.commafeed.backend.model.UserSettings; -import com.commafeed.backend.service.UserService; import com.commafeed.backend.service.db.DatabaseStartupService; -import com.commafeed.backend.service.db.H2MigrationService; -import com.commafeed.backend.task.ScheduledTask; -import com.commafeed.frontend.auth.PasswordConstraintValidator; -import com.commafeed.frontend.auth.SecurityCheckFactoryProvider; -import com.commafeed.frontend.resource.AdminREST; -import com.commafeed.frontend.resource.CategoryREST; -import com.commafeed.frontend.resource.EntryREST; -import com.commafeed.frontend.resource.FeedREST; -import com.commafeed.frontend.resource.ServerREST; -import com.commafeed.frontend.resource.UserREST; -import com.commafeed.frontend.resource.fever.FeverREST; -import com.commafeed.frontend.servlet.CustomCssServlet; -import com.commafeed.frontend.servlet.CustomJsServlet; -import com.commafeed.frontend.servlet.LogoutServlet; -import com.commafeed.frontend.servlet.NextUnreadServlet; -import com.commafeed.frontend.servlet.RobotsTxtDisallowAllServlet; -import com.commafeed.frontend.session.SessionHelperFactoryProvider; -import com.commafeed.frontend.ws.WebSocketConfigurator; -import com.commafeed.frontend.ws.WebSocketEndpoint; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.MapperFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.google.inject.Guice; -import com.google.inject.Injector; -import com.google.inject.Key; -import com.google.inject.TypeLiteral; +import com.commafeed.backend.task.TaskScheduler; +import com.commafeed.security.password.PasswordConstraintValidator; -import io.dropwizard.assets.AssetsBundle; -import io.dropwizard.configuration.DefaultConfigurationFactoryFactory; -import io.dropwizard.configuration.EnvironmentVariableSubstitutor; -import io.dropwizard.configuration.SubstitutingSourceProvider; -import io.dropwizard.core.Application; -import io.dropwizard.core.ConfiguredBundle; -import io.dropwizard.core.setup.Bootstrap; -import io.dropwizard.core.setup.Environment; -import io.dropwizard.db.DataSourceFactory; -import io.dropwizard.forms.MultiPartBundle; -import io.dropwizard.hibernate.HibernateBundle; -import io.dropwizard.migrations.MigrationsBundle; -import io.dropwizard.servlets.CacheBustingFilter; -import io.whitfin.dropwizard.configuration.EnvironmentSubstitutor; -import jakarta.servlet.DispatcherType; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.websocket.server.ServerEndpointConfig; +import io.quarkus.runtime.ShutdownEvent; +import io.quarkus.runtime.StartupEvent; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Singleton; +import lombok.RequiredArgsConstructor; -public class CommaFeedApplication extends Application { +@Singleton +@RequiredArgsConstructor +public class CommaFeedApplication { public static final String USERNAME_ADMIN = "admin"; public static final String USERNAME_DEMO = "demo"; public static final Instant STARTUP_TIME = Instant.now(); - private HibernateBundle hibernateBundle; + private final DatabaseStartupService databaseStartupService; + private final FeedRefreshEngine feedRefreshEngine; + private final TaskScheduler taskScheduler; + private final CommaFeedConfiguration config; - @Override - public String getName() { - return "CommaFeed"; + public void start(@Observes StartupEvent ev) { + PasswordConstraintValidator.setStrict(config.users().strictPasswordPolicy()); + + databaseStartupService.populateInitialData(); + + feedRefreshEngine.start(); + taskScheduler.start(); } - @Override - public void initialize(Bootstrap bootstrap) { - configureEnvironmentSubstitutor(bootstrap); - configureObjectMapper(bootstrap.getObjectMapper()); - - // run h2 migration as the first bundle because we need to migrate before hibernate is initialized - bootstrap.addBundle(new ConfiguredBundle<>() { - @Override - public void run(CommaFeedConfiguration config, Environment environment) { - DataSourceFactory dataSourceFactory = config.getDataSourceFactory(); - String url = dataSourceFactory.getUrl(); - if (isFileBasedH2(url)) { - Path path = getFilePath(url); - String user = dataSourceFactory.getUser(); - String password = dataSourceFactory.getPassword(); - new H2MigrationService().migrateIfNeeded(path, user, password); - } - } - - private boolean isFileBasedH2(String url) { - return url.startsWith("jdbc:h2:") && !url.startsWith("jdbc:h2:mem:"); - } - - private Path getFilePath(String url) { - String name = url.substring("jdbc:h2:".length()).split(";")[0]; - return Paths.get(name + ".mv.db"); - } - }); - - bootstrap.addBundle(hibernateBundle = new HibernateBundle<>(AbstractModel.class, Feed.class, FeedCategory.class, FeedEntry.class, - FeedEntryContent.class, FeedEntryStatus.class, FeedEntryTag.class, FeedSubscription.class, User.class, UserRole.class, - UserSettings.class) { - @Override - public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) { - DataSourceFactory factory = configuration.getDataSourceFactory(); - - factory.getProperties().put(AvailableSettings.PREFERRED_POOLED_OPTIMIZER, "pooled-lo"); - - factory.getProperties().put(AvailableSettings.STATEMENT_BATCH_SIZE, "50"); - factory.getProperties().put(AvailableSettings.BATCH_VERSIONED_DATA, "true"); - factory.getProperties().put(AvailableSettings.ORDER_INSERTS, "true"); - factory.getProperties().put(AvailableSettings.ORDER_UPDATES, "true"); - return factory; - } - }); - - bootstrap.addBundle(new MigrationsBundle<>() { - @Override - public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) { - return configuration.getDataSourceFactory(); - } - }); - - bootstrap.addBundle(new AssetsBundle("/assets/", "/", "index.html")); - bootstrap.addBundle(new MultiPartBundle()); + public void stop(@Observes ShutdownEvent ev) { + feedRefreshEngine.stop(); + taskScheduler.stop(); } - private static void configureEnvironmentSubstitutor(Bootstrap bootstrap) { - bootstrap.setConfigurationFactoryFactory(new DefaultConfigurationFactoryFactory<>() { - @Override - protected ObjectMapper configureObjectMapper(ObjectMapper objectMapper) { - // disable case sensitivity because EnvironmentSubstitutor maps MYPROPERTY to myproperty and not to myProperty - return objectMapper - .setConfig(objectMapper.getDeserializationConfig().with(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)); - } - }); - - bootstrap.setConfigurationSourceProvider(buildEnvironmentSubstitutor(bootstrap)); - } - - private static void configureObjectMapper(ObjectMapper objectMapper) { - // read and write instants as milliseconds instead of nanoseconds - objectMapper.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false) - .configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false); - - // add support for serializing metrics - objectMapper.registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.SECONDS, false)); - } - - private static EnvironmentSubstitutor buildEnvironmentSubstitutor(Bootstrap bootstrap) { - // enable config.yml string substitution - // e.g. having a custom config.yml file with app.session.path=${SOME_ENV_VAR} will substitute SOME_ENV_VAR - SubstitutingSourceProvider substitutingSourceProvider = new SubstitutingSourceProvider(bootstrap.getConfigurationSourceProvider(), - new EnvironmentVariableSubstitutor(false)); - - // enable config.yml properties override with env variables prefixed with CF_ - // e.g. setting CF_APP_ALLOWREGISTRATIONS=true will set app.allowRegistrations to true - return new EnvironmentSubstitutor("CF", substitutingSourceProvider); - } - - @Override - public void run(CommaFeedConfiguration config, Environment environment) { - PasswordConstraintValidator.setStrict(config.getApplicationSettings().getStrictPasswordPolicy()); - - // guice init - Injector injector = Guice.createInjector(new CommaFeedModule(hibernateBundle.getSessionFactory(), config, environment.metrics())); - - // session management - environment.servlets().setSessionHandler(config.getSessionHandlerFactory().build(config.getDataSourceFactory())); - - // support for "@SecurityCheck User user" injection - environment.jersey() - .register(new SecurityCheckFactoryProvider.Binder(injector.getInstance(UserDAO.class), - injector.getInstance(UserService.class), config)); - // support for "@Context SessionHelper sessionHelper" injection - environment.jersey().register(new SessionHelperFactoryProvider.Binder()); - - // REST resources - environment.jersey().setUrlPattern("/rest/*"); - environment.jersey().register(injector.getInstance(AdminREST.class)); - environment.jersey().register(injector.getInstance(CategoryREST.class)); - environment.jersey().register(injector.getInstance(EntryREST.class)); - environment.jersey().register(injector.getInstance(FeedREST.class)); - environment.jersey().register(injector.getInstance(ServerREST.class)); - environment.jersey().register(injector.getInstance(UserREST.class)); - environment.jersey().register(injector.getInstance(FeverREST.class)); - - // Servlets - environment.servlets().addServlet("next", injector.getInstance(NextUnreadServlet.class)).addMapping("/next"); - environment.servlets().addServlet("logout", injector.getInstance(LogoutServlet.class)).addMapping("/logout"); - environment.servlets().addServlet("customCss", injector.getInstance(CustomCssServlet.class)).addMapping("/custom_css.css"); - environment.servlets().addServlet("customJs", injector.getInstance(CustomJsServlet.class)).addMapping("/custom_js.js"); - if (Boolean.TRUE.equals(config.getApplicationSettings().getHideFromWebCrawlers())) { - environment.servlets() - .addServlet("robots.txt", injector.getInstance(RobotsTxtDisallowAllServlet.class)) - .addMapping("/robots.txt"); - } - - // WebSocket endpoint - JakartaWebSocketServletContainerInitializer.configure(environment.getApplicationContext(), (context, container) -> { - container.setDefaultMaxSessionIdleTimeout(config.getApplicationSettings().getWebsocketPingInterval().toMilliseconds() + 10000); - - container.addEndpoint(ServerEndpointConfig.Builder.create(WebSocketEndpoint.class, "/ws") - .configurator(injector.getInstance(WebSocketConfigurator.class)) - .build()); - }); - - // Scheduled tasks - Set tasks = injector.getInstance(Key.get(new TypeLiteral<>() { - })); - ScheduledExecutorService executor = environment.lifecycle() - .scheduledExecutorService("task-scheduler", true) - .threads(tasks.size()) - .build(); - for (ScheduledTask task : tasks) { - task.register(executor); - } - - // database init/changelogs - environment.lifecycle().manage(injector.getInstance(DatabaseStartupService.class)); - - // start feed fetching engine - environment.lifecycle().manage(injector.getInstance(FeedRefreshEngine.class)); - - // prevent caching index.html, so that the webapp is always up to date - environment.servlets() - .addFilter("index-cache-busting-filter", new CacheBustingFilter()) - .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/"); - - // prevent caching openapi files, so that the documentation is always up to date - environment.servlets() - .addFilter("openapi-cache-busting-filter", new CacheBustingFilter()) - .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/openapi.json", "/openapi.yaml"); - - // prevent caching REST resources, except for favicons - environment.servlets().addFilter("rest-cache-busting-filter", new CacheBustingFilter() { - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - String path = ((HttpServletRequest) request).getRequestURI(); - if (path.contains("/feed/favicon")) { - chain.doFilter(request, response); - } else { - super.doFilter(request, response, chain); - } - } - }).addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/rest/*"); - - } - - public static void main(String[] args) throws Exception { - new CommaFeedApplication().run(args); - } } diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java index 845c01cd..eb64073f 100644 --- a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java @@ -1,188 +1,287 @@ package com.commafeed; -import java.io.IOException; -import java.io.InputStream; +import java.time.Duration; import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Properties; +import java.util.Optional; -import com.commafeed.backend.cache.RedisPoolFactory; -import com.commafeed.frontend.session.SessionHandlerFactory; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.commafeed.backend.feed.FeedRefreshIntervalCalculator; -import io.dropwizard.core.Configuration; -import io.dropwizard.db.DataSourceFactory; -import io.dropwizard.util.DataSize; -import io.dropwizard.util.Duration; -import jakarta.validation.Valid; +import io.quarkus.runtime.configuration.MemorySize; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; -import lombok.Getter; -import lombok.Setter; +import redis.clients.jedis.DefaultJedisClientConfig; +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.JedisClientConfig; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.Protocol; -@Getter -@Setter -public class CommaFeedConfiguration extends Configuration { +/** + * CommaFeed configuration + * + * Default values are for production, they can be overridden in application.properties + */ +@ConfigMapping(prefix = "commafeed") +public interface CommaFeedConfiguration { - public enum CacheType { - NOOP, REDIS + /** + * URL used to access commafeed, used for various redirects. + * + */ + @NotBlank + @WithDefault("http://localhost:8082") + String publicUrl(); + + /** + * Whether to expose a robots.txt file that disallows web crawlers and search engine indexers. + */ + @WithDefault("true") + boolean hideFromWebCrawlers(); + + /** + * If enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser. + * + * This is useful if commafeed is accessed through a restricting proxy that blocks some feeds that are followed. + */ + @WithDefault("false") + boolean imageProxyEnabled(); + + /** + * Message displayed in a notification at the bottom of the page. + */ + Optional announcement(); + + /** + * Google Analytics tracking code. + */ + Optional googleAnalyticsTrackingCode(); + + /** + * Google Auth key for fetching Youtube favicons. + */ + Optional googleAuthKey(); + + /** + * Feed refresh engine settings. + */ + FeedRefresh feedRefresh(); + + /** + * Database settings. + */ + Database database(); + + /** + * Users settings. + */ + Users users(); + + /** + * Websocket settings. + */ + Websocket websocket(); + + /** + * SMTP settings for password recovery. + */ + Optional smtp(); + + /** + * Redis settings to enable caching. This is only really useful on instances with a lot of users. + */ + Redis redis(); + + interface FeedRefresh { + /** + * Amount of time CommaFeed will wait before refreshing the same feed. + */ + @WithDefault("5m") + Duration interval(); + + /** + * If true, CommaFeed will calculate the next refresh time based on the feed's average entry interval and the time since the last + * entry was published. See {@link FeedRefreshIntervalCalculator} for details. + */ + @WithDefault("false") + boolean intervalEmpirical(); + + /** + * Amount of http threads used to fetch feeds. + */ + @Min(1) + @WithDefault("3") + int httpThreads(); + + /** + * Amount of threads used to insert new entries in the database. + */ + @Min(1) + @WithDefault("1") + int databaseThreads(); + + /** + * If a feed is larger than this, it will be discarded to prevent memory issues while parsing the feed. + */ + @WithDefault("5M") + MemorySize maxResponseSize(); + + /** + * Duration after which a user is considered inactive. Feeds for inactive users are not refreshed until they log in again. + */ + @WithDefault("0") + Duration userInactivityPeriod(); + + /** + * User-Agent string that will be used by the http client, leave empty for the default one. + */ + Optional userAgent(); } - @Valid - @NotNull - @JsonProperty("database") - private final DataSourceFactory dataSourceFactory = new DataSourceFactory(); + interface Database { + /** + * Database query timeout. + */ + @WithDefault("0") + int queryTimeout(); - @Valid - @NotNull - @JsonProperty("redis") - private final RedisPoolFactory redisPoolFactory = new RedisPoolFactory(); + Cleanup cleanup(); - @Valid - @NotNull - @JsonProperty("session") - private final SessionHandlerFactory sessionHandlerFactory = new SessionHandlerFactory(); + interface Cleanup { + /** + * Maximum age of feed entries in the database. Older entries will be deleted. 0 to disable. + */ + @WithDefault("365d") + Duration entriesMaxAge(); - @Valid - @NotNull - @JsonProperty("app") - private ApplicationSettings applicationSettings; + /** + * Maximum age of feed entry statuses (read/unread) in the database. Older statuses will be deleted. 0 to disable. + */ + @WithDefault("0") + Duration statusesMaxAge(); - private final String version; - private final String gitCommit; + /** + * Maximum number of entries per feed to keep in the database. 0 to disable. + */ + @WithDefault("500") + int maxFeedCapacity(); - public CommaFeedConfiguration() { - Properties properties = new Properties(); - try (InputStream stream = getClass().getResourceAsStream("/git.properties")) { - if (stream != null) { - properties.load(stream); + /** + * Limit the number of feeds a user can subscribe to. 0 to disable. + */ + @WithDefault("0") + int maxFeedsPerUser(); + + /** + * Rows to delete per query while cleaning up old entries. + */ + @Positive + @WithDefault("100") + int batchSize(); + + default Instant statusesInstantThreshold() { + return statusesMaxAge().toMillis() > 0 ? Instant.now().minus(statusesMaxAge()) : null; } - } catch (IOException e) { - throw new RuntimeException(e); } - - this.version = properties.getProperty("git.build.version", "unknown"); - this.gitCommit = properties.getProperty("git.commit.id.abbrev", "unknown"); } - @Getter - @Setter - public static class ApplicationSettings { - @NotNull - @NotBlank - @Valid - private String publicUrl; + interface Users { + /** + * Whether to let users create accounts for themselves. + */ + @WithDefault("false") + boolean allowRegistrations(); - @NotNull - @Valid - private Boolean hideFromWebCrawlers = true; + /** + * Whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char). + */ + @WithDefault("true") + boolean strictPasswordPolicy(); - @NotNull - @Valid - private Boolean allowRegistrations; + /** + * Whether to create a demo account the first time the app starts. + */ + @WithDefault("false") + boolean createDemoAccount(); + } - @NotNull - @Valid - private Boolean strictPasswordPolicy = true; + interface Smtp { + String host(); - @NotNull - @Valid - private Boolean createDemoAccount; + int port(); - private String googleAnalyticsTrackingCode; + boolean tls(); - private String googleAuthKey; + String userName(); - @NotNull - @Min(1) - @Valid - private Integer backgroundThreads; + String password(); - @NotNull - @Min(1) - @Valid - private Integer databaseUpdateThreads; + String fromAddress(); + } - @NotNull - @Positive - @Valid - private Integer databaseCleanupBatchSize = 100; + interface Websocket { + /** + * Enable websocket connection so the server can notify the web client that there are new entries for your feeds. + */ + @WithDefault("true") + boolean enabled(); - private String smtpHost; - private int smtpPort; - private boolean smtpTls; - private String smtpUserName; - private String smtpPassword; - private String smtpFromAddress; + /** + * Interval at which the client will send a ping message on the websocket to keep the connection alive. + */ + @WithDefault("15m") + Duration pingInterval(); - private boolean graphiteEnabled; - private String graphitePrefix; - private String graphiteHost; - private int graphitePort; - private int graphiteInterval; + /** + * If the websocket connection is disabled or the connection is lost, the client will reload the feed tree at this interval. + */ + @WithDefault("30s") + Duration treeReloadInterval(); + } - @NotNull - @Valid - private Boolean heavyLoad; + interface Redis { - @NotNull - @Valid - private Boolean imageProxyEnabled; + Optional host(); - @NotNull - @Min(0) - @Valid - private Integer queryTimeout; + @WithDefault("" + Protocol.DEFAULT_PORT) + int port(); - @NotNull - @Min(0) - @Valid - private Integer keepStatusDays; + /** + * Username is only required when using Redis ACLs + */ + Optional username(); - @NotNull - @Min(0) - @Valid - private Integer maxFeedCapacity; + Optional password(); - @NotNull - @Min(0) - @Valid - private Integer maxEntriesAgeDays = 0; + @WithDefault("" + Protocol.DEFAULT_TIMEOUT) + int timeout(); - @NotNull - @Valid - private Integer maxFeedsPerUser = 0; + @WithDefault("" + Protocol.DEFAULT_DATABASE) + int database(); - @NotNull - @Valid - private DataSize maxFeedResponseSize = DataSize.megabytes(5); + @WithDefault("500") + int maxTotal(); - @NotNull - @Min(0) - @Valid - private Integer refreshIntervalMinutes; + default JedisPool build() { + Optional host = host(); + if (host.isEmpty()) { + throw new IllegalStateException("Redis host is required"); + } - @NotNull - @Valid - private CacheType cache; + JedisPoolConfig poolConfig = new JedisPoolConfig(); + poolConfig.setMaxTotal(maxTotal()); - @Valid - private String announcement; + JedisClientConfig clientConfig = DefaultJedisClientConfig.builder() + .user(username().orElse(null)) + .password(password().orElse(null)) + .timeoutMillis(timeout()) + .database(database()) + .build(); - private String userAgent; - - private Boolean websocketEnabled = true; - - private Duration websocketPingInterval = Duration.minutes(15); - - private Duration treeReloadInterval = Duration.seconds(30); - - public Instant getUnreadThreshold() { - return getKeepStatusDays() > 0 ? Instant.now().minus(getKeepStatusDays(), ChronoUnit.DAYS) : null; + return new JedisPool(poolConfig, new HostAndPort(host.get(), port()), clientConfig); } - } } diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedModule.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedModule.java deleted file mode 100644 index 945aa5b7..00000000 --- a/commafeed-server/src/main/java/com/commafeed/CommaFeedModule.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.commafeed; - -import java.net.InetSocketAddress; -import java.util.concurrent.TimeUnit; - -import org.hibernate.SessionFactory; - -import com.codahale.metrics.MetricFilter; -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.graphite.Graphite; -import com.codahale.metrics.graphite.GraphiteReporter; -import com.commafeed.CommaFeedConfiguration.ApplicationSettings; -import com.commafeed.CommaFeedConfiguration.CacheType; -import com.commafeed.backend.cache.CacheService; -import com.commafeed.backend.cache.NoopCacheService; -import com.commafeed.backend.cache.RedisCacheService; -import com.commafeed.backend.favicon.AbstractFaviconFetcher; -import com.commafeed.backend.favicon.DefaultFaviconFetcher; -import com.commafeed.backend.favicon.FacebookFaviconFetcher; -import com.commafeed.backend.favicon.YoutubeFaviconFetcher; -import com.commafeed.backend.task.DemoAccountCleanupTask; -import com.commafeed.backend.task.EntriesExceedingFeedCapacityCleanupTask; -import com.commafeed.backend.task.OldEntriesCleanupTask; -import com.commafeed.backend.task.OldStatusesCleanupTask; -import com.commafeed.backend.task.OrphanedContentsCleanupTask; -import com.commafeed.backend.task.OrphanedFeedsCleanupTask; -import com.commafeed.backend.task.ScheduledTask; -import com.commafeed.backend.urlprovider.FeedURLProvider; -import com.commafeed.backend.urlprovider.InPageReferenceFeedURLProvider; -import com.commafeed.backend.urlprovider.YoutubeFeedURLProvider; -import com.google.inject.AbstractModule; -import com.google.inject.Provides; -import com.google.inject.multibindings.Multibinder; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@RequiredArgsConstructor -@Slf4j -public class CommaFeedModule extends AbstractModule { - - @Getter(onMethod = @__({ @Provides })) - private final SessionFactory sessionFactory; - - @Getter(onMethod = @__({ @Provides })) - private final CommaFeedConfiguration config; - - @Getter(onMethod = @__({ @Provides })) - private final MetricRegistry metrics; - - @Override - protected void configure() { - CacheService cacheService = config.getApplicationSettings().getCache() == CacheType.NOOP ? new NoopCacheService() - : new RedisCacheService(config.getRedisPoolFactory().build()); - log.info("using cache {}", cacheService.getClass()); - bind(CacheService.class).toInstance(cacheService); - - Multibinder faviconMultibinder = Multibinder.newSetBinder(binder(), AbstractFaviconFetcher.class); - faviconMultibinder.addBinding().to(YoutubeFaviconFetcher.class); - faviconMultibinder.addBinding().to(FacebookFaviconFetcher.class); - faviconMultibinder.addBinding().to(DefaultFaviconFetcher.class); - - Multibinder urlProviderMultibinder = Multibinder.newSetBinder(binder(), FeedURLProvider.class); - urlProviderMultibinder.addBinding().to(InPageReferenceFeedURLProvider.class); - urlProviderMultibinder.addBinding().to(YoutubeFeedURLProvider.class); - - Multibinder taskMultibinder = Multibinder.newSetBinder(binder(), ScheduledTask.class); - taskMultibinder.addBinding().to(OldStatusesCleanupTask.class); - taskMultibinder.addBinding().to(EntriesExceedingFeedCapacityCleanupTask.class); - taskMultibinder.addBinding().to(OldEntriesCleanupTask.class); - taskMultibinder.addBinding().to(OrphanedFeedsCleanupTask.class); - taskMultibinder.addBinding().to(OrphanedContentsCleanupTask.class); - taskMultibinder.addBinding().to(DemoAccountCleanupTask.class); - - ApplicationSettings settings = config.getApplicationSettings(); - - if (settings.isGraphiteEnabled()) { - final String graphitePrefix = settings.getGraphitePrefix(); - final String graphiteHost = settings.getGraphiteHost(); - final int graphitePort = settings.getGraphitePort(); - final int graphiteInterval = settings.getGraphiteInterval(); - - log.info("Graphite Metrics will be sent to host={}, port={}, prefix={}, interval={}sec", graphiteHost, graphitePort, - graphitePrefix, graphiteInterval); - - final Graphite graphite = new Graphite(new InetSocketAddress(graphiteHost, graphitePort)); - final GraphiteReporter reporter = GraphiteReporter.forRegistry(metrics) - .prefixedWith(graphitePrefix) - .convertRatesTo(TimeUnit.SECONDS) - .convertDurationsTo(TimeUnit.MILLISECONDS) - .filter(MetricFilter.ALL) - .build(graphite); - reporter.start(graphiteInterval, TimeUnit.SECONDS); - } - } -} diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedProducers.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedProducers.java new file mode 100644 index 00000000..63df69dd --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedProducers.java @@ -0,0 +1,31 @@ +package com.commafeed; + +import com.codahale.metrics.MetricRegistry; +import com.commafeed.CommaFeedConfiguration.Redis; +import com.commafeed.backend.cache.CacheService; +import com.commafeed.backend.cache.NoopCacheService; +import com.commafeed.backend.cache.RedisCacheService; + +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Singleton; + +@Singleton +public class CommaFeedProducers { + + @Produces + @Singleton + public CacheService cacheService(CommaFeedConfiguration config) { + Redis redis = config.redis(); + if (redis.host().isEmpty()) { + return new NoopCacheService(); + } + + return new RedisCacheService(redis.build()); + } + + @Produces + @Singleton + public MetricRegistry metricRegistry() { + return new MetricRegistry(); + } +} diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedVersion.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedVersion.java new file mode 100644 index 00000000..63188ed5 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedVersion.java @@ -0,0 +1,31 @@ +package com.commafeed; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +import jakarta.inject.Singleton; +import lombok.Getter; + +@Singleton +@Getter +public class CommaFeedVersion { + + private final String version; + private final String gitCommit; + + public CommaFeedVersion() { + Properties properties = new Properties(); + try (InputStream stream = getClass().getResourceAsStream("/git.properties")) { + if (stream != null) { + properties.load(stream); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + this.version = properties.getProperty("git.build.version", "unknown"); + this.gitCommit = properties.getProperty("git.commit.id.abbrev", "unknown"); + } + +} diff --git a/commafeed-server/src/main/java/com/commafeed/ExceptionMappers.java b/commafeed-server/src/main/java/com/commafeed/ExceptionMappers.java new file mode 100644 index 00000000..68618094 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/ExceptionMappers.java @@ -0,0 +1,33 @@ +package com.commafeed; + +import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.RestResponse.Status; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; + +import io.quarkus.security.AuthenticationFailedException; +import jakarta.annotation.Priority; +import jakarta.validation.ValidationException; +import jakarta.ws.rs.ext.Provider; + +@Provider +@Priority(1) +public class ExceptionMappers { + + // display a message when the user fails to authenticate + @ServerExceptionMapper(AuthenticationFailedException.class) + public RestResponse authenticationFailed(AuthenticationFailedException e) { + return RestResponse.status(RestResponse.Status.UNAUTHORIZED, new AuthenticationExceptionInfo(e.getMessage())); + } + + // display a message for validation errors + @ServerExceptionMapper(ValidationException.class) + public RestResponse validationException(ValidationException e) { + return RestResponse.status(Status.BAD_REQUEST, new ValidationExceptionInfo(e.getMessage())); + } + + public record AuthenticationExceptionInfo(String message) { + } + + public record ValidationExceptionInfo(String message) { + } +} diff --git a/commafeed-server/src/main/java/com/commafeed/JacksonCustomizer.java b/commafeed-server/src/main/java/com/commafeed/JacksonCustomizer.java new file mode 100644 index 00000000..0bc56613 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/JacksonCustomizer.java @@ -0,0 +1,27 @@ +package com.commafeed; + +import java.util.concurrent.TimeUnit; + +import com.codahale.metrics.json.MetricsModule; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import io.quarkus.jackson.ObjectMapperCustomizer; +import jakarta.inject.Singleton; + +@Singleton +public class JacksonCustomizer implements ObjectMapperCustomizer { + @Override + public void customize(ObjectMapper objectMapper) { + objectMapper.registerModule(new JavaTimeModule()); + + // read and write instants as milliseconds instead of nanoseconds + objectMapper.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false) + .configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false); + + // add support for serializing metrics + objectMapper.registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.SECONDS, false)); + } +} diff --git a/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java b/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java new file mode 100644 index 00000000..5f6b145b --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java @@ -0,0 +1,165 @@ +package com.commafeed; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.Gauge; +import com.codahale.metrics.Histogram; +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +@RegisterForReflection( + targets = { + // metrics + MetricRegistry.class, Meter.class, Gauge.class, Counter.class, Timer.class, Histogram.class, + + // rome + com.rometools.rome.feed.module.DCModuleImpl.class, com.rometools.rome.feed.module.DCSubjectImpl.class, + com.rometools.modules.content.ContentModuleImpl.class, com.rometools.modules.mediarss.MediaModuleImpl.class, + com.rometools.modules.mediarss.MediaEntryModuleImpl.class, + + // extracted from all 3 rome.properties files of rome library + com.rometools.rome.io.impl.RSS090Parser.class, com.rometools.rome.io.impl.RSS091NetscapeParser.class, + com.rometools.rome.io.impl.RSS091UserlandParser.class, com.rometools.rome.io.impl.RSS092Parser.class, + com.rometools.rome.io.impl.RSS093Parser.class, com.rometools.rome.io.impl.RSS094Parser.class, + com.rometools.rome.io.impl.RSS10Parser.class, com.rometools.rome.io.impl.RSS20wNSParser.class, + com.rometools.rome.io.impl.RSS20Parser.class, com.rometools.rome.io.impl.Atom10Parser.class, + com.rometools.rome.io.impl.Atom03Parser.class, + + com.rometools.rome.io.impl.SyModuleParser.class, com.rometools.rome.io.impl.DCModuleParser.class, + + com.rometools.rome.io.impl.RSS090Generator.class, com.rometools.rome.io.impl.RSS091NetscapeGenerator.class, + com.rometools.rome.io.impl.RSS091UserlandGenerator.class, com.rometools.rome.io.impl.RSS092Generator.class, + com.rometools.rome.io.impl.RSS093Generator.class, com.rometools.rome.io.impl.RSS094Generator.class, + com.rometools.rome.io.impl.RSS10Generator.class, com.rometools.rome.io.impl.RSS20Generator.class, + com.rometools.rome.io.impl.Atom10Generator.class, com.rometools.rome.io.impl.Atom03Generator.class, + + com.rometools.rome.feed.synd.impl.ConverterForAtom10.class, com.rometools.rome.feed.synd.impl.ConverterForAtom03.class, + com.rometools.rome.feed.synd.impl.ConverterForRSS090.class, + com.rometools.rome.feed.synd.impl.ConverterForRSS091Netscape.class, + com.rometools.rome.feed.synd.impl.ConverterForRSS091Userland.class, + com.rometools.rome.feed.synd.impl.ConverterForRSS092.class, com.rometools.rome.feed.synd.impl.ConverterForRSS093.class, + com.rometools.rome.feed.synd.impl.ConverterForRSS094.class, com.rometools.rome.feed.synd.impl.ConverterForRSS10.class, + com.rometools.rome.feed.synd.impl.ConverterForRSS20.class, + + com.rometools.modules.mediarss.io.RSS20YahooParser.class, + + com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.content.io.ContentModuleParser.class, + com.rometools.modules.itunes.io.ITunesParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class, + com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class, + com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class, + com.rometools.modules.mediarss.io.MediaModuleParser.class, com.rometools.modules.atom.io.AtomModuleParser.class, + com.rometools.modules.itunes.io.ITunesParserOldNamespace.class, + com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.sle.io.ModuleParser.class, + com.rometools.modules.yahooweather.io.WeatherModuleParser.class, com.rometools.modules.feedpress.io.FeedpressParser.class, + com.rometools.modules.fyyd.io.FyydParser.class, + + com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.content.io.ContentModuleParser.class, + com.rometools.modules.itunes.io.ITunesParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class, + com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class, + com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class, + com.rometools.modules.mediarss.io.MediaModuleParser.class, com.rometools.modules.atom.io.AtomModuleParser.class, + com.rometools.modules.itunes.io.ITunesParserOldNamespace.class, + com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.sle.io.ModuleParser.class, + com.rometools.modules.yahooweather.io.WeatherModuleParser.class, com.rometools.modules.feedpress.io.FeedpressParser.class, + com.rometools.modules.fyyd.io.FyydParser.class, + + com.rometools.modules.cc.io.ModuleParserRSS1.class, com.rometools.modules.content.io.ContentModuleParser.class, + + com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, + com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.georss.W3CGeoParser.class, + com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class, + com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, + + com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, + com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.georss.W3CGeoParser.class, + com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class, + com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, + com.rometools.modules.feedpress.io.FeedpressParser.class, com.rometools.modules.fyyd.io.FyydParser.class, + + com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.base.io.GoogleBaseParser.class, + com.rometools.modules.content.io.ContentModuleParser.class, com.rometools.modules.slash.io.SlashModuleParser.class, + com.rometools.modules.itunes.io.ITunesParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class, + com.rometools.modules.atom.io.AtomModuleParser.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, + com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.georss.W3CGeoParser.class, + com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.itunes.io.ITunesParserOldNamespace.class, + com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.sle.io.ItemParser.class, + com.rometools.modules.yahooweather.io.WeatherModuleParser.class, + com.rometools.modules.psc.io.PodloveSimpleChapterParser.class, + + com.rometools.modules.cc.io.ModuleParserRSS1.class, com.rometools.modules.base.io.GoogleBaseParser.class, + com.rometools.modules.base.io.CustomTagParser.class, com.rometools.modules.content.io.ContentModuleParser.class, + com.rometools.modules.slash.io.SlashModuleParser.class, + + com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.base.io.GoogleBaseParser.class, + com.rometools.modules.base.io.CustomTagParser.class, com.rometools.modules.slash.io.SlashModuleParser.class, + com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class, + com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class, + com.rometools.modules.mediarss.io.MediaModuleParser.class, + com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, + + com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.base.io.GoogleBaseParser.class, + com.rometools.modules.base.io.CustomTagParser.class, com.rometools.modules.slash.io.SlashModuleParser.class, + com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class, + com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class, + com.rometools.modules.mediarss.io.MediaModuleParser.class, + com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, + com.rometools.modules.thr.io.ThreadingModuleParser.class, com.rometools.modules.psc.io.PodloveSimpleChapterParser.class, + + com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.content.io.ContentModuleGenerator.class, + com.rometools.modules.itunes.io.ITunesGenerator.class, + com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class, + com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class, + com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.atom.io.AtomModuleGenerator.class, + com.rometools.modules.sle.io.ModuleGenerator.class, com.rometools.modules.yahooweather.io.WeatherModuleGenerator.class, + com.rometools.modules.feedpress.io.FeedpressGenerator.class, com.rometools.modules.fyyd.io.FyydGenerator.class, + + com.rometools.modules.content.io.ContentModuleGenerator.class, + + com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, + com.rometools.modules.georss.SimpleGenerator.class, com.rometools.modules.georss.W3CGeoGenerator.class, + com.rometools.modules.photocast.io.Generator.class, com.rometools.modules.mediarss.io.MediaModuleGenerator.class, + + com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, + com.rometools.modules.georss.SimpleGenerator.class, com.rometools.modules.georss.W3CGeoGenerator.class, + com.rometools.modules.photocast.io.Generator.class, com.rometools.modules.mediarss.io.MediaModuleGenerator.class, + com.rometools.modules.feedpress.io.FeedpressGenerator.class, com.rometools.modules.fyyd.io.FyydGenerator.class, + + com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.base.io.GoogleBaseGenerator.class, + com.rometools.modules.base.io.CustomTagGenerator.class, com.rometools.modules.content.io.ContentModuleGenerator.class, + com.rometools.modules.slash.io.SlashModuleGenerator.class, com.rometools.modules.itunes.io.ITunesGenerator.class, + com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class, + com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class, + com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.atom.io.AtomModuleGenerator.class, + com.rometools.modules.yahooweather.io.WeatherModuleGenerator.class, + com.rometools.modules.psc.io.PodloveSimpleChapterGenerator.class, + + com.rometools.modules.base.io.GoogleBaseGenerator.class, com.rometools.modules.content.io.ContentModuleGenerator.class, + com.rometools.modules.slash.io.SlashModuleGenerator.class, + + com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.base.io.GoogleBaseGenerator.class, + com.rometools.modules.base.io.CustomTagGenerator.class, com.rometools.modules.slash.io.SlashModuleGenerator.class, + com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class, + com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class, + com.rometools.modules.mediarss.io.MediaModuleGenerator.class, + + com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.base.io.CustomTagGenerator.class, + com.rometools.modules.slash.io.SlashModuleGenerator.class, + com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class, + com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class, + com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.thr.io.ThreadingModuleGenerator.class, + com.rometools.modules.psc.io.PodloveSimpleChapterGenerator.class, + + com.rometools.modules.mediarss.io.MediaModuleParser.class, + + com.rometools.modules.mediarss.io.MediaModuleGenerator.class, + + com.rometools.opml.io.impl.OPML10Generator.class, com.rometools.opml.io.impl.OPML20Generator.class, + + com.rometools.opml.io.impl.OPML10Parser.class, com.rometools.opml.io.impl.OPML20Parser.class, + + com.rometools.opml.feed.synd.impl.ConverterForOPML10.class, com.rometools.opml.feed.synd.impl.ConverterForOPML20.class, }) + +public class NativeImageClasses { +} diff --git a/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java b/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java index c7bc6155..2d5a59f0 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java @@ -21,21 +21,21 @@ import org.apache.hc.client5.http.protocol.RedirectLocations; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; import org.apache.hc.core5.http.message.BasicHeader; import org.apache.hc.core5.util.TimeValue; import org.apache.hc.core5.util.Timeout; -import org.eclipse.jetty.http.HttpStatus; import com.codahale.metrics.MetricRegistry; import com.commafeed.CommaFeedConfiguration; +import com.commafeed.CommaFeedVersion; import com.google.common.collect.Iterables; import com.google.common.io.ByteStreams; import com.google.common.net.HttpHeaders; -import io.dropwizard.util.DataSize; -import jakarta.inject.Inject; +import io.quarkus.runtime.configuration.MemorySize; import jakarta.inject.Singleton; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -51,15 +51,15 @@ import nl.altindag.ssl.apache5.util.Apache5SslUtils; public class HttpGetter { private final CloseableHttpClient client; - private final DataSize maxResponseSize; + private final MemorySize maxResponseSize; - @Inject - public HttpGetter(CommaFeedConfiguration config, MetricRegistry metrics) { - PoolingHttpClientConnectionManager connectionManager = newConnectionManager(config.getApplicationSettings().getBackgroundThreads()); - String userAgent = Optional.ofNullable(config.getApplicationSettings().getUserAgent()) - .orElseGet(() -> String.format("CommaFeed/%s (https://github.com/Athou/commafeed)", config.getVersion())); + public HttpGetter(CommaFeedConfiguration config, CommaFeedVersion version, MetricRegistry metrics) { + PoolingHttpClientConnectionManager connectionManager = newConnectionManager(config.feedRefresh().httpThreads()); + String userAgent = config.feedRefresh() + .userAgent() + .orElseGet(() -> String.format("CommaFeed/%s (https://github.com/Athou/commafeed)", version.getVersion())); this.client = newClient(connectionManager, userAgent); - this.maxResponseSize = config.getApplicationSettings().getMaxFeedResponseSize(); + this.maxResponseSize = config.feedRefresh().maxResponseSize(); metrics.registerGauge(MetricRegistry.name(getClass(), "pool", "max"), () -> connectionManager.getTotalStats().getMax()); metrics.registerGauge(MetricRegistry.name(getClass(), "pool", "size"), @@ -98,7 +98,7 @@ public class HttpGetter { context.setRequestConfig(RequestConfig.custom().setResponseTimeout(timeout, TimeUnit.MILLISECONDS).build()); HttpResponse response = client.execute(request, context, resp -> { - byte[] content = resp.getEntity() == null ? null : toByteArray(resp.getEntity(), maxResponseSize.toBytes()); + byte[] content = resp.getEntity() == null ? null : toByteArray(resp.getEntity(), maxResponseSize.asLongValue()); int code = resp.getCode(); String lastModifiedHeader = Optional.ofNullable(resp.getFirstHeader(HttpHeaders.LAST_MODIFIED)) .map(NameValuePair::getValue) @@ -120,7 +120,7 @@ public class HttpGetter { }); int code = response.getCode(); - if (code == HttpStatus.NOT_MODIFIED_304) { + if (code == HttpStatus.SC_NOT_MODIFIED) { throw new NotModifiedException("'304 - not modified' http code received"); } else if (code >= 300) { throw new HttpResponseException(code, "Server returned HTTP error code " + code); diff --git a/commafeed-server/src/main/java/com/commafeed/backend/cache/RedisPoolFactory.java b/commafeed-server/src/main/java/com/commafeed/backend/cache/RedisPoolFactory.java deleted file mode 100644 index b74fbf19..00000000 --- a/commafeed-server/src/main/java/com/commafeed/backend/cache/RedisPoolFactory.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.commafeed.backend.cache; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import lombok.Getter; -import redis.clients.jedis.DefaultJedisClientConfig; -import redis.clients.jedis.HostAndPort; -import redis.clients.jedis.JedisClientConfig; -import redis.clients.jedis.JedisPool; -import redis.clients.jedis.JedisPoolConfig; -import redis.clients.jedis.Protocol; - -@Getter -public class RedisPoolFactory { - - @JsonProperty - private String host = "localhost"; - - @JsonProperty - private int port = Protocol.DEFAULT_PORT; - - @JsonProperty - private String username; - - @JsonProperty - private String password; - - @JsonProperty - private int timeout = Protocol.DEFAULT_TIMEOUT; - - @JsonProperty - private int database = Protocol.DEFAULT_DATABASE; - - @JsonProperty - private int maxTotal = 500; - - public JedisPool build() { - JedisPoolConfig poolConfig = new JedisPoolConfig(); - poolConfig.setMaxTotal(maxTotal); - - JedisClientConfig clientConfig = DefaultJedisClientConfig.builder() - .user(username) - .password(password) - .timeoutMillis(timeout) - .database(database) - .build(); - - return new JedisPool(poolConfig, new HostAndPort(host, port), clientConfig); - } - -} diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedCategoryDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedCategoryDAO.java index 16bb496b..a84449a0 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedCategoryDAO.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedCategoryDAO.java @@ -3,25 +3,22 @@ package com.commafeed.backend.dao; import java.util.List; import java.util.Objects; -import org.hibernate.SessionFactory; - import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.QFeedCategory; import com.commafeed.backend.model.QUser; import com.commafeed.backend.model.User; import com.querydsl.core.types.Predicate; -import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; @Singleton public class FeedCategoryDAO extends GenericDAO { private static final QFeedCategory CATEGORY = QFeedCategory.feedCategory; - @Inject - public FeedCategoryDAO(SessionFactory sessionFactory) { - super(sessionFactory); + public FeedCategoryDAO(EntityManager entityManager) { + super(entityManager, FeedCategory.class); } public List findAll(User user) { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedDAO.java index f34a923e..cdaa690e 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedDAO.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedDAO.java @@ -4,7 +4,6 @@ import java.time.Instant; import java.util.List; import org.apache.commons.lang3.StringUtils; -import org.hibernate.SessionFactory; import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.QFeed; @@ -12,8 +11,8 @@ import com.commafeed.backend.model.QFeedSubscription; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQuery; -import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; @Singleton public class FeedDAO extends GenericDAO { @@ -21,9 +20,8 @@ public class FeedDAO extends GenericDAO { private static final QFeed FEED = QFeed.feed; private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription; - @Inject - public FeedDAO(SessionFactory sessionFactory) { - super(sessionFactory); + public FeedDAO(EntityManager entityManager) { + super(entityManager, Feed.class); } public List findNextUpdatable(int count, Instant lastLoginThreshold) { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryContentDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryContentDAO.java index 8d65b449..0b742e64 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryContentDAO.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryContentDAO.java @@ -2,16 +2,14 @@ package com.commafeed.backend.dao; import java.util.List; -import org.hibernate.SessionFactory; - import com.commafeed.backend.model.FeedEntryContent; import com.commafeed.backend.model.QFeedEntry; import com.commafeed.backend.model.QFeedEntryContent; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.JPQLSubQuery; -import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; @Singleton public class FeedEntryContentDAO extends GenericDAO { @@ -19,9 +17,8 @@ public class FeedEntryContentDAO extends GenericDAO { private static final QFeedEntryContent CONTENT = QFeedEntryContent.feedEntryContent; private static final QFeedEntry ENTRY = QFeedEntry.feedEntry; - @Inject - public FeedEntryContentDAO(SessionFactory sessionFactory) { - super(sessionFactory); + public FeedEntryContentDAO(EntityManager entityManager) { + super(entityManager, FeedEntryContent.class); } public List findExisting(String contentHash, String titleHash) { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryDAO.java index 2cf226ad..27d5de3f 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryDAO.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryDAO.java @@ -3,16 +3,14 @@ package com.commafeed.backend.dao; import java.time.Instant; import java.util.List; -import org.hibernate.SessionFactory; - import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.QFeedEntry; import com.querydsl.core.Tuple; import com.querydsl.core.types.dsl.NumberExpression; -import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; import lombok.AllArgsConstructor; import lombok.Getter; @@ -21,9 +19,8 @@ public class FeedEntryDAO extends GenericDAO { private static final QFeedEntry ENTRY = QFeedEntry.feedEntry; - @Inject - public FeedEntryDAO(SessionFactory sessionFactory) { - super(sessionFactory); + public FeedEntryDAO(EntityManager entityManager) { + super(entityManager, FeedEntry.class); } public FeedEntry findExisting(String guidHash, Feed feed) { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryStatusDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryStatusDAO.java index 358fbaa2..dee56442 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryStatusDAO.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryStatusDAO.java @@ -7,7 +7,6 @@ import java.util.Map; import java.util.stream.Collectors; import org.apache.commons.collections4.CollectionUtils; -import org.hibernate.SessionFactory; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.feed.FeedEntryKeyword; @@ -28,8 +27,8 @@ import com.querydsl.core.BooleanBuilder; import com.querydsl.core.Tuple; import com.querydsl.jpa.impl.JPAQuery; -import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; @Singleton public class FeedEntryStatusDAO extends GenericDAO { @@ -42,9 +41,8 @@ public class FeedEntryStatusDAO extends GenericDAO { private final FeedEntryTagDAO feedEntryTagDAO; private final CommaFeedConfiguration config; - @Inject - public FeedEntryStatusDAO(SessionFactory sessionFactory, FeedEntryTagDAO feedEntryTagDAO, CommaFeedConfiguration config) { - super(sessionFactory); + public FeedEntryStatusDAO(EntityManager entityManager, FeedEntryTagDAO feedEntryTagDAO, CommaFeedConfiguration config) { + super(entityManager, FeedEntryStatus.class); this.feedEntryTagDAO = feedEntryTagDAO; this.config = config; } @@ -60,8 +58,8 @@ public class FeedEntryStatusDAO extends GenericDAO { */ private FeedEntryStatus handleStatus(User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) { if (status == null) { - Instant unreadThreshold = config.getApplicationSettings().getUnreadThreshold(); - boolean read = unreadThreshold != null && entry.getPublished().isBefore(unreadThreshold); + Instant statusesInstantThreshold = config.database().cleanup().statusesInstantThreshold(); + boolean read = statusesInstantThreshold != null && entry.getPublished().isBefore(statusesInstantThreshold); status = new FeedEntryStatus(user, sub, entry); status.setRead(read); status.setMarkable(!read); @@ -84,6 +82,7 @@ public class FeedEntryStatusDAO extends GenericDAO { boolean includeContent) { JPAQuery query = query().selectFrom(STATUS).where(STATUS.user.eq(user), STATUS.starred.isTrue()); if (includeContent) { + query.join(STATUS.entry).fetchJoin(); query.join(STATUS.entry.content).fetchJoin(); } @@ -105,7 +104,7 @@ public class FeedEntryStatusDAO extends GenericDAO { query.limit(limit); } - setTimeout(query, config.getApplicationSettings().getQueryTimeout()); + setTimeout(query, config.database().queryTimeout()); List statuses = query.fetch(); statuses.forEach(s -> s.setMarkable(true)); @@ -179,7 +178,7 @@ public class FeedEntryStatusDAO extends GenericDAO { query.limit(limit); } - setTimeout(query, config.getApplicationSettings().getQueryTimeout()); + setTimeout(query, config.database().queryTimeout()); List statuses = new ArrayList<>(); List tuples = query.fetch(); @@ -217,9 +216,9 @@ public class FeedEntryStatusDAO extends GenericDAO { or.or(STATUS.read.isNull()); or.or(STATUS.read.isFalse()); - Instant unreadThreshold = config.getApplicationSettings().getUnreadThreshold(); - if (unreadThreshold != null) { - return or.and(ENTRY.published.goe(unreadThreshold)); + Instant statusesInstantThreshold = config.database().cleanup().statusesInstantThreshold(); + if (statusesInstantThreshold != null) { + return or.and(ENTRY.published.goe(statusesInstantThreshold)); } else { return or; } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryTagDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryTagDAO.java index 5d87d729..bb00ad12 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryTagDAO.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryTagDAO.java @@ -4,24 +4,21 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import org.hibernate.SessionFactory; - import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntryTag; import com.commafeed.backend.model.QFeedEntryTag; import com.commafeed.backend.model.User; -import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; @Singleton public class FeedEntryTagDAO extends GenericDAO { private static final QFeedEntryTag TAG = QFeedEntryTag.feedEntryTag; - @Inject - public FeedEntryTagDAO(SessionFactory sessionFactory) { - super(sessionFactory); + public FeedEntryTagDAO(EntityManager entityManager) { + super(entityManager, FeedEntryTag.class); } public List findByUser(User user) { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedSubscriptionDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedSubscriptionDAO.java index 05b1cde7..990ac21c 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedSubscriptionDAO.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedSubscriptionDAO.java @@ -5,12 +5,11 @@ import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; -import org.hibernate.SessionFactory; -import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.event.service.spi.EventListenerRegistry; import org.hibernate.event.spi.EventType; +import org.hibernate.event.spi.PostCommitInsertEventListener; import org.hibernate.event.spi.PostInsertEvent; -import org.hibernate.event.spi.PostInsertEventListener; import org.hibernate.persister.entity.EntityPersister; import com.commafeed.backend.model.AbstractModel; @@ -23,28 +22,28 @@ import com.commafeed.backend.model.User; import com.google.common.collect.Iterables; import com.querydsl.jpa.JPQLQuery; -import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; @Singleton public class FeedSubscriptionDAO extends GenericDAO { private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription; - private final SessionFactory sessionFactory; + private final EntityManager entityManager; - @Inject - public FeedSubscriptionDAO(SessionFactory sessionFactory) { - super(sessionFactory); - this.sessionFactory = sessionFactory; + public FeedSubscriptionDAO(EntityManager entityManager) { + super(entityManager, FeedSubscription.class); + this.entityManager = entityManager; } public void onPostCommitInsert(Consumer consumer) { - sessionFactory.unwrap(SessionFactoryImplementor.class) + entityManager.unwrap(SharedSessionContractImplementor.class) + .getFactory() .getServiceRegistry() .getService(EventListenerRegistry.class) .getEventListenerGroup(EventType.POST_COMMIT_INSERT) - .appendListener(new PostInsertEventListener() { + .appendListener(new PostCommitInsertEventListener() { @Override public void onPostInsert(PostInsertEvent event) { if (event.getEntity() instanceof FeedSubscription s) { @@ -56,6 +55,11 @@ public class FeedSubscriptionDAO extends GenericDAO { public boolean requiresPostCommitHandling(EntityPersister persister) { return true; } + + @Override + public void onPostInsertCommitFailed(PostInsertEvent event) { + // do nothing + } }); } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/GenericDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/GenericDAO.java index b2a75bfb..8041295b 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/dao/GenericDAO.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/GenericDAO.java @@ -2,7 +2,7 @@ package com.commafeed.backend.dao; import java.util.Collection; -import org.hibernate.SessionFactory; +import org.hibernate.Session; import org.hibernate.jpa.SpecHints; import com.commafeed.backend.model.AbstractModel; @@ -12,45 +12,43 @@ import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import com.querydsl.jpa.impl.JPAUpdateClause; -import io.dropwizard.hibernate.AbstractDAO; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; -public abstract class GenericDAO extends AbstractDAO { +@RequiredArgsConstructor +public abstract class GenericDAO { - protected GenericDAO(SessionFactory sessionFactory) { - super(sessionFactory); - } + private final EntityManager entityManager; + private final Class entityClass; protected JPAQueryFactory query() { - return new JPAQueryFactory(currentSession()); + return new JPAQueryFactory(entityManager); } protected JPAUpdateClause updateQuery(EntityPath entityPath) { - return new JPAUpdateClause(currentSession(), entityPath); + return new JPAUpdateClause(entityManager, entityPath); } protected JPADeleteClause deleteQuery(EntityPath entityPath) { - return new JPADeleteClause(currentSession(), entityPath); + return new JPADeleteClause(entityManager, entityPath); } + @SuppressWarnings("deprecation") public void saveOrUpdate(T model) { - persist(model); + entityManager.unwrap(Session.class).saveOrUpdate(model); } public void saveOrUpdate(Collection models) { - models.forEach(this::persist); - } - - public void update(T model) { - currentSession().merge(model); + models.forEach(this::saveOrUpdate); } public T findById(Long id) { - return get(id); + return entityManager.find(entityClass, id); } public void delete(T object) { if (object != null) { - currentSession().remove(object); + entityManager.remove(object); } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/UnitOfWork.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/UnitOfWork.java index 87be4676..5caa1ed2 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/dao/UnitOfWork.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/UnitOfWork.java @@ -1,68 +1,20 @@ package com.commafeed.backend.dao; -import org.hibernate.Session; -import org.hibernate.SessionFactory; -import org.hibernate.Transaction; -import org.hibernate.context.internal.ManagedSessionContext; - -import jakarta.inject.Inject; +import io.quarkus.narayana.jta.QuarkusTransaction; import jakarta.inject.Singleton; -import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) @Singleton public class UnitOfWork { - private final SessionFactory sessionFactory; - - public void run(SessionRunner sessionRunner) { + public void run(SessionRunner runner) { call(() -> { - sessionRunner.runInSession(); + runner.runInSession(); return null; }); } - public T call(SessionRunnerReturningValue sessionRunner) { - T t = null; - - boolean sessionAlreadyBound = ManagedSessionContext.hasBind(sessionFactory); - try (Session session = sessionFactory.openSession()) { - if (!sessionAlreadyBound) { - ManagedSessionContext.bind(session); - } - - Transaction tx = session.beginTransaction(); - try { - t = sessionRunner.runInSession(); - commitTransaction(tx); - } catch (Exception e) { - rollbackTransaction(tx); - UnitOfWork.rethrow(e); - } - } finally { - if (!sessionAlreadyBound) { - ManagedSessionContext.unbind(sessionFactory); - } - } - - return t; - } - - private static void rollbackTransaction(Transaction tx) { - if (tx != null && tx.isActive()) { - tx.rollback(); - } - } - - private static void commitTransaction(Transaction tx) { - if (tx != null && tx.isActive()) { - tx.commit(); - } - } - - @SuppressWarnings("unchecked") - private static void rethrow(Exception e) throws E { - throw (E) e; + public T call(SessionRunnerReturningValue runner) { + return QuarkusTransaction.joiningExisting().call(runner::runInSession); } @FunctionalInterface diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/UserDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/UserDAO.java index ae7d87f4..ea7e475c 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/dao/UserDAO.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/UserDAO.java @@ -1,21 +1,18 @@ package com.commafeed.backend.dao; -import org.hibernate.SessionFactory; - import com.commafeed.backend.model.QUser; import com.commafeed.backend.model.User; -import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; @Singleton public class UserDAO extends GenericDAO { private static final QUser USER = QUser.user; - @Inject - public UserDAO(SessionFactory sessionFactory) { - super(sessionFactory); + public UserDAO(EntityManager entityManager) { + super(entityManager, User.class); } public User findByName(String name) { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/UserRoleDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/UserRoleDAO.java index 37c23e92..81d06301 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/dao/UserRoleDAO.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/UserRoleDAO.java @@ -4,24 +4,21 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; -import org.hibernate.SessionFactory; - import com.commafeed.backend.model.QUserRole; import com.commafeed.backend.model.User; import com.commafeed.backend.model.UserRole; import com.commafeed.backend.model.UserRole.Role; -import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; @Singleton public class UserRoleDAO extends GenericDAO { private static final QUserRole ROLE = QUserRole.userRole; - @Inject - public UserRoleDAO(SessionFactory sessionFactory) { - super(sessionFactory); + public UserRoleDAO(EntityManager entityManager) { + super(entityManager, UserRole.class); } public List findAll() { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/UserSettingsDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/UserSettingsDAO.java index a95450ad..89048624 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/dao/UserSettingsDAO.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/UserSettingsDAO.java @@ -1,22 +1,19 @@ package com.commafeed.backend.dao; -import org.hibernate.SessionFactory; - import com.commafeed.backend.model.QUserSettings; import com.commafeed.backend.model.User; import com.commafeed.backend.model.UserSettings; -import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; @Singleton public class UserSettingsDAO extends GenericDAO { private static final QUserSettings SETTINGS = QUserSettings.userSettings; - @Inject - public UserSettingsDAO(SessionFactory sessionFactory) { - super(sessionFactory); + public UserSettingsDAO(EntityManager entityManager) { + super(entityManager, UserSettings.class); } public UserSettings findByUser(User user) { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/favicon/DefaultFaviconFetcher.java b/commafeed-server/src/main/java/com/commafeed/backend/favicon/DefaultFaviconFetcher.java index 7b814fef..32a0ee11 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/favicon/DefaultFaviconFetcher.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/favicon/DefaultFaviconFetcher.java @@ -10,7 +10,7 @@ import com.commafeed.backend.HttpGetter.HttpResult; import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.model.Feed; -import jakarta.inject.Inject; +import jakarta.annotation.Priority; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -20,8 +20,9 @@ import lombok.extern.slf4j.Slf4j; * */ @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton +@Priority(Integer.MIN_VALUE) public class DefaultFaviconFetcher extends AbstractFaviconFetcher { private final HttpGetter getter; diff --git a/commafeed-server/src/main/java/com/commafeed/backend/favicon/FacebookFaviconFetcher.java b/commafeed-server/src/main/java/com/commafeed/backend/favicon/FacebookFaviconFetcher.java index fd9722f0..fb0f0fb9 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/favicon/FacebookFaviconFetcher.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/favicon/FacebookFaviconFetcher.java @@ -11,13 +11,12 @@ import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter.HttpResult; import com.commafeed.backend.model.Feed; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class FacebookFaviconFetcher extends AbstractFaviconFetcher { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/favicon/YoutubeFaviconFetcher.java b/commafeed-server/src/main/java/com/commafeed/backend/favicon/YoutubeFaviconFetcher.java index def1dda5..ea219e3e 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/favicon/YoutubeFaviconFetcher.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/favicon/YoutubeFaviconFetcher.java @@ -22,13 +22,12 @@ import com.google.api.services.youtube.model.ChannelListResponse; import com.google.api.services.youtube.model.PlaylistListResponse; import com.google.api.services.youtube.model.Thumbnail; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class YoutubeFaviconFetcher extends AbstractFaviconFetcher { @@ -43,8 +42,8 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher { return null; } - String googleAuthKey = config.getApplicationSettings().getGoogleAuthKey(); - if (googleAuthKey == null) { + Optional googleAuthKey = config.googleAuthKey(); + if (googleAuthKey.isEmpty()) { log.debug("no google auth key configured"); return null; } @@ -63,13 +62,13 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher { ChannelListResponse response = null; if (userId.isPresent()) { log.debug("contacting youtube api for user {}", userId.get().getValue()); - response = fetchForUser(youtube, googleAuthKey, userId.get().getValue()); + response = fetchForUser(youtube, googleAuthKey.get(), userId.get().getValue()); } else if (channelId.isPresent()) { log.debug("contacting youtube api for channel {}", channelId.get().getValue()); - response = fetchForChannel(youtube, googleAuthKey, channelId.get().getValue()); + response = fetchForChannel(youtube, googleAuthKey.get(), channelId.get().getValue()); } else if (playlistId.isPresent()) { log.debug("contacting youtube api for playlist {}", playlistId.get().getValue()); - response = fetchForPlaylist(youtube, googleAuthKey, playlistId.get().getValue()); + response = fetchForPlaylist(youtube, googleAuthKey.get(), playlistId.get().getValue()); } if (response == null || response.isEmpty() || CollectionUtils.isEmpty(response.getItems())) { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedFetcher.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedFetcher.java index 18f9f2a0..03be80a9 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedFetcher.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedFetcher.java @@ -2,7 +2,7 @@ package com.commafeed.backend.feed; import java.io.IOException; import java.time.Instant; -import java.util.Set; +import java.util.List; import org.apache.commons.codec.binary.StringUtils; @@ -15,22 +15,26 @@ import com.commafeed.backend.feed.parser.FeedParserResult; import com.commafeed.backend.urlprovider.FeedURLProvider; import com.rometools.rome.io.FeedException; -import jakarta.inject.Inject; +import io.quarkus.arc.All; import jakarta.inject.Singleton; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; /** * Fetches a feed then parses it */ @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) @Singleton public class FeedFetcher { private final FeedParser parser; private final HttpGetter getter; - private final Set urlProviders; + private final List urlProviders; + + public FeedFetcher(FeedParser parser, HttpGetter getter, @All List urlProviders) { + this.parser = parser; + this.getter = getter; + this.urlProviders = urlProviders; + } public FeedFetcherResult fetch(String feedUrl, boolean extractFeedUrlFromHtml, String lastModified, String eTag, Instant lastPublishedDate, String lastContentHash) throws FeedException, IOException, NotModifiedException { @@ -87,7 +91,7 @@ public class FeedFetcher { result.getDuration()); } - private static String extractFeedUrl(Set urlProviders, String url, String urlContent) { + private static String extractFeedUrl(List urlProviders, String url, String urlContent) { for (FeedURLProvider urlProvider : urlProviders) { String feedUrl = urlProvider.get(url, urlContent); if (feedUrl != null) { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshEngine.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshEngine.java index 9f39cbf0..bd1ece18 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshEngine.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshEngine.java @@ -1,6 +1,5 @@ package com.commafeed.backend.feed; -import java.time.Duration; import java.time.Instant; import java.util.List; import java.util.concurrent.BlockingDeque; @@ -21,14 +20,12 @@ import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.model.AbstractModel; import com.commafeed.backend.model.Feed; -import io.dropwizard.lifecycle.Managed; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton -public class FeedRefreshEngine implements Managed { +public class FeedRefreshEngine { private final UnitOfWork unitOfWork; private final FeedDAO feedDAO; @@ -45,7 +42,6 @@ public class FeedRefreshEngine implements Managed { private final ThreadPoolExecutor workerExecutor; private final ThreadPoolExecutor databaseUpdaterExecutor; - @Inject public FeedRefreshEngine(UnitOfWork unitOfWork, FeedDAO feedDAO, FeedRefreshWorker worker, FeedRefreshUpdater updater, CommaFeedConfiguration config, MetricRegistry metrics) { this.unitOfWork = unitOfWork; @@ -60,15 +56,14 @@ public class FeedRefreshEngine implements Managed { this.feedProcessingLoopExecutor = Executors.newSingleThreadExecutor(); this.refillLoopExecutor = Executors.newSingleThreadExecutor(); this.refillExecutor = newDiscardingSingleThreadExecutorService(); - this.workerExecutor = newBlockingExecutorService(config.getApplicationSettings().getBackgroundThreads()); - this.databaseUpdaterExecutor = newBlockingExecutorService(config.getApplicationSettings().getDatabaseUpdateThreads()); + this.workerExecutor = newBlockingExecutorService(config.feedRefresh().httpThreads()); + this.databaseUpdaterExecutor = newBlockingExecutorService(config.feedRefresh().databaseThreads()); metrics.register(MetricRegistry.name(getClass(), "queue", "size"), (Gauge) queue::size); metrics.register(MetricRegistry.name(getClass(), "worker", "active"), (Gauge) workerExecutor::getActiveCount); metrics.register(MetricRegistry.name(getClass(), "updater", "active"), (Gauge) databaseUpdaterExecutor::getActiveCount); } - @Override public void start() { startFeedProcessingLoop(); startRefillLoop(); @@ -165,22 +160,20 @@ public class FeedRefreshEngine implements Managed { private List getNextUpdatableFeeds(int max) { return unitOfWork.call(() -> { - Instant lastLoginThreshold = Boolean.TRUE.equals(config.getApplicationSettings().getHeavyLoad()) - ? Instant.now().minus(Duration.ofDays(30)) - : null; + Instant lastLoginThreshold = config.feedRefresh().userInactivityPeriod().isZero() ? null + : Instant.now().minus(config.feedRefresh().userInactivityPeriod()); List feeds = feedDAO.findNextUpdatable(max, lastLoginThreshold); // update disabledUntil to prevent feeds from being returned again by feedDAO.findNextUpdatable() - Instant nextUpdateDate = Instant.now().plus(Duration.ofMinutes(config.getApplicationSettings().getRefreshIntervalMinutes())); + Instant nextUpdateDate = Instant.now().plus(config.feedRefresh().interval()); feedDAO.setDisabledUntil(feeds.stream().map(AbstractModel::getId).toList(), nextUpdateDate); return feeds; }); } private int getBatchSize() { - return Math.min(100, 3 * config.getApplicationSettings().getBackgroundThreads()); + return Math.min(100, 3 * config.feedRefresh().httpThreads()); } - @Override public void stop() { this.feedProcessingLoopExecutor.shutdownNow(); this.refillLoopExecutor.shutdownNow(); diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshIntervalCalculator.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshIntervalCalculator.java index d4097332..8aea399e 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshIntervalCalculator.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshIntervalCalculator.java @@ -6,24 +6,22 @@ import java.time.temporal.ChronoUnit; import com.commafeed.CommaFeedConfiguration; -import jakarta.inject.Inject; import jakarta.inject.Singleton; @Singleton public class FeedRefreshIntervalCalculator { - private final boolean heavyLoad; - private final int refreshIntervalMinutes; + private final Duration refreshInterval; + private final boolean empiricalInterval; - @Inject public FeedRefreshIntervalCalculator(CommaFeedConfiguration config) { - this.heavyLoad = config.getApplicationSettings().getHeavyLoad(); - this.refreshIntervalMinutes = config.getApplicationSettings().getRefreshIntervalMinutes(); + this.refreshInterval = config.feedRefresh().interval(); + this.empiricalInterval = config.feedRefresh().intervalEmpirical(); } public Instant onFetchSuccess(Instant publishedDate, Long averageEntryInterval) { Instant defaultRefreshInterval = getDefaultRefreshInterval(); - return heavyLoad ? computeRefreshIntervalForHeavyLoad(publishedDate, averageEntryInterval, defaultRefreshInterval) + return empiricalInterval ? computeRefreshIntervalForHeavyLoad(publishedDate, averageEntryInterval, defaultRefreshInterval) : defaultRefreshInterval; } @@ -33,7 +31,7 @@ public class FeedRefreshIntervalCalculator { public Instant onFetchError(int errorCount) { int retriesBeforeDisable = 3; - if (errorCount < retriesBeforeDisable || !heavyLoad) { + if (errorCount < retriesBeforeDisable || !empiricalInterval) { return getDefaultRefreshInterval(); } @@ -42,7 +40,7 @@ public class FeedRefreshIntervalCalculator { } private Instant getDefaultRefreshInterval() { - return Instant.now().plus(Duration.ofMinutes(refreshIntervalMinutes)); + return Instant.now().plus(refreshInterval); } private Instant computeRefreshIntervalForHeavyLoad(Instant publishedDate, Long averageEntryInterval, Instant defaultRefreshInterval) { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java index ea891b03..b2237929 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java @@ -32,7 +32,6 @@ import com.commafeed.frontend.ws.WebSocketMessageBuilder; import com.commafeed.frontend.ws.WebSocketSessions; import com.google.common.util.concurrent.Striped; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -58,7 +57,6 @@ public class FeedRefreshUpdater { private final Meter feedUpdated; private final Meter entryInserted; - @Inject public FeedRefreshUpdater(UnitOfWork unitOfWork, FeedService feedService, FeedEntryService feedEntryService, MetricRegistry metrics, FeedSubscriptionDAO feedSubscriptionDAO, CacheService cache, WebSocketSessions webSocketSessions) { this.unitOfWork = unitOfWork; diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshWorker.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshWorker.java index fa855f2a..11421313 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshWorker.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshWorker.java @@ -16,7 +16,6 @@ import com.commafeed.backend.feed.FeedFetcher.FeedFetcherResult; import com.commafeed.backend.feed.parser.FeedParserResult.Entry; import com.commafeed.backend.model.Feed; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.extern.slf4j.Slf4j; @@ -32,7 +31,6 @@ public class FeedRefreshWorker { private final CommaFeedConfiguration config; private final Meter feedFetched; - @Inject public FeedRefreshWorker(FeedRefreshIntervalCalculator refreshIntervalCalculator, FeedFetcher fetcher, CommaFeedConfiguration config, MetricRegistry metrics) { this.refreshIntervalCalculator = refreshIntervalCalculator; @@ -51,14 +49,14 @@ public class FeedRefreshWorker { List entries = result.feed().entries(); - Integer maxFeedCapacity = config.getApplicationSettings().getMaxFeedCapacity(); + int maxFeedCapacity = config.database().cleanup().maxFeedCapacity(); if (maxFeedCapacity > 0) { entries = entries.stream().limit(maxFeedCapacity).toList(); } - Integer maxEntriesAgeDays = config.getApplicationSettings().getMaxEntriesAgeDays(); - if (maxEntriesAgeDays > 0) { - Instant threshold = Instant.now().minus(Duration.ofDays(maxEntriesAgeDays)); + Duration maxEntriesAgeDays = config.database().cleanup().entriesMaxAge(); + if (!maxEntriesAgeDays.isZero()) { + Instant threshold = Instant.now().minus(maxEntriesAgeDays); entries = entries.stream().filter(entry -> entry.published().isAfter(threshold)).toList(); } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/FeedParser.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/FeedParser.java index 2f62c530..d25f7c13 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/FeedParser.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/FeedParser.java @@ -38,14 +38,13 @@ import com.rometools.rome.feed.synd.SyndLinkImpl; import com.rometools.rome.io.FeedException; import com.rometools.rome.io.SyndFeedInput; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; /** * Parses raw xml into a FeedParserResult object */ -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class FeedParser { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLExporter.java b/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLExporter.java index 5dc3329d..f451ac93 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLExporter.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLExporter.java @@ -15,11 +15,10 @@ import com.rometools.opml.feed.opml.Attribute; import com.rometools.opml.feed.opml.Opml; import com.rometools.opml.feed.opml.Outline; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class OPMLExporter { @@ -28,7 +27,7 @@ public class OPMLExporter { public Opml export(User user) { Opml opml = new Opml(); - opml.setFeedType("opml_1.1"); + opml.setFeedType("opml_1.0"); opml.setTitle(String.format("%s subscriptions in CommaFeed", user.getName())); opml.setCreated(new Date()); diff --git a/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLImporter.java b/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLImporter.java index a4a00480..f28c5247 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLImporter.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLImporter.java @@ -17,13 +17,12 @@ import com.rometools.opml.feed.opml.Outline; import com.rometools.rome.io.FeedException; import com.rometools.rome.io.WireFeedInput; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class OPMLImporter { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Generator.java b/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Generator.java index 4bfc44af..480a1ba7 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Generator.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Generator.java @@ -4,10 +4,13 @@ import org.jdom2.Element; import com.rometools.opml.feed.opml.Opml; +import io.quarkus.runtime.annotations.RegisterForReflection; + /** * Add missing title to the generated OPML * */ +@RegisterForReflection public class OPML11Generator extends com.rometools.opml.io.impl.OPML10Generator { public OPML11Generator() { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Parser.java b/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Parser.java index 3342a5c9..8770667a 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Parser.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Parser.java @@ -9,10 +9,13 @@ import com.rometools.opml.io.impl.OPML10Parser; import com.rometools.rome.feed.WireFeed; import com.rometools.rome.io.FeedException; +import io.quarkus.runtime.annotations.RegisterForReflection; + /** * Support for OPML 1.1 parsing * */ +@RegisterForReflection public class OPML11Parser extends OPML10Parser { public OPML11Parser() { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionConverter.java b/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionConverter.java index c2154565..404364b2 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionConverter.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionConverter.java @@ -6,10 +6,13 @@ import com.rometools.rome.feed.synd.SyndContentImpl; import com.rometools.rome.feed.synd.SyndEntry; import com.rometools.rome.feed.synd.impl.ConverterForRSS090; +import io.quarkus.runtime.annotations.RegisterForReflection; + /** * Support description tag for RSS09 * */ +@RegisterForReflection public class RSS090DescriptionConverter extends ConverterForRSS090 { @Override diff --git a/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionParser.java b/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionParser.java index d30f82dc..abde98ed 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionParser.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionParser.java @@ -8,10 +8,13 @@ import com.rometools.rome.feed.rss.Description; import com.rometools.rome.feed.rss.Item; import com.rometools.rome.io.impl.RSS090Parser; +import io.quarkus.runtime.annotations.RegisterForReflection; + /** * Support description tag for RSS09 * */ +@RegisterForReflection public class RSS090DescriptionParser extends RSS090Parser { @Override diff --git a/commafeed-server/src/main/java/com/commafeed/backend/rome/RSSRDF10Parser.java b/commafeed-server/src/main/java/com/commafeed/backend/rome/RSSRDF10Parser.java index 4c101230..6c7e11c7 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/rome/RSSRDF10Parser.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/rome/RSSRDF10Parser.java @@ -10,6 +10,9 @@ import org.jdom2.Namespace; import com.google.common.collect.Lists; import com.rometools.rome.io.impl.RSS10Parser; +import io.quarkus.runtime.annotations.RegisterForReflection; + +@RegisterForReflection public class RSSRDF10Parser extends RSS10Parser { private static final String RSS_URI = "http://purl.org/rss/1.0/"; diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentCleaningService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentCleaningService.java index 5886f20a..f3e90352 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentCleaningService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentCleaningService.java @@ -21,12 +21,11 @@ import org.w3c.dom.css.CSSStyleDeclaration; import com.steadystate.css.parser.CSSOMParser; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Slf4j @Singleton public class FeedEntryContentCleaningService { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentService.java index 1ad16ab5..879f42f3 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentService.java @@ -12,11 +12,10 @@ import com.commafeed.backend.feed.parser.FeedParserResult.Enclosure; import com.commafeed.backend.feed.parser.FeedParserResult.Media; import com.commafeed.backend.model.FeedEntryContent; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class FeedEntryContentService { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryFilteringService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryFilteringService.java index 2ca965a2..6832c58a 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryFilteringService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryFilteringService.java @@ -25,11 +25,10 @@ import org.jsoup.Jsoup; import com.commafeed.backend.model.FeedEntry; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class FeedEntryFilteringService { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryService.java index d75e1915..7b789042 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryService.java @@ -18,13 +18,12 @@ import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.User; import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterException; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class FeedEntryService { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryTagService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryTagService.java index 70f451c9..540956ab 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryTagService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryTagService.java @@ -10,11 +10,10 @@ import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntryTag; import com.commafeed.backend.model.User; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class FeedEntryTagService { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedService.java index 69a5ecf2..49cfd653 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedService.java @@ -2,8 +2,8 @@ package com.commafeed.backend.service; import java.io.IOException; import java.time.Instant; +import java.util.List; import java.util.Objects; -import java.util.Set; import com.commafeed.backend.Digests; import com.commafeed.backend.dao.FeedDAO; @@ -14,19 +14,18 @@ import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Models; import com.google.common.io.Resources; -import jakarta.inject.Inject; +import io.quarkus.arc.All; import jakarta.inject.Singleton; @Singleton public class FeedService { private final FeedDAO feedDAO; - private final Set faviconFetchers; + private final List faviconFetchers; private final Favicon defaultFavicon; - @Inject - public FeedService(FeedDAO feedDAO, Set faviconFetchers) { + public FeedService(FeedDAO feedDAO, @All List faviconFetchers) { this.feedDAO = feedDAO; this.faviconFetchers = faviconFetchers; diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedSubscriptionService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedSubscriptionService.java index 444b9f34..5f48a7c1 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedSubscriptionService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedSubscriptionService.java @@ -21,7 +21,6 @@ import com.commafeed.backend.model.Models; import com.commafeed.backend.model.User; import com.commafeed.frontend.model.UnreadCount; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.extern.slf4j.Slf4j; @@ -37,7 +36,6 @@ public class FeedSubscriptionService { private final CacheService cache; private final CommaFeedConfiguration config; - @Inject public FeedSubscriptionService(FeedDAO feedDAO, FeedEntryStatusDAO feedEntryStatusDAO, FeedSubscriptionDAO feedSubscriptionDAO, FeedService feedService, FeedRefreshEngine feedRefreshEngine, CacheService cache, CommaFeedConfiguration config) { this.feedDAO = feedDAO; @@ -63,7 +61,7 @@ public class FeedSubscriptionService { public long subscribe(User user, String url, String title, FeedCategory category, int position) { - final String pubUrl = config.getApplicationSettings().getPublicUrl(); + final String pubUrl = config.publicUrl(); if (StringUtils.isBlank(pubUrl)) { throw new FeedSubscriptionException("Public URL of this CommaFeed instance is not set"); } @@ -71,7 +69,7 @@ public class FeedSubscriptionService { throw new FeedSubscriptionException("Could not subscribe to a feed from this CommaFeed instance"); } - Integer maxFeedsPerUser = config.getApplicationSettings().getMaxFeedsPerUser(); + Integer maxFeedsPerUser = config.database().cleanup().maxFeedsPerUser(); if (maxFeedsPerUser > 0 && feedSubscriptionDAO.count(user) >= maxFeedsPerUser) { String message = String.format("You cannot subscribe to more feeds on this CommaFeed instance (max %s feeds per user)", maxFeedsPerUser); diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/MailService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/MailService.java index e50556f0..758945aa 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/MailService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/MailService.java @@ -4,10 +4,9 @@ import java.util.Optional; import java.util.Properties; import com.commafeed.CommaFeedConfiguration; -import com.commafeed.CommaFeedConfiguration.ApplicationSettings; +import com.commafeed.CommaFeedConfiguration.Smtp; import com.commafeed.backend.model.User; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import jakarta.mail.Authenticator; import jakarta.mail.Message; @@ -22,27 +21,29 @@ import lombok.RequiredArgsConstructor; * Mailing service * */ -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class MailService { private final CommaFeedConfiguration config; public void sendMail(User user, String subject, String content) throws Exception { + Optional settings = config.smtp(); + if (settings.isEmpty()) { + throw new IllegalArgumentException("SMTP settings not configured"); + } - ApplicationSettings settings = config.getApplicationSettings(); - - final String username = settings.getSmtpUserName(); - final String password = settings.getSmtpPassword(); - final String fromAddress = Optional.ofNullable(settings.getSmtpFromAddress()).orElse(settings.getSmtpUserName()); + final String username = settings.get().userName(); + final String password = settings.get().password(); + final String fromAddress = Optional.ofNullable(settings.get().fromAddress()).orElse(settings.get().userName()); String dest = user.getEmail(); Properties props = new Properties(); props.put("mail.smtp.auth", "true"); - props.put("mail.smtp.starttls.enable", String.valueOf(settings.isSmtpTls())); - props.put("mail.smtp.host", settings.getSmtpHost()); - props.put("mail.smtp.port", String.valueOf(settings.getSmtpPort())); + props.put("mail.smtp.starttls.enable", String.valueOf(settings.get().tls())); + props.put("mail.smtp.host", settings.get().host()); + props.put("mail.smtp.port", String.valueOf(settings.get().port())); Session session = Session.getInstance(props, new Authenticator() { @Override diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/PasswordEncryptionService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/PasswordEncryptionService.java index 26aa70b9..a6f7d5c6 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/PasswordEncryptionService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/PasswordEncryptionService.java @@ -12,7 +12,6 @@ import javax.crypto.spec.PBEKeySpec; import org.apache.commons.lang3.StringUtils; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -20,7 +19,7 @@ import lombok.extern.slf4j.Slf4j; // taken from http://www.javacodegeeks.com/2012/05/secure-password-storage-donts-dos-and.html @SuppressWarnings("serial") @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class PasswordEncryptionService implements Serializable { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/UserService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/UserService.java index 76a0df1c..b56d4c47 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/UserService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/UserService.java @@ -24,11 +24,10 @@ import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.service.internal.PostLoginActivities; import com.google.common.base.Preconditions; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class UserService { @@ -117,8 +116,7 @@ public class UserService { public User register(String name, String password, String email, Collection roles, boolean forceRegistration) { if (!forceRegistration) { - Preconditions.checkState(config.getApplicationSettings().getAllowRegistrations(), - "Registrations are closed on this CommaFeed instance"); + Preconditions.checkState(config.users().allowRegistrations(), "Registrations are closed on this CommaFeed instance"); } Preconditions.checkArgument(userDAO.findByName(name) == null, "Name already taken"); diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseCleaningService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseCleaningService.java index a8f54d4e..d64bb25b 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseCleaningService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseCleaningService.java @@ -14,7 +14,6 @@ import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.model.Feed; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.extern.slf4j.Slf4j; @@ -35,7 +34,6 @@ public class DatabaseCleaningService { private final FeedEntryStatusDAO feedEntryStatusDAO; private final Meter entriesDeletedMeter; - @Inject public DatabaseCleaningService(CommaFeedConfiguration config, UnitOfWork unitOfWork, FeedDAO feedDAO, FeedEntryDAO feedEntryDAO, FeedEntryContentDAO feedEntryContentDAO, FeedEntryStatusDAO feedEntryStatusDAO, MetricRegistry metrics) { this.unitOfWork = unitOfWork; @@ -43,7 +41,7 @@ public class DatabaseCleaningService { this.feedEntryDAO = feedEntryDAO; this.feedEntryContentDAO = feedEntryContentDAO; this.feedEntryStatusDAO = feedEntryStatusDAO; - this.batchSize = config.getApplicationSettings().getDatabaseCleanupBatchSize(); + this.batchSize = config.database().cleanup().batchSize(); this.entriesDeletedMeter = metrics.meter(MetricRegistry.name(getClass(), "entriesDeleted")); } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseStartupService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseStartupService.java index 865118e6..a5bbe584 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseStartupService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseStartupService.java @@ -1,109 +1,41 @@ package com.commafeed.backend.service.db; -import java.util.HashMap; -import java.util.Map; - -import org.hibernate.Session; -import org.hibernate.SessionFactory; +import org.kohsuke.MetaInfServices; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.service.UserService; -import io.dropwizard.lifecycle.Managed; -import jakarta.inject.Inject; import jakarta.inject.Singleton; -import liquibase.Scope; -import liquibase.UpdateSummaryEnum; -import liquibase.changelog.ChangeLogParameters; -import liquibase.command.CommandScope; -import liquibase.command.core.UpdateCommandStep; -import liquibase.command.core.helpers.DatabaseChangelogCommandStep; -import liquibase.command.core.helpers.DbUrlConnectionArgumentsCommandStep; -import liquibase.command.core.helpers.ShowSummaryArgument; import liquibase.database.Database; -import liquibase.database.DatabaseFactory; import liquibase.database.core.PostgresDatabase; -import liquibase.database.jvm.JdbcConnection; -import liquibase.exception.DatabaseException; -import liquibase.resource.ClassLoaderResourceAccessor; import liquibase.structure.DatabaseObject; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton -public class DatabaseStartupService implements Managed { +public class DatabaseStartupService { private final UnitOfWork unitOfWork; - private final SessionFactory sessionFactory; private final UserDAO userDAO; private final UserService userService; private final CommaFeedConfiguration config; - @Override - public void start() { - updateSchema(); + public void populateInitialData() { long count = unitOfWork.call(userDAO::count); if (count == 0) { unitOfWork.run(this::initialData); } } - private void updateSchema() { - log.info("checking if database schema needs updating"); - - try (Session session = sessionFactory.openSession()) { - session.doWork(connection -> { - try { - JdbcConnection jdbcConnection = new JdbcConnection(connection); - Database database = getDatabase(jdbcConnection); - - Map scopeObjects = new HashMap<>(); - scopeObjects.put(Scope.Attr.database.name(), database); - scopeObjects.put(Scope.Attr.resourceAccessor.name(), - new ClassLoaderResourceAccessor(Thread.currentThread().getContextClassLoader())); - - Scope.child(scopeObjects, () -> { - CommandScope command = new CommandScope(UpdateCommandStep.COMMAND_NAME); - command.addArgumentValue(DbUrlConnectionArgumentsCommandStep.DATABASE_ARG, database); - command.addArgumentValue(UpdateCommandStep.CHANGELOG_FILE_ARG, "migrations.xml"); - command.addArgumentValue(DatabaseChangelogCommandStep.CHANGELOG_PARAMETERS, new ChangeLogParameters(database)); - command.addArgumentValue(ShowSummaryArgument.SHOW_SUMMARY, UpdateSummaryEnum.OFF); - command.execute(); - }); - - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - } - - log.info("database schema is up to date"); - } - - private Database getDatabase(JdbcConnection connection) throws DatabaseException { - Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(connection); - if (database instanceof PostgresDatabase) { - database = new PostgresDatabase() { - @Override - public String escapeObjectName(String objectName, Class objectType) { - return objectName; - } - }; - database.setConnection(connection); - } - - return database; - } - private void initialData() { log.info("populating database with default values"); try { userService.createAdminUser(); - if (config.getApplicationSettings().getCreateDemoAccount()) { + if (config.users().createDemoAccount()) { userService.createDemoUser(); } } catch (Exception e) { @@ -111,4 +43,20 @@ public class DatabaseStartupService implements Managed { } } + /** + * Register a postgresql database in liquibase that doesn't escape columns, so that we can use lower case columns + */ + @MetaInfServices(Database.class) + public static class LowerCaseColumnsPostgresDatabase extends PostgresDatabase { + @Override + public String escapeObjectName(String objectName, Class objectType) { + return objectName; + } + + @Override + public int getPriority() { + return super.getPriority() + 1; + } + } + } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/db/H2MigrationService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/db/H2MigrationService.java deleted file mode 100644 index 75b839c4..00000000 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/db/H2MigrationService.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.commafeed.backend.service.db; - -import java.io.BufferedReader; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.stream.Stream; - -import org.apache.commons.lang3.StringUtils; - -import com.manticore.h2.H2MigrationTool; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@RequiredArgsConstructor -@Slf4j -public class H2MigrationService { - - private static final String H2_FILE_SUFFIX = ".mv.db"; - - public void migrateIfNeeded(Path path, String user, String password) { - if (Files.notExists(path)) { - return; - } - - int format; - try { - format = getH2FileFormat(path); - } catch (IOException e) { - throw new RuntimeException("could not detect H2 format", e); - } - - if (format == 2) { - try { - migrate(path, user, password, "2.1.214", "2.2.224"); - } catch (Exception e) { - throw new RuntimeException("could not migrate H2 to format 3", e); - } - } - } - - public int getH2FileFormat(Path path) throws IOException { - try (BufferedReader reader = Files.newBufferedReader(path)) { - String headers = reader.readLine(); - - return Stream.of(headers.split(",")) - .filter(h -> h.startsWith("format:")) - .map(h -> h.split(":")[1]) - .map(Integer::parseInt) - .findFirst() - .orElseThrow(() -> new RuntimeException("could not find format in H2 file headers")); - } - } - - private void migrate(Path path, String user, String password, String fromVersion, String toVersion) throws Exception { - log.info("migrating H2 database at {} from format {} to format {}", path, fromVersion, toVersion); - - Path scriptPath = path.resolveSibling("script-%d.sql".formatted(System.currentTimeMillis())); - Path newVersionPath = path.resolveSibling("%s.%s%s".formatted(StringUtils.removeEnd(path.getFileName().toString(), H2_FILE_SUFFIX), - getPatchVersion(toVersion), H2_FILE_SUFFIX)); - Path oldVersionBackupPath = path.resolveSibling("%s.%s.backup".formatted(path.getFileName(), getPatchVersion(fromVersion))); - - Files.deleteIfExists(scriptPath); - Files.deleteIfExists(newVersionPath); - Files.deleteIfExists(oldVersionBackupPath); - - H2MigrationTool.readDriverRecords(); - new H2MigrationTool().migrate(fromVersion, toVersion, path.toAbsolutePath().toString(), user, password, - scriptPath.toAbsolutePath().toString(), "", "", false, false, ""); - if (!Files.exists(newVersionPath)) { - throw new RuntimeException("H2 migration failed, new version file not found"); - } - - Files.move(path, oldVersionBackupPath); - Files.move(newVersionPath, path); - Files.delete(oldVersionBackupPath); - Files.delete(scriptPath); - - log.info("migrated H2 database from format {} to format {}", fromVersion, toVersion); - } - - private String getPatchVersion(String version) { - return StringUtils.substringAfterLast(version, "."); - } - -} diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/internal/PostLoginActivities.java b/commafeed-server/src/main/java/com/commafeed/backend/service/internal/PostLoginActivities.java index 752ef81b..bd66f02b 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/internal/PostLoginActivities.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/internal/PostLoginActivities.java @@ -7,11 +7,10 @@ import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.model.User; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class PostLoginActivities { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/DemoAccountCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/DemoAccountCleanupTask.java index ccd2a5d2..faa2757a 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/task/DemoAccountCleanupTask.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/DemoAccountCleanupTask.java @@ -9,12 +9,11 @@ import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.model.User; import com.commafeed.backend.service.UserService; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton @Slf4j public class DemoAccountCleanupTask extends ScheduledTask { @@ -26,7 +25,7 @@ public class DemoAccountCleanupTask extends ScheduledTask { @Override protected void run() { - if (!config.getApplicationSettings().getCreateDemoAccount()) { + if (!config.users().createDemoAccount()) { return; } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/EntriesExceedingFeedCapacityCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/EntriesExceedingFeedCapacityCleanupTask.java index 5508cbd7..09351381 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/task/EntriesExceedingFeedCapacityCleanupTask.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/EntriesExceedingFeedCapacityCleanupTask.java @@ -5,11 +5,10 @@ import java.util.concurrent.TimeUnit; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.service.db.DatabaseCleaningService; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class EntriesExceedingFeedCapacityCleanupTask extends ScheduledTask { @@ -18,7 +17,7 @@ public class EntriesExceedingFeedCapacityCleanupTask extends ScheduledTask { @Override public void run() { - int maxFeedCapacity = config.getApplicationSettings().getMaxFeedCapacity(); + int maxFeedCapacity = config.database().cleanup().maxFeedCapacity(); if (maxFeedCapacity > 0) { cleaner.cleanEntriesForFeedsExceedingCapacity(maxFeedCapacity); } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/OldEntriesCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/OldEntriesCleanupTask.java index fad42a75..4bc0cd32 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/task/OldEntriesCleanupTask.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/OldEntriesCleanupTask.java @@ -7,11 +7,10 @@ import java.util.concurrent.TimeUnit; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.service.db.DatabaseCleaningService; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class OldEntriesCleanupTask extends ScheduledTask { @@ -20,9 +19,9 @@ public class OldEntriesCleanupTask extends ScheduledTask { @Override public void run() { - int maxAgeDays = config.getApplicationSettings().getMaxEntriesAgeDays(); - if (maxAgeDays > 0) { - Instant threshold = Instant.now().minus(Duration.ofDays(maxAgeDays)); + Duration entriesMaxAge = config.database().cleanup().entriesMaxAge(); + if (!entriesMaxAge.isZero()) { + Instant threshold = Instant.now().minus(entriesMaxAge); cleaner.cleanEntriesOlderThan(threshold); } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/OldStatusesCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/OldStatusesCleanupTask.java index 0c19afe3..62e87c90 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/task/OldStatusesCleanupTask.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/OldStatusesCleanupTask.java @@ -6,11 +6,10 @@ import java.util.concurrent.TimeUnit; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.service.db.DatabaseCleaningService; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class OldStatusesCleanupTask extends ScheduledTask { @@ -19,7 +18,7 @@ public class OldStatusesCleanupTask extends ScheduledTask { @Override public void run() { - Instant threshold = config.getApplicationSettings().getUnreadThreshold(); + Instant threshold = config.database().cleanup().statusesInstantThreshold(); if (threshold != null) { cleaner.cleanStatusesOlderThan(threshold); } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedContentsCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedContentsCleanupTask.java index 7b2afcaa..786b7708 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedContentsCleanupTask.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedContentsCleanupTask.java @@ -4,11 +4,10 @@ import java.util.concurrent.TimeUnit; import com.commafeed.backend.service.db.DatabaseCleaningService; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class OrphanedContentsCleanupTask extends ScheduledTask { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedFeedsCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedFeedsCleanupTask.java index 4606dad3..81eb14ea 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedFeedsCleanupTask.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedFeedsCleanupTask.java @@ -4,11 +4,10 @@ import java.util.concurrent.TimeUnit; import com.commafeed.backend.service.db.DatabaseCleaningService; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class OrphanedFeedsCleanupTask extends ScheduledTask { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/TaskScheduler.java b/commafeed-server/src/main/java/com/commafeed/backend/task/TaskScheduler.java new file mode 100644 index 00000000..73a37bf0 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/TaskScheduler.java @@ -0,0 +1,28 @@ +package com.commafeed.backend.task; + +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import io.quarkus.arc.All; +import jakarta.inject.Singleton; + +@Singleton +public class TaskScheduler { + + private final List tasks; + private final ScheduledExecutorService executor; + + public TaskScheduler(@All List tasks) { + this.tasks = tasks; + this.executor = Executors.newScheduledThreadPool(tasks.size()); + } + + public void start() { + tasks.forEach(task -> task.register(executor)); + } + + public void stop() { + executor.shutdownNow(); + } +} diff --git a/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/InPageReferenceFeedURLProvider.java b/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/InPageReferenceFeedURLProvider.java index 36913423..b2bf773c 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/InPageReferenceFeedURLProvider.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/InPageReferenceFeedURLProvider.java @@ -4,6 +4,9 @@ import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.select.Elements; +import jakarta.inject.Singleton; + +@Singleton public class InPageReferenceFeedURLProvider implements FeedURLProvider { @Override diff --git a/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/YoutubeFeedURLProvider.java b/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/YoutubeFeedURLProvider.java index ab94a1c0..882da01f 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/YoutubeFeedURLProvider.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/YoutubeFeedURLProvider.java @@ -3,12 +3,15 @@ package com.commafeed.backend.urlprovider; import java.util.regex.Matcher; import java.util.regex.Pattern; +import jakarta.inject.Singleton; + /** * Workaround for Youtube channels * * converts the channel URL https://www.youtube.com/channel/CHANNEL_ID to the valid feed URL * https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID */ +@Singleton public class YoutubeFeedURLProvider implements FeedURLProvider { private static final Pattern REGEXP = Pattern.compile("(.*\\byoutube\\.com)\\/channel\\/([^\\/]+)", Pattern.CASE_INSENSITIVE); diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheck.java b/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheck.java deleted file mode 100644 index 0e685686..00000000 --- a/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheck.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.commafeed.frontend.auth; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import com.commafeed.backend.model.UserRole.Role; - -@Inherited -@Target({ ElementType.PARAMETER }) -@Retention(RetentionPolicy.RUNTIME) -public @interface SecurityCheck { - - /** - * Roles needed. - */ - Role value() default Role.USER; - - boolean apiKeyAllowed() default false; -} \ No newline at end of file diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactory.java b/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactory.java deleted file mode 100644 index 2e14bd7a..00000000 --- a/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactory.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.commafeed.frontend.auth; - -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.Function; - -import org.glassfish.jersey.server.ContainerRequest; - -import com.commafeed.CommaFeedConfiguration; -import com.commafeed.backend.dao.UserDAO; -import com.commafeed.backend.model.User; -import com.commafeed.backend.model.UserRole.Role; -import com.commafeed.backend.service.UserService; -import com.commafeed.frontend.session.SessionHelper; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.core.HttpHeaders; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public class SecurityCheckFactory implements Function { - - private static final String PREFIX = "Basic"; - - private final UserDAO userDAO; - private final UserService userService; - private final CommaFeedConfiguration config; - private final HttpServletRequest request; - private final Role role; - private final boolean apiKeyAllowed; - - @Override - public User apply(ContainerRequest req) { - Optional user = apiKeyLogin(); - if (user.isEmpty()) { - user = basicAuthenticationLogin(); - } - if (user.isEmpty()) { - user = cookieSessionLogin(new SessionHelper(request)); - } - - if (user.isPresent()) { - Set roles = userService.getRoles(user.get()); - if (roles.contains(role)) { - return user.get(); - } else { - throw buildWebApplicationException(Response.Status.FORBIDDEN, "You don't have the required role to access this resource."); - } - } else { - throw buildWebApplicationException(Response.Status.UNAUTHORIZED, "Credentials are required to access this resource."); - } - } - - Optional cookieSessionLogin(SessionHelper sessionHelper) { - Optional loggedInUser = sessionHelper.getLoggedInUserId().map(userDAO::findById); - loggedInUser.ifPresent(userService::performPostLoginActivities); - return loggedInUser; - } - - private Optional basicAuthenticationLogin() { - String header = request.getHeader(HttpHeaders.AUTHORIZATION); - if (header != null) { - int space = header.indexOf(' '); - if (space > 0) { - String method = header.substring(0, space); - if (PREFIX.equalsIgnoreCase(method)) { - byte[] decodedBytes = Base64.getDecoder().decode(header.substring(space + 1)); - String decoded = new String(decodedBytes, StandardCharsets.ISO_8859_1); - int i = decoded.indexOf(':'); - if (i > 0) { - String username = decoded.substring(0, i); - String password = decoded.substring(i + 1); - return userService.login(username, password); - } - } - } - } - return Optional.empty(); - } - - private Optional apiKeyLogin() { - String apiKey = request.getParameter("apiKey"); - if (apiKey != null && apiKeyAllowed) { - return userService.login(apiKey); - } - return Optional.empty(); - } - - private WebApplicationException buildWebApplicationException(Response.Status status, String message) { - Map response = new HashMap<>(); - response.put("message", message); - response.put("allowRegistrations", config.getApplicationSettings().getAllowRegistrations()); - return new WebApplicationException(Response.status(status).entity(response).type(MediaType.APPLICATION_JSON).build()); - } - -} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactoryProvider.java b/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactoryProvider.java deleted file mode 100644 index 4e70d479..00000000 --- a/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactoryProvider.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.commafeed.frontend.auth; - -import java.util.function.Function; - -import org.glassfish.hk2.utilities.binding.AbstractBinder; -import org.glassfish.jersey.server.ContainerRequest; -import org.glassfish.jersey.server.internal.inject.AbstractValueParamProvider; -import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider; -import org.glassfish.jersey.server.model.Parameter; -import org.glassfish.jersey.server.spi.internal.ValueParamProvider; - -import com.commafeed.CommaFeedConfiguration; -import com.commafeed.backend.dao.UserDAO; -import com.commafeed.backend.model.User; -import com.commafeed.backend.service.UserService; - -import jakarta.inject.Inject; -import jakarta.inject.Singleton; -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; - -@Singleton -public class SecurityCheckFactoryProvider extends AbstractValueParamProvider { - - private final UserService userService; - private final UserDAO userDAO; - private final CommaFeedConfiguration config; - private final HttpServletRequest request; - - @Inject - public SecurityCheckFactoryProvider(final MultivaluedParameterExtractorProvider extractorProvider, UserDAO userDAO, - UserService userService, CommaFeedConfiguration config, HttpServletRequest request) { - super(() -> extractorProvider, Parameter.Source.UNKNOWN); - this.userDAO = userDAO; - this.userService = userService; - this.config = config; - this.request = request; - } - - @Override - protected Function createValueProvider(Parameter parameter) { - final Class classType = parameter.getRawType(); - - SecurityCheck securityCheck = parameter.getAnnotation(SecurityCheck.class); - if (securityCheck == null) { - return null; - } - - if (!classType.isAssignableFrom(User.class)) { - return null; - } - - return new SecurityCheckFactory(userDAO, userService, config, request, securityCheck.value(), securityCheck.apiKeyAllowed()); - } - - @RequiredArgsConstructor - public static class Binder extends AbstractBinder { - - private final UserDAO userDAO; - private final UserService userService; - private final CommaFeedConfiguration config; - - @Override - protected void configure() { - bind(SecurityCheckFactoryProvider.class).to(ValueParamProvider.class).in(Singleton.class); - bind(userDAO).to(UserDAO.class); - bind(userService).to(UserService.class); - bind(config).to(CommaFeedConfiguration.class); - } - } - -} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/Category.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/Category.java index c360d7cd..631e56a7 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/Category.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/Category.java @@ -4,6 +4,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import io.quarkus.runtime.annotations.RegisterForReflection; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import lombok.Data; @@ -11,6 +12,7 @@ import lombok.Data; @SuppressWarnings("serial") @Schema(description = "Entry details") @Data +@RegisterForReflection public class Category implements Serializable { @Schema(description = "category id", requiredMode = RequiredMode.REQUIRED) diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/Entries.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/Entries.java index 9e7d6483..9d51ee62 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/Entries.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/Entries.java @@ -4,6 +4,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import io.quarkus.runtime.annotations.RegisterForReflection; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import lombok.Data; @@ -11,6 +12,7 @@ import lombok.Data; @SuppressWarnings("serial") @Schema(description = "List of entries with some metadata") @Data +@RegisterForReflection public class Entries implements Serializable { @Schema(description = "name of the feed or the category requested", requiredMode = RequiredMode.REQUIRED) diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/Entry.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/Entry.java index 87624d77..b7add91f 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/Entry.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/Entry.java @@ -19,6 +19,7 @@ import com.rometools.rome.feed.synd.SyndEnclosureImpl; import com.rometools.rome.feed.synd.SyndEntry; import com.rometools.rome.feed.synd.SyndEntryImpl; +import io.quarkus.runtime.annotations.RegisterForReflection; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import lombok.Data; @@ -26,6 +27,7 @@ import lombok.Data; @SuppressWarnings("serial") @Schema(description = "Entry details") @Data +@RegisterForReflection public class Entry implements Serializable { @Schema(description = "entry id", requiredMode = RequiredMode.REQUIRED) diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/FeedInfo.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/FeedInfo.java index ffc4cca3..643ad6a5 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/FeedInfo.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/FeedInfo.java @@ -2,6 +2,7 @@ package com.commafeed.frontend.model; import java.io.Serializable; +import io.quarkus.runtime.annotations.RegisterForReflection; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import lombok.Data; @@ -9,6 +10,7 @@ import lombok.Data; @SuppressWarnings("serial") @Schema(description = "Feed details") @Data +@RegisterForReflection public class FeedInfo implements Serializable { @Schema(description = "url", requiredMode = RequiredMode.REQUIRED) diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/ServerInfo.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/ServerInfo.java index 0eee703b..f7ffd7cf 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/ServerInfo.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/ServerInfo.java @@ -2,6 +2,7 @@ package com.commafeed.frontend.model; import java.io.Serializable; +import io.quarkus.runtime.annotations.RegisterForReflection; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import lombok.Data; @@ -9,6 +10,7 @@ import lombok.Data; @SuppressWarnings("serial") @Schema(description = "Server infos") @Data +@RegisterForReflection public class ServerInfo implements Serializable { @Schema diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/Settings.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/Settings.java index 28850422..e28412f0 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/Settings.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/Settings.java @@ -2,6 +2,7 @@ package com.commafeed.frontend.model; import java.io.Serializable; +import io.quarkus.runtime.annotations.RegisterForReflection; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import lombok.Data; @@ -9,6 +10,7 @@ import lombok.Data; @SuppressWarnings("serial") @Schema(description = "User settings") @Data +@RegisterForReflection public class Settings implements Serializable { @Schema(description = "user's preferred language, english if none", requiredMode = RequiredMode.REQUIRED) diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/Subscription.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/Subscription.java index 1a022af2..902de115 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/Subscription.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/Subscription.java @@ -8,6 +8,7 @@ import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedSubscription; +import io.quarkus.runtime.annotations.RegisterForReflection; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import lombok.Data; @@ -15,6 +16,7 @@ import lombok.Data; @SuppressWarnings("serial") @Schema(description = "User information") @Data +@RegisterForReflection public class Subscription implements Serializable { @Schema(description = "subscription id", requiredMode = RequiredMode.REQUIRED) diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/UnreadCount.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/UnreadCount.java index 4f946ea2..6ecd7a1a 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/UnreadCount.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/UnreadCount.java @@ -3,12 +3,14 @@ package com.commafeed.frontend.model; import java.io.Serializable; import java.time.Instant; +import io.quarkus.runtime.annotations.RegisterForReflection; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @SuppressWarnings("serial") @Schema(description = "Unread count") @Data +@RegisterForReflection public class UnreadCount implements Serializable { @Schema diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/UserModel.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/UserModel.java index 7c7d332a..43d5faea 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/UserModel.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/UserModel.java @@ -3,6 +3,7 @@ package com.commafeed.frontend.model; import java.io.Serializable; import java.time.Instant; +import io.quarkus.runtime.annotations.RegisterForReflection; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import lombok.Data; @@ -10,6 +11,7 @@ import lombok.Data; @SuppressWarnings("serial") @Schema(description = "User information") @Data +@RegisterForReflection public class UserModel implements Serializable { @Schema(description = "user id", requiredMode = RequiredMode.REQUIRED) diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/LoginRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/LoginRequest.java deleted file mode 100644 index c5dc4c98..00000000 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/LoginRequest.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.commafeed.frontend.model.request; - -import java.io.Serializable; - -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.Size; -import lombok.Data; - -@SuppressWarnings("serial") -@Data -@Schema -public class LoginRequest implements Serializable { - - @Schema(description = "username", requiredMode = RequiredMode.REQUIRED) - @Size(min = 3, max = 32) - private String name; - - @Schema(description = "password", requiredMode = RequiredMode.REQUIRED) - @NotEmpty - @Size(max = 128) - private String password; -} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/ProfileModificationRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/ProfileModificationRequest.java index c066a44d..1c2e1612 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/ProfileModificationRequest.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/ProfileModificationRequest.java @@ -2,7 +2,7 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; -import com.commafeed.frontend.auth.ValidPassword; +import com.commafeed.security.password.ValidPassword; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/RegistrationRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/RegistrationRequest.java index bdf84ec9..cc27052e 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/RegistrationRequest.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/RegistrationRequest.java @@ -2,7 +2,7 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; -import com.commafeed.frontend.auth.ValidPassword; +import com.commafeed.security.password.ValidPassword; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/AdminREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/AdminREST.java index 1eccadb7..7f4f2c27 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/AdminREST.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/AdminREST.java @@ -7,10 +7,7 @@ import java.util.Set; import org.apache.commons.lang3.StringUtils; import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.annotation.Timed; import com.commafeed.CommaFeedApplication; -import com.commafeed.CommaFeedConfiguration; -import com.commafeed.CommaFeedConfiguration.ApplicationSettings; import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.dao.UserRoleDAO; import com.commafeed.backend.model.User; @@ -18,14 +15,14 @@ import com.commafeed.backend.model.UserRole; import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.service.PasswordEncryptionService; import com.commafeed.backend.service.UserService; -import com.commafeed.frontend.auth.SecurityCheck; import com.commafeed.frontend.model.UserModel; import com.commafeed.frontend.model.request.AdminSaveUserRequest; import com.commafeed.frontend.model.request.IDRequest; +import com.commafeed.security.AuthenticationContext; +import com.commafeed.security.Roles; import com.google.common.base.Preconditions; import com.google.common.collect.Sets; -import io.dropwizard.hibernate.UnitOfWork; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; @@ -33,8 +30,9 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.inject.Inject; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Singleton; +import jakarta.transaction.Transactional; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; @@ -46,30 +44,29 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import lombok.RequiredArgsConstructor; -@Path("/admin") +@Path("/rest/admin") +@RolesAllowed(Roles.ADMIN) @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton @Tag(name = "Admin") public class AdminREST { + private final AuthenticationContext authenticationContext; private final UserDAO userDAO; private final UserRoleDAO userRoleDAO; private final UserService userService; private final PasswordEncryptionService encryptionService; - private final CommaFeedConfiguration config; private final MetricRegistry metrics; @Path("/user/save") @POST - @UnitOfWork + @Transactional @Operation( summary = "Save or update a user", description = "Save or update a user. If the id is not specified, a new user will be created") - @Timed - public Response adminSaveUser(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user, - @Parameter(required = true) AdminSaveUserRequest req) { + public Response adminSaveUser(@Parameter(required = true) AdminSaveUserRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getName()); @@ -87,6 +84,7 @@ public class AdminREST { return Response.status(Status.CONFLICT).entity(e.getMessage()).build(); } } else { + User user = authenticationContext.getCurrentUser(); if (req.getId().equals(user.getId()) && !req.isEnabled()) { return Response.status(Status.FORBIDDEN).entity("You cannot disable your own account.").build(); } @@ -121,14 +119,12 @@ public class AdminREST { @Path("/user/get/{id}") @GET - @UnitOfWork + @Transactional @Operation( summary = "Get user information", description = "Get user information", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = UserModel.class))) }) - @Timed - public Response adminGetUser(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user, - @Parameter(description = "user id", required = true) @PathParam("id") Long id) { + public Response adminGetUser(@Parameter(description = "user id", required = true) @PathParam("id") Long id) { Preconditions.checkNotNull(id); User u = userDAO.findById(id); UserModel userModel = new UserModel(); @@ -142,13 +138,12 @@ public class AdminREST { @Path("/user/getAll") @GET - @UnitOfWork + @Transactional @Operation( summary = "Get all users", description = "Get all users", responses = { @ApiResponse(content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserModel.class)))) }) - @Timed - public Response adminGetUsers(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user) { + public Response adminGetUsers() { Map users = new HashMap<>(); for (UserRole role : userRoleDAO.findAll()) { User u = role.getUser(); @@ -173,11 +168,9 @@ public class AdminREST { @Path("/user/delete") @POST - @UnitOfWork + @Transactional @Operation(summary = "Delete a user", description = "Delete a user, and all his subscriptions") - @Timed - public Response adminDeleteUser(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user, - @Parameter(required = true) IDRequest req) { + public Response adminDeleteUser(@Parameter(required = true) IDRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); @@ -185,6 +178,8 @@ public class AdminREST { if (u == null) { return Response.status(Status.NOT_FOUND).build(); } + + User user = authenticationContext.getCurrentUser(); if (user.getId().equals(u.getId())) { return Response.status(Status.FORBIDDEN).entity("You cannot delete your own user.").build(); } @@ -192,24 +187,11 @@ public class AdminREST { return Response.ok().build(); } - @Path("/settings") - @GET - @UnitOfWork - @Operation( - summary = "Retrieve application settings", - description = "Retrieve application settings", - responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = ApplicationSettings.class))) }) - @Timed - public Response getApplicationSettings(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user) { - return Response.ok(config.getApplicationSettings()).build(); - } - @Path("/metrics") @GET - @UnitOfWork + @Transactional @Operation(summary = "Retrieve server metrics") - @Timed - public Response getMetrics(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user) { + public Response getMetrics() { return Response.ok(metrics).build(); } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/CategoryREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/CategoryREST.java index 374f03a4..3a6fd362 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/CategoryREST.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/CategoryREST.java @@ -13,7 +13,6 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; -import com.codahale.metrics.annotation.Timed; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.dao.FeedCategoryDAO; @@ -29,7 +28,6 @@ import com.commafeed.backend.model.UserSettings.ReadingMode; import com.commafeed.backend.model.UserSettings.ReadingOrder; import com.commafeed.backend.service.FeedEntryService; import com.commafeed.backend.service.FeedSubscriptionService; -import com.commafeed.frontend.auth.SecurityCheck; import com.commafeed.frontend.model.Category; import com.commafeed.frontend.model.Entries; import com.commafeed.frontend.model.Entry; @@ -40,13 +38,14 @@ import com.commafeed.frontend.model.request.CategoryModificationRequest; import com.commafeed.frontend.model.request.CollapseRequest; import com.commafeed.frontend.model.request.IDRequest; import com.commafeed.frontend.model.request.MarkRequest; +import com.commafeed.security.AuthenticationContext; +import com.commafeed.security.Roles; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.rometools.rome.feed.synd.SyndFeed; import com.rometools.rome.feed.synd.SyndFeedImpl; import com.rometools.rome.io.SyndFeedOutput; -import io.dropwizard.hibernate.UnitOfWork; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; @@ -54,8 +53,9 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.inject.Inject; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Singleton; +import jakarta.transaction.Transactional; import jakarta.validation.Valid; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DefaultValue; @@ -70,11 +70,12 @@ import jakarta.ws.rs.core.Response.Status; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -@Path("/category") +@Path("/rest/category") +@RolesAllowed(Roles.USER) @Slf4j @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton @Tag(name = "Feed categories") public class CategoryREST { @@ -82,6 +83,7 @@ public class CategoryREST { public static final String ALL = "all"; public static final String STARRED = "starred"; + private final AuthenticationContext authenticationContext; private final FeedCategoryDAO feedCategoryDAO; private final FeedEntryStatusDAO feedEntryStatusDAO; private final FeedSubscriptionDAO feedSubscriptionDAO; @@ -92,13 +94,12 @@ public class CategoryREST { @Path("/entries") @GET - @UnitOfWork + @Transactional @Operation( summary = "Get category entries", description = "Get a list of category entries", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Entries.class))) }) - @Timed - public Response getCategoryEntries(@Parameter(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user, + public Response getCategoryEntries( @Parameter(description = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id, @Parameter( description = "all entries or only unread ones", @@ -137,22 +138,24 @@ public class CategoryREST { excludedIds = Arrays.stream(excludedSubscriptionIds.split(",")).map(Long::valueOf).toList(); } + User user = authenticationContext.getCurrentUser(); if (ALL.equals(id)) { entries.setName(Optional.ofNullable(tag).orElse("All")); + List subs = feedSubscriptionDAO.findAll(user); removeExcludedSubscriptions(subs, excludedIds); List list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate, offset, limit + 1, order, true, tag, null, null); for (FeedEntryStatus status : list) { - entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled())); + entries.getEntries().add(Entry.build(status, config.imageProxyEnabled())); } } else if (STARRED.equals(id)) { entries.setName("Starred"); List starred = feedEntryStatusDAO.findStarred(user, newerThanDate, offset, limit + 1, order, true); for (FeedEntryStatus status : starred) { - entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled())); + entries.getEntries().add(Entry.build(status, config.imageProxyEnabled())); } } else { FeedCategory parent = feedCategoryDAO.findById(user, Long.valueOf(id)); @@ -164,7 +167,7 @@ public class CategoryREST { offset, limit + 1, order, true, tag, null, null); for (FeedEntryStatus status : list) { - entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled())); + entries.getEntries().add(Entry.build(status, config.imageProxyEnabled())); } entries.setName(parent.getName()); } else { @@ -186,11 +189,10 @@ public class CategoryREST { @Path("/entriesAsFeed") @GET - @UnitOfWork + @Transactional @Operation(summary = "Get category entries as feed", description = "Get a feed of category entries") @Produces(MediaType.APPLICATION_XML) - @Timed - public Response getCategoryEntriesAsFeed(@Parameter(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user, + public Response getCategoryEntriesAsFeed( @Parameter(description = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id, @Parameter( description = "all entries or only unread ones", @@ -205,7 +207,7 @@ public class CategoryREST { description = "comma-separated list of excluded subscription ids") @QueryParam("excludedSubscriptionIds") String excludedSubscriptionIds, @Parameter(description = "keep only entries tagged with this tag") @QueryParam("tag") String tag) { - Response response = getCategoryEntries(user, id, readType, newerThan, offset, limit, order, keywords, excludedSubscriptionIds, tag); + Response response = getCategoryEntries(id, readType, newerThan, offset, limit, order, keywords, excludedSubscriptionIds, tag); if (response.getStatus() != Status.OK.getStatusCode()) { return response; } @@ -215,7 +217,7 @@ public class CategoryREST { feed.setFeedType("rss_2.0"); feed.setTitle("CommaFeed - " + entries.getName()); feed.setDescription("CommaFeed - " + entries.getName()); - feed.setLink(config.getApplicationSettings().getPublicUrl()); + feed.setLink(config.publicUrl()); feed.setEntries(entries.getEntries().stream().map(Entry::asRss).toList()); SyndFeedOutput output = new SyndFeedOutput(); @@ -231,11 +233,9 @@ public class CategoryREST { @Path("/mark") @POST - @UnitOfWork + @Transactional @Operation(summary = "Mark category entries", description = "Mark feed entries of this category as read") - @Timed - public Response markCategoryEntries(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(description = "category id, or 'all'", required = true) MarkRequest req) { + public Response markCategoryEntries(@Valid @Parameter(description = "category id, or 'all'", required = true) MarkRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); @@ -244,6 +244,7 @@ public class CategoryREST { String keywords = req.getKeywords(); List entryKeywords = FeedEntryKeyword.fromQueryString(keywords); + User user = authenticationContext.getCurrentUser(); if (ALL.equals(req.getId())) { List subs = feedSubscriptionDAO.findAll(user); removeExcludedSubscriptions(subs, req.getExcludedSubscriptions()); @@ -268,17 +269,17 @@ public class CategoryREST { @Path("/add") @POST - @UnitOfWork + @Transactional @Operation( summary = "Add a category", description = "Add a new feed category", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Long.class))) }) - @Timed - public Response addCategory(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(required = true) AddCategoryRequest req) { + public Response addCategory(@Valid @Parameter(required = true) AddCategoryRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getName()); + User user = authenticationContext.getCurrentUser(); + FeedCategory cat = new FeedCategory(); cat.setName(req.getName()); cat.setUser(user); @@ -296,14 +297,14 @@ public class CategoryREST { @POST @Path("/delete") - @UnitOfWork + @Transactional @Operation(summary = "Delete a category", description = "Delete an existing feed category") - @Timed - public Response deleteCategory(@Parameter(hidden = true) @SecurityCheck User user, @Parameter(required = true) IDRequest req) { + public Response deleteCategory(@Parameter(required = true) IDRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); + User user = authenticationContext.getCurrentUser(); FeedCategory cat = feedCategoryDAO.findById(user, req.getId()); if (cat != null) { List subs = feedSubscriptionDAO.findByCategory(user, cat); @@ -329,14 +330,13 @@ public class CategoryREST { @POST @Path("/modify") - @UnitOfWork + @Transactional @Operation(summary = "Rename a category", description = "Rename an existing feed category") - @Timed - public Response modifyCategory(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(required = true) CategoryModificationRequest req) { + public Response modifyCategory(@Valid @Parameter(required = true) CategoryModificationRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); + User user = authenticationContext.getCurrentUser(); FeedCategory category = feedCategoryDAO.findById(user, req.getId()); if (StringUtils.isNotBlank(req.getName())) { @@ -380,13 +380,13 @@ public class CategoryREST { @POST @Path("/collapse") - @UnitOfWork + @Transactional @Operation(summary = "Collapse a category", description = "Save collapsed or expanded status for a category") - @Timed - public Response collapseCategory(@Parameter(hidden = true) @SecurityCheck User user, @Parameter(required = true) CollapseRequest req) { + public Response collapseCategory(@Parameter(required = true) CollapseRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); + User user = authenticationContext.getCurrentUser(); FeedCategory category = feedCategoryDAO.findById(user, req.getId()); if (category == null) { return Response.status(Status.NOT_FOUND).build(); @@ -399,25 +399,25 @@ public class CategoryREST { @GET @Path("/unreadCount") - @UnitOfWork + @Transactional @Operation( summary = "Get unread count for feed subscriptions", responses = { @ApiResponse(content = @Content(array = @ArraySchema(schema = @Schema(implementation = UnreadCount.class)))) }) - @Timed - public Response getUnreadCount(@Parameter(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user) { + public Response getUnreadCount() { + User user = authenticationContext.getCurrentUser(); Map unreadCount = feedSubscriptionService.getUnreadCount(user); return Response.ok(Lists.newArrayList(unreadCount.values())).build(); } @GET @Path("/get") - @UnitOfWork + @Transactional @Operation( summary = "Get root category", description = "Get all categories and subscriptions of the user", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Category.class))) }) - @Timed - public Response getRootCategory(@Parameter(hidden = true) @SecurityCheck User user) { + public Response getRootCategory() { + User user = authenticationContext.getCurrentUser(); Category root = cache.getUserRootCategory(user); if (root == null) { log.debug("tree cache miss for {}", user.getId()); diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/EntryREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/EntryREST.java index 2726f4d2..1ebe6954 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/EntryREST.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/EntryREST.java @@ -2,24 +2,24 @@ package com.commafeed.frontend.resource; import java.util.List; -import com.codahale.metrics.annotation.Timed; import com.commafeed.backend.dao.FeedEntryTagDAO; import com.commafeed.backend.model.User; import com.commafeed.backend.service.FeedEntryService; import com.commafeed.backend.service.FeedEntryTagService; -import com.commafeed.frontend.auth.SecurityCheck; import com.commafeed.frontend.model.request.MarkRequest; import com.commafeed.frontend.model.request.MultipleMarkRequest; import com.commafeed.frontend.model.request.StarRequest; import com.commafeed.frontend.model.request.TagRequest; +import com.commafeed.security.AuthenticationContext; +import com.commafeed.security.Roles; import com.google.common.base.Preconditions; -import io.dropwizard.hibernate.UnitOfWork; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.inject.Inject; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Singleton; +import jakarta.transaction.Transactional; import jakarta.validation.Valid; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; @@ -30,42 +30,42 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import lombok.RequiredArgsConstructor; -@Path("/entry") +@Path("/rest/entry") +@RolesAllowed(Roles.USER) @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton @Tag(name = "Feed entries") public class EntryREST { + private final AuthenticationContext authenticationContext; private final FeedEntryTagDAO feedEntryTagDAO; private final FeedEntryService feedEntryService; private final FeedEntryTagService feedEntryTagService; @Path("/mark") @POST - @UnitOfWork + @Transactional @Operation(summary = "Mark a feed entry", description = "Mark a feed entry as read/unread") - @Timed - public Response markEntry(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(description = "Mark Request", required = true) MarkRequest req) { + public Response markEntry(@Valid @Parameter(description = "Mark Request", required = true) MarkRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); + User user = authenticationContext.getCurrentUser(); feedEntryService.markEntry(user, Long.valueOf(req.getId()), req.isRead()); return Response.ok().build(); } @Path("/markMultiple") @POST - @UnitOfWork + @Transactional @Operation(summary = "Mark multiple feed entries", description = "Mark feed entries as read/unread") - @Timed - public Response markEntries(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(description = "Multiple Mark Request", required = true) MultipleMarkRequest req) { + public Response markEntries(@Valid @Parameter(description = "Multiple Mark Request", required = true) MultipleMarkRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getRequests()); + User user = authenticationContext.getCurrentUser(); for (MarkRequest r : req.getRequests()) { Preconditions.checkNotNull(r.getId()); feedEntryService.markEntry(user, Long.valueOf(r.getId()), r.isRead()); @@ -76,15 +76,14 @@ public class EntryREST { @Path("/star") @POST - @UnitOfWork + @Transactional @Operation(summary = "Star a feed entry", description = "Mark a feed entry as read/unread") - @Timed - public Response starEntry(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(description = "Star Request", required = true) StarRequest req) { + public Response starEntry(@Valid @Parameter(description = "Star Request", required = true) StarRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); Preconditions.checkNotNull(req.getFeedId()); + User user = authenticationContext.getCurrentUser(); feedEntryService.starEntry(user, Long.valueOf(req.getId()), req.getFeedId(), req.isStarred()); return Response.ok().build(); @@ -92,24 +91,23 @@ public class EntryREST { @Path("/tags") @GET - @UnitOfWork + @Transactional @Operation(summary = "Get list of tags for the user", description = "Get list of tags for the user") - @Timed - public Response getTags(@Parameter(hidden = true) @SecurityCheck User user) { + public Response getTags() { + User user = authenticationContext.getCurrentUser(); List tags = feedEntryTagDAO.findByUser(user); return Response.ok(tags).build(); } @Path("/tag") @POST - @UnitOfWork + @Transactional @Operation(summary = "Set feed entry tags") - @Timed - public Response tagEntry(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(description = "Tag Request", required = true) TagRequest req) { + public Response tagEntry(@Valid @Parameter(description = "Tag Request", required = true) TagRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getEntryId()); + User user = authenticationContext.getCurrentUser(); feedEntryTagService.updateTags(user, req.getEntryId(), req.getTags()); return Response.ok().build(); diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/FeedREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/FeedREST.java index 0d25fa42..52879805 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/FeedREST.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/FeedREST.java @@ -1,9 +1,7 @@ package com.commafeed.frontend.resource; -import java.io.InputStream; import java.io.StringWriter; import java.net.URI; -import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.Calendar; import java.util.Collections; @@ -13,9 +11,8 @@ import java.util.Objects; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; -import org.glassfish.jersey.media.multipart.FormDataParam; +import org.jboss.resteasy.reactive.RestForm; -import com.codahale.metrics.annotation.Timed; import com.commafeed.CommaFeedApplication; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.cache.CacheService; @@ -44,7 +41,6 @@ import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterEx import com.commafeed.backend.service.FeedEntryService; import com.commafeed.backend.service.FeedService; import com.commafeed.backend.service.FeedSubscriptionService; -import com.commafeed.frontend.auth.SecurityCheck; import com.commafeed.frontend.model.Entries; import com.commafeed.frontend.model.Entry; import com.commafeed.frontend.model.FeedInfo; @@ -55,6 +51,8 @@ import com.commafeed.frontend.model.request.FeedModificationRequest; import com.commafeed.frontend.model.request.IDRequest; import com.commafeed.frontend.model.request.MarkRequest; import com.commafeed.frontend.model.request.SubscribeRequest; +import com.commafeed.security.AuthenticationContext; +import com.commafeed.security.Roles; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import com.rometools.opml.feed.opml.Opml; @@ -63,15 +61,15 @@ import com.rometools.rome.feed.synd.SyndFeedImpl; import com.rometools.rome.io.SyndFeedOutput; import com.rometools.rome.io.WireFeedOutput; -import io.dropwizard.hibernate.UnitOfWork; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.inject.Inject; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Singleton; +import jakarta.transaction.Transactional; import jakarta.validation.Valid; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DefaultValue; @@ -90,17 +88,19 @@ import jakarta.ws.rs.core.Response.Status; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -@Path("/feed") +@Path("/rest/feed") +@RolesAllowed(Roles.USER) @Slf4j @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton @Tag(name = "Feeds") public class FeedREST { private static final FeedEntry TEST_ENTRY = initTestEntry(); + private final AuthenticationContext authenticationContext; private final FeedSubscriptionDAO feedSubscriptionDAO; private final FeedCategoryDAO feedCategoryDAO; private final FeedEntryStatusDAO feedEntryStatusDAO; @@ -129,14 +129,12 @@ public class FeedREST { @Path("/entries") @GET - @UnitOfWork + @Transactional @Operation( summary = "Get feed entries", description = "Get a list of feed entries", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Entries.class))) }) - @Timed - public Response getFeedEntries(@Parameter(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user, - @Parameter(description = "id of the feed", required = true) @QueryParam("id") String id, + public Response getFeedEntries(@Parameter(description = "id of the feed", required = true) @QueryParam("id") String id, @Parameter( description = "all entries or only unread ones", required = true) @DefaultValue("unread") @QueryParam("readType") ReadingMode readType, @@ -164,6 +162,7 @@ public class FeedREST { Instant newerThanDate = newerThan == null ? null : Instant.ofEpochMilli(newerThan); + User user = authenticationContext.getCurrentUser(); FeedSubscription subscription = feedSubscriptionDAO.findById(user, Long.valueOf(id)); if (subscription != null) { entries.setName(subscription.getTitle()); @@ -175,7 +174,7 @@ public class FeedREST { entryKeywords, newerThanDate, offset, limit + 1, order, true, null, null, null); for (FeedEntryStatus status : list) { - entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled())); + entries.getEntries().add(Entry.build(status, config.imageProxyEnabled())); } boolean hasMore = entries.getEntries().size() > limit; @@ -195,12 +194,10 @@ public class FeedREST { @Path("/entriesAsFeed") @GET - @UnitOfWork + @Transactional @Operation(summary = "Get feed entries as a feed", description = "Get a feed of feed entries") @Produces(MediaType.APPLICATION_XML) - @Timed - public Response getFeedEntriesAsFeed(@Parameter(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user, - @Parameter(description = "id of the feed", required = true) @QueryParam("id") String id, + public Response getFeedEntriesAsFeed(@Parameter(description = "id of the feed", required = true) @QueryParam("id") String id, @Parameter( description = "all entries or only unread ones", required = true) @DefaultValue("all") @QueryParam("readType") ReadingMode readType, @@ -210,7 +207,7 @@ public class FeedREST { @Parameter(description = "date ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order, @Parameter( description = "search for keywords in either the title or the content of the entries, separated by spaces, 3 characters minimum") @QueryParam("keywords") String keywords) { - Response response = getFeedEntries(user, id, readType, newerThan, offset, limit, order, keywords); + Response response = getFeedEntries(id, readType, newerThan, offset, limit, order, keywords); if (response.getStatus() != Status.OK.getStatusCode()) { return response; } @@ -220,7 +217,7 @@ public class FeedREST { feed.setFeedType("rss_2.0"); feed.setTitle("CommaFeed - " + entries.getName()); feed.setDescription("CommaFeed - " + entries.getName()); - feed.setLink(config.getApplicationSettings().getPublicUrl()); + feed.setLink(config.publicUrl()); feed.setEntries(entries.getEntries().stream().map(Entry::asRss).toList()); SyndFeedOutput output = new SyndFeedOutput(); @@ -253,14 +250,12 @@ public class FeedREST { @POST @Path("/fetch") - @UnitOfWork + @Transactional @Operation( summary = "Fetch a feed", description = "Fetch a feed by its url", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = FeedInfo.class))) }) - @Timed - public Response fetchFeed(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(description = "feed url", required = true) FeedInfoRequest req) { + public Response fetchFeed(@Valid @Parameter(description = "feed url", required = true) FeedInfoRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getUrl()); @@ -279,25 +274,23 @@ public class FeedREST { @Path("/refreshAll") @GET - @UnitOfWork + @Transactional @Operation(summary = "Queue all feeds of the user for refresh", description = "Manually add all feeds of the user to the refresh queue") - @Timed - public Response queueAllForRefresh(@Parameter(hidden = true) @SecurityCheck User user) { + public Response queueAllForRefresh() { + User user = authenticationContext.getCurrentUser(); feedSubscriptionService.refreshAll(user); return Response.ok().build(); } @Path("/refresh") @POST - @UnitOfWork + @Transactional @Operation(summary = "Queue a feed for refresh", description = "Manually add a feed to the refresh queue") - @Timed - public Response queueForRefresh(@Parameter(hidden = true) @SecurityCheck User user, - @Parameter(description = "Feed id", required = true) IDRequest req) { - + public Response queueForRefresh(@Parameter(description = "Feed id", required = true) IDRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); + User user = authenticationContext.getCurrentUser(); FeedSubscription sub = feedSubscriptionDAO.findById(user, req.getId()); if (sub != null) { Feed feed = sub.getFeed(); @@ -309,11 +302,9 @@ public class FeedREST { @Path("/mark") @POST - @UnitOfWork + @Transactional @Operation(summary = "Mark feed entries", description = "Mark feed entries as read (unread is not supported)") - @Timed - public Response markFeedEntries(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(description = "Mark request", required = true) MarkRequest req) { + public Response markFeedEntries(@Valid @Parameter(description = "Mark request", required = true) MarkRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); @@ -322,6 +313,7 @@ public class FeedREST { String keywords = req.getKeywords(); List entryKeywords = FeedEntryKeyword.fromQueryString(keywords); + User user = authenticationContext.getCurrentUser(); FeedSubscription subscription = feedSubscriptionDAO.findById(user, Long.valueOf(req.getId())); if (subscription != null) { feedEntryService.markSubscriptionEntries(user, Collections.singletonList(subscription), olderThan, insertedBefore, @@ -332,15 +324,14 @@ public class FeedREST { @GET @Path("/get/{id}") - @UnitOfWork + @Transactional @Operation( summary = "get feed", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Subscription.class))) }) - @Timed - public Response getFeed(@Parameter(hidden = true) @SecurityCheck User user, - @Parameter(description = "user id", required = true) @PathParam("id") Long id) { - + public Response getFeed(@Parameter(description = "user id", required = true) @PathParam("id") Long id) { Preconditions.checkNotNull(id); + + User user = authenticationContext.getCurrentUser(); FeedSubscription sub = feedSubscriptionDAO.findById(user, id); if (sub == null) { return Response.status(Status.NOT_FOUND).build(); @@ -351,13 +342,12 @@ public class FeedREST { @GET @Path("/favicon/{id}") - @UnitOfWork + @Transactional @Operation(summary = "Fetch a feed's icon", description = "Fetch a feed's icon") - @Timed - public Response getFeedFavicon(@Parameter(hidden = true) @SecurityCheck User user, - @Parameter(description = "subscription id", required = true) @PathParam("id") Long id) { - + public Response getFeedFavicon(@Parameter(description = "subscription id", required = true) @PathParam("id") Long id) { Preconditions.checkNotNull(id); + + User user = authenticationContext.getCurrentUser(); FeedSubscription subscription = feedSubscriptionDAO.findById(user, id); if (subscription == null) { return Response.status(Status.NOT_FOUND).build(); @@ -382,14 +372,12 @@ public class FeedREST { @POST @Path("/subscribe") - @UnitOfWork + @Transactional @Operation( summary = "Subscribe to a feed", description = "Subscribe to a feed", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Long.class))) }) - @Timed - public Response subscribe(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(description = "subscription request", required = true) SubscribeRequest req) { + public Response subscribe(@Valid @Parameter(description = "subscription request", required = true) SubscribeRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getTitle()); Preconditions.checkNotNull(req.getUrl()); @@ -401,6 +389,7 @@ public class FeedREST { } FeedInfo info = fetchFeedInternal(prependHttp(req.getUrl())); + User user = authenticationContext.getCurrentUser(); long subscriptionId = feedSubscriptionService.subscribe(user, info.getUrl(), req.getTitle(), category); return Response.ok(subscriptionId).build(); } catch (Exception e) { @@ -413,19 +402,18 @@ public class FeedREST { @GET @Path("/subscribe") - @UnitOfWork + @Transactional @Operation(summary = "Subscribe to a feed", description = "Subscribe to a feed") - @Timed - public Response subscribeFromUrl(@Parameter(hidden = true) @SecurityCheck User user, - @Parameter(description = "feed url", required = true) @QueryParam("url") String url) { + public Response subscribeFromUrl(@Parameter(description = "feed url", required = true) @QueryParam("url") String url) { try { Preconditions.checkNotNull(url); FeedInfo info = fetchFeedInternal(prependHttp(url)); + User user = authenticationContext.getCurrentUser(); feedSubscriptionService.subscribe(user, info.getUrl(), info.getTitle()); } catch (Exception e) { log.info("Could not subscribe to url {} : {}", url, e.getMessage()); } - return Response.temporaryRedirect(URI.create(config.getApplicationSettings().getPublicUrl())).build(); + return Response.temporaryRedirect(URI.create(config.publicUrl())).build(); } private String prependHttp(String url) { @@ -437,13 +425,13 @@ public class FeedREST { @POST @Path("/unsubscribe") - @UnitOfWork + @Transactional @Operation(summary = "Unsubscribe from a feed", description = "Unsubscribe from a feed") - @Timed - public Response unsubscribe(@Parameter(hidden = true) @SecurityCheck User user, @Parameter(required = true) IDRequest req) { + public Response unsubscribe(@Parameter(required = true) IDRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); + User user = authenticationContext.getCurrentUser(); boolean deleted = feedSubscriptionService.unsubscribe(user, req.getId()); if (deleted) { return Response.ok().build(); @@ -454,11 +442,9 @@ public class FeedREST { @POST @Path("/modify") - @UnitOfWork + @Transactional @Operation(summary = "Modify a subscription", description = "Modify a feed subscription") - @Timed - public Response modifyFeed(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(description = "subscription id", required = true) FeedModificationRequest req) { + public Response modifyFeed(@Valid @Parameter(description = "subscription id", required = true) FeedModificationRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); @@ -468,6 +454,7 @@ public class FeedREST { return Response.status(Status.BAD_REQUEST).entity(e.getCause().getMessage()).type(MediaType.TEXT_PLAIN).build(); } + User user = authenticationContext.getCurrentUser(); FeedSubscription subscription = feedSubscriptionDAO.findById(user, req.getId()); subscription.setFilter(StringUtils.lowerCase(req.getFilter())); @@ -509,39 +496,36 @@ public class FeedREST { @POST @Path("/import") - @UnitOfWork + @Transactional @Consumes(MediaType.MULTIPART_FORM_DATA) @Operation(summary = "OPML import", description = "Import an OPML file, posted as a FORM with the 'file' name") - @Timed - public Response importOpml(@Parameter(hidden = true) @SecurityCheck User user, - @Parameter(description = "ompl file", required = true) @FormDataParam("file") InputStream input) { + public Response importOpml(@Parameter(description = "ompl file", required = true) @RestForm("file") String opml) { + User user = authenticationContext.getCurrentUser(); if (CommaFeedApplication.USERNAME_DEMO.equals(user.getName())) { return Response.status(Status.FORBIDDEN).entity("Import is disabled for the demo account").build(); } try { - String opml = new String(input.readAllBytes(), StandardCharsets.UTF_8); opmlImporter.importOpml(user, opml); } catch (Exception e) { - log.error(e.getMessage(), e); - throw new WebApplicationException(Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build()); + return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build(); } return Response.ok().build(); } @GET @Path("/export") - @UnitOfWork + @Transactional @Produces(MediaType.APPLICATION_XML) @Operation(summary = "OPML export", description = "Export an OPML file of the user's subscriptions") - @Timed - public Response exportOpml(@Parameter(hidden = true) @SecurityCheck User user) { + public Response exportOpml() { + User user = authenticationContext.getCurrentUser(); Opml opml = opmlExporter.export(user); WireFeedOutput output = new WireFeedOutput(); String opmlString; try { opmlString = output.outputString(opml); } catch (Exception e) { - return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e).build(); + return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build(); } return Response.ok(opmlString).build(); } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/ServerREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/ServerREST.java index eb213f50..881a7a04 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/ServerREST.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/ServerREST.java @@ -1,25 +1,23 @@ package com.commafeed.frontend.resource; -import org.apache.commons.lang3.StringUtils; - -import com.codahale.metrics.annotation.Timed; import com.commafeed.CommaFeedConfiguration; +import com.commafeed.CommaFeedVersion; import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter.HttpResult; import com.commafeed.backend.feed.FeedUtils; -import com.commafeed.backend.model.User; -import com.commafeed.frontend.auth.SecurityCheck; import com.commafeed.frontend.model.ServerInfo; +import com.commafeed.security.Roles; -import io.dropwizard.hibernate.UnitOfWork; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.inject.Inject; +import jakarta.annotation.security.PermitAll; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Singleton; +import jakarta.transaction.Transactional; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @@ -30,49 +28,50 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import lombok.RequiredArgsConstructor; -@Path("/server") +@Path("/rest/server") + @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton @Tag(name = "Server") public class ServerREST { private final HttpGetter httpGetter; private final CommaFeedConfiguration config; + private final CommaFeedVersion version; @Path("/get") @GET - @UnitOfWork + @PermitAll + @Transactional @Operation( summary = "Get server infos", description = "Get server infos", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = ServerInfo.class))) }) - @Timed public Response getServerInfos() { ServerInfo infos = new ServerInfo(); - infos.setAnnouncement(config.getApplicationSettings().getAnnouncement()); - infos.setVersion(config.getVersion()); - infos.setGitCommit(config.getGitCommit()); - infos.setAllowRegistrations(config.getApplicationSettings().getAllowRegistrations()); - infos.setGoogleAnalyticsCode(config.getApplicationSettings().getGoogleAnalyticsTrackingCode()); - infos.setSmtpEnabled(StringUtils.isNotBlank(config.getApplicationSettings().getSmtpHost())); - infos.setDemoAccountEnabled(config.getApplicationSettings().getCreateDemoAccount()); - infos.setWebsocketEnabled(config.getApplicationSettings().getWebsocketEnabled()); - infos.setWebsocketPingInterval(config.getApplicationSettings().getWebsocketPingInterval().toMilliseconds()); - infos.setTreeReloadInterval(config.getApplicationSettings().getTreeReloadInterval().toMilliseconds()); + infos.setAnnouncement(config.announcement().orElse(null)); + infos.setVersion(version.getVersion()); + infos.setGitCommit(version.getGitCommit()); + infos.setAllowRegistrations(config.users().allowRegistrations()); + infos.setGoogleAnalyticsCode(config.googleAnalyticsTrackingCode().orElse(null)); + infos.setSmtpEnabled(config.smtp().isPresent()); + infos.setDemoAccountEnabled(config.users().createDemoAccount()); + infos.setWebsocketEnabled(config.websocket().enabled()); + infos.setWebsocketPingInterval(config.websocket().pingInterval().toMillis()); + infos.setTreeReloadInterval(config.websocket().treeReloadInterval().toMillis()); return Response.ok(infos).build(); } @Path("/proxy") @GET - @UnitOfWork + @RolesAllowed(Roles.USER) + @Transactional @Operation(summary = "proxy image") @Produces("image/png") - @Timed - public Response getProxiedImage(@Parameter(hidden = true) @SecurityCheck User user, - @Parameter(description = "image url", required = true) @QueryParam("u") String url) { - if (!config.getApplicationSettings().getImageProxyEnabled()) { + public Response getProxiedImage(@Parameter(description = "image url", required = true) @QueryParam("u") String url) { + if (!config.imageProxyEnabled()) { return Response.status(Status.FORBIDDEN).build(); } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/UserREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/UserREST.java index 3ab79050..b391631f 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/UserREST.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/UserREST.java @@ -10,7 +10,6 @@ import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.apache.hc.core5.net.URIBuilder; -import com.codahale.metrics.annotation.Timed; import com.commafeed.CommaFeedApplication; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.Digests; @@ -29,25 +28,25 @@ import com.commafeed.backend.model.UserSettings.ScrollMode; import com.commafeed.backend.service.MailService; import com.commafeed.backend.service.PasswordEncryptionService; import com.commafeed.backend.service.UserService; -import com.commafeed.frontend.auth.SecurityCheck; import com.commafeed.frontend.model.Settings; import com.commafeed.frontend.model.UserModel; -import com.commafeed.frontend.model.request.LoginRequest; import com.commafeed.frontend.model.request.PasswordResetRequest; import com.commafeed.frontend.model.request.ProfileModificationRequest; import com.commafeed.frontend.model.request.RegistrationRequest; -import com.commafeed.frontend.session.SessionHelper; +import com.commafeed.security.AuthenticationContext; +import com.commafeed.security.Roles; import com.google.common.base.Preconditions; -import io.dropwizard.hibernate.UnitOfWork; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.inject.Inject; +import jakarta.annotation.security.PermitAll; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Singleton; +import jakarta.transaction.Transactional; import jakarta.validation.Valid; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; @@ -56,22 +55,23 @@ import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -@Path("/user") +@Path("/rest/user") +@RolesAllowed(Roles.USER) @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton @Tag(name = "Users") public class UserREST { + private final AuthenticationContext authenticationContext; private final UserDAO userDAO; private final UserRoleDAO userRoleDAO; private final UserSettingsDAO userSettingsDAO; @@ -82,14 +82,15 @@ public class UserREST { @Path("/settings") @GET - @UnitOfWork + @Transactional @Operation( summary = "Retrieve user settings", description = "Retrieve user settings", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Settings.class))) }) - @Timed - public Response getUserSettings(@Parameter(hidden = true) @SecurityCheck User user) { + public Response getUserSettings() { Settings s = new Settings(); + + User user = authenticationContext.getCurrentUser(); UserSettings settings = userSettingsDAO.findByUser(user); if (settings != null) { s.setReadingMode(settings.getReadingMode().name()); @@ -145,12 +146,12 @@ public class UserREST { @Path("/settings") @POST - @UnitOfWork + @Transactional @Operation(summary = "Save user settings", description = "Save user settings") - @Timed - public Response saveUserSettings(@Parameter(hidden = true) @SecurityCheck User user, @Parameter(required = true) Settings settings) { + public Response saveUserSettings(@Parameter(required = true) Settings settings) { Preconditions.checkNotNull(settings); + User user = authenticationContext.getCurrentUser(); UserSettings s = userSettingsDAO.findByUser(user); if (s == null) { s = new UserSettings(); @@ -187,12 +188,13 @@ public class UserREST { @Path("/profile") @GET - @UnitOfWork + @Transactional @Operation( summary = "Retrieve user's profile", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = UserModel.class))) }) - @Timed - public Response getUserProfile(@Parameter(hidden = true) @SecurityCheck User user) { + public Response getUserProfile() { + User user = authenticationContext.getCurrentUser(); + UserModel userModel = new UserModel(); userModel.setId(user.getId()); userModel.setName(user.getName()); @@ -209,11 +211,10 @@ public class UserREST { @Path("/profile") @POST - @UnitOfWork + @Transactional @Operation(summary = "Save user's profile") - @Timed - public Response saveUserProfile(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(required = true) ProfileModificationRequest request) { + public Response saveUserProfile(@Valid @Parameter(required = true) ProfileModificationRequest request) { + User user = authenticationContext.getCurrentUser(); if (CommaFeedApplication.USERNAME_DEMO.equals(user.getName())) { return Response.status(Status.FORBIDDEN).build(); } @@ -242,49 +243,29 @@ public class UserREST { user.setApiKey(userService.generateApiKey(user)); } - userDAO.update(user); + userDAO.saveOrUpdate(user); return Response.ok().build(); } @Path("/register") + @PermitAll @POST - @UnitOfWork + @Transactional @Operation(summary = "Register a new account") - @Timed - public Response registerUser(@Valid @Parameter(required = true) RegistrationRequest req, - @Context @Parameter(hidden = true) SessionHelper sessionHelper) { + public Response registerUser(@Valid @Parameter(required = true) RegistrationRequest req) { try { - User registeredUser = userService.register(req.getName(), req.getPassword(), req.getEmail(), - Collections.singletonList(Role.USER)); - userService.login(req.getName(), req.getPassword()); - sessionHelper.setLoggedInUser(registeredUser); + userService.register(req.getName(), req.getPassword(), req.getEmail(), Collections.singletonList(Role.USER)); return Response.ok().build(); } catch (final IllegalArgumentException e) { throw new BadRequestException(e.getMessage()); } } - @Path("/login") - @POST - @UnitOfWork - @Operation(summary = "Login and create a session") - @Timed - public Response login(@Valid @Parameter(required = true) LoginRequest req, - @Parameter(hidden = true) @Context SessionHelper sessionHelper) { - Optional user = userService.login(req.getName(), req.getPassword()); - if (user.isPresent()) { - sessionHelper.setLoggedInUser(user.get()); - return Response.ok().build(); - } else { - return Response.status(Response.Status.UNAUTHORIZED).entity("wrong username or password").type(MediaType.TEXT_PLAIN).build(); - } - } - @Path("/passwordReset") + @PermitAll @POST - @UnitOfWork + @Transactional @Operation(summary = "send a password reset email") - @Timed public Response sendPasswordReset(@Valid @Parameter(required = true) PasswordResetRequest req) { User user = userDAO.findByEmail(req.getEmail()); if (user == null) { @@ -304,7 +285,7 @@ public class UserREST { } private String buildEmailContent(User user) throws Exception { - String publicUrl = FeedUtils.removeTrailingSlash(config.getApplicationSettings().getPublicUrl()); + String publicUrl = FeedUtils.removeTrailingSlash(config.publicUrl()); publicUrl += "/rest/user/passwordResetCallback"; return String.format( "You asked for password recovery for account '%s', follow this link to change your password. Ignore this if you didn't request a password recovery.", @@ -320,10 +301,10 @@ public class UserREST { } @Path("/passwordResetCallback") + @PermitAll @GET - @UnitOfWork + @Transactional @Produces(MediaType.TEXT_HTML) - @Timed public Response passwordRecoveryCallback(@Parameter(required = true) @QueryParam("email") String email, @Parameter(required = true) @QueryParam("token") String token) { Preconditions.checkNotNull(email); @@ -352,16 +333,16 @@ public class UserREST { String message = "Your new password is: " + passwd; message += "
"; - message += String.format("Back to Homepage", config.getApplicationSettings().getPublicUrl()); + message += String.format("Back to Homepage", config.publicUrl()); return Response.ok(message).build(); } @Path("/profile/deleteAccount") @POST - @UnitOfWork + @Transactional @Operation(summary = "Delete the user account") - @Timed - public Response deleteUser(@Parameter(hidden = true) @SecurityCheck User user) { + public Response deleteUser() { + User user = authenticationContext.getCurrentUser(); if (CommaFeedApplication.USERNAME_ADMIN.equals(user.getName()) || CommaFeedApplication.USERNAME_DEMO.equals(user.getName())) { return Response.status(Status.FORBIDDEN).build(); } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverREST.java index 551f1f8e..69b7ba1a 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverREST.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverREST.java @@ -13,9 +13,9 @@ import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.jboss.resteasy.reactive.server.multipart.FormValue; +import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput; -import com.codahale.metrics.annotation.Timed; import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO; @@ -37,10 +37,10 @@ import com.commafeed.frontend.resource.fever.FeverResponse.FeverFeedGroup; import com.commafeed.frontend.resource.fever.FeverResponse.FeverGroup; import com.commafeed.frontend.resource.fever.FeverResponse.FeverItem; -import io.dropwizard.hibernate.UnitOfWork; import io.swagger.v3.oas.annotations.Hidden; -import jakarta.inject.Inject; +import jakarta.annotation.security.PermitAll; import jakarta.inject.Singleton; +import jakarta.transaction.Transactional; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; @@ -64,9 +64,10 @@ import lombok.RequiredArgsConstructor; * * See https://feedafever.com/api */ -@Path("/fever") +@Path("/rest/fever") +@PermitAll @Produces(MediaType.APPLICATION_JSON) -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton @Hidden public class FeverREST { @@ -88,8 +89,7 @@ public class FeverREST { @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Path(PATH) @POST - @UnitOfWork - @Timed + @Transactional public FeverResponse formUrlencoded(@Context UriInfo uri, @PathParam("userId") Long userId, MultivaluedMap form) { Map params = new HashMap<>(); uri.getQueryParameters().forEach((k, v) -> params.put(k, v.get(0))); @@ -101,8 +101,7 @@ public class FeverREST { // e.g. FeedMe @Path(PATH) @POST - @UnitOfWork - @Timed + @Transactional public FeverResponse noForm(@Context UriInfo uri, @PathParam("userId") Long userId) { Map params = new HashMap<>(); uri.getQueryParameters().forEach((k, v) -> params.put(k, v.get(0))); @@ -113,8 +112,7 @@ public class FeverREST { // e.g. Unread @Path(PATH) @GET - @UnitOfWork - @Timed + @Transactional public FeverResponse get(@Context UriInfo uri, @PathParam("userId") Long userId) { Map params = new HashMap<>(); uri.getQueryParameters().forEach((k, v) -> params.put(k, v.get(0))); @@ -126,12 +124,11 @@ public class FeverREST { @Consumes(MediaType.MULTIPART_FORM_DATA) @Path(PATH) @POST - @UnitOfWork - @Timed - public FeverResponse formData(@Context UriInfo uri, @PathParam("userId") Long userId, FormDataMultiPart form) { + @Transactional + public FeverResponse formData(@Context UriInfo uri, @PathParam("userId") Long userId, MultipartFormDataInput form) { Map params = new HashMap<>(); uri.getQueryParameters().forEach((k, v) -> params.put(k, v.get(0))); - form.getFields().forEach((k, v) -> params.put(k, v.get(0).getValue())); + form.getValues().forEach((k, v) -> params.put(k, v.stream().map(FormValue::getValue).findFirst().orElse(null))); return handle(userId, params); } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/AbstractCustomCodeServlet.java b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/AbstractCustomCodeServlet.java deleted file mode 100644 index 29df13f8..00000000 --- a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/AbstractCustomCodeServlet.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.commafeed.frontend.servlet; - -import java.io.IOException; -import java.util.Optional; - -import com.commafeed.backend.dao.UnitOfWork; -import com.commafeed.backend.dao.UserDAO; -import com.commafeed.backend.dao.UserSettingsDAO; -import com.commafeed.backend.model.User; -import com.commafeed.backend.model.UserSettings; -import com.commafeed.frontend.session.SessionHelper; - -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -abstract class AbstractCustomCodeServlet extends HttpServlet { - - private static final long serialVersionUID = 1L; - - private final transient UnitOfWork unitOfWork; - private final transient UserDAO userDAO; - private final transient UserSettingsDAO userSettingsDAO; - - @Override - protected final void doGet(final HttpServletRequest req, HttpServletResponse resp) throws IOException { - resp.setContentType(getMimeType()); - - SessionHelper sessionHelper = new SessionHelper(req); - Optional userId = sessionHelper.getLoggedInUserId(); - final Optional user = unitOfWork.call(() -> userId.map(userDAO::findById)); - if (user.isEmpty()) { - return; - } - - UserSettings settings = unitOfWork.call(() -> userSettingsDAO.findByUser(user.get())); - if (settings == null) { - return; - } - - String customCode = getCustomCode(settings); - if (customCode == null) { - return; - } - - resp.getWriter().write(customCode); - } - - protected abstract String getMimeType(); - - protected abstract String getCustomCode(UserSettings settings); -} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomCssServlet.java b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomCssServlet.java index 7bcc1f91..0f95ee28 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomCssServlet.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomCssServlet.java @@ -1,28 +1,39 @@ package com.commafeed.frontend.servlet; import com.commafeed.backend.dao.UnitOfWork; -import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.dao.UserSettingsDAO; +import com.commafeed.backend.model.User; import com.commafeed.backend.model.UserSettings; +import com.commafeed.security.AuthenticationContext; -import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import lombok.RequiredArgsConstructor; -public class CustomCssServlet extends AbstractCustomCodeServlet { +@Path("/custom_css.css") +@Produces("text/css") +@RequiredArgsConstructor +@Singleton +public class CustomCssServlet { - private static final long serialVersionUID = 1L; + private final AuthenticationContext authenticationContext; + private final UserSettingsDAO userSettingsDAO; + private final UnitOfWork unitOfWork; - @Inject - public CustomCssServlet(UnitOfWork unitOfWork, UserDAO userDAO, UserSettingsDAO userSettingsDAO) { - super(unitOfWork, userDAO, userSettingsDAO); - } + @GET + public String get() { + User user = authenticationContext.getCurrentUser(); + if (user == null) { + return ""; + } - @Override - protected String getMimeType() { - return "text/css"; - } + UserSettings settings = unitOfWork.call(() -> userSettingsDAO.findByUser(user)); + if (settings == null) { + return ""; + } - @Override - protected String getCustomCode(UserSettings settings) { return settings.getCustomCss(); } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomJsServlet.java b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomJsServlet.java index 0c29b7a9..2d651db5 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomJsServlet.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomJsServlet.java @@ -1,30 +1,39 @@ package com.commafeed.frontend.servlet; import com.commafeed.backend.dao.UnitOfWork; -import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.dao.UserSettingsDAO; +import com.commafeed.backend.model.User; import com.commafeed.backend.model.UserSettings; +import com.commafeed.security.AuthenticationContext; -import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import lombok.RequiredArgsConstructor; +@Path("/custom_js.js") +@Produces("application/javascript") +@RequiredArgsConstructor @Singleton -public class CustomJsServlet extends AbstractCustomCodeServlet { +public class CustomJsServlet { - private static final long serialVersionUID = 1L; + private final AuthenticationContext authenticationContext; + private final UserSettingsDAO userSettingsDAO; + private final UnitOfWork unitOfWork; - @Inject - public CustomJsServlet(UnitOfWork unitOfWork, UserDAO userDAO, UserSettingsDAO userSettingsDAO) { - super(unitOfWork, userDAO, userSettingsDAO); - } + @GET + public String get() { + User user = authenticationContext.getCurrentUser(); + if (user == null) { + return ""; + } - @Override - protected String getMimeType() { - return "application/javascript"; - } + UserSettings settings = unitOfWork.call(() -> userSettingsDAO.findByUser(user)); + if (settings == null) { + return ""; + } - @Override - protected String getCustomCode(UserSettings settings) { return settings.getCustomJs(); } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/LogoutServlet.java b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/LogoutServlet.java index 6d405709..09a7b426 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/LogoutServlet.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/LogoutServlet.java @@ -1,26 +1,35 @@ package com.commafeed.frontend.servlet; -import java.io.IOException; +import java.net.URI; +import java.time.Instant; +import java.util.Date; + +import org.eclipse.microprofile.config.inject.ConfigProperty; import com.commafeed.CommaFeedConfiguration; -import jakarta.inject.Inject; +import jakarta.annotation.security.PermitAll; import jakarta.inject.Singleton; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.NewCookie; +import jakarta.ws.rs.core.Response; import lombok.RequiredArgsConstructor; -@SuppressWarnings("serial") -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@Path("/logout") +@PermitAll +@RequiredArgsConstructor @Singleton -public class LogoutServlet extends HttpServlet { +public class LogoutServlet { + + @ConfigProperty(name = "quarkus.http.auth.form.cookie-name") + String cookieName; private final CommaFeedConfiguration config; - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - req.getSession().invalidate(); - resp.sendRedirect(resp.encodeRedirectURL(config.getApplicationSettings().getPublicUrl())); + @GET + public Response get() { + NewCookie removeCookie = new NewCookie.Builder(cookieName).maxAge(0).expiry(Date.from(Instant.EPOCH)).path("/").build(); + return Response.temporaryRedirect(URI.create(config.publicUrl())).cookie(removeCookie).build(); } } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java index dabda582..80ee3974 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java @@ -1,8 +1,7 @@ package com.commafeed.frontend.servlet; -import java.io.IOException; +import java.net.URI; import java.util.List; -import java.util.Optional; import org.apache.commons.lang3.StringUtils; @@ -11,86 +10,68 @@ import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO; import com.commafeed.backend.dao.UnitOfWork; -import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedEntryStatus; import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.User; import com.commafeed.backend.model.UserSettings.ReadingOrder; import com.commafeed.backend.service.FeedEntryService; -import com.commafeed.backend.service.UserService; import com.commafeed.frontend.resource.CategoryREST; -import com.commafeed.frontend.session.SessionHelper; +import com.commafeed.security.AuthenticationContext; import com.google.common.collect.Iterables; -import jakarta.inject.Inject; import jakarta.inject.Singleton; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Response; import lombok.RequiredArgsConstructor; -@SuppressWarnings("serial") -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@Path("/next") +@RequiredArgsConstructor @Singleton -public class NextUnreadServlet extends HttpServlet { - - private static final String PARAM_CATEGORYID = "category"; - private static final String PARAM_READINGORDER = "order"; +public class NextUnreadServlet { private final UnitOfWork unitOfWork; private final FeedSubscriptionDAO feedSubscriptionDAO; private final FeedEntryStatusDAO feedEntryStatusDAO; private final FeedCategoryDAO feedCategoryDAO; - private final UserDAO userDAO; - private final UserService userService; private final FeedEntryService feedEntryService; private final CommaFeedConfiguration config; + private final AuthenticationContext authenticationContext; - @Override - protected void doGet(final HttpServletRequest req, HttpServletResponse resp) throws IOException { - final String categoryId = req.getParameter(PARAM_CATEGORYID); - String orderParam = req.getParameter(PARAM_READINGORDER); - - SessionHelper sessionHelper = new SessionHelper(req); - Optional userId = sessionHelper.getLoggedInUserId(); - Optional user = unitOfWork.call(() -> userId.map(userDAO::findById)); - user.ifPresent(value -> unitOfWork.run(() -> userService.performPostLoginActivities(value))); - if (user.isEmpty()) { - resp.sendRedirect(resp.encodeRedirectURL(config.getApplicationSettings().getPublicUrl())); - return; + @GET + public Response get(@QueryParam("category") String categoryId, @QueryParam("order") @DefaultValue("desc") ReadingOrder order) { + User user = authenticationContext.getCurrentUser(); + if (user == null) { + return Response.temporaryRedirect(URI.create(config.publicUrl())).build(); } - final ReadingOrder order = StringUtils.equals(orderParam, "asc") ? ReadingOrder.asc : ReadingOrder.desc; - FeedEntryStatus status = unitOfWork.call(() -> { FeedEntryStatus s = null; if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) { - List subs = feedSubscriptionDAO.findAll(user.get()); - List statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subs, true, null, null, 0, 1, order, - true, null, null, null); + List subs = feedSubscriptionDAO.findAll(user); + List statuses = feedEntryStatusDAO.findBySubscriptions(user, subs, true, null, null, 0, 1, order, true, + null, null, null); s = Iterables.getFirst(statuses, null); } else { - FeedCategory category = feedCategoryDAO.findById(user.get(), Long.valueOf(categoryId)); + FeedCategory category = feedCategoryDAO.findById(user, Long.valueOf(categoryId)); if (category != null) { - List children = feedCategoryDAO.findAllChildrenCategories(user.get(), category); - List subscriptions = feedSubscriptionDAO.findByCategories(user.get(), children); - List statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subscriptions, true, null, null, 0, - 1, order, true, null, null, null); + List children = feedCategoryDAO.findAllChildrenCategories(user, category); + List subscriptions = feedSubscriptionDAO.findByCategories(user, children); + List statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, 0, 1, + order, true, null, null, null); s = Iterables.getFirst(statuses, null); } } if (s != null) { - feedEntryService.markEntry(user.get(), s.getEntry().getId(), true); + feedEntryService.markEntry(user, s.getEntry().getId(), true); } return s; }); - if (status == null) { - resp.sendRedirect(resp.encodeRedirectURL(config.getApplicationSettings().getPublicUrl())); - } else { - String url = status.getEntry().getUrl(); - resp.sendRedirect(resp.encodeRedirectURL(url)); - } + String url = status == null ? config.publicUrl() : status.getEntry().getUrl(); + return Response.temporaryRedirect(URI.create(url)).build(); } } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/RobotsTxtDisallowAllServlet.java b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/RobotsTxtDisallowAllServlet.java index b9873ffe..3e29cc6d 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/RobotsTxtDisallowAllServlet.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/RobotsTxtDisallowAllServlet.java @@ -1,22 +1,33 @@ package com.commafeed.frontend.servlet; -import java.io.IOException; +import org.apache.hc.core5.http.HttpStatus; +import com.commafeed.CommaFeedConfiguration; + +import jakarta.annotation.security.PermitAll; import jakarta.inject.Singleton; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import lombok.RequiredArgsConstructor; +@Path("/robots.txt") +@PermitAll +@Produces(MediaType.TEXT_PLAIN) +@RequiredArgsConstructor @Singleton -public class RobotsTxtDisallowAllServlet extends HttpServlet { +public class RobotsTxtDisallowAllServlet { - private static final long serialVersionUID = 1L; + private final CommaFeedConfiguration config; - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - resp.setContentType("text/plain"); - resp.getWriter().write("User-agent: *"); - resp.getWriter().write("\n"); - resp.getWriter().write("Disallow: /"); + @GET + public Response get() { + if (config.hideFromWebCrawlers()) { + return Response.ok("User-agent: *\nDisallow: /").build(); + } else { + return Response.status(HttpStatus.SC_NOT_FOUND).build(); + } } -} +} \ No newline at end of file diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHandlerFactory.java b/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHandlerFactory.java deleted file mode 100644 index a3388db4..00000000 --- a/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHandlerFactory.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.commafeed.frontend.session; - -import org.eclipse.jetty.server.session.DatabaseAdaptor; -import org.eclipse.jetty.server.session.DefaultSessionCache; -import org.eclipse.jetty.server.session.JDBCSessionDataStore; -import org.eclipse.jetty.server.session.SessionCache; -import org.eclipse.jetty.server.session.SessionHandler; - -import com.codahale.metrics.MetricRegistry; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.google.common.collect.ImmutableSet; - -import io.dropwizard.db.DataSourceFactory; -import io.dropwizard.util.Duration; -import jakarta.servlet.SessionTrackingMode; - -public class SessionHandlerFactory { - - @JsonProperty - private Duration cookieMaxAge = Duration.days(30); - - @JsonProperty - private Duration cookieRefreshAge = Duration.days(1); - - @JsonProperty - private Duration maxInactiveInterval = Duration.days(30); - - @JsonProperty - private Duration savePeriod = Duration.minutes(5); - - public SessionHandler build(DataSourceFactory dataSourceFactory) { - SessionHandler sessionHandler = new SessionHandler(); - sessionHandler.setHttpOnly(true); - sessionHandler.setSessionTrackingModes(ImmutableSet.of(SessionTrackingMode.COOKIE)); - sessionHandler.setMaxInactiveInterval((int) maxInactiveInterval.toSeconds()); - sessionHandler.setRefreshCookieAge((int) cookieRefreshAge.toSeconds()); - sessionHandler.getSessionCookieConfig().setMaxAge((int) cookieMaxAge.toSeconds()); - - SessionCache sessionCache = new DefaultSessionCache(sessionHandler); - sessionHandler.setSessionCache(sessionCache); - - JDBCSessionDataStore dataStore = new JDBCSessionDataStore(); - dataStore.setSavePeriodSec((int) savePeriod.toSeconds()); - sessionCache.setSessionDataStore(dataStore); - - DatabaseAdaptor adaptor = new DatabaseAdaptor(); - adaptor.setDatasource(dataSourceFactory.build(new MetricRegistry(), "sessions")); - dataStore.setDatabaseAdaptor(adaptor); - - return sessionHandler; - } - -} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHelper.java b/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHelper.java deleted file mode 100644 index 97341ea3..00000000 --- a/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHelper.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.commafeed.frontend.session; - -import java.util.Optional; - -import com.commafeed.backend.model.User; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpSession; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public class SessionHelper { - - public static final String SESSION_KEY_USER_ID = "user-id"; - - private final HttpServletRequest request; - - public Optional getLoggedInUserId() { - HttpSession session = request.getSession(false); - return getLoggedInUserId(session); - } - - public static Optional getLoggedInUserId(HttpSession session) { - if (session == null) { - return Optional.empty(); - } - Long userId = (Long) session.getAttribute(SESSION_KEY_USER_ID); - return Optional.ofNullable(userId); - } - - public void setLoggedInUser(User user) { - request.getSession(true).setAttribute(SESSION_KEY_USER_ID, user.getId()); - } - -} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHelperFactoryProvider.java b/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHelperFactoryProvider.java deleted file mode 100644 index 4ff25de6..00000000 --- a/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHelperFactoryProvider.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.commafeed.frontend.session; - -import java.util.function.Function; - -import org.glassfish.hk2.utilities.binding.AbstractBinder; -import org.glassfish.jersey.server.ContainerRequest; -import org.glassfish.jersey.server.internal.inject.AbstractValueParamProvider; -import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider; -import org.glassfish.jersey.server.model.Parameter; -import org.glassfish.jersey.server.spi.internal.ValueParamProvider; - -import jakarta.inject.Inject; -import jakarta.inject.Singleton; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.ws.rs.core.Context; - -@Singleton -public class SessionHelperFactoryProvider extends AbstractValueParamProvider { - - private final HttpServletRequest request; - - @Inject - public SessionHelperFactoryProvider(final MultivaluedParameterExtractorProvider extractorProvider, HttpServletRequest request) { - super(() -> extractorProvider, Parameter.Source.CONTEXT); - this.request = request; - } - - @Override - protected Function createValueProvider(Parameter parameter) { - final Class classType = parameter.getRawType(); - - Context context = parameter.getAnnotation(Context.class); - if (context == null) { - return null; - } - - if (!classType.isAssignableFrom(SessionHelper.class)) { - return null; - } - - return r -> new SessionHelper(request); - } - - public static class Binder extends AbstractBinder { - - @Override - protected void configure() { - bind(SessionHelperFactoryProvider.class).to(ValueParamProvider.class).in(Singleton.class); - } - } -} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketConfigurator.java b/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketConfigurator.java deleted file mode 100644 index 70d6a903..00000000 --- a/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketConfigurator.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.commafeed.frontend.ws; - -import java.util.Optional; - -import com.commafeed.frontend.session.SessionHelper; - -import jakarta.inject.Inject; -import jakarta.inject.Singleton; -import jakarta.servlet.http.HttpSession; -import jakarta.websocket.HandshakeResponse; -import jakarta.websocket.server.HandshakeRequest; -import jakarta.websocket.server.ServerEndpointConfig; -import jakarta.websocket.server.ServerEndpointConfig.Configurator; -import lombok.RequiredArgsConstructor; - -@Singleton -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) -public class WebSocketConfigurator extends Configurator { - - public static final String SESSIONKEY_USERID = "userId"; - - private final WebSocketSessions webSocketSessions; - - @Override - public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) { - HttpSession httpSession = (HttpSession) request.getHttpSession(); - if (httpSession != null) { - Optional userId = SessionHelper.getLoggedInUserId(httpSession); - userId.ifPresent(value -> config.getUserProperties().put(SESSIONKEY_USERID, value)); - } - } - - @SuppressWarnings("unchecked") - @Override - public T getEndpointInstance(Class endpointClass) { - return (T) new WebSocketEndpoint(webSocketSessions); - } -} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketEndpoint.java b/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketEndpoint.java index 4d145a75..93202ebf 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketEndpoint.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketEndpoint.java @@ -2,39 +2,54 @@ package com.commafeed.frontend.ws; import java.io.IOException; -import jakarta.inject.Inject; +import com.commafeed.CommaFeedConfiguration; +import com.commafeed.backend.model.User; +import com.commafeed.security.AuthenticationContext; + import jakarta.inject.Singleton; import jakarta.websocket.CloseReason; import jakarta.websocket.CloseReason.CloseCodes; -import jakarta.websocket.Endpoint; -import jakarta.websocket.EndpointConfig; +import jakarta.websocket.OnClose; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; import jakarta.websocket.Session; +import jakarta.websocket.server.ServerEndpoint; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) -public class WebSocketEndpoint extends Endpoint { +@ServerEndpoint("/ws") +@RequiredArgsConstructor +public class WebSocketEndpoint { + private final AuthenticationContext authenticationContext; + private final CommaFeedConfiguration config; private final WebSocketSessions sessions; - @Override - public void onOpen(Session session, EndpointConfig config) { - Long userId = (Long) config.getUserProperties().get(WebSocketConfigurator.SESSIONKEY_USERID); - if (userId == null) { + @OnOpen + public void onOpen(Session session) { + User user = authenticationContext.getCurrentUser(); + if (user == null) { reject(session); return; } - log.debug("created websocket session for user {}", userId); - sessions.add(userId, session); + log.debug("created websocket session for user '{}'", user.getName()); + sessions.add(user.getId(), session); + session.setMaxIdleTimeout(config.websocket().pingInterval().toMillis() + 10000); + } - session.addMessageHandler(String.class, message -> { - if ("ping".equals(message)) { - session.getAsyncRemote().sendText("pong"); - } - }); + @OnMessage + public void onMessage(String message, Session session) { + if ("ping".equals(message)) { + session.getAsyncRemote().sendText("pong"); + } + } + + @OnClose + public void onClose(Session session) { + sessions.remove(session); } private void reject(Session session) { @@ -45,9 +60,4 @@ public class WebSocketEndpoint extends Endpoint { } } - @Override - public void onClose(Session session, CloseReason reason) { - sessions.remove(session); - } - } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketSessions.java b/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketSessions.java index e82563e7..93caea5b 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketSessions.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketSessions.java @@ -8,7 +8,6 @@ import com.codahale.metrics.Gauge; import com.codahale.metrics.MetricRegistry; import com.commafeed.backend.model.User; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import jakarta.websocket.Session; import lombok.extern.slf4j.Slf4j; @@ -20,7 +19,6 @@ public class WebSocketSessions { // a user may have multiple sessions (two tabs, two devices, ...) private final Map> sessions = new ConcurrentHashMap<>(); - @Inject public WebSocketSessions(MetricRegistry metrics) { metrics.register(MetricRegistry.name(getClass(), "users"), (Gauge) () -> sessions.values().stream().filter(v -> !v.isEmpty()).count()); diff --git a/commafeed-server/src/main/java/com/commafeed/security/AuthenticationContext.java b/commafeed-server/src/main/java/com/commafeed/security/AuthenticationContext.java new file mode 100644 index 00000000..e0f5f794 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/security/AuthenticationContext.java @@ -0,0 +1,29 @@ +package com.commafeed.security; + +import com.commafeed.backend.dao.UserDAO; +import com.commafeed.backend.model.User; + +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.inject.Singleton; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Singleton +public class AuthenticationContext { + + private final SecurityIdentity securityIdentity; + private final UserDAO userDAO; + + public User getCurrentUser() { + if (securityIdentity.isAnonymous()) { + return null; + } + + String userId = securityIdentity.getPrincipal().getName(); + if (userId == null) { + return null; + } + + return userDAO.findById(Long.valueOf(userId)); + } +} diff --git a/commafeed-server/src/main/java/com/commafeed/security/Roles.java b/commafeed-server/src/main/java/com/commafeed/security/Roles.java new file mode 100644 index 00000000..f614ca94 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/security/Roles.java @@ -0,0 +1,6 @@ +package com.commafeed.security; + +public class Roles { + public static final String USER = "USER"; + public static final String ADMIN = "ADMIN"; +} diff --git a/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseApiKeyIdentityProvider.java b/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseApiKeyIdentityProvider.java new file mode 100644 index 00000000..5c1c95e7 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseApiKeyIdentityProvider.java @@ -0,0 +1,50 @@ +package com.commafeed.security.identity; + +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import com.commafeed.backend.dao.UnitOfWork; +import com.commafeed.backend.model.User; +import com.commafeed.backend.model.UserRole.Role; +import com.commafeed.backend.service.UserService; + +import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.TokenAuthenticationRequest; +import io.quarkus.security.runtime.QuarkusPrincipal; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.smallrye.mutiny.Uni; +import jakarta.inject.Singleton; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Singleton +public class DatabaseApiKeyIdentityProvider implements IdentityProvider { + + private final UnitOfWork unitOfWork; + private final UserService userService; + + @Override + public Class getRequestType() { + return TokenAuthenticationRequest.class; + } + + @Override + public Uni authenticate(TokenAuthenticationRequest request, AuthenticationRequestContext context) { + return context.runBlocking(() -> { + Optional user = unitOfWork.call(() -> userService.login(request.getToken().getToken())); + if (user.isEmpty()) { + throw new AuthenticationFailedException("could not find a user with this api key"); + } + + Set roles = unitOfWork.call(() -> userService.getRoles(user.get())); + return QuarkusSecurityIdentity.builder() + .setPrincipal(new QuarkusPrincipal(String.valueOf(user.get().getId()))) + .addRoles(roles.stream().map(Enum::name).collect(Collectors.toSet())) + .build(); + }); + } +} diff --git a/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseUsernamePasswordIdentityProvider.java b/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseUsernamePasswordIdentityProvider.java new file mode 100644 index 00000000..dc3537da --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseUsernamePasswordIdentityProvider.java @@ -0,0 +1,51 @@ +package com.commafeed.security.identity; + +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import com.commafeed.backend.dao.UnitOfWork; +import com.commafeed.backend.model.User; +import com.commafeed.backend.model.UserRole.Role; +import com.commafeed.backend.service.UserService; + +import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; +import io.quarkus.security.runtime.QuarkusPrincipal; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.smallrye.mutiny.Uni; +import jakarta.inject.Singleton; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Singleton +public class DatabaseUsernamePasswordIdentityProvider implements IdentityProvider { + + private final UnitOfWork unitOfWork; + private final UserService userService; + + @Override + public Class getRequestType() { + return UsernamePasswordAuthenticationRequest.class; + } + + @Override + public Uni authenticate(UsernamePasswordAuthenticationRequest request, AuthenticationRequestContext context) { + return context.runBlocking(() -> { + Optional user = unitOfWork + .call(() -> userService.login(request.getUsername(), new String(request.getPassword().getPassword()))); + if (user.isEmpty()) { + throw new AuthenticationFailedException("wrong username or password"); + } + + Set roles = unitOfWork.call(() -> userService.getRoles(user.get())); + return QuarkusSecurityIdentity.builder() + .setPrincipal(new QuarkusPrincipal(String.valueOf(user.get().getId()))) + .addRoles(roles.stream().map(Enum::name).collect(Collectors.toSet())) + .build(); + }); + } +} diff --git a/commafeed-server/src/main/java/com/commafeed/security/identity/TrustedIdentityProvider.java b/commafeed-server/src/main/java/com/commafeed/security/identity/TrustedIdentityProvider.java new file mode 100644 index 00000000..ecfffc57 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/security/identity/TrustedIdentityProvider.java @@ -0,0 +1,57 @@ +package com.commafeed.security.identity; + +import java.util.Set; +import java.util.stream.Collectors; + +import com.commafeed.backend.dao.UnitOfWork; +import com.commafeed.backend.dao.UserDAO; +import com.commafeed.backend.model.User; +import com.commafeed.backend.model.UserRole.Role; +import com.commafeed.backend.service.UserService; +import com.commafeed.backend.service.internal.PostLoginActivities; + +import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.TrustedAuthenticationRequest; +import io.quarkus.security.runtime.QuarkusPrincipal; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.smallrye.mutiny.Uni; +import jakarta.inject.Singleton; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Singleton +public class TrustedIdentityProvider implements IdentityProvider { + + private final UnitOfWork unitOfWork; + private final UserService userService; + private final UserDAO userDAO; + private final PostLoginActivities postLoginActivities; + + @Override + public Class getRequestType() { + return TrustedAuthenticationRequest.class; + } + + @Override + public Uni authenticate(TrustedAuthenticationRequest request, AuthenticationRequestContext context) { + return context.runBlocking(() -> { + Long userId = Long.valueOf(request.getPrincipal()); + User user = unitOfWork.call(() -> userDAO.findById(userId)); + if (user == null) { + throw new AuthenticationFailedException("user not found"); + } + + // execute post login activities manually because we didn't call login() since we received a trusted authentication request + unitOfWork.run(() -> postLoginActivities.executeFor(user)); + + Set roles = unitOfWork.call(() -> userService.getRoles(user)); + return QuarkusSecurityIdentity.builder() + .setPrincipal(new QuarkusPrincipal(String.valueOf(userId))) + .addRoles(roles.stream().map(Enum::name).collect(Collectors.toSet())) + .build(); + }); + } +} diff --git a/commafeed-server/src/main/java/com/commafeed/security/mechanism/ApiKeyAuthenticationMecanism.java b/commafeed-server/src/main/java/com/commafeed/security/mechanism/ApiKeyAuthenticationMecanism.java new file mode 100644 index 00000000..b611f827 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/security/mechanism/ApiKeyAuthenticationMecanism.java @@ -0,0 +1,46 @@ +package com.commafeed.security.mechanism; + +import java.util.Optional; +import java.util.Set; + +import io.quarkus.security.credential.TokenCredential; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.identity.request.TokenAuthenticationRequest; +import io.quarkus.vertx.http.runtime.security.ChallengeData; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; +import jakarta.inject.Singleton; + +@Singleton +public class ApiKeyAuthenticationMecanism implements HttpAuthenticationMechanism { + + @Override + public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { + // only authorize api key for GET requests + if (!context.request().method().name().equals("GET")) { + return Uni.createFrom().optional(Optional.empty()); + } + + String apiKey = context.request().getParam("apiKey"); + if (apiKey == null) { + return Uni.createFrom().optional(Optional.empty()); + } + + TokenCredential token = new TokenCredential(apiKey, "apiKey"); + TokenAuthenticationRequest request = new TokenAuthenticationRequest(token); + return identityProviderManager.authenticate(request); + } + + @Override + public Uni getChallenge(RoutingContext context) { + return Uni.createFrom().optional(Optional.empty()); + } + + @Override + public Set> getCredentialTypes() { + return Set.of(TokenAuthenticationRequest.class); + } +} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/auth/PasswordConstraintValidator.java b/commafeed-server/src/main/java/com/commafeed/security/password/PasswordConstraintValidator.java similarity index 94% rename from commafeed-server/src/main/java/com/commafeed/frontend/auth/PasswordConstraintValidator.java rename to commafeed-server/src/main/java/com/commafeed/security/password/PasswordConstraintValidator.java index 5063770f..4b842c77 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/auth/PasswordConstraintValidator.java +++ b/commafeed-server/src/main/java/com/commafeed/security/password/PasswordConstraintValidator.java @@ -1,4 +1,4 @@ -package com.commafeed.frontend.auth; +package com.commafeed.security.password; import java.util.List; diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/auth/ValidPassword.java b/commafeed-server/src/main/java/com/commafeed/security/password/ValidPassword.java similarity index 90% rename from commafeed-server/src/main/java/com/commafeed/frontend/auth/ValidPassword.java rename to commafeed-server/src/main/java/com/commafeed/security/password/ValidPassword.java index 63efdb4d..fc559c50 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/auth/ValidPassword.java +++ b/commafeed-server/src/main/java/com/commafeed/security/password/ValidPassword.java @@ -1,4 +1,4 @@ -package com.commafeed.frontend.auth; +package com.commafeed.security.password; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/commafeed-server/src/main/resources/META-INF/native-image/commafeed/resource-config.json b/commafeed-server/src/main/resources/META-INF/native-image/commafeed/resource-config.json new file mode 100644 index 00000000..a8a50b21 --- /dev/null +++ b/commafeed-server/src/main/resources/META-INF/native-image/commafeed/resource-config.json @@ -0,0 +1,10 @@ +{ + "resources": { + "includes": [ + { "pattern": "^default_banner\\.txt$" }, + { "pattern": "^images/default_favicon\\.gif$" }, + { "pattern": "^git\\.properties$" }, + { "pattern": "^rome\\.properties$" } + ] + } +} diff --git a/commafeed-server/src/main/resources/application.properties b/commafeed-server/src/main/resources/application.properties new file mode 100644 index 00000000..733fd569 --- /dev/null +++ b/commafeed-server/src/main/resources/application.properties @@ -0,0 +1,43 @@ +# http +quarkus.http.port=8082 +quarkus.http.test-port=8085 + +# security +quarkus.http.auth.basic=true +quarkus.http.auth.form.enabled=true +quarkus.http.auth.form.http-only-cookie=true +quarkus.http.auth.form.timeout=P30d +quarkus.http.auth.form.landing-page= +quarkus.http.auth.form.login-page= +quarkus.http.auth.form.error-page= + +# websocket +quarkus.websocket.dispatch-to-worker=true + +# database +quarkus.datasource.db-kind=h2 +quarkus.liquibase.change-log=migrations.xml +quarkus.liquibase.migrate-at-start=true + +# shutdown +quarkus.shutdown.timeout=5s + + +# dev profile overrides +%dev.quarkus.http.port=8083 +%dev.quarkus.http.auth.session.encryption-key=123456789012345678901234567890 +%dev.quarkus.log.category."com.commafeed".level=DEBUG +# %dev.quarkus.hibernate-orm.log.sql=true + + +# test profile overrides +%test.quarkus.log.category."org.mockserver".level=WARN +%test.quarkus.log.category."liquibase".level=WARN +%test.commafeed.users.create-demo-account=true +%test.commafeed.users.allow-registrations=true +%test.commafeed.smtp.host=localhost +%test.commafeed.smtp.port=3025 +%test.commafeed.smtp.tls=false +%test.commafeed.smtp.user-name=user +%test.commafeed.smtp.password=pass +%test.commafeed.smtp.from-address=noreply@commafeed.com diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-1.0.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-1.0.xml index 7b60e188..b1713439 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-1.0.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-1.0.xml @@ -1,7 +1,7 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> 7:6d3ad493d25dd9c50067e804efc9ffcc diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-1.1.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-1.1.xml index 065ead9f..094de039 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-1.1.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-1.1.xml @@ -1,7 +1,7 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-1.2.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-1.2.xml index 3bbe49c6..c735f521 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-1.2.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-1.2.xml @@ -1,6 +1,7 @@ - + @@ -14,7 +15,7 @@ + referencedTableName="FEEDS" referencedColumnNames="id" /> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-1.3.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-1.3.xml index 691dbde5..3e5f07d2 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-1.3.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-1.3.xml @@ -1,6 +1,7 @@ - + diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-1.4.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-1.4.xml index 4920edcf..578c0bc0 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-1.4.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-1.4.xml @@ -1,6 +1,7 @@ - + @@ -31,9 +32,9 @@ + referencedTableName="FEEDENTRIES" referencedColumnNames="id" /> + referencedTableName="USERS" referencedColumnNames="id" /> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-1.5.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-1.5.xml index 09ce1137..2a8f58f7 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-1.5.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-1.5.xml @@ -1,6 +1,7 @@ - + 8:58e8060bba0ec9d448f4346eb35d815c diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-2.1.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-2.1.xml index df158560..6125c98e 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-2.1.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-2.1.xml @@ -1,6 +1,7 @@ - + diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-2.2.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-2.2.xml index 1af324a6..6fb75000 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-2.2.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-2.2.xml @@ -1,6 +1,7 @@ - + diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-2.6.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-2.6.xml index 726e4228..29e7fd95 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-2.6.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-2.6.xml @@ -1,7 +1,7 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> @@ -26,7 +26,7 @@ 8:39e5a9ff312af90d82f87c88abf1c66d + columnDataType="VARCHAR(4096)" /> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-3.2.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-3.2.xml index 782777bc..9d337166 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-3.2.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-3.2.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-3.5.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-3.5.xml index 238b9b1b..87835d25 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-3.5.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-3.5.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-3.6.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-3.6.xml index eee22181..c3c1c916 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-3.6.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-3.6.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-3.8.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-3.8.xml index 2eaeaa95..52bd6b36 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-3.8.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-3.8.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-3.9.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-3.9.xml index 83ce42b0..f2c386a3 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-3.9.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-3.9.xml @@ -1,7 +1,7 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-4.0.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-4.0.xml index 6e898e72..3d3d6717 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-4.0.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-4.0.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-4.1.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-4.1.xml index 7540a1fe..9c00a813 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-4.1.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-4.1.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-4.2.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-4.2.xml index a52f67d6..0af84537 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-4.2.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-4.2.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> 9:bf66bf7def9ec3dab1f365f7230d92cf diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-4.3.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-4.3.xml index 806095f7..a9d696c1 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-4.3.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-4.3.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> 9:663bcc7c6df5b832ec2109a3afcff5c6 diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-4.4.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-4.4.xml index c74b779c..c15ed003 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-4.4.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-4.4.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> 9:078593b238a4639a97a3cd82f7e5e30d diff --git a/commafeed-server/src/main/resources/banner.txt b/commafeed-server/src/main/resources/default_banner.txt similarity index 100% rename from commafeed-server/src/main/resources/banner.txt rename to commafeed-server/src/main/resources/default_banner.txt diff --git a/commafeed-server/src/main/resources/migrations.xml b/commafeed-server/src/main/resources/migrations.xml index 2d951ed1..eae6c227 100644 --- a/commafeed-server/src/main/resources/migrations.xml +++ b/commafeed-server/src/main/resources/migrations.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> diff --git a/commafeed-server/src/test/java/com/commafeed/CommaFeedDropwizardAppExtension.java b/commafeed-server/src/test/java/com/commafeed/CommaFeedDropwizardAppExtension.java deleted file mode 100644 index 0cc7a0fb..00000000 --- a/commafeed-server/src/test/java/com/commafeed/CommaFeedDropwizardAppExtension.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.commafeed; - -import java.io.IOException; -import java.io.InputStream; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -import javax.sql.DataSource; - -import org.mockserver.socket.PortFactory; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.JdbcDatabaseContainer; -import org.testcontainers.containers.MariaDBContainer; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.containers.PostgreSQLContainer; -import org.testcontainers.utility.DockerImageName; - -import com.codahale.metrics.MetricRegistry; -import com.commafeed.CommaFeedConfiguration.CacheType; - -import io.dropwizard.testing.ConfigOverride; -import io.dropwizard.testing.ResourceHelpers; -import io.dropwizard.testing.junit5.DropwizardAppExtension; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisPool; - -public class CommaFeedDropwizardAppExtension extends DropwizardAppExtension { - private static final String TEST_DATABASE = System.getenv().getOrDefault("TEST_DATABASE", "h2"); - private static final boolean REDIS_ENABLED = Boolean.parseBoolean(System.getenv().getOrDefault("REDIS", "false")); - - private static final ConfigOverride[] CONFIG_OVERRIDES; - private static final List DROP_ALL_STATEMENTS; - static { - List overrides = new ArrayList<>(); - overrides.add(ConfigOverride.config("server.applicationConnectors[0].port", String.valueOf(PortFactory.findFreePort()))); - - Properties imageNames = readProperties("/docker-images.properties"); - - DatabaseConfiguration config = buildConfiguration(TEST_DATABASE, imageNames.getProperty(TEST_DATABASE)); - JdbcDatabaseContainer container = config.container(); - if (container != null) { - container.withDatabaseName("commafeed"); - container.withEnv("TZ", "UTC"); - container.start(); - - overrides.add(ConfigOverride.config("database.url", container.getJdbcUrl())); - overrides.add(ConfigOverride.config("database.user", container.getUsername())); - overrides.add(ConfigOverride.config("database.password", container.getPassword())); - overrides.add(ConfigOverride.config("database.driverClass", container.getDriverClassName())); - } - - if (REDIS_ENABLED) { - GenericContainer redis = new GenericContainer<>(DockerImageName.parse(imageNames.getProperty("redis"))) - .withExposedPorts(6379); - redis.start(); - - overrides.add(ConfigOverride.config("app.cache", "redis")); - overrides.add(ConfigOverride.config("redis.host", redis.getHost())); - overrides.add(ConfigOverride.config("redis.port", redis.getMappedPort(6379).toString())); - } - - CONFIG_OVERRIDES = overrides.toArray(new ConfigOverride[0]); - DROP_ALL_STATEMENTS = config.dropAllStatements(); - } - - public CommaFeedDropwizardAppExtension() { - super(CommaFeedApplication.class, ResourceHelpers.resourceFilePath("config.test.yml"), CONFIG_OVERRIDES); - } - - private static DatabaseConfiguration buildConfiguration(String databaseName, String imageName) { - if ("postgresql".equals(databaseName)) { - JdbcDatabaseContainer container = new PostgreSQLContainer<>(imageName).withTmpFs(Map.of("/var/lib/postgresql/data", "rw")); - return new DatabaseConfiguration(container, List.of("DROP SCHEMA public CASCADE", "CREATE SCHEMA public")); - } else if ("mysql".equals(databaseName)) { - JdbcDatabaseContainer container = new MySQLContainer<>(imageName).withTmpFs(Map.of("/var/lib/mysql", "rw")); - return new DatabaseConfiguration(container, List.of("DROP DATABASE IF EXISTS commafeed", " CREATE DATABASE commafeed")); - } else if ("mariadb".equals(databaseName)) { - JdbcDatabaseContainer container = new MariaDBContainer<>(imageName).withTmpFs(Map.of("/var/lib/mysql", "rw")); - return new DatabaseConfiguration(container, List.of("DROP DATABASE IF EXISTS commafeed", " CREATE DATABASE commafeed")); - } else { - // h2 - return new DatabaseConfiguration(null, List.of("DROP ALL OBJECTS")); - } - } - - private static Properties readProperties(String path) { - Properties properties = new Properties(); - try (InputStream is = CommaFeedDropwizardAppExtension.class.getResourceAsStream(path)) { - properties.load(is); - } catch (IOException e) { - throw new RuntimeException("could not read resource " + path, e); - } - return properties; - } - - @Override - public void after() { - super.after(); - - // clean database after each test - DataSource dataSource = getConfiguration().getDataSourceFactory().build(new MetricRegistry(), "cleanup"); - try (Connection connection = dataSource.getConnection()) { - for (String statement : DROP_ALL_STATEMENTS) { - connection.prepareStatement(statement).executeUpdate(); - } - } catch (SQLException e) { - throw new RuntimeException("could not cleanup database", e); - } - - // clean redis cache after each test - if (getConfiguration().getApplicationSettings().getCache() == CacheType.REDIS) { - try (JedisPool pool = getConfiguration().getRedisPoolFactory().build(); Jedis jedis = pool.getResource()) { - jedis.flushAll(); - } - } - } - - private record DatabaseConfiguration(JdbcDatabaseContainer container, List dropAllStatements) { - } - -} diff --git a/commafeed-server/src/test/java/com/commafeed/DatabaseReset.java b/commafeed-server/src/test/java/com/commafeed/DatabaseReset.java new file mode 100644 index 00000000..97e43ddd --- /dev/null +++ b/commafeed-server/src/test/java/com/commafeed/DatabaseReset.java @@ -0,0 +1,34 @@ +package com.commafeed; + +import org.kohsuke.MetaInfServices; + +import com.commafeed.backend.service.db.DatabaseStartupService; + +import io.quarkus.liquibase.LiquibaseFactory; +import io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback; +import io.quarkus.test.junit.callback.QuarkusTestMethodContext; +import jakarta.enterprise.inject.spi.CDI; +import liquibase.Liquibase; +import liquibase.exception.LiquibaseException; + +/** + * Resets database between tests + */ +@MetaInfServices +public class DatabaseReset implements QuarkusTestBeforeEachCallback { + + @SuppressWarnings("deprecation") + @Override + public void beforeEach(QuarkusTestMethodContext context) { + LiquibaseFactory liquibaseFactory = CDI.current().select(LiquibaseFactory.class).get(); + try (Liquibase liquibase = liquibaseFactory.createLiquibase()) { + liquibase.dropAll(); + liquibase.update(liquibaseFactory.createContexts(), liquibaseFactory.createLabels()); + } catch (LiquibaseException e) { + throw new RuntimeException(e); + } + + DatabaseStartupService databaseStartupService = CDI.current().select(DatabaseStartupService.class).get(); + databaseStartupService.populateInitialData(); + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java b/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java index 06da263d..e0289599 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java @@ -1,14 +1,16 @@ package com.commafeed.backend; import java.io.IOException; +import java.math.BigInteger; import java.net.SocketTimeoutException; import java.util.Arrays; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.io.IOUtils; import org.apache.hc.client5.http.ConnectTimeoutException; -import org.eclipse.jetty.http.HttpStatus; +import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,13 +28,13 @@ import org.mockserver.model.MediaType; import com.codahale.metrics.MetricRegistry; import com.commafeed.CommaFeedConfiguration; -import com.commafeed.CommaFeedConfiguration.ApplicationSettings; +import com.commafeed.CommaFeedVersion; import com.commafeed.backend.HttpGetter.HttpResponseException; import com.commafeed.backend.HttpGetter.HttpResult; import com.commafeed.backend.HttpGetter.NotModifiedException; import com.google.common.net.HttpHeaders; -import io.dropwizard.util.DataSize; +import io.quarkus.runtime.configuration.MemorySize; @ExtendWith(MockServerExtension.class) class HttpGetterTest { @@ -51,21 +53,17 @@ class HttpGetterTest { this.feedUrl = "http://localhost:" + this.mockServerClient.getPort() + "/"; this.feedContent = IOUtils.toByteArray(Objects.requireNonNull(getClass().getResource("/feed/rss.xml"))); - ApplicationSettings settings = new ApplicationSettings(); - settings.setUserAgent("http-getter-test"); - settings.setBackgroundThreads(3); - settings.setMaxFeedResponseSize(DataSize.kilobytes(10)); + CommaFeedConfiguration config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(config.feedRefresh().userAgent()).thenReturn(Optional.of("http-getter-test")); + Mockito.when(config.feedRefresh().httpThreads()).thenReturn(3); + Mockito.when(config.feedRefresh().maxResponseSize()).thenReturn(new MemorySize(new BigInteger("10000"))); - CommaFeedConfiguration config = new CommaFeedConfiguration(); - config.setApplicationSettings(settings); - - this.getter = new HttpGetter(config, Mockito.mock(MetricRegistry.class)); + this.getter = new HttpGetter(config, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class)); } @ParameterizedTest @ValueSource( - ints = { HttpStatus.UNAUTHORIZED_401, HttpStatus.FORBIDDEN_403, HttpStatus.NOT_FOUND_404, - HttpStatus.INTERNAL_SERVER_ERROR_500 }) + ints = { HttpStatus.SC_UNAUTHORIZED, HttpStatus.SC_FORBIDDEN, HttpStatus.SC_NOT_FOUND, HttpStatus.SC_INTERNAL_SERVER_ERROR }) void errorCodes(int code) { this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(HttpResponse.response().withStatusCode(code)); @@ -93,8 +91,8 @@ class HttpGetterTest { @ParameterizedTest @ValueSource( - ints = { HttpStatus.MOVED_PERMANENTLY_301, HttpStatus.MOVED_TEMPORARILY_302, HttpStatus.TEMPORARY_REDIRECT_307, - HttpStatus.PERMANENT_REDIRECT_308 }) + ints = { HttpStatus.SC_MOVED_PERMANENTLY, HttpStatus.SC_MOVED_TEMPORARILY, HttpStatus.SC_TEMPORARY_REDIRECT, + HttpStatus.SC_PERMANENT_REDIRECT }) void followRedirects(int code) throws Exception { // first redirect this.mockServerClient.when(HttpRequest.request().withMethod("GET").withPath("/")) @@ -129,7 +127,7 @@ class HttpGetterTest { void connectTimeout() { // try to connect to a non-routable address // https://stackoverflow.com/a/904609 - Assertions.assertThrows(ConnectTimeoutException.class, () -> getter.getBinary("http://10.255.255.1", 2000)); + Assertions.assertThrows(ConnectTimeoutException.class, () -> getter.getBinary("http://10.255.255.1", 500)); } @Test @@ -144,7 +142,7 @@ class HttpGetterTest { @Test void lastModifiedReturns304() { this.mockServerClient.when(HttpRequest.request().withMethod("GET").withHeader(HttpHeaders.IF_MODIFIED_SINCE, "123456")) - .respond(HttpResponse.response().withStatusCode(HttpStatus.NOT_MODIFIED_304)); + .respond(HttpResponse.response().withStatusCode(HttpStatus.SC_NOT_MODIFIED)); Assertions.assertThrows(NotModifiedException.class, () -> getter.getBinary(this.feedUrl, "123456", null, TIMEOUT)); } @@ -152,7 +150,7 @@ class HttpGetterTest { @Test void eTagReturns304() { this.mockServerClient.when(HttpRequest.request().withMethod("GET").withHeader(HttpHeaders.IF_NONE_MATCH, "78910")) - .respond(HttpResponse.response().withStatusCode(HttpStatus.NOT_MODIFIED_304)); + .respond(HttpResponse.response().withStatusCode(HttpStatus.SC_NOT_MODIFIED)); Assertions.assertThrows(NotModifiedException.class, () -> getter.getBinary(this.feedUrl, null, "78910", TIMEOUT)); } @@ -195,7 +193,7 @@ class HttpGetterTest { @Test void largeFeedWithContentLengthHeader() { - byte[] bytes = new byte[(int) DataSize.kilobytes(100).toBytes()]; + byte[] bytes = new byte[100000]; Arrays.fill(bytes, (byte) 1); this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(HttpResponse.response().withBody(bytes)); @@ -205,7 +203,7 @@ class HttpGetterTest { @Test void largeFeedWithoutContentLengthHeader() { - byte[] bytes = new byte[(int) DataSize.kilobytes(100).toBytes()]; + byte[] bytes = new byte[100000]; Arrays.fill(bytes, (byte) 1); this.mockServerClient.when(HttpRequest.request().withMethod("GET")) .respond(HttpResponse.response() diff --git a/commafeed-server/src/test/java/com/commafeed/backend/feed/FeedFetcherTest.java b/commafeed-server/src/test/java/com/commafeed/backend/feed/FeedFetcherTest.java index b47ed808..c231cdd9 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/feed/FeedFetcherTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/feed/FeedFetcherTest.java @@ -1,7 +1,7 @@ package com.commafeed.backend.feed; import java.time.Instant; -import java.util.Set; +import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -28,7 +28,7 @@ class FeedFetcherTest { private HttpGetter getter; @Mock - private Set urlProviders; + private List urlProviders; private FeedFetcher fetcher; diff --git a/commafeed-server/src/test/java/com/commafeed/backend/service/db/H2MigrationServiceTest.java b/commafeed-server/src/test/java/com/commafeed/backend/service/db/H2MigrationServiceTest.java deleted file mode 100644 index 578cbc3e..00000000 --- a/commafeed-server/src/test/java/com/commafeed/backend/service/db/H2MigrationServiceTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.commafeed.backend.service.db; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Objects; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -class H2MigrationServiceTest { - - @TempDir - private Path root; - - @Test - void testMigrateIfNeeded() throws IOException { - Path path = root.resolve("database.mv.db"); - Files.copy(Objects.requireNonNull(getClass().getResourceAsStream("/h2-migration/database-v2.1.214.mv.db")), path); - - H2MigrationService service = new H2MigrationService(); - Assertions.assertEquals(2, service.getH2FileFormat(path)); - - service.migrateIfNeeded(path, "sa", "sa"); - Assertions.assertEquals(3, service.getH2FileFormat(path)); - } - -} \ No newline at end of file diff --git a/commafeed-server/src/test/java/com/commafeed/e2e/AuthentificationIT.java b/commafeed-server/src/test/java/com/commafeed/e2e/AuthentificationIT.java index 56f12089..2fcfbbf6 100644 --- a/commafeed-server/src/test/java/com/commafeed/e2e/AuthentificationIT.java +++ b/commafeed-server/src/test/java/com/commafeed/e2e/AuthentificationIT.java @@ -1,20 +1,35 @@ package com.commafeed.e2e; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import com.commafeed.CommaFeedDropwizardAppExtension; +import com.microsoft.playwright.Browser; import com.microsoft.playwright.Locator; import com.microsoft.playwright.Page; +import com.microsoft.playwright.Playwright; import com.microsoft.playwright.assertions.PlaywrightAssertions; import com.microsoft.playwright.options.AriaRole; -import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.quarkus.test.junit.QuarkusTest; -@ExtendWith(DropwizardExtensionsSupport.class) -class AuthentificationIT extends PlaywrightTestBase { +@QuarkusTest +class AuthentificationIT { - private static final CommaFeedDropwizardAppExtension EXT = new CommaFeedDropwizardAppExtension(); + private final Playwright playwright = Playwright.create(); + private final Browser browser = playwright.chromium().launch(); + + private Page page; + + @BeforeEach + void init() { + page = browser.newContext().newPage(); + } + + @AfterEach + void cleanup() { + playwright.close(); + } @Test void loginFail() { @@ -29,7 +44,7 @@ class AuthentificationIT extends PlaywrightTestBase { void loginSuccess() { page.navigate(getLoginPageUrl()); PlaywrightTestUtils.login(page); - PlaywrightAssertions.assertThat(page).hasURL("http://localhost:" + EXT.getLocalPort() + "/#/app/category/all"); + PlaywrightAssertions.assertThat(page).hasURL("http://localhost:8085/#/app/category/all"); } @Test @@ -56,10 +71,10 @@ class AuthentificationIT extends PlaywrightTestBase { page.getByPlaceholder("E-mail address").fill("user@domain.com"); page.getByPlaceholder("Password").fill("MyPassword1!"); page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign up")).click(); - PlaywrightAssertions.assertThat(page).hasURL("http://localhost:" + EXT.getLocalPort() + "/#/app/category/all"); + PlaywrightAssertions.assertThat(page).hasURL("http://localhost:8085/#/app/category/all"); } private String getLoginPageUrl() { - return "http://localhost:" + EXT.getLocalPort() + "/#/login"; + return "http://localhost:8085/#/login"; } } diff --git a/commafeed-server/src/test/java/com/commafeed/e2e/PlaywrightTestBase.java b/commafeed-server/src/test/java/com/commafeed/e2e/PlaywrightTestBase.java deleted file mode 100644 index 81735c3b..00000000 --- a/commafeed-server/src/test/java/com/commafeed/e2e/PlaywrightTestBase.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.commafeed.e2e; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Optional; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.extension.BeforeEachCallback; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.TestWatcher; - -import com.microsoft.playwright.Browser; -import com.microsoft.playwright.Browser.NewContextOptions; -import com.microsoft.playwright.BrowserContext; -import com.microsoft.playwright.Page; -import com.microsoft.playwright.Playwright; -import com.microsoft.playwright.Tracing; - -/** - * Base class for all Playwright tests. - * - *
    - *
  • Takes a screenshot on failure
  • - *
  • Keeps the video on failure
  • - *
  • Saves a trace file on failure
  • - *
- * - * inspired by https://github.com/microsoft/playwright-java/issues/503#issuecomment-872636373 - * - */ -@ExtendWith(PlaywrightTestBase.SaveArtifactsOnTestFailed.class) -public abstract class PlaywrightTestBase { - - private static Playwright playwright; - private static Browser browser; - - protected Page page; - private BrowserContext context; - - @BeforeAll - static void initBrowser() { - playwright = Playwright.create(); - browser = playwright.chromium().launch(); - } - - @AfterAll - static void closeBrowser() { - playwright.close(); - } - - protected void customizeNewContextOptions(NewContextOptions options) { - // override in subclasses to customize the browser context - } - - protected static class SaveArtifactsOnTestFailed implements TestWatcher, BeforeEachCallback { - - // defined in the config of maven-failsafe-plugin in pom.xml - private final String buildDirectory = System.getProperty("buildDirectory", "target"); - private final String directory = buildDirectory + "/playwright-artifacts"; - - @Override - public void beforeEach(ExtensionContext context) { - PlaywrightTestBase testInstance = getTestInstance(context); - - NewContextOptions newContextOptions = new Browser.NewContextOptions().setRecordVideoDir(Paths.get(directory)); - testInstance.customizeNewContextOptions(newContextOptions); - testInstance.context = PlaywrightTestBase.browser.newContext(newContextOptions); - testInstance.context.tracing().start(new Tracing.StartOptions().setScreenshots(true).setSnapshots(true)); - - testInstance.page = testInstance.context.newPage(); - } - - @Override - public void testFailed(ExtensionContext context, Throwable cause) { - PlaywrightTestBase testInstance = getTestInstance(context); - - String fileName = getFileName(context); - - saveScreenshot(testInstance, fileName); - saveTrace(testInstance, fileName); - - testInstance.context.close(); - - saveVideo(testInstance, fileName); - } - - @Override - public void testAborted(ExtensionContext context, Throwable cause) { - PlaywrightTestBase testInstance = getTestInstance(context); - testInstance.context.close(); - testInstance.page.video().delete(); - } - - @Override - public void testDisabled(ExtensionContext context, Optional reason) { - PlaywrightTestBase testInstance = getTestInstance(context); - testInstance.context.close(); - testInstance.page.video().delete(); - } - - @Override - public void testSuccessful(ExtensionContext context) { - PlaywrightTestBase testInstance = getTestInstance(context); - testInstance.context.close(); - testInstance.page.video().delete(); - } - - private PlaywrightTestBase getTestInstance(ExtensionContext context) { - return (PlaywrightTestBase) context.getRequiredTestInstance(); - } - - private String getFileName(ExtensionContext context) { - return String.format("%s.%s-%s", context.getRequiredTestClass().getSimpleName(), context.getRequiredTestMethod().getName(), - DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm-ss").format(ZonedDateTime.now())); - } - - private void saveScreenshot(PlaywrightTestBase testInstance, String fileName) { - byte[] screenshot = testInstance.page.screenshot(); - try { - Files.write(Paths.get(directory, fileName + ".png"), screenshot); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private void saveTrace(PlaywrightTestBase testInstance, String fileName) { - testInstance.context.tracing().stop(new Tracing.StopOptions().setPath(Paths.get(directory, fileName + ".zip"))); - } - - private void saveVideo(PlaywrightTestBase testInstance, String fileName) { - testInstance.page.video().saveAs(Paths.get(directory, fileName + ".webm")); - testInstance.page.video().delete(); - } - } -} diff --git a/commafeed-server/src/test/java/com/commafeed/e2e/ReadingIT.java b/commafeed-server/src/test/java/com/commafeed/e2e/ReadingIT.java index 2264701f..fa99d241 100644 --- a/commafeed-server/src/test/java/com/commafeed/e2e/ReadingIT.java +++ b/commafeed-server/src/test/java/com/commafeed/e2e/ReadingIT.java @@ -6,43 +6,51 @@ import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.mockserver.client.MockServerClient; -import org.mockserver.junit.jupiter.MockServerExtension; +import org.mockserver.integration.ClientAndServer; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; -import com.commafeed.CommaFeedDropwizardAppExtension; +import com.microsoft.playwright.Browser; import com.microsoft.playwright.Locator; import com.microsoft.playwright.Page; +import com.microsoft.playwright.Playwright; import com.microsoft.playwright.assertions.PlaywrightAssertions; import com.microsoft.playwright.options.AriaRole; -import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.quarkus.test.junit.QuarkusTest; -@ExtendWith(DropwizardExtensionsSupport.class) -@ExtendWith(MockServerExtension.class) -class ReadingIT extends PlaywrightTestBase { +@QuarkusTest +class ReadingIT { - private static final CommaFeedDropwizardAppExtension EXT = new CommaFeedDropwizardAppExtension(); + private final Playwright playwright = Playwright.create(); + private final Browser browser = playwright.chromium().launch(); + private Page page; private MockServerClient mockServerClient; @BeforeEach - void init(MockServerClient mockServerClient) throws IOException { - this.mockServerClient = mockServerClient; + void init() throws IOException { + this.page = browser.newContext().newPage(); + this.mockServerClient = ClientAndServer.startClientAndServer(0); this.mockServerClient.when(HttpRequest.request().withMethod("GET")) .respond(HttpResponse.response() .withBody(IOUtils.toString(getClass().getResource("/feed/rss.xml"), StandardCharsets.UTF_8)) .withDelay(TimeUnit.MILLISECONDS, 100)); } + @AfterEach + void cleanup() { + playwright.close(); + } + @Test void scenario() { // login - page.navigate("http://localhost:" + EXT.getLocalPort()); + page.navigate("http://localhost:8085"); page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Log in")).click(); PlaywrightTestUtils.login(page); diff --git a/commafeed-server/src/test/java/com/commafeed/frontend/auth/SecurityCheckFactoryTest.java b/commafeed-server/src/test/java/com/commafeed/frontend/auth/SecurityCheckFactoryTest.java deleted file mode 100644 index 1069ef8f..00000000 --- a/commafeed-server/src/test/java/com/commafeed/frontend/auth/SecurityCheckFactoryTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.commafeed.frontend.auth; - -import java.util.Optional; - -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import com.commafeed.backend.dao.UserDAO; -import com.commafeed.backend.model.User; -import com.commafeed.backend.service.UserService; -import com.commafeed.backend.service.internal.PostLoginActivities; -import com.commafeed.frontend.session.SessionHelper; - -class SecurityCheckFactoryTest { - - @Test - void cookieLoginShouldPerformPostLoginActivities() { - User userInSession = new User(); - UserDAO userDAO = Mockito.mock(UserDAO.class); - Mockito.when(userDAO.findById(1L)).thenReturn(userInSession); - - SessionHelper sessionHelper = Mockito.mock(SessionHelper.class); - Mockito.when(sessionHelper.getLoggedInUserId()).thenReturn(Optional.of(1L)); - - PostLoginActivities postLoginActivities = Mockito.mock(PostLoginActivities.class); - - UserService service = new UserService(null, null, null, null, null, null, null, postLoginActivities); - - SecurityCheckFactory factory = new SecurityCheckFactory(userDAO, service, null, null, null, false); - factory.cookieSessionLogin(sessionHelper); - - Mockito.verify(postLoginActivities).executeFor(userInSession); - } - -} diff --git a/commafeed-server/src/test/java/com/commafeed/frontend/resource/UserRestTest.java b/commafeed-server/src/test/java/com/commafeed/frontend/resource/UserRestTest.java deleted file mode 100644 index a1ccdb4d..00000000 --- a/commafeed-server/src/test/java/com/commafeed/frontend/resource/UserRestTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.commafeed.frontend.resource; - -import java.util.Collections; -import java.util.Optional; - -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatchers; -import org.mockito.InOrder; -import org.mockito.Mockito; - -import com.commafeed.backend.model.User; -import com.commafeed.backend.model.UserRole.Role; -import com.commafeed.backend.service.UserService; -import com.commafeed.frontend.model.request.LoginRequest; -import com.commafeed.frontend.model.request.RegistrationRequest; -import com.commafeed.frontend.session.SessionHelper; - -class UserRestTest { - - @Test - void loginShouldNotPopulateHttpSessionIfUnsuccessfull() { - // Absent user - Optional absentUser = Optional.empty(); - - // Create UserService partial mock - UserService service = Mockito.mock(UserService.class); - Mockito.when(service.login("user", "password")).thenReturn(absentUser); - - UserREST userREST = new UserREST(null, null, null, service, null, null, null); - SessionHelper sessionHelper = Mockito.mock(SessionHelper.class); - - LoginRequest req = new LoginRequest(); - req.setName("user"); - req.setPassword("password"); - - userREST.login(req, sessionHelper); - - Mockito.verify(sessionHelper, Mockito.never()).setLoggedInUser(Mockito.any(User.class)); - } - - @Test - void loginShouldPopulateHttpSessionIfSuccessfull() { - // Create a user - User user = new User(); - - // Create UserService mock - UserService service = Mockito.mock(UserService.class); - Mockito.when(service.login("user", "password")).thenReturn(Optional.of(user)); - - LoginRequest req = new LoginRequest(); - req.setName("user"); - req.setPassword("password"); - - UserREST userREST = new UserREST(null, null, null, service, null, null, null); - SessionHelper sessionHelper = Mockito.mock(SessionHelper.class); - - userREST.login(req, sessionHelper); - - Mockito.verify(sessionHelper).setLoggedInUser(user); - } - - @Test - void registerShouldRegisterAndThenLogin() { - // Create UserService mock - UserService service = Mockito.mock(UserService.class); - - RegistrationRequest req = new RegistrationRequest(); - req.setName("user"); - req.setPassword("password"); - req.setEmail("test@test.com"); - - InOrder inOrder = Mockito.inOrder(service); - - SessionHelper sessionHelper = Mockito.mock(SessionHelper.class); - UserREST userREST = new UserREST(null, null, null, service, null, null, null); - - userREST.registerUser(req, sessionHelper); - - inOrder.verify(service).register("user", "password", "test@test.com", Collections.singletonList(Role.USER)); - inOrder.verify(service).login("user", "password"); - } - - @Test - void registerShouldPopulateHttpSession() { - // Create a user - User user = new User(); - - // Create UserService mock - UserService service = Mockito.mock(UserService.class); - Mockito.when(service.register(Mockito.any(String.class), Mockito.any(String.class), Mockito.any(String.class), - ArgumentMatchers.anyList())).thenReturn(user); - Mockito.when(service.login(Mockito.any(String.class), Mockito.any(String.class))).thenReturn(Optional.of(user)); - - RegistrationRequest req = new RegistrationRequest(); - req.setName("user"); - req.setPassword("password"); - req.setEmail("test@test.com"); - - SessionHelper sessionHelper = Mockito.mock(SessionHelper.class); - UserREST userREST = new UserREST(null, null, null, service, null, null, null); - - userREST.registerUser(req, sessionHelper); - - Mockito.verify(sessionHelper).setLoggedInUser(user); - } - -} diff --git a/commafeed-server/src/test/java/com/commafeed/frontend/session/SessionHelperTest.java b/commafeed-server/src/test/java/com/commafeed/frontend/session/SessionHelperTest.java deleted file mode 100644 index 6f535879..00000000 --- a/commafeed-server/src/test/java/com/commafeed/frontend/session/SessionHelperTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.commafeed.frontend.session; - -import java.util.Optional; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpSession; - -class SessionHelperTest { - - @Test - void gettingUserDoesNotCreateSession() { - HttpServletRequest request = Mockito.mock(HttpServletRequest.class); - - SessionHelper sessionHelper = new SessionHelper(request); - sessionHelper.getLoggedInUserId(); - - Mockito.verify(request).getSession(false); - } - - @Test - void gettingUserShouldNotReturnUserIfThereIsNoPreexistingHttpSession() { - HttpServletRequest request = Mockito.mock(HttpServletRequest.class); - Mockito.when(request.getSession(false)).thenReturn(null); - - SessionHelper sessionHelper = new SessionHelper(request); - Optional userId = sessionHelper.getLoggedInUserId(); - - Assertions.assertFalse(userId.isPresent()); - } - - @Test - void gettingUserShouldNotReturnUserIfUserNotPresentInHttpSession() { - HttpSession session = Mockito.mock(HttpSession.class); - Mockito.when(session.getAttribute(SessionHelper.SESSION_KEY_USER_ID)).thenReturn(null); - - HttpServletRequest request = Mockito.mock(HttpServletRequest.class); - Mockito.when(request.getSession(false)).thenReturn(session); - - SessionHelper sessionHelper = new SessionHelper(request); - Optional userId = sessionHelper.getLoggedInUserId(); - - Assertions.assertFalse(userId.isPresent()); - } - - @Test - void gettingUserShouldReturnUserIfUserPresentInHttpSession() { - HttpSession session = Mockito.mock(HttpSession.class); - Mockito.when(session.getAttribute(SessionHelper.SESSION_KEY_USER_ID)).thenReturn(1L); - - HttpServletRequest request = Mockito.mock(HttpServletRequest.class); - Mockito.when(request.getSession(false)).thenReturn(session); - - SessionHelper sessionHelper = new SessionHelper(request); - Optional userId = sessionHelper.getLoggedInUserId(); - - Assertions.assertTrue(userId.isPresent()); - } - -} diff --git a/commafeed-server/src/test/java/com/commafeed/integration/BaseIT.java b/commafeed-server/src/test/java/com/commafeed/integration/BaseIT.java index a6f3ad68..0d2f40a0 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/BaseIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/BaseIT.java @@ -1,84 +1,82 @@ package com.commafeed.integration; import java.io.IOException; +import java.net.HttpCookie; import java.net.URL; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.List; import java.util.Objects; import org.apache.commons.io.IOUtils; +import org.apache.hc.core5.http.HttpStatus; import org.awaitility.Awaitility; -import org.eclipse.jetty.http.HttpStatus; import org.glassfish.jersey.client.JerseyClientBuilder; -import org.glassfish.jersey.media.multipart.MultiPartFeature; +import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJsonProvider; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.extension.ExtendWith; import org.mockserver.client.MockServerClient; -import org.mockserver.junit.jupiter.MockServerExtension; +import org.mockserver.integration.ClientAndServer; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; -import com.commafeed.CommaFeedDropwizardAppExtension; +import com.commafeed.JacksonCustomizer; import com.commafeed.frontend.model.Entries; import com.commafeed.frontend.model.Subscription; -import com.commafeed.frontend.model.request.LoginRequest; import com.commafeed.frontend.model.request.SubscribeRequest; +import com.fasterxml.jackson.databind.ObjectMapper; -import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.restassured.RestAssured; +import io.restassured.http.Header; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; import lombok.Getter; @Getter -@ExtendWith(DropwizardExtensionsSupport.class) -@ExtendWith(MockServerExtension.class) public abstract class BaseIT { private static final HttpRequest FEED_REQUEST = HttpRequest.request().withMethod("GET").withPath("/"); - private final CommaFeedDropwizardAppExtension extension = new CommaFeedDropwizardAppExtension() { - @Override - protected JerseyClientBuilder clientBuilder() { - return configureClientBuilder(super.clientBuilder().register(MultiPartFeature.class)); - } - }; - - private Client client; - - private String feedUrl; - - private String baseUrl; - - private String apiBaseUrl; - - private String webSocketUrl; - private MockServerClient mockServerClient; + private Client client; + private String feedUrl; + private String baseUrl; + private String apiBaseUrl; + private String webSocketUrl; protected JerseyClientBuilder configureClientBuilder(JerseyClientBuilder base) { return base; } @BeforeEach - void init(MockServerClient mockServerClient) throws IOException { - this.mockServerClient = mockServerClient; + void init() throws IOException { + this.mockServerClient = ClientAndServer.startClientAndServer(0); + + ObjectMapper mapper = new ObjectMapper(); + new JacksonCustomizer().customize(mapper); + this.client = configureClientBuilder(new JerseyClientBuilder().register(new JacksonJsonProvider(mapper))).build(); + this.feedUrl = "http://localhost:" + mockServerClient.getPort() + "/"; + this.baseUrl = "http://localhost:8085/"; + this.apiBaseUrl = this.baseUrl + "rest/"; + this.webSocketUrl = "ws://localhost:8085/ws"; URL resource = Objects.requireNonNull(getClass().getResource("/feed/rss.xml")); - mockServerClient.when(FEED_REQUEST).respond(HttpResponse.response().withBody(IOUtils.toString(resource, StandardCharsets.UTF_8))); - - this.client = extension.client(); - this.feedUrl = "http://localhost:" + mockServerClient.getPort() + "/"; - this.baseUrl = "http://localhost:" + extension.getLocalPort() + "/"; - this.apiBaseUrl = this.baseUrl + "rest/"; - this.webSocketUrl = "ws://localhost:" + extension.getLocalPort() + "/ws"; + this.mockServerClient.when(FEED_REQUEST) + .respond(HttpResponse.response().withBody(IOUtils.toString(resource, StandardCharsets.UTF_8))); } @AfterEach void cleanup() { - this.client.close(); + if (this.mockServerClient != null) { + this.mockServerClient.close(); + } + + if (this.client != null) { + this.client.close(); + } } protected void feedNowReturnsMoreEntries() throws IOException { @@ -88,14 +86,20 @@ public abstract class BaseIT { mockServerClient.when(FEED_REQUEST).respond(HttpResponse.response().withBody(IOUtils.toString(resource, StandardCharsets.UTF_8))); } - protected String login() { - LoginRequest req = new LoginRequest(); - req.setName("admin"); - req.setPassword("admin"); - try (Response response = client.target(apiBaseUrl + "user/login").request().post(Entity.json(req))) { - Assertions.assertEquals(HttpStatus.OK_200, response.getStatus()); - return response.getCookies().get("JSESSIONID").getValue(); - } + protected List login() { + Form form = new Form(); + form.param("j_username", "admin"); + form.param("j_password", "admin"); + + List
setCookieHeaders = RestAssured.given() + .formParams("j_username", "admin", "j_password", "admin") + .post(baseUrl + "j_security_check") + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .headers() + .getList(HttpHeaders.SET_COOKIE); + return setCookieHeaders.stream().flatMap(h -> HttpCookie.parse(h.getValue()).stream()).toList(); } protected Long subscribe(String feedUrl) { diff --git a/commafeed-server/src/test/java/com/commafeed/integration/SecurityIT.java b/commafeed-server/src/test/java/com/commafeed/integration/SecurityIT.java index 11fe4b59..3864cd7b 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/SecurityIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/SecurityIT.java @@ -2,25 +2,28 @@ package com.commafeed.integration; import java.util.Base64; -import org.eclipse.jetty.http.HttpStatus; +import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import com.commafeed.frontend.model.Entries; import com.commafeed.frontend.model.UserModel; +import com.commafeed.frontend.model.request.MarkRequest; import com.commafeed.frontend.model.request.ProfileModificationRequest; import com.commafeed.frontend.model.request.SubscribeRequest; +import io.quarkus.test.junit.QuarkusTest; import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; +@QuarkusTest class SecurityIT extends BaseIT { @Test void notLoggedIn() { try (Response response = getClient().target(getApiBaseUrl() + "user/profile").request().get()) { - Assertions.assertEquals(HttpStatus.UNAUTHORIZED_401, response.getStatus()); + Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatus()); } } @@ -31,18 +34,18 @@ class SecurityIT extends BaseIT { .request() .header(HttpHeaders.AUTHORIZATION, auth) .get()) { - Assertions.assertEquals(HttpStatus.UNAUTHORIZED_401, response.getStatus()); + Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatus()); } } @Test void missingRole() { String auth = "Basic " + Base64.getEncoder().encodeToString("demo:demo".getBytes()); - try (Response response = getClient().target(getApiBaseUrl() + "admin/settings") + try (Response response = getClient().target(getApiBaseUrl() + "admin/metrics") .request() .header(HttpHeaders.AUTHORIZATION, auth) .get()) { - Assertions.assertEquals(HttpStatus.FORBIDDEN_403, response.getStatus()); + Assertions.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatus()); } } @@ -84,5 +87,16 @@ class SecurityIT extends BaseIT { .request() .get(Entries.class); Assertions.assertEquals("my title for this feed", entries.getName()); + + // mark entry as read and expect it won't work because it's not a GET request + MarkRequest markRequest = new MarkRequest(); + markRequest.setId("1"); + markRequest.setRead(true); + try (Response markResponse = getClient().target(getApiBaseUrl() + "entry/mark") + .queryParam("apiKey", apiKey) + .request() + .post(Entity.json(markRequest))) { + Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, markResponse.getStatus()); + } } } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/WebSocketIT.java b/commafeed-server/src/test/java/com/commafeed/integration/WebSocketIT.java index 7cec67db..d5acb22c 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/WebSocketIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/WebSocketIT.java @@ -1,6 +1,7 @@ package com.commafeed.integration; import java.io.IOException; +import java.net.HttpCookie; import java.net.URI; import java.util.Collections; import java.util.List; @@ -8,6 +9,7 @@ import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; import org.awaitility.Awaitility; import org.glassfish.jersey.client.JerseyClientBuilder; @@ -17,6 +19,7 @@ import org.junit.jupiter.api.Test; import com.commafeed.frontend.model.request.FeedModificationRequest; +import io.quarkus.test.junit.QuarkusTest; import jakarta.websocket.ClientEndpointConfig; import jakarta.websocket.CloseReason; import jakarta.websocket.ContainerProvider; @@ -25,8 +28,10 @@ import jakarta.websocket.Endpoint; import jakarta.websocket.EndpointConfig; import jakarta.websocket.Session; import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.HttpHeaders; import lombok.extern.slf4j.Slf4j; +@QuarkusTest @Slf4j class WebSocketIT extends BaseIT { @@ -49,18 +54,17 @@ class WebSocketIT extends BaseIT { public void onClose(Session session, CloseReason closeReason) { closeReasonRef.set(closeReason); } - }, buildConfig("fake-session-id"), URI.create(getWebSocketUrl()))) { + }, buildConfig(List.of()), URI.create(getWebSocketUrl()))) { Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected); log.info("connected to {}", session.getRequestURI()); Awaitility.await().atMost(15, TimeUnit.SECONDS).until(() -> closeReasonRef.get() != null); - Assertions.assertEquals(CloseReason.CloseCodes.VIOLATED_POLICY, closeReasonRef.get().getCloseCode()); } } @Test void subscribeAndGetsNotified() throws DeploymentException, IOException { - String sessionId = login(); + List cookies = login(); AtomicBoolean connected = new AtomicBoolean(); AtomicReference messageRef = new AtomicReference<>(); @@ -70,7 +74,7 @@ class WebSocketIT extends BaseIT { session.addMessageHandler(String.class, messageRef::set); connected.set(true); } - }, buildConfig(sessionId), URI.create(getWebSocketUrl()))) { + }, buildConfig(cookies), URI.create(getWebSocketUrl()))) { Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected); log.info("connected to {}", session.getRequestURI()); @@ -83,7 +87,7 @@ class WebSocketIT extends BaseIT { @Test void notNotifiedForFilteredEntries() throws DeploymentException, IOException { - String sessionId = login(); + List cookies = login(); Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); FeedModificationRequest req = new FeedModificationRequest(); @@ -100,7 +104,7 @@ class WebSocketIT extends BaseIT { session.addMessageHandler(String.class, messageRef::set); connected.set(true); } - }, buildConfig(sessionId), URI.create(getWebSocketUrl()))) { + }, buildConfig(cookies), URI.create(getWebSocketUrl()))) { Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected); log.info("connected to {}", session.getRequestURI()); @@ -115,7 +119,7 @@ class WebSocketIT extends BaseIT { @Test void pingPong() throws DeploymentException, IOException { - String sessionId = login(); + List cookies = login(); AtomicBoolean connected = new AtomicBoolean(); AtomicReference messageRef = new AtomicReference<>(); @@ -125,7 +129,7 @@ class WebSocketIT extends BaseIT { session.addMessageHandler(String.class, messageRef::set); connected.set(true); } - }, buildConfig(sessionId), URI.create(getWebSocketUrl()))) { + }, buildConfig(cookies), URI.create(getWebSocketUrl()))) { Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected); log.info("connected to {}", session.getRequestURI()); @@ -136,11 +140,12 @@ class WebSocketIT extends BaseIT { } } - private ClientEndpointConfig buildConfig(String sessionId) { + private ClientEndpointConfig buildConfig(List cookies) { return ClientEndpointConfig.Builder.create().configurator(new ClientEndpointConfig.Configurator() { @Override public void beforeRequest(Map> headers) { - headers.put("Cookie", Collections.singletonList("JSESSIONID=" + sessionId)); + headers.put(HttpHeaders.COOKIE, + Collections.singletonList(cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";")))); } }).build(); } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/rest/AdminIT.java b/commafeed-server/src/test/java/com/commafeed/integration/rest/AdminIT.java index 138fd821..c6e53815 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/rest/AdminIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/rest/AdminIT.java @@ -9,14 +9,15 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import com.commafeed.CommaFeedConfiguration.ApplicationSettings; import com.commafeed.backend.model.User; import com.commafeed.frontend.model.UserModel; import com.commafeed.frontend.model.request.IDRequest; import com.commafeed.integration.BaseIT; +import io.quarkus.test.junit.QuarkusTest; import jakarta.ws.rs.client.Entity; +@QuarkusTest class AdminIT extends BaseIT { @Override @@ -24,12 +25,6 @@ class AdminIT extends BaseIT { return base.register(HttpAuthenticationFeature.basic("admin", "admin")); } - @Test - void getApplicationSettings() { - ApplicationSettings settings = getClient().target(getApiBaseUrl() + "admin/settings").request().get(ApplicationSettings.class); - Assertions.assertTrue(settings.getAllowRegistrations()); - } - @Nested class Users { @Test diff --git a/commafeed-server/src/test/java/com/commafeed/integration/rest/FeedIT.java b/commafeed-server/src/test/java/com/commafeed/integration/rest/FeedIT.java index c5c6c2a5..6b561c2d 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/rest/FeedIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/rest/FeedIT.java @@ -9,9 +9,8 @@ import java.time.ZoneOffset; import java.util.Objects; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; +import org.apache.hc.core5.http.HttpStatus; import org.awaitility.Awaitility; -import org.eclipse.jetty.http.HttpStatus; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.JerseyClientBuilder; import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; @@ -30,10 +29,12 @@ import com.commafeed.frontend.model.request.IDRequest; import com.commafeed.frontend.model.request.MarkRequest; import com.commafeed.integration.BaseIT; +import io.quarkus.test.junit.QuarkusTest; import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +@QuarkusTest class FeedIT extends BaseIT { @Override @@ -69,19 +70,19 @@ class FeedIT extends BaseIT { .property(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE) .request() .get()) { - Assertions.assertEquals(HttpStatus.TEMPORARY_REDIRECT_307, response.getStatus()); + Assertions.assertEquals(HttpStatus.SC_TEMPORARY_REDIRECT, response.getStatus()); } } @Test void unsubscribeFromUnknownFeed() { - Assertions.assertEquals(HttpStatus.NOT_FOUND_404, unsubsribe(1L)); + Assertions.assertEquals(HttpStatus.SC_NOT_FOUND, unsubsribe(1L)); } @Test void unsubscribeFromKnownFeed() { long subscriptionId = subscribe(getFeedUrl()); - Assertions.assertEquals(HttpStatus.OK_200, unsubsribe(subscriptionId)); + Assertions.assertEquals(HttpStatus.SC_OK, unsubsribe(subscriptionId)); } private int unsubsribe(long subscriptionId) { @@ -212,20 +213,7 @@ class FeedIT extends BaseIT { void importExportOpml() throws IOException { importOpml(); String opml = getClient().target(getApiBaseUrl() + "feed/export").request().get(String.class); - String expextedOpml = """ - - - - admin subscriptions in CommaFeed - - - - - - - - """; - Assertions.assertEquals(StringUtils.normalizeSpace(expextedOpml), StringUtils.normalizeSpace(opml)); + Assertions.assertTrue(opml.contains("admin subscriptions in CommaFeed")); } void importOpml() throws IOException { diff --git a/commafeed-server/src/test/java/com/commafeed/integration/rest/FeverIT.java b/commafeed-server/src/test/java/com/commafeed/integration/rest/FeverIT.java index 8f8ea1a0..8a52a003 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/rest/FeverIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/rest/FeverIT.java @@ -12,9 +12,11 @@ import com.commafeed.frontend.model.request.ProfileModificationRequest; import com.commafeed.frontend.resource.fever.FeverResponse; import com.commafeed.integration.BaseIT; +import io.quarkus.test.junit.QuarkusTest; import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.Form; +@QuarkusTest class FeverIT extends BaseIT { private Long userId; @@ -26,7 +28,7 @@ class FeverIT extends BaseIT { } @BeforeEach - void init() { + void setup() { // create api key ProfileModificationRequest req = new ProfileModificationRequest(); req.setCurrentPassword("admin"); @@ -73,6 +75,7 @@ class FeverIT extends BaseIT { Form form = new Form(); form.param("api_key", Digests.md5Hex("admin:" + apiKey)); form.param(what, "1"); + return getClient().target(getApiBaseUrl() + "fever/user/{userId}") .resolveTemplate("userId", userId) .request() diff --git a/commafeed-server/src/test/java/com/commafeed/integration/rest/ServerIT.java b/commafeed-server/src/test/java/com/commafeed/integration/rest/ServerIT.java index 82201f6f..2a9a6958 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/rest/ServerIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/rest/ServerIT.java @@ -6,6 +6,9 @@ import org.junit.jupiter.api.Test; import com.commafeed.frontend.model.ServerInfo; import com.commafeed.integration.BaseIT; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest class ServerIT extends BaseIT { @Test @@ -16,7 +19,7 @@ class ServerIT extends BaseIT { Assertions.assertTrue(serverInfos.isDemoAccountEnabled()); Assertions.assertTrue(serverInfos.isWebsocketEnabled()); Assertions.assertEquals(900000, serverInfos.getWebsocketPingInterval()); - Assertions.assertEquals(10000, serverInfos.getTreeReloadInterval()); + Assertions.assertEquals(30000, serverInfos.getTreeReloadInterval()); } } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/rest/UserIT.java b/commafeed-server/src/test/java/com/commafeed/integration/rest/UserIT.java index 3eccde06..ae077b1c 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/rest/UserIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/rest/UserIT.java @@ -1,30 +1,38 @@ package com.commafeed.integration.rest; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; import com.commafeed.frontend.model.request.PasswordResetRequest; import com.commafeed.integration.BaseIT; -import com.icegreen.greenmail.junit5.GreenMailExtension; +import com.icegreen.greenmail.util.GreenMail; import com.icegreen.greenmail.util.ServerSetupTest; +import io.quarkus.test.junit.QuarkusTest; import jakarta.mail.internet.MimeMessage; import jakarta.ws.rs.client.Entity; +@QuarkusTest class UserIT extends BaseIT { @Nested class PasswordReset { - @RegisterExtension - static final GreenMailExtension GREEN_MAIL = new GreenMailExtension(ServerSetupTest.SMTP); + private GreenMail greenMail; @BeforeEach - void init() { - GREEN_MAIL.setUser("noreply@commafeed.com", "user", "pass"); + void setup() { + this.greenMail = new GreenMail(ServerSetupTest.SMTP); + this.greenMail.start(); + this.greenMail.setUser("noreply@commafeed.com", "user", "pass"); + } + + @AfterEach + void cleanup() { + this.greenMail.stop(); } @Test @@ -34,7 +42,7 @@ class UserIT extends BaseIT { getClient().target(getApiBaseUrl() + "user/passwordReset").request().post(Entity.json(req), Void.TYPE); - MimeMessage message = GREEN_MAIL.getReceivedMessages()[0]; + MimeMessage message = greenMail.getReceivedMessages()[0]; Assertions.assertEquals("CommaFeed - Password recovery", message.getSubject()); Assertions.assertTrue(message.getContent().toString().startsWith("You asked for password recovery for account 'admin'")); Assertions.assertEquals("CommaFeed ", message.getFrom()[0].toString()); diff --git a/commafeed-server/src/test/java/com/commafeed/integration/servlet/CustomCodeIT.java b/commafeed-server/src/test/java/com/commafeed/integration/servlet/CustomCodeIT.java index 0a903fb5..9e6229c6 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/servlet/CustomCodeIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/servlet/CustomCodeIT.java @@ -1,5 +1,9 @@ package com.commafeed.integration.servlet; +import java.net.HttpCookie; +import java.util.List; +import java.util.stream.Collectors; + import org.glassfish.jersey.client.JerseyClientBuilder; import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; import org.junit.jupiter.api.Assertions; @@ -8,10 +12,12 @@ import org.junit.jupiter.api.Test; import com.commafeed.frontend.model.Settings; import com.commafeed.integration.BaseIT; +import io.quarkus.test.junit.QuarkusTest; import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; +@QuarkusTest class CustomCodeIT extends BaseIT { @Override @@ -30,16 +36,16 @@ class CustomCodeIT extends BaseIT { getClient().target(getApiBaseUrl() + "user/settings").request().post(Entity.json(settings), Void.TYPE); // check custom code servlets - String cookie = login(); + List cookies = login(); try (Response response = getClient().target(getBaseUrl() + "custom_js.js") .request() - .header(HttpHeaders.COOKIE, "JSESSIONID=" + cookie) + .header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";"))) .get()) { Assertions.assertEquals("custom-js", response.readEntity(String.class)); } try (Response response = getClient().target(getBaseUrl() + "custom_css.css") .request() - .header(HttpHeaders.COOKIE, "JSESSIONID=" + cookie) + .header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";"))) .get()) { Assertions.assertEquals("custom-css", response.readEntity(String.class)); } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/servlet/LogoutIT.java b/commafeed-server/src/test/java/com/commafeed/integration/servlet/LogoutIT.java index 10e97246..b12aedb1 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/servlet/LogoutIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/servlet/LogoutIT.java @@ -1,32 +1,34 @@ package com.commafeed.integration.servlet; -import org.eclipse.jetty.http.HttpStatus; +import java.net.HttpCookie; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.hc.core5.http.HttpStatus; import org.glassfish.jersey.client.ClientProperties; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import com.commafeed.frontend.model.UserModel; import com.commafeed.integration.BaseIT; -import jakarta.ws.rs.NotAuthorizedException; -import jakarta.ws.rs.client.Invocation.Builder; +import io.quarkus.test.junit.QuarkusTest; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; +@QuarkusTest class LogoutIT extends BaseIT { @Test void test() { - String cookie = login(); + List cookies = login(); try (Response response = getClient().target(getBaseUrl() + "logout") .request() - .header(HttpHeaders.COOKIE, "JSESSIONID=" + cookie) + .header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";"))) .property(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE) .get()) { - Assertions.assertEquals(HttpStatus.FOUND_302, response.getStatus()); + Assertions.assertEquals(HttpStatus.SC_TEMPORARY_REDIRECT, response.getStatus()); + List setCookieHeaders = response.getStringHeaders().get(HttpHeaders.SET_COOKIE); + Assertions.assertTrue(setCookieHeaders.stream().flatMap(c -> HttpCookie.parse(c).stream()).allMatch(c -> c.getMaxAge() == 0)); } - - Builder req = getClient().target(getApiBaseUrl() + "user/profile").request().header(HttpHeaders.COOKIE, "JSESSIONID=" + cookie); - Assertions.assertThrows(NotAuthorizedException.class, () -> req.get(UserModel.class)); } } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/servlet/NextUnreadIT.java b/commafeed-server/src/test/java/com/commafeed/integration/servlet/NextUnreadIT.java index cb6ada00..60331b75 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/servlet/NextUnreadIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/servlet/NextUnreadIT.java @@ -1,6 +1,10 @@ package com.commafeed.integration.servlet; -import org.eclipse.jetty.http.HttpStatus; +import java.net.HttpCookie; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.hc.core5.http.HttpStatus; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.JerseyClientBuilder; import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; @@ -9,9 +13,11 @@ import org.junit.jupiter.api.Test; import com.commafeed.integration.BaseIT; +import io.quarkus.test.junit.QuarkusTest; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; +@QuarkusTest class NextUnreadIT extends BaseIT { @Override @@ -23,13 +29,13 @@ class NextUnreadIT extends BaseIT { void test() { subscribeAndWaitForEntries(getFeedUrl()); - String cookie = login(); + List cookies = login(); Response response = getClient().target(getBaseUrl() + "next") .property(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE) .request() - .header(HttpHeaders.COOKIE, "JSESSIONID=" + cookie) + .header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";"))) .get(); - Assertions.assertEquals(HttpStatus.FOUND_302, response.getStatus()); + Assertions.assertEquals(HttpStatus.SC_TEMPORARY_REDIRECT, response.getStatus()); Assertions.assertEquals("https://hostname.local/commafeed/2", response.getHeaderString(HttpHeaders.LOCATION)); } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/servlet/RobotsTxtIT.java b/commafeed-server/src/test/java/com/commafeed/integration/servlet/RobotsTxtIT.java index 142b0c27..f16a5fa9 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/servlet/RobotsTxtIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/servlet/RobotsTxtIT.java @@ -5,8 +5,10 @@ import org.junit.jupiter.api.Test; import com.commafeed.integration.BaseIT; +import io.quarkus.test.junit.QuarkusTest; import jakarta.ws.rs.core.Response; +@QuarkusTest class RobotsTxtIT extends BaseIT { @Test void test() { diff --git a/commafeed-server/src/test/resources/config.test.yml b/commafeed-server/src/test/resources/config.test.yml deleted file mode 100644 index 209f3efc..00000000 --- a/commafeed-server/src/test/resources/config.test.yml +++ /dev/null @@ -1,138 +0,0 @@ -# CommaFeed settings -# ------------------ -app: - # url used to access commafeed - publicUrl: http://localhost:8082/ - - # whether to expose a robots.txt file that disallows web crawlers and search engine indexers - hideFromWebCrawlers: true - - # whether to allow user registrations - allowRegistrations: true - - # whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char) - strictPasswordPolicy: true - - # create a demo account the first time the app starts - createDemoAccount: true - - # put your google analytics tracking code here - googleAnalyticsTrackingCode: - - # put your google server key (used for youtube favicon fetching) - googleAuthKey: - - # number of http threads - backgroundThreads: 3 - - # number of database updating threads - databaseUpdateThreads: 1 - - # rows to delete per query while cleaning up old entries - databaseCleanupBatchSize: 100 - - # settings for sending emails (password recovery) - smtpHost: localhost - smtpPort: 3025 - smtpTls: false - smtpUserName: user - smtpPassword: pass - smtpFromAddress: noreply@commafeed.com - - # Graphite Metric settings - # Allows those who use Graphite to have CommaFeed send metrics for graphing (time in seconds) - graphiteEnabled: false - graphitePrefix: "test.commafeed" - graphiteHost: "localhost" - graphitePort: 2003 - graphiteInterval: 60 - - # whether this commafeed instance has a lot of feeds to refresh - # leave this to false in almost all cases - heavyLoad: false - - # minimum amount of time commafeed will wait before refreshing the same feed - refreshIntervalMinutes: 5 - - # if enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser - # useful if commafeed is usually accessed through a restricting proxy - imageProxyEnabled: false - - # database query timeout (in milliseconds), 0 to disable - queryTimeout: 0 - - # time to keep unread statuses (in days), 0 to disable - keepStatusDays: 0 - - # entries to keep per feed, old entries will be deleted, 0 to disable - maxFeedCapacity: 500 - - # entries older than this will be deleted, 0 to disable - maxEntriesAgeDays: 0 - - # limit the number of feeds a user can subscribe to, 0 to disable - maxFeedsPerUser: 0 - - # don't parse feeds that are too large to prevent memory issues - maxFeedResponseSize: 5M - - # cache service to use, possible values are 'noop' and 'redis' - cache: noop - - # announcement string displayed on the main page - announcement: - - # user-agent string that will be used by the http client, leave empty for the default one - userAgent: - - # enable websocket connection so the server can notify the web client that there are new entries for your feeds - websocketEnabled: true - - # interval at which the client will send a ping message on the websocket to keep the connection alive - websocketPingInterval: 15m - - # if websocket is disabled or the connection is lost, the client will reload the feed tree at this interval - treeReloadInterval: 10s - -# Database connection -# ------------------- -# for MariaDB -# driverClass is org.mariadb.jdbc.Driver -# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC -# -# for MySQL -# driverClass is com.mysql.cj.jdbc.Driver -# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC -# -# for PostgreSQL -# driverClass is org.postgresql.Driver -# url is jdbc:postgresql://localhost:5432/commafeed - -database: - driverClass: org.h2.Driver - url: jdbc:h2:mem:commafeed - user: sa - password: sa - properties: - charSet: UTF-8 - validationQuery: "/* CommaFeed Health Check */ SELECT 1" - minSize: 1 - maxSize: 5 - initialSize: 1 - -server: - applicationConnectors: - - type: http - port: 8083 - adminConnectors: [ ] - -logging: - level: INFO - loggers: - com.commafeed: DEBUG - liquibase: INFO - org.hibernate.SQL: INFO # or ALL for sql debugging - org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN - appenders: - - type: console - \ No newline at end of file diff --git a/commafeed-server/src/test/resources/docker-images.properties b/commafeed-server/src/test/resources/docker-images.properties deleted file mode 100644 index b40aa81e..00000000 --- a/commafeed-server/src/test/resources/docker-images.properties +++ /dev/null @@ -1,4 +0,0 @@ -postgresql=postgres:${postgresql.image.version} -mysql=mysql:${mysql.image.version} -mariadb=mariadb:${mariadb.image.version} -redis=redis:${redis.image.version} \ No newline at end of file diff --git a/commafeed-server/src/test/resources/h2-migration/database-v2.1.214.mv.db b/commafeed-server/src/test/resources/h2-migration/database-v2.1.214.mv.db deleted file mode 100644 index b11490bc..00000000 Binary files a/commafeed-server/src/test/resources/h2-migration/database-v2.1.214.mv.db and /dev/null differ diff --git a/commafeed-server/src/test/resources/logback-test.xml b/commafeed-server/src/test/resources/logback-test.xml deleted file mode 100644 index a452f81a..00000000 --- a/commafeed-server/src/test/resources/logback-test.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - %-5p %d{ISO8601} [%thread] [%c{0}:%L] %m %rEx%n - - - - - - - \ No newline at end of file