Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image validation and size optimization for Profile image upload from both front-end and back-end #513

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions csm_web/csm_web/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from factory.django import DjangoModelFactory
from rest_framework.serializers import ModelSerializer, Serializer
from sentry_sdk.integrations.django import DjangoIntegration
from storages.backends.s3boto3 import S3Boto3Storage

# Analogous to RAILS_ENV, is one of {prod, staging, dev}. Defaults to dev. This default can
# be dangerous, but is worth it to avoid the hassle for developers setting the local ENV var
Expand Down Expand Up @@ -67,6 +68,7 @@
"frontend",
"django_extensions",
"django.contrib.postgres",
"storages",
]

SHELL_PLUS_SUBCLASSES_IMPORT = [ModelSerializer, Serializer, DjangoModelFactory]
Expand Down Expand Up @@ -175,11 +177,30 @@
AWS_QUERYSTRING_AUTH = False # public bucket
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.1/howto/static-files/

STATIC_URL = "/static/"


class ProfileImageStorage(S3Boto3Storage):
bucket_name = "csm-web-profile-pictures"
file_overwrite = True # should be true so that we replace one profile for user

def get_accessed_time(self, name):
# Implement logic to get the last accessed time
raise NotImplementedError("This backend does not support this method.")

def get_created_time(self, name):
# Implement logic to get the creation time
raise NotImplementedError("This backend does not support this method.")

def path(self, name):
# S3 does not support file paths
raise NotImplementedError("This backend does not support absolute paths.")


if DJANGO_ENV in (PRODUCTION, STAGING):
# Enables compression and caching
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
Expand Down Expand Up @@ -231,6 +252,8 @@
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
"DEFAULT_PARSER_CLASSES": [
"djangorestframework_camel_case.parser.CamelCaseJSONParser",
"rest_framework.parsers.FormParser",
"rest_framework.parsers.MultiPartParser",
],
}

Expand Down
13 changes: 12 additions & 1 deletion csm_web/frontend/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { emptyRoles, Roles } from "../utils/user";
import CourseMenu from "./CourseMenu";
import Home from "./Home";
import Policies from "./Policies";
import UserProfile from "./UserProfile";
import { DataExport } from "./data_export/DataExport";
import { EnrollmentMatcher } from "./enrollment_automation/EnrollmentMatcher";
import { Resources } from "./resource_aggregation/Resources";
Expand Down Expand Up @@ -41,6 +42,13 @@ const App = () => {
<Route path="matcher/*" element={<EnrollmentMatcher />} />
<Route path="policies/*" element={<Policies />} />
<Route path="export/*" element={<DataExport />} />
{
// TODO: add route for profiles (/profile/:id/* element = {UserProfile})
// TODO: add route for your own profile /profile/*
// reference Section
}
<Route path="profile/*" element={<UserProfile />} />
<Route path="profile/:id/*" element={<UserProfile />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
Expand Down Expand Up @@ -79,7 +87,7 @@ function Header(): React.ReactElement {
};

/**
* Helper function to determine class name for the home NavLInk component;
* Helper function to determine class name for the home NavLnk component;
* is always active unless we're in another tab.
*/
const homeNavlinkClass = () => {
Expand Down Expand Up @@ -140,6 +148,9 @@ function Header(): React.ReactElement {
<NavLink to="/policies" className={navlinkClassSubtitle}>
<h3 className="site-subtitle">Policies</h3>
</NavLink>
<NavLink to="/profile" className={navlinkClassSubtitle}>
<h3 className="site-subtitle">Profile</h3>
</NavLink>
<a id="logout-btn" href="/logout" title="Log out">
<LogOutIcon className="icon" />
</a>
Expand Down
2 changes: 1 addition & 1 deletion csm_web/frontend/src/components/CourseMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import React, { useEffect, useState } from "react";
import { Link, Route, Routes } from "react-router-dom";

import { DEFAULT_LONG_LOCALE_OPTIONS, DEFAULT_TIMEZONE } from "../utils/datetime";
import { useUserInfo } from "../utils/queries/base";
import { useCourses } from "../utils/queries/courses";
import { useUserInfo } from "../utils/queries/profiles";
import { Course as CourseType, UserInfo } from "../utils/types";
import LoadingSpinner from "./LoadingSpinner";
import Course from "./course/Course";
Expand Down
67 changes: 67 additions & 0 deletions csm_web/frontend/src/components/ImageUploader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { useState, useEffect } from "react";

Check warning on line 1 in csm_web/frontend/src/components/ImageUploader.tsx

View workflow job for this annotation

GitHub Actions / ESLint

csm_web/frontend/src/components/ImageUploader.tsx#L1

'useEffect' is defined but never used. Allowed unused vars must match /^_/u (@typescript-eslint/no-unused-vars)
import { fetchWithMethod, HTTP_METHODS } from "../utils/api";

// file size limits
const MAX_SIZE_MB = 2;
const MAX_FILE_SIZE_BYTES = MAX_SIZE_MB * 1024 * 1024;

const ImageUploader = () => {
const [file, setFile] = useState<File | null>(null);
const [status, setStatus] = useState<string>("");

// useEffect(() => {
// if (file) {
// }
// }, [file]);

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
const selectedFile = e.target.files[0];

if (selectedFile) {
if (selectedFile.size > MAX_FILE_SIZE_BYTES) {
setStatus(`File size exceeds max limit of ${MAX_SIZE_MB}MB.`);
} else {
setFile(selectedFile);
setStatus("");
}
}
}
};

const handleUpload = async () => {
try {
if (!file) {
setStatus("Please select a file to upload");
return;
}
const formData = new FormData();

formData.append("file", file);

const response = await fetchWithMethod(`user/upload_image/`, HTTP_METHODS.POST, formData, true);

console.log(response);

if (!response.ok) {
const errorData = await response.json();
console.error("Error:", errorData.error || "Unknown error");
throw new Error(errorData.error || "Failed to upload file");
}
setStatus(`File uploaded successfully`);
} catch (error) {
setStatus(`Upload failed: ${(error as Error).message}`);
}
};

return (
<div>
<h1>Image Upload Tester</h1>
<input type="file" onChange={handleFileChange} />
<button onClick={handleUpload}>Upload</button>
{status && <p>{status}</p>}
</div>
);
};

export default ImageUploader;
Loading
Loading