diff --git a/Core/Resgrid.Model/MobileCarriers.cs b/Core/Resgrid.Model/MobileCarriers.cs index fe9fbc51..7215a564 100644 --- a/Core/Resgrid.Model/MobileCarriers.cs +++ b/Core/Resgrid.Model/MobileCarriers.cs @@ -302,7 +302,8 @@ public static class Carriers MobileCarriers.Vodacom, MobileCarriers.MTN, MobileCarriers.TelkomMobile, - MobileCarriers.CellC + MobileCarriers.CellC, + MobileCarriers.TMobile }; public static HashSet OnPremSmsGatewayCarriers = new HashSet() diff --git a/Core/Resgrid.Services/MessageService.cs b/Core/Resgrid.Services/MessageService.cs index ce06fee0..870bd39d 100644 --- a/Core/Resgrid.Services/MessageService.cs +++ b/Core/Resgrid.Services/MessageService.cs @@ -50,7 +50,7 @@ public async Task GetMessageByIdAsync(int messageId) public async Task> GetInboxMessagesByUserIdAsync(string userId) { var list = await _messageRepository.GetInboxMessagesByUserIdAsync(userId); - return list.ToList(); + return list.OrderByDescending(x => x.SentOn).ToList(); } public async Task> GetUnreadInboxMessagesByUserIdAsync(string userId) @@ -65,7 +65,7 @@ public async Task> GetSentMessagesByUserIdAsync(string userId) var items = await _messageRepository.GetSentMessagesByUserIdAsync(userId); if (items != null && items.Any()) - return items.ToList(); + return items.OrderByDescending(x => x.SentOn).ToList(); return new List(); } diff --git a/Providers/Resgrid.Providers.Claims/ClaimsLogic.cs b/Providers/Resgrid.Providers.Claims/ClaimsLogic.cs index 0654a9b9..8b9f25bb 100644 --- a/Providers/Resgrid.Providers.Claims/ClaimsLogic.cs +++ b/Providers/Resgrid.Providers.Claims/ClaimsLogic.cs @@ -1101,16 +1101,18 @@ public static void AddContactsClaims(ClaimsIdentity identity, bool isAdmin, List } else if (permission.Action == (int)PermissionActions.DepartmentAdminsAndSelectRoles && !isAdmin) { - var roleIds = permission.Data.Split(char.Parse(",")).Select(int.Parse); - var role = from r in roles - where roleIds.Contains(r.PersonnelRoleId) - select r; - - if (role.Any()) + if (!String.IsNullOrWhiteSpace(permission.Data)) { - identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Contacts, ResgridClaimTypes.Actions.View)); - } + var roleIds = permission.Data.Split(char.Parse(",")).Select(int.Parse); + var role = from r in roles + where roleIds.Contains(r.PersonnelRoleId) + select r; + if (role.Any()) + { + identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Contacts, ResgridClaimTypes.Actions.View)); + } + } } else if (permission.Action == (int)PermissionActions.Everyone) { @@ -1143,17 +1145,19 @@ where roleIds.Contains(r.PersonnelRoleId) } else if (permission.Action == (int)PermissionActions.DepartmentAdminsAndSelectRoles && !isAdmin) { - var roleIds = permission.Data.Split(char.Parse(",")).Select(int.Parse); - var role = from r in roles - where roleIds.Contains(r.PersonnelRoleId) - select r; - - if (role.Any()) + if (!String.IsNullOrWhiteSpace(permission.Data)) { - identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Contacts, ResgridClaimTypes.Actions.Update)); - identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Contacts, ResgridClaimTypes.Actions.Create)); - } + var roleIds = permission.Data.Split(char.Parse(",")).Select(int.Parse); + var role = from r in roles + where roleIds.Contains(r.PersonnelRoleId) + select r; + if (role.Any()) + { + identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Contacts, ResgridClaimTypes.Actions.Update)); + identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Contacts, ResgridClaimTypes.Actions.Create)); + } + } } else if (permission.Action == (int)PermissionActions.Everyone) { @@ -1186,16 +1190,18 @@ where roleIds.Contains(r.PersonnelRoleId) } else if (permission.Action == (int)PermissionActions.DepartmentAdminsAndSelectRoles && !isAdmin) { - var roleIds = permission.Data.Split(char.Parse(",")).Select(int.Parse); - var role = from r in roles - where roleIds.Contains(r.PersonnelRoleId) - select r; - - if (role.Any()) + if (!String.IsNullOrWhiteSpace(permission.Data)) { - identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Contacts, ResgridClaimTypes.Actions.Delete)); - } + var roleIds = permission.Data.Split(char.Parse(",")).Select(int.Parse); + var role = from r in roles + where roleIds.Contains(r.PersonnelRoleId) + select r; + if (role.Any()) + { + identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Contacts, ResgridClaimTypes.Actions.Delete)); + } + } } else if (permission.Action == (int)PermissionActions.Everyone) { diff --git a/Web/Resgrid.Web.Services/Controllers/v4/AvatarsController.cs b/Web/Resgrid.Web.Services/Controllers/v4/AvatarsController.cs new file mode 100644 index 00000000..3327135a --- /dev/null +++ b/Web/Resgrid.Web.Services/Controllers/v4/AvatarsController.cs @@ -0,0 +1,184 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Resgrid.Framework; +using Resgrid.Model; +using Resgrid.Model.Services; +using System; +using System.IO; +using System.Net.Mime; +using System.Threading.Tasks; + +using Resgrid.Web.Services.Models; +using Resgrid.Web.ServicesCore.Helpers; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + + +namespace Resgrid.Web.Services.Controllers.v4 +{ + /// + /// Used to interact with the user avatars (profile pictures) in the Resgrid system. The authentication header isn't required to access this method. + /// + [Route("api/v{VersionId:apiVersion}/[controller]")] + [ApiVersion("4.0")] + [ApiExplorerSettings(GroupName = "v4")] + //[EnableCors("_resgridWebsiteAllowSpecificOrigins")] + public class AvatarsController : ControllerBase + { + private readonly IImageService _imageService; + private static byte[] _defaultProfileImage; + + public AvatarsController(IImageService imageService) + { + _imageService = imageService; + } + + /// + /// Get a users avatar from the Resgrid system based on their ID + /// + /// ID of the user + /// + [HttpGet("Get")] + [Produces(MediaTypeNames.Image.Jpeg)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Get(string id, int? type) + { + byte[] data = null; + if (type == null) + data = await _imageService.GetImageAsync(ImageTypes.Avatar, id); + else + data = await _imageService.GetImageAsync((ImageTypes)type.Value, id); + + if (data == null || data.Length <= 0) + return File(GetDefaultProfileImage(), "image/png"); + + return File(data, "image/jpeg"); + } + + [HttpPost("Upload")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task Upload([FromQuery] string id, int? type) + { + var img = HttpContext.Request.Form.Files.Count > 0 ? + HttpContext.Request.Form.Files[0] : null; + + // check for a valid mediatype + if (!img.ContentType.StartsWith("image/")) + return BadRequest(); + + // load the image from the upload and generate a new filename + //var image = Image.FromStream(img.OpenReadStream()); + var extension = Path.GetExtension(img.FileName); + byte[] imgArray; + int width = 0; + int height = 0; + + using (Image image = Image.Load(img.OpenReadStream())) + { + //image.Mutate(x => x + // .Resize(image.Width / 2, image.Height / 2) + // .Grayscale()); + + width = image.Width; + height = image.Height; + + MemoryStream ms = new MemoryStream(); + await image.SaveAsPngAsync(ms); + imgArray = ms.ToArray(); + + //image.Save()"output/fb.png"); // Automatic encoder selected based on extension. + } + + //ImageConverter converter = new ImageConverter(); + //byte[] imgArray = (byte[])converter.ConvertTo(image, typeof(byte[])); + + if (type == null) + await _imageService.SaveImageAsync(ImageTypes.Avatar, id, imgArray); + else + await _imageService.SaveImageAsync((ImageTypes)type.Value, id, imgArray); + + var baseUrl = Config.SystemBehaviorConfig.ResgridApiBaseUrl; + + string url; + + if (type == null) + url = baseUrl + "/api/v4/Avatars/Get?id=" + id; + else + url = baseUrl + "/api/v4/Avatars/Get?id=" + id + "&type=" + type.Value; + + var obj = new + { + status = CroppicStatuses.Success, + url = url, + width = width, + height = height + }; + + return CreatedAtAction(nameof(Upload), new { id = obj.url }, obj); + } + + [HttpPut("Crop")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task Crop([FromBody] CropRequest model) + { + // extract original image ID and generate a new filename for the cropped result + var originalUri = new Uri(model.imgUrl); + var originalId = originalUri.Query.Replace("?id=", ""); + + try + { + byte[] imgArray; + + using (var ms = new MemoryStream(await _imageService.GetImageAsync(ImageTypes.Avatar, originalId))) + using (var image = Image.Load(ms)) + { + // load the original picture and resample it to the scaled values + var bitmap = ImageUtils.Resize(image, (int)model.imgW, (int)model.imgH); + + var croppedBitmap = ImageUtils.Crop(bitmap, model.imgX1, model.imgY1, model.cropW, model.cropH); + + using (var ms2 = new MemoryStream()) + { + await croppedBitmap.SaveAsPngAsync(ms2); + imgArray = ms2.ToArray(); + } + } + + await _imageService.SaveImageAsync(ImageTypes.Avatar, originalId, imgArray); + } + catch (Exception ex) + { + Logging.LogException(ex, $"Error cropping avatar image for ID: {originalId}"); + return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while cropping the image"); + } + + var obj = new + { + status = CroppicStatuses.Success, + url = originalId + }; + + return CreatedAtAction(nameof(Crop), new { id = obj.url }, obj); + } + + private byte[] GetDefaultProfileImage() + { + if (_defaultProfileImage == null) + _defaultProfileImage = EmbeddedResources.GetApiRequestFile(typeof(AvatarsController), "Resgrid.Web.Services.Properties.Resources.defaultProfile.png"); + + return _defaultProfileImage; + } + } + + internal static class CroppicStatuses + { + public const string Success = "success"; + public const string Error = "error"; + } +} diff --git a/Web/Resgrid.Web.Services/Controllers/v4/MessagesController.cs b/Web/Resgrid.Web.Services/Controllers/v4/MessagesController.cs index 8026d34a..312c8b2d 100644 --- a/Web/Resgrid.Web.Services/Controllers/v4/MessagesController.cs +++ b/Web/Resgrid.Web.Services/Controllers/v4/MessagesController.cs @@ -312,13 +312,19 @@ public async Task> SendMessage([FromBody] NewMes // Add all the explict people foreach (var person in newMessageInput.Recipients.Where(x => x.Type == 1)) { - if (usersToSendTo.All(x => x != person.Id) && person.Id != UserId) + if (!String.IsNullOrWhiteSpace(person.Id)) { - // Ensure the user is in the same department - if (departmentUsers.Any(x => x.UserId == person.Id)) + // New RN Apps add a prefix to ID's from the Recipients list, guard against the prefix here. + var userIdToSendTo = person.Id.Replace("P:", "").Trim(); + + if (usersToSendTo.All(x => x != userIdToSendTo) && userIdToSendTo != UserId) { - usersToSendTo.Add(person.Id); - message.AddRecipient(person.Id); + // Ensure the user is in the same department + if (departmentUsers.Any(x => x.UserId == userIdToSendTo)) + { + usersToSendTo.Add(userIdToSendTo); + message.AddRecipient(userIdToSendTo); + } } } } @@ -328,8 +334,11 @@ public async Task> SendMessage([FromBody] NewMes { if (!String.IsNullOrWhiteSpace(group.Id)) { + // New RN Apps add a prefix to ID's from the Recipients list, guard against the prefix here. + var groupIdToSendTo = group.Id.Replace("G:", "").Trim(); + int groupId = 0; - if (int.TryParse(group.Id.Trim(), out groupId)) + if (int.TryParse(groupIdToSendTo, out groupId)) { if (departmentGroups.Any(x => x.DepartmentGroupId == groupId)) { @@ -356,8 +365,11 @@ public async Task> SendMessage([FromBody] NewMes { if (!String.IsNullOrWhiteSpace(role.Id)) { + // New RN Apps add a prefix to ID's from the Recipients list, guard against the prefix here. + var roleIdToSendTo = role.Id.Replace("R:", "").Trim(); + int roleId = 0; - if (int.TryParse(role.Id.Trim(), out roleId)) + if (int.TryParse(roleIdToSendTo, out roleId)) { if (departmentRoles.Any(x => x.PersonnelRoleId == roleId)) { diff --git a/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml b/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml index 398c5eec..79d2c033 100644 --- a/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml +++ b/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml @@ -30,6 +30,18 @@ Type to get + + + Used to interact with the user avatars (profile pictures) in the Resgrid system. The authentication header isn't required to access this method. + + + + + Get a users avatar from the Resgrid system based on their ID + + ID of the user + + Mobile or Tablet Device specific operations diff --git a/Web/Resgrid.Web/Areas/User/Controllers/ConnectController.cs b/Web/Resgrid.Web/Areas/User/Controllers/ConnectController.cs index 2b63fbf4..561e8a16 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/ConnectController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/ConnectController.cs @@ -43,7 +43,7 @@ public async Task Index() var model = new IndexView(); model.Department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); model.Profile = _departmentProfileService.GetOrInitializeDepartmentProfile(DepartmentId); - model.ImageUrl = $"{Config.SystemBehaviorConfig.ResgridApiBaseUrl}/api/v3/Avatars/Get?id={model.Profile.DepartmentId}&type=1"; + model.ImageUrl = $"{Config.SystemBehaviorConfig.ResgridApiBaseUrl}/api/v4/Avatars/Get?id={model.Profile.DepartmentId}&type=1"; var posts = _departmentProfileService.GetArticlesForDepartment(model.Profile.DepartmentProfileId); var visiblePosts = _departmentProfileService.GetVisibleArticlesForDepartment(model.Profile.DepartmentProfileId); @@ -67,7 +67,7 @@ public async Task Profile() model.ApiUrl = Config.SystemBehaviorConfig.ResgridApiBaseUrl; model.Department = await _departmentsService.GetDepartmentByUserIdAsync(UserId); - model.ImageUrl = $"{model.ApiUrl}/api/v3/Avatars/Get?id={model.Department.DepartmentId}&type=1"; + model.ImageUrl = $"{model.ApiUrl}/api/v4/Avatars/Get?id={model.Department.DepartmentId}&type=1"; var profile = _departmentProfileService.GetOrInitializeDepartmentProfile(DepartmentId); @@ -98,7 +98,7 @@ public async Task Profile(ProfileView model) { model.ApiUrl = Config.SystemBehaviorConfig.ResgridApiBaseUrl; model.Department = await _departmentsService.GetDepartmentByUserIdAsync(UserId); - model.ImageUrl = $"{model.ApiUrl}/api/v3/Avatars/Get?id={model.Department.DepartmentId}&type=1"; + model.ImageUrl = $"{model.ApiUrl}/api/v4/Avatars/Get?id={model.Department.DepartmentId}&type=1"; if (ModelState.IsValid) { diff --git a/Web/Resgrid.Web/Areas/User/Views/Dispatch/Chat.cshtml b/Web/Resgrid.Web/Areas/User/Views/Dispatch/Chat.cshtml index a9069a5a..225b43c0 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Dispatch/Chat.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Dispatch/Chat.cshtml @@ -166,7 +166,7 @@ function AddUser(chatHub, id, userId, name) { var myId = $('#myConId').val(); - var code = $('
  • ' + name + '0
  • '); + var code = $('
  • ' + name + '0
  • '); $(code).dblclick(function () { var id = $(this).attr('id'); diff --git a/Web/Resgrid.Web/Areas/User/Views/Home/EditUserProfile.cshtml b/Web/Resgrid.Web/Areas/User/Views/Home/EditUserProfile.cshtml index 1b4ac3b9..6a2c871e 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Home/EditUserProfile.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Home/EditUserProfile.cshtml @@ -292,7 +292,7 @@
    @if (Model.HasCustomIamge) { - + } else { diff --git a/Web/Resgrid.Web/Areas/User/Views/Messages/_UnreadTopMessagesPartial.cshtml b/Web/Resgrid.Web/Areas/User/Views/Messages/_UnreadTopMessagesPartial.cshtml index 7e5fbdde..3f4aece2 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Messages/_UnreadTopMessagesPartial.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Messages/_UnreadTopMessagesPartial.cshtml @@ -7,7 +7,7 @@