Skip to content

Add REST controllers for each domain - #603

Draft
Slartibartfass2 wants to merge 39 commits into
developfrom
feat/600-feature-add-rest-controllers-for-each-domain
Draft

Add REST controllers for each domain#603
Slartibartfass2 wants to merge 39 commits into
developfrom
feat/600-feature-add-rest-controllers-for-each-domain

Conversation

@Slartibartfass2

@Slartibartfass2 Slartibartfass2 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Closes #600

What I have made

Waiting for #595

Checklist

Either tick or cross out the items that do not apply (using ~~example text~~) and give a reason why the item does not apply.

Author

  • I have updated the documentation accordingly and commented my code
    • I have updated AGENTS.md if the changes affect project structure, commands, tooling, or conventions
  • I have manually tested my changes
  • I have added tests that prove my fix is effective or that my feature works

Reviewer

  • I have checked the changes against the requirements

@Slartibartfass2 Slartibartfass2 self-assigned this Aug 14, 2026
@Slartibartfass2 Slartibartfass2 linked an issue Aug 14, 2026 that may be closed by this pull request
1 task
@gitguardian

gitguardian Bot commented Aug 14, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 2 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secrets in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
36073591 Triggered Generic Password 1df56f6 src/test/kotlin/se/uulm/snowballr/backend/rest/AuthWorkflowIntegrationTest.kt View secret
24483668 Triggered Generic Password 1df56f6 src/test/kotlin/se/uulm/snowballr/backend/rest/AuthWorkflowIntegrationTest.kt View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown

Code Coverage

Overall Project 98.12% -0.08% 🍏
Files changed 0%

File Coverage
AddPaperToProjectRequest.kt 0%
UpdateMemberRoleRequest.kt 0%
InviteUserRequest.kt 0%

@Slartibartfass2
Slartibartfass2 force-pushed the feat/600-feature-add-rest-controllers-for-each-domain branch from 3393e72 to f6d4484 Compare August 16, 2026 14:19
Slartibartfass2 and others added 25 commits August 17, 2026 23:01
writeQueuedCookies ran before filterChain.doFilter, so cookies queued
by handlers during the request (e.g. login, logout) were never
written to the response - only pre-emptive refresh/clear cookies
queued during authentication were flushed.
Drives register, verify-email, login, status, change-password, and
logout through MockMvc and the actual security filter chain, rather
than calling the service layer directly, so cookie handling on the
wire is exercised the same way a real client would use it.
Both endpoints were caught by anyRequest().authenticated(), so
/auth/status could never report UNAUTHENTICATED and /auth/logout
could never clear cookies once both tokens had expired.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…requests

SameSite=Strict blocks classic cross-site CSRF but still permits
requests from a sibling subdomain, which is same-site. Requiring a
custom header as defense-in-depth closes that gap, since forged
cross-origin form/fetch requests cannot set one without triggering a
CORS preflight.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cookies queued by pre-emptive token refresh (or refresh-failure
clearing) were only written in the finally block after the handler
ran, so a handler streaming a large enough body to auto-flush the
output buffer silently dropped them, since Set-Cookie is a no-op on
an already-committed response. Flush them immediately instead, and
re-flush only cookies the handler itself changed afterward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Only /actuator/health is exposed today, but the wildcard sat above
anyRequest().authenticated(), so exposing more endpoints later (e.g.
/actuator/env, /actuator/heapdump) would have made them anonymously
reachable without anyone touching the security config.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Matches the constructor-injection convention used by every other
service/manager in the codebase and avoids exposing the injected
service as public API on the controller bean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Matches the explicit binding annotations used by every other handler
in this controller instead of relying on implicit simple-type param
resolution.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…akage

Switches the Docker healthcheck from grpc-health-probe to Spring Boot
actuator on port 8090, replacing gRPC health with HTTP health now that
Spring is the process entrypoint.

This exposed a latent bug that made the REST server unreachable:
shadowJar's default duplicatesStrategy (EXCLUDE) silently dropped
nearly all Spring Boot autoconfiguration metadata, since several
dependency jars ship META-INF/spring.factories and
META-INF/spring/*.imports at the same path. Without
TomcatServletWebServerAutoConfiguration surviving the merge, the app
booted as a plain (non-web) ApplicationContext and never bound port
8090 at all. Fixed by setting duplicatesStrategy = INCLUDE and
merging/appending the affected resource files instead of picking one
arbitrarily.

The healthcheck itself uses wget --spider rather than curl: curl was
only ever needed for pycurl's build (libcurl/curl-dev) and isn't
installed by default, while wget is already present via BusyBox in the
eclipse-temurin alpine base, so no extra package is needed. It also
drops a `grep "status":"UP"` step, since Spring Boot Actuator already
maps health status to HTTP status codes by default (200 for UP, 503
otherwise) — wget's own exit code is sufficient.

Verified with a full docker compose build/up against the real .env and
database: actuator/health returns 200 and the container reaches
Docker's healthy state using the wget-based check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GetAllUsers/GetUserByEmail (consolidated as a query filter), GetCurrentUser,
GetUserById, UpdateUser (full-replace), SoftDeleteUser, GetUserSettings, and
GetInviteCandidates (moved from Invitation per GRPC_TO_REST_MAPPING.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GetAllProjectsForUser/GetAllArchivedProjectsForUser/GetAllDeletedProjectsForUser
consolidated into GetAllProjects as userId/status query filters, plus
UpdateProject (full-replace), GetProjectInformation (always whole-object), and
GetDecisionStatisticsForStage, per GRPC_TO_REST_MAPPING.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Slartibartfass2 and others added 13 commits August 17, 2026 23:02
GetAvailableExportFormats (public, no auth per proto) and ExportProject as a
binary download, per GRPC_TO_REST_MAPPING.md's case (b) exception - kept off
/projects to keep ProjectsController smaller.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GetProjectMembers, UpdateProjectMemberRole (role-only intent endpoint), and
RemoveProjectMember (keyed by email - covers pending invitees too), nested
under /projects/{projectId}/members per GRPC_TO_REST_MAPPING.md's
resource-nesting rule.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
InviteUserToProject and GetPendingInvitationsForProject nest under
/projects/{id}/invitations; AcceptProjectInvitation is a public
/invitations/{token}/accept, keyed only by the opaque token per the proto
(no @auth tag, matches GRPC_TO_REST_MAPPING.md's case (a) exception).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Nested under /users/me/reading-list rather than a flat /reading-list, since
the list is current-user-owned just like GetUserSettings. AddPaperToReadingList
is PUT (client-known key, idempotent), IsPaperOnReadingList is HEAD with a
200/404 existence check instead of a boolean body.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GetAllProjectPapersForProject, AddPaperToProject, GetProjectPaperByRelativeId
and GetPapersToReviewForProject nest under /projects/{id}/papers*; GetNextPaper,
GetNextPaperToReview, GetPreviousPaper and GetProjectPaperById stay on the flat
/project-papers/{id} since those gRPC methods never receive a project ID, per
GRPC_TO_REST_MAPPING.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GetReviewById is a case (a) exception (review-only ID, no project-paper ID to
nest under); GetAllReviewsForProjectPaper and CreateReview nest under
/project-papers/{id}/reviews. UpdateReview/DeleteReview are unimplemented in
gRPC, so reviews are create/read-only via REST for now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GetAllCriteriaForProject nests under /projects/{id}/criteria; CreateCriterion
stays flat on /criteria since projectId is optional (global vs. project
criteria); GetCriterionById/UpdateCriterion are case (a) exceptions (no
project ID in the request). DeleteCriterion is unimplemented, so no DELETE.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GetPaperById, CreatePaper, UpdatePaper (full-replace), and forward/backward
reference lookups. Flat /papers throughout - Paper has no owning resource in
the domain model, papers exist independently of any project.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GetAvailableFetchers is a global, non-nesting registry (/fetchers);
SearchLocalProjectPaperCandidates and SearchFetcherProjectPaperCandidates
nest under /projects/{id}/paper-candidates/{local,fetcher}.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Boolean naming (onList -> isOnReadingList) and ForbiddenVoid
(ResponseEntity<Void> -> ResponseEntity<Unit>) in ReadingListController,
plus a ktlint reformat of ProjectsController.updateProject.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Value-returning handlers keep expression body (fun x(): T = onRequest { ... });
Unit-returning ones now consistently use block body instead of relying on
implicit Unit inference. Fixes the two pre-existing outliers (AuthController's
login/logout/verifyEmail/changePassword, UsersController.register) that
predated this session's new controllers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
KoinBridge now eagerly resolves 9 additional service beans for the new REST
controllers; the test's minimal Koin module only covered the original 3,
so the Spring context failed to start. Mock every IXService KoinBridge
requires, and regenerate the committed OpenAPI spec now that it succeeds.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Slartibartfass2
Slartibartfass2 force-pushed the feat/600-feature-add-rest-controllers-for-each-domain branch from f6d4484 to c95f59b Compare August 17, 2026 21:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Add REST controllers for each domain

1 participant