Skip to content

Comments

File attachments & uploads#15

Merged
dbca-serkank merged 63 commits intomainfrom
uploads-backend
Feb 25, 2026
Merged

File attachments & uploads#15
dbca-serkank merged 63 commits intomainfrom
uploads-backend

Conversation

@dbca-serkank
Copy link
Collaborator

@dbca-serkank dbca-serkank commented Feb 10, 2026

Pull Request: File Attachments Feature

Working copy in UAT:

https://authorisations-uat-internal-oim03.dbca.wa.gov.au/my-applications

Please go with the "Care and Use of Animals" application and you will see 2 file attachment fields on the "Project Details" step:

A) 8. Attach your current CI competency form
F) 2. Please attach location map of the fieldwork

Overview

This PR implements a comprehensive file attachment system for applications, allowing users to upload, manage, and track files associated with their submissions. The feature includes frontend drag-and-drop functionality, backend validation, and a dedicated database model for attachment management.

Key Implementation Details

Frontend: Drag & Drop Upload

Files: frontend/src/components/inputs/file.tsx

  • React Dropzone integration with drag-and-drop support
  • Client-side validation:
    • File size check against clientConfig.upload_max_size (10MB limit)
    • MIME type validation against UPLOAD_MIME_TYPES (JPEG, PNG, PDF)
  • Upload progress tracking for slow connections with visual feedback
  • Error handling with user-friendly messages

Backend: File Attachment Model

File: backend/applications/models.py

New ApplicationAttachment model provides:

  • Direct 1-to-many relationship with Application
  • Immutable file content (stored via Django's FileField)
  • Soft-delete support via is_deleted flag for data integrity
  • File rename capability for downloads (separate from actual filename)
  • Metadata tracking: upload timestamp, uploaded by user

API Endpoint: File Upload & Management

Files: backend/api/views.py, backend/applications/serialisers.py

Endpoint: GET, POST, PATCH, DEL /api/attachments

  • AttachmentViewSet handles all attachment related API methods
  • Owner only access to the endpoint
  • AttachmentSerialiser validates attachment metadata and uploaded file
  • Returns updated IApplicationAttachment with attachment metadata to frontend

Server-Side Validation

File: backend/applications/serialisers.py

  • Validates user is the owner of application upon creation
  • AttachmentSerialiser.validate_file() enforces UPLOAD_MAX_SIZE limit
  • Magic byte validation (content-based) prevents spoofed file types
  • FileTooLargeError exception returns appropriate HTTP 413 status
  • Human readable file size formatting in error messages for clarity

Configuration

File: backend/config/settings.py

  • UPLOAD_MAX_SIZE = 10 * 1024 * 1024 (10MB)
  • UPLOAD_MIME_TYPES whitelist: JPEG, PNG, PDF
  • Client config synced via backend/api/serialisers.py ClientConfigSerialiser

Frontend API Management

File: frontend/src/context/ApiManager.tsx

  • ApiManager.uploadAttachment() method handles multipart form upload
  • ApiManager.deleteAttachment() to handle attachment DEL method
  • ApiManager.renameAttachment() to handle attachment rename PATCH method
  • Upload progress callback integration
  • AbortController support for request cancellation

Areas for Review

  1. Magic Byte Validation: Verify file type detection is robust against spoofed extensions (try with fake files and misleading extensions)
  2. Storage Backend: File storage in "private media" - see PrivateMediaStorage in backend/applications/models.py for storage path integration
  3. Concurrent Uploads: Test behaviour under simultaneous upload scenarios
  4. Dropdown Behaviour: Upload, delete, rename and download the uploaded attachments

Testing Checklist

  • Upload valid files (JPEG, PNG, PDF)
  • Reject oversized files (>10MB)
  • Reject invalid MIME types
  • Verify soft-delete functionality
  • Test file rename operations
  • Confirm drag-and-drop UX
  • Validate error messaging

@socket-security
Copy link

socket-security bot commented Feb 10, 2026

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report

@dbca-serkank
Copy link
Collaborator Author

@SocketSecurity ignore npm/entities@4.5.0

@dbca-serkank dbca-serkank requested a review from ropable February 13, 2026 02:43
Copy link
Member

@ropable ropable left a comment

Choose a reason for hiding this comment

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

Recommendations:

  • models.PrivateMediaStorage: for all media uploads, consider a using Azure blob storage (in conjunction with django-storages) as the storage method rather than local file storage in order to help make the system more portable/resilient. Note that this recommendation will have implications for your views (it's generally easier to serve blobs).
  • models.PrivateMediaStorage: consider not extending the Django FileSystemStorage class at all, not using a separate local "private" media directory path, and instead manage all file access/authorisation within the model and views.
  • models.PrivateMediaStorage: if you absolutely must use this class, consider removing the path.exists check in init as it might make bootstrapping a new project harder. Also consider moving the class out of models.py (e.g. storage.py).
  • If you keep PrivateMediaStorage, give settings.PRIVATE_MEDIA_ROOT a default value (even an empty string).
  • entrypoint.sh: remove the migrate command (run this manually during changes to avoid it being run simultaneously in production environments).
  • entrypoint.sh: move the collectstatic command out and into the Dockerfile instead in order to guarantee the state of static assets in a runnng container, rather than running it dynamically on every start.

@socket-security
Copy link

socket-security bot commented Feb 20, 2026

@dbca-serkank
Copy link
Collaborator Author

Recommendations:

Hey @ropable,

Thanks very much for your review and recommendations. I wanted to run this with you before merging to main but as far as I can see you're on leave until Wednesday. So, I'm writing this to update you on what has been addressed and what has not, with reasons behind.

  • models.PrivateMediaStorage: for all media uploads, consider a using Azure blob storage (in conjunction with django-storages) as the storage method rather than local file storage in order to help make the system more portable/resilient. Note that this recommendation will have implications for your views (it's generally easier to serve blobs).

I have replaced the storage backend to Azure Blob as you recommended. It is working nicely on UAT (with DEV container though, waiting on OIM to give me the UAT container).

  • models.PrivateMediaStorage: consider not extending the Django FileSystemStorage class at all, not using a separate local "private" media directory path, and instead manage all file access/authorisation within the model and views.

I explicitly want to avoid using Django's MEDIA file system, although I am still managing the access from views / models. The files we are managing are official sensitive and personally identifiable. I don't want these files to be written and served from the "default public" MEDIA files.

  • models.PrivateMediaStorage: if you absolutely must use this class, consider removing the path.exists check in init as it might make bootstrapping a new project harder. Also consider moving the class out of models.py (e.g. storage.py).

I have moved that class to its own Python file, not checking / throwing error if the folder doesn't exist on initialisation as you recommended. I did create that directory in Django settings replicating the PRS application behaviour.

  • If you keep PrivateMediaStorage, give settings.PRIVATE_MEDIA_ROOT a default value (even an empty string).

I want the application to fail explicitly if the path was not given by the environment in combination of using LOCAL_MEDIA_STORAGE in Django settings. Empty string will fail quietly and will start writing uploaded files to god knows where.

  • entrypoint.sh: remove the migrate command (run this manually during changes to avoid it being run simultaneously in production environments).
  • entrypoint.sh: move the collectstatic command out and into the Dockerfile instead in order to guarantee the state of static assets in a runnng container, rather than running it dynamically on every start.

Thank you for catching these 2 issues. I will definitely address them asap but on the main branch.

Comment on lines -214 to 224
rootProps.onDragLeave = (e) => e.preventDefault();
rootProps.onDragOver = (e) => e.preventDefault();
}
// Calculate the maximum file size in MB for human readable display
const maxFilesizeMB = Math.round(clientConfig.upload_max_size / (1024 * 1024));

return (
<Box {...rootProps}>
<VisuallyHiddenInput {...inputProps} />
{styling.icon}
<br /><br />
{progress === null ?
styling.icon :
<LinearProgress variant="determinate" value={progress} color="success" className="w-full mt-8" />
}

{filename ? (
Copy link

Choose a reason for hiding this comment

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

Bug: The file upload request in DropzoneDialogContent is not cancelled when the component unmounts, such as when a user navigates to another form step during an upload.
Severity: MEDIUM

Suggested Fix

Create an AbortController instance within the DropzoneDialogContent component. Pass its signal to the ApiManager.uploadAttachment function call. Implement a useEffect cleanup function that calls controller.abort() when the component unmounts. This will ensure that any in-progress upload is cancelled if the user navigates away.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: frontend/src/components/inputs/file.tsx#L174-L224

Potential issue: The refactored `DropzoneDialogContent` component no longer uses an
`AbortController` for file uploads. If a user starts an upload and then navigates to a
different form step, the component unmounts but the underlying `axios` request is not
cancelled. This continues the upload in the background, consuming server resources and
bandwidth. When the request eventually completes, its promise handlers (`.then()` and
`.finally()`) will attempt to update the state of the unmounted component, leading to
React warnings and the creation of orphaned attachment records on the server for an
upload the user effectively abandoned.

@dbca-serkank dbca-serkank merged commit a97ae1d into main Feb 25, 2026
5 checks passed
@dbca-serkank dbca-serkank deleted the uploads-backend branch February 25, 2026 06:47
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.

4 participants