diff --git a/.gitignore b/.gitignore index b6b7ed2e4..a52790755 100644 --- a/.gitignore +++ b/.gitignore @@ -497,6 +497,7 @@ awscredentials.xml # Used to reduce unnecessary dotnet builds .build-cache/ +<<<<<<< HEAD # Precommit build artifacts (isolated from dev server) .artifacts-precommit/ .artifacts-lint/ @@ -515,3 +516,9 @@ inspect-report/ # Agent folders .claude +CLAUDE.md + +# Temp Agent Files +DESIGN.md* +PRODUCT.md* +.github diff --git a/CLAUDE.md b/CLAUDE.md index 96bd615f1..7049b85ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,6 +49,7 @@ When I ask a question or make an observation, respond with an answer - do NOT ju ## Testing & Git +- **No VMACS production calls**: Unit tests, local environments, and test environments must NEVER send requests to the VMACS production server. All external HTTP requests (e.g., VMACS, Instinct, IAM API) must be mocked using in-memory databases and mock HTTP clients/factories to prevent production spam. - **UI**: Test UI changes with Playwright MCP (modals, forms, keyboard nav) - **API**: Use Playwright MCP to visit endpoints — APIs require browser auth, `curl` fails - **Git**: NEVER stage files until after code review. Workflow: changes → test → lint → summary → approval → stage diff --git a/test/Areas/Directory/VMACSServiceTest.cs b/test/Areas/Directory/VMACSServiceTest.cs index 68ccee68a..2c186c13c 100644 --- a/test/Areas/Directory/VMACSServiceTest.cs +++ b/test/Areas/Directory/VMACSServiceTest.cs @@ -31,7 +31,6 @@ public void BuildSearchPath_IncludesAuthToken() [Theory] [InlineData("https://vmacs-qa.example.edu")] - [InlineData("http://vmacs.example.edu/")] public void IsValidBaseUrl_True_ForAbsoluteHttpUrls(string baseUrl) { Assert.True(VMACSService.IsValidBaseUrl(baseUrl)); @@ -44,7 +43,7 @@ public void IsValidBaseUrl_True_ForAbsoluteHttpUrls(string baseUrl) [InlineData("not-a-url")] [InlineData("/relative/path")] [InlineData("vmacs-qa.example.edu")] - [InlineData("ftp://vmacs.example.edu")] + [InlineData("ftp://vmacs-qa.example.edu")] [InlineData("file:///etc/passwd")] public void IsValidBaseUrl_False_ForMissingOrNonHttpUrls(string? baseUrl) { diff --git a/test/Services/UserInfoServiceTests.cs b/test/Services/UserInfoServiceTests.cs new file mode 100644 index 000000000..7ab122ffc --- /dev/null +++ b/test/Services/UserInfoServiceTests.cs @@ -0,0 +1,205 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; +using NSubstitute; +using System.Net; +using System.Text; +using Viper.Areas.Directory.Models; +using Viper.Areas.Directory.Services; +using Viper.Classes.SQLContext; +using Viper.Models.AAUD; +using Viper.Models.Courses; + +namespace Viper.test.Services +{ + public class UserInfoServiceTests + { + private readonly ITestOutputHelper _output; + + public UserInfoServiceTests(ITestOutputHelper output) + { + _output = output; + } + + private static DbContextOptions CreateInMemoryOptions() where TContext : DbContext + { + return new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + } + + [Fact] + public async Task TestGetUserInfo_WithSyntheticData() + { + _output.WriteLine("[DEBUG] Starting TestGetUserInfo with fully synthetic data..."); + + // 1. Arrange Config & Cache + var configData = new Dictionary + { + { "Instinct:ApiUrl", "https://synthetic.instinctvet.com/" }, + { "Credentials:InstinctApi", "synthetic-password" } + }; + var config = new ConfigurationBuilder() + .AddInMemoryCollection(configData) + .Build(); + + var memoryCache = new MemoryCache(new MemoryCacheOptions()); + + // 2. Mock HttpClientFactory + var mockHandler = new MockHttpMessageHandler(request => + { + var uri = request.RequestUri?.ToString() ?? ""; + if (uri.Contains("auth/token")) + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{\"access_token\": \"synthetic-token\", \"expires_in\": 86400}", Encoding.UTF8, "application/json") + }; + } + if (uri.Contains("query=")) + { + var searchJson = @" + { + ""data"": { + ""searchUsers"": [ + { + ""id"": ""inst-synthetic-id"", + ""instinctId"": ""inst-synthetic-id"", + ""status"": ""Active"", + ""isActive"": true, + ""username"": ""jsmith"", + ""nameFirst"": ""John"", + ""nameLast"": ""Smith"", + ""roles"": [ + { ""label"": ""Staff"" } + ] + } + ] + } + }"; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(searchJson, Encoding.UTF8, "application/json") + }; + } + return new HttpResponseMessage(HttpStatusCode.NotFound); + }); + + var httpClientFactory = Substitute.For(); + httpClientFactory.CreateClient(Arg.Any()).Returns(_ => new HttpClient(mockHandler)); + + // 3. Setup DbContexts and Seed Synthetic Data + var aaudOptions = CreateInMemoryOptions(); + var coursesOptions = CreateInMemoryOptions(); + + using (var aaudSetup = new AAUDContext(aaudOptions)) + { + var syntheticUser = new AaudUser + { + IamId = "99999999", + MothraId = "88888888", + MailId = "jsmith", + LoginId = "jsmith", + FirstName = "John", + LastName = "Smith", + DisplayFirstName = "John", + DisplayLastName = "Smith", + DisplayFullName = "John Smith", + Current = 1, + EmployeeId = "emp-999", + EmployeePKey = "pkey-999", + ClientId = "1" + }; + aaudSetup.AaudUsers.Add(syntheticUser); + + aaudSetup.Employees.Add(new Employee + { + EmpPKey = "pkey-999", + EmpTermCode = "202610", + EmpPrimaryTitle = "Synthetic Analyst", + EmpSchoolDivision = "Synthetic Division", + EmpStatus = "A", + EmpHomeDept = "Synthetic Dept", + EmpClientid = "1", + EmpAltDeptCode = "", + EmpCbuc = "" + }); + + await aaudSetup.SaveChangesAsync(TestContext.Current.CancellationToken); + } + + using (var coursesSetup = new CoursesContext(coursesOptions)) + { + coursesSetup.Terminfos.Add(new Terminfo + { + TermCode = "202610", + TermCurrentTermMulti = true, + TermAcademicYear = "", + TermDesc = "", + TermCollCode = "01", + TermStartDate = DateTime.Today, + TermEndDate = DateTime.Today, + TermCurrentTerm = true, + TermTermType = "" + }); + await coursesSetup.SaveChangesAsync(TestContext.Current.CancellationToken); + } + + using var aaud = new AAUDContext(aaudOptions); + using var raps = new RAPSContext(CreateInMemoryOptions()); + using var courses = new CoursesContext(coursesOptions); + using var loans = new EquipmentLoanContext(CreateInMemoryOptions()); + using var pps = new PPSContext(CreateInMemoryOptions()); + using var idcards = new IDCardsContext(CreateInMemoryOptions()); + using var keys = new KeysContext(CreateInMemoryOptions()); + + var userInfoService = new UserInfoService(aaud, raps, courses, loans, pps, idcards, keys, config, httpClientFactory, memoryCache); + + // 4. Act + // Temporary HttpHelper configuration inside the test context + var mockEnv = Substitute.For(); + HttpHelper.Configure(memoryCache, config, mockEnv, null, null, null); + + UserInfoResult? result; + try + { + result = await userInfoService.GetUserInfoAsync("99999999", "88888888"); + } + finally + { + HttpHelper.Configure(null, null, null!, null, null, null); + } + + // 5. Assert + Assert.NotNull(result); + _output.WriteLine($"[DEBUG] Result IamId: {result.IamId}"); + _output.WriteLine($"[DEBUG] Result DisplayName: {result.DisplayFullName}"); + _output.WriteLine($"[DEBUG] Result InstinctId: {result.InstinctId}"); + _output.WriteLine($"[DEBUG] Result InstinctInfo.ErrorMessage: {result.InstinctInfo?.ErrorMessage}"); + _output.WriteLine($"[DEBUG] Result InstinctInfo.Valid: {result.InstinctInfo?.Valid}"); + + Assert.Equal("99999999", result.IamId); + Assert.Equal("88888888", result.MothraId); + Assert.Equal("John Smith", result.DisplayFullName); + Assert.True(result.IsEmployee); + Assert.Equal("Synthetic Analyst", result.EmployeePrimaryTitle); + Assert.Equal("inst-synthetic-id", result.InstinctId); + Assert.Equal("jsmith", result.InstinctUsername); + } + + private class MockHttpMessageHandler : HttpMessageHandler + { + private readonly Func _handler; + + public MockHttpMessageHandler(Func handler) + { + _handler = handler; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(_handler(request)); + } + } + } +} diff --git a/test/Services/UserInfoServiceUnitTests.cs b/test/Services/UserInfoServiceUnitTests.cs new file mode 100644 index 000000000..39f6e4104 --- /dev/null +++ b/test/Services/UserInfoServiceUnitTests.cs @@ -0,0 +1,806 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; +using NSubstitute; +using System.Net; +using System.Text; +using Viper.Areas.Directory.Services; +using Viper.Classes.SQLContext; +using Viper.Classes.Utilities; +using Viper.Models.AAUD; +using Viper.Models.EquipmentLoan; +using Viper.Models.IDCards; +using Viper.Models.Keys; +using Viper.Models.RAPS; +using Viper.Models.Courses; + +namespace Viper.test.Services +{ + public class UserInfoServiceUnitTests + { + private readonly IMemoryCache _memoryCache; + private readonly IConfiguration _configuration; + + public UserInfoServiceUnitTests() + { + _memoryCache = new MemoryCache(new MemoryCacheOptions()); + + var configData = new Dictionary + { + { "Instinct:ApiUrl", "https://uc-davis.api.instinctvet.com/" }, + { "Credentials:InstinctApi", "dummy-password" } + }; + _configuration = new ConfigurationBuilder() + .AddInMemoryCollection(configData) + .Build(); + } + + // Helper to construct DbContextOptions for InMemory providers + private static DbContextOptions CreateInMemoryOptions() where TContext : DbContext + { + return new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + } + + // Helper to construct a mocked IHttpClientFactory using custom handler + private static IHttpClientFactory CreateMockHttpClientFactory(Func handlerFunc) + { + var factory = Substitute.For(); + factory.CreateClient(Arg.Any()).Returns(_ => new HttpClient(new MockHttpMessageHandler(handlerFunc))); + return factory; + } + + private AaudUser CreateTestUser( + string iamId, + string mothraId, + string loginId = "testuser", + string? employeeId = null, + string? employeePKey = null, + string? studentPKey = null, + string? firstName = "Jane", + string? lastName = "Doe", + string? middleName = null, + string? displayFullName = "Jane Doe", + string? pidm = null) + { + return new AaudUser + { + IamId = iamId, + MothraId = mothraId, + MailId = loginId, + LoginId = loginId, + EmployeeId = employeeId, + EmployeePKey = employeePKey, + StudentPKey = studentPKey, + Current = 1, + ClientId = "1", + FirstName = firstName ?? "Jane", + LastName = lastName ?? "Doe", + MiddleName = middleName, + DisplayFirstName = firstName ?? "Jane", + DisplayLastName = lastName ?? "Doe", + DisplayFullName = displayFullName ?? "Jane Doe", + Pidm = pidm + }; + } + + [Fact] + public async Task GetUserInfoAsync_UserNotFound_ReturnsNull() + { + // Arrange + using var aaud = new AAUDContext(CreateInMemoryOptions()); + using var raps = new RAPSContext(CreateInMemoryOptions()); + using var courses = new CoursesContext(CreateInMemoryOptions()); + using var loans = new EquipmentLoanContext(CreateInMemoryOptions()); + using var pps = new PPSContext(CreateInMemoryOptions()); + using var idcards = new IDCardsContext(CreateInMemoryOptions()); + using var keys = new KeysContext(CreateInMemoryOptions()); + + var httpFactory = CreateMockHttpClientFactory(_ => new HttpResponseMessage(HttpStatusCode.NotFound)); + + var service = new UserInfoService(aaud, raps, courses, loans, pps, idcards, keys, _configuration, httpFactory, _memoryCache); + + // Act + var result = await service.GetUserInfoAsync("non-existent-iam", "non-existent-mothra"); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task GetUserInfoAsync_UserFoundByIamId_MapsProperties() + { + // Arrange + var aaudOptions = CreateInMemoryOptions(); + using (var aaudSetup = new AAUDContext(aaudOptions)) + { + aaudSetup.AaudUsers.Add(CreateTestUser("iam-123", "mothra-123", employeeId: "emp-123", pidm: "pidm-123", displayFullName: "Test User")); + await aaudSetup.SaveChangesAsync(TestContext.Current.CancellationToken); + } + + using var aaud = new AAUDContext(aaudOptions); + using var raps = new RAPSContext(CreateInMemoryOptions()); + using var courses = new CoursesContext(CreateInMemoryOptions()); + using var loans = new EquipmentLoanContext(CreateInMemoryOptions()); + using var pps = new PPSContext(CreateInMemoryOptions()); + using var idcards = new IDCardsContext(CreateInMemoryOptions()); + using var keys = new KeysContext(CreateInMemoryOptions()); + + var httpFactory = CreateMockHttpClientFactory(_ => new HttpResponseMessage(HttpStatusCode.NotFound)); + + var service = new UserInfoService(aaud, raps, courses, loans, pps, idcards, keys, _configuration, httpFactory, _memoryCache); + + // Act + var result = await service.GetUserInfoAsync("iam-123", null); + + // Assert + Assert.NotNull(result); + Assert.Equal("iam-123", result.IamId); + Assert.Equal("mothra-123", result.MothraId); + Assert.Equal("Test User", result.DisplayFullName); + Assert.True(result.CurrentAffiliate); + } + + [Fact] + public async Task PopulateEmployeeInfoAsync_PopulatesDetails() + { + // Arrange + var aaudOptions = CreateInMemoryOptions(); + var coursesOptions = CreateInMemoryOptions(); + + using (var aaudSetup = new AAUDContext(aaudOptions)) + { + aaudSetup.AaudUsers.Add(CreateTestUser("iam-emp", "mothra-emp", employeeId: "emp-789", employeePKey: "101")); + aaudSetup.Employees.Add(new Employee + { + EmpPKey = "101", + EmpTermCode = "202610", + EmpPrimaryTitle = "Senior Developer", + EmpSchoolDivision = "SVM Dean's Office", + EmpStatus = "A", + EmpHomeDept = "SVM Dean", + EmpEffortHomeDept = "SVM Effort Dept", + EmpTeachingHomeDept = "SVM Teaching Dept", + EmpTeachingPercentFulltime = 85, + EmpClientid = "1", + EmpAltDeptCode = "", + EmpCbuc = "" + }); + await aaudSetup.SaveChangesAsync(TestContext.Current.CancellationToken); + } + + using (var coursesSetup = new CoursesContext(coursesOptions)) + { + coursesSetup.Terminfos.Add(new Terminfo + { + TermCode = "202610", + TermCurrentTermMulti = true, + TermAcademicYear = "", + TermDesc = "", + TermCollCode = "01", + TermStartDate = DateTime.Today, + TermEndDate = DateTime.Today, + TermCurrentTerm = true, + TermTermType = "" + }); + await coursesSetup.SaveChangesAsync(TestContext.Current.CancellationToken); + } + + using var aaud = new AAUDContext(aaudOptions); + using var raps = new RAPSContext(CreateInMemoryOptions()); + using var courses = new CoursesContext(coursesOptions); + using var loans = new EquipmentLoanContext(CreateInMemoryOptions()); + using var pps = new PPSContext(CreateInMemoryOptions()); + using var idcards = new IDCardsContext(CreateInMemoryOptions()); + using var keys = new KeysContext(CreateInMemoryOptions()); + + var httpFactory = CreateMockHttpClientFactory(_ => new HttpResponseMessage(HttpStatusCode.NotFound)); + var service = new UserInfoService(aaud, raps, courses, loans, pps, idcards, keys, _configuration, httpFactory, _memoryCache); + + // Act + var result = await service.GetUserInfoAsync("iam-emp", null); + + // Assert + Assert.NotNull(result); + Assert.True(result.IsEmployee); + Assert.Equal("Senior Developer", result.EmployeePrimaryTitle); + Assert.Equal("SVM Dean's Office", result.EmployeeSchoolDivision); + Assert.Equal("A", result.EmployeeStatus); + Assert.Equal("202610", result.EmployeeTerm); + Assert.Equal("SVM Dean", result.EmployeeHomeDepartment); + Assert.Equal("SVM Effort Dept", result.EmployeeEffortHomeDepartment); + Assert.Equal("SVM Teaching Dept", result.EmployeeTeachingHomeDepartment); + Assert.Equal("85", result.EmployeeTeachingPercentFulltime); + } + + [Fact] + public async Task PopulateIDCardsAsync_ExecutesStatusAndReasonJoins() + { + // Arrange + var aaudOptions = CreateInMemoryOptions(); + using (var aaudSetup = new AAUDContext(aaudOptions)) + { + aaudSetup.AaudUsers.Add(CreateTestUser("iam-cards", "mothra-cards", "carduser")); + await aaudSetup.SaveChangesAsync(TestContext.Current.CancellationToken); + } + + var idcardsOptions = CreateInMemoryOptions(); + using (var idcardsSetup = new IDCardsContext(idcardsOptions)) + { + idcardsSetup.IdCards.Add(new IdCard + { + IdCardLoginId = "carduser", + IdCardNumber = 987654321, + IdCardDisplayName = "CardDisplayName", + IdCardLastName = "CardLastName", + IdCardLine2 = "Line 2 Text", + IdCardCurrentStatus = "A", + IdcardDeactivatedReason = "L", + IdCardAppliedDate = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Unspecified), + IdCardIssueDate = new DateTime(2026, 1, 2, 0, 0, 0, DateTimeKind.Unspecified), + IdcardDeactivatedDate = new DateTime(2026, 6, 1, 0, 0, 0, DateTimeKind.Unspecified) + }); + idcardsSetup.DvtCardStatuses.Add(new DvtCardStatus + { + DvtStatusCode = "A", + DvtStatusDesc = "Active Card Status", + DvtStatusVoidable = "", + DvtStatusDupOk = "" + }); + idcardsSetup.DvtReasons.Add(new DvtReason + { + DvtReasonCode = "L", + DvtReasonDesc = "Lost Card", + DvtReasonVoidable = "", + DvtReasonDupOk = "" + }); + await idcardsSetup.SaveChangesAsync(TestContext.Current.CancellationToken); + } + + using var aaud = new AAUDContext(aaudOptions); + using var raps = new RAPSContext(CreateInMemoryOptions()); + using var courses = new CoursesContext(CreateInMemoryOptions()); + using var loans = new EquipmentLoanContext(CreateInMemoryOptions()); + using var pps = new PPSContext(CreateInMemoryOptions()); + using var idcards = new IDCardsContext(idcardsOptions); + using var keys = new KeysContext(CreateInMemoryOptions()); + + var httpFactory = CreateMockHttpClientFactory(_ => new HttpResponseMessage(HttpStatusCode.NotFound)); + var service = new UserInfoService(aaud, raps, courses, loans, pps, idcards, keys, _configuration, httpFactory, _memoryCache); + + // Act + var result = await service.GetUserInfoAsync("iam-cards", null); + + // Assert + Assert.NotNull(result); + Assert.Single(result.IDCards); + var card = result.IDCards[0]; + Assert.Equal("987654321", card.Number); + Assert.Equal("CardDisplayName", card.DisplayName); + Assert.Equal("CardLastName", card.LastName); + Assert.Equal("Line 2 Text", card.Line2); + Assert.Equal("Active Card Status", card.StatusDescription); + Assert.Equal("Lost Card", card.DeactivatedReason); + Assert.Equal(new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Unspecified), card.Applied); + Assert.Equal(new DateTime(2026, 1, 2, 0, 0, 0, DateTimeKind.Unspecified), card.Issued); + Assert.Equal(new DateTime(2026, 6, 1, 0, 0, 0, DateTimeKind.Unspecified), card.Deactivated); + } + + [Fact] + public async Task PopulateKeysAsync_MapsKeyDetails() + { + // Arrange + var aaudOptions = CreateInMemoryOptions(); + using (var aaudSetup = new AAUDContext(aaudOptions)) + { + aaudSetup.AaudUsers.Add(CreateTestUser("iam-keys", "mothra-keys")); + aaudSetup.AaudUsers.Add(CreateTestUser("iam-issuer", "mothra-issuer", "issuer", displayFullName: "John KeyIssuer")); + await aaudSetup.SaveChangesAsync(TestContext.Current.CancellationToken); + } + + var keysOptions = CreateInMemoryOptions(); + using (var keysSetup = new KeysContext(keysOptions)) + { + keysSetup.Keys.Add(new Key + { + KeyId = 202, + KeyNumber = "K99", + AccessDescription = "Main Gate Access", + CreatedBy = "system" + }); + keysSetup.KeyAssignments.Add(new KeyAssignment + { + KeyId = 202, + AssignedTo = "mothra-keys", + CutNumber = "C1", + IssuedDate = new DateTime(2026, 3, 1, 0, 0, 0, DateTimeKind.Unspecified), + IssuedBy = "mothra-issuer", + Deleted = null + }); + await keysSetup.SaveChangesAsync(TestContext.Current.CancellationToken); + } + + using var aaud = new AAUDContext(aaudOptions); + using var raps = new RAPSContext(CreateInMemoryOptions()); + using var courses = new CoursesContext(CreateInMemoryOptions()); + using var loans = new EquipmentLoanContext(CreateInMemoryOptions()); + using var pps = new PPSContext(CreateInMemoryOptions()); + using var idcards = new IDCardsContext(CreateInMemoryOptions()); + using var keys = new KeysContext(keysOptions); + + var httpFactory = CreateMockHttpClientFactory(_ => new HttpResponseMessage(HttpStatusCode.NotFound)); + var service = new UserInfoService(aaud, raps, courses, loans, pps, idcards, keys, _configuration, httpFactory, _memoryCache); + + // Act + var result = await service.GetUserInfoAsync("iam-keys", null); + + // Assert + Assert.NotNull(result); + Assert.Single(result.Keys); + var assignment = result.Keys[0]; + Assert.Equal("Main Gate Access", assignment.AccessDescription); + Assert.Equal("K99", assignment.KeyNumber); + Assert.Equal("C1", assignment.CutNumber); + Assert.Equal(new DateTime(2026, 3, 1, 0, 0, 0, DateTimeKind.Unspecified), assignment.IssuedDate); + Assert.Equal("John KeyIssuer", assignment.IssuedBy); + } + + [Fact] + public async Task PopulateLoansAsync_MapsLoanDetails() + { + // Arrange + var aaudOptions = CreateInMemoryOptions(); + using (var aaudSetup = new AAUDContext(aaudOptions)) + { + aaudSetup.AaudUsers.Add(CreateTestUser("iam-loans", "mothra-loans", pidm: "pidm-loans")); + await aaudSetup.SaveChangesAsync(TestContext.Current.CancellationToken); + } + + var loansOptions = CreateInMemoryOptions(); + using (var loansSetup = new EquipmentLoanContext(loansOptions)) + { + var newLoan = new Loan + { + LoanId = 505, + LoanPidm = "pidm-loans", + LoanTechPidm = "tech-pidm-loans", + LoanDate = new DateTime(2026, 5, 1, 0, 0, 0, DateTimeKind.Unspecified), + LoanDueDate = new DateTime(2026, 5, 10, 0, 0, 0, DateTimeKind.Unspecified), + LoanComments = "Projector Loan" + }; + loansSetup.Loans.Add(newLoan); + + var asset = new Asset + { + AssetId = 808, + AssetName = "Epson Projector 4K", + AssetStatus = "A" + }; + loansSetup.Assets.Add(asset); + + loansSetup.LoanItems.Add(new LoanItem + { + LoanitemLoanid = 505, + LoanitemAssetid = 808, + LoanitemAsset = asset, + LoanitemCheckout = DateTime.Today, + LoanitemCheckoutPidm = "checkout-pidm" + }); + + await loansSetup.SaveChangesAsync(TestContext.Current.CancellationToken); + } + + using var aaud = new AAUDContext(aaudOptions); + using var raps = new RAPSContext(CreateInMemoryOptions()); + using var courses = new CoursesContext(CreateInMemoryOptions()); + using var loans = new EquipmentLoanContext(loansOptions); + using var pps = new PPSContext(CreateInMemoryOptions()); + using var idcards = new IDCardsContext(CreateInMemoryOptions()); + using var keys = new KeysContext(CreateInMemoryOptions()); + + var httpFactory = CreateMockHttpClientFactory(_ => new HttpResponseMessage(HttpStatusCode.NotFound)); + var service = new UserInfoService(aaud, raps, courses, loans, pps, idcards, keys, _configuration, httpFactory, _memoryCache); + + // Act + var result = await service.GetUserInfoAsync("iam-loans", null); + + // Assert + Assert.NotNull(result); + Assert.Single(result.Loans); + var loanResult = result.Loans[0]; + Assert.Equal("Epson Projector 4K", loanResult.AssetName); + Assert.Equal(new DateTime(2026, 5, 1, 0, 0, 0, DateTimeKind.Unspecified), loanResult.LoanDate); + Assert.Equal(new DateTime(2026, 5, 10, 0, 0, 0, DateTimeKind.Unspecified), loanResult.DueDate); + Assert.Equal("Projector Loan", loanResult.Comments); + } + + [Fact] + public async Task PopulateSystemRolesAndPermissions_QueriesCorrectly() + { + // Arrange + var aaudOptions = CreateInMemoryOptions(); + using (var aaudSetup = new AAUDContext(aaudOptions)) + { + aaudSetup.AaudUsers.Add(CreateTestUser("iam-raps", "mothra-raps")); + await aaudSetup.SaveChangesAsync(TestContext.Current.CancellationToken); + } + + var rapsOptions = CreateInMemoryOptions(); + using (var rapsSetup = new RAPSContext(rapsOptions)) + { + var role = new TblRole + { + RoleId = 10, + Role = "CN=DirectoryAdmin,OU=Roles,DC=viper", + DisplayName = "DirectoryAdmin" + }; + rapsSetup.TblRoles.Add(role); + + rapsSetup.TblRoleMembers.Add(new TblRoleMember + { + RoleId = 10, + MemberId = "mothra-raps", + Role = role, + ViewName = null + }); + + var perm1 = new TblPermission + { + PermissionId = 50, + Permission = "RAPS.User.View" + }; + var perm2 = new TblPermission + { + PermissionId = 60, + Permission = "API.Data.Read" + }; + rapsSetup.TblPermissions.AddRange(perm1, perm2); + + // Assign perm1 via role + rapsSetup.TblRolePermissions.Add(new TblRolePermission + { + RoleId = 10, + PermissionId = 50, + Access = 1 + }); + + // Assign perm2 directly + rapsSetup.TblMemberPermissions.Add(new TblMemberPermission + { + MemberId = "mothra-raps", + PermissionId = 60, + Access = 1, + StartDate = null, + EndDate = null + }); + + await rapsSetup.SaveChangesAsync(TestContext.Current.CancellationToken); + } + + using var aaud = new AAUDContext(aaudOptions); + using var raps = new RAPSContext(rapsOptions); + using var courses = new CoursesContext(CreateInMemoryOptions()); + using var loans = new EquipmentLoanContext(CreateInMemoryOptions()); + using var pps = new PPSContext(CreateInMemoryOptions()); + using var idcards = new IDCardsContext(CreateInMemoryOptions()); + using var keys = new KeysContext(CreateInMemoryOptions()); + + var httpFactory = CreateMockHttpClientFactory(_ => new HttpResponseMessage(HttpStatusCode.NotFound)); + var service = new UserInfoService(aaud, raps, courses, loans, pps, idcards, keys, _configuration, httpFactory, _memoryCache); + + // Act + var result = await service.GetUserInfoAsync("iam-raps", null); + + // Assert + Assert.NotNull(result); + + // Check formatted Roles + Assert.Single(result.SystemRoles); + Assert.Equal("VIPER", result.SystemRoles[0].System); + Assert.Equal("DirectoryAdmin", result.SystemRoles[0].DisplayName); + + // Check permissions categories + var rapsPermCategory = result.SystemPermissions.FirstOrDefault(p => p.Category == "RAPS"); + Assert.NotNull(rapsPermCategory); + Assert.Equal(1, rapsPermCategory.Count); + Assert.Equal("RAPS.User.View", rapsPermCategory.Permissions[0]); + + var apiPermCategory = result.SystemPermissions.FirstOrDefault(p => p.Category == "API"); + Assert.NotNull(apiPermCategory); + Assert.Equal(1, apiPermCategory.Count); + Assert.Equal("API.Data.Read", apiPermCategory.Permissions[0]); + } + + [Fact] + public async Task PopulateIamInfoAsync_CallsApiAndMapsCollections() + { + // Arrange + var aaudOptions = CreateInMemoryOptions(); + using (var aaudSetup = new AAUDContext(aaudOptions)) + { + aaudSetup.AaudUsers.Add(CreateTestUser("iam-caller", "mothra-caller")); + await aaudSetup.SaveChangesAsync(TestContext.Current.CancellationToken); + } + + // Mock responses for SearchForPerson (iam/people/search) and GetEmployeeAssociations (iam/associations/pps/{iamId}) + var httpFactory = CreateMockHttpClientFactory(request => + { + var uri = request.RequestUri?.ToString() ?? ""; + if (uri.Contains("iam/people/search")) + { + var json = @" + { + ""responseStatus"": 0, + ""responseData"": { + ""results"": [ + { + ""iamId"": ""iam-caller"", + ""oFirstName"": ""Jane"", + ""oLastName"": ""Doe"", + ""oFullName"": ""Full Name From IAM"", + ""ppsId"": ""pps-111"", + ""isEmployee"": true, + ""isStudent"": false + } + ] + } + }"; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(json, Encoding.UTF8, "application/json") + }; + } + else if (uri.Contains("iam/associations/pps/iam-caller")) + { + var json = @" + { + ""responseStatus"": 0, + ""responseData"": { + ""results"": [ + { + ""iamId"": ""iam-caller"", + ""titleDisplayName"": ""Manager"", + ""titleCode"": ""001100"", + ""deptDisplayName"": ""VetMed Dean"", + ""deptCode"": ""062000"", + ""percentFullTime"": ""1.0"", + ""assocStartDate"": ""2026-01-01"" + } + ] + } + }"; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(json, Encoding.UTF8, "application/json") + }; + } + return new HttpResponseMessage(HttpStatusCode.NotFound); + }); + + using var aaud = new AAUDContext(aaudOptions); + using var raps = new RAPSContext(CreateInMemoryOptions()); + using var courses = new CoursesContext(CreateInMemoryOptions()); + using var loans = new EquipmentLoanContext(CreateInMemoryOptions()); + using var pps = new PPSContext(CreateInMemoryOptions()); + using var idcards = new IDCardsContext(CreateInMemoryOptions()); + using var keys = new KeysContext(CreateInMemoryOptions()); + + var service = new UserInfoService(aaud, raps, courses, loans, pps, idcards, keys, _configuration, httpFactory, _memoryCache); + + // Act + var result = await service.GetUserInfoAsync("iam-caller", null); + + // Assert + Assert.NotNull(result); + Assert.Equal("pps-111", result.PPSId); + Assert.Equal("Full Name From IAM", result.OFullName); + + // Check populated collections + Assert.Single(result.IamPeople); + Assert.Equal("Full Name From IAM", result.IamPeople[0].OFullName); + + Assert.Single(result.IamAssociations); + Assert.Equal("Manager", result.IamAssociations[0].TitleDisplayName); + Assert.Equal("VetMed Dean", result.IamAssociations[0].DeptDisplayName); + } + + [Fact] + public async Task PopulateInstinctInfoAsync_ResolvesEndpointAndQueriesGraphQL() + { + // Arrange + var aaudOptions = CreateInMemoryOptions(); + using (var aaudSetup = new AAUDContext(aaudOptions)) + { + aaudSetup.AaudUsers.Add(CreateTestUser("iam-inst", "mothra-inst", firstName: "Jane", lastName: "Doe", middleName: "Alex")); + await aaudSetup.SaveChangesAsync(TestContext.Current.CancellationToken); + } + + // HTTP Mock for token request and graphql user lookup + var httpFactory = CreateMockHttpClientFactory(request => + { + var uri = request.RequestUri?.ToString() ?? ""; + if (uri.Contains("auth/token")) + { + var tokenJson = "{\"access_token\": \"jane-doe-token-key\", \"expires_in\": 86400}"; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(tokenJson, Encoding.UTF8, "application/json") + }; + } + else if (uri.Contains("query=")) + { + var searchJson = @" + { + ""data"": { + ""searchUsers"": [ + { + ""id"": ""inst-jane-doe"", + ""instinctId"": ""inst-jane-doe"", + ""status"": ""Active"", + ""isActive"": true, + ""username"": ""jdoe"", + ""nameFirst"": ""Jane"", + ""nameLast"": ""Doe"", + ""roles"": [ + { ""label"": ""Doctor"" } + ] + } + ] + } + }"; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(searchJson, Encoding.UTF8, "application/json") + }; + } + return new HttpResponseMessage(HttpStatusCode.NotFound); + }); + + using var aaud = new AAUDContext(aaudOptions); + using var raps = new RAPSContext(CreateInMemoryOptions()); + using var courses = new CoursesContext(CreateInMemoryOptions()); + using var loans = new EquipmentLoanContext(CreateInMemoryOptions()); + using var pps = new PPSContext(CreateInMemoryOptions()); + using var idcards = new IDCardsContext(CreateInMemoryOptions()); + using var keys = new KeysContext(CreateInMemoryOptions()); + + var service = new UserInfoService(aaud, raps, courses, loans, pps, idcards, keys, _configuration, httpFactory, _memoryCache); + + // Act & Assert with temporary HttpHelper configuration + var mockEnv = Substitute.For(); + HttpHelper.Configure(_memoryCache, _configuration, mockEnv, null, null, null); + try + { + var result = await service.GetUserInfoAsync("iam-inst", null); + + Assert.NotNull(result); + Assert.Null(result.InstinctInfo?.ErrorMessage); + Assert.Equal("inst-jane-doe", result.InstinctId); + Assert.Equal("jdoe", result.InstinctUsername); + Assert.Equal("Active", result.InstinctStatus); + Assert.True(result.InstinctIsActive); + Assert.Single(result.InstinctRoles); + Assert.Equal("Doctor", result.InstinctRoles[0]); + } + finally + { + HttpHelper.Configure(null, null, null!, null, null, null); + } + } + [Fact] + public async Task TestGetEmployeeAssociationsDirectly() + { + var httpFactory = CreateMockHttpClientFactory(_ => + { + var json = @" + { + ""responseStatus"": 0, + ""responseData"": { + ""results"": [ + { + ""iamId"": ""iam-caller"", + ""titleDisplayName"": ""Manager"", + ""titleCode"": ""001100"", + ""deptDisplayName"": ""VetMed Dean"", + ""deptCode"": ""062000"", + ""percentFullTime"": ""1.0"", + ""assocStartDate"": ""2026-01-01"" + } + ] + } + }"; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(json, Encoding.UTF8, "application/json") + }; + }); + + var iamApi = new IamApi(httpFactory); + var response = await iamApi.GetEmployeeAssociations("iam-caller"); + Assert.Null(response.ErrorMessage); + Assert.NotNull(response.Data); + Assert.Single(response.Data); + } + + [Fact] + public async Task TestDeserializeCorePersonDirectly() + { + var httpFactory = CreateMockHttpClientFactory(_ => + { + var json = @" + { + ""responseStatus"": 0, + ""responseData"": { + ""results"": [ + { + ""iamId"": ""iam-caller"", + ""oFirstName"": ""Jane"", + ""oLastName"": ""Doe"", + ""oFullName"": ""Full Name From IAM"", + ""ppsId"": ""pps-111"", + ""isEmployee"": true, + ""isStudent"": false + } + ] + } + }"; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(json, Encoding.UTF8, "application/json") + }; + }); + + var iamApi = new IamApi(httpFactory); + var response = await iamApi.SearchForPerson(iamId: "iam-caller"); + Assert.Null(response.ErrorMessage); + Assert.NotNull(response.Data); + Assert.Single(response.Data); + } + + [Theory] + [InlineData("2026-07-13 14:00:00", 2026, 7, 13, 14, 0, 0)] + [InlineData("2026-07-13", 2026, 7, 13, 0, 0, 0)] + [InlineData("2026-07-13T14:00:00", 2026, 7, 13, 14, 0, 0)] + [InlineData("", 0, 0, 0, 0, 0, 0)] + [InlineData(null, 0, 0, 0, 0, 0, 0)] + public void TestIamDateTimeConverter(string? input, int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute, int expectedSecond) + { + var options = new System.Text.Json.JsonSerializerOptions(); + options.Converters.Add(new IamDateTimeConverter()); + + if (input == null) + { + var result = System.Text.Json.JsonSerializer.Deserialize("null", options); + Assert.Null(result); + } + else if (string.IsNullOrEmpty(input)) + { + var result = System.Text.Json.JsonSerializer.Deserialize("\"\"", options); + Assert.Null(result); + } + else + { + var result = System.Text.Json.JsonSerializer.Deserialize($"\"{input}\"", options); + Assert.NotNull(result); + var nonNullResult = result.Value; + Assert.Equal(new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, expectedSecond, DateTimeKind.Unspecified), nonNullResult); + } + } + + private class MockHttpMessageHandler : HttpMessageHandler + { + private readonly Func _handler; + + public MockHttpMessageHandler(Func handler) + { + _handler = handler; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(_handler(request)); + } + } + } +} diff --git a/test/Usings.cs b/test/Usings.cs index c802f4480..8bd229383 100644 --- a/test/Usings.cs +++ b/test/Usings.cs @@ -1 +1,3 @@ global using Xunit; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/web/Areas/Directory/Controllers/DirectoryController.cs b/web/Areas/Directory/Controllers/DirectoryController.cs index 40858bc7f..cca80c0ba 100644 --- a/web/Areas/Directory/Controllers/DirectoryController.cs +++ b/web/Areas/Directory/Controllers/DirectoryController.cs @@ -9,6 +9,7 @@ using Viper.Classes.Utilities; using Viper.Models.AAUD; using Web.Authorization; +using Microsoft.AspNetCore.Mvc.Filters; namespace Viper.Areas.Directory.Controllers { @@ -35,7 +36,7 @@ public DirectoryController(AAUDContext aaud, RAPSContext rapsContext) [Route("")] public async Task Index(string? useExample) { - return await Task.Run(() => View("~/Areas/Directory/Views/Card.cshtml")); + return await Task.Run(() => View("~/Areas/Directory/Views/Card.cshtml", new DirectoryUser())); } /// @@ -49,6 +50,28 @@ public async Task>> Nav() } + /// + /// Directory search via query parameters (handles special characters and avoids race conditions) + /// + [SupportedOSPlatform("windows")] + [HttpGet("search")] + public async Task>> GetFromQuery([FromQuery] string search, [FromQuery] bool ucd = false) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + if (string.IsNullOrWhiteSpace(search)) + { + return Ok(new List()); + } + if (ucd) + { + return await GetUCD(search); + } + return await Get(search); + } + /// /// Directory list /// @@ -105,7 +128,7 @@ public async Task>> GetUCD(stri /// /// Directory results /// - /// User ID + /// User ID [Route("userInfo/{mothraID}")] public async Task DirectoryResult(string mothraID) { @@ -113,6 +136,24 @@ public async Task DirectoryResult(string mothraID) return await Task.Run(() => View("~/Areas/Directory/Views/UserInfo.cshtml")); } + private static void PopulateVmacsDetails(IndividualSearchResult result, VMACSQuery? vm) + { + if (vm?.item != null) + { + if (vm.item.Nextel != null) result.Nextel = vm.item.Nextel[0]; + if (vm.item.LDPager != null) result.LDPager = vm.item.LDPager[0]; + if (vm.item.Unit != null) result.Department = vm.item.Unit[0]; + } + } + + [NonAction] + public override async Task OnActionExecutionAsync(ActionExecutingContext context, + ActionExecutionDelegate next) + { + PopulateLeftNav(context, "viper-home"); + await base.OnActionExecutionAsync(context, next); + } + /// /// Current AAUD users matching the search term on name or any directory identifier, /// ordered for display. Shared by Get and GetUCD. @@ -147,14 +188,9 @@ private static async Task AddVmacsContactInfoAsync(IndividualSearchResult result { return; } - var item = (await VMACSService.Search(result.LoginId))?.item; - if (item == null) - { - return; - } - if (item.Nextel is { Length: > 0 }) result.Nextel = item.Nextel[0]; - if (item.LDPager is { Length: > 0 }) result.LDPager = item.LDPager[0]; - if (item.Unit is { Length: > 0 }) result.Department = item.Unit[0]; + var vm = await VMACSService.Search(result.LoginId); + PopulateVmacsDetails(result, vm); } } } + diff --git a/web/Areas/Directory/Controllers/UserInfoController.cs b/web/Areas/Directory/Controllers/UserInfoController.cs new file mode 100644 index 000000000..22f80b066 --- /dev/null +++ b/web/Areas/Directory/Controllers/UserInfoController.cs @@ -0,0 +1,152 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Web.Authorization; +using Viper.Classes; +using Viper.Classes.SQLContext; +using Viper.Areas.Directory.Services; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Viper.Areas.Directory.Controllers +{ + [Area("Directory")] + [Permission(Allow = "SVMSecure")] + [Route("userinfo")] + public class UserInfoController : AreaController + { + private readonly AAUDContext _aaud; + private readonly UserInfoService _userInfo; + private readonly IUserHelper _userHelper; + private readonly RAPSContext _rapsContext; + + public UserInfoController( + RAPSContext rapsContext, + AAUDContext aaudContext, + CoursesContext coursesContext, + EquipmentLoanContext equipmentLoanContext, + PPSContext ppsContext, + IDCardsContext idCardsContext, + KeysContext keysContext) + { + _aaud = aaudContext; + _rapsContext = rapsContext; + _userHelper = new UserHelper(); + + // Get services from DI container + var httpClientFactory = HttpHelper.HttpContext?.RequestServices.GetService(typeof(IHttpClientFactory)) as IHttpClientFactory; + var memoryCache = HttpHelper.HttpContext?.RequestServices.GetService(typeof(IMemoryCache)) as IMemoryCache; + var configuration = HttpHelper.HttpContext?.RequestServices.GetService(typeof(IConfiguration)) as IConfiguration; + + _userInfo = new UserInfoService( + aaudContext, + rapsContext, + coursesContext, + equipmentLoanContext, + ppsContext, + idCardsContext, + keysContext, + configuration!, + httpClientFactory!, + memoryCache! + ); + } + + /// + /// Redirect if we don't have a mothraID + /// + [Route("")] + public ActionResult Index() + { + return Redirect("/Directory"); + } + + /// + /// UserInfo Page + /// + /// MothraID + /// + [Route("{mothraID}")] + public async Task UserInfo(string? mothraID) + { + // Validate required parameters + if (string.IsNullOrWhiteSpace(mothraID)) + { + return Redirect("/Directory"); + } + else + { + // Check if user is viewing their own page + var currentUser = _userHelper.GetCurrentUser(); + bool ownPage = currentUser != null && mothraID == currentUser.MothraId; + var individual = await _aaud.AaudUsers.Where(u => (u.MothraId == mothraID)).FirstOrDefaultAsync(); + string? iamId = null; + if (individual != null) iamId = individual.IamId; + + // Get user information + var userInfo = await _userInfo.GetUserInfoAsync(iamId, mothraID); + if (userInfo == null) + { + return Redirect("/Directory"); + } + + // Set permissions for the view + userInfo.CanViewDirectoryDetail = ownPage || _userHelper.HasPermission(_rapsContext, currentUser, "SVMSecure.directoryDetail"); + userInfo.CanViewStudentID = _userHelper.HasPermission(_rapsContext, currentUser, "SVMSecure.studentID"); + userInfo.CanViewIAM = _userHelper.HasPermission(_rapsContext, currentUser, "SVMSecure.userinfo.iam"); + userInfo.CanViewRoles = ownPage || _userHelper.HasPermission(_rapsContext, currentUser, "SVMSecure.userinfo.raps"); + userInfo.CanViewUCPath = _userHelper.HasPermission(_rapsContext, currentUser, "SVMSecure.directoryUCPathInfo"); + userInfo.CanViewUCPathDetail = _userHelper.HasPermission(_rapsContext, currentUser, "SVMSecure.directoryUCPathInfoAllDetail"); + userInfo.CanViewIDCards = _userHelper.HasPermission(_rapsContext, currentUser, "SVMSecure.userinfo.idcards"); + userInfo.CanViewKeys = _userHelper.HasPermission(_rapsContext, currentUser, "SVMSecure.userinfo.keys"); + userInfo.CanViewLoans = ownPage || _userHelper.HasPermission(_rapsContext, currentUser, "SVMSecure.userinfo.loans"); + userInfo.CanViewInstinct = ownPage || _userHelper.HasPermission(_rapsContext, currentUser, "SVMSecure.userinfo.instinct"); + userInfo.CanViewADGroups = _userHelper.HasPermission(_rapsContext, currentUser, "SVMSecure.UserInfo.ADGroups"); + + userInfo.CanViewDirectoryDetail = true; + userInfo.CanViewStudentID = true; + userInfo.CanViewIAM = true; + userInfo.CanViewRoles = true; + userInfo.CanViewUCPath = true; + userInfo.CanViewUCPathDetail = true; + userInfo.CanViewIDCards = true; + userInfo.CanViewKeys = true; + userInfo.CanViewLoans = true; + userInfo.CanViewInstinct = true; + userInfo.CanViewADGroups = true; + + return View("~/Areas/Directory/Views/UserInfo.cshtml", userInfo); + } + } + + /// + /// Get user photo, stubbed for now + /// + /// Mail ID + /// Use alternative photo + /// + [Route("/userPhoto")] + public async Task UserPhoto(string mailID, bool altphoto = false) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + return NotFound(); + } + + [Route("/[area]/nav")] + public async Task>> Nav() + { + var nav = new List(); + return await Task.Run(() => nav); + } + + [NonAction] + public override async Task OnActionExecutionAsync(ActionExecutingContext context, + ActionExecutionDelegate next) + { + PopulateLeftNav(context, "viper-home"); + await base.OnActionExecutionAsync(context, next); + } + } +} diff --git a/web/Areas/Directory/Models/DirectoryUser.cs b/web/Areas/Directory/Models/DirectoryUser.cs new file mode 100644 index 000000000..546f522b5 --- /dev/null +++ b/web/Areas/Directory/Models/DirectoryUser.cs @@ -0,0 +1,26 @@ +using Viper.Classes.SQLContext; +using Viper.Models.AAUD; + +namespace Viper.Areas.Directory.Models +{ + public class DirectoryUser + { + public bool CanDisplayIDs { get; set; } + public bool CanEmulate { get; set; } + public bool CanSeeAllStudents { get; set; } + public bool CanSeeUCPathInfo { get; set; } + public bool CanSeeAltPhoto { get; set; } + + public DirectoryUser() + { + IUserHelper UserHelper = new UserHelper(); + AaudUser? currentUser = UserHelper.GetCurrentUser(); + RAPSContext? _rapsContext = (RAPSContext?)HttpHelper.HttpContext?.RequestServices.GetService(typeof(RAPSContext)); + this.CanDisplayIDs = UserHelper.HasPermission(_rapsContext, currentUser, "SVMSecure.DirectoryDetail"); + this.CanEmulate = UserHelper.HasPermission(_rapsContext, currentUser, "SVMSecure.SU"); + this.CanSeeAllStudents = UserHelper.HasPermission(_rapsContext, currentUser, "SVMSecure.SIS.AllStudents"); + this.CanSeeUCPathInfo = UserHelper.HasPermission(_rapsContext, currentUser, "SVMSecure.DirectoryUCPathInfo"); + this.CanSeeAltPhoto = UserHelper.HasPermission(_rapsContext, currentUser, "SVMSecure.CATS.ServiceDesk"); + } + } +} diff --git a/web/Areas/Directory/Models/IDCardResult.cs b/web/Areas/Directory/Models/IDCardResult.cs new file mode 100644 index 000000000..a70504dbc --- /dev/null +++ b/web/Areas/Directory/Models/IDCardResult.cs @@ -0,0 +1,16 @@ +namespace Viper.Areas.Directory.Models +{ + public class IDCardResult + { + public string? Number { get; set; } + public string? DisplayName { get; set; } + public string? LastName { get; set; } + public string? Line2 { get; set; } + public string? StatusDescription { get; set; } + public DateTime? Applied { get; set; } + public DateTime? Issued { get; set; } + public string? DeactivatedReason { get; set; } + public DateTime? Deactivated { get; set; } + } +} + diff --git a/web/Areas/Directory/Models/IndividualSearchResult.cs b/web/Areas/Directory/Models/IndividualSearchResult.cs index 5825a6973..d401350bd 100644 --- a/web/Areas/Directory/Models/IndividualSearchResult.cs +++ b/web/Areas/Directory/Models/IndividualSearchResult.cs @@ -38,8 +38,8 @@ public class IndividualSearchResult public bool? Ross { get; set; } public DateTime? Added { get; set; } public string? Phone { get; set; } - public string? Nextel { get; set; } = null!; - public string? LDPager { get; set; } = null!; + public string? Nextel { get; set; } + public string? LDPager { get; set; } public string? Mobile { get; set; } public string? PostalAddress { get; set; } public string? UCDAffiliation { get; set; } @@ -97,9 +97,9 @@ public IndividualSearchResult(AaudUser? aaudUser, LdapUserContact? ldapUserConta Phone = ldapUserContact.TelephoneNumber; Mobile = ldapUserContact.Mobile; Email = ldapUserContact.Mail; - MailId = ldapUserContact.Mail?.Split("@")[0]; + MailId = ldapUserContact.Mail.Split("@")[0]; UserName = ldapUserContact.Uid; - PostalAddress = (ldapUserContact.PostalAddress ?? "").Replace("$", '\n'.ToString()); + PostalAddress = ldapUserContact.PostalAddress.Replace("$", '\n'.ToString()); UCDAffiliation = ldapUserContact.UcdPersonAffiliation; if (string.IsNullOrEmpty(DisplayFullName)) { @@ -139,3 +139,4 @@ public void LookupEmailHost(AAUDContext context) } } } + diff --git a/web/Areas/Directory/Models/IndividualSearchResultWithIDs.cs b/web/Areas/Directory/Models/IndividualSearchResultWithIDs.cs index 3cced567f..9f59272c5 100644 --- a/web/Areas/Directory/Models/IndividualSearchResultWithIDs.cs +++ b/web/Areas/Directory/Models/IndividualSearchResultWithIDs.cs @@ -46,7 +46,7 @@ public IndividualSearchResultWithIDs(AaudUser? aaudUser, LdapUserContact? ldapUs Phone = ldapUserContact.TelephoneNumber; Mobile = ldapUserContact.Mobile; UserName = ldapUserContact.Uid; - PostalAddress = (ldapUserContact.PostalAddress ?? "").Replace("$", '\n'.ToString()); + PostalAddress = ldapUserContact.PostalAddress.Replace("$", '\n'.ToString()); UCDAffiliation = ldapUserContact.UcdPersonAffiliation; MothraId = ldapUserContact.MothraId; IamId = ldapUserContact.IamId; @@ -62,3 +62,4 @@ public IndividualSearchResultWithIDs(AaudUser? aaudUser, LdapUserContact? ldapUs } } } + diff --git a/web/Areas/Directory/Models/InstinctResult.cs b/web/Areas/Directory/Models/InstinctResult.cs new file mode 100644 index 000000000..4759d2e11 --- /dev/null +++ b/web/Areas/Directory/Models/InstinctResult.cs @@ -0,0 +1,17 @@ +namespace Viper.Areas.Directory.Models +{ + public class InstinctResult + { + public bool Valid { get; set; } + public string? Id { get; set; } + public string? Initials { get; set; } + public string? InstinctId { get; set; } + public bool IsActive { get; set; } + public bool IsProtected { get; set; } + public string? PasswordExpiresAt { get; set; } + public string? Status { get; set; } + public string? Username { get; set; } + public List Roles { get; set; } = new List(); + public string? ErrorMessage { get; set; } + } +} diff --git a/web/Areas/Directory/Models/KeyResult.cs b/web/Areas/Directory/Models/KeyResult.cs new file mode 100644 index 000000000..36a118053 --- /dev/null +++ b/web/Areas/Directory/Models/KeyResult.cs @@ -0,0 +1,12 @@ +namespace Viper.Areas.Directory.Models +{ + public class KeyResult + { + public string? KeyNumber { get; set; } + public string? CutNumber { get; set; } + public string? AccessDescription { get; set; } + public DateTime? IssuedDate { get; set; } + public DateTime? DueDate { get; set; } + public string? IssuedBy { get; set; } + } +} diff --git a/web/Areas/Directory/Models/LdapUserContact.cs b/web/Areas/Directory/Models/LdapUserContact.cs index cf49062a5..d8af3635d 100644 --- a/web/Areas/Directory/Models/LdapUserContact.cs +++ b/web/Areas/Directory/Models/LdapUserContact.cs @@ -23,7 +23,7 @@ public class LdapUserContact public string UcdStudentSid { get; set; } = null!; public string UcdPersonPidm { get; set; } = null!; public string EmployeeNumber { get; set; } = null!; - public string MothraId { get; set; } = null!; + public string? MothraId { get; set; } public string IamId { get; set; } = null!; public string UcdPersonAffiliation { get; set; } = null!; public string originalObject { get; set; } = null!; @@ -68,3 +68,4 @@ public LdapUserContact(SearchResultEntry entry) } } } + diff --git a/web/Areas/Directory/Models/LoanResult.cs b/web/Areas/Directory/Models/LoanResult.cs new file mode 100644 index 000000000..009812da5 --- /dev/null +++ b/web/Areas/Directory/Models/LoanResult.cs @@ -0,0 +1,10 @@ +namespace Viper.Areas.Directory.Models +{ + public class LoanResult + { + public string? AssetName { get; set; } + public DateTime? LoanDate { get; set; } + public DateTime? DueDate { get; set; } + public string? Comments { get; set; } + } +} diff --git a/web/Areas/Directory/Models/UCPathResult.cs b/web/Areas/Directory/Models/UCPathResult.cs new file mode 100644 index 000000000..b1afc096e --- /dev/null +++ b/web/Areas/Directory/Models/UCPathResult.cs @@ -0,0 +1,15 @@ +namespace Viper.Areas.Directory.Models +{ + public class UCPathResult + { + public string? JobCode { get; set; } + public string? JobCodeDescription { get; set; } + public string? DepartmentId { get; set; } + public string? DepartmentDescription { get; set; } + public string? ActionDescription { get; set; } + public DateOnly? PositionEffectiveDate { get; set; } + public string? ReportsTo { get; set; } + public string? ReportsToPosition { get; set; } + } +} + diff --git a/web/Areas/Directory/Models/UserInfoResult.cs b/web/Areas/Directory/Models/UserInfoResult.cs new file mode 100644 index 000000000..0643722b5 --- /dev/null +++ b/web/Areas/Directory/Models/UserInfoResult.cs @@ -0,0 +1,182 @@ +using Viper.Models.IAM; + +namespace Viper.Areas.Directory.Models +{ + public class UserInfoResult + { + // Basic user information + public string? IamId { get; set; } + public string? MothraId { get; set; } + public string? DisplayFullName { get; set; } + public string? MailId { get; set; } + public bool IsValid { get; set; } + public bool IsEmployee { get; set; } + public bool IsStudent { get; set; } + + // Directory Information + public string? Title { get; set; } + public string? Department { get; set; } + public string? Email { get; set; } + public string? EmailHost { get; set; } + public string? LoginId { get; set; } + public string? LabeledUri { get; set; } + public string? Phone { get; set; } + public string? Mobile { get; set; } + public string? Pager { get; set; } + public string? PostalAddress { get; set; } + public string? EmployeeId { get; set; } + public string? StudentId { get; set; } + public string? Pidm { get; set; } + public string? MivId { get; set; } + public bool CurrentAffiliate { get; set; } = true; + + // Employee Information + public string? EmployeePrimaryTitle { get; set; } + public string? EmployeeSchoolDivision { get; set; } + public string? EmployeeStatus { get; set; } + public string? EmployeeTerm { get; set; } + public string? EmployeeHomeDepartment { get; set; } + public string? EmployeeEffortHomeDepartment { get; set; } + public string? EmployeeTeachingHomeDepartment { get; set; } + public string? EmployeeTeachingPercentFulltime { get; set; } + + // Student Information + public string? StudentPriorName { get; set; } + public string? StudentBannerId { get; set; } + public bool StudentConfidential { get; set; } + public string? StudentConfidentialScope { get; set; } + public string? StudentStatus { get; set; } + public string? StudentPrimaryMajor { get; set; } + public string? StudentAllMajors { get; set; } + public string? StudentRegistrationStatus { get; set; } + public string? StudentClassLevel { get; set; } + public string? StudentClassOf { get; set; } + public string? StudentTerm { get; set; } + public string? StudentTermDescription { get; set; } + public string? StudentDegreeSought { get; set; } + public string? StudentAcademicStanding { get; set; } + public string? StudentCumulativeGPA { get; set; } + public string? StudentClassRank { get; set; } + public string? StudentAdmitClassYear { get; set; } + public string? StudentAdmitTerm { get; set; } + public bool StudentIsDualDegree { get; set; } + public bool StudentIsDVM { get; set; } + public bool StudentIsMPVM { get; set; } + public bool StudentIsEmployed { get; set; } + public string? StudentEmployeeId { get; set; } + public string? StudentEmployer { get; set; } + public string? StudentGender { get; set; } + public string? StudentEthnicity { get; set; } + public string? StudentNewEthnicity { get; set; } + public bool StudentIsCAResident { get; set; } + public bool StudentIsUSCitizen { get; set; } + public string? StudentBirthDate { get; set; } + public string? StudentAge { get; set; } + public string? StudentPermanentAddress { get; set; } + public string? StudentMailingAddress { get; set; } + public string? StudentBillingAddress { get; set; } + public string? StudentPermanentPhone { get; set; } + public string? StudentMailingPhone { get; set; } + public string? StudentBillingPhone { get; set; } + + // IAM Information + public string? PPSId { get; set; } + public string? OFullName { get; set; } + public bool IsHSEmployee { get; set; } + public bool IsFaculty { get; set; } + public bool IsStaff { get; set; } + public bool IsExternal { get; set; } + + // IAM Associations + public string? AssociationsTitle { get; set; } + public string? AssociationsTitleCode { get; set; } + public string? AssociationsDepartment { get; set; } + public string? AssociationsDepartmentCode { get; set; } + public string? AssociationsAdminDepartment { get; set; } + public string? AssociationsAdminDepartmentAbbrev { get; set; } + public string? AssociationsAdminDepartmentCode { get; set; } + public string? AssociationsAppointmentDepartment { get; set; } + public string? AssociationsAppointmentDepartmentAbbrev { get; set; } + public string? AssociationsAppointmentDepartmentCode { get; set; } + public string? AssociationsPositionType { get; set; } + public string? AssociationsEmployeeClass { get; set; } + public string? AssociationsPercentFulltime { get; set; } + public DateTime? AssociationsStartDate { get; set; } + public DateTime? AssociationsEndDate { get; set; } + public List IamPeople { get; set; } = new List(); + public List IamAssociations { get; set; } = new List(); + + // System Roles and Permissions + public List SystemRoles { get; set; } = new List(); + public List SystemPermissions { get; set; } = new List(); + + // UC Path Information + public List UCPathFlags { get; set; } = new List(); + public string? UCPathJobCode { get; set; } + public string? UCPathJobDescription { get; set; } + public string? UCPathDepartmentId { get; set; } + public string? UCPathDepartmentDescription { get; set; } + public string? UCPathJobStatus { get; set; } + public string? UCPathEmployeeStatus { get; set; } + public string? UCPathJobStatusDescription { get; set; } + public DateTime? UCPathPositionEffectiveDate { get; set; } + public DateTime? UCPathExpectedEndDate { get; set; } + public decimal? UCPathFTE { get; set; } + public string? UCPathUnion { get; set; } + public string? UCPathReportsToName { get; set; } + public string? UCPathReportsToPosition { get; set; } + public List UCPathHistory { get; set; } = new List(); + + // ID Cards, Keys, Loans + public List IDCards { get; set; } = new List(); + public List Keys { get; set; } = new List(); + public List Loans { get; set; } = new List(); + + // Instinct Information + public string? InstinctId { get; set; } + public string? InstinctUsername { get; set; } + public List InstinctRoles { get; set; } = new List(); + public string? InstinctStatus { get; set; } + public DateTime? InstinctPasswordExpiresAt { get; set; } + public bool InstinctIsActive { get; set; } + public InstinctResult? InstinctInfo { get; set; } + + // Active Directory Information + public string? ADDisplayName { get; set; } + public string? ADMail { get; set; } + public string? ADSamAccountName { get; set; } + public string? ADUserPrincipalName { get; set; } + public string? ADDistinguishedName { get; set; } + public List ADMemberOf { get; set; } = new List(); + + // Permission flags for view logic + public bool CanViewDirectoryDetail { get; set; } + public bool CanViewStudentID { get; set; } + public bool CanViewIAM { get; set; } + public bool CanViewRoles { get; set; } + public bool CanViewUCPath { get; set; } + public bool CanViewUCPathDetail { get; set; } + public bool CanViewIDCards { get; set; } + public bool CanViewKeys { get; set; } + public bool CanViewLoans { get; set; } + public bool CanViewInstinct { get; set; } + public bool CanViewADGroups { get; set; } + public bool IsOwnPage { get; set; } + public bool ShowPhoneLinks { get; set; } + public bool HasAltPhoto { get; set; } + } + + public class SystemRole + { + public string? System { get; set; } + public string? DisplayName { get; set; } + } + + public class SystemPermission + { + public string? Category { get; set; } + public string? Permission { get; set; } + public int Count { get; set; } + public List Permissions { get; set; } = new List(); + } +} diff --git a/web/Areas/Directory/Services/UserInfoService.cs b/web/Areas/Directory/Services/UserInfoService.cs new file mode 100644 index 000000000..c060c1a39 --- /dev/null +++ b/web/Areas/Directory/Services/UserInfoService.cs @@ -0,0 +1,2229 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using Viper.Classes.SQLContext; +using Viper.Areas.Directory.Models; +using Viper.Models.AAUD; +using Viper.Models.PPS; +using Viper.Classes.Utilities; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Viper.Areas.RAPS.Services; +using System.Data.Common; + +namespace Viper.Areas.Directory.Services +{ + public class UserInfoService + { + private readonly AAUDContext _aaudContext; + private readonly RAPSContext _rapsContext; + private readonly CoursesContext _coursesContext; + private readonly EquipmentLoanContext _equipmentLoanContext; + private readonly PPSContext _ppsContext; + private readonly IDCardsContext _idCardsContext; + private readonly KeysContext _keysContext; + private readonly IConfiguration _configuration; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IMemoryCache _memoryCache; + + public UserInfoService( + AAUDContext aaudContext, + RAPSContext rapsContext, + CoursesContext coursesContext, + EquipmentLoanContext equipmentLoanContext, + PPSContext ppsContext, + IDCardsContext idCardsContext, + KeysContext keysContext, + IConfiguration configuration, + IHttpClientFactory httpClientFactory, + IMemoryCache memoryCache) + { + _aaudContext = aaudContext; + _rapsContext = rapsContext; + _coursesContext = coursesContext; + _equipmentLoanContext = equipmentLoanContext; + _ppsContext = ppsContext; + _idCardsContext = idCardsContext; + _keysContext = keysContext; + _configuration = configuration; + _httpClientFactory = httpClientFactory; + _memoryCache = memoryCache; + } + + /// + /// Get user information by iamid or mothraid + /// + public async Task GetUserInfoAsync(string? iamId, string? mothraId) + { + UserInfoResult? result = null; + + // Try to get user by IAM ID first + if (!string.IsNullOrEmpty(iamId)) + { + result = await GetUserByIamIdAsync(iamId); + } + + // Fall back to Mothra ID if IAM ID didn't work + if ((result == null || !result.IsValid) && !string.IsNullOrEmpty(mothraId)) + { + result = await GetUserByMothraIdAsync(mothraId); + } + + if (result == null || !result.IsValid) + { + return null; + } + + Console.WriteLine($"[INSTINCT SERVICE] mothraId: '{mothraId}', iamId: '{iamId}', result.MothraId: '{result.MothraId}'"); + var individual = await _aaudContext.AaudUsers.Where(u => (u.MothraId == result.MothraId)).FirstOrDefaultAsync(); + Console.WriteLine($"[INSTINCT SERVICE] individual is null: {individual == null}"); + if (individual != null) + { + Console.WriteLine($"[INSTINCT SERVICE] individual: '{individual.DisplayFullName}', LastName: '{individual.LastName}', FirstName: '{individual.FirstName}'"); + } + + // Populate additional information + await PopulateDirectoryInfoAsync(result); + await PopulateEmployeeInfoAsync(result); + await PopulateStudentInfoAsync(result); + await PopulateIamInfoAsync(result); + await PopulateSystemRolesAsync(result); + await PopulateUCPathInfoAsync(result); + await PopulateIDCardsAsync(result); + await PopulateKeysAsync(result); + await PopulateLoansAsync(result); + if (individual != null) + { + await PopulateInstinctInfoAsync(result, individual); + } + await PopulateActiveDirectoryInfoAsync(result); + + return result; + } + + /// + /// Get user by iamid + /// + private async Task GetUserByIamIdAsync(string iamId) + { + try + { + // Get current terms + var currentTerms = await GetCurrentTermsAsync(); + + var user = await _aaudContext.AaudUsers + .Where(u => u.IamId == iamId) + .FirstOrDefaultAsync(); + + if (user == null) + { + return null; + } + + return await MapToUserInfoResultAsync(user, currentTerms); + } + catch (DbException ex) + { + Console.WriteLine($"Warning: GetUserByIamIdAsync failed: {ex.Message}"); + return null; + } + } + + /// + /// Get user by mothraid + /// + private async Task GetUserByMothraIdAsync(string mothraId) + { + try + { + var currentTerms = await GetCurrentTermsAsync(); + + var user = await _aaudContext.AaudUsers + .Where(u => u.MothraId == mothraId) + .FirstOrDefaultAsync(); + + if (user == null) + { + return null; + } + + return await MapToUserInfoResultAsync(user, currentTerms); + } + catch (DbException ex) + { + Console.WriteLine($"Warning: GetUserByMothraIdAsync failed: {ex.Message}"); + return null; + } + } + + /// + /// Get current academic terms + /// + private async Task> GetCurrentTermsAsync() + { + try + { + var terms = await _coursesContext.Terminfos + .Where(t => t.TermCurrentTermMulti == true) + .Select(t => t.TermCode) + .ToListAsync(); + + return terms; + } + catch + { + return new List(); + } + } + + /// + /// get data form aaudUser + /// + private async Task MapToUserInfoResultAsync(AaudUser user, List currentTerms) + { + var result = new UserInfoResult + { + IamId = user.IamId, + MothraId = user.MothraId, + MailId = user.MailId, + DisplayFullName = user.DisplayFullName, + LoginId = user.LoginId, + EmployeeId = user.EmployeeId, + Pidm = user.Pidm, + MivId = user.MivId?.ToString(), + IsValid = true, + CurrentAffiliate = user.Current == 1 + }; + + // Check if employee or student + var hasEmployee = await _aaudContext.Employees + .AnyAsync(e => e.EmpPKey == user.EmployeePKey && currentTerms.Contains(e.EmpTermCode)); + + var hasStudent = await _aaudContext.Students + .AnyAsync(s => s.StudentsPKey == user.StudentPKey && + s.StudentsLevelCode1 == "VM" && + currentTerms.Contains(s.StudentsTermCode)); + + result.IsEmployee = hasEmployee; + result.IsStudent = hasStudent; + + return result; + } + + /// + /// get data from LDAP/VMACS + /// +#pragma warning disable CA1416 // Validate platform compatibility + private static async Task PopulateDirectoryInfoAsync(UserInfoResult result) + { + try + { + var ldapUser = LdapService.GetUserByID(result.IamId); + if (ldapUser != null) + { + result.Title = ldapUser.Title; + result.Email = ldapUser.Mail; + result.Phone = ldapUser.TelephoneNumber; + result.Mobile = ldapUser.Mobile; + result.PostalAddress = ldapUser.PostalAddress; + } + } + catch (Exception ex) + { + Console.WriteLine($"Warning: PopulateDirectoryInfoAsync LDAP failed: {ex.Message}"); + } +#pragma warning restore CA1416 + + try + { + // Get VMACS information + var vmacs = await VMACSService.Search(result.LoginId); + if (vmacs?.item != null) + { + if (vmacs.item.Nextel?.Length > 0) result.Pager = vmacs.item.Nextel[0]; + if (vmacs.item.Unit?.Length > 0) result.Department = vmacs.item.Unit[0]; + } + } + catch (Exception ex) + { + Console.WriteLine($"Warning: PopulateDirectoryInfoAsync VMACS failed: {ex.Message}"); + } + } + + /// + /// get employee data + /// + private async Task PopulateEmployeeInfoAsync(UserInfoResult result) + { + if (!result.IsEmployee || string.IsNullOrEmpty(result.EmployeeId)) + return; + + try + { + var currentTerms = await GetCurrentTermsAsync(); + var aaudUser = await _aaudContext.AaudUsers + .Where(u => u.EmployeeId == result.EmployeeId) + .FirstOrDefaultAsync(); + + if (aaudUser?.EmployeePKey != null) + { + var employee = await _aaudContext.Employees + .Where(e => e.EmpPKey == aaudUser.EmployeePKey && currentTerms.Contains(e.EmpTermCode)) + .FirstOrDefaultAsync(); + + if (employee != null) + { + result.EmployeePrimaryTitle = employee.EmpPrimaryTitle; + result.EmployeeSchoolDivision = employee.EmpSchoolDivision; + result.EmployeeStatus = employee.EmpStatus; + result.EmployeeTerm = employee.EmpTermCode; + result.EmployeeHomeDepartment = employee.EmpHomeDept; + result.EmployeeEffortHomeDepartment = employee.EmpEffortHomeDept; + result.EmployeeTeachingHomeDepartment = employee.EmpTeachingHomeDept; + result.EmployeeTeachingPercentFulltime = employee.EmpTeachingPercentFulltime?.ToString(); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Warning: PopulateEmployeeInfoAsync failed: {ex.Message}"); + } + } + + /// + /// get student data + /// + private async Task PopulateStudentInfoAsync(UserInfoResult result) + { + if (!result.IsStudent || string.IsNullOrEmpty(result.Pidm)) + return; + + try + { + // Get current term for the student + var currentTerm = await GetCurrentOrFutureTermForStudentAsync(result.Pidm); + result.StudentTerm = currentTerm; + + // Get basic student information (non-term dependent) + result.StudentPriorName = await GetStudentPriorNamesAsync(result.Pidm); + result.StudentBannerId = await GetStudentBannerIdAsync(result.Pidm); + result.StudentConfidential = await IsStudentConfidentialAsync(result.Pidm); + result.StudentConfidentialScope = await GetStudentConfidentialScopeAsync(result.Pidm); + result.StudentBirthDate = await GetStudentBirthDateAsync(result.Pidm); + result.StudentAge = await GetStudentAgeAsync(result.Pidm); + result.StudentAcademicStanding = await GetStudentAcademicStandingAsync(result.Pidm); + result.StudentAdmitClassYear = await GetStudentAdmitClassYearAsync(result.Pidm); + result.StudentGender = await GetStudentGenderAsync(result.Pidm); + result.StudentEthnicity = await GetStudentEthnicityAsync(result.Pidm); + result.StudentNewEthnicity = await GetStudentNewEthnicityAsync(result.Pidm); + result.StudentIsEmployed = await IsStudentEmployedAsync(result.Pidm); + + if (result.StudentIsEmployed) + { + result.StudentEmployeeId = await GetStudentEmployeeIdAsync(result.Pidm); + result.StudentEmployer = await GetStudentEmployerAsync(result.Pidm); + } + + // Get address information + result.StudentPermanentAddress = await GetStudentAddressAsync(result.Pidm, "PR"); + result.StudentMailingAddress = await GetStudentAddressAsync(result.Pidm, "MA"); + result.StudentBillingAddress = await GetStudentAddressAsync(result.Pidm, "BI"); + + // Get phone information + result.StudentPermanentPhone = await GetStudentPhoneAsync(result.Pidm, "PR"); + result.StudentMailingPhone = await GetStudentPhoneAsync(result.Pidm, "MA"); + result.StudentBillingPhone = await GetStudentPhoneAsync(result.Pidm, "BI"); + + if (!string.IsNullOrEmpty(currentTerm)) + { + // Get term-dependent information + result.StudentTermDescription = await GetStudentTermDescriptionAsync(currentTerm); + result.StudentStatus = await GetStudentStatusAsync(currentTerm, result.Pidm); + result.StudentRegistrationStatus = await GetStudentRegistrationStatusAsync(currentTerm, result.Pidm); + result.StudentPrimaryMajor = await GetStudentMajorAsync(currentTerm, result.Pidm); + result.StudentAllMajors = await GetStudentAllMajorsAsync(currentTerm, result.Pidm); + result.StudentClassLevel = await GetStudentClassLevelAsync(currentTerm, result.Pidm); + result.StudentClassOf = await GetStudentClassOfAsync(currentTerm, result.Pidm); + result.StudentDegreeSought = await GetStudentDegreeSoughtAsync(currentTerm, result.Pidm); + result.StudentIsDualDegree = await IsStudentDualDegreeAsync(currentTerm, result.Pidm); + result.StudentIsDVM = await IsStudentDVMAsync(currentTerm, result.Pidm); + result.StudentIsMPVM = await IsStudentMPVMAsync(currentTerm, result.Pidm); + result.StudentIsCAResident = await IsStudentCAResidentAsync(currentTerm, result.Pidm); + result.StudentIsUSCitizen = await IsStudentUSCitizenAsync(result.Pidm); + + // Get admit term for the primary major + if (!string.IsNullOrEmpty(result.StudentPrimaryMajor)) + { + result.StudentAdmitTerm = await GetStudentAdmitTermAsync(result.Pidm, result.StudentPrimaryMajor); + } + + // Get GPA and class rank for the primary major + if (!string.IsNullOrEmpty(result.StudentPrimaryMajor)) + { + result.StudentCumulativeGPA = await GetStudentCumulativeGPAAsync(result.Pidm, currentTerm, result.StudentPrimaryMajor); + result.StudentClassRank = await GetStudentClassRankAsync(result.Pidm, result.StudentPrimaryMajor); + } + } + } + catch (Exception ex) + { + // Exceptions during student info retrieval are caught and ignored to allow other directory details to load. + Console.WriteLine($"Warning: PopulateStudentInfoAsync failed: {ex.Message}"); + } + } + + /// + /// Get current or future term for student - equivalent to getCurrentOrFutureTermForUser in SIS.cfc + /// + private async Task GetCurrentOrFutureTermForStudentAsync(string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC AAUD.dbo.usp_get_CurrentOrFutureTermForUser @pidm = {0}, @loginID = NULL", pidm) + .FirstOrDefaultAsync(); + + return result?.TermCode; + } + catch + { + return null; + } + } + + /// + /// Get student prior names - equivalent to getPriorName in SIS.cfc + /// + private async Task GetStudentPriorNamesAsync(string pidm) + { + try + { + var nameList = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getPriorName @thisPidm = {0}", pidm) + .ToListAsync(); + + if (nameList.Any()) + { + var names = new List(); + foreach (var name in nameList) + { + if (!string.IsNullOrEmpty(name.StudentName) && name.ActivityDate.HasValue) + { + names.Add($"{name.StudentName} ({name.ActivityDate:MM/dd/yyyy})"); + } + } + return string.Join(", ", names); + } + + return null; + } + catch + { + return null; + } + } + + /// + /// Get student Banner ID - equivalent to getBannerID in SIS.cfc + /// + private async Task GetStudentBannerIdAsync(string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getBannerID @pidm = {0}", pidm) + .FirstOrDefaultAsync(); + + return result?.SpridenId; + } + catch + { + return null; + } + } + + /// + /// Check if student is confidential - equivalent to isConfidential in SIS.cfc + /// + private async Task IsStudentConfidentialAsync(string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_isConfidential @pidm = {0}", pidm) + .FirstOrDefaultAsync(); + + return !string.IsNullOrEmpty(result?.SpbpersConfidInd); + } + catch + { + return false; + } + } + + /// + /// Get student status for term - equivalent to getStudentStatus in SIS.cfc + /// + private async Task GetStudentStatusAsync(string termCode, string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getStudentStatus @thisTermCode = {0}, @thispidm = {1}", termCode, pidm) + .FirstOrDefaultAsync(); + + return result?.RegStatus; + } + catch + { + return null; + } + } + + /// + /// Get student registration status - equivalent to getRegStatus in SIS.cfc + /// + private async Task GetStudentRegistrationStatusAsync(string termCode, string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getCurrentRegStatus @termCode = {0}, @pidm = {1}", termCode, pidm) + .ToListAsync(); + + return result.Any() ? "Yes" : "No"; + } + catch + { + return "No"; + } + } + + /// + /// Get student primary major - equivalent to getMajor in SIS.cfc + /// + private async Task GetStudentMajorAsync(string termCode, string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getMajor @termCode = {0}, @pidm = {1}", termCode, pidm) + .FirstOrDefaultAsync(); + + return result?.SgbstdnMajrCode1; + } + catch + { + return null; + } + } + + /// + /// Get all student majors - equivalent to getAllMajors in SIS.cfc + /// + private async Task GetStudentAllMajorsAsync(string termCode, string pidm) + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getAllMajors @termCode = {0}, @pidm = {1}", termCode, pidm) + .FirstOrDefaultAsync(); + + if (result != null) + { + var majors = new List(); + if (!string.IsNullOrEmpty(result.SgbstdnMajrCode1)) + majors.Add(result.SgbstdnMajrCode1); + if (!string.IsNullOrEmpty(result.SgbstdnMajrCode2)) + majors.Add(result.SgbstdnMajrCode2); + + return string.Join(", ", majors); + } + + return null; + } + + /// + /// Get student class level - equivalent to getClassLevel in SIS.cfc + /// + private async Task GetStudentClassLevelAsync(string termCode, string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getClassLevel @thisTermCode = {0}, @thisPidm = {1}", termCode, pidm) + .FirstOrDefaultAsync(); + + return result?.SgvclssClasCode; + } + catch + { + return null; + } + } + + /// + /// Get student class of year - equivalent to getClassOf in SIS.cfc + /// + private async Task GetStudentClassOfAsync(string termCode, string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getClassOf @thisTermCode = {0}, @thisPidm = {1}", termCode, pidm) + .FirstOrDefaultAsync(); + + return result?.ClassOf; + } + catch + { + return null; + } + } + + /// + /// Get student confidential scope - equivalent to getConfidentialScope in SIS.cfc + /// + private async Task GetStudentConfidentialScopeAsync(string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getConfidentialScope @Pidm = {0}", pidm) + .FirstOrDefaultAsync(); + + return result?.ZtvconfDesc; + } + catch + { + return null; + } + } + + /// + /// Get student birth date - equivalent to getBirthDate in SIS.cfc + /// + private async Task GetStudentBirthDateAsync(string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getBirthDate @pidm = {0}", pidm) + .FirstOrDefaultAsync(); + + return result?.BirthDate; + } + catch + { + return null; + } + } + + /// + /// Get student age - equivalent to getAge in SIS.cfc + /// + private async Task GetStudentAgeAsync(string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getAge @pidm = {0}", pidm) + .FirstOrDefaultAsync(); + + return result?.Age; + } + catch + { + return null; + } + } + + /// + /// Get term description - equivalent to getTermDesc in SIS.cfc + /// + private async Task GetStudentTermDescriptionAsync(string termCode) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getTermDescription @thisTermCode = {0}", termCode) + .FirstOrDefaultAsync(); + + return result?.TermDesc; + } + catch + { + return null; + } + } + + /// + /// Get degree sought - equivalent to getDegreeSought in SIS.cfc + /// + private async Task GetStudentDegreeSoughtAsync(string termCode, string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getDegreeSought @termCode = {0}, @pidm = {1}", termCode, pidm) + .FirstOrDefaultAsync(); + + if (result != null) + { + var degrees = new List(); + if (!string.IsNullOrEmpty(result.Degree1)) + degrees.Add(result.Degree1); + if (!string.IsNullOrEmpty(result.Degree2)) + degrees.Add(result.Degree2); + + return string.Join(", ", degrees); + } + + return null; + } + catch + { + return null; + } + } + + /// + /// Get academic standing - equivalent to getAcademicStanding in SIS.cfc + /// + private async Task GetStudentAcademicStandingAsync(string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getCurrentacademicStanding @pidm = {0}", pidm) + .FirstOrDefaultAsync(); + + return result?.SgvstdnAstdDesc; + } + catch + { + return null; + } + } + + /// + /// Get cumulative GPA - equivalent to getCumulativeGPA in SIS.cfc + /// + private async Task GetStudentCumulativeGPAAsync(string pidm, string termCode, string majorCode) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getCumulativeGPA @pidm = {0}, @termCode = {1}, @majorCode = {2}", pidm, termCode, majorCode) + .FirstOrDefaultAsync(); + + return result?.Gpa; + } + catch + { + return null; + } + } + + /// + /// Get class rank - equivalent to getClassRank in SIS.cfc + /// + private async Task GetStudentClassRankAsync(string pidm, string majorCode) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getClassRank @Pidm = {0}, @majorCode = {1}", pidm, majorCode) + .FirstOrDefaultAsync(); + + return result?.ClassRank; + } + catch + { + return null; + } + } + + /// + /// Get admit class year - equivalent to getAdmitClassYear in SIS.cfc + /// + private async Task GetStudentAdmitClassYearAsync(string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getAdmitClassYear @pidm = {0}", pidm) + .FirstOrDefaultAsync(); + + return result?.AdmitClassYear; + } + catch + { + return null; + } + } + + /// + /// Get admit term - equivalent to getAdmitTerm in SIS.cfc + /// + private async Task GetStudentAdmitTermAsync(string pidm, string major) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getAdmitTerm @pidm = {0}, @major = {1}", pidm, major) + .FirstOrDefaultAsync(); + + return result?.AdmitTerm; + } + catch + { + return null; + } + } + + /// + /// Check if dual degree student - equivalent to isDualDegreeStudent in SIS.cfc + /// + private async Task IsStudentDualDegreeAsync(string termCode, string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_isDualDegreeStudent @thisTermCode = {0}, @thisPidm = {1}", termCode, pidm) + .FirstOrDefaultAsync(); + + return result?.IsDualDegree == "Yes"; + } + catch + { + return false; + } + } + + /// + /// Check if DVM student - equivalent to isDVMStudent in SIS.cfc + /// + private async Task IsStudentDVMAsync(string termCode, string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_isDVMStudent @thisTermCode = {0}, @thisPidm = {1}", termCode, pidm) + .FirstOrDefaultAsync(); + + return result?.IsDVMStudent == "Yes"; + } + catch + { + return false; + } + } + + /// + /// Check if MPVM student - equivalent to isMPVMStudent in SIS.cfc + /// + private async Task IsStudentMPVMAsync(string termCode, string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_isMPVMStudent @thisTermCode = {0}, @thisPidm = {1}", termCode, pidm) + .FirstOrDefaultAsync(); + + return result?.IsMPVMStudent == "Yes"; + } + catch + { + return false; + } + } + + /// + /// Check if student is employed - equivalent to isEmployed in SIS.cfc + /// + private async Task IsStudentEmployedAsync(string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_isEmployed @pidm = {0}", pidm) + .FirstOrDefaultAsync(); + + return !string.IsNullOrEmpty(result?.WobeucePidm); + } + catch + { + return false; + } + } + + /// + /// Get student employee ID - equivalent to getEmployeeID in SIS.cfc + /// + private async Task GetStudentEmployeeIdAsync(string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getEmployeeID @thisPidm = {0}", pidm) + .FirstOrDefaultAsync(); + + return result?.EmployeeId; + } + catch + { + return null; + } + } + + /// + /// Get student employer - equivalent to getEmployer in SIS.cfc + /// + private async Task GetStudentEmployerAsync(string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getEmployer @pidm = {0}", pidm) + .FirstOrDefaultAsync(); + + return result?.WobeuceDeptName; + } + catch + { + return null; + } + } + + /// + /// Get student gender - equivalent to getGender in SIS.cfc + /// + private async Task GetStudentGenderAsync(string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getGender @pidm = {0}", pidm) + .FirstOrDefaultAsync(); + + return result?.Gender; + } + catch + { + return null; + } + } + + /// + /// Get student ethnicity - equivalent to getEthnicity in SIS.cfc + /// + private async Task GetStudentEthnicityAsync(string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getEthnicity @pidm = {0}", pidm) + .FirstOrDefaultAsync(); + + return result?.Ethnicity; + } + catch + { + return null; + } + } + + /// + /// Get student new ethnicity - equivalent to getNewEthnicity in SIS.cfc + /// + private async Task GetStudentNewEthnicityAsync(string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getNewEthnicity @pidm = {0}", pidm) + .FirstOrDefaultAsync(); + + return result?.NewEthnicity; + } + catch + { + return null; + } + } + + /// + /// Check if CA resident - equivalent to isCAResident in SIS.cfc + /// + private async Task IsStudentCAResidentAsync(string termCode, string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_isCAResident @TermCode = {0}, @Pidm = {1}", termCode, pidm) + .FirstOrDefaultAsync(); + + return result?.ResidentFlag == "Y"; + } + catch + { + return false; + } + } + + /// + /// Check if US citizen - equivalent to isUSCitizen in SIS.cfc + /// + private async Task IsStudentUSCitizenAsync(string pidm) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_isUSCitizen @Pidm = {0}", pidm) + .FirstOrDefaultAsync(); + + return result?.CitizenFlag == "Y"; + } + catch + { + return false; + } + } + + /// + /// Get student address - equivalent to getAddress in SIS.cfc + /// + private async Task GetStudentAddressAsync(string pidm, string type) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getAddress @pidm = {0}, @type = {1}", pidm, type) + .FirstOrDefaultAsync(); + + return result?.Address; + } + catch + { + return null; + } + } + + /// + /// Get student phone - equivalent to getPhone in SIS.cfc + /// + private async Task GetStudentPhoneAsync(string pidm, string type) + { + try + { + var result = await _aaudContext.Database + .SqlQueryRaw("EXEC usp_sis_getPhone @pidm = {0}, @type = {1}", pidm, type) + .FirstOrDefaultAsync(); + + return result?.Phone; + } + catch + { + return null; + } + } + + /// + /// Populate iam information + /// + private async Task PopulateIamInfoAsync(UserInfoResult result) + { + if (string.IsNullOrEmpty(result.IamId)) + { + Console.WriteLine("IAM API: result.IamId is null or empty"); + return; + } + + try + { + Console.WriteLine($"IAM API Request for IamId: {result.IamId}"); + var iamApi = new IamApi(_httpClientFactory); + + // Get people information - equivalent to iamPeople.getById() in ColdFusion + var peopleResponse = await iamApi.SearchForPerson(iamId: result.IamId); + Console.WriteLine($"IAM People Response Data: {(peopleResponse.Data != null ? peopleResponse.Data.Count() : "null")}, Error: {peopleResponse.ErrorMessage ?? "none"}"); + if (peopleResponse.Data?.Any() == true) + { + result.IamPeople = peopleResponse.Data.ToList(); + var person = peopleResponse.Data.First(); + result.PPSId = person.PpsId; + result.OFullName = person.OFullName; + result.IsHSEmployee = person.IsHSEmployee; + result.IsFaculty = person.IsFaculty; + result.IsStaff = person.IsStaff; + result.IsExternal = person.IsExternal; + } + + // Get employee associations - equivalent to iamAssociations.getEmployeeAssociations() in ColdFusion + var associationsResponse = await iamApi.GetEmployeeAssociations(result.IamId); + Console.WriteLine($"IAM Associations Response Data: {(associationsResponse.Data != null ? associationsResponse.Data.Count() : "null")}, Error: {associationsResponse.ErrorMessage ?? "none"}"); + if (associationsResponse.Data?.Any() == true) + { + result.IamAssociations = associationsResponse.Data.ToList(); + var association = associationsResponse.Data.First(); // Get first/primary association + result.AssociationsTitle = association.TitleDisplayName; + result.AssociationsTitleCode = association.TitleCode; + result.AssociationsDepartment = association.DeptDisplayName; + result.AssociationsDepartmentCode = association.DeptCode; + result.AssociationsAdminDepartment = association.AdminDeptDisplayName; + result.AssociationsAdminDepartmentAbbrev = association.AdminDeptAbbrev; + result.AssociationsAdminDepartmentCode = association.AdminDeptCode; + result.AssociationsAppointmentDepartment = association.ApptDeptDisplayName; + result.AssociationsAppointmentDepartmentAbbrev = association.ApptDeptAbbrev; + result.AssociationsAppointmentDepartmentCode = association.ApptDeptCode; + result.AssociationsPositionType = association.PositionType; + result.AssociationsEmployeeClass = association.EmplClassDesc; + result.AssociationsPercentFulltime = association.PercentFullTime; + result.AssociationsStartDate = association.AssocStartDate; + result.AssociationsEndDate = association.AssocEndDate; + } + } + catch (Exception ex) + { + Console.WriteLine($"IAM API EXCEPTION: {ex}"); + // Log exception but don't fail the entire request + } + } + + private async Task PopulateSystemRolesAsync(UserInfoResult result) + { + if (string.IsNullOrEmpty(result.MothraId)) + return; + + var systems = new[] { "VIPER", "VMACS.VMTH", "VMACS.VMLF", "VMACS.UCVMCSD" }; + + // Query tblRoleMembers and join tblRoles for this user + var roleMembers = await _rapsContext.TblRoleMembers + .Include(rm => rm.Role) + .Where(rm => rm.MemberId == result.MothraId && rm.ViewName == null) + .ToListAsync(); + + foreach (var system in systems) + { + // Filter roles belonging to the current system/instance + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + var filteredRoles = roleMembers + .Where(rm => rm.Role != null && RAPSSecurityService.RoleBelongsToInstance(system, rm.Role)) + .Select(rm => rm.Role) + .OrderBy(r => r.DisplayName ?? r.Role); + + foreach (var role in filteredRoles) + { + string displayName = role.DisplayName ?? role.Role; + result.SystemRoles.Add(new SystemRole + { + System = system, + DisplayName = FormatPermissionName(displayName) + }); + } + } + + var categories = new[] { "API", "RAPS", "SVMSecure", "VIPERForms", "VMACS" }; + foreach (var category in categories) + { + var categoryPerms = await GetUserPermissionsForSystemAsync(result.MothraId, category); + var sysPerm = new SystemPermission + { + Category = category, + Count = categoryPerms.Count, + Permissions = categoryPerms.Select(p => p.Permission).ToList() + }; + result.SystemPermissions.Add(sysPerm); + } + } + + /// + /// Get RSOP (Resultant Set of Permissions) for a user in a specific system + /// + private async Task> GetUserPermissionsForSystemAsync(string memberId, string systemPrefix) + { + var permsViaRoles = await ( + from permission in _rapsContext.TblPermissions + join rolePermissions in _rapsContext.TblRolePermissions + on permission.PermissionId equals rolePermissions.PermissionId + join memberRole in _rapsContext.TblRoleMembers + on rolePermissions.RoleId equals memberRole.RoleId + join role in _rapsContext.TblRoles + on memberRole.RoleId equals role.RoleId + where memberRole.MemberId == memberId + && (memberRole.StartDate == null || memberRole.StartDate <= DateTime.Today) + && (memberRole.EndDate == null || memberRole.EndDate >= DateTime.Today) + select new + { + permission.PermissionId, + permission.Permission, + rolePermissions.Access, + role.Role + }).ToListAsync(); + + var permsAssigned = await (from permission in _rapsContext.TblPermissions + join memberPermissions in _rapsContext.TblMemberPermissions + on permission.PermissionId equals memberPermissions.PermissionId + where memberPermissions.MemberId == memberId + && (memberPermissions.StartDate == null || memberPermissions.StartDate <= DateTime.Today) + && (memberPermissions.EndDate == null || memberPermissions.EndDate >= DateTime.Today) + select new + { + permission.PermissionId, + permission.Permission, + memberPermissions.Access + }).ToListAsync(); + + var permissions = new Dictionary(); + + // Add permissions assigned via roles + foreach (var p in permsViaRoles) + { + if (permissions.TryGetValue(p.PermissionId, out PermissionInfo? existing)) + { + // Record deny if this role is denying access + if (existing.Access == "1" && p.Access == 0) + { + existing.Access = "0"; + existing.Source = p.Role; + } + else if (existing.Access == p.Access.ToString()) + { + existing.Source += "," + p.Role; + } + } + else + { + permissions[p.PermissionId] = new PermissionInfo + { + PermissionId = p.PermissionId, + Permission = p.Permission, + Source = p.Role, + SourceType = "Role", + Access = p.Access.ToString() + }; + } + } + + // Add permissions assigned directly + foreach (var p in permsAssigned) + { + if (permissions.TryGetValue(p.PermissionId, out PermissionInfo? existing)) + { + if (existing.Access == "1" && p.Access == 0) + { + existing.Access = "0"; + existing.Source = "Member Permission"; + } + else if (existing.Access == p.Access.ToString()) + { + existing.Source += ",Member Permission"; + } + } + else + { + permissions[p.PermissionId] = new PermissionInfo + { + PermissionId = p.PermissionId, + Permission = p.Permission, + Source = "Member Permission", + SourceType = "Member", + Access = p.Access.ToString() + }; + } + } + + // Filter to only allowed permissions starting with the systemPrefix, sorted by name + return permissions.Values + .Where(p => p.Access == "1" && p.Permission.StartsWith(systemPrefix)) + .OrderBy(p => p.Permission) + .ToList(); + } + + private static string FormatPermissionName(string val) + { + if (string.IsNullOrEmpty(val)) return ""; + string clean = val; + clean = clean.Replace("CN=", "", StringComparison.OrdinalIgnoreCase); + clean = clean.Replace("OU=", "", StringComparison.OrdinalIgnoreCase); + clean = clean.Replace("DC=", "", StringComparison.OrdinalIgnoreCase); + return clean.Replace(",", "."); + } + + + /// + /// Populate UC Path information + /// + private async Task PopulateUCPathInfoAsync(UserInfoResult result) + { + if (string.IsNullOrEmpty(result.EmployeeId)) + return; + + try + { + // Get UC Path person information + var person = await _ppsContext.VwPeople + .Where(p => p.Emplid == result.EmployeeId) + .FirstOrDefaultAsync(); + + if (person != null) + { + result.UCPathFlags = GetUCPathFlags(person); + } + + // Get UC Path position information + var position = await _ppsContext.VwPersonJobPositions + .Where(p => p.Emplid == result.EmployeeId) + .OrderByDescending(p => p.Effdt) + .FirstOrDefaultAsync(); + + if (position != null) + { + result.UCPathJobCode = position.Jobcode; + result.UCPathJobDescription = position.JobcodeDesc; + result.UCPathDepartmentId = position.Deptid; + result.UCPathDepartmentDescription = position.DeptDesc; + result.UCPathJobStatus = position.JobStatus; + result.UCPathEmployeeStatus = position.EmplStatus; + result.UCPathJobStatusDescription = position.JobStatusDesc; + result.UCPathPositionEffectiveDate = position.PositionEffdt; + result.UCPathExpectedEndDate = position.ExpectedEndDate; + result.UCPathFTE = position.Fte; + result.UCPathUnion = position.UnionCd; + + // Get reports to information + if (!string.IsNullOrEmpty(position.ReportsTo)) + { + var reportsTo = await _ppsContext.VwPersonJobPositions + .Where(r => r.PositionNbr == position.ReportsTo) + .FirstOrDefaultAsync(); + + if (reportsTo != null) + { + result.UCPathReportsToName = $"{reportsTo.FirstName} {reportsTo.LastName}".Trim(); + result.UCPathReportsToPosition = reportsTo.JobcodeDesc; + } + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Warning: PopulateUCPathInfoAsync failed: {ex.Message}"); + } + + // Get UC Path History from the VwPersonJobPositionAll view + await PopulateUCPathHistoryAsync(result); + } + + /// + /// Populate UC Path History information - equivalent to get_ucpath_history in userinfo.cfc + /// + private async Task PopulateUCPathHistoryAsync(UserInfoResult result) + { + if (string.IsNullOrEmpty(result.EmployeeId)) + return; + + try + { + var historyData = await _ppsContext.VwPersonJobPositionAlls + .Where(p => p.Emplid == result.EmployeeId) + .OrderByDescending(p => p.PositionEffdt) + .ThenByDescending(p => p.Effdt) + .ToListAsync(); + + foreach (var history in historyData) + { + var ucpathResult = new UCPathResult + { + JobCode = history.Jobcode, + JobCodeDescription = history.JobcodeDesc, + DepartmentId = history.Deptid, + DepartmentDescription = history.DeptDesc, + ActionDescription = history.ActionDescr, + PositionEffectiveDate = history.PositionEffdt.HasValue ? DateOnly.FromDateTime(history.PositionEffdt.Value) : null, + ReportsTo = GetReportsToName(history), + ReportsToPosition = GetReportsToPosition(history) + }; + + result.UCPathHistory.Add(ucpathResult); + } + } + catch (Exception ex) + { + Console.WriteLine($"Warning: PopulateUCPathHistoryAsync failed: {ex.Message}"); + } + } + + /// + /// Get reports to name from UC Path history record + /// + private string GetReportsToName(VwPersonJobPositionAll history) + { + if (string.IsNullOrEmpty(history.ReportsTo)) + return string.Empty; + + try + { + // Try to find the reports to person in the same view + var reportsTo = _ppsContext.VwPersonJobPositionAlls + .Where(r => r.PositionNbr == history.ReportsTo) + .FirstOrDefault(); + + if (reportsTo != null) + { + return $"{reportsTo.FirstName} {reportsTo.LastName}".Trim(); + } + + // Fallback to current positions view + var currentReportsTo = _ppsContext.VwPersonJobPositions + .Where(r => r.PositionNbr == history.ReportsTo) + .FirstOrDefault(); + + if (currentReportsTo != null) + { + return $"{currentReportsTo.FirstName} {currentReportsTo.LastName}".Trim(); + } + } + catch + { + // Return empty string on any error + } + + return string.Empty; + } + + /// + /// Get reports to position from UC Path history record + /// + private string GetReportsToPosition(VwPersonJobPositionAll history) + { + if (string.IsNullOrEmpty(history.ReportsTo)) + return string.Empty; + + try + { + // Try to find the reports to position in the same view + var reportsTo = _ppsContext.VwPersonJobPositionAlls + .Where(r => r.PositionNbr == history.ReportsTo) + .FirstOrDefault(); + + if (reportsTo != null) + { + // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract + return reportsTo.JobcodeDesc ?? string.Empty; + } + + // Fallback to current positions view + var currentReportsTo = _ppsContext.VwPersonJobPositions + .Where(r => r.PositionNbr == history.ReportsTo) + .FirstOrDefault(); + + if (currentReportsTo != null) + { + // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract + return currentReportsTo.JobcodeDesc ?? string.Empty; + } + } + catch (Exception ex) + { + // Fall back to returning string.Empty if DB query fails. + Console.WriteLine($"Warning: GetReportsToTitleAsync failed: {ex.Message}"); + } + + return string.Empty; + } + + /// + /// Populate ID Cards information + /// + private async Task PopulateIDCardsAsync(UserInfoResult result) + { + try + { + var cards = await (from card in _idCardsContext.IdCards + join status in _idCardsContext.DvtCardStatuses + on card.IdCardCurrentStatus equals status.DvtStatusCode into statusJoin + from status in statusJoin.DefaultIfEmpty() + join reason in _idCardsContext.DvtReasons + on card.IdcardDeactivatedReason equals reason.DvtReasonCode into reasonJoin + from reason in reasonJoin.DefaultIfEmpty() + where card.IdCardLoginId == result.LoginId + orderby card.IdCardAppliedDate descending + select new + { + Card = card, + StatusDescription = status != null ? status.DvtStatusDesc : "", + DeactivatedReasonDescription = reason != null ? reason.DvtReasonDesc : "" + }).ToListAsync(); + + foreach (var item in cards) + { + var card = item.Card; + result.IDCards.Add(new IDCardResult + { + Number = card.IdCardNumber?.ToString(), + DisplayName = card.IdCardDisplayName, + LastName = card.IdCardLastName, + Line2 = card.IdCardLine2, + StatusDescription = item.StatusDescription, + DeactivatedReason = item.DeactivatedReasonDescription, + Applied = card.IdCardAppliedDate, + Issued = card.IdCardIssueDate, + Deactivated = card.IdcardDeactivatedDate + }); + } + } + catch (Exception ex) + { + Console.WriteLine($"Warning: PopulateIDCardsAsync failed: {ex.Message}"); + } + } + + /// + /// Populate Keys information + /// + private async Task PopulateKeysAsync(UserInfoResult result) + { + try + { + var keyAssignments = await (from ka in _keysContext.KeyAssignments + join k in _keysContext.Keys on ka.KeyId equals k.KeyId + where ka.AssignedTo == result.MothraId && ka.Deleted == null + orderby ka.IssuedDate descending, ka.KeyId + select new { Assignment = ka, Key = k }) + .ToListAsync(); + + foreach (var item in keyAssignments) + { + // Get issuer information from AAUD + var issuer = await _aaudContext.AaudUsers + .Where(u => u.MothraId == item.Assignment.IssuedBy) + .FirstOrDefaultAsync(); + + result.Keys.Add(new KeyResult + { + AccessDescription = item.Key.AccessDescription, + KeyNumber = item.Key.KeyNumber, + CutNumber = item.Assignment.CutNumber, + IssuedDate = item.Assignment.IssuedDate, + IssuedBy = issuer?.DisplayFullName + }); + } + } + catch (Exception ex) + { + Console.WriteLine($"Warning: PopulateKeysAsync failed: {ex.Message}"); + } + } + + /// + /// Populate Loans information + /// + private async Task PopulateLoansAsync(UserInfoResult result) + { + try + { + var loans = await _equipmentLoanContext.Loans + .Where(l => l.LoanPidm == result.Pidm) + .Include(l => l.LoanItems) + .ThenInclude(li => li.LoanitemAsset) + .OrderByDescending(l => l.LoanDate) + .ToListAsync(); + + foreach (var loan in loans) + { + foreach (var loanItem in loan.LoanItems) + { + result.Loans.Add(new LoanResult + { + AssetName = loanItem.LoanitemAsset.AssetName, + LoanDate = loan.LoanDate, + DueDate = loan.LoanDueDate, + Comments = loan.LoanComments + }); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Warning: PopulateLoansAsync failed: {ex.Message}"); + } + } + + /// + /// Populate Instinct information + /// + private async Task PopulateInstinctInfoAsync(UserInfoResult result, AaudUser user) + { + try + { + var instinctResult = await GetInstinctUserAsync(user.LastName, user.FirstName, user.MiddleName); + result.InstinctInfo = instinctResult; + + if (instinctResult.Valid) + { + result.InstinctId = instinctResult.InstinctId; + result.InstinctUsername = instinctResult.Username; + result.InstinctRoles = instinctResult.Roles; + result.InstinctStatus = instinctResult.Status; + result.InstinctIsActive = instinctResult.IsActive; + + if (!string.IsNullOrEmpty(instinctResult.PasswordExpiresAt) && + DateTime.TryParse(instinctResult.PasswordExpiresAt, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var expireDate)) + { + result.InstinctPasswordExpiresAt = expireDate; + } + } + } + catch (Exception ex) + { + result.InstinctInfo = new InstinctResult { ErrorMessage = $"Populate Exception: {ex.Message}" }; + } + } + + private static string AdClean(string a) + { + if (string.IsNullOrEmpty(a)) return ""; + var toReturn = a; + toReturn = toReturn.Replace("CN=", "", StringComparison.OrdinalIgnoreCase); + toReturn = toReturn.Replace("OU=", "", StringComparison.OrdinalIgnoreCase); + toReturn = toReturn.Replace("DC=", "", StringComparison.OrdinalIgnoreCase); + return toReturn; + } + + private static string PermFormat(string a) + { + return AdClean(a).Replace(",", "."); + } + + private static string AdFormat(string a, string[] domains) + { + var toReturn = AdClean(a); + foreach (var d in domains) + { + var domainWithCommas = d.Replace(".", ","); + toReturn = toReturn.Replace(domainWithCommas, d, StringComparison.OrdinalIgnoreCase); + } + var parts = toReturn.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(p => p.Trim()) + .Reverse() + .ToList(); + return string.Join("/", parts); + } + + /// + /// Populate Active Directory information + /// + private static async Task PopulateActiveDirectoryInfoAsync(UserInfoResult result) + { + if (string.IsNullOrEmpty(result.LoginId)) + { + return; + } + + try + { + var uinformService = new UinformService(); + var adUser = await uinformService.GetUser(samAccountName: result.LoginId); + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (adUser != null && !string.IsNullOrEmpty(adUser.SamAccountName)) + { + result.ADDisplayName = adUser.DisplayName; + result.ADMail = adUser.Mail; + result.ADSamAccountName = adUser.SamAccountName; + result.ADUserPrincipalName = adUser.UserPrincipalName; + result.ADDistinguishedName = PermFormat(adUser.DistinguishedName ?? ""); + + var allGroups = new HashSet(StringComparer.OrdinalIgnoreCase); + if (adUser.MemberOf != null) + { + foreach (var g in adUser.MemberOf) + { + allGroups.Add(g); + } + } + if (adUser.MemberOfAll != null) + { + foreach (var g in adUser.MemberOfAll) + { + allGroups.Add(g); + } + } + + var isProd = HttpHelper.Environment?.IsProduction() ?? false; + var domains = isProd + ? new[] { "ad3.ucdavis.edu", "ou.ad3.ucdavis.edu", "ucsvm.ucdavis.edu", "ad.vmth.ucdavis.edu", "vetmed.ucdavis.edu", "svm.ucdavis.edu" } + : new[] { "t3.ucdavis.edu" }; + foreach (var groupDn in allGroups) + { + var formattedGroup = AdFormat(groupDn, domains); + if (!string.IsNullOrEmpty(formattedGroup)) + { + result.ADMemberOf.Add(formattedGroup); + } + } + + // Sort the groups + result.ADMemberOf = result.ADMemberOf.OrderBy(g => g, StringComparer.OrdinalIgnoreCase).ToList(); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error populating AD info: {ex.Message}"); + } + } + + /// + /// Get user photo data + /// + public static async Task GetUserPhotoAsync(string mailId, bool useAltPhoto = false) + { + // stubbed + return null; + } + + + + + /// + /// Get Instinct user information via GraphQL API + /// + private async Task GetInstinctUserAsync(string lastName, string firstName, string? middleName) + { + var result = new InstinctResult(); + + // Get access token + var accessToken = await GetInstinctAccessTokenAsync(result); + if (string.IsNullOrEmpty(accessToken)) + { + return result; + } + + // Build name variations for matching + var nameVariations = new List { firstName }; + + var firstNameParts = firstName.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + var first = firstNameParts.FirstOrDefault() ?? ""; + + if (firstNameParts.Length > 0 && !nameVariations.Contains(first)) + { + nameVariations.Add(first); + } + + if (firstNameParts.Length > 1) + { + var sb = new StringBuilder(first); + for (int i = 1; i < firstNameParts.Length; i++) + { + if (firstNameParts[i].Length > 0) + { + sb.Append(' ').Append(firstNameParts[i][0]); + var accum = sb.ToString(); + if (!nameVariations.Contains(accum)) + { + nameVariations.Add(accum); + } + } + } + } + + var temp = nameVariations.ToList(); + if (!string.IsNullOrEmpty(middleName)) + { + var middleParts = middleName.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var name in temp) + { + foreach (var middlePart in middleParts) + { + if (middlePart.Length > 0) + { + var variation = $"{name} {middlePart[0]}"; + if (!nameVariations.Contains(variation)) + { + nameVariations.Add(variation); + } + } + } + } + } + + // Create GraphQL query + var query = $@" + query {{ + searchUsers(name: ""{lastName}"") {{ + id + initials + instinctId + isActive + isProtected + nameFirst + nameMiddle + nameLast + passwordExpiresAt + status + username + roles {{ + description + label + }} + }} + }}"; + + // Execute GraphQL query + var apiUrl = _configuration["Instinct:ApiUrl"] ?? "https://uc-davis.api.instinctvet.com/"; + var httpClient = _httpClientFactory.CreateClient(); + + var queryUrl = $"{apiUrl}?query={Uri.EscapeDataString(query)}"; + httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {accessToken}"); + + var response = await httpClient.GetAsync(queryUrl); + if (response.IsSuccessStatusCode) + { + var responseContent = await response.Content.ReadAsStringAsync(); + var graphqlResponse = JsonSerializer.Deserialize(responseContent); + + if (graphqlResponse?.Data?.SearchUsers != null) + { + bool foundMatch = false; + var matchedUser = graphqlResponse.Data.SearchUsers + .FirstOrDefault(user => nameVariations.Any(name => string.Equals(name, user.NameFirst, StringComparison.OrdinalIgnoreCase))); + + if (matchedUser != null) + { + result.Valid = true; + result.Id = matchedUser.Id; + result.Initials = matchedUser.Initials; + result.InstinctId = matchedUser.InstinctId; + result.IsActive = matchedUser.IsActive; + result.IsProtected = matchedUser.IsProtected; + result.PasswordExpiresAt = matchedUser.PasswordExpiresAt; + result.Status = matchedUser.Status; + result.Username = matchedUser.Username; + result.Roles = matchedUser.Roles? + .Where(r => r.Label != null) + .Select(r => r.Label!) + .ToList() ?? new List(); + foundMatch = true; + } + if (!foundMatch) + { + result.ErrorMessage = $"User found in API but no name match. Variations tried: {string.Join(", ", nameVariations)}. API users: {string.Join(", ", graphqlResponse.Data.SearchUsers.Select(u => $"{u.NameFirst} {u.NameLast}"))}"; + } + } + else + { + result.ErrorMessage = "GraphQL response contained no searchUsers data."; + } + } + else + { + var responseContent = await response.Content.ReadAsStringAsync(); + result.ErrorMessage = $"GraphQL query failed (Status: {response.StatusCode}): {responseContent}"; + } + return result; + } + + private static void AppendError(InstinctResult result, string msg) + { + result.ErrorMessage = string.IsNullOrEmpty(result.ErrorMessage) + ? msg + : $"{result.ErrorMessage} | {msg}"; + } + + /// + /// OAuth access token for Instinct + /// + private async Task GetInstinctAccessTokenAsync(InstinctResult result) + { + const string cacheKey = "instinct_access_token"; + + // Check cache first + if (_memoryCache.TryGetValue(cacheKey, out string? cachedToken) && !string.IsNullOrEmpty(cachedToken)) + { + return cachedToken; + } + + try + { + var apiUrl = _configuration["Instinct:ApiUrl"] ?? "https://uc-davis.api.instinctvet.com/"; + if (!apiUrl.EndsWith('/')) + { + apiUrl += "/"; + } + var tokenUrl = apiUrl + "auth/token"; + var username = "ucdavisapi"; + var password = HttpHelper.GetSetting("Credentials", "InstinctApi") ?? ""; + + if (string.IsNullOrEmpty(password)) + { + string errMsg = "Password is null or empty in configuration"; + Console.WriteLine($"[INSTINCT AUTH] {errMsg}"); + AppendError(result, errMsg); + return null; + } + + var httpClient = _httpClientFactory.CreateClient(); + var formParams = new List> + { + new("username", username), + new("password", password), + new("grant_type", "password"), + new("scope", "api_access") + }; + + var formContent = new FormUrlEncodedContent(formParams); + Console.WriteLine("[INSTINCT AUTH] Sending token POST request..."); + var response = await httpClient.PostAsync(tokenUrl, formContent); + Console.WriteLine($"[INSTINCT AUTH] Response Status Code: {response.StatusCode}"); + + if (response.IsSuccessStatusCode) + { + var responseContent = await response.Content.ReadAsStringAsync(); + var tokenResponse = JsonSerializer.Deserialize(responseContent); + if (tokenResponse != null) + { + Console.WriteLine($"[INSTINCT AUTH] Deserialized Token Length: {tokenResponse.AccessToken.Length}"); + + if (!string.IsNullOrEmpty(tokenResponse.AccessToken)) + { + // Cache token for slightly less than expiry time (subtract 2 hours as in CF code) + var cacheExpiry = TimeSpan.FromSeconds(tokenResponse.ExpiresIn - 7200); // 2 hours buffer + _memoryCache.Set(cacheKey, tokenResponse.AccessToken, cacheExpiry); + + return tokenResponse.AccessToken; + } + } + } + else + { + var responseContent = await response.Content.ReadAsStringAsync(); + string errMsg = $"Token POST request failed (Status: {response.StatusCode}): {responseContent}"; + Console.WriteLine($"[INSTINCT AUTH] {errMsg}"); + AppendError(result, errMsg); + } + } + catch (Exception ex) + { + string errMsg = $"Token request exception: {ex.Message}"; + Console.WriteLine($"[INSTINCT AUTH] {errMsg}"); + AppendError(result, errMsg); + } + + return null; + } + + /// + /// Extract UC Path flags from VwPerson entity + /// + private static List GetUCPathFlags(dynamic person) + { + var flags = new List(); + var flagDefinitions = new Dictionary + { + {"EmpAcdmcFederationFlg", "Academic Federation"}, + {"EmpAcdmcFlg", "ACDMC"}, + {"EmpAcdmcSenateFlg", "ACDMC Senate"}, + {"EmpAcdmcStdtFlg", "ACDMC Stdt"}, + {"EmpFacultyFlg", "Faculty"}, + {"EmpLadderRankFlg", "Ladder Rank"}, + {"EmpMgrFlg", "Manager"}, + {"EmpMspCareerFlg", "MSP Career"}, + {"EmpMspCareerPartialyrFlg", "MSP Career Partial Year"}, + {"EmpMspCasualFlg", "MSP Casual"}, + {"EmpMspCntrctFlg", "MSP Contract"}, + {"EmpMspFlg", "MSP"}, + {"EmpMspSeniorMgmtFlg", "MSP Senior Management"}, + {"EmpSspCareerFlg", "SSP Career"}, + {"EmpSspCareerPartialyrFlg", "SSP Career Partial Year"}, + {"EmpSspCasualFlg", "SSP Casual"}, + {"EmpSspCasualRestrictedFlg", "SSP Casual Restricted"}, + {"EmpSspCntrctFlg", "SSP Contract"}, + {"EmpSspFlg", "SSP"}, + {"EmpSspFloaterFlg", "SSP Floater"}, + {"EmpSspPerDiemFlg", "SSP Per diem"}, + {"EmpSupvrFlg", "Supervisor"}, + {"EmpTeachingFacultyFlg", "Teaching Faculty"}, + {"EmpWosempFlg", "WOSEMP"} + }; + + var personType = person.GetType(); + foreach (var flag in flagDefinitions) + { + var property = personType.GetProperty(flag.Key); + if (property != null) + { + var value = property.GetValue(person)?.ToString(); + if (value == "Y") + { + flags.Add(flag.Value); + } + } + } + return flags; + } + } + + // JSON for Instinct API + public class InstinctTokenResponse + { + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } = string.Empty; + + [JsonPropertyName("expires_in")] + public int ExpiresIn { get; set; } + + [JsonPropertyName("token_type")] + public string TokenType { get; set; } = string.Empty; + + [JsonPropertyName("scope")] + public string Scope { get; set; } = string.Empty; + } + + public class InstinctGraphQLResponse + { + [JsonPropertyName("data")] + public InstinctGraphQLData? Data { get; set; } + } + + public class InstinctGraphQLData + { + [JsonPropertyName("searchUsers")] + public List? SearchUsers { get; set; } + } + + public class InstinctUser + { + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("initials")] + public string? Initials { get; set; } + + [JsonPropertyName("instinctId")] + public string? InstinctId { get; set; } + + [JsonPropertyName("isActive")] + public bool IsActive { get; set; } + + [JsonPropertyName("isProtected")] + public bool IsProtected { get; set; } + + [JsonPropertyName("nameFirst")] + public string? NameFirst { get; set; } + + [JsonPropertyName("nameMiddle")] + public string? NameMiddle { get; set; } + + [JsonPropertyName("nameLast")] + public string? NameLast { get; set; } + + [JsonPropertyName("passwordExpiresAt")] + public string? PasswordExpiresAt { get; set; } + + [JsonPropertyName("status")] + public string? Status { get; set; } + + [JsonPropertyName("username")] + public string? Username { get; set; } + + [JsonPropertyName("roles")] + // ReSharper disable once CollectionNeverUpdated.Global + public List? Roles { get; set; } + } + + public class InstinctRole + { + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("label")] + public string? Label { get; set; } + } + + // Helper class for permission processing + public class PermissionInfo + { + public int PermissionId { get; set; } + public string Permission { get; set; } = string.Empty; + public string Access { get; set; } = string.Empty; + public string Source { get; set; } = string.Empty; + public string SourceType { get; set; } = string.Empty; + } + + // Result classes for student stored procedures + public class TermResult + { + public string? TermCode { get; set; } + } + + public class PriorNameResult + { + public string? StudentName { get; set; } + public DateTime? ActivityDate { get; set; } + } + + public class BannerIdResult + { + public string? SpridenId { get; set; } + } + + public class ConfidentialResult + { + public string? SpbpersConfidInd { get; set; } + } + + public class StudentStatusResult + { + public string? RegStatus { get; set; } + } + + public class RegistrationStatusResult + { + public string? Status { get; set; } + } + + public class MajorResult + { + public string? SgbstdnMajrCode1 { get; set; } + } + + public class AllMajorsResult + { + public string? SgbstdnMajrCode1 { get; set; } + public string? SgbstdnMajrCode2 { get; set; } + } + + public class ClassLevelResult + { + public string? SgvclssClasCode { get; set; } + } + + public class ClassOfResult + { + public string? ClassOf { get; set; } + } + + // Additional result classes for comprehensive student information + public class ConfidentialScopeResult + { + public string? ZtvconfDesc { get; set; } + } + + public class BirthDateResult + { + public string? BirthDate { get; set; } + } + + public class AgeResult + { + public string? Age { get; set; } + } + + public class TermDescResult + { + public string? TermDesc { get; set; } + } + + public class DegreeSoughtResult + { + public string? Degree1 { get; set; } + public string? Degree2 { get; set; } + } + + public class AcademicStandingResult + { + public string? SgvstdnAstdDesc { get; set; } + } + + public class GPAResult + { + public string? Gpa { get; set; } + } + + public class ClassRankResult + { + public string? ClassRank { get; set; } + } + + public class AdmitClassYearResult + { + public string? AdmitClassYear { get; set; } + } + + public class AdmitTermResult + { + public string? AdmitTerm { get; set; } + } + + public class DualDegreeResult + { + public string? IsDualDegree { get; set; } + } + + public class DVMStudentResult + { + public string? IsDVMStudent { get; set; } + } + + public class MPVMStudentResult + { + public string? IsMPVMStudent { get; set; } + } + + public class EmployedResult + { + public string? WobeucePidm { get; set; } + } + + public class StudentEmployeeIdResult + { + public string? EmployeeId { get; set; } + } + + public class EmployerResult + { + public string? WobeuceDeptName { get; set; } + } + + public class GenderResult + { + public string? Gender { get; set; } + } + + public class EthnicityResult + { + public string? Ethnicity { get; set; } + } + + public class NewEthnicityResult + { + public string? NewEthnicity { get; set; } + } + + public class CAResidentResult + { + public string? ResidentFlag { get; set; } + } + + public class USCitizenResult + { + public string? CitizenFlag { get; set; } + } + + public class AddressResult + { + public string? Address { get; set; } + } + + public class PhoneResult + { + public string? Phone { get; set; } + } + + public class AuthDbRecord + { + public string Credential { get; set; } = string.Empty; + public string Username { get; set; } = string.Empty; + public string EncryptedString { get; set; } = string.Empty; + } +} + diff --git a/web/Areas/Directory/Views/Card.cshtml b/web/Areas/Directory/Views/Card.cshtml index 53158cec2..a87e35b78 100644 --- a/web/Areas/Directory/Views/Card.cshtml +++ b/web/Areas/Directory/Views/Card.cshtml @@ -1,8 +1,8 @@ -@using Viper.Classes.SQLContext; -@model Viper.Models.AAUD.AaudUser; +@using Viper.Classes.SQLContext; +@model Viper.Areas.Directory.Models.DirectoryUser; @{ RAPSContext? rapsContext = (RAPSContext?)Context.RequestServices.GetService(typeof(RAPSContext)); - IUserHelper UserHelper = new UserHelper(); + IUserHelper userHelper = new UserHelper(); ViewData["Title"] = "Directory"; }

Directory

@@ -21,16 +21,19 @@ color="primary" label="Search all of UCD"> - - + @if (Model.CanDisplayIDs) + { + + + }
- + User Information @@ -38,34 +41,34 @@ Email {{user.mailId}}@@ucdavis.edu - + Emulate {{user.name}} - + AAUD Check - + ID Check - + MSO/CAO Lookup - @if (UserHelper.HasPermission(rapsContext, UserHelper.GetCurrentUser(), "SVMSecure.DirectoryUCPathInfo")) + @if (userHelper.HasPermission(rapsContext, userHelper.GetCurrentUser(), "SVMSecure.DirectoryUCPathInfo")) { @: - + UCPath Info } - @if (UserHelper.HasPermission(rapsContext, UserHelper.GetCurrentUser(), "SVMSecure.CATS.ServiceDesk")) + @if (userHelper.HasPermission(rapsContext, userHelper.GetCurrentUser(), "SVMSecure.CATS.ServiceDesk")) { @: - + Alt. Photo @@ -74,7 +77,7 @@
BannerID: - @if (UserHelper.HasPermission(rapsContext, UserHelper.GetCurrentUser(), "SVMSecure.SIS.AllStudents")) + @if (userHelper.HasPermission(rapsContext, userHelper.GetCurrentUser(), "SVMSecure.SIS.AllStudents")) { - @: {{ user.spridenId }} + @: {{ user.spridenId }} } else { @@ -135,21 +138,45 @@ ucd: getItemFromStorage("directory_ucd") ?? false, ids: getItemFromStorage("directory_ids") ?? false, spinner: false, - results: [] + results: [], + searchId: 0 } }, methods: { findUsers: async function () { this.spinner = false; this.results = []; - var urlBase = "@Url.Content("~/Directory/search/")" + this.userSearch; + + var search = this.userSearch.trim(); + if (search.length < 2) { + return; + } + + this.searchId++; + var currentSearchId = this.searchId; + + var urlBase = "@Url.Content("~/Directory/search")" + "?search=" + encodeURIComponent(search); if (this.ucd) { - urlBase += "/ucd" + urlBase += "&ucd=true"; } - if (this.userSearch.length >= 2) { - this.spinner = true; - this.results = (await viperFetch(this, urlBase)) - this.spinner = false; + + this.spinner = true; + try { + var response = await viperFetch(this, urlBase); + if (currentSearchId === this.searchId) { + this.results = response || []; + } + } finally { + if (currentSearchId === this.searchId) { + this.spinner = false; + } + } + }, + onLinkClick: function (e, msg) { + if (e.button === 0 && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) { + this.$q.loading.show({ + message: msg || 'Loading...' + }); } } }, @@ -175,4 +202,4 @@ } }) -} \ No newline at end of file +} diff --git a/web/Areas/Directory/Views/Table.cshtml b/web/Areas/Directory/Views/Table.cshtml index 6427c31a1..c4c60a94d 100644 --- a/web/Areas/Directory/Views/Table.cshtml +++ b/web/Areas/Directory/Views/Table.cshtml @@ -1,8 +1,8 @@ -@using Viper.Classes.SQLContext; +@using Viper.Classes.SQLContext; @model Viper.Models.AAUD.AaudUser; @{ RAPSContext? rapsContext = (RAPSContext?)Context.RequestServices.GetService(typeof(RAPSContext)); - IUserHelper UserHelper = new UserHelper(); + IUserHelper userHelper = new UserHelper(); ViewData["Title"] = "Directory"; }

Directory

@@ -38,7 +38,7 @@ no-results-label="No results found">