I’m using cropper.js in an MVC .net 6 app.
It works on my local machine but not on IIS.
_Layout.cshtml:
@using Microsoft.AspNetCore.Identity
@using Memayo.ViewModels
@using Microsoft.AspNetCore.Http
@using System.Security.Claims
@inject SignInManager<AppUser> SignInManager
@inject UserManager<AppUser> UserManager
@inject IHttpContextAccessor HttpContextAccessor
@{
var userId = HttpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
var user = userId != null ? await UserManager.FindByIdAsync(userId) : null;
var avatarUrl = user?.Avatar != null ? $"/images/uploads/small/{user.Avatar}" : "/images/default-avatar.png";
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Memayo</title>
<link rel="stylesheet" href="~/bootstrap/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" />
<link href="~/css/memayo.css" rel="stylesheet" />
@* <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-eval';"> *@
@* <link href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.css" rel="stylesheet" /> *@
<link href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.css" rel="stylesheet" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="nav-linkxx xxtext-dark" asp-controller="Home" asp-action="Index">
<img class="memayologo" src="~/images/logo.png" />
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
@if (SignInManager.IsSignedIn(User))
{
@if (User.IsInRole("admin"))
{
<li class="nav-item">
<a class="nav-link text-lightx text-danger" asp-area="venus" asp-controller="AHome" asp-action="Index">Admin Area</a>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link" asp-controller="Yarn" asp-action="Index">Yarn</a>
</li>
<li class="nav-item">
<a class="nav-link" asp-controller="Question" asp-action="Index">Chapters</a>
</li>
<li class="nav-item">
<a class="nav-link" asp-controller="Photo" asp-action="Index">Photos</a>
</li>
<li class="nav-item">
<a class="nav-link" asp-controller="YourQuestion" asp-action="Index">My Q&A</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">Friends</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" asp-controller="Friend" asp-action="Index">Friends List</a></li>
<li><a class="dropdown-item" asp-controller="Friend" asp-action="Requests">Friend Requests</a></li>
<li><a class="dropdown-item" asp-controller="Friend" asp-action="Search">Search for Friends</a></li>
</ul>
</li>
}
}
</ul>
<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
<li class="nav-item dropdown">
<a class="nav-link d-flex align-items-center gap-2" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<div class="rounded-circle overflow-hidden" style="width: 38px; height: 38px;">
@if (user?.Avatar != null)
{
<img src="@avatarUrl" alt="User Avatar" class="w-100 h-100 object-fit-cover" />
}
else
{
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" class="w-100 h-100">
<circle cx="64" cy="64" r="64" fill="#E2E8F0" />
<path d="M64 36c7.732 0 14 6.268 14 14s-6.268 14-14 14-14-6.268-14-14 6.268-14 14-14zM64 70c18.778 0 34 8.222 34 20v8c0 2-2 2-2 2H32s-2 0-2-2v-8c0-11.778 15.222-20 34-20z" fill="#94A3B8" />
</svg>
}
</div>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-down" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z" />
</svg>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" asp-controller="MyProfile" asp-action="Profile">Profile</a></li>
<li><a class="dropdown-item" asp-controller="MyProfile" asp-action="Avatar">Avatar</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" asp-controller="account" asp-action="Logout">Logout</a></li>
</ul>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark" asp-controller="account" asp-action="Register">Register</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-controller="account" asp-action="Login">Login</a>
</li>
}
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
<partial name="_MessagePartial" />
@RenderBody()
</main>
</div>
<footer class="border-top footer text-muted">
<div class="container">
<partial name="_VersionPartial" />
<a asp-controller="Home" asp-action="Privacy">Privacy</a> |
<a asp-controller="Home" asp-action="Terms">Terms</a>
</div>
</footer>
<script src="~/js/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/bootstrap/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.js"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
Controller:
using CSharpAwsSesServiceHelper.EmailService;
using Memayo.Models;
using Memayo.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using System.Collections.Generic;
using Microsoft.Extensions.Hosting;
using Memayo.Repositories;
using Microsoft.AspNetCore.Http;
using System.IO;
using System.Linq;
using System;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
namespace Memayo.Controllers
{
[Authorize(Roles = "user")]
public class MyProfileController : Controller
{
private readonly ILogger<MyProfileController> _logger;
private IWebHostEnvironment _env;
private SignInManager<AppUser> _signManager;
private UserManager<AppUser> _userManager;
private readonly IEmailService _emailService;
private readonly IUserRepository _userRepository;
private readonly IHttpContextAccessor _httpContextAccessor;
private string userId => _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
public MyProfileController(ILogger<MyProfileController> logger, IWebHostEnvironment env,
UserManager<AppUser> userManager, SignInManager<AppUser> signManager,
IEmailService emailService, IUserRepository userRepository,
IHttpContextAccessor httpContextAccessor)
{
_logger = logger;
_env = env;
_userManager = userManager;
_signManager = signManager;
_emailService = emailService;
_userRepository = userRepository;
_httpContextAccessor = httpContextAccessor;
}
public IActionResult Index()
{
return View();
}
public async Task<IActionResult> Profile()
{
var user = await _userRepository.GetAsync(userId);
return View(user);
}
[HttpPost]
public async Task<IActionResult> Profile(User user)
{
await _userRepository.UpdateProfileAsync(user);
TempData["message"] = "Profile Updated";
return RedirectToAction("Index", "Home");
}
[Authorize(Roles = "user")]
public async Task<IActionResult> Avatar()
{
var user = await _userRepository.GetAsync(userId);
return View(user);
}
[HttpPost]
[Authorize(Roles = "user")]
public async Task<IActionResult> Avatar(IFormFile avatar)
{
try
{
if (avatar == null)
{
_logger.LogError("Avatar is null");
return Json(new { success = false, error = "No file received" });
}
_logger.LogInformation($"Received file: {avatar.FileName}, Length: {avatar.Length}, ContentType: {avatar.ContentType}");
if (avatar.Length == 0)
{
TempData["messageError"] = "Please upload a valid image.";
return Json(new { success = false, error = "Empty file" });
}
// Check file type
var allowedExtensions = new[] { ".png", ".jpg", ".jpeg" };
var extension = Path.GetExtension(avatar.FileName).ToLower();
if (!allowedExtensions.Contains(extension))
{
TempData["messageError"] = "Only PNG and JPG images are allowed.";
return Json(new { success = false, error = "Invalid file type" });
}
// Delete old avatar if exists, get user
var user = await _userRepository.GetAsync(userId);
var uploadsDir = Path.Combine(_env.WebRootPath, "images", "uploads");
var filename = $"{Guid.NewGuid()}{extension}";
var oldAvatarPathLarge = Path.Combine(uploadsDir, "large", user.Avatar);
var oldAvatarPathSmall = Path.Combine(uploadsDir, "small", user.Avatar);
var newAvatarPathLarge = Path.Combine(uploadsDir, "large", filename);
var newAvatarPathSmall = Path.Combine(uploadsDir, "small", filename);
_logger.LogInformation($"Paths: New Large: {newAvatarPathLarge}, New Small: {newAvatarPathSmall}");
// Delete old avatar if exists
if (System.IO.File.Exists(oldAvatarPathLarge))
{
try
{
System.IO.File.Delete(oldAvatarPathLarge);
}
catch (Exception ex)
{
_logger.LogError($"Error deleting old large avatar: {ex.Message}");
}
}
if (System.IO.File.Exists(oldAvatarPathSmall))
{
try
{
System.IO.File.Delete(oldAvatarPathSmall);
}
catch (Exception ex)
{
_logger.LogError($"Error deleting old small avatar: {ex.Message}");
}
}
// Resize the image
using (var image = await Image.LoadAsync(avatar.OpenReadStream()))
{
_logger.LogInformation("Image loaded successfully");
var maxWidthLarge = 1000;
var maxHeightLarge = 1000;
var maxWidthSmall = 200;
var maxHeightSmall = 200;
var largeImage = image.Clone(ctx => ctx.Resize(new ResizeOptions
{
Size = new SixLabors.ImageSharp.Size(maxWidthLarge, maxHeightLarge),
Mode = ResizeMode.Max
}));
var smallImage = image.Clone(ctx => ctx.Resize(new ResizeOptions
{
Size = new SixLabors.ImageSharp.Size(maxWidthSmall, maxHeightSmall),
Mode = ResizeMode.Max
}));
_logger.LogInformation("Images resized");
try
{
if (extension == ".png")
{
await largeImage.SaveAsync(newAvatarPathLarge, new PngEncoder());
await smallImage.SaveAsync(newAvatarPathSmall, new PngEncoder());
}
else
{
await largeImage.SaveAsync(newAvatarPathLarge, new JpegEncoder());
await smallImage.SaveAsync(newAvatarPathSmall, new JpegEncoder());
}
_logger.LogInformation("Images saved successfully");
}
catch (Exception ex)
{
_logger.LogError($"Error saving images: {ex.Message}nStack: {ex.StackTrace}");
throw;
}
}
// Update the user's avatar path in the database
user.Avatar = filename;
await _userRepository.UpdateAvatarAsync(user);
_logger.LogInformation("Database updated successfully");
TempData["message"] = "Avatar updated successfully.";
return Json(new { success = true });
}
catch (Exception ex)
{
_logger.LogError($"Avatar upload error: {ex.Message}nStack: {ex.StackTrace}");
return Json(new { success = false, error = ex.Message });
}
}
}
}
View:
@model Memayo.Models.User
@{
ViewData["Title"] = "Edit Avatar";
}
<h2>Edit Avatar</h2>
<div class="avatar-section">
<h4>Current Avatar</h4>
<div class="current-avatar">
@if (!string.IsNullOrEmpty(Model.Avatar))
{
<img src="/images/uploads/large/@Model.Avatar" alt="User Avatar" class="rounded-circle" style="max-width: 200px; max-height: 200px;" />
}
else
{
<p>No avatar uploaded. Please upload an avatar.</p>
}
</div>
</div>
<hr />
<div class="upload-section">
@if (TempData["message"] != null)
{
<div class="alert alert-success">@TempData["message"]</div>
}
@if (TempData["messageError"] != null)
{
<div class="alert alert-danger">@TempData["messageError"]</div>
}
<form id="avatarForm" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="avatar">Select New Avatar</label>
<input type="file" class="form-control" id="avatar" name="avatar" accept=".jpg,.jpeg,.png" />
<small class="form-text text-muted">Allowed: .jpg, .png</small>
</div>
<div class="form-group">
<img id="preview" class="img-fluid" />
</div>
<button type="button" class="btn btn-primary" id="cropButton">Crop and Upload Avatar</button>
<a asp-controller="Home" asp-action="Index" class="btn btn-secondary">Cancel</a>
</form>
</div>
@section Scripts {
<script type="module">
// Initialize state
const state = {
cropper: null,
uploadUrl: '@Url.Action("Avatar", "MyProfile")',
redirectUrl: '@Url.Action("Index", "Home")',
};
// Configuration object for Cropper
const cropperConfig = {
aspectRatio: 1,
viewMode: 1,
autoCropArea: 1,
};
// Handle file input change
function handleFileChange(event) {
const image = document.getElementById('preview');
const file = event.target.files[0];
if (!file) return;
const objectURL = URL.createObjectURL(file);
image.src = objectURL;
image.addEventListener('load', () => {
if (state.cropper) {
state.cropper.destroy();
}
// Initialize the cropper with config object
state.cropper = new Cropper(image, cropperConfig);
}, { once: true }); // Use once: true to prevent memory leaks
}
// Handle crop and upload
async function handleCropAndUpload() {
if (!state.cropper) {
alert('Please select an image to crop.');
return;
}
try {
const canvas = state.cropper.getCroppedCanvas({
width: 1000,
height: 1000
});
if (!canvas) {
throw new Error('Failed to generate canvas');
}
const blob = await new Promise((resolve) => {
canvas.toBlob(resolve, 'image/png', 0.9);
});
if (!blob) {
throw new Error('Failed to generate image blob');
}
const formData = new FormData();
formData.append('avatar', new File([blob], 'avatar.png', {
type: 'image/png'
}));
const response = await fetch(state.uploadUrl, {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
window.location.href = state.redirectUrl;
} else {
throw new Error(data.error || 'Upload failed');
}
} catch (error) {
console.error('Upload error:', error);
alert(`An error occurred: ${error.message}`);
}
}
// Add event listeners
document.getElementById('avatar').addEventListener('change', handleFileChange);
document.getElementById('cropButton').addEventListener('click', handleCropAndUpload);
</script>
}
I get this error in an alert:
Error uploading avatar: Value cannot be null. (Parameter ‘path3’)
I also get this error in browser tools
The Content Security Policy (CSP) prevents cross-site scripting
attacks by blocking inline execution of scripts and style sheets.To solve this, move all inline scripts (e.g. onclick=[JS code]) and
styles into external files.⚠️ Allowing inline execution comes at the risk of script injection via
injection of HTML script elements. If you absolutely must, you can
allow inline script and styles by:adding unsafe-inline as a source to the CSP header adding the hash or
nonce of the inline script to your CSP header. 1 directive
Directive Element Source location Status
script-src-elem Avatar:119 blocked Learn more: Content Security
Policy – Inline Code
I’ve asked both Claude and Chat GPT 40 and o1. They suggested that I put this meta tag in but it didn’t help
and doing that that may cause security issues anyway.
I have a feeling it’s related to the unsafe CSP header but a google search revealed nothing.
How can I get this working on the server?
Thanks