feat: Add resolution document management for conferences - #430
Conversation
… screen Participants can now download the conference's adopted resolutions from the post-conference (POST state) dashboard, and management can upload/manage them in the conference configuration. - prisma: new `Resolution` model (title, fileName, base64 content, optional committee tag) with a migration; relations on Conference and Committee - api: `resolution` resolver module (findMany/findUnique queries; create/update/delete mutations) and CASL abilities scoping read to conference participants and write to PROJECT_MANAGEMENT team members - config: multi-file PDF upload (≤10 MB) with an inline management list (rename, re-assign committee, delete) in the documents tab - dashboard: resolutions download section on the Certificate (POST) screen, grouped by committee - api route: `/api/resolution/[id]` streams the stored PDF (permission-checked) - i18n: German and English translations for the new strings - schema.graphql regenerated to include the new types Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsRXmXgAXefuM4BSYAeAAj
|
Warning Review limit reached
Next review available in: 40 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdded resolution PDF storage, GraphQL operations, authorization, conference management UI, secure downloads, and dashboard display. Updated English and German messages, dependency overrides, and Trivy vulnerability suppressions. ChangesResolution management
Tooling security updates
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant ConferenceConfiguration
participant uploadResolutions
participant createResolution
participant ResolutionDatabase
ConferenceConfiguration->>uploadResolutions: Submit PDF files
uploadResolutions->>uploadResolutions: Validate PDF type and 10 MB limit
uploadResolutions->>createResolution: Create each accepted file
createResolution->>ResolutionDatabase: Store resolution metadata and content
ResolutionDatabase-->>ConferenceConfiguration: Return upload count
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 6 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (6 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…ions Resolves Trivy/dependabot advisories flagged on the dependency tree (all DoS / ReDoS / algorithmic-complexity issues, no code changes): - axios 1.16.0 → 1.18.0 (GHSA-gcfj-64vw-6mp9) - brace-expansion → 5.0.8 (CVE-2026-13149, CVE-2026-14257) - fast-uri 3.1.2 → 3.1.4 (CVE-2026-13676, CVE-2026-16221) - js-yaml → 4.3.0 override (CVE-2026-59869) - linkify-it 5.0.1 → 5.0.2 (CVE-2026-59887) - shell-quote 1.8.4 → 1.9.0 (CVE-2026-13311) - postcss ^8.5.6 → ^8.5.18 (CVE-2026-45623, GHSA-r28c-9q8g-f849) Not addressed here (require separate work): @opentelemetry/propagator-jaeger (needs the OTEL stack upgraded to 2.x) and the golang.org/x/text advisory in the lefthook dev binary (1.13.6 is the latest release; dev-only, not shipped). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsRXmXgAXefuM4BSYAeAAj
…unfixable CVEs The Trivy fs-scan was reading a committed package-lock.json that is not used by the build (the app installs via `bun install --frozen-lockfile`). It was added incidentally in an unrelated PR, is badly drifted (e.g. axios 1.13.2 vs the app's 1.18.0), and cannot be regenerated in this repo (npm rejects the bun-only `overrides` with EOVERRIDE). Removing it so the scan reflects the real dependency tree in bun.lock. - delete package-lock.json (stale, unmaintainable, not used by the bun build) - add `postcss` to overrides so the transitive 8.5.6 copy (pulled by vite / better-svelte-email) collapses onto the fixed 8.5.25 — the earlier direct devDependency bump only moved the top-level copy - .trivyignore two advisories that cannot be bumped right now, with justification: - CVE-2026-59892 @opentelemetry/propagator-jaeger — unused (app uses OTLP-HTTP, not Jaeger propagation; not imported in src/); fix needs the OTEL stack on 2.x - CVE-2026-56852 golang.org/x/text — inside the lefthook dev binary (1.13.6 is latest), not shipped in the app; matches the existing esbuild/lefthook section Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsRXmXgAXefuM4BSYAeAAj
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (4)
src/routes/(authenticated)/management/[conferenceId]/configuration/ResolutionManager.svelte (1)
179-179: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse Duotone icon classes.
These icons use
fas. Line 213 already usesfa-duotone fa-file-pdf. Align the new icons with the Duotone style for consistency.♻️ Proposed fix
- <i class="fas fa-spinner fa-spin"></i> + <i class="fa-duotone fa-spinner fa-spin"></i>- <i class="fas fa-circle-info"></i> + <i class="fa-duotone fa-circle-info"></i>- <i class="fas fa-download"></i> + <i class="fa-duotone fa-download"></i>- <i class="fas fa-trash"></i> + <i class="fa-duotone fa-trash"></i>As per coding guidelines: "Use FontAwesome Duotone icons with
fa-duotone fa-icon-nameclasses".Also applies to: 188-188, 242-242, 250-250
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/routes/`(authenticated)/management/[conferenceId]/configuration/ResolutionManager.svelte at line 179, Replace the `fas` class on the spinner icons at the referenced locations with the Duotone FontAwesome class pattern, using `fa-duotone` together with the existing icon names. Apply this consistently to the icons near the resolution manager loading and action states, while preserving their current icon behavior.Source: Coding guidelines
src/routes/(authenticated)/dashboard/[conferenceId]/stages/Common/Certificate.svelte (1)
234-234: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse the duotone icon class.
Line 232 uses
fa-duotone, and line 234 usesfasfor the download icon.- <i class="fas fa-download ml-auto"></i> + <i class="fa-duotone fa-download ml-auto"></i>As per coding guidelines: "Use FontAwesome Duotone icons with
fa-duotone fa-icon-nameclasses".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/routes/`(authenticated)/dashboard/[conferenceId]/stages/Common/Certificate.svelte at line 234, Update the download icon class on the <i> element in the Certificate component to use the FontAwesome Duotone prefix, replacing the solid-style `fas` class while preserving the existing `fa-download` and layout classes.Source: Coding guidelines
prisma/migrations/20260731000000_add_resolution_downloads/migration.sql (1)
15-19: 🚀 Performance & Scalability | 🔵 TrivialConsider indexing the two foreign key columns.
PostgreSQL does not create an index for a foreign key column automatically. The dashboard query filters resolutions by
conferenceId, and a committee deletion performs theSET NULLupdate oncommitteeId. Both operations scan the table without indexes. Add@@index([conferenceId])and@@index([committeeId])to theResolutionmodel inprisma/schema.prisma, then regenerate the migration.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@prisma/migrations/20260731000000_add_resolution_downloads/migration.sql` around lines 15 - 19, Add indexes for conferenceId and committeeId to the Resolution model in prisma/schema.prisma, then regenerate the migration so it creates both indexes alongside the foreign keys.src/routes/api/resolution/[resolutionId]/+server.ts (1)
41-49: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winHarden the download response headers.
Two improvements apply here. Pin the response type to
application/pdfand addX-Content-Type-Options: nosniff, becausemimecomes from the uploaded file's declared type and is not validated at creation time. Also add the RFC 5987filename*parameter, because afileNamewith non-ASCII characters is currently placed unencoded into the header value and browsers then fall back to an unpredictable name.The MIME validation itself belongs in
createResolution; see the comment insrc/api/resolvers/modules/resolution.ts.🔒 Proposed header changes
const safeName = (resolution.fileName || 'resolution.pdf').replace(/["\r\n]/g, ''); return new Response(bytes, { headers: { - 'Content-Type': mime, + 'Content-Type': 'application/pdf', 'Content-Length': String(bytes.byteLength), - 'Content-Disposition': `attachment; filename="${safeName}"`, + 'Content-Disposition': `attachment; filename="${safeName}"; filename*=UTF-8''${encodeURIComponent(safeName)}`, + 'X-Content-Type-Options': 'nosniff', 'Cache-Control': 'private, no-store' } });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/routes/api/resolution/`[resolutionId]/+server.ts around lines 41 - 49, Update the download response headers in the resolution handler to use application/pdf instead of the uploaded mime value, add X-Content-Type-Options: nosniff, and include an RFC 5987-encoded filename* parameter alongside the existing sanitized filename. Keep filename safe for ASCII compatibility while encoding non-ASCII names for reliable browser downloads; MIME validation remains in createResolution.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/api/resolvers/modules/resolution.ts`:
- Around line 100-113: Update createResolution to validate args.file before
calling toDataURL or db.resolution.create: reject uploads whose MIME type is not
application/pdf and uploads exceeding 10 MB, using the resolver’s existing
error/validation mechanism. Only proceed to derive fileName, encode content, and
persist the resolution after both checks pass.
In
`@src/routes/`(authenticated)/dashboard/[conferenceId]/stages/Common/Certificate.svelte:
- Around line 79-94: Carry the stable committee identifier from the key computed
in groupedResolutions into each ResolutionGroup, using a nullable value for
resolutions without a committee. Update the keyed each block to use this
committee id rather than the formatted committeeName, while preserving the
existing display name and grouping behavior.
In
`@src/routes/`(authenticated)/management/[conferenceId]/configuration/+page.server.ts:
- Around line 265-279: Update the file-upload loop around
CreateResolutionMutation.mutate to handle each file independently, collecting
successful and failed file names instead of aborting on the first mutation
error. Return both success and failure details to the client, avoid marking the
cache stale when no upload succeeds, and preserve the existing conference and
committee mutation inputs.
- Around line 254-263: Update the resolution upload validation loop to require
file.type === 'application/pdf' rather than accepting a filename extension with
a different MIME type, or normalize accepted files to application/pdf before
persistence. Add a maximum file-count check before iterating, return fail(400, {
uploadError: m.resolutionUploadTooManyFiles() }) when exceeded, and add the
matching resolutionUploadTooManyFiles message key to the English and German
message files.
In
`@src/routes/`(authenticated)/management/[conferenceId]/configuration/ResolutionManager.svelte:
- Around line 66-67: Remove the HTMLInputElement and related as-casts from
handleFilesSelected and the other affected handlers, and type each event
parameter so currentTarget is inferred as the correct element type. Update the
handler signatures at the referenced locations while preserving their existing
behavior.
- Around line 261-272: The ResolutionManager modal currently sits inside the
conference settings form, causing both modal actions to submit it. In
src/routes/(authenticated)/management/[conferenceId]/configuration/ResolutionManager.svelte
lines 261-272, add type="button" to the Cancel and Delete buttons; in
src/routes/(authenticated)/management/[conferenceId]/configuration/+page.svelte
lines 802-803, move the ResolutionManager component out of the Form and into a
separate Documents-tab wrapper after the closing Form tag.
---
Nitpick comments:
In `@prisma/migrations/20260731000000_add_resolution_downloads/migration.sql`:
- Around line 15-19: Add indexes for conferenceId and committeeId to the
Resolution model in prisma/schema.prisma, then regenerate the migration so it
creates both indexes alongside the foreign keys.
In
`@src/routes/`(authenticated)/dashboard/[conferenceId]/stages/Common/Certificate.svelte:
- Line 234: Update the download icon class on the <i> element in the Certificate
component to use the FontAwesome Duotone prefix, replacing the solid-style `fas`
class while preserving the existing `fa-download` and layout classes.
In
`@src/routes/`(authenticated)/management/[conferenceId]/configuration/ResolutionManager.svelte:
- Line 179: Replace the `fas` class on the spinner icons at the referenced
locations with the Duotone FontAwesome class pattern, using `fa-duotone`
together with the existing icon names. Apply this consistently to the icons near
the resolution manager loading and action states, while preserving their current
icon behavior.
In `@src/routes/api/resolution/`[resolutionId]/+server.ts:
- Around line 41-49: Update the download response headers in the resolution
handler to use application/pdf instead of the uploaded mime value, add
X-Content-Type-Options: nosniff, and include an RFC 5987-encoded filename*
parameter alongside the existing sanitized filename. Keep filename safe for
ASCII compatibility while encoding non-ASCII names for reliable browser
downloads; MIME validation remains in createResolution.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: e0237f59-6a31-4bdd-9dd3-bf5372545872
⛔ Files ignored due to path filters (2)
bun.lockis excluded by!**/*.lockpackage-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (17)
.trivyignoremessages/de.jsonmessages/en.jsonpackage.jsonprisma/migrations/20260731000000_add_resolution_downloads/migration.sqlprisma/schema.prismaschema.graphqlsrc/api/abilities/abilities.tssrc/api/abilities/entities/resolution.tssrc/api/resolvers/api.tssrc/api/resolvers/modules/resolution.tssrc/lib/queries/conferenceResolutionsQuery.tssrc/routes/(authenticated)/dashboard/[conferenceId]/stages/Common/Certificate.sveltesrc/routes/(authenticated)/management/[conferenceId]/configuration/+page.server.tssrc/routes/(authenticated)/management/[conferenceId]/configuration/+page.sveltesrc/routes/(authenticated)/management/[conferenceId]/configuration/ResolutionManager.sveltesrc/routes/api/resolution/[resolutionId]/+server.ts
|
|
||
| const content = await toDataURL(args.file); | ||
| const fileName = args.file.name || 'resolution.pdf'; | ||
|
|
||
| return await db.resolution.create({ | ||
| data: { | ||
| conferenceId: args.conferenceId, | ||
| committeeId: args.committeeId ?? null, | ||
| title: args.title?.trim() || fileName, | ||
| fileName, | ||
| content | ||
| }, | ||
| ...query | ||
| }); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Locate every size/MIME restriction applied to resolution uploads.
rg -n -C6 'createResolution|MAX_|application/pdf|\.size\b' --glob '!**/node_modules/**' \
src/api/resolvers/modules/resolution.ts \
"src/routes/(authenticated)/management/[conferenceId]/configuration"Repository: DeutscheModelUnitedNations/munify-delegator
Length of output: 12495
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== resolution resolver outline =="
ast-grep outline src/api/resolvers/modules/resolution.ts --view expanded | sed -n '1,180p' || true
echo
echo "== resolution resolver relevant lines =="
nl -ba src/api/resolvers/modules/resolution.ts | sed -n '1,150p'
echo
echo "== route resolution echo handler candidates =="
fd -a '^\+server\.ts$' src/routes | rg 'api/resolution|resolution' || true
echo
echo "== resolution download/API handler contents =="
for f in $(fd '^\+server\.ts$' src/routes | rg 'api/resolution|resolution'); do
echo "-- $f"
nl -ba "$f" | sed -n '1,160p'
done
echo
echo "== all createResolution calls/usages =="
rg -n "createResolution|CreateResolution" src --glob '!**/node_modules/**'
echo
echo "== file upload helpers in resolver =="
rg -n "toDataURL|Buffer|Buffer.from|data:image|base64|type" src/api/resolvers/modules/resolution.ts src/api/resolvers -g '*.ts' | sed -n '1,220p'Repository: DeutscheModelUnitedNations/munify-delegator
Length of output: 407
🏁 Script executed:
#!/bin/bash
set -u
echo "== resolution resolver relevant lines =="
awk '{printf "%6d\t%s\n", NR, $0}' src/api/resolvers/modules/resolution.ts | sed -n '1,180p'
echo
echo "== route resolution handler candidates =="
find src/routes -type f -path '*resolution*' -print | sort
echo
echo "== resolution download/API handler contents =="
for f in $(find src/routes -type f -path '*resolution*' -print | sort); do
echo "-- $f"
awk '{printf "%6d\t%s\n", NR, $0}' "$f" | sed -n '1,180p'
done
echo
echo "== all createResolution calls/usages =="
rg -n "createResolution|CreateResolution" src --glob '!**/node_modules/**' || true
echo
echo "== file upload helpers in resolver =="
rg -n "toDataURL|Buffer|fromData|data:image|base64|type" src/api/resolvers/modules/resolution.ts src/api/resolvers -g '*.ts' | sed -n '1,220p' || true
echo
echo "== repository search for GRAPHQL_FILE_SIZE_LIMIT / upload constraints =="
rg -n "GRAPHQL_FILE_SIZE_LIMIT|file size|resolutionUploadTooLarge|resolutionUploadOnlyPdf|application/pdf|createResolution" src --glob '!**/node_modules/**' | sed -n '1,260p' || trueRepository: DeutscheModelUnitedNations/munify-delegator
Length of output: 50400
🏁 Script executed:
#!/bin/bash
set -u
echo "== resolution resolver relevant lines =="
awk '{printf "%6d\t%s\n", NR, $0}' src/api/resolvers/modules/resolution.ts | sed -n '1,180p'
echo
echo "== small resolution-related files =="
find src/routes src/api -type f \( -path '*resolution*' -o -name 'api.ts' \) -print | sort | while read -r f; do
if [ "$(wc -l < "$f")" -le 200 ]; then
echo "-- $f ($(wc -l < "$f") lines)"
fi
done
echo
echo "== key resolution-related files =="
while read -r f; do
echo "-- $f ($(wc -l < "$f") lines)"
awk '{printf "%6d\t%s\n", NR, $0}' "$f" | sed -n '1,220p'
done < <(find src/routes src/api -type f \( -path '*resolution*' -o -name 'api.ts' -o -name '+page.server.ts' -o -name 'form-schema.ts' \) -print | sort | awk 'FILENAME=="src/routes/(authenticated)/management/[conferenceId]/configuration/+page.server.ts"*{if(line<80)print; next} {start=1} /^\//{next} 1' | head -20)Repository: DeutscheModelUnitedNations/munify-delegator
Length of output: 7353
🏁 Script executed:
#!/bin/bash
set -u
echo "== related file contents =="
for f in src/api/abilities/entities/resolution.ts src/api/resolvers/api.ts src/routes/api/resolution/'[resolutionId]'/+server.ts 'src/routes/(authenticated)/management/[conferenceId]/configuration/+page.server.ts'; do
if [ -f "$f" ]; then
echo "-- $f ($(wc -l < "$f") lines)"
awk '{printf "%6d\t%s\n", NR, $0}' "$f" | sed -n '1,220p'
fi
done
echo
echo "== file upload service =="
fd '^fileToDataURL\.(ts|js)$' src -x sh -c 'echo "-- $1"; awk '\''{printf "%6d\t%s\n", NR, $0}'\'' "$1" | sed -n '\''1,220p'\''' sh {}
echo
echo "== upload constraints and usages =="
rg -n "createResolution|CreateResolution|MIME|application/pdf|resolutionUpload|uploadError|FileUploadLimit|MAX|10_000_000|1024|resolution.*content|Content-Type" src --glob '!**/node_modules/**' || true
echo
echo "== focused route files under authenticated management configuration =="
find 'src/routes/(authenticated)/management/[conferenceId]/configuration' -type f -print | sort | while read -r f; do
echo "-- $f ($(wc -l < "$f") lines)"
awk '{printf "%6d\t%s\n", NR, $0}' "$f" | sed -n '1,260p'
doneRepository: DeutscheModelUnitedNations/munify-delegator
Length of output: 50400
Validate resolution uploads in createResolution.
createResolution accepts any File and writes it through toDataURL(args.file) without size or MIME checks. The 10 MB PDF rule in the management page only covers one server action and can be bypassed directly through the authenticated GraphQL mutation. Reject non-PDF files and files over 10 MB in the resolver before storing content.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/api/resolvers/modules/resolution.ts` around lines 100 - 113, Update
createResolution to validate args.file before calling toDataURL or
db.resolution.create: reject uploads whose MIME type is not application/pdf and
uploads exceeding 10 MB, using the resolver’s existing error/validation
mechanism. Only proceed to derive fileName, encode content, and persist the
resolution after both checks pass.
| type ResolutionGroup = { committeeName: string | null; items: (typeof resolutions)[number][] }; | ||
| const groupedResolutions = $derived.by(() => { | ||
| const groups: ResolutionGroup[] = []; | ||
| const indexByKey: Record<string, number> = {}; | ||
| for (const resolution of resolutions) { | ||
| const key = resolution.committee?.id ?? '__none__'; | ||
| if (!(key in indexByKey)) { | ||
| indexByKey[key] = groups.length; | ||
| groups.push({ | ||
| committeeName: resolution.committee | ||
| ? `${resolution.committee.name} (${resolution.committee.abbreviation})` | ||
| : null, | ||
| items: [] | ||
| }); | ||
| } | ||
| groups[indexByKey[key]].items.push(resolution); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Carry the committee id into the group and key the each block with it.
The group object keeps only the formatted committee name. Line 217 then uses that name as the keyed-each key. Two committees with the same name and abbreviation produce the same key, and Svelte throws a duplicate key error for the keyed each block. Committee.name has no unique constraint in prisma/schema.prisma. The grouping already computes a stable id at line 84.
🐛 Proposed fix
- type ResolutionGroup = { committeeName: string | null; items: (typeof resolutions)[number][] };
+ type ResolutionGroup = {
+ key: string;
+ committeeName: string | null;
+ items: (typeof resolutions)[number][];
+ };
const groupedResolutions = $derived.by(() => {
const groups: ResolutionGroup[] = [];
const indexByKey: Record<string, number> = {};
for (const resolution of resolutions) {
const key = resolution.committee?.id ?? '__none__';
if (!(key in indexByKey)) {
indexByKey[key] = groups.length;
groups.push({
+ key,
committeeName: resolution.committee
? `${resolution.committee.name} (${resolution.committee.abbreviation})`
: null,
items: []
});
}
groups[indexByKey[key]].items.push(resolution);
}Then update the each block at line 217:
- {`#each` groupedResolutions as group (group.committeeName ?? '__none__')}
+ {`#each` groupedResolutions as group (group.key)}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| type ResolutionGroup = { committeeName: string | null; items: (typeof resolutions)[number][] }; | |
| const groupedResolutions = $derived.by(() => { | |
| const groups: ResolutionGroup[] = []; | |
| const indexByKey: Record<string, number> = {}; | |
| for (const resolution of resolutions) { | |
| const key = resolution.committee?.id ?? '__none__'; | |
| if (!(key in indexByKey)) { | |
| indexByKey[key] = groups.length; | |
| groups.push({ | |
| committeeName: resolution.committee | |
| ? `${resolution.committee.name} (${resolution.committee.abbreviation})` | |
| : null, | |
| items: [] | |
| }); | |
| } | |
| groups[indexByKey[key]].items.push(resolution); | |
| type ResolutionGroup = { | |
| key: string; | |
| committeeName: string | null; | |
| items: (typeof resolutions)[number][]; | |
| }; | |
| const groupedResolutions = $derived.by(() => { | |
| const groups: ResolutionGroup[] = []; | |
| const indexByKey: Record<string, number> = {}; | |
| for (const resolution of resolutions) { | |
| const key = resolution.committee?.id ?? '__none__'; | |
| if (!(key in indexByKey)) { | |
| indexByKey[key] = groups.length; | |
| groups.push({ | |
| key, | |
| committeeName: resolution.committee | |
| ? `${resolution.committee.name} (${resolution.committee.abbreviation})` | |
| : null, | |
| items: [] | |
| }); | |
| } | |
| groups[indexByKey[key]].items.push(resolution); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/routes/`(authenticated)/dashboard/[conferenceId]/stages/Common/Certificate.svelte
around lines 79 - 94, Carry the stable committee identifier from the key
computed in groupedResolutions into each ResolutionGroup, using a nullable value
for resolutions without a committee. Update the keyed each block to use this
committee id rather than the formatted committeeName, while preserving the
existing display name and grouping behavior.
| // PDF-only, max 10 MB each - mirrors the base document upload limits. | ||
| for (const file of files) { | ||
| const isPdf = file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf'); | ||
| if (!isPdf) { | ||
| return fail(400, { uploadError: m.resolutionUploadOnlyPdf() }); | ||
| } | ||
| if (file.size > 10_000_000) { | ||
| return fail(400, { uploadError: m.resolutionUploadTooLarge() }); | ||
| } | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Tighten the PDF check and cap the upload count.
Two gaps exist in this validation block:
- The check uses
||. A file with any MIME type passes if the name ends with.pdf. The stored MIME is later used as theContent-Typeof the download response insrc/routes/api/resolution/[resolutionId]/+server.ts(line 41). Require the MIME type, or normalize the stored type toapplication/pdf. - The action accepts an unbounded number of files. Each file may be 10 MB, and base64 encoding expands it by about one third. A single request can therefore allocate hundreds of megabytes and persist them. Add a maximum file count.
🔒️ Proposed fix
// PDF-only, max 10 MB each - mirrors the base document upload limits.
+ if (files.length > 20) {
+ return fail(400, { uploadError: m.resolutionUploadTooManyFiles() });
+ }
for (const file of files) {
- const isPdf = file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf');
+ const isPdf = file.type === 'application/pdf' && file.name.toLowerCase().endsWith('.pdf');
if (!isPdf) {
return fail(400, { uploadError: m.resolutionUploadOnlyPdf() });
}Add the matching resolutionUploadTooManyFiles key to messages/de.json and messages/en.json.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/routes/`(authenticated)/management/[conferenceId]/configuration/+page.server.ts
around lines 254 - 263, Update the resolution upload validation loop to require
file.type === 'application/pdf' rather than accepting a filename extension with
a different MIME type, or normalize accepted files to application/pdf before
persistence. Add a maximum file-count check before iterating, return fail(400, {
uploadError: m.resolutionUploadTooManyFiles() }) when exceeded, and add the
matching resolutionUploadTooManyFiles message key to the English and German
message files.
| for (const file of files) { | ||
| await CreateResolutionMutation.mutate( | ||
| { | ||
| conferenceId, | ||
| committeeId: typeof committeeId === 'string' && committeeId ? committeeId : undefined, | ||
| title: undefined, | ||
| file | ||
| }, | ||
| { event } | ||
| ); | ||
| } | ||
|
|
||
| cache.markStale(); | ||
|
|
||
| return { uploaded: files.length }; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Report partial upload failures.
The loop persists resolutions one by one without error handling. If a mutation fails on the third file, the first two remain stored, and the client receives a generic error. The user then re-uploads and creates duplicates. Collect per-file results and return the successful and failed file names.
♻️ Proposed fix
- for (const file of files) {
- await CreateResolutionMutation.mutate(
- {
- conferenceId,
- committeeId: typeof committeeId === 'string' && committeeId ? committeeId : undefined,
- title: undefined,
- file
- },
- { event }
- );
- }
-
- cache.markStale();
-
- return { uploaded: files.length };
+ const uploaded: string[] = [];
+ const failed: string[] = [];
+ for (const file of files) {
+ try {
+ await CreateResolutionMutation.mutate(
+ {
+ conferenceId,
+ committeeId: typeof committeeId === 'string' && committeeId ? committeeId : undefined,
+ title: undefined,
+ file
+ },
+ { event }
+ );
+ uploaded.push(file.name);
+ } catch {
+ failed.push(file.name);
+ }
+ }
+
+ cache.markStale();
+
+ if (failed.length > 0) {
+ return fail(500, { uploadError: m.resolutionUploadPartialError({ files: failed.join(', ') }) });
+ }
+
+ return { uploaded: uploaded.length };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/routes/`(authenticated)/management/[conferenceId]/configuration/+page.server.ts
around lines 265 - 279, Update the file-upload loop around
CreateResolutionMutation.mutate to handle each file independently, collecting
successful and failed file names instead of aborting on the first mutation
error. Return both success and failure details to the client, avoid marking the
cache stale when no upload succeeds, and preserve the existing conference and
committee mutation inputs.
| async function handleFilesSelected(event: Event) { | ||
| const input = event.currentTarget as HTMLInputElement; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Replace the as casts with typed event parameters.
The coding guidelines forbid type casting. Svelte 5 provides the element type on currentTarget when you type the handler parameter. Type the parameters instead of asserting inside the function.
♻️ Proposed fix
- async function handleFilesSelected(event: Event) {
- const input = event.currentTarget as HTMLInputElement;
+ async function handleFilesSelected(
+ event: Event & { currentTarget: EventTarget & HTMLInputElement }
+ ) {
+ const input = event.currentTarget;
if (!input.files || input.files.length === 0) return;- async function saveTitle(resolution: Resolution, event: FocusEvent) {
- const target = event.currentTarget as HTMLInputElement;
+ async function saveTitle(
+ resolution: Resolution,
+ event: FocusEvent & { currentTarget: EventTarget & HTMLInputElement }
+ ) {
+ const target = event.currentTarget;- async function changeCommittee(resolution: Resolution, event: Event) {
- const value = (event.currentTarget as HTMLSelectElement).value;
+ async function changeCommittee(
+ resolution: Resolution,
+ event: Event & { currentTarget: EventTarget & HTMLSelectElement }
+ ) {
+ const value = event.currentTarget.value;As per coding guidelines: "Never use type casting (as Type) in TypeScript - investigate and fix type definitions at the source instead".
Also applies to: 105-106, 119-120
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/routes/`(authenticated)/management/[conferenceId]/configuration/ResolutionManager.svelte
around lines 66 - 67, Remove the HTMLInputElement and related as-casts from
handleFilesSelected and the other affected handlers, and type each event
parameter so currentTarget is inferred as the correct element type. Update the
handler signatures at the referenced locations while preserving their existing
behavior.
Source: Coding guidelines
| <Modal bind:open={deleteModalOpen} title={m.resolutionDeleteTitle()}> | ||
| <p class="py-4">{m.resolutionDeleteConfirm({ title: deleteTarget.title })}</p> | ||
| {#snippet action()} | ||
| <button class="btn" onclick={() => (deleteModalOpen = false)}> | ||
| {m.cancel()} | ||
| </button> | ||
| <button class="btn btn-error" onclick={confirmDelete}> | ||
| <i class="fas fa-trash mr-2"></i> | ||
| {m.delete()} | ||
| </button> | ||
| {/snippet} | ||
| </Modal> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Modal buttons submit the conference settings form. ResolutionManager renders inside <Form {form} ... action="?/updateSettings">, and its Modal buttons have no type attribute. The browser therefore treats them as submit buttons, so Cancel and Delete both post the settings form. All other Modals on the configuration page sit outside that form element.
src/routes/(authenticated)/management/[conferenceId]/configuration/ResolutionManager.svelte#L261-L272: addtype="button"to the Cancel button and the Delete button in theactionsnippet.src/routes/(authenticated)/management/[conferenceId]/configuration/+page.svelte#L802-L803: move<ResolutionManager ... />out of theFormelement, into a separate Documents-tab wrapper after the closing</Form>tag.
📍 Affects 2 files
src/routes/(authenticated)/management/[conferenceId]/configuration/ResolutionManager.svelte#L261-L272(this comment)src/routes/(authenticated)/management/[conferenceId]/configuration/+page.svelte#L802-L803
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/routes/`(authenticated)/management/[conferenceId]/configuration/ResolutionManager.svelte
around lines 261 - 272, The ResolutionManager modal currently sits inside the
conference settings form, causing both modal actions to submit it. In
src/routes/(authenticated)/management/[conferenceId]/configuration/ResolutionManager.svelte
lines 261-272, add type="button" to the Cancel and Delete buttons; in
src/routes/(authenticated)/management/[conferenceId]/configuration/+page.svelte
lines 802-803, move the ResolutionManager component out of the Form and into a
separate Documents-tab wrapper after the closing Form tag.
The docker-build image scan failed on tar (CVE-2026-59873/59874) and brace-expansion (CVE-2026-13149/14257) reported from package.json files *inside the image*. These are not app dependencies: the Dockerfile installs Node.js for Prisma engine generation, and Node ships a bundled npm whose vendored node_modules carry their own tar and brace-expansion. The app is built and run with Bun (bun.lock has brace-expansion 5.0.8 and no tar), and npm is never invoked at build or runtime, so these copies are unreachable. Suppressed with justification, matching the existing base-image/toolchain entries. The repo fs-scan (security job) already passes on the app deps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsRXmXgAXefuM4BSYAeAAj
…ge scan The docker-build image scan (stricter than the fs scan - it reads the fully resolved node_modules) surfaced newly-published CVEs. Two classes: Real app dependencies - bumped to fixed versions via overrides: - brace-expansion 5.0.8 -> 5.0.9 (CVE-2026-69152) - fast-uri 3.1.4 -> 3.1.5 (CVE-2026-18446) - undici 7.28.0 -> 7.29.0 (CVE-2026-13697) Base-image npm bundle (tar, brace-expansion, ip-address, ... vendored inside the Node.js-shipped npm at usr/lib/node_modules/npm) - not app deps, npm is never invoked. Rather than chase each new npm-vendored CVE in .trivyignore, the image scans now `skip-dirs` that tree. Kept the known-CVE .trivyignore entries (plus ip-address CVE-2026-69192) as a belt-and-suspenders fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsRXmXgAXefuM4BSYAeAAj
Summary
This PR adds a complete resolution document management system that allows conference organizers to upload, manage, and organize adopted resolution PDFs, which participants can then download from the after-conference screen.
Key Changes
Backend
src/api/resolvers/modules/resolution.ts): New resolver module with mutations for creating, updating, and deleting resolutions, plus queries for listing and fetching individual resolutionsprisma/schema.prisma): NewResolutionmodel storing resolution metadata (title, filename, base64-encoded PDF content) with relationships toConferenceand optionalCommitteeResolutiontable with proper foreign key constraintssrc/api/abilities/entities/resolution.ts): CASL ability rules allowing conference participants to list/read resolutions, and only project management team members to create/update/delete themsrc/routes/api/resolution/[resolutionId]/+server.ts): Server route to stream resolution PDFs for download with proper content-disposition headers and access controlFrontend
Resolution Manager Component (
src/routes/(authenticated)/management/[conferenceId]/configuration/ResolutionManager.svelte): New UI component for conference admins to:Configuration Page (
src/routes/(authenticated)/management/[conferenceId]/configuration/+page.server.ts):uploadResolutionsserver action to handle file uploads with validation (PDF-only, max 10MB per file)ConfigurationResolutionsQueryto fetch resolutions for managementCertificate/After-Conference Screen (
src/routes/(authenticated)/dashboard/[conferenceId]/stages/Common/Certificate.svelte):conferenceResolutionsQueryto fetch and display downloadable resolutionsGraphQL Query (
src/lib/queries/conferenceResolutionsQuery.ts): New query for fetching resolutions with committee informationInternationalization
Schema Updates
schema.graphqlwith newResolutiontype and mutations (createResolution,updateResolution,deleteResolution)ConferenceandCommitteeinput/output typesImplementation Details
readability used by the GraphQL layerhttps://claude.ai/code/session_01LsRXmXgAXefuM4BSYAeAAj
Summary by CodeRabbit
New Features
Security