Skip to content

Commit 08bab19

Browse files
author
Timothy Dodd
committed
feat(mail-group): Refactor mail group management UI and functionality
- Replaced DropdownComponent with SelectComponent for better user experience. - Updated the layout to use a more streamlined inline form for creating new mail groups. - Enhanced the display of mail groups in a table format with improved actions and status indicators. - Added retention settings management for mail groups with caching for better performance. - Implemented user management enhancements, including subdomain display and input in user creation. - Updated styles for better responsiveness and visual consistency. - Added new utility methods in AuthService for subdomain retrieval. - Adjusted CSS variable names for select components to improve clarity.
1 parent 329beb0 commit 08bab19

23 files changed

Lines changed: 1746 additions & 3602 deletions

src/MailVoidApi/Controllers/MailController.cs

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,13 @@ await db.ExecuteAsync(
5555
public async Task<IEnumerable<MailBox>> GetBoxes(bool showAll = false)
5656
{
5757
var currentUserId = _userService.GetUserId();
58-
var role = _userService.GetRole();
59-
var isAdmin = role == "Admin";
58+
var isAdmin = _userService.IsAdmin();
59+
var userSubdomain = _userService.GetSubdomain();
6060

6161
using var db = await _db.GetConnectionAsync();
6262

63-
// Get distinct mailboxes with their mail group info using a simpler approach
6463
string sql;
65-
if (isAdmin && showAll)
64+
if (isAdmin)
6665
{
6766
sql = @"SELECT DISTINCT m.`To`, m.MailGroupPath, mg.Subdomain, mg.IsPublic, mg.OwnerUserId
6867
FROM Mail m
@@ -73,12 +72,12 @@ FROM Mail m
7372
sql = @"SELECT DISTINCT m.`To`, m.MailGroupPath, mg.Subdomain, mg.IsPublic, mg.OwnerUserId
7473
FROM Mail m
7574
LEFT JOIN MailGroup mg ON m.MailGroupPath = mg.Path
76-
WHERE (mg.IsPublic = 1 OR mg.OwnerUserId = @UserId
77-
OR EXISTS (SELECT 1 FROM MailGroupUser mgu WHERE mgu.MailGroupId = mg.Id AND mgu.UserId = @UserId))
78-
OR (mg.Id IS NULL AND @IsAdmin = 1)";
75+
WHERE mg.OwnerUserId = @UserId
76+
OR mg.Subdomain = @UserSubdomain
77+
OR EXISTS (SELECT 1 FROM MailGroupUser mgu WHERE mgu.MailGroupId = mg.Id AND mgu.UserId = @UserId)";
7978
}
8079

81-
var mailboxes = await db.QueryAsync<dynamic>(sql, new { UserId = currentUserId, IsAdmin = isAdmin });
80+
var mailboxes = await db.QueryAsync<dynamic>(sql, new { UserId = currentUserId, UserSubdomain = userSubdomain });
8281

8382
var result = new List<MailBox>();
8483
foreach (var mailbox in mailboxes)
@@ -109,8 +108,8 @@ AND NOT EXISTS (SELECT 1 FROM UserMailRead umr WHERE umr.MailId = m.Id AND umr.U
109108
public async Task<PagedResults<MailWithReadStatus>> GetMails([FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] FilterOptions? options = null)
110109
{
111110
var currentUserId = _userService.GetUserId();
112-
var role = _userService.GetRole();
113-
var isAdmin = role == "Admin";
111+
var isAdmin = _userService.IsAdmin();
112+
var userSubdomain = _userService.GetSubdomain();
114113

115114
var results = new PagedResults<MailWithReadStatus>();
116115
options ??= new FilterOptions();
@@ -120,21 +119,19 @@ public async Task<PagedResults<MailWithReadStatus>> GetMails([FromBody(EmptyBody
120119
var whereClauses = new List<string>();
121120
var parameters = new DynamicParameters();
122121
parameters.Add("UserId", currentUserId);
123-
parameters.Add("IsAdmin", isAdmin);
122+
parameters.Add("UserSubdomain", userSubdomain);
124123

125124
if (!string.IsNullOrEmpty(options.To))
126125
{
127126
whereClauses.Add("m.`To` = @To");
128127
parameters.Add("To", options.To);
129128
}
130129

131-
// Filter by mail group access - user must have access to the mail group
132-
// Admins can see all, others can only see emails in groups they have access to
133130
if (!isAdmin)
134131
{
135132
whereClauses.Add(@"(
136-
mg.IsPublic = 1
137-
OR mg.OwnerUserId = @UserId
133+
mg.OwnerUserId = @UserId
134+
OR mg.Subdomain = @UserSubdomain
138135
OR EXISTS (SELECT 1 FROM MailGroupUser mgu WHERE mgu.MailGroupId = mg.Id AND mgu.UserId = @UserId)
139136
)");
140137
}
@@ -150,7 +147,7 @@ OR EXISTS (SELECT 1 FROM MailGroupUser mgu WHERE mgu.MailGroupId = mg.Id AND mgu
150147
var offset = (options.Page - 1) * options.PageSize;
151148
var mails = await db.QueryAsync<Mail>(
152149
$"SELECT m.* FROM Mail m LEFT JOIN MailGroup mg ON m.MailGroupPath = mg.Path {whereClause} ORDER BY m.CreatedOn DESC LIMIT @Limit OFFSET @Offset",
153-
new { Limit = options.PageSize, Offset = offset, To = options.To, UserId = currentUserId, IsAdmin = isAdmin });
150+
new { Limit = options.PageSize, Offset = offset, To = options.To, UserId = currentUserId, UserSubdomain = userSubdomain });
154151

155152
var mailList = mails.ToList();
156153
var mailIds = mailList.Select(m => m.Id).ToList();
@@ -201,15 +198,28 @@ public async Task<IActionResult> DeleteBox([FromBody] FilterOptions options)
201198
public async Task<IActionResult> GetMailGroups()
202199
{
203200
var userId = _userService.GetUserId();
201+
var isAdmin = _userService.IsAdmin();
202+
var userSubdomain = _userService.GetSubdomain();
204203
using var db = await _db.GetConnectionAsync();
205204

206-
var groups = await db.QueryAsync<dynamic>(
207-
@"SELECT mg.Id, mg.Path, mg.Subdomain, mg.Description, mg.IsPublic, mg.CreatedAt, mg.LastActivity, mg.OwnerUserId, mg.IsUserPrivate
208-
FROM MailGroup mg
209-
WHERE mg.Subdomain IS NOT NULL AND mg.IsUserPrivate = 0
210-
AND (mg.IsPublic = 1 OR mg.OwnerUserId = @UserId
211-
OR EXISTS (SELECT 1 FROM MailGroupUser mgu WHERE mgu.MailGroupId = mg.Id AND mgu.UserId = @UserId))",
212-
new { UserId = userId });
205+
string sql;
206+
if (isAdmin)
207+
{
208+
sql = @"SELECT mg.Id, mg.Path, mg.Subdomain, mg.Description, mg.IsPublic, mg.CreatedAt, mg.LastActivity, mg.OwnerUserId, mg.IsUserPrivate
209+
FROM MailGroup mg
210+
WHERE mg.Subdomain IS NOT NULL AND mg.IsUserPrivate = 0";
211+
}
212+
else
213+
{
214+
sql = @"SELECT mg.Id, mg.Path, mg.Subdomain, mg.Description, mg.IsPublic, mg.CreatedAt, mg.LastActivity, mg.OwnerUserId, mg.IsUserPrivate
215+
FROM MailGroup mg
216+
WHERE mg.Subdomain IS NOT NULL AND mg.IsUserPrivate = 0
217+
AND (mg.OwnerUserId = @UserId
218+
OR mg.Subdomain = @UserSubdomain
219+
OR EXISTS (SELECT 1 FROM MailGroupUser mgu WHERE mgu.MailGroupId = mg.Id AND mgu.UserId = @UserId))";
220+
}
221+
222+
var groups = await db.QueryAsync<dynamic>(sql, new { UserId = userId, UserSubdomain = userSubdomain });
213223

214224
var result = groups.Select(mg => new
215225
{
@@ -367,7 +377,8 @@ public async Task<IActionResult> GetUsers()
367377
u.Id,
368378
u.UserName,
369379
u.Role,
370-
u.TimeStamp
380+
u.TimeStamp,
381+
u.Subdomain
371382
}).ToList();
372383

373384
return Ok(result);

src/MailVoidApi/Controllers/UserManagementController.cs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ public async Task<ActionResult<List<UserDto>>> GetAllUsers()
3434
Id = u.Id.ToString(),
3535
UserName = u.UserName,
3636
Role = u.Role,
37-
TimeStamp = u.TimeStamp
37+
TimeStamp = u.TimeStamp,
38+
Subdomain = u.Subdomain
3839
}).ToList();
3940

4041
return Ok(userDtos);
@@ -61,7 +62,8 @@ public async Task<ActionResult<UserDto>> GetUser(Guid id)
6162
Id = user.Id.ToString(),
6263
UserName = user.UserName,
6364
Role = user.Role,
64-
TimeStamp = user.TimeStamp
65+
TimeStamp = user.TimeStamp,
66+
Subdomain = user.Subdomain
6567
};
6668

6769
return Ok(userDto);
@@ -87,10 +89,19 @@ public async Task<ActionResult<UserDto>> CreateUser(CreateUserDto createUserDto)
8789
return BadRequest("Username already exists.");
8890
}
8991

90-
var user = await _userManagementService.CreateUserAsync(
91-
createUserDto.UserName,
92-
createUserDto.Password,
93-
createUserDto.Role);
92+
User? user;
93+
try
94+
{
95+
user = await _userManagementService.CreateUserAsync(
96+
createUserDto.UserName,
97+
createUserDto.Password,
98+
createUserDto.Role,
99+
createUserDto.Subdomain);
100+
}
101+
catch (InvalidOperationException ex)
102+
{
103+
return BadRequest(ex.Message);
104+
}
94105

95106
if (user == null)
96107
{
@@ -102,7 +113,8 @@ public async Task<ActionResult<UserDto>> CreateUser(CreateUserDto createUserDto)
102113
Id = user.Id.ToString(),
103114
UserName = user.UserName,
104115
Role = user.Role,
105-
TimeStamp = user.TimeStamp
116+
TimeStamp = user.TimeStamp,
117+
Subdomain = user.Subdomain
106118
};
107119

108120
return CreatedAtAction(nameof(GetUser), new { id = user.Id }, userDto);
@@ -193,13 +205,15 @@ public class UserDto
193205
public required string UserName { get; set; }
194206
public Role Role { get; set; }
195207
public DateTime TimeStamp { get; set; }
208+
public string? Subdomain { get; set; }
196209
}
197210

198211
public class CreateUserDto
199212
{
200213
public required string UserName { get; set; }
201214
public required string Password { get; set; }
202215
public Role Role { get; set; } = Role.User;
216+
public string? Subdomain { get; set; }
203217
}
204218

205219
public class UpdateRoleDto

src/MailVoidApi/Models/User.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,8 @@ public class User
2424

2525
[Required]
2626
public Role Role { get; set; } = Role.User;
27+
28+
[StringLength(255)]
29+
[Index("IX_User_Subdomain", IsUnique = true)]
30+
public string? Subdomain { get; set; }
2731
}

src/MailVoidApi/Services/AuthService.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ public string GenerateJwtToken(User user)
5353
new Claim(ClaimTypes.Role, user.Role.ToString()),
5454
};
5555

56+
if (!string.IsNullOrEmpty(user.Subdomain))
57+
{
58+
claims.Add(new Claim("subdomain", user.Subdomain));
59+
}
60+
5661
// Create token using JwtSecurityToken directly for more control
5762
var token = new JwtSecurityToken(
5863
issuer: issuer,

src/MailVoidApi/Services/MailGroupService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public async Task<MailGroup> GetOrCreateMailGroup(string subdomain, Guid? userId
8484
Path = path,
8585
Subdomain = subdomain,
8686
OwnerUserId = adminUser.Id,
87-
IsPublic = true,
87+
IsPublic = false,
8888
Description = $"Auto-generated group for {subdomain} subdomain"
8989
};
9090

src/MailVoidApi/Services/UserManagementService.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public async Task<List<User>> GetAllUsersAsync()
2929
return await db.SingleByIdAsync<User>(userId);
3030
}
3131

32-
public async Task<User?> CreateUserAsync(string userName, string password, Role role = Role.User)
32+
public async Task<User?> CreateUserAsync(string userName, string password, Role role = Role.User, string? subdomain = null)
3333
{
3434
using var db = await _db.GetConnectionAsync();
3535

@@ -41,13 +41,25 @@ public async Task<List<User>> GetAllUsersAsync()
4141
return null; // Username already exists
4242
}
4343

44+
// Check if subdomain is already taken
45+
if (!string.IsNullOrWhiteSpace(subdomain))
46+
{
47+
subdomain = subdomain.Trim().ToLowerInvariant();
48+
var existingSubdomain = await db.ExistsAsync<User>(u => u.Subdomain == subdomain);
49+
if (existingSubdomain)
50+
{
51+
throw new InvalidOperationException("Subdomain is already assigned to another user.");
52+
}
53+
}
54+
4455
var user = new User
4556
{
4657
Id = Guid.NewGuid(),
4758
UserName = userName,
4859
PasswordHash = "", // Will be set below
4960
TimeStamp = DateTime.UtcNow,
50-
Role = role
61+
Role = role,
62+
Subdomain = subdomain
5163
};
5264

5365
// Hash the password

src/MailVoidApi/Services/UserService.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,20 @@ public bool IsAdmin()
5858
}
5959
}
6060

61+
public string? GetSubdomain()
62+
{
63+
var httpContext = _httpContextAccessor.HttpContext;
64+
if (httpContext?.User?.Identity is not ClaimsIdentity identity)
65+
return null;
66+
67+
var subdomainClaim = identity.FindFirst("subdomain");
68+
return subdomainClaim?.Value;
69+
}
6170
}
6271
public interface IUserService
6372
{
6473
Guid GetUserId();
6574
string GetRole();
6675
bool IsAdmin();
76+
string? GetSubdomain();
6777
}

0 commit comments

Comments
 (0)