Skip to content

Commit 2b24b51

Browse files
committed
CU-868f7n1u3 Adding in Countly for analytics. PW validation fix.
1 parent 5312ac7 commit 2b24b51

12 files changed

Lines changed: 328 additions & 16 deletions

File tree

Core/Resgrid.Config/TelemetryConfig.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ public static class TelemetryConfig
1616
public static string AptabaseBigBoardApiKey = "";
1717
public static string AptabaseDispatchApiKey = "";
1818

19+
public static string CountlyUrl = "";
20+
public static string CountlyWebKey = "";
21+
1922
public static string GetAnalyticsKey()
2023
{
2124
if (ExporterType == TelemetryExporters.PostHog)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System;
2+
using System.ComponentModel.DataAnnotations;
3+
using System.Linq;
4+
5+
namespace Resgrid.Framework
6+
{
7+
/// <summary>
8+
/// Validation attribute for password complexity requirements
9+
/// </summary>
10+
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
11+
public sealed class PasswordComplexityAttribute : ValidationAttribute
12+
{
13+
public int MinLength { get; set; } = 8;
14+
public bool RequireUppercase { get; set; } = true;
15+
public bool RequireLowercase { get; set; } = true;
16+
public bool RequireDigit { get; set; } = true;
17+
public bool RequireSpecialChar { get; set; } = false;
18+
19+
public PasswordComplexityAttribute()
20+
{
21+
ErrorMessage = "Password does not meet complexity requirements";
22+
}
23+
24+
public override bool IsValid(object value)
25+
{
26+
if (value is not string password)
27+
{
28+
return false;
29+
}
30+
31+
var result = StringHelpers.VerifyPasswordComplexity(
32+
password,
33+
MinLength,
34+
RequireUppercase,
35+
RequireLowercase,
36+
RequireDigit,
37+
RequireSpecialChar);
38+
39+
return result.IsValid;
40+
}
41+
42+
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
43+
{
44+
if (value is not string password)
45+
{
46+
return new ValidationResult("Invalid password format");
47+
}
48+
49+
var result = StringHelpers.VerifyPasswordComplexity(
50+
password,
51+
MinLength,
52+
RequireUppercase,
53+
RequireLowercase,
54+
RequireDigit,
55+
RequireSpecialChar);
56+
57+
if (result.IsValid)
58+
{
59+
return ValidationResult.Success;
60+
}
61+
62+
var errorMessage = string.Join(", ", result.Errors);
63+
return new ValidationResult(errorMessage, new[] { validationContext.MemberName });
64+
}
65+
}
66+
}

Core/Resgrid.Framework/StringHelpers.cs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@
1212

1313
namespace Resgrid.Framework
1414
{
15+
/// <summary>
16+
/// Result of password complexity verification
17+
/// </summary>
18+
public sealed record PasswordComplexityResult(
19+
bool IsValid,
20+
List<string> Errors);
21+
1522
public static class StringHelpers
1623
{
1724
/// <summary>
@@ -247,5 +254,79 @@ public static string SanitizeCoordinatesString(string source)
247254
return resultStrBuilder.ToString();
248255
}
249256

257+
/// <summary>
258+
/// Verifies password complexity against security requirements
259+
/// </summary>
260+
/// <param name="password">The password to verify</param>
261+
/// <param name="minLength">Minimum password length (default: 8)</param>
262+
/// <param name="requireUppercase">Require at least one uppercase letter (default: true)</param>
263+
/// <param name="requireLowercase">Require at least one lowercase letter (default: true)</param>
264+
/// <param name="requireDigit">Require at least one digit (default: true)</param>
265+
/// <param name="requireSpecialChar">Require at least one special character (default: false)</param>
266+
/// <returns>PasswordComplexityResult indicating validity and any errors</returns>
267+
public static PasswordComplexityResult VerifyPasswordComplexity(
268+
string password,
269+
int minLength = 8,
270+
bool requireUppercase = true,
271+
bool requireLowercase = true,
272+
bool requireDigit = true,
273+
bool requireSpecialChar = false)
274+
{
275+
var errors = new List<string>();
276+
277+
if (string.IsNullOrWhiteSpace(password))
278+
{
279+
errors.Add("Password cannot be empty");
280+
return new PasswordComplexityResult(false, errors);
281+
}
282+
283+
// Check minimum length
284+
if (password.Length < minLength)
285+
{
286+
errors.Add($"Password must be at least {minLength} characters long");
287+
}
288+
289+
// Check for uppercase letter
290+
if (requireUppercase && !password.Any(char.IsUpper))
291+
{
292+
errors.Add("Password must include an uppercase letter");
293+
}
294+
295+
// Check for lowercase letter
296+
if (requireLowercase && !password.Any(char.IsLower))
297+
{
298+
errors.Add("Password must include a lowercase letter");
299+
}
300+
301+
// Check for digit
302+
if (requireDigit && !password.Any(char.IsDigit))
303+
{
304+
errors.Add("Password must include a number (digit)");
305+
}
306+
307+
// Check for special character
308+
if (requireSpecialChar)
309+
{
310+
var specialChars = "!@#$%^&*()_+-=[]{}|;:,.<>?";
311+
if (!password.Any(c => specialChars.Contains(c)))
312+
{
313+
errors.Add("Password must include a special character");
314+
}
315+
}
316+
317+
return new PasswordComplexityResult(errors.Count == 0, errors);
318+
}
319+
320+
/// <summary>
321+
/// Validates password complexity using default Resgrid requirements
322+
/// </summary>
323+
/// <param name="password">The password to validate</param>
324+
/// <returns>True if password meets complexity requirements</returns>
325+
public static bool IsValidPassword(string password)
326+
{
327+
var result = VerifyPasswordComplexity(password);
328+
return result.IsValid;
329+
}
330+
250331
}
251332
}

Core/Resgrid.Services/PushService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public PushService(IPushLogsService pushLogsService, INotificationProvider notif
3535

3636
public async Task<bool> Register(PushUri pushUri)
3737
{
38-
if (pushUri == null || String.IsNullOrWhiteSpace(pushUri.DeviceId) || string.IsNullOrWhiteSpace(pushUri.PushLocation))
38+
if (pushUri == null || string.IsNullOrWhiteSpace(pushUri.DeviceId) || string.IsNullOrWhiteSpace(pushUri.PushLocation))
3939
return false;
4040

4141
var code = pushUri.PushLocation;
@@ -61,7 +61,7 @@ public async Task<bool> UnRegister(PushUri pushUri)
6161

6262
public async Task<bool> RegisterUnit(PushUri pushUri)
6363
{
64-
if (pushUri == null || !pushUri.UnitId.HasValue || string.IsNullOrWhiteSpace(pushUri.PushLocation))
64+
if (pushUri == null || !pushUri.UnitId.HasValue || string.IsNullOrWhiteSpace(pushUri.DeviceId) || string.IsNullOrWhiteSpace(pushUri.PushLocation))
6565
return false;
6666

6767
var unitId = pushUri.UnitId.Value;
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System;
2+
using System.Linq;
3+
using Resgrid.Framework;
4+
5+
namespace Resgrid.Tests.Framework
6+
{
7+
/// <summary>
8+
/// Example usage and tests for password complexity verification
9+
/// </summary>
10+
public static class PasswordComplexityExamples
11+
{
12+
/// <summary>
13+
/// Demonstrates how to use the password complexity verification
14+
/// </summary>
15+
public static void RunExamples()
16+
{
17+
// Test cases
18+
var testPasswords = new[]
19+
{
20+
"abc123", // Too short, no uppercase
21+
"ABC123", // No lowercase
22+
"abcDEF", // No digits
23+
"Abc123", // Valid according to default requirements
24+
"Password123", // Valid
25+
"MyPassword1", // Valid
26+
"", // Empty
27+
"Abc123!@#" // Valid with special chars
28+
};
29+
30+
Console.WriteLine("Password Complexity Verification Examples:");
31+
Console.WriteLine("=========================================");
32+
33+
foreach (var password in testPasswords)
34+
{
35+
var result = StringHelpers.VerifyPasswordComplexity(password);
36+
Console.WriteLine($"Password: '{password}'");
37+
Console.WriteLine($"Valid: {result.IsValid}");
38+
if (!result.IsValid)
39+
{
40+
Console.WriteLine($"Errors: {string.Join(", ", result.Errors)}");
41+
}
42+
Console.WriteLine();
43+
}
44+
45+
// Test with custom requirements matching current RegisterViewModel
46+
Console.WriteLine("Resgrid Default Requirements:");
47+
Console.WriteLine("============================");
48+
49+
var resgridDefaults = new[]
50+
{
51+
"abc123", // Should fail
52+
"Password1", // Should pass
53+
"MySecurePass123" // Should pass
54+
};
55+
56+
foreach (var password in resgridDefaults)
57+
{
58+
var result = StringHelpers.VerifyPasswordComplexity(
59+
password,
60+
minLength: 8,
61+
requireUppercase: true,
62+
requireLowercase: true,
63+
requireDigit: true,
64+
requireSpecialChar: false);
65+
66+
Console.WriteLine($"Password: '{password}'");
67+
Console.WriteLine($"Valid: {result.IsValid}");
68+
if (!result.IsValid)
69+
{
70+
Console.WriteLine($"Errors: {string.Join(", ", result.Errors)}");
71+
}
72+
Console.WriteLine();
73+
}
74+
}
75+
76+
/// <summary>
77+
/// Simple validation method for quick checks using Resgrid defaults
78+
/// </summary>
79+
public static bool ValidatePasswordForResgrid(string password)
80+
{
81+
return StringHelpers.VerifyPasswordComplexity(
82+
password,
83+
minLength: 8,
84+
requireUppercase: true,
85+
requireLowercase: true,
86+
requireDigit: true,
87+
requireSpecialChar: false).IsValid;
88+
}
89+
}
90+
}

Web/Resgrid.Web/Areas/User/Models/AddPersonModel.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
using System;
1+
using Microsoft.AspNetCore.Mvc.Rendering;
2+
using Resgrid.Framework;
3+
using Resgrid.Model;
4+
using Resgrid.Model.Identity;
5+
using System;
26
using System.Collections.Generic;
37
using System.ComponentModel.DataAnnotations;
4-
using Microsoft.AspNetCore.Mvc.Rendering;
5-
using Resgrid.Model.Identity;
6-
using Resgrid.Model;
78

89
namespace Resgrid.Web.Areas.User.Models
910
{
@@ -42,7 +43,13 @@ public class AddPersonModel: BaseUserModel
4243
[DataType(DataType.EmailAddress)]
4344
public string Email { get; set; }
4445

45-
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
46+
[StringLength(100, ErrorMessage = "The password must be at least 8 characters long", MinimumLength = 8)]
47+
[PasswordComplexity(
48+
MinLength = 8,
49+
RequireUppercase = true,
50+
RequireLowercase = true,
51+
RequireDigit = true,
52+
RequireSpecialChar = false)]
4653
[DataType(DataType.Password)]
4754
[Display(Name = "Password")]
4855
[Required]

Web/Resgrid.Web/Areas/User/Views/Shared/_UserLayout.cshtml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,19 @@
5858
})
5959
</script>
6060
}
61+
62+
@if (!String.IsNullOrWhiteSpace(Resgrid.Config.TelemetryConfig.CountlyUrl) && !String.IsNullOrWhiteSpace(Resgrid.Config.TelemetryConfig.CountlyWebKey))
63+
{
64+
<script type='text/javascript' src="~/lib/countly-sdk-web/lib/countly.min.js"></script>
65+
<script type='text/javascript'>
66+
if (Countly && window["Countly"]) {
67+
Countly.init({
68+
app_key: "@Resgrid.Config.TelemetryConfig.CountlyWebKey",
69+
url: "@Resgrid.Config.TelemetryConfig.CountlyUrl"
70+
});
71+
}
72+
</script>
73+
}
6174
</head>
6275
<body>
6376
<!-- Wrapper-->

Web/Resgrid.Web/Models/AccountViewModels/LoginViewModel.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ namespace Resgrid.Web.Models.AccountViewModels
55
public class LoginViewModel
66
{
77
[Required]
8-
public string Username { get; set; }
8+
[StringLength(250, ErrorMessage = "The username must be at least 2 characters long and contain only alphanumeric characters.", MinimumLength = 2)]
9+
public string Username { get; set; }
910

1011
[Required]
11-
[DataType(DataType.Password)]
12+
[StringLength(100, ErrorMessage = "The password must be at least 8 characters long, include a number (digit) and an uppercase letter", MinimumLength = 4)]
13+
[DataType(DataType.Password)]
1214
public string Password { get; set; }
1315

1416
[Display(Name = "Remember me?")]

Web/Resgrid.Web/Models/AccountViewModels/RegisterViewModel.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Resgrid.WebCore.Models;
2+
using Resgrid.Framework;
23
using System.Collections.Generic;
34
using System.ComponentModel.DataAnnotations;
45

@@ -36,7 +37,13 @@ public class RegisterViewModel: GoogleReCaptchaModelBase
3637
public string Email { get; set; }
3738

3839
[Required]
39-
[StringLength(100, ErrorMessage = "The passowrd must be at least 8 characters long, include a number (digit) and an uppercase letter", MinimumLength = 8)]
40+
[StringLength(100, ErrorMessage = "The password must be at least 8 characters long", MinimumLength = 8)]
41+
[PasswordComplexity(
42+
MinLength = 8,
43+
RequireUppercase = true,
44+
RequireLowercase = true,
45+
RequireDigit = true,
46+
RequireSpecialChar = false)]
4047
[DataType(DataType.Password)]
4148
[Display(Name = "Password")]
4249
public string Password { get; set; }

0 commit comments

Comments
 (0)