Skip to content

Commit 8df1b13

Browse files
committed
Add image handling utilities and enhance user response DTO in UsersController
1 parent be5257c commit 8df1b13

2 files changed

Lines changed: 128 additions & 27 deletions

File tree

Controllers/UsersController.cs

Lines changed: 127 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
using System.Security.Claims;
99
using System;
1010
using System.Collections.Generic;
11+
using System.Linq;
12+
using System.Text.Json;
1113
using FlowModels = Flowboard_Project_Management_System_Backend.Models.FlowboardModel;
1214

1315
[ApiController]
@@ -16,6 +18,8 @@
1618
public class UsersController : ControllerBase
1719
{
1820
private readonly MongoDbService _mongoDbService;
21+
private const long MaxImageSizeBytes = 5 * 1024 * 1024; // 5MB max
22+
private static readonly string[] AllowedImageTypes = { "image/jpeg", "image/png", "image/gif", "image/webp" };
1923

2024
public UsersController(MongoDbService mongoDbService)
2125
{
@@ -36,18 +40,104 @@ public UsersController(MongoDbService mongoDbService)
3640
return string.IsNullOrWhiteSpace(userId) ? null : userId;
3741
}
3842

43+
// Helper: Convert byte[] to base64 data URL
44+
private static string? BytesToDataUrl(byte[]? imageBytes)
45+
{
46+
if (imageBytes == null || imageBytes.Length == 0) return null;
47+
48+
// Detect image type from magic bytes
49+
string mimeType = "image/png"; // default
50+
if (imageBytes.Length >= 3 && imageBytes[0] == 0xFF && imageBytes[1] == 0xD8 && imageBytes[2] == 0xFF)
51+
mimeType = "image/jpeg";
52+
else if (imageBytes.Length >= 8 && imageBytes[0] == 0x89 && imageBytes[1] == 0x50 && imageBytes[2] == 0x4E && imageBytes[3] == 0x47)
53+
mimeType = "image/png";
54+
else if (imageBytes.Length >= 6 && imageBytes[0] == 0x47 && imageBytes[1] == 0x49 && imageBytes[2] == 0x46)
55+
mimeType = "image/gif";
56+
else if (imageBytes.Length >= 4 && imageBytes[0] == 0x52 && imageBytes[1] == 0x49 && imageBytes[2] == 0x46 && imageBytes[3] == 0x46)
57+
mimeType = "image/webp";
58+
59+
return $"data:{mimeType};base64,{Convert.ToBase64String(imageBytes)}";
60+
}
61+
62+
// Helper: Convert base64 data URL to byte[]
63+
private static byte[]? DataUrlToBytes(string? dataUrl)
64+
{
65+
if (string.IsNullOrWhiteSpace(dataUrl)) return null;
66+
67+
try
68+
{
69+
// Handle data URL format: data:image/png;base64,iVBORw0KGgo...
70+
if (dataUrl.StartsWith("data:"))
71+
{
72+
var commaIndex = dataUrl.IndexOf(',');
73+
if (commaIndex > 0)
74+
{
75+
dataUrl = dataUrl.Substring(commaIndex + 1);
76+
}
77+
}
78+
79+
return Convert.FromBase64String(dataUrl);
80+
}
81+
catch
82+
{
83+
return null;
84+
}
85+
}
86+
87+
// Helper: Convert User to response DTO with base64 image
88+
private object UserToResponse(FlowModels.User user)
89+
{
90+
return new
91+
{
92+
id = user.Id,
93+
userName = user.UserName,
94+
firstName = user.FirstName,
95+
lastName = user.LastName,
96+
middleName = user.MiddleName,
97+
contactNumber = user.ContactNumber,
98+
birthDate = user.BirthDate,
99+
email = user.Email,
100+
userIMG = BytesToDataUrl(user.UserIMG),
101+
createdAt = user.CreatedAt
102+
};
103+
}
104+
105+
// Helper: Extract string value from object (handles JsonElement)
106+
private static string? GetStringValue(object? value)
107+
{
108+
if (value == null) return null;
109+
110+
if (value is JsonElement jsonElement)
111+
{
112+
return jsonElement.ValueKind == JsonValueKind.Null ? null : jsonElement.GetString() ?? jsonElement.ToString();
113+
}
114+
115+
return value.ToString();
116+
}
117+
118+
// Helper: Check if value is null (handles JsonElement)
119+
private static bool IsNullValue(object? value)
120+
{
121+
if (value == null) return true;
122+
123+
if (value is JsonElement jsonElement)
124+
{
125+
return jsonElement.ValueKind == JsonValueKind.Null;
126+
}
127+
128+
return false;
129+
}
130+
39131
// Returns all users (passwords stripped) for assignment dropdowns
40132
[HttpGet]
41133
public IActionResult GetAll()
42134
{
43135
var db = _mongoDbService.GetDatabase();
44136
var usersCollection = db.GetCollection<FlowModels.User>("user");
45137
var users = usersCollection.Find(_ => true).ToList();
46-
foreach (var u in users)
47-
{
48-
u.Password = string.Empty;
49-
}
50-
return Ok(users);
138+
139+
var response = users.Select(u => UserToResponse(u)).ToList();
140+
return Ok(response);
51141
}
52142

53143
// Get a user by ID (still protected)
@@ -61,8 +151,7 @@ public IActionResult GetById(string id)
61151
var user = usersCollection.Find(u => u.Id == id).FirstOrDefault();
62152
if (user == null) return NotFound(new { message = "User not found." });
63153

64-
user.Password = string.Empty;
65-
return Ok(user);
154+
return Ok(UserToResponse(user));
66155
}
67156

68157
// PATCH /api/users/{id} - Partial update (only provided fields are updated)
@@ -89,7 +178,7 @@ public IActionResult Patch(string id, [FromBody] Dictionary<string, object> upda
89178
// email conflict check (done early if present)
90179
if (updates.TryGetValue("email", out var emailObj) && emailObj != null)
91180
{
92-
var emailStr = emailObj.ToString()?.Trim() ?? string.Empty;
181+
var emailStr = GetStringValue(emailObj)?.Trim() ?? string.Empty;
93182
if (!string.IsNullOrEmpty(emailStr))
94183
{
95184
var existing = usersCollection.Find(u => u.Email.ToLower() == emailStr.ToLower()).FirstOrDefault();
@@ -103,51 +192,62 @@ public IActionResult Patch(string id, [FromBody] Dictionary<string, object> upda
103192
{
104193
var key = kv.Key.ToLowerInvariant();
105194
var value = kv.Value;
195+
var stringValue = GetStringValue(value);
196+
106197
switch (key)
107198
{
108199
case "username":
109-
case "userName":
110200
case "user_name":
111-
updateDefs.Add(Builders<FlowModels.User>.Update.Set(u => u.UserName, value?.ToString()));
201+
if (!string.IsNullOrWhiteSpace(stringValue))
202+
updateDefs.Add(Builders<FlowModels.User>.Update.Set(u => u.UserName, stringValue));
112203
break;
113204
case "firstname":
114-
case "firstName":
115-
updateDefs.Add(Builders<FlowModels.User>.Update.Set(u => u.FirstName, value?.ToString()));
205+
if (!string.IsNullOrWhiteSpace(stringValue))
206+
updateDefs.Add(Builders<FlowModels.User>.Update.Set(u => u.FirstName, stringValue));
116207
break;
117208
case "lastname":
118-
case "lastName":
119-
updateDefs.Add(Builders<FlowModels.User>.Update.Set(u => u.LastName, value?.ToString()));
209+
if (!string.IsNullOrWhiteSpace(stringValue))
210+
updateDefs.Add(Builders<FlowModels.User>.Update.Set(u => u.LastName, stringValue));
120211
break;
121212
case "middlename":
122-
case "middleName":
123-
updateDefs.Add(Builders<FlowModels.User>.Update.Set(u => u.MiddleName, value?.ToString()));
213+
updateDefs.Add(Builders<FlowModels.User>.Update.Set(u => u.MiddleName, stringValue ?? string.Empty));
124214
break;
125215
case "contactnumber":
126216
case "contact":
127217
case "contact_number":
128-
updateDefs.Add(Builders<FlowModels.User>.Update.Set(u => u.ContactNumber, value?.ToString()));
218+
if (!string.IsNullOrWhiteSpace(stringValue))
219+
updateDefs.Add(Builders<FlowModels.User>.Update.Set(u => u.ContactNumber, stringValue));
129220
break;
130221
case "birthdate":
131222
case "birth_date":
132-
if (DateTime.TryParse(value?.ToString(), out var birthDate))
223+
if (DateTime.TryParse(stringValue, out var birthDate))
133224
updateDefs.Add(Builders<FlowModels.User>.Update.Set(u => u.BirthDate, birthDate));
134225
break;
135226
case "userimg":
136227
case "user_img":
137228
case "user_img_base64":
138-
if (value is string base64 && !string.IsNullOrWhiteSpace(base64))
229+
if (IsNullValue(value))
230+
{
231+
// Allow clearing the image
232+
updateDefs.Add(Builders<FlowModels.User>.Update.Set(u => u.UserIMG, (byte[]?)null));
233+
}
234+
else if (!string.IsNullOrWhiteSpace(stringValue))
139235
{
140-
try {
141-
var bytes = Convert.FromBase64String(base64);
236+
var bytes = DataUrlToBytes(stringValue);
237+
if (bytes != null)
238+
{
239+
if (bytes.Length > MaxImageSizeBytes)
240+
{
241+
return BadRequest(new { message = "Image size exceeds 5MB limit." });
242+
}
142243
updateDefs.Add(Builders<FlowModels.User>.Update.Set(u => u.UserIMG, bytes));
143244
}
144-
catch { /* invalid base64; ignore or respond with BadRequest? For now ignore */ }
145245
}
146246
break;
147247
case "password":
148-
if (!string.IsNullOrWhiteSpace(value?.ToString()))
248+
if (!string.IsNullOrWhiteSpace(stringValue))
149249
{
150-
var hashed = BCrypt.Net.BCrypt.HashPassword(value?.ToString() ?? string.Empty);
250+
var hashed = BCrypt.Net.BCrypt.HashPassword(stringValue);
151251
updateDefs.Add(Builders<FlowModels.User>.Update.Set(u => u.Password, hashed));
152252
}
153253
break;
@@ -174,8 +274,9 @@ public IActionResult Patch(string id, [FromBody] Dictionary<string, object> upda
174274
return NotFound(new { message = "User not found." });
175275

176276
var updatedUser = usersCollection.Find(u => u.Id == id).FirstOrDefault();
177-
if (updatedUser != null) updatedUser.Password = string.Empty;
178-
return Ok(updatedUser);
277+
if (updatedUser == null) return NotFound(new { message = "User not found after update." });
278+
279+
return Ok(UserToResponse(updatedUser));
179280
}
180281
catch (Exception ex)
181282
{
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Subproject commit 5377f3300593536d7644a12eaa9f080ac086b839
1+
Subproject commit edfaa3d7eb368fb108388a4cbdf3937f626fd004

0 commit comments

Comments
 (0)