Skip to content
Merged
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
3 changes: 2 additions & 1 deletion Core/Resgrid.Model/MobileCarriers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,8 @@ public static class Carriers
MobileCarriers.Vodacom,
MobileCarriers.MTN,
MobileCarriers.TelkomMobile,
MobileCarriers.CellC
MobileCarriers.CellC,
MobileCarriers.TMobile
Comment on lines +305 to +306
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Inconsistency: TMobile added to DirectSendCarriers but CarriersMap still uses email gateway.

Adding MobileCarriers.TMobile to DirectSendCarriers implies it should use direct SMS API, but line 195 in CarriersMap still maps TMobile to the email gateway "{0}@tmomail.net". This is inconsistent with other carriers that use direct send (e.g., TracFone at line 197, BellMobility at line 203, TelusMobility at line 204 all map to "Direct").

This mismatch could cause TMobile messages to be routed incorrectly depending on which mapping the SMS sending logic consults first.

Apply this diff to align the CarriersMap with the DirectSendCarriers configuration:

-					{MobileCarriers.TMobile, "{0}@tmomail.net"},
+					{MobileCarriers.TMobile, "Direct"},

Alternatively, if TMobile should continue using the email gateway, remove it from DirectSendCarriers instead.

🤖 Prompt for AI Agents
In Core/Resgrid.Model/MobileCarriers.cs around lines 305-306 (TMobile added to
DirectSendCarriers) and CarriersMap at line ~195 (TMobile mapped to
"{0}@tmomail.net"), there's an inconsistency: either update the CarriersMap
entry for TMobile to use "Direct" (to match DirectSendCarriers) or remove
TMobile from the DirectSendCarriers list if it should continue using the email
gateway; pick one approach and make the corresponding change so both lists agree
(update the mapping to "Direct" if you want direct API sending, otherwise remove
TMobile from DirectSendCarriers).

};

public static HashSet<MobileCarriers> OnPremSmsGatewayCarriers = new HashSet<MobileCarriers>()
Expand Down
4 changes: 2 additions & 2 deletions Core/Resgrid.Services/MessageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public async Task<Message> GetMessageByIdAsync(int messageId)
public async Task<List<Message>> GetInboxMessagesByUserIdAsync(string userId)
{
var list = await _messageRepository.GetInboxMessagesByUserIdAsync(userId);
return list.ToList();
return list.OrderByDescending(x => x.SentOn).ToList();
}

public async Task<List<Message>> GetUnreadInboxMessagesByUserIdAsync(string userId)
Expand All @@ -65,7 +65,7 @@ public async Task<List<Message>> 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<Message>();
}
Expand Down
56 changes: 31 additions & 25 deletions Providers/Resgrid.Providers.Claims/ClaimsLogic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand Down
184 changes: 184 additions & 0 deletions Web/Resgrid.Web.Services/Controllers/v4/AvatarsController.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Used to interact with the user avatars (profile pictures) in the Resgrid system. The authentication header isn't required to access this method.
/// </summary>
[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;
}

/// <summary>
/// Get a users avatar from the Resgrid system based on their ID
/// </summary>
/// <param name="id">ID of the user</param>
/// <returns></returns>
[HttpGet("Get")]
[Produces(MediaTypeNames.Image.Jpeg)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> 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<ActionResult> 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<ActionResult> 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";
}
}
26 changes: 19 additions & 7 deletions Web/Resgrid.Web.Services/Controllers/v4/MessagesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -312,13 +312,19 @@ public async Task<ActionResult<SendMessageResult>> 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);
}
}
}
}
Expand All @@ -328,8 +334,11 @@ public async Task<ActionResult<SendMessageResult>> 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))
{
Expand All @@ -356,8 +365,11 @@ public async Task<ActionResult<SendMessageResult>> 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))
{
Expand Down
12 changes: 12 additions & 0 deletions Web/Resgrid.Web.Services/Resgrid.Web.Services.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Web/Resgrid.Web/Areas/User/Controllers/ConnectController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public async Task<IActionResult> 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);
Expand All @@ -67,7 +67,7 @@ public async Task<IActionResult> 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);
Expand Down Expand Up @@ -98,7 +98,7 @@ public async Task<IActionResult> 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)
{
Expand Down
Loading
Loading