Skip to content

Add profile picture upload, crop, and profile rendering#592

Draft
benckx with Copilot wants to merge 20 commits into
masterfrom
copilot/add-profile-picture-upload
Draft

Add profile picture upload, crop, and profile rendering#592
benckx with Copilot wants to merge 20 commits into
masterfrom
copilot/add-profile-picture-upload

Conversation

Copilot AI commented May 13, 2026

Copy link
Copy Markdown
Contributor

This adds first-class profile picture support for user profiles. Users can choose and crop a square image in settings, upload it to the CDN under a profile-scoped path, and have it rendered on their public profile.

Changes Made

  • Settings UI

    • Added profile picture controls to the user settings page.
    • Added an in-browser square crop editor with zoom and drag support.
    • Kept the newer sessions/settings changes from master while resolving merge conflicts.
  • Upload pipeline

    • Added an authenticated /api/user/settings/profile-picture upload endpoint.
    • Accepts PNG/JPEG, enforces the 500 KB limit, normalizes uploads to a 100x100 square, and uploads to:
      • <profile>/profile-pictures/<userId>.<original extension>
    • Reuses the runtime application profile from app config so environments such as dev, prod, and local-backup do not share the same CDN folder.
    • Validates the profile segment before using it in the CDN path.
    • Returns the resolved public CDN URL for immediate UI refresh.
  • Persistence and profile data

    • Added a profile_picture_extension column via Liquibase.
    • Stores only the extension in DB; the public URL is derived from profile + userId + extension.
    • Extended profile/settings DTO and service logic so both the settings page and public profile can resolve the current image.
  • Public profile rendering

    • Added profile picture rendering to the profile header when present.
    • Escaped the rendered alt text.
    • Preserved compatibility with the latest renderer changes from master during conflict resolution.
  • Merge resolution follow-up

    • Resolved the latest conflicts against master and kept the profile picture feature intact while preserving newer upstream changes.
    • Updated the Liquibase changelog merge so master's new 0105/0106/0107 changesets remain in place, and the profile-picture migration is applied as 0108.
    • Kept master's appConfig.salt refactor in UserService while retaining the profile field, and added the new required symmetricKey/salt AppConfig parameters to UserProfilePictureServiceTest.

Testing

  • JAVA_HOME=/usr/lib/jvm/temurin-21-jdk-amd64 ./gradlew --no-daemon liquibaseGeneration :webapp-service-layer:test --tests io.elephantchess.servicelayer.services.UserServiceTest --tests io.elephantchess.servicelayer.services.UserProfilePictureServiceTest --tests io.elephantchess.db.services.UserDaoServiceTest :webapp:test --tests io.elephantchess.webapp.rendering.UserProfilePageRendererTest
  • cd webapp/src/test/js && node --test
  • ✅ Parallel validation completed successfully with no CodeQL alerts

Example storage/public path convention:

CDN object key: <profile>/profile-pictures/<userId>.png
Public URL:     https://cdn.elephantchess.io/<profile>/profile-pictures/<userId>.png

Copilot AI linked an issue May 13, 2026 that may be closed by this pull request
Copilot AI changed the title [WIP] Add user profile picture upload feature Add profile picture upload, crop, and profile rendering May 13, 2026
Copilot finished work on behalf of benckx May 13, 2026 07:46
Copilot AI requested a review from benckx May 13, 2026 07:46

@benckx benckx left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should create different folders on the CDN by "profile" (dev, prod, local-backup) on the CDN (because we only have 1). We need to add this folder to the key. Re-use the profile used in "parse configs" in the Main.

Copilot AI commented May 13, 2026

Copy link
Copy Markdown
Contributor Author

I think we should create different folders on the CDN by "profile" (dev, prod, local-backup) on the CDN (because we only have 1). We need to add this folder to the key. Re-use the profile used in "par...

Updated in 12e7966. Profile pictures now use the runtime app profile as a CDN path prefix, so keys/URLs resolve like <profile>/profile-pictures/<userId>.<ext> (for example local-backup/profile-pictures/...). I also added profile-segment validation around that path handling. Screenshot: /tmp/elephantchess-profile-picture-demo/profile-picture-demo.png.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • accounts.google.com
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=network --no-sandbox --use-angle=swiftshader-webgl --crashpad-handler-pid=5503 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/org.chromium.Chromium.scoped_dir.AKQgrZ --change-stack-guard-on-fork=enable --shared-files=network_parent_dirs_pipe:100,v8_context_snapshot_data:101 --field-trial-handle=3,i,9502094821879038207,17100798646544470801,262144 --disable-features=PaintHolding --variations-seed-version --pseudonymization-salt-handle=7,i,10809956789252372990,6039235179098036239,4 --trace-process-track-uuid=3190708989122997041 (dns block)
    • Triggering command: /usr/bin/chromium-browser chromium-browser --headless=new --no-sandbox --disable-gpu --allow-file-access-from-files --screenshot=/tmp/elephantchess-profile-picture-demo/profile-picture-demo.png --window-size=1400,1800 --run-all-compositor-stages-before-draw file:///tmp/elephantchess-profile-picture-demo/demo.html (dns block)
  • cdn.elephantchess.io
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=network --no-sandbox --use-angle=swiftshader-webgl --crashpad-handler-pid=5503 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/org.chromium.Chromium.scoped_dir.AKQgrZ --change-stack-guard-on-fork=enable --shared-files=network_parent_dirs_pipe:100,v8_context_snapshot_data:101 --field-trial-handle=3,i,9502094821879038207,17100798646544470801,262144 --disable-features=PaintHolding --variations-seed-version --pseudonymization-salt-handle=7,i,10809956789252372990,6039235179098036239,4 --trace-process-track-uuid=3190708989122997041 (dns block)
    • Triggering command: /usr/bin/chromium-browser chromium-browser --headless=new --no-sandbox --disable-gpu --allow-file-access-from-files --screenshot=/tmp/elephantchess-profile-picture-demo/profile-picture-demo.png --window-size=1400,1800 --run-all-compositor-stages-before-draw file:///tmp/elephantchess-profile-picture-demo/demo.html (dns block)
  • clients2.google.com
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=network --no-sandbox --use-angle=swiftshader-webgl --crashpad-handler-pid=5503 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/org.chromium.Chromium.scoped_dir.AKQgrZ --change-stack-guard-on-fork=enable --shared-files=network_parent_dirs_pipe:100,v8_context_snapshot_data:101 --field-trial-handle=3,i,9502094821879038207,17100798646544470801,262144 --disable-features=PaintHolding --variations-seed-version --pseudonymization-salt-handle=7,i,10809956789252372990,6039235179098036239,4 --trace-process-track-uuid=3190708989122997041 (dns block)
    • Triggering command: /usr/bin/chromium-browser chromium-browser --headless=new --no-sandbox --disable-gpu --allow-file-access-from-files --screenshot=/tmp/elephantchess-profile-picture-demo/profile-picture-demo.png --window-size=1400,1800 --run-all-compositor-stages-before-draw file:///tmp/elephantchess-profile-picture-demo/demo.html (dns block)
  • dl.google.com
    • Triggering command: /usr/lib/jvm/temurin-21-jdk-amd64/bin/java /usr/lib/jvm/temurin-21-jdk-amd64/bin/java --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=java.base/java.nio.charset=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens=java.xml/javax.xml.namespace=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED -XX:MaxMetaspaceSize=384m -XX:&#43;HeapDumpOnOutOfMemoryError -Xms256m -Xmx512m -Dfile.encoding=UTF-8 -Duser.country -Duser.language=en (dns block)
  • redirector.gvt1.com
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=network --no-sandbox --use-angle=swiftshader-webgl --crashpad-handler-pid=5503 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/org.chromium.Chromium.scoped_dir.AKQgrZ --change-stack-guard-on-fork=enable --shared-files=network_parent_dirs_pipe:100,v8_context_snapshot_data:101 --field-trial-handle=3,i,9502094821879038207,17100798646544470801,262144 --disable-features=PaintHolding --variations-seed-version --pseudonymization-salt-handle=7,i,10809956789252372990,6039235179098036239,4 --trace-process-track-uuid=3190708989122997041 (dns block)
    • Triggering command: /usr/bin/chromium-browser chromium-browser --headless=new --no-sandbox --disable-gpu --allow-file-access-from-files --screenshot=/tmp/elephantchess-profile-picture-demo/profile-picture-demo.png --window-size=1400,1800 --run-all-compositor-stages-before-draw file:///tmp/elephantchess-profile-picture-demo/demo.html (dns block)
  • www.google.com
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=network --no-sandbox --use-angle=swiftshader-webgl --crashpad-handler-pid=5503 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/org.chromium.Chromium.scoped_dir.AKQgrZ --change-stack-guard-on-fork=enable --shared-files=network_parent_dirs_pipe:100,v8_context_snapshot_data:101 --field-trial-handle=3,i,9502094821879038207,17100798646544470801,262144 --disable-features=PaintHolding --variations-seed-version --pseudonymization-salt-handle=7,i,10809956789252372990,6039235179098036239,4 --trace-process-track-uuid=3190708989122997041 (dns block)
    • Triggering command: /usr/bin/chromium-browser chromium-browser --headless=new --no-sandbox --disable-gpu --allow-file-access-from-files --screenshot=/tmp/elephantchess-profile-picture-demo/profile-picture-demo.png --window-size=1400,1800 --run-all-compositor-stages-before-draw file:///tmp/elephantchess-profile-picture-demo/demo.html (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot finished work on behalf of benckx May 13, 2026 13:02
Copilot AI requested a review from benckx May 13, 2026 13:02
@benckx

benckx commented May 26, 2026

Copy link
Copy Markdown
Owner

@copilot resolve the merge conflicts in this pull request

Copilot AI commented May 26, 2026

Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

Resolved and merged in a1b99c9. I fetched origin/master, resolved the four conflicted files, kept the profile-picture CDN profile-prefix changes, and validated the merge with liquibaseGeneration plus the focused profile-picture tests. Screenshot: /tmp/elephantchess-merge-conflict-resolution/merge-resolution-demo.png.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • android.clients.google.com
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=network --no-sandbox --use-angle=swiftshader-webgl --crashpad-handler-pid=7058 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/org.chromium.Chromium.scoped_dir.RDVeHI --change-stack-guard-on-fork=enable --shared-files=network_parent_dirs_pipe:100,v8_context_snapshot_data:101 --field-trial-handle=3,i,11205980070010861969,6184571711548537066,262144 --disable-features=PaintHolding --variations-seed-version --pseudonymization-salt-handle=7,i,9198368288855447448,15034705414902014784,4 --trace-process-track-uuid=3190708989122997041 (dns block)
    • Triggering command: /usr/bin/chromium-browser chromium-browser --headless=new --no-sandbox --disable-gpu --allow-file-access-from-files --screenshot=/tmp/elephantchess-merge-conflict-resolution/merge-resolution-demo.png --window-size=1400,1800 --run-all-compositor-stages-before-draw file:///tmp/elephantchess-merge-conflict-resolution/merge-resolution-demo.html (dns block)
  • ktor.io
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node /home/REDACTED/work/_temp/ghcca-node/node/bin/node --enable-source-maps /home/REDACTED/work/_temp/copilot-developer-action-main/dist/index.js (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot finished work on behalf of benckx May 26, 2026 14:00
@benckx

benckx commented May 27, 2026

Copy link
Copy Markdown
Owner

@copilot resolve the merge conflicts in this pull request

Copilot finished work on behalf of benckx May 28, 2026 13:12
@benckx

benckx commented Jun 2, 2026

Copy link
Copy Markdown
Owner

@copilot resolve the merge conflicts in this pull request

…-picture-upload

# Conflicts:
#	webapp-dao-migration/src/main/resources/liquibase-changelog-generation.xml
#	webapp-dao-migration/src/main/resources/liquibase-changelog.xml

Copilot AI commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

Resolved in ce5f016. I merged the latest master, fixed the Liquibase changelog conflicts by keeping master's new 0101/0102 changesets and moving the profile-picture migration to 0103, then reran JAVA_HOME=/usr/lib/jvm/temurin-21-jdk-amd64 ./gradlew --no-daemon liquibaseGeneration :webapp-service-layer:test --tests io.elephantchess.servicelayer.services.UserProfilePictureServiceTest :webapp:test --tests io.elephantchess.webapp.rendering.UserProfilePageRendererTest.

Copilot finished work on behalf of benckx June 2, 2026 06:48
}
}

suspend fun fetchPublicProfile(username: String): Record6<String, String, String?, String?, Int, String?>? {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot instead of Record6, map to User (jOOQ generated pojo)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 738980b. fetchPublicProfile() now maps directly to the generated jOOQ User POJO, UserService.fetchProfile() reads named properties from that POJO, and I added a focused regression test for the mapping.

UI.pushInfoNotification('Profile picture successfully updated!', UI_NOTIFICATION_TIMEOUT);
});

fetch(PROFILE_PICTURE_UPLOAD_URL, {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot we should we able to re-use postAndHandle (api.js)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0064e81. postAndHandle now supports FormData payloads without forcing a JSON content type, and the profile-picture upload now reuses that helper directly.

Copilot finished work on behalf of benckx June 2, 2026 09:59
@benckx

benckx commented Jun 8, 2026

Copy link
Copy Markdown
Owner

@copilot resolve the merge conflicts in this pull request

Copilot AI commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

Resolved in bde44b3. I merged the latest origin/master, kept master's new 0103 analytics-index Liquibase changeset, moved the profile-picture migration to 0104, and re-ran JAVA_HOME=/usr/lib/jvm/temurin-21-jdk-amd64 ./gradlew --no-daemon liquibaseGeneration :webapp-service-layer:test --tests io.elephantchess.servicelayer.services.UserServiceTest --tests io.elephantchess.servicelayer.services.UserProfilePictureServiceTest :webapp:test --tests io.elephantchess.webapp.rendering.UserProfilePageRendererTest plus cd webapp/src/test/js && node --test.

Copilot finished work on behalf of benckx June 8, 2026 07:15
@benckx

benckx commented Jun 9, 2026

Copy link
Copy Markdown
Owner

@copilot resolve the merge conflicts in this pull request

Copilot AI commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

Resolved in d640f48. I merged the latest origin/master, kept master's new 0104 user_agent text migration changeset, moved the profile-picture migration to 0105, regenerated liquibase-changelog-generation.xml, and re-ran liquibaseGeneration plus the focused tests (UserServiceTest, UserProfilePictureServiceTest, UserProfilePageRendererTest).

Copilot finished work on behalf of benckx June 9, 2026 07:37
@benckx

benckx commented Jun 15, 2026

Copy link
Copy Markdown
Owner

@copilot resolve the merge conflicts in this pull request

Copilot AI commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

Resolved in 26739c4. I merged the latest origin/master and fixed five conflicts: kept master's new 0105/0106/0107 Liquibase changesets and moved the profile-picture migration to 0108 (regenerating liquibase-changelog-generation.xml); kept master's appConfig.salt refactor in UserService while retaining the profile field; kept both the fetchPublicProfile and fetchRatingSummary tests; and added master's new required symmetricKey/salt AppConfig params to UserProfilePictureServiceTest. Re-ran the focused tests (UserServiceTest, UserProfilePictureServiceTest, UserDaoServiceTest, UserProfilePageRendererTest) plus the JS tests.

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.

profile picture

2 participants