diff --git a/Gordon360/Controllers/DiningController.cs b/Gordon360/Controllers/DiningController.cs index 5f0eaaf89..6c4a6a3da 100644 --- a/Gordon360/Controllers/DiningController.cs +++ b/Gordon360/Controllers/DiningController.cs @@ -23,7 +23,7 @@ public async Task> GetAsync() var sessionCode = Helpers.GetCurrentSession(context); var authenticatedUsername = AuthUtils.GetUsername(User); var authenticatedUserId = int.Parse(accountService.GetAccountByUsername(authenticatedUsername).GordonID); - var diningInfo = diningService.GetDiningPlanInfo(authenticatedUserId, sessionCode); + var diningInfo = await diningService.GetDiningPlanInfoAsync(authenticatedUserId, sessionCode); if (diningInfo == null) { @@ -31,7 +31,7 @@ public async Task> GetAsync() } if (diningInfo.ChoiceDescription == "None") { - var diningBalance = DiningService.GetBalance(authenticatedUserId, FACSTAFF_MEALPLAN_ID); + var diningBalance = await diningService.GetBalanceAsync(authenticatedUserId, FACSTAFF_MEALPLAN_ID); if (diningBalance == null) { return NotFound(); diff --git a/Gordon360/Documentation/Gordon360.xml b/Gordon360/Documentation/Gordon360.xml index 1e12248d8..cacfbfd34 100644 --- a/Gordon360/Documentation/Gordon360.xml +++ b/Gordon360/Documentation/Gordon360.xml @@ -1317,6 +1317,14 @@ From account table + + + Validates a specific named options instance (or all when is ). + + The name of the options instance being validated. + The options instance. + Validation result. + Service Class that facilitates data transactions between the AcademicCheckInController and the CheckIn database model. @@ -1597,7 +1605,12 @@ Service that allows for meal control - + + + Service that allows for meal control + + + @@ -1605,7 +1618,7 @@ - + Get information about the selected plan for the student user diff --git a/Gordon360/Models/ViewModels/ProfileViewModel.cs b/Gordon360/Models/ViewModels/ProfileViewModel.cs index 9c0fefecf..fe1167681 100644 --- a/Gordon360/Models/ViewModels/ProfileViewModel.cs +++ b/Gordon360/Models/ViewModels/ProfileViewModel.cs @@ -1,105 +1,108 @@ -namespace Gordon360.Models.ViewModels; +using System.Text.Json.Serialization; -public record ProfileViewModel( +namespace Gordon360.Models.ViewModels; + +public record ProfileViewModel +{ // All Profiles - string ID, - string Title, - string FirstName, - string MiddleName, - string LastName, - string Suffix, - string MaidenName, - string NickName, - string Email, - string Gender, - string HomeStreet1, - string HomeStreet2, - string HomeCity, - string HomeState, - string HomePostalCode, - string HomeCountry, - string HomePhone, - string HomeFax, - string AD_Username, - int? show_pic, - int? preferred_photo, - string Country, - string Barcode, - string Facebook, - string Twitter, - string Instagram, - string LinkedIn, - string Handshake, - string Calendar, + public string ID { get; set; } + public string Title { get; set; } + public string FirstName { get; set; } + public string MiddleName { get; set; } + public string LastName { get; set; } + public string Suffix { get; set; } + public string MaidenName { get; set; } + public string NickName { get; set; } + public string Email { get; set; } + public string Gender { get; set; } + public string HomeStreet1 { get; set; } + public string HomeStreet2 { get; set; } + public string HomeCity { get; set; } + public string HomeState { get; set; } + public string HomePostalCode { get; set; } + public string HomeCountry { get; set; } + public string HomePhone { get; set; } + public string HomeFax { get; set; } + public string AD_Username { get; set; } + public int? show_pic { get; set; } + public int? preferred_photo { get; set; } + public string Country { get; set; } + public string Barcode { get; set; } + public string Facebook { get; set; } + public string Twitter { get; set; } + public string Instagram { get; set; } + public string LinkedIn { get; set; } + public string Handshake { get; set; } + public string Calendar { get; set; } // Student Only - string OnOffCampus, - string OffCampusStreet1, - string OffCampusStreet2, - string OffCampusCity, - string OffCampusState, - string OffCampusPostalCode, - string OffCampusCountry, - string OffCampusPhone, - string OffCampusFax, - string Major3, - string Major3Description, - string Minor1, - string Minor1Description, - string Minor2, - string Minor2Description, - string Minor3, - string Minor3Description, - string GradDate, - string PlannedGradYear, - string MobilePhone, - bool IsMobilePhonePrivate, - int? ChapelRequired, - int? ChapelAttended, - string Cohort, - string Class, - string AdvisorIDs, - string Married, - string Commuter, + public string OnOffCampus { get; set; } + public string OffCampusStreet1 { get; set; } + public string OffCampusStreet2 { get; set; } + public string OffCampusCity { get; set; } + public string OffCampusState { get; set; } + public string OffCampusPostalCode { get; set; } + public string OffCampusCountry { get; set; } + public string OffCampusPhone { get; set; } + public string OffCampusFax { get; set; } + public string Major3 { get; set; } + public string Major3Description { get; set; } + public string Minor1 { get; set; } + public string Minor1Description { get; set; } + public string Minor2 { get; set; } + public string Minor2Description { get; set; } + public string Minor3 { get; set; } + public string Minor3Description { get; set; } + public string GradDate { get; set; } + public string PlannedGradYear { get; set; } + public string MobilePhone { get; set; } + public bool IsMobilePhonePrivate { get; set; } + public int? ChapelRequired { get; set; } + public int? ChapelAttended { get; set; } + public string Cohort { get; set; } + public string Class { get; set; } + public string AdvisorIDs { get; set; } + public string Married { get; set; } + public string Commuter { get; set; } // Alumni Only - string? WebUpdate, - string HomeEmail, - string MaritalStatus, - string College, - string ClassYear, - string? PreferredClassYear, - string ShareName, - string? ShareAddress, + public int? WebUpdate { get; set; } + public string HomeEmail { get; set; } + public string MaritalStatus { get; set; } + public string College { get; set; } + public string ClassYear { get; set; } + public string? PreferredClassYear { get; set; } + public string ShareName { get; set; } + public string? ShareAddress { get; set; } // Student And Alumni Only - string Major, - string Major1Description, - string Major2, - string Major2Description, - string grad_student, + public string Major { get; set; } + public string Major1Description { get; set; } + public string Major2 { get; set; } + public string Major2Description { get; set; } + public string grad_student { get; set; } // FacStaff Only - string? OnCampusDepartment, - string? Type, - string? office_hours, - string Dept, - string Mail_Description, + public string? OnCampusDepartment { get; set; } + public string? Type { get; set; } + public string? office_hours { get; set; } + public string Dept { get; set; } + public string Mail_Description { get; set; } // FacStaff and Alumni Only - string JobTitle, - string SpouseName, + public string JobTitle { get; set; } + public string SpouseName { get; set; } // FacStaff and Student Only - string BuildingDescription, - string Mail_Location, - string OnCampusBuilding, - string OnCampusRoom, - string OnCampusPhone, - string OnCampusPrivatePhone, - string OnCampusFax, - string KeepPrivate, + public string BuildingDescription { get; set; } + public string Mail_Location { get; set; } + public string OnCampusBuilding { get; set; } + public string OnCampusRoom { get; set; } + public string OnCampusPhone { get; set; } + public string OnCampusPrivatePhone { get; set; } + public string OnCampusFax { get; set; } + public string KeepPrivate { get; set; } // ProfileViewModel Only - string PersonType - ); \ No newline at end of file + public string PersonType { get; set; } +} diff --git a/Gordon360/Options/BonAppetitOptions.cs b/Gordon360/Options/BonAppetitOptions.cs new file mode 100644 index 000000000..a5a05b049 --- /dev/null +++ b/Gordon360/Options/BonAppetitOptions.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Options; +using System.ComponentModel.DataAnnotations; + +namespace Gordon360.Options; + +public sealed record BonAppetitOptions +{ + public const string BonAppetit = "BonAppetit"; + + [Required] + public required string IssuerID { get; set; } + [Required] + public required string ApplicationID { get; set; } + [Required] + public required string Secret { get; set; } +} + +[OptionsValidator] +public partial class ValidateBonAppetitOptions : IValidateOptions { } diff --git a/Gordon360/Options/OptionsExtensions.cs b/Gordon360/Options/OptionsExtensions.cs new file mode 100644 index 000000000..401751e86 --- /dev/null +++ b/Gordon360/Options/OptionsExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Gordon360.Options; + +public static class OptionsExtensions +{ + public static IServiceCollection Add360Options(this IServiceCollection services) + { + services.AddSingleton, ValidateBonAppetitOptions>(); + services.AddOptions() + .BindConfiguration(BonAppetitOptions.BonAppetit) + .ValidateOnStart(); + + return services; + } +} diff --git a/Gordon360/Program.cs b/Gordon360/Program.cs index 794dd0807..744ec0aa3 100644 --- a/Gordon360/Program.cs +++ b/Gordon360/Program.cs @@ -2,6 +2,7 @@ using Gordon360.Models.CCT.Context; using Gordon360.Models.MyGordon.Context; using Gordon360.Models.webSQL.Context; +using Gordon360.Options; using Gordon360.Services; using Gordon360.Utilities; using Microsoft.AspNetCore.Builder; @@ -13,7 +14,6 @@ using Microsoft.Identity.Web; using Microsoft.OpenApi.Models; using Serilog; -using Serilog.Formatting.Compact; using System; using System.Collections.Generic; using System.IO; @@ -92,6 +92,7 @@ options.UseSqlServer(builder.Configuration.GetConnectionString("webSQL")) ); + builder.Services.Add360Options(); builder.Services.Add360Services(); builder.Services.AddHostedService(); builder.Services.AddScoped(); diff --git a/Gordon360/Properties/launchSettings.json b/Gordon360/Properties/launchSettings.json index 83132a2de..9d52b6b20 100644 --- a/Gordon360/Properties/launchSettings.json +++ b/Gordon360/Properties/launchSettings.json @@ -8,7 +8,7 @@ "ASPNETCORE_ENVIRONMENT": "Development" }, "sqlDebugging": true, - "applicationUrl": "https://localhost:51627;http://localhost:51626" + "applicationUrl": "https://localhost:51627;http://172.26.160.1:51626" }, "Train": { "commandName": "Project", diff --git a/Gordon360/Services/DiningService.cs b/Gordon360/Services/DiningService.cs index 89f95b20b..5a0cb008f 100644 --- a/Gordon360/Services/DiningService.cs +++ b/Gordon360/Services/DiningService.cs @@ -1,15 +1,17 @@ using Gordon360.Models.CCT.Context; using Gordon360.Exceptions; using Gordon360.Models.ViewModels; -using Microsoft.Extensions.Configuration; -using Newtonsoft.Json.Linq; using System; using System.Data; -using System.IO; using System.Linq; -using System.Net; using System.Security.Cryptography; using System.Text; +using System.Text.Json.Nodes; +using System.Net.Http; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Gordon360.Options; // // We use this service to pull meal data from blackboard and parse it @@ -19,103 +21,45 @@ namespace Gordon360.Services; /// /// Service that allows for meal control /// -public class DiningService : IDiningService +public class DiningService(CCTContext context, IOptions options) : IDiningService { - private CCTContext _context; - private static string issuerID; - private static string applicationId; - private static string secret; - //private static string issuerID = System.Web.Configuration.WebConfigurationManager.AppSettings["bonAppetitIssuerID"]; - //private static string applicationId = System.Web.Configuration.WebConfigurationManager.AppSettings["bonAppetitApplicationID"]; - //private static string secret = System.Web.Configuration.WebConfigurationManager.AppSettings["bonAppetitSecret"]; - - public DiningService(CCTContext context, IConfiguration config) - { - _context = context; - issuerID = config["BonAppetit:IssuerID"]; - applicationId = config["BonAppetit:ApplicationID"]; - secret = config["BonAppetit:Secret"]; - } - - private static string getTimestamp() - { - DateTime baseDate = new DateTime(1970, 1, 1, 0, 0, 0); - TimeSpan diff = DateTime.UtcNow - baseDate; - Int64 millis = Convert.ToInt64(diff.TotalMilliseconds); - return millis.ToString(); - } - - private static string getHash(int cardHolderID, string planID, string timestamp) - { - string hashstring = (secret + issuerID + cardHolderID.ToString() + planID + - applicationId + timestamp); - - SHA1 sha1 = SHA1.Create(); - var hash = sha1.ComputeHash(Encoding.ASCII.GetBytes(hashstring)); - var sb = new StringBuilder(hash.Length * 2); - - foreach (byte b in hash) - { - // can be "x2" if you want lowercase - sb.Append(b.ToString("x2")); - } - Console.WriteLine(timestamp); - Console.WriteLine(sb.ToString()); - return sb.ToString(); - } - + private BonAppetitOptions Options = options.Value; + /// /// /// /// /// /// - public static string GetBalance(int cardHolderID, string planID) + public async Task GetBalanceAsync(int cardHolderID, string planID) { try { + long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - ServicePointManager.Expect100Continue = false; - - WebRequest request = WebRequest.Create("https://bbapi.campuscardcenter.com/cs/api/mealplanDrCr"); - - request.Method = "POST"; - - string timestamp = getTimestamp(); - - // Create POST data and convert it to a byte array. - string postData = $"issuerId={issuerID}&cardholderId={cardHolderID}&planId={planID}&applicationId={applicationId}&valueCmd=bal&value=0×tamp={timestamp}&hash={getHash(cardHolderID, planID, timestamp)}"; - byte[] byteArray = Encoding.UTF8.GetBytes(postData); - - request.ContentType = "application/x-www-form-urlencoded"; - request.ContentLength = byteArray.Length; - - Stream dataStream = request.GetRequestStream(); - dataStream.Write(byteArray, 0, byteArray.Length); - dataStream.Close(); - - // Get the response. - WebResponse response = request.GetResponse(); - Console.WriteLine(((HttpWebResponse)response).StatusDescription); - - // Get the stream containing content returned by the server. - dataStream = response.GetResponseStream(); - - // Read the content. - StreamReader reader = new StreamReader(dataStream); - string responseFromServer = reader.ReadToEnd(); - JObject json = JObject.Parse(responseFromServer); - string balance = json["balance"].ToString(); - - // Display the content. - Console.WriteLine(responseFromServer); - Console.WriteLine("Balance: " + balance); - - // Clean up the streams. - reader.Close(); - dataStream.Close(); - response.Close(); - return balance; + HttpRequestMessage request = new(HttpMethod.Post, "https://bbapi.campuscardcenter.com/cs/api/mealplanDrCr") + { + Content = new FormUrlEncodedContent(new Dictionary() + { + ["issuerId"] = Options.IssuerID.ToString(), + ["cardholderId"] = cardHolderID.ToString(), + ["planId"] = planID, + ["applicationId"] = Options.ApplicationID, + ["valueCmd"] = "bal", + ["value"] = "0", + ["timestamp"] = timestamp.ToString(), + ["hash"] = GetHash(cardHolderID, planID, timestamp.ToString()), + }) + }; + + using var client = new HttpClient(); + var response = await client.SendAsync(request); + + var responseString = await response.Content.ReadAsStringAsync(); + JsonNode? json = JsonNode.Parse(responseString); + string? balance = json?["balance"]?.GetValue(); + + return balance ?? "0"; } catch { @@ -129,18 +73,21 @@ public static string GetBalance(int cardHolderID, string planID) /// Student's Gordon ID /// Current Session Code /// - public DiningViewModel GetDiningPlanInfo(int cardHolderID, string sessionCode) + public async Task GetDiningPlanInfoAsync(int cardHolderID, string sessionCode) { - var result = _context.DiningInfo.Where(d => d.StudentId == cardHolderID && d.SessionCode == sessionCode) - .Select(d => new DiningTableViewModel + List result = []; + foreach (var plan in context.DiningInfo.Where(d => d.StudentId == cardHolderID && d.SessionCode == sessionCode)) + { + result.Add(new DiningTableViewModel { - ChoiceDescription = d.ChoiceDescription, - PlanDescriptions = d.PlanDescriptions, - PlanId = d.PlanId, - PlanType = d.PlanType, - InitialBalance = d.InitialBalance ?? 0, - CurrentBalance = GetBalance(cardHolderID, d.PlanId) + ChoiceDescription = plan.ChoiceDescription, + PlanDescriptions = plan.PlanDescriptions, + PlanId = plan.PlanId, + PlanType = plan.PlanType, + InitialBalance = plan.InitialBalance ?? 0, + CurrentBalance = await GetBalanceAsync(cardHolderID, plan.PlanId) }); + } if (result == null) { @@ -149,4 +96,16 @@ public DiningViewModel GetDiningPlanInfo(int cardHolderID, string sessionCode) return new DiningViewModel(result); } + + private string GetHash(int cardHolderID, string planID, string timestamp) + { + string hashstring = Options.Secret + + Options.IssuerID + + cardHolderID.ToString() + + planID + + Options.ApplicationID + + timestamp; + byte[] hash = SHA1.HashData(Encoding.ASCII.GetBytes(hashstring)); + return Convert.ToHexString(hash); + } } diff --git a/Gordon360/Services/ServiceInterfaces.cs b/Gordon360/Services/ServiceInterfaces.cs index ec9565488..b2bdd324b 100644 --- a/Gordon360/Services/ServiceInterfaces.cs +++ b/Gordon360/Services/ServiceInterfaces.cs @@ -59,7 +59,8 @@ public interface IEventService public interface IDiningService { - DiningViewModel GetDiningPlanInfo(int id, string sessionCode); + Task GetDiningPlanInfoAsync(int id, string sessionCode); + Task GetBalanceAsync(int cardHolderID, string planID); } public interface IAccountService diff --git a/Gordon360/appsettings.json b/Gordon360/appsettings.json index 744888b43..6b95d91ec 100644 --- a/Gordon360/appsettings.json +++ b/Gordon360/appsettings.json @@ -1,34 +1,34 @@ { "AllowedHosts": "localhost;360.gordon.edu", - "AllowedOrigin": "", + "AllowedOrigin": null, "AzureAd": { - "Instance": "", - "ClientId": "", - "TenantId": "", - "Audience": "" + "Instance": null, + "ClientId": null, + "TenantId": null, + "Audience": null }, "BonAppetit": { - "IssuerID": "", - "ApplicationID": "", - "Secret": "" + "IssuerID": null, + "ApplicationID": null, + "Secret": null }, "ConnectionStrings": { - "CCT": "", - "MyGordon": "", - "StudentTimesheets": "" + "CCT": null, + "MyGordon": null, + "StudentTimesheets": null }, - "DEFAULT_ACTIVITY_IMAGE_PATH": "", - "DEFAULT_PROFILE_IMAGE_PATH": "", - "PREFERRED_IMAGE_PATH": "", - "DEFAULT_IMAGE_PATH": "", - "DEFAULT_ID_SUBMISSION_PATH": "", - "DATABASE_IMAGE_PATH": "", + "DEFAULT_ACTIVITY_IMAGE_PATH": null, + "DEFAULT_PROFILE_IMAGE_PATH": null, + "PREFERRED_IMAGE_PATH": null, + "DEFAULT_IMAGE_PATH": null, + "DEFAULT_ID_SUBMISSION_PATH": null, + "DATABASE_IMAGE_PATH": null, "Emails": { "Sender": { - "Username": "", - "Password": "" + "Username": null, + "Password": null }, - "AlumniProfileUpdateRequestApprover": "" + "AlumniProfileUpdateRequestApprover": null }, "Logging": { "LogLevel": {