Skip to content

Commit 710705b

Browse files
Merge branch 'feature/userinfo' into Development
# Conflicts: # web/wwwroot/css/userinfo.css
2 parents dd885de + 59710b1 commit 710705b

10 files changed

Lines changed: 122 additions & 23 deletions

File tree

‎VueApp/src/styles/colors.css‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,22 @@
112112
border-left: 0.25rem solid var(--q-negative);
113113
color: var(--q-negative);
114114
}
115+
116+
/* Warning-state surface — mirrors StatusBanner type="warning" treatment
117+
(own padding/layout, not layered onto Quasar's .q-banner) for Razor pages
118+
that have no Vue mount, e.g. UserInfo.cshtml's unavailable-sections banner.
119+
Mirrored into web/wwwroot/css/site.css so Razor and Vue render it
120+
identically - see that copy for why it can't just reuse .q-banner. */
121+
.warning-surface {
122+
display: flex;
123+
align-items: flex-start;
124+
gap: 0.75rem;
125+
padding: 0.75rem 1rem;
126+
border-radius: 4px;
127+
background-color: color-mix(in srgb, var(--q-warning) 15%, white);
128+
border-left: 0.25rem solid var(--q-warning);
129+
color: #5d4600;
130+
}
131+
.warning-surface .material-icons {
132+
flex: none;
133+
}

‎scripts/build-dotnet.js‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ const artifactsPath = ".artifacts-precommit"
2525
// that, which would surface as a false "build failed" instead of the real result.
2626
const MAX_BUILD_OUTPUT = 10_485_760
2727

28+
// .artifacts-precommit is a separate, isolated output path (deliberately kept apart from
29+
// the normal bin/obj folders to avoid file-lock conflicts with a running dev server), so a
30+
// rebuild here doesn't get the benefit of warm incremental state the way a plain `dotnet
31+
// build` does. A cold build of this solution - more so right after a merge, and with
32+
// SonarAnalyzer's security/vulnerability rule categories enabled - can take a while. 120s
33+
// was too tight and got mistaken for a real compile failure when execFileSync killed the
34+
// process mid-build; give it more headroom.
35+
const BUILD_TIMEOUT_MS = 600_000
36+
2837
// Check if either project needs rebuild
2938
const webNeedsBuild = needsBuild("web", "Viper.csproj")
3039
const testNeedsBuild = needsBuild("test", "Viper.test.csproj")
@@ -55,7 +64,7 @@ try {
5564
["build", "test/", "--artifacts-path", artifactsPath, "--verbosity", "quiet", "--nologo"],
5665
{
5766
encoding: "utf8",
58-
timeout: 120_000,
67+
timeout: BUILD_TIMEOUT_MS,
5968
maxBuffer: MAX_BUILD_OUTPUT,
6069
stdio: ["inherit", "pipe", "pipe"],
6170
env: { ...env, DOTNET_USE_COMPILER_SERVER: "1" },
@@ -68,6 +77,25 @@ try {
6877
logger.success("Build complete")
6978
} catch (error) {
7079
const output = (error.stdout || "") + (error.stderr || "")
80+
81+
if (error.signal) {
82+
// execFileSync kills the child and sets `signal` (not a normal exit code) when the
83+
// timeout fires - that's an environment/timing issue, not a deterministic function
84+
// of the code, so it must NOT be cached as a build failure. Caching it would make
85+
// every future run replay this same "failure" (via the cached-output branch above)
86+
// for as long as the file hashes stay unchanged, exactly like a real compile error,
87+
// even though the code may be fine and just needs another (or longer) attempt.
88+
logger.error(
89+
`Build timed out after ${BUILD_TIMEOUT_MS / 1000}s (signal ${error.signal}) - ` +
90+
"not a compile error, so nothing was cached. Try again; if it keeps timing out, " +
91+
"BUILD_TIMEOUT_MS in this script may need to go up further.",
92+
)
93+
if (output) {
94+
console.error(filterBuildErrors(output))
95+
}
96+
process.exit(1)
97+
}
98+
7199
// Cache failure - no point rebuilding if code hasn't changed
72100
markAsBuilt("web", "Viper.csproj", output, false)
73101
markAsBuilt("test", "Viper.test.csproj", output, false)

‎web/Areas/CMS/Services/CmsUserPhotoService.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public async Task<CmsUserPhotoResult> GetUserPhotoAsync(string? mailId, string?
8787
return (mailId, iamId);
8888
}
8989

90-
var query = _aaudContext.AaudUsers.AsNoTracking().Where(u => u.Current != 0);
90+
var query = _aaudContext.AaudUsers.AsNoTracking().Where(u => u.Current != 0 || u.Future != 0);
9191
if (!string.IsNullOrEmpty(mailId))
9292
{
9393
query = query.Where(u => u.MailId == mailId);

‎web/Areas/Directory/Models/LdapUserContact.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ public class LdapUserContact
1515
public string DisplayName { get; set; } = null!;
1616
public string EduPersonNickname { get; set; } = null!;
1717
public string Title { get; set; } = null!;
18-
public string LabeledUri { get; set; } = null!;
1918
public string PostalAddress { get; set; } = null!;
2019
public string TelephoneNumber { get; set; } = null!;
2120
public string Mobile { get; set; } = null!;
2221
public string Mail { get; set; } = null!;
22+
public string LabeledUri { get; set; } = null!;
2323
public string UcdStudentLevel { get; set; } = null!;
2424
public string UcdStudentSid { get; set; } = null!;
2525
public string UcdPersonPidm { get; set; } = null!;
@@ -51,11 +51,11 @@ public LdapUserContact(SearchResultEntry entry)
5151
case "eduPersonNickname": EduPersonNickname = v.ToString(); break;
5252

5353
case "title": Title = v.ToString(); break;
54-
case "labeledUri": LabeledUri = v.ToString(); break;
5554
case "postalAddress": PostalAddress = v.ToString(); break;
5655
case "telephoneNumber": TelephoneNumber = v.ToString(); break;
5756
case "mobile": Mobile = v.ToString(); break;
5857
case "mail": Mail = v.ToString(); break;
58+
case "labeledUri": LabeledUri = v.ToString(); break;
5959

6060
case "employeeNumber": EmployeeNumber = v.ToString(); break;
6161
case "ucdStudentLevel": UcdStudentLevel = v.ToString(); break;

‎web/Areas/Directory/Services/UserInfoService.cs‎

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -454,23 +454,38 @@ private async Task PopulateStudentInfoAsync(UserInfoResult result)
454454
}
455455

456456
/// <summary>
457-
/// Get current or future term for student - equivalent to getCurrentOrFutureTermForUser in SIS.cfc
457+
/// Get current or future term for student - equivalent to getCurrentOrFutureTermForUser in SIS.cfc.
458+
///
459+
/// Runs over its own connection built from the AAUD connection string rather than
460+
/// _aaudContext, which this class otherwise uses only for EF entity queries - mixing raw
461+
/// SQL and EF entities on the same context causes auth failures. No database qualifier on
462+
/// the proc name either, so this follows whatever database the connection string points at,
463+
/// same as the SIS raw SQL calls below.
458464
/// </summary>
459465
private async Task<string?> GetCurrentOrFutureTermForStudentAsync(UserInfoResult result, string pidm)
460466
{
461467
try
462468
{
469+
var connectionString = _configuration.GetConnectionString("AAUD")
470+
?? throw new InvalidOperationException("Connection string 'AAUD' not configured");
471+
472+
await using var connection = new Microsoft.Data.SqlClient.SqlConnection(connectionString);
473+
await using var command = new Microsoft.Data.SqlClient.SqlCommand(
474+
"EXEC dbo.usp_get_CurrentOrFutureTermForUser @pidm = @pidm, @loginID = NULL, @termCode = @termCode OUTPUT",
475+
connection);
476+
477+
command.Parameters.Add(new Microsoft.Data.SqlClient.SqlParameter("@pidm", pidm));
478+
463479
var termCodeParam = new Microsoft.Data.SqlClient.SqlParameter
464480
{
465481
ParameterName = "@termCode",
466482
SqlDbType = System.Data.SqlDbType.Int,
467483
Direction = System.Data.ParameterDirection.Output
468484
};
485+
command.Parameters.Add(termCodeParam);
469486

470-
await _aaudContext.Database.ExecuteSqlRawAsync(
471-
"EXEC AAUD.dbo.usp_get_CurrentOrFutureTermForUser @pidm = @pidm, @loginID = NULL, @termCode = @termCode OUTPUT",
472-
new Microsoft.Data.SqlClient.SqlParameter("@pidm", pidm),
473-
termCodeParam);
487+
await connection.OpenAsync();
488+
await command.ExecuteNonQueryAsync();
474489

475490
var value = termCodeParam.Value;
476491
return value == null || value == DBNull.Value ? null : value.ToString();
@@ -723,7 +738,7 @@ private async Task PopulateIamInfoAsync(UserInfoResult result)
723738
// Get people information - equivalent to iamPeople.getById() in ColdFusion
724739
var peopleResponse = await iamApi.SearchForPerson(iamId: result.IamId);
725740
_logger.LogDebug("IAM people response data count {Count}, error {Error}",
726-
peopleResponse.Data?.Count(), peopleResponse.ErrorMessage ?? "none");
741+
peopleResponse.Data?.Count(), LogSanitizer.SanitizeString(peopleResponse.ErrorMessage) ?? "none");
727742
if (peopleResponse.Data?.Any() == true)
728743
{
729744
result.IamPeople = peopleResponse.Data.ToList();
@@ -739,7 +754,7 @@ private async Task PopulateIamInfoAsync(UserInfoResult result)
739754
// Get employee associations - equivalent to iamAssociations.getEmployeeAssociations() in ColdFusion
740755
var associationsResponse = await iamApi.GetEmployeeAssociations(result.IamId);
741756
_logger.LogDebug("IAM associations response data count {Count}, error {Error}",
742-
associationsResponse.Data?.Count(), associationsResponse.ErrorMessage ?? "none");
757+
associationsResponse.Data?.Count(), LogSanitizer.SanitizeString(associationsResponse.ErrorMessage) ?? "none");
743758
if (associationsResponse.Data?.Any() == true)
744759
{
745760
result.IamAssociations = associationsResponse.Data.ToList();
@@ -1513,9 +1528,14 @@ query SearchUsers($name: String!) {
15131528
}
15141529
else
15151530
{
1531+
// No candidates is the common case - most VIPER users don't have an
1532+
// Instinct account. That's not a failure, so leave Valid false and
1533+
// ErrorMessage unset: PopulateInstinctInfoAsync only flags "Instinct" as
1534+
// an unavailable section when ErrorMessage is set, and this isn't one.
15161535
// Do not include other candidates' names from the search results here -
15171536
// they belong to unrelated people and would leak into this user's directory page.
1518-
result.ErrorMessage = $"User found in API but no name match. Variations tried: {string.Join(", ", nameVariations)}.";
1537+
_logger.LogDebug("Instinct API: no account matched {LastName}, {FirstName}. Variations tried: {Variations}",
1538+
LogSanitizer.SanitizeString(lastName), LogSanitizer.SanitizeString(firstName), string.Join(", ", nameVariations));
15191539
}
15201540
}
15211541
else
@@ -1548,7 +1568,7 @@ private static void AppendError(InstinctResult result, string msg)
15481568
if (string.IsNullOrWhiteSpace(apiUrl))
15491569
{
15501570
const string errMsg = "Instinct:ApiUrl is not configured";
1551-
_logger.LogWarning("Instinct API: {ErrorMessage}", errMsg);
1571+
_logger.LogWarning("Instinct API: {ErrorMessage}", LogSanitizer.SanitizeString(errMsg));
15521572
AppendError(result, errMsg);
15531573
return null;
15541574
}
@@ -1586,7 +1606,7 @@ private static void AppendError(InstinctResult result, string msg)
15861606
if (string.IsNullOrEmpty(password))
15871607
{
15881608
string errMsg = "Password is null or empty in configuration";
1589-
_logger.LogWarning("Instinct auth: {ErrorMessage}", errMsg);
1609+
_logger.LogWarning("Instinct auth: {ErrorMessage}", LogSanitizer.SanitizeString(errMsg));
15901610
AppendError(result, errMsg);
15911611
return null;
15921612
}
@@ -1629,7 +1649,7 @@ private static void AppendError(InstinctResult result, string msg)
16291649
{
16301650
var responseContent = await response.Content.ReadAsStringAsync();
16311651
string errMsg = $"Token POST request failed (Status: {response.StatusCode}): {responseContent}";
1632-
_logger.LogWarning("Instinct auth: {ErrorMessage}", errMsg);
1652+
_logger.LogWarning("Instinct auth: {ErrorMessage}", LogSanitizer.SanitizeString(errMsg));
16331653
AppendError(result, errMsg);
16341654
}
16351655
}

‎web/Areas/Directory/Views/Card.cshtml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
</q-card-section>
8080
<q-card-section class="row">
8181
<q-card-section class="col photo">
82-
<q-img :src="'@HttpHelper.GetRootURL()/api/cms/photos/by-mail/' + user.mailId + '?altphoto=true'"
82+
<q-img :src="'@HttpHelper.GetRootURL()/api/cms/photos/by-iam/' + user.iamId + '?altphoto=true'"
8383
:alt="'Photo of ' + user.name"
8484
class="photo_avatar"
8585
fit="cover"

‎web/Areas/Directory/Views/UserInfo.cshtml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@
88
}
99
<article class="userinfo">
1010

11+
@* Unavailable Sections *@
12+
@if (Model.UnavailableSections.Any())
13+
{
14+
<div class="warning-surface" role="status">
15+
<i class="q-icon notranslate material-icons" aria-hidden="true" role="img">warning</i>
16+
<div class="text-body2">
17+
<strong>Some information may be unavailable.</strong> The following sections could not be loaded and may be showing incomplete data: @string.Join(", ", Model.UnavailableSections).
18+
</div>
19+
</div>
20+
}
1121

1222
@* Directory Information *@
1323
@if (Model.CanViewDirectoryDetail || Model.IsOwnPage)

‎web/Classes/SQLContext/PPSContext.cs‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,6 @@ public PPSContext(DbContextOptions<PPSContext> options)
6868

6969
public virtual DbSet<EthnicityGender> EthnicityGenders { get; set; }
7070

71-
public virtual DbSet<EthnicityGender20210201> EthnicityGender20210201s { get; set; }
72-
7371
public virtual DbSet<Export> Exports { get; set; }
7472

7573
public virtual DbSet<FurloughTarget> FurloughTargets { get; set; }
@@ -244,8 +242,6 @@ public PPSContext(DbContextOptions<PPSContext> options)
244242

245243
public virtual DbSet<UcpathMissingPerson> UcpathMissingPeople { get; set; }
246244

247-
public virtual DbSet<UcpathMissingPerson20190821> UcpathMissingPerson20190821s { get; set; }
248-
249245
public virtual DbSet<UcpathOverride> UcpathOverrides { get; set; }
250246

251247
public virtual DbSet<UcpathVerificationItem> UcpathVerificationItems { get; set; }

‎web/wwwroot/css/site.css‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,3 +523,24 @@ table.htable tr th {
523523
border-left: 0.25rem solid var(--q-negative);
524524
color: var(--q-negative);
525525
}
526+
527+
/* Warning-state surface - same tint/border/text treatment as StatusBanner
528+
type="warning" (see VueApp/src/components/StatusBanner.vue) for Razor
529+
pages, which have no Vue mount and so can't render StatusBanner directly.
530+
Self-contained (own padding/layout) rather than layered onto Quasar's
531+
.q-banner: site.css loads before quasar.prod.css in _VIPERLayout.cshtml,
532+
so a shared class would lose the background-color override to Quasar's
533+
own white .q-banner background at equal specificity. */
534+
.warning-surface {
535+
display: flex;
536+
align-items: flex-start;
537+
gap: 0.75rem;
538+
padding: 0.75rem 1rem;
539+
border-radius: 4px;
540+
background-color: color-mix(in srgb, var(--q-warning) 15%, white);
541+
border-left: 0.25rem solid var(--q-warning);
542+
color: #5d4600;
543+
}
544+
.warning-surface .material-icons {
545+
flex: none;
546+
}

‎web/wwwroot/css/userinfo.css‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@
7272
padding-top: 1em;
7373
border-top: 1px solid #888;
7474
}
75-
75+
.userinfo .warning-surface + div {
76+
border-top: none;
77+
}
7678
.direct {
7779
position: relative;
7880
min-height: 17rem;
@@ -86,19 +88,22 @@
8688

8789
.direct span.photo2 {
8890
position: absolute;
89-
top: 10.5rem;
90-
left: 25rem;
91+
top: 3rem;
92+
left: 33rem;
9193
}
9294

9395
.userinfo span.photo b,
9496
.userinfo span.photo2 b {
9597
display: block;
9698
}
99+
<<<<<<< HEAD
97100

98101
.userinfo span.photo2 b {
99102
padding-top: 1.5rem;
100103
}
101104

105+
=======
106+
>>>>>>> feature/userinfo
102107
.direct ul {
103108
width: 23rem;
104109
}

0 commit comments

Comments
 (0)