Skip to content

feat: add check-in progress photos (front, back, side)#1533

Merged
CodeWithCJ merged 6 commits into
CodeWithCJ:mainfrom
zeusal:feature/issue-229-progress-photos
Jun 19, 2026
Merged

feat: add check-in progress photos (front, back, side)#1533
CodeWithCJ merged 6 commits into
CodeWithCJ:mainfrom
zeusal:feature/issue-229-progress-photos

Conversation

@zeusal

@zeusal zeusal commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Resolves #229. Add the ability to upload and track progress photos from the check-in screen, with three fixed angle slots (front, back, side) per day, plus the backend storage, retrieval, and security hardening.

Frontend:

  • New CheckInPhotos UI on the check-in screen with front/back/side slots (upload / replace / remove) and en/es translations.

Backend:

  • New check_in_photos table, routes, service, schemas, and upload middleware.
  • Photos stored under uploads/check-in/<userId>/<date>/<type>.<ext>; only the relative path is persisted, so records stay portable across deployments.
  • Uploads validated by real image magic bytes (not the spoofable mime type or filename), buffered in memory, and written to a temp file that is atomically promoted only after the DB commit, so a failed insert never orphans a file and a same-name replace never clobbers a valid photo.
  • Photos served through an authenticated, ownership-checked route (RLS-scoped) with X-Content-Type-Options: nosniff; direct static access to /uploads/check-in is blocked.
  • RLS: enable row-level security and add the per-user diary policy for check_in_photos (previously RLS was enabled with no policy, so every insert was denied).
  • DB: point check_in_photos.user_id at public."user" (Better Auth), matching check_in_measurements; uploads failed with a foreign key violation for users absent from the legacy auth.users table. Add the prerequisite PRIMARY KEY on check_in_measurements so the photos foreign key target is valid.

Tip

Help us review and merge your PR faster!
Please ensure you have completed the Checklist below.
For Frontend changes, please run pnpm run validate to check for any errors.
PRs that include tests and clear screenshots are highly preferred!
Note: AI-generated descriptions must be manually edited for conciseness. Do not paste raw AI summaries.

Description

What problem does this PR solve?
(Keep it concise. 1–2 sentences.)

How did you implement the solution?
(Brief technical approach.)

Linked Issue

Linked Issue: Closes #229

How to Test

  1. Check out this branch and start the server (pnpm start in SparkyFitnessServer/).
    The new migrations apply automatically on boot (check_in_measurements PK →
    check_in_photos table → FK repoint to public."user"), and RLS policies are
    re-applied, so an existing DB is upgraded in place.
  2. Start the frontend (pnpm dev in SparkyFitnessFrontend/) and log in.
  3. Navigate to Check-In and find the new Progress Photos section.
  4. Upload a Front, Back, and Side photo. Verify each shows a preview,
    can be replaced, and persists after reloading the page / reopening the date.
  5. Verify files land under SparkyFitnessServer/uploads/check-in/<userId>/<date>/.
  6. Verify a non-image (e.g. a renamed .txt.jpg) is rejected on upload.
  7. (Multi-user) Log in as a second user, add photos for the same date, and confirm
    each user only sees their own — photos are served by id through an
    ownership/RLS-checked route, and /uploads/check-in is not publicly served.

PR Type

  • Issue (bug fix)
  • New Feature
  • Refactor
  • Documentation

Checklist

All PRs:

  • [MANDATORY - ALL] Integrity & License: I certify this is my own work, free of malicious code, and I agree to the License terms.

New features only:

  • [MANDATORY for new feature] Alignment: I have raised a GitHub issue and it was reviewed/approved by maintainers or it was approved on Discord.

Frontend changes (SparkyFitnessFrontend/):

  • [MANDATORY for Frontend changes] Quality: I have run pnpm run validate and it passes.
  • [MANDATORY for Frontend changes] Translations: I have only updated the English (en) translation file.

Backend changes (SparkyFitnessServer/):

  • [MANDATORY for Backend changes] Code Quality: I have run typecheck, lint, and tests. New files use TypeScript, new endpoints have Zod schemas, and new endpoints include tests.
  • [MANDATORY for Backend changes] Database Security: I have updated rls_policies.sql for any new user-specific tables.

UI changes (components, screens, pages):

  • [MANDATORY for UI changes] Screenshots: I have attached Before/After screenshots below.

Mobile changes (SparkyFitnessMobile/):

  • [MANDATORY for Mobile changes] Tested on device or emulator: N/A — no mobile changes.

Screenshots

Before (upstream main) After (this PR)
image image
--- image

Notes for Reviewers

Optional — use this for anything that doesn't fit above: known tradeoffs, areas you'd like specific feedback on, qustions you have or context that helps reviewers.

@github-actions github-actions Bot added backend enhancement New feature or request frontend labels Jun 14, 2026
@github-actions

github-actions Bot commented Jun 14, 2026

Copy link
Copy Markdown

PR Validation Results

Change Detection

  • 🖥️ Frontend changes detected
  • ⚙️ Backend changes detected
  • 🌐 en translation file modified
  • 🗄️ rls_policies.sql modified

✅ All checks passed. Thank you!

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a progress photos feature to the check-in flow, allowing users to upload, replace, and delete front, back, and side photos. The changes span both the frontend (React components, React Query hooks, and translation updates) and the backend (Express routes, multer middleware with magic-byte image validation, Zod schemas, database migrations, and RLS policies). The review feedback highlights a critical timezone-related bug in checkInPhotoService.ts where formatting database DATE values using toISOString().split('T')[0] can cause day shifts, and recommends using a shared date utility library instead.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread SparkyFitnessServer/services/checkInPhotoService.ts
Comment thread SparkyFitnessServer/services/checkInPhotoService.ts
@CodeWithCJ

Copy link
Copy Markdown
Owner

merge the three migration sql into a single file. and also attach some screenshots in the AFTER section of the PR desc

@zeusal zeusal force-pushed the feature/issue-229-progress-photos branch from 1eca85c to 19ac745 Compare June 14, 2026 21:36
@zeusal

zeusal commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

merge the three migration sql into a single file. and also attach some screenshots in the AFTER section of the PR desc

I have tried to do it, can you verify again please.

Resolves CodeWithCJ#229. Add the ability to upload and track progress photos from the
check-in screen, with three fixed angle slots (front, back, side) per day,
plus the backend storage, retrieval, and security hardening.

Frontend:
- New CheckInPhotos UI on the check-in screen with front/back/side slots
  (upload / replace / remove) and English (en) translation strings.

Backend:
- New check_in_photos table, routes, service, schemas, and upload middleware.
- Photos stored under uploads/check-in/<userId>/<date>/<type>.<ext>; only the
  relative path is persisted, so records stay portable across deployments.
- Uploads validated by real image magic bytes (not the spoofable mime type or
  filename), buffered in memory, and written to a temp file that is atomically
  promoted only after the DB commit, so a failed insert never orphans a file
  and a same-name replace never clobbers a valid photo.
- Photos served through an authenticated, ownership-checked route (RLS-scoped)
  with X-Content-Type-Options: nosniff; direct static access to
  /uploads/check-in is blocked.
- RLS: enable row-level security and add the per-user diary policy for
  check_in_photos (previously RLS was enabled with no policy, so every insert
  was denied).
- DB: point check_in_photos.user_id at public."user" (Better Auth), matching
  check_in_measurements; uploads failed with a foreign key violation for users
  absent from the legacy auth.users table. Add the prerequisite PRIMARY KEY on
  check_in_measurements so the photos foreign key target is valid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@zeusal zeusal force-pushed the feature/issue-229-progress-photos branch from 19ac745 to 563d4b9 Compare June 14, 2026 21:45
@zeusal zeusal marked this pull request as ready for review June 14, 2026 21:53
@CodeWithCJ

Copy link
Copy Markdown
Owner

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a progress photos feature (front, back, and side) for check-ins, including frontend components, a custom hook, backend routes, services, database migrations, and RLS policies. The code review identifies a high-severity security vulnerability (IDOR/Authorization Bypass) where Row-Level Security could be bypassed, allowing unauthorized access to private photos. Additionally, the feedback highlights performance concerns regarding synchronous file system operations blocking the Node.js event loop, and suggests frontend optimizations using useMemo to prevent unnecessary map re-creations on every render.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread SparkyFitnessServer/services/checkInPhotoService.ts
Comment thread SparkyFitnessServer/services/checkInPhotoService.ts
Comment thread SparkyFitnessServer/services/checkInPhotoService.ts Outdated
@@ -0,0 +1,183 @@
import { useRef, useCallback } from 'react';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Import useMemo from 'react' to support the memoization of the photo map.

Suggested change
import { useRef, useCallback } from 'react';
import { useRef, useCallback, useMemo } from 'react';

Comment thread SparkyFitnessFrontend/src/pages/CheckIn/CheckInPhotos.tsx Outdated
@CodeWithCJ

Copy link
Copy Markdown
Owner

Take a look at gemini comments. some times it could also be false positive.

@CodeWithCJ

Copy link
Copy Markdown
Owner

@apedley could you review this.

- checkInPhotoService: convert synchronous fs calls (existsSync/unlinkSync/
  mkdirSync/writeFileSync/renameSync) to async fs.promises equivalents so file
  I/O no longer blocks the event loop; handle ENOENT instead of pre-checking
  existence. Preserves the temp-file -> commit -> rename atomic semantics.
- CheckInPhotos: memoize photoMap with useMemo([photos]) so the map is not
  rebuilt on every render, and import useMemo.

The flagged IDOR in getPhotoFileById is a false positive: the query runs under
getClient(userId), which sets the RLS context, and check_in_photos has a
per-user diary RLS policy, so ownership is enforced at the database layer.

https://claude.ai/code/session_01LEQ8rd34Jgy2LA24F96aP3
@zeusal

zeusal commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a progress photo upload feature for check-ins, including frontend UI components, React Query hooks, backend API routes, database migrations, and magic-byte image validation. Feedback on the changes suggests using the standard apiCall wrapper instead of direct fetch in the frontend, deriving file extensions from validated magic bytes rather than trusting client-provided filenames, and removing the redundant check_in_measurement_id column to avoid data inconsistency. Additionally, it is recommended to remove capture="environment" to allow gallery uploads, add WebP support to the allowed image signatures, increase the photo_type column length for future-proofing, and replace any types with proper Express types in the route handler.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread SparkyFitnessFrontend/src/api/CheckIn/checkInPhotoService.ts
Comment thread SparkyFitnessServer/services/checkInPhotoService.ts Outdated
Comment thread SparkyFitnessServer/services/checkInPhotoService.ts
Comment thread SparkyFitnessFrontend/src/pages/CheckIn/CheckInPhotos.tsx
Comment thread SparkyFitnessServer/middleware/checkInPhotoUpload.ts Outdated
Comment on lines +168 to +169
async (req: any, res: any) => {
if (!req.file) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using any for req and res disables TypeScript's type safety and requires disabling the eslint rule. Type them properly using express.Request and express.Response to maintain type safety.

Suggested change
async (req: any, res: any) => {
if (!req.file) {
async (req: express.Request & { file?: Express.Multer.File; userId?: string }, res: express.Response) => {

- upload (frontend): route uploadCheckInPhoto through apiCall with
  isFormData instead of a raw fetch, so it shares the app's API base URL,
  cookie auth, and error handling (Gemini #1).
- stored extension: derive the file extension from the validated image
  magic bytes (getImageExtension) instead of the spoofable client filename,
  so the served Content-Type can't be mismatched under nosniff (Gemini #2).
- file input: drop capture="environment" so mobile users can pick the
  camera or an existing gallery photo, not just the camera (Gemini #4).
- uploads: accept WebP, validating the full RIFF....WEBP signature (not just
  the RIFF prefix, which also matches WAV/AVI) (Gemini #5).
- routes: type the upload handler as (req, res) with a narrow req.file cast
  instead of (req: any, res: any) (Gemini #7).
- tests: assert the derived extension and add WebP accept / non-WebP RIFF
  reject cases.

https://claude.ai/code/session_01LEQ8rd34Jgy2LA24F96aP3
@zeusal

zeusal commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request implements a progress photos feature for check-ins, allowing users to upload, view, and delete front, back, and side photos. It includes frontend UI components, React Query hooks, and backend support with a database migration, RLS policies, secure image validation, and file system persistence. The review feedback suggests several improvements: expanding the database photo_type column length for future extensibility, removing a redundant index, logging instead of throwing file deletion errors to avoid unnecessary 500 responses, adding client-side file size validation, and disabling all photo slots during an active upload to prevent concurrent upload race conditions.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread SparkyFitnessServer/db/migrations/20260614000000_add_checkin_photos.sql Outdated
Comment thread SparkyFitnessServer/services/checkInPhotoService.ts
Comment thread SparkyFitnessFrontend/src/hooks/CheckIn/useCheckInPhotos.ts Outdated
Comment thread SparkyFitnessFrontend/src/pages/CheckIn/CheckInPhotos.tsx
Comment thread SparkyFitnessFrontend/src/pages/CheckIn/CheckInPhotos.tsx Outdated
- migration: drop the redundant (user_id, entry_date) index; the unique
  constraint on (user_id, entry_date, photo_type) already builds a composite
  index whose leading columns cover those lookups (Gemini #2).
- deletePhoto: the DB row is the source of truth, so a non-ENOENT unlink
  failure is now logged instead of thrown, avoiding a 500 (and a confusing
  orphaned-file state) after a successful delete (Gemini #3).
- upload (frontend): reject files over 10 MB client-side with a toast before
  the request, mirroring the server multer limit (Gemini #4).
- CheckInPhotos: add a global 'disabled' prop so every slot is blocked while
  any upload is in flight, while the spinner stays on the active slot only,
  preventing a second upload from racing the first (Gemini #5).

https://claude.ai/code/session_01LEQ8rd34Jgy2LA24F96aP3
@zeusal

zeusal commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a progress photo upload feature (front, back, and side views) to track physique over time. On the frontend, it adds translation keys, an API service, a custom React Query hook, and a CheckInPhotos UI component. On the backend, it implements a database migration with RLS policies, a secure multer upload middleware with magic-byte validation, and routes/services to handle photo management securely. The review feedback highlights several improvement opportunities: handling potential transmission errors in res.sendFile with a callback, verifying file existence on disk before serving, and exposing/utilizing the deletion loading state (isDeleting) in the frontend hook and components to prevent concurrent conflicting operations.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread SparkyFitnessServer/routes/checkInPhotoRoutes.ts Outdated
Comment thread SparkyFitnessServer/services/checkInPhotoService.ts
Comment thread SparkyFitnessFrontend/src/hooks/CheckIn/useCheckInPhotos.ts
Comment thread SparkyFitnessFrontend/src/pages/CheckIn/CheckInPhotos.tsx Outdated
Comment thread SparkyFitnessFrontend/src/pages/CheckIn/CheckInPhotos.tsx Outdated
- serve route: pass an error callback to res.sendFile so an async
  transmission failure is logged and answered with 500 only when headers
  haven't been sent yet, instead of escaping the try/catch (Gemini #1).
- getPhotoFileById: verify the file exists on disk (fs.promises.access)
  before returning its path, so a missing file yields a clean 404 rather
  than a 500 from sendFile (Gemini #2).
- useCheckInPhotos: expose isDeleting (deleteMutation.isPending) (Gemini #3).
- CheckInPhotos: consume isDeleting and disable every slot while a delete is
  in flight (disabled={isUploading || isDeleting}), matching the existing
  upload guard so concurrent operations can't race (Gemini #4, #5).

https://claude.ai/code/session_01LEQ8rd34Jgy2LA24F96aP3
@zeusal

zeusal commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Hi @CodeWithCJ I'm trying to put all of Gemini's suggestions into practice, but if you think the code is fine, I'll stop asking the bot.

@CodeWithCJ

Copy link
Copy Markdown
Owner

@apedley will review and provide his feedback today or tomorrow.

@apedley

apedley commented Jun 19, 2026

Copy link
Copy Markdown
Contributor
  1. Looks like upload directory is harcoded in the service. It's set to SparkyFitnessServer/uploads when it should respect SPARKY_FITNESS_CUSTOM_UPLOADS_DIRECTORY
  2. Schema backup file is not updated all the way. Missing the constraints on check_in_photos
  3. UI Wise, can you make it much smaller if there are no images? It's a lot to scroll by for users who don't use the feature

…kup, compact empty UI

Address review feedback on the progress photos feature:

- checkInPhotoService: resolve the uploads root from
  SPARKY_FITNESS_CUSTOM_UPLOADS_DIRECTORY (matching the rest of the
  server) instead of hardcoding SparkyFitnessServer/uploads. The stored
  'uploads/...'-rooted file_path is resolved against the configured root,
  keeping the path-traversal guard in getPhotoFileById correct.
- db_schema_backup.sql: add the missing check_in_photos primary key and
  (user_id, entry_date, photo_type) unique constraint, plus the
  check_in_measurements primary key the migration adds.
- CheckInPhotos: collapse the section to a compact single-row header when
  no photos exist so users who don't use the feature have less to scroll.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqrJhyCWBwLYvHGXmWVizX
@zeusal

zeusal commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author
  1. Looks like upload directory is harcoded in the service. It's set to SparkyFitnessServer/uploads when it should respect SPARKY_FITNESS_CUSTOM_UPLOADS_DIRECTORY
  2. Schema backup file is not updated all the way. Missing the constraints on check_in_photos
  3. UI Wise, can you make it much smaller if there are no images? It's a lot to scroll by for users who don't use the feature

Hi, as far as the user interface is concerned, I have two option: one of them is a drop-down menu

image image

And second one:
image
image
image
image

Which one do you prefer?

@apedley

apedley commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

First one looks good, thanks!

@zeusal

zeusal commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

First one looks good, thanks!

The first one is already push to branch

@CodeWithCJ CodeWithCJ merged commit f21ad2c into CodeWithCJ:main Jun 19, 2026
7 checks passed
@zeusal zeusal deleted the feature/issue-229-progress-photos branch June 24, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Add progress pictures/videos

4 participants