diff --git a/Api/OnlineSalesAutoCrop.CoreAPI.Models/Objects/Systems/User.cs b/Api/OnlineSalesAutoCrop.CoreAPI.Models/Objects/Systems/User.cs index 0034b57..cf7851d 100644 --- a/Api/OnlineSalesAutoCrop.CoreAPI.Models/Objects/Systems/User.cs +++ b/Api/OnlineSalesAutoCrop.CoreAPI.Models/Objects/Systems/User.cs @@ -1,4 +1,6 @@ -using System; +using DocumentFormat.OpenXml.Office2010.ExcelAc; +using System; +using System.Collections.Generic; namespace OnlineSalesAutoCrop.CoreAPI.Models.Objects.Systems { @@ -33,7 +35,11 @@ namespace OnlineSalesAutoCrop.CoreAPI.Models.Objects.Systems public string? DesignationCode { get; set; } public string? DesignationName { get; set; } public EnumLoginStatus LoginStatus { get; set; } - public DateTime? NextLoginTime { get; set; } + public DateTime? NextLoginTime { get; set; } + public int NotificationCount { get; set; } + + public List ModuleIds { get; set; } = new List(); + public List TeamSpaceIds { get; set; } = new List(); } public class LoginHistory diff --git a/Api/OnlineSalesAutoCrop.CoreAPI.Models/Responses/Systems/UserResponse.cs b/Api/OnlineSalesAutoCrop.CoreAPI.Models/Responses/Systems/UserResponse.cs index 2ddc7ee..ee38085 100644 --- a/Api/OnlineSalesAutoCrop.CoreAPI.Models/Responses/Systems/UserResponse.cs +++ b/Api/OnlineSalesAutoCrop.CoreAPI.Models/Responses/Systems/UserResponse.cs @@ -27,46 +27,28 @@ namespace OnlineSalesAutoCrop.CoreAPI.Models.Responses.Systems public DateTime? Expires { get; set; } public DateTime SystemDate { get; set; } public string LoginTime { get; set; } - public int LogId { get; set; } public DateTime? LogoutTime { get; private set; } - public string MenuLayout { get; set; } - public string ThemeName { get; set; } - public string SchemeName { get; set; } - public bool DbOnStartup { get; set; } - public bool ViewOwnTaskOnly { get; private set; } - public int? EmployeeId { get; private set; } public int NotificationCount { get; private set; } - public int IdleTime { get; set; } - public int PingTime { get; set; } - public int TimeoutTime { get; set; } - public bool BatchEnabled { get; private set; } - public int BmProcessId { get; private set; } - public int PrProcessId { get; private set; } - public string IdsValue { get; set; } - //public List ModuleIds { get; set; } = []; + public List ModuleIds { get; set; } = []; - public void Map(User source) + public string RefreshToken { get;set; } + public UserPlatFormType? UserPlatFormType { get; set; } + public UserRoleTypeEnum? UserRoleType { get; set; } + public string? EmployeeNumber { get; set; } + + public void Map(User source) { Id = source.UserId; LoginId = source.LoginId; UserName = source.UserName; - ThemeName = source.ThemeName; - //ModuleIds = source.ModuleIds; + ModuleIds = source.ModuleIds; LogoutTime = source.LogoutTime; - SchemeName = source.SchemeName; - MenuLayout = source.MenuLayout; LoginStatus = source.LoginStatus; - //DbOnStartup = source.DbOnStartup; - //TimeoutTime = source.TimeoutTime; - //BmProcessId = source.BmProcessId; - //PrProcessId = source.PrProcessId; - //BatchEnabled = source.BatchEnabled; - //ViewOwnTaskOnly = source.ViewOwnTaskOnly; - //NotificationCount = source.NotificationCount; - //AuthRequiredAtLogin = source.AuthRequiredAtLogin; - //IdsValue = Newtonsoft.Json.JsonConvert.SerializeObject(source.ModuleIds); - } + UserPlatFormType = source.UserPlatFormType; + UserRoleType = source.UserRoleType; + EmployeeNumber = source.EmployeeNumber; + } } public class MenuResponse diff --git a/Api/OnlineSalesAutoCrop.CoreAPI.Services/Services/Auth/RefreshTokenService.cs b/Api/OnlineSalesAutoCrop.CoreAPI.Services/Services/Auth/RefreshTokenService.cs index a262637..41905e4 100644 --- a/Api/OnlineSalesAutoCrop.CoreAPI.Services/Services/Auth/RefreshTokenService.cs +++ b/Api/OnlineSalesAutoCrop.CoreAPI.Services/Services/Auth/RefreshTokenService.cs @@ -267,8 +267,8 @@ public class RefreshTokenService : IRefreshTokenService UserId = userId, TokenHash = tokenHash, IpAddress = ipAddress, - CreatedAt = DateTime.UtcNow, - ExpiresAt = DateTime.UtcNow.AddDays(_settings.RefreshTokenDuration) + CreatedAt = DateTime.Now, + ExpiresAt = DateTime.Now.AddMinutes(_settings.RefreshTokenDuration) }; AddAsync(tc,refreshToken); @@ -276,7 +276,7 @@ public class RefreshTokenService : IRefreshTokenService return new GenerateRefreshTokenResponse { RefreshToken = tokenHash, - ExpireTime = DateTime.UtcNow.AddDays(_settings.RefreshTokenDuration) + ExpireTime = DateTime.Now.AddMinutes(_settings.RefreshTokenDuration) }; } diff --git a/Api/OnlineSalesAutoCrop.CoreAPI.Services/Services/Systems/UserService.cs b/Api/OnlineSalesAutoCrop.CoreAPI.Services/Services/Systems/UserService.cs index c36eaed..15eaf22 100644 --- a/Api/OnlineSalesAutoCrop.CoreAPI.Services/Services/Systems/UserService.cs +++ b/Api/OnlineSalesAutoCrop.CoreAPI.Services/Services/Systems/UserService.cs @@ -95,220 +95,192 @@ namespace OnlineSalesAutoCrop.CoreAPI.Services.Services.Systems #endregion - //if (user.LoginStatus != EnumLoginStatus.VersionMismatch) - //{ - // #region Read User data using authentication data + if (user.LoginStatus != EnumLoginStatus.VersionMismatch) + { + #region Read User data using authentication data - // string commandText; - // if (!checkPwd) - // { - // commandText = SQLParser.MakeSQL("SELECT UserId, UserName, Status, MobileNo, EmailAddress, AuthReqAtlogin, AuthMethod," - // + " AuthKey, AppId, AccessStatus, NeverExpire, LastPasswords, LastPassChgDate, ExpireDate, ThemeName, SchemeName, MenuLayout," - // + " IsLocked, NextLoginTime, DBOnStartup, DAMultiLogin, ViewOwnTaskOnly, EmployeeId, LoginID, EmployeeCode FROM Users WHERE LoginID=%s", request.LoginId); - // } - // else - // { - // commandText = SQLParser.MakeSQL("SELECT UserId, UserName, Status, MobileNo, EmailAddress, AuthReqAtlogin, AuthMethod," - // + " AuthKey, AppId, AccessStatus, NeverExpire, LastPasswords, LastPassChgDate, ExpireDate, ThemeName, SchemeName, MenuLayout," - // + " IsLocked, NextLoginTime, DBOnStartup, DAMultiLogin, ViewOwnTaskOnly, EmployeeId, LoginID, EmployeeCode FROM Users" - // + " WHERE (LoginID=%s OR MobileNo=%s OR EmailAddress=%s) AND Password=%s", request.LoginId, request.LoginId, request.LoginId, password); - // } + string commandText = SQLParser.MakeSQL("SELECT UserId,LoginId, UserName, Status,AccessStatus,NeverExpires, MobileNo, EmailAddress, " + + " IsLocked, UserPlatformId, UserRoleType, EmployeeNumber FROM Users" + + " WHERE (LoginID=%s OR MobileNo=%s OR EmailAddress=%s) AND Password=%s", request.LoginId, request.LoginId, request.LoginId, password); - // using (IDataReader dr = tc.ExecuteReader(commandText: commandText)) - // { - // if (dr.Read()) - // { - // user = new User - // { - // Id = dr.GetInt32(0), - // UserName = dr.GetString(1), - // Status = (EnumStatus)dr.GetInt16(2), - // MobileNo = dr.GetString(3), - // EmailAddress = dr.GetString(4), - // AuthRequiredAtLogin = !dr.IsDBNull(5) && dr.GetInt16(5) > 0, - // AuthMethod = (EnumAuthenticationMethod)dr.GetInt16(6), - // AuthKey = dr.IsDBNull(7) ? string.Empty : dr.GetString(7), - // AppId = dr.IsDBNull(8) ? string.Empty : dr.GetString(8), - // AccessStatus = (EnumAccessStatus)dr.GetInt16(9), - // NeverExpires = !dr.IsDBNull(10) && dr.GetInt16(10) > 0, - // LastPasswords = dr.IsDBNull(11) ? string.Empty : dr.GetString(11), - // LastPassChgDate = dr.IsDBNull(12) ? null : dr.GetDateTime(12), - // ExpireDate = dr.IsDBNull(13) ? null : dr.GetDateTime(13), - // ThemeName = dr.IsDBNull(14) ? "yellow" : dr.GetString(14), - // SchemeName = dr.IsDBNull(15) ? "dark" : dr.GetString(15), - // MenuLayout = dr.IsDBNull(16) ? "static" : dr.GetString(16), - // IsLocked = !dr.IsDBNull(17) && dr.GetInt16(17) > 0, - // NextLoginTime = dr.IsDBNull(18) ? null : dr.GetDateTime(18), - // DbOnStartup = dr.GetInt16(19) != 0, - // DisallowMultiLogin = dr.GetInt16(20) != 0, - // ViewOwnTaskOnly = dr.GetInt16(21) != 0, - // EmployeeId = dr.IsDBNull(22) ? null : dr.GetInt32(22), - // LoginId = dr.GetString(23), - // EmployeeCode = dr.IsDBNull(24) ? string.Empty : dr.GetString(24), + using (IDataReader dr = tc.ExecuteReader(commandText: commandText)) + { + if (dr.Read()) + { + user = new User + { + UserId = dr.GetInt32(0), + LoginId = dr.GetString(1), + UserName = dr.GetString(2), + Status = (EnumStatus)dr.GetInt32(3), + AccessStatus = (EnumAccessStatus)dr.GetInt32(4), + NeverExpires = dr.GetBoolean(5), + MobileNo = dr.IsDBNull(6) ? null : dr.GetString(6), + EmailAddress = dr.IsDBNull(7) ? null : dr.GetString(7), + IsLocked = dr.GetBoolean(8), + UserPlatFormType = dr.IsDBNull(9) ? null : (UserPlatFormType)dr.GetInt32(9), + UserRoleType = dr.IsDBNull(10) ? null : (UserRoleTypeEnum)dr.GetInt32(10), + EmployeeNumber = dr.IsDBNull(11) ? null : dr.GetString(11), + LoginStatus = EnumLoginStatus.Success + }; + } + dr.Close(); + } - // TeamSpaceIds = [], - // IdleTime = idleTime, - // PingTime = pingTime, - // SystemDate = sysDate, - // TimeoutTime = timeoutTime, - // PrProcessId = prProcessId, - // BmProcessId = bmProcessId, - // BatchEnabled = batchEnabled, - // LoginStatus = EnumLoginStatus.Success - // }; - // } - // dr.Close(); + #endregion - // user.MinReportDate = marMonths <= 0 ? new DateTime(year: 2015, month: 1, day: 1, hour: 0, minute: 0, second: 0, kind: DateTimeKind.Local) : sysDate.AddMonths(-1 * marMonths); - // } + #region If the user was locked, try set set unlock if Time expired - // #endregion + if (!request.LoginId.ToLower().Equals(User.SuperUser_LoginId) && user.IsLocked) + { + int isSuccessful = 0; + SqlParameter[] p = + [ + SqlHelperExtension.CreateInParam(pName: "@LoginId", pType: SqlDbType.VarChar, pValue: request.LoginId, size: 30), + SqlHelperExtension.CreateInParam(pName: "@LockTime", pType: SqlDbType.Int, pValue: lockTime), + SqlHelperExtension.CreateOutParam(pName: "@IsSuccessful", pType: SqlDbType.Int, pValue: isSuccessful), + ]; + _ = tc.ExecuteNonQuerySp(spName: "dbo.DoUnlockUser", parameterValues: p); + if (p[2] != null && p[2].Value != null && p[2].Value != DBNull.Value) + isSuccessful = Convert.ToInt32(p[2].Value); - // #region If the user was locked, try set set unlock if Time expired + if (isSuccessful == 1) + user.IsLocked = false; + } - // if (!request.LoginId.ToLower().Equals(User.SuperUser_LoginId) && user.IsLocked) - // { - // int isSuccessful = 0; - // SqlParameter[] p = - // [ - // SqlHelperExtension.CreateInParam(pName: "@LoginId", pType: SqlDbType.VarChar, pValue: request.LoginId, size: 30), - // SqlHelperExtension.CreateInParam(pName: "@LockTime", pType: SqlDbType.Int, pValue: lockTime), - // SqlHelperExtension.CreateOutParam(pName: "@IsSuccessful", pType: SqlDbType.Int, pValue: isSuccessful), - // ]; - // _ = tc.ExecuteNonQuerySp(spName: "dbo.DoUnlockUser", parameterValues: p); - // if (p[2] != null && p[2].Value != null && p[2].Value != DBNull.Value) - // isSuccessful = Convert.ToInt32(p[2].Value); + #endregion - // if (isSuccessful == 1) - // user.IsLocked = false; - // } + #region Keep log for unauthrise access and Set user lock if exceeds max try - // #endregion + if (!request.LoginId.ToLower().Equals(User.SuperUser_LoginId) && maxTryCount > 0 && user.LoginStatus == EnumLoginStatus.Unsuccessful) + { + int remainsTry = 0; + DateTime? nextLoginTime = null; + string tryLoginInfo = $"{request.LoginId}~{password}~13"; + SqlParameter[] p = + [ + SqlHelperExtension.CreateInParam(pName: "@LoginId", pType: SqlDbType.VarChar, pValue: request.LoginId, size: 30), + SqlHelperExtension.CreateInParam(pName: "@TryLoginInfo", pType: SqlDbType.VarChar, pValue: tryLoginInfo, size: 100), + SqlHelperExtension.CreateInParam(pName: "@IpAddress", pType: SqlDbType.VarChar, pValue: ipAddress, size: 20), + SqlHelperExtension.CreateInParam(pName: "@MaxTryCount", pType: SqlDbType.SmallInt, pValue: maxTryCount), + SqlHelperExtension.CreateInParam(pName: "@LockTime", pType: SqlDbType.Int, pValue: lockTime), + SqlHelperExtension.CreateOutParam(pName: "@RemainingTry", pType: SqlDbType.SmallInt, pValue: remainsTry), + SqlHelperExtension.CreateOutParam(pName: "@NextLoginTime", pType: SqlDbType.DateTime, pValue: nextLoginTime), + ]; + _ = tc.ExecuteNonQuerySp(spName: "dbo.LogUnauthorizeAccess", parameterValues: p); + if (p[5] != null && p[5].Value != null && p[5].Value != DBNull.Value) + remainsTry = Convert.ToInt32(p[5].Value); - // #region Keep log for unauthrise access and Set user lock if exceeds max try + if (p[6] != null && p[6].Value != null && p[6].Value != DBNull.Value) + nextLoginTime = Convert.ToDateTime(p[6].Value); - // if (!request.LoginId.ToLower().Equals(User.SuperUser_LoginId) && maxTryCount > 0 && user.LoginStatus == EnumLoginStatus.Unsuccessful) - // { - // int remainsTry = 0; - // DateTime? nextLoginTime = null; - // string tryLoginInfo = $"{request.LoginId}~{password}~13"; - // SqlParameter[] p = - // [ - // SqlHelperExtension.CreateInParam(pName: "@LoginId", pType: SqlDbType.VarChar, pValue: request.LoginId, size: 30), - // SqlHelperExtension.CreateInParam(pName: "@TryLoginInfo", pType: SqlDbType.VarChar, pValue: tryLoginInfo, size: 100), - // SqlHelperExtension.CreateInParam(pName: "@IpAddress", pType: SqlDbType.VarChar, pValue: ipAddress, size: 20), - // SqlHelperExtension.CreateInParam(pName: "@MaxTryCount", pType: SqlDbType.SmallInt, pValue: maxTryCount), - // SqlHelperExtension.CreateInParam(pName: "@LockTime", pType: SqlDbType.Int, pValue: lockTime), - // SqlHelperExtension.CreateOutParam(pName: "@RemainingTry", pType: SqlDbType.SmallInt, pValue: remainsTry), - // SqlHelperExtension.CreateOutParam(pName: "@NextLoginTime", pType: SqlDbType.DateTime, pValue: nextLoginTime), - // ]; - // _ = tc.ExecuteNonQuerySp(spName: "dbo.LogUnauthorizeAccess", parameterValues: p); - // if (p[5] != null && p[5].Value != null && p[5].Value != DBNull.Value) - // remainsTry = Convert.ToInt32(p[5].Value); + if (remainsTry <= 0) + { + user.IsLocked = true; + if (lockTime <= 0) + { + user.NextLoginTime = nextLoginTime; + user.UnsuccessfulMsg = "Please contact with Head office to Unlock"; + } + else + { + user.NextLoginTime = nextLoginTime; + user.UnsuccessfulMsg = $"You can Login after {user.NextLoginTime:dd-MMM-yyyy H:mm:ss}"; + } + } + else + { + user.UnsuccessfulMsg = $"{remainsTry} More attempt{(remainsTry > 1 ? "s" : "")} remaining"; + } + } - // if (p[6] != null && p[6].Value != null && p[6].Value != DBNull.Value) - // nextLoginTime = Convert.ToDateTime(p[6].Value); + #endregion - // if (remainsTry <= 0) - // { - // user.IsLocked = true; - // if (lockTime <= 0) - // { - // user.NextLoginTime = nextLoginTime; - // user.UnsuccessfulMsg = "Please contact with Head office to Unlock"; - // } - // else - // { - // user.NextLoginTime = nextLoginTime; - // user.UnsuccessfulMsg = $"You can Login after {user.NextLoginTime:dd-MMM-yyyy H:mm:ss}"; - // } - // } - // else - // { - // user.UnsuccessfulMsg = $"{remainsTry} More attempt{(remainsTry > 1 ? "s" : "")} remaining"; - // } - // } - // #endregion + #region If login successful and user is active read module id for this user - // #region Generate and Save Otp if Otp is enabled and send thru SMS/Email + if (user.LoginStatus == EnumLoginStatus.Success && user.Status == EnumStatus.Authorized && !user.IsLocked) + { + SqlParameter[] p = + [ + SqlHelperExtension.CreateInParam(pName: "@UserId", pType: SqlDbType.Int, pValue: user.UserId), + SqlHelperExtension.CreateInParam(pName: "@LoginId", pType: SqlDbType.VarChar, pValue: user.LoginId, size: 30), + SqlHelperExtension.CreateInParam(pName: "@IpAddress", pType: SqlDbType.VarChar, pValue: ipAddress, size: 20), + SqlHelperExtension.CreateInParam(pName: "@LoginTime", pType: SqlDbType.DateTime, pValue: DateTime.Now), + SqlHelperExtension.CreateInParam(pName: "@LogoutTime", pType: SqlDbType.DateTime, pValue: null) + ]; - // if (user.LoginStatus == EnumLoginStatus.Success && user.Status == EnumStatus.Authorized && (user.AuthMethod == EnumAuthenticationMethod.Email || user.AuthMethod == EnumAuthenticationMethod.MobileSMS)) - // { - // string secretKey = $"{user.Id}~{user.LoginId}"; - // secretKey = secretKey.EncodeAsBase32String(addPadding: false); - // user.AuthValue = TOtpService.GetCurrentPIN(secretKey: secretKey); - // SetAuthValue(tc: tc, authValue: user.AuthValue, validMinutes: 5, userId: user.Id); - // } + _ = tc.ExecuteNonQuerySp(spName: "dbo.SaveAccessLog", parameterValues: p); + } - // #endregion + #endregion - // #region If login successful and user is active read module id for this user - // if (user.LoginStatus == EnumLoginStatus.Success && user.Status == EnumStatus.Authorized && !user.IsLocked) - // { - // int logId = 0; - // DateTime? logoutTime = null; - // SqlParameter[] p = - // [ - // SqlHelperExtension.CreateInParam(pName: "@UserId", pType: SqlDbType.Int, pValue: user.Id), - // SqlHelperExtension.CreateInParam(pName: "@IpAddress", pType: SqlDbType.VarChar, pValue: ipAddress, size: 20), - // SqlHelperExtension.CreateInParam(pName: "@AppId", pType: SqlDbType.VarChar, pValue: request.AppId, size: 250), - // SqlHelperExtension.CreateInParam(pName: "@LoginId", pType: SqlDbType.VarChar, pValue: user.LoginId, size: 30), - // SqlHelperExtension.CreateInParam(pName: "@LockTime", pType: SqlDbType.Int, pValue: lockTime), - // SqlHelperExtension.CreateInParam(pName: "@AttendanceLogin", pType: SqlDbType.Int, pValue: request.AttendanceLogin? 1:0), - // SqlHelperExtension.CreateInParam(pName: "@LocalIp", pType: SqlDbType.VarChar, pValue: request.IpAddress, size: 20), - // SqlHelperExtension.CreateInParam(pName: "@MacAddress", pType: SqlDbType.VarChar, pValue: request.MacAddress, size: 30), - // SqlHelperExtension.CreateInParam(pName: "@HostName", pType: SqlDbType.VarChar, pValue: request.HostName, size: 100), - // SqlHelperExtension.CreateInParam(pName: "@LoginRemarks", pType: SqlDbType.VarChar, pValue: request.LoginRemarks, size: 50) - // ]; - // using (IDataReader dr = tc.ExecuteReaderSp(spName: "dbo.GetPermissionKeys", parameterValues: p)) - // { - // user.ModuleIds = []; - // while (dr.Read()) - // { - // string moduleId = dr.GetString(0); - // user.ModuleIds.Add(moduleId); + #region If login successful and user is active read module id for this user - // logId = dr.GetInt32(1); + if (user.LoginStatus == EnumLoginStatus.Success && user.Status == EnumStatus.Authorized && !user.IsLocked) + { + int logId = 0; + DateTime? logoutTime = null; + SqlParameter[] p = + [ + SqlHelperExtension.CreateInParam(pName: "@UserId", pType: SqlDbType.Int, pValue: user.UserId), + SqlHelperExtension.CreateInParam(pName: "@IpAddress", pType: SqlDbType.VarChar, pValue: ipAddress, size: 20), + SqlHelperExtension.CreateInParam(pName: "@AppId", pType: SqlDbType.VarChar, pValue: request.AppId, size: 250), + SqlHelperExtension.CreateInParam(pName: "@LoginId", pType: SqlDbType.VarChar, pValue: user.LoginId, size: 30), + SqlHelperExtension.CreateInParam(pName: "@LockTime", pType: SqlDbType.Int, pValue: lockTime), + SqlHelperExtension.CreateInParam(pName: "@AttendanceLogin", pType: SqlDbType.Int, pValue: request.AttendanceLogin? 1:0), + SqlHelperExtension.CreateInParam(pName: "@LocalIp", pType: SqlDbType.VarChar, pValue: request.IpAddress, size: 20), + SqlHelperExtension.CreateInParam(pName: "@MacAddress", pType: SqlDbType.VarChar, pValue: request.MacAddress, size: 30), + SqlHelperExtension.CreateInParam(pName: "@HostName", pType: SqlDbType.VarChar, pValue: request.HostName, size: 100), + SqlHelperExtension.CreateInParam(pName: "@LoginRemarks", pType: SqlDbType.VarChar, pValue: request.LoginRemarks, size: 50) + ]; + using (IDataReader dr = tc.ExecuteReaderSp(spName: "dbo.GetPermissionKeys", parameterValues: p)) + { + user.ModuleIds = []; + while (dr.Read()) + { + string moduleId = dr.GetString(0); + user.ModuleIds.Add(moduleId); - // if (dr.GetInt16(2) != 0) //Alow add - // { - // user.ModuleIds.Add($"{moduleId}_1"); - // } - // if (dr.GetInt16(3) != 0) //Alow edit - // { - // user.ModuleIds.Add($"{moduleId}_2"); - // } + logId = dr.GetInt32(1); - // if (dr.GetInt16(4) != 0) //Allow Delete - // { - // user.ModuleIds.Add($"{moduleId}_3"); - // } + if (dr.GetInt16(2) != 0) //Alow add + { + user.ModuleIds.Add($"{moduleId}_1"); + } + if (dr.GetInt16(3) != 0) //Alow edit + { + user.ModuleIds.Add($"{moduleId}_2"); + } - // logoutTime = dr.IsDBNull(5) ? null : dr.GetDateTime(5); - // } - // dr.Close(); - // } - // user.LogId = logId; - // user.LogoutTime = logoutTime; + if (dr.GetInt16(4) != 0) //Allow Delete + { + user.ModuleIds.Add($"{moduleId}_3"); + } - // //Read User TeamSpace Ids - // using (IDataReader dr = tc.ExecuteReader("SELECT TeamSpaceId FROM TeamSpaceUsers WHERE UserId=%n", user.Id)) - // { - // while (dr.Read()) - // { - // user.TeamSpaceIds.Add(dr.GetInt32(0)); - // } - // dr.Close(); - // } + logoutTime = dr.IsDBNull(5) ? null : dr.GetDateTime(5); + } + dr.Close(); + } + user.LogoutTime = logoutTime; - // //Pending Notification count - // user.NotificationCount = GetPendingNotifCount(tc: tc, userId: user.Id); - // } + //Read User TeamSpace Ids + using (IDataReader dr = tc.ExecuteReader("SELECT TeamSpaceId FROM TeamSpaceUsers WHERE UserId=%n", user.UserId)) + { + while (dr.Read()) + { + user.TeamSpaceIds.Add(dr.GetInt32(0)); + } + dr.Close(); + } - // #endregion - //} + //Pending Notification count + //user.NotificationCount = GetPendingNotifCount(tc: tc, userId: user.Id); + } + + #endregion + } tc.End(); } diff --git a/Api/OnlineSalesAutoCrop.CoreAPI/Controllers/Mobile/MobileAuthController.cs b/Api/OnlineSalesAutoCrop.CoreAPI/Controllers/Mobile/MobileAuthController.cs index 8e9197c..776928f 100644 --- a/Api/OnlineSalesAutoCrop.CoreAPI/Controllers/Mobile/MobileAuthController.cs +++ b/Api/OnlineSalesAutoCrop.CoreAPI/Controllers/Mobile/MobileAuthController.cs @@ -166,7 +166,7 @@ namespace OnlineSalesAutoCrop.CoreAPI.Controllers loginReponse.LoginId = loginReponse.LoginId; loginReponse.AccessToken = userToken; loginReponse.RefreshToken = refreshToken.RefreshToken; - loginReponse.AccessTokenExpiryTime = DateTime.UtcNow.AddHours(12); + loginReponse.AccessTokenExpiryTime = DateTime.Now.AddMinutes(_appSettings.AccessTokenDuration); loginReponse.RefreshTokenExpiryTime = refreshToken.ExpireTime; return Ok(MobileResponseBase.Success(loginReponse)); @@ -217,7 +217,7 @@ namespace OnlineSalesAutoCrop.CoreAPI.Controllers Helper.CreateClaim("UserId", $"{userRefreshToken.UserId}"), Helper.CreateClaim("HashKey", Guid.NewGuid().ToString()) ]), - Expires = DateTime.UtcNow.AddHours(12), + Expires = DateTime.UtcNow.AddMinutes(_appSettings.AccessTokenDuration), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha512Signature) }; diff --git a/Api/OnlineSalesAutoCrop.CoreAPI/Controllers/Web/UserController.cs b/Api/OnlineSalesAutoCrop.CoreAPI/Controllers/Web/UserController.cs index 87d461c..9be295e 100644 --- a/Api/OnlineSalesAutoCrop.CoreAPI/Controllers/Web/UserController.cs +++ b/Api/OnlineSalesAutoCrop.CoreAPI/Controllers/Web/UserController.cs @@ -1,16 +1,5 @@ using Asp.Versioning; -using OnlineSalesAutoCrop.CoreAPI.Configurations; -using OnlineSalesAutoCrop.CoreAPI.Models; -using OnlineSalesAutoCrop.CoreAPI.Models.Global; -using OnlineSalesAutoCrop.CoreAPI.Models.Objects; -using OnlineSalesAutoCrop.CoreAPI.Models.Objects.Systems; -using OnlineSalesAutoCrop.CoreAPI.Models.Requests; -using OnlineSalesAutoCrop.CoreAPI.Models.Requests.Setups; -using OnlineSalesAutoCrop.CoreAPI.Models.Requests.Systems; -using OnlineSalesAutoCrop.CoreAPI.Models.Responses; -using OnlineSalesAutoCrop.CoreAPI.Models.Responses.Systems; -using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Systems; -using OnlineSalesAutoCrop.CoreAPI.SignalRHub; +using Google.Apis.Auth.OAuth2.Requests; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -18,6 +7,20 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; +using OnlineSalesAutoCrop.CoreAPI.Configurations; +using OnlineSalesAutoCrop.CoreAPI.Models; +using OnlineSalesAutoCrop.CoreAPI.Models.Global; +using OnlineSalesAutoCrop.CoreAPI.Models.Objects; +using OnlineSalesAutoCrop.CoreAPI.Models.Objects.Systems; +using OnlineSalesAutoCrop.CoreAPI.Models.Requests; +using OnlineSalesAutoCrop.CoreAPI.Models.Requests.Integrations; +using OnlineSalesAutoCrop.CoreAPI.Models.Requests.Setups; +using OnlineSalesAutoCrop.CoreAPI.Models.Requests.Systems; +using OnlineSalesAutoCrop.CoreAPI.Models.Responses; +using OnlineSalesAutoCrop.CoreAPI.Models.Responses.Systems; +using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Auth; +using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Systems; +using OnlineSalesAutoCrop.CoreAPI.SignalRHub; using System; using System.Collections.Generic; using System.DirectoryServices; @@ -45,11 +48,11 @@ namespace OnlineSalesAutoCrop.CoreAPI.Controllers.Web [Authorize] [ApiController] [ApiVersion("1.0")] - [Route("api/users")] [ValidateAntiForgeryToken] [Route("api/v{version:apiVersion}/users")] - public class UserController(IUserService service, IOptions appSettings, IEaseCache cache, ILogger logger, IHubContext hub) : ControllerBase + public class UserController(IUserService service, IOptions appSettings, IEaseCache cache, ILogger logger, + IHubContext hub, IRefreshTokenService refreshTokenService) : ControllerBase { private readonly ILogger _logger = logger; private readonly IEaseCache _cache = cache; @@ -57,6 +60,7 @@ namespace OnlineSalesAutoCrop.CoreAPI.Controllers.Web private readonly AppSettings _appSettings = appSettings?.Value; private readonly DateTimeOffset _options = Helper.CreateEaseCacheOptions(); private readonly IHubContext _hub = hub; + private readonly IRefreshTokenService _refreshTokenService= refreshTokenService; /// /// Login using your credential data retrieve from SqlServer @@ -215,17 +219,15 @@ namespace OnlineSalesAutoCrop.CoreAPI.Controllers.Web { Subject = new ClaimsIdentity( [ - //Helper.CreateClaim("UserId", $"{user.Id}"), - //Helper.CreateClaim("LoginId", user.LoginId), - //Helper.CreateClaim("UserPwd", userPwd), - //Helper.CreateClaim("AuthKey", $"{user.AuthKey}"), - //Helper.CreateClaim("TeamSpaceIds", $"{string.Join(',',user.TeamSpaceIds)}"), - //Helper.CreateClaim("BatchEnabled", user.BatchEnabled ? "1" : "0"), - //Helper.CreateClaim("BmProcessId", $"{user.BmProcessId}"), - //Helper.CreateClaim("PrProcessId", $"{user.PrProcessId}"), - //Helper.CreateClaim("EmployeeId", $"{user.EmployeeCode}") + Helper.CreateClaim("UserId", $"{user.UserId}"), + Helper.CreateClaim("LoginId", user.LoginId), + Helper.CreateClaim("AuthKey", $"{user.AuthKey}"), + Helper.CreateClaim("TeamSpaceIds", $"{string.Join(',',user.TeamSpaceIds)}"), + Helper.CreateClaim("UserPlatformId", ((int)user.UserPlatFormType).ToString()), + Helper.CreateClaim("UserRoleType", $"{user.UserRoleType}"), + Helper.CreateClaim("EmployeeCode", $"{user.EmployeeNumber}") ]), - Expires = DateTime.UtcNow.AddHours(12), + Expires = DateTime.UtcNow.AddMinutes(_appSettings.AccessTokenDuration), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha512Signature) }; @@ -246,37 +248,17 @@ namespace OnlineSalesAutoCrop.CoreAPI.Controllers.Web response.AuthenticationToken = userToken; response.LoginTime = $"{DateTime.Now:dd-MM-yy H:mm:ss}"; - //await HttpContext.Session.SetModulesToSession(key: userToken, value: user.ModuleIds); - //if (user.LoginStatus == EnumLoginStatus.Success) - //{ - // if (user.AuthMethod == EnumAuthenticationMethod.Email && !string.IsNullOrEmpty(user.EmailAddress) && !string.IsNullOrWhiteSpace(user.EmailAddress) && !string.IsNullOrWhiteSpace(user.AuthValue)) - // { - // List to = [.. user.EmailAddress.Split(separator: ';', options: StringSplitOptions.RemoveEmptyEntries)]; - // await MailHelper.SendMailMessageAsync(settings: _appSettings, to: to, - // cc: null, bcc: null, attachments: null, embeddedImages: null, isHtmlBody: false, priority: System.Net.Mail.MailPriority.Normal, - // subject: "Your OTP", messageBody: string.Format("Your OTP: {0} and is valid for 5 minutes only", user.AuthValue)); - // } - // else if (user.AuthMethod == EnumAuthenticationMethod.MobileSMS && !string.IsNullOrEmpty(user.MobileNo) && !string.IsNullOrWhiteSpace(user.MobileNo) && !string.IsNullOrWhiteSpace(user.AuthValue)) - // { - // MailHelper.SendSMSOrWhatsAppMessage(settings: _appSettings, whatsAppMsg: false, msg: string.Format("Your OTP: {0} and is valid for 5 minutes only", user.AuthValue), mobileNumber: user.MobileNo); - // } - //} + GenerateRefreshTokenRequest refreshTokenRequest = new GenerateRefreshTokenRequest() + { + UserId = user.UserId, + IpAddress = ipAddress, + RawRefreshToken = string.Empty + }; - //if (user.DisallowMultiLogin) - //{ - // await _hub.Clients.All.NotifySubscriber(userId: user.Id, msgType: 1, itemId: 0, ipAddress: ipAddress); - // if (request.AttendanceLogin) - // await _hub.Clients.All.NotifySubscriber(userId: user.Id, msgType: 6, itemId: 0, ipAddress: ipAddress); - //} - //else - //{ - // await _hub.Clients.All.NotifySubscriber(userId: user.Id, msgType: 2, itemId: 0, ipAddress: ipAddress); - // if (request.AttendanceLogin) - // await _hub.Clients.All.NotifySubscriber(userId: user.Id, msgType: 6, itemId: 0, ipAddress: ipAddress); - //} - //response.IdsValue = Ease.NetCore.Utility.Global.CipherFunctions.EncryptByAES(data: Newtonsoft.Json.JsonConvert.SerializeObject(user.ModuleIds), privateKey: cipherSecretKey, publicKey: cipherSecretKey, output: 2); + var refreshToken = await _refreshTokenService.GenerateRefreshTokenByUserAsync(refreshTokenRequest); - return Ok(response); + response.RefreshToken = refreshToken.RefreshToken; + return Ok(response); } catch (Exception ex) { diff --git a/Api/OnlineSalesAutoCrop.CoreAPI/Startup.cs b/Api/OnlineSalesAutoCrop.CoreAPI/Startup.cs index f5250a4..4818f30 100644 --- a/Api/OnlineSalesAutoCrop.CoreAPI/Startup.cs +++ b/Api/OnlineSalesAutoCrop.CoreAPI/Startup.cs @@ -33,7 +33,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; -using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Text; diff --git a/Api/OnlineSalesAutoCrop.CoreAPI/appsettings.json b/Api/OnlineSalesAutoCrop.CoreAPI/appsettings.json index e1e89c5..5a400f3 100644 --- a/Api/OnlineSalesAutoCrop.CoreAPI/appsettings.json +++ b/Api/OnlineSalesAutoCrop.CoreAPI/appsettings.json @@ -84,8 +84,8 @@ "WaAuthToken": "024a6897584671d9f9fa588d7c94aa96", "WaMsgSvcSid": "MG8401d33a9a3b2aea95619bda3e5757b5", "WaSenderId": "+8801326755660", - "RefreshTokenDuration": "15", - "AccessTokenDuration": "60", + "RefreshTokenDuration": "2", + "AccessTokenDuration": "1", "AutoCropSAPApi": { "BaseUrl": "https://accl-test-pad-l06vsvh7.it-cpi004-rt.cfapps.ap11.hana.ondemand.com/", "UserName": "sb-1635ae0e-9941-4998-962f-e23f592d29b5!b45657|it-rt-accl-test-pad-l06vsvh7!b68", diff --git a/App/ClientApp/src/app/app.breadcrumb.component.ts b/App/ClientApp/src/app/app.breadcrumb.component.ts index d5e2cc9..77882dd 100644 --- a/App/ClientApp/src/app/app.breadcrumb.component.ts +++ b/App/ClientApp/src/app/app.breadcrumb.component.ts @@ -33,10 +33,7 @@ export class AppBreadcrumbComponent implements OnDestroy { if (this.authService?.currentUserValue) { - if (this.authService.currentUserValue.dbOnStartup) - this.router.navigate(['/dashboard']); - else - this.router.navigate(['/home']); + this.router.navigate(['/home']); } } diff --git a/App/ClientApp/src/app/app.menu.component.ts b/App/ClientApp/src/app/app.menu.component.ts index 3252b46..ef31372 100644 --- a/App/ClientApp/src/app/app.menu.component.ts +++ b/App/ClientApp/src/app/app.menu.component.ts @@ -6,6 +6,7 @@ import { Component, Input, OnInit, AfterViewInit, forwardRef, inject } from '@an import { AppComponent } from './app.component'; import { UserService } from '../providers/user/user.service.'; import { MenuItemComponent } from './components/menu/menuitem.component'; +import { MENU_DEFINITION, PROFILE_MENU, MenuNode, buildPermittedMenu } from './components/models/menu.definition'; @Component({ standalone: true, @@ -28,6 +29,7 @@ export class AppMenuComponent implements OnInit, AfterViewInit public isAuthenicated: boolean = false; private menuItems: any; + private routePaths: Set; ngOnInit() { @@ -43,15 +45,14 @@ export class AppMenuComponent implements OnInit, AfterViewInit } this.reloadMenu(); - this.modelUngrouped = [{ label: 'Main Menu', icon: 'fa fa-home', items: this.modelGrouped }]; } private reloadMenu() { - if (this.isAuthenicated && this.service?.authService?.currentUserValue?.moduleIds) + const currentUser = this.service?.authService?.currentUserValue; + if (this.isAuthenicated && currentUser?.moduleIds) { - const dbOnStartup = this.service.authService.currentUserValue.dbOnStartup; - this.app.detailInfo = `Version: ${this.app.swVersion} | User: ${this.service.authService.currentUserValue.userName} | Login Time: ${this.service.authService.currentUserValue.loginTime}`; + this.app.detailInfo = `Version: ${this.app.swVersion} | User: ${currentUser.userName} | Login Time: ${currentUser.loginTime}`; this.modelGrouped = [ { @@ -59,7 +60,7 @@ export class AppMenuComponent implements OnInit, AfterViewInit items: [ { label: 'Logout', icon: 'fa fa-power-off', routerLink: ['/logout'] }, - { label: dbOnStartup ? 'Dashboard' : 'Home', icon: dbOnStartup ? 'fa fa-dashboard' : 'fa fa-home', routerLink: [dbOnStartup ? '/dashboard' : '/home'] } + { label: 'Home', icon: 'fa fa-home', routerLink: ['/home'] } ] }, { @@ -79,6 +80,8 @@ export class AppMenuComponent implements OnInit, AfterViewInit items: [{ label: 'Login', icon: 'fa fa-sign-in', routerLink: ['/login'] }] }]; } + + this.modelUngrouped = [{ label: 'Main Menu', icon: 'fa fa-home', items: this.modelGrouped }]; } ngAfterViewInit() @@ -129,35 +132,18 @@ export class AppMenuComponent implements OnInit, AfterViewInit private onAuthenication(value: any): void { this.isAuthenicated = value.loggedIn; - if (value.loggedIn && value.userId && value.userId !=0) + const currentUser = this.service.authService.currentUserValue; + if (value.loggedIn && value.userId && value.userId != 0 && currentUser) { - this.service.loadMenu(value.userId).subscribe( - { - next: (resp: any) => - { - this.menuItems = resp || []; - this.app.userId = this.service.authService.currentUserValue?.id; - this.app.loginId = this.service.authService.currentUserValue?.loginId; - this.app.layoutMode = this.service.authService.currentUserValue?.menuLayout; - this.app.notificationCount = this.service.authService.currentUserValue?.notificationCount; - this.app.autoScrollMenu = (this.app.layoutMode === 'static' || this.app.layoutMode === 'overlay') ? 'auto-scroll-menu' : ''; - this.changeTheme(this.service.authService.currentUserValue?.themeName, this.service.authService.currentUserValue?.schemeName); + this.menuItems = { items: this.buildMenuItems(currentUser.moduleIds) }; + this.app.userId = currentUser.id; + this.app.loginId = currentUser.loginId; + this.app.layoutMode = currentUser.menuLayout; + this.app.notificationCount = currentUser.notificationCount; + this.app.autoScrollMenu = (this.app.layoutMode === 'static' || this.app.layoutMode === 'overlay') ? 'auto-scroll-menu' : ''; + this.changeTheme(currentUser.themeName, currentUser.schemeName); - this.reloadMenu(); - }, - error: () => - { - this.menuItems = []; - this.app.layoutMode = 'overlay' - this.app.loginId = this.app.autoScrollMenu = ''; - this.app.userId = this.app.notificationCount = 0; - - this.service.authService.loggedout(false); - this.reloadMenu(); - - this.router.navigate(['/login']); - } - }); + this.reloadMenu(); } else { @@ -172,6 +158,28 @@ export class AppMenuComponent implements OnInit, AfterViewInit this.router.navigate(['/login']); } } + + /** + * Builds the activity menu out of the permission keys the login endpoint returned + * in `moduleIds`, the profile pages are always available to a logged in user. + */ + private buildMenuItems(moduleIds: string[]): MenuNode[] + { + const routeExists = (routerLink: string): boolean => this.registeredRoutes.has(routerLink); + + const items: MenuNode[] = buildPermittedMenu([PROFILE_MENU], moduleIds, routeExists); + items.push(...buildPermittedMenu(MENU_DEFINITION, moduleIds, routeExists)); + + return items; + } + + /** Paths registered in the router, keeps permitted but not yet implemented pages out of the sidebar. */ + private get registeredRoutes(): Set + { + this.routePaths ??= new Set(this.router.config.map(route => route.path).filter(path => !!path && path !== '**')); + + return this.routePaths; + } } @Component({ diff --git a/App/ClientApp/src/app/components/models/menu.definition.ts b/App/ClientApp/src/app/components/models/menu.definition.ts new file mode 100644 index 0000000..ef1b94e --- /dev/null +++ b/App/ClientApp/src/app/components/models/menu.definition.ts @@ -0,0 +1,424 @@ +/** + * Client side mirror of the server menu (GlobalFunctions.BuildMenu). + * + * The login response returns the permission keys of the logged in user in `moduleIds`, + * so the sidebar is built here instead of calling `v1/users/loadMenu`. + * + * `moduleIds` holds one entry per permitted module plus the operation suffixes: + * ELIT.1.2.2 -> select (the page itself) + * ELIT.1.2.2_1 -> add + * ELIT.1.2.2_2 -> edit + * ELIT.1.2.2_3 -> delete + * + * Only items that are visible on the server menu are listed below, permission only + * keys (ELIT.1.1A, ELIT.4.1.10, ...) never render so they are left out. + */ +export interface MenuNode +{ + label: string; + icon?: string; + moduleId?: string; + routerLink?: string; + items?: MenuNode[]; +} + +/** Always available to a logged in user, the server adds these without any permission check. */ +export const PROFILE_MENU: MenuNode = +{ + label: 'User Profile', icon: 'fa fa-user-md', + items: + [ + { label: 'My Profile', icon: 'fa fa-user', routerLink: 'myprofile' }, + { label: 'Change My Password', icon: 'fa fa-unlock', routerLink: 'changemypwd' }, + { label: 'Change My Theme', icon: 'fa fa-user', routerLink: 'changemytheme' }, + { label: 'WhatsApp Message', icon: 'fa fa-whatsapp', routerLink: 'sendwhatsappmsg' }, + { label: 'Access Log', icon: 'fa fa-list-alt', routerLink: 'accesslog' } + ] +}; + +export const MENU_DEFINITION: MenuNode[] = + [ + { + moduleId: 'ELIT.1', label: 'System Setup', icon: 'fa fa-sun-o', + items: + [ + { moduleId: 'ELIT.1.3', label: 'Label Setting', icon: 'fa fa-sliders', routerLink: 'labelsetting' }, + { moduleId: 'ELIT.1.1', label: 'System Information', icon: 'fa fa-cog', routerLink: 'thissystem' }, + { + moduleId: 'ELIT.1.2', label: 'User Management', icon: 'fa fa-users', + items: + [ + { moduleId: 'ELIT.1.2.1', label: 'Groups', icon: 'fa fa-address-book-o', routerLink: 'groups' }, + { moduleId: 'ELIT.1.2.2', label: 'Users', icon: 'fa fa-address-card-o', routerLink: 'users' }, + { moduleId: 'ELIT.1.2.4', label: 'Force Logout', icon: 'fa fa-address-card-o', routerLink: 'forcelogout' } + ] + }, + { moduleId: 'ELIT.1.3A', label: 'Lookup Data', icon: 'fa fa-sliders', routerLink: 'lookupdata' }, + { moduleId: 'ELIT.1.4', label: 'Custom Report Setup', icon: 'fa fa-table', routerLink: 'customreports' }, + { + moduleId: 'ELIT.1.5', label: 'Basic Setup', icon: 'fa fa-wrench', + items: + [ + { moduleId: 'ELIT.1.5.1', label: 'Clients', icon: 'fa fa-address-card-o', routerLink: 'clients' }, + { moduleId: 'ELIT.1.5.2', label: 'Suppliers', icon: 'fa fa-credit-card', routerLink: 'suppliers' }, + { moduleId: 'ELIT.1.5.3', label: 'Employees', icon: 'fa fa-user-plus', routerLink: 'employees' }, + { moduleId: 'ELIT.1.5.4', label: 'Project Leads', icon: 'fa fa-user-circle-o', routerLink: 'projectleads' }, + { moduleId: 'ELIT.1.5.5', label: 'Project Managers', icon: 'fa fa-user-circle-o', routerLink: 'projectmgts' }, + { moduleId: 'ELIT.1.5.6', label: 'Projects', icon: 'fa fa-product-hunt', routerLink: 'projects' }, + { moduleId: 'ELIT.1.5.7', label: 'Stores', icon: 'fa fa-pencil-square', routerLink: 'stores' }, + { moduleId: 'ELIT.1.5.8', label: 'Banks & Branches', icon: 'fa fa-university', routerLink: 'bankbranches' }, + { moduleId: 'ELIT.1.5.9', label: 'Bank Accounts', icon: 'fa fa-credit-card', routerLink: 'bankaccounts' }, + { moduleId: 'ELIT.1.5.10', label: 'GL Codes', icon: 'fa fa-book', routerLink: 'glcodes' }, + { moduleId: 'ELIT.1.5.11', label: 'User Clients & Projects', icon: 'fa fa-product-hunt', routerLink: 'userprojects' }, + { moduleId: 'ELIT.1.5.12', label: 'User Suppliers', icon: 'fa fa-product-hunt', routerLink: 'usersuppliers' } + ] + } + ] + }, + { + moduleId: 'ELIT.2', label: 'Authentication Process', icon: 'fa fa-cogs', + items: + [ + { moduleId: 'ELIT.2.1', label: 'Authentication', icon: 'fa fa-check', routerLink: 'authentications' }, + { moduleId: 'ELIT.2.2', label: 'Authorization', icon: 'fa fa-check-square-o', routerLink: 'authorizations' }, + { moduleId: 'ELIT.2.3', label: 'Deactivation', icon: 'fa fa-window-close-o', routerLink: 'deactivations' } + ] + }, + { + moduleId: 'ELIT.3', label: 'General Ledger', icon: 'fa fa-university', + items: + [ + { + moduleId: 'ELIT.3.1', label: 'Basic Setup', icon: 'fa fa-wrench', + items: + [ + { moduleId: 'ELIT.3.1.1', label: 'Chart of Accounts', icon: 'fa fa-th-list', routerLink: 'chartofaccounts' }, + { moduleId: 'ELIT.3.1.2', label: 'Fix GL Balance', icon: 'fa fa-wrench', routerLink: 'fixBalance' }, + { moduleId: 'ELIT.3.1.3', label: 'GL Head Identification', icon: 'fa fa-cog', routerLink: 'identifyglhead' } + ] + }, + { + moduleId: 'ELIT.3.2', label: 'Transactions', icon: 'fa fa-building', + items: + [ + { moduleId: 'ELIT.3.2.1', label: 'GL Transactions', icon: 'fa fa-archive', routerLink: 'gltrans' }, + { moduleId: 'ELIT.3.2.2', label: 'Upload Auto Voucher', icon: 'fa fa-upload', routerLink: 'gltranav' }, + { moduleId: 'ELIT.3.2.3', label: 'Year End Process', icon: 'fa fa-cog', routerLink: 'yearendprocess' }, + { moduleId: 'ELIT.3.2.4', label: 'Undo Year End', icon: 'fa fa-undo', routerLink: 'undoyearend' }, + { moduleId: 'ELIT.3.2.5', label: 'Authorize Transactions', icon: 'fa fa-check-square', routerLink: 'authgltrans' }, + { moduleId: 'ELIT.3.2.7', label: 'Rollback Transactions', icon: 'fa fa-undo', routerLink: 'rollbackgltrans' }, + { moduleId: 'ELIT.3.2.8', label: 'Dashboard Processor', icon: 'fa fa-cog', routerLink: 'dbprocessor' } + ] + }, + { + moduleId: 'ELIT.3.3', label: 'Reports', icon: 'fa fa-newspaper-o', + items: + [ + { moduleId: 'ELIT.3.3.1', label: 'Ledger', icon: 'fa fa-file-text-o', routerLink: 'glledger' }, + { moduleId: 'ELIT.3.3.2', label: 'Monthly Turnover', icon: 'fa fa-file-text-o', routerLink: 'turnover' }, + { moduleId: 'ELIT.3.3.3', label: 'Trial Balance', icon: 'fa fa-file-text-o', routerLink: 'trialbalance' }, + { moduleId: 'ELIT.3.3.8', label: 'Income & Expenditure', icon: 'fa fa-file-text-o', routerLink: 'incomeexpense' }, + { moduleId: 'ELIT.3.3.4', label: 'Day Book', icon: 'fa fa-file-text-o', routerLink: 'daybook' }, + { moduleId: 'ELIT.3.3.5', label: 'Bank/Cash Book', icon: 'fa fa-file-text-o', routerLink: 'bankcashbook' }, + { moduleId: 'ELIT.3.3.6', label: 'Schedule', icon: 'fa fa-file-text-o', routerLink: 'glschedule' }, + { moduleId: 'ELIT.3.3.7', label: 'Previous Final Reports', icon: 'fa fa-file-text-o', routerLink: 'prvfinalreports' } + ] + } + ] + }, + { + moduleId: 'ELIT.4', label: 'Order To Cash', icon: 'fa fa-money', + items: + [ + { + moduleId: 'ELIT.4.1', label: 'Basic Setup', icon: 'fa fa-wrench', + items: + [ + { moduleId: 'ELIT.4.1.1', label: 'Income GL Codes', icon: 'fa fa-pencil-square-o', routerLink: 'incomeglcodes' }, + { moduleId: 'ELIT.4.1.2', label: 'VAT Liability GL Codes', icon: 'fa fa-pencil-square', routerLink: 'vatglcodes' }, + { moduleId: 'ELIT.4.1.3', label: 'AIT GL Codes', icon: 'fa fa-book', routerLink: 'aitglcodes' } + ] + }, + { + moduleId: 'ELIT.4.2', label: 'Regular Activities', icon: 'fa fa-building', + items: + [ + { moduleId: 'ELIT.4.2.1', label: 'Purchase Orders', icon: 'fa fa-smile-o', routerLink: 'purchaseorders' }, + { moduleId: 'ELIT.4.2.2', label: 'Invoices', icon: 'fa fa-share-square', routerLink: 'invoices' }, + { moduleId: 'ELIT.4.2.3', label: 'Payments', icon: 'fa fa-money', routerLink: 'payments' }, + { moduleId: 'ELIT.4.2.4', label: 'Freeze PO/Invoice', icon: 'fa fa-archive', routerLink: 'archivepoinvs' }, + { moduleId: 'ELIT.4.2.8', label: 'VAT Challan Update', icon: 'fa fa-sliders', routerLink: 'updvatchlano2c' } + ] + }, + { + moduleId: 'ELIT.4.3', label: 'Reports', icon: 'fa fa-newspaper-o', + items: + [ + { moduleId: 'ELIT.4.3.1', label: 'GL Vouchers', icon: 'fa fa-th-list', routerLink: 'glvouchers' }, + { moduleId: 'ELIT.4.3.2', label: 'Transactional Reports', icon: 'fa fa-file-text-o', routerLink: 'txnreports' }, + { moduleId: 'ELIT.4.3.3', label: 'Contract Expiry Report', icon: 'fa fa-file-text-o', routerLink: 'contexpryreports' }, + { moduleId: 'ELIT.4.3.4', label: 'Customized Report', icon: 'fa fa-file-text-o', routerLink: 'customreporto2c' }, + { moduleId: 'ELIT.4.3.5', label: 'Client Ledger', icon: 'fa fa-file-text-o', routerLink: 'clientledgero2c' }, + { moduleId: 'ELIT.4.3.6', label: 'Monthly Trends', icon: 'fa fa-file-text-o', routerLink: 'monthlytrendso2c' } + ] + } + ] + }, + { + moduleId: 'ELIT.9', label: 'Sales System', icon: 'fa fa-sitemap', + items: + [ + { + moduleId: 'ELIT.9.1', label: 'Basic Setup', icon: 'fa fa-wrench', + items: + [ + { moduleId: 'ELIT.9.1.1', label: 'Market Hierarchies', icon: 'fa fa-pencil-square', routerLink: 'markethierarchies' }, + { moduleId: 'ELIT.9.1.2', label: 'Sales Points', icon: 'fa fa-pencil-square', routerLink: 'salespoints' }, + { moduleId: 'ELIT.9.1.3', label: 'Product Hierarchies', icon: 'fa fa-pencil-square', routerLink: 'producthierarchies' }, + { moduleId: 'ELIT.9.1.4', label: 'Brands', icon: 'fa fa-pencil-square', routerLink: 'brands' }, + { moduleId: 'ELIT.9.1.5', label: 'SKUs', icon: 'fa fa-pencil-square', routerLink: 'skus' }, + { moduleId: 'ELIT.9.1.6', label: 'Channel Hierarchies', icon: 'fa fa-pencil-square', routerLink: 'channelhierarchies' }, + { moduleId: 'ELIT.9.1.7', label: 'Customers', icon: 'fa fa-pencil-square', routerLink: 'customers' }, + { moduleId: 'ELIT.9.1.8', label: 'Sales Officers', icon: 'fa fa-pencil-square', routerLink: 'salesofficers' }, + { moduleId: 'ELIT.9.1.9', label: 'Product Prices', icon: 'fa fa-pencil-square', routerLink: 'skupricess' }, + { moduleId: 'ELIT.9.1.10', label: 'Product VAT-SD-Comm', icon: 'fa fa-pencil-square', routerLink: 'skuvattaxes' }, + { moduleId: 'ELIT.9.1.11', label: 'Customer Credit', icon: 'fa fa-pencil-square', routerLink: 'customercredit' }, + { moduleId: 'ELIT.9.1.12', label: 'External Products', icon: 'fa fa-pencil-square', routerLink: 'extnlproducts' }, + { moduleId: 'ELIT.9.1.13', label: 'Sales Promotions', icon: 'fa fa-pencil-square', routerLink: 'salespromotions' }, + { moduleId: 'ELIT.9.1.14', label: 'Order Authorization Limit', icon: 'fa fa-pencil-square', routerLink: 'ordrauthlimit' } + ] + }, + { + moduleId: 'ELIT.9.2', label: 'Regular Activities', icon: 'fa fa-building', + items: + [ + { moduleId: 'ELIT.9.2.1', label: 'Customer Opening Balance', icon: 'fa fa-puzzle-piece', routerLink: 'slscustsopnbal' }, + { moduleId: 'ELIT.9.2.2', label: 'Customer Adjustment', icon: 'fa fa-pencil-square', routerLink: 'slscustsadjustment' }, + { moduleId: 'ELIT.9.2.3', label: 'Initial Stock', icon: 'fa fa-puzzle-piece', routerLink: 'slsinitstocks' }, + { moduleId: 'ELIT.9.2.4', label: 'Receive Stocks', icon: 'fa fa-reply-all', routerLink: 'slsrcvstocks' }, + { moduleId: 'ELIT.9.2.5', label: 'Sales Orders', icon: 'fa fa-pencil-square', routerLink: 'slsorders' }, + { moduleId: 'ELIT.9.2.6', label: 'Spot Sales', icon: 'fa fa-pencil-square', routerLink: 'slsdirectinvoices' }, + { moduleId: 'ELIT.9.2.7', label: 'Sales Invoices', icon: 'fa fa-pencil-square', routerLink: 'slsinvoices' }, + { moduleId: 'ELIT.9.2.8', label: 'Payment Against Invoices', icon: 'fa fa-archive', routerLink: 'slsinvpayments' }, + { moduleId: 'ELIT.9.2.9', label: 'Stock Transfers', icon: 'fa fa-archive', routerLink: 'slsstockxfers' }, + { moduleId: 'ELIT.9.2.10', label: 'Receive Transferred Stocks', icon: 'fa fa-archive', routerLink: 'slsstockxferrcvs' }, + { moduleId: 'ELIT.9.2.11', label: 'Fix Transfer Anomalies', icon: 'fa fa-archive', routerLink: 'slsstockxferanmls' }, + { moduleId: 'ELIT.9.2.12', label: 'Sales Return', icon: 'fa fa-archive', routerLink: 'slsrtnstocks' }, + { moduleId: 'ELIT.9.2.13', label: 'Damage Stocks', icon: 'fa fa-archive', routerLink: 'slsdmgstocks' }, + { moduleId: 'ELIT.9.2.14', label: 'Stock Take', icon: 'fa fa-archive', routerLink: 'slsstocktakes' } + ] + }, + { + moduleId: 'ELIT.9.3', label: 'Reports', icon: 'fa fa-newspaper-o', + items: + [ + { moduleId: 'ELIT.9.3.1', label: 'Customer Balance', icon: 'fa fa-th-list', routerLink: 'salescustbalance' }, + { moduleId: 'ELIT.9.3.2', label: 'Customer Ledger', icon: 'fa fa-th-list', routerLink: 'salescustledger' }, + { moduleId: 'ELIT.9.3.3', label: 'Current Stock', icon: 'fa fa-file-text-o', routerLink: 'salescurrentstock' }, + { moduleId: 'ELIT.9.3.4', label: 'Stock Ledger', icon: 'fa fa-th-list', routerLink: 'salesstockledger' } + ] + } + ] + }, + { + moduleId: 'ELIT.5', label: 'Purchase To Pay', icon: 'fa fa-shopping-cart', + items: + [ + { + moduleId: 'ELIT.5.1', label: 'Basic Setup', icon: 'fa fa-wrench', + items: + [ + { moduleId: 'ELIT.5.1.1', label: 'Product Types', icon: 'fa fa-address-card-o', routerLink: 'prodtypes' }, + { moduleId: 'ELIT.5.1.2', label: 'Products', icon: 'fa fa-university', routerLink: 'products' }, + { moduleId: 'ELIT.5.1.3', label: 'WO Terms & Conditions', icon: 'fa fa-pencil-square-o', routerLink: 'woterms' }, + { moduleId: 'ELIT.5.1.4', label: 'VAT & AIT Rates', icon: 'fa fa-user-circle-o', routerLink: 'aitvatratep2p' } + ] + }, + { + moduleId: 'ELIT.5.2', label: 'Regular Activities', icon: 'fa fa-building', + items: + [ + { moduleId: 'ELIT.5.2.0', label: 'Initial Stock', icon: 'fa fa-puzzle-piece', routerLink: 'initstocksp2p' }, + { moduleId: 'ELIT.5.2.13', label: 'Purchase Requisitions', icon: 'fa fa-puzzle-piece', routerLink: 'purchrequisitionsp2p' }, + { moduleId: 'ELIT.5.2.1', label: 'Issue Work Orders', icon: 'fa fa-smile-o', routerLink: 'workordersp2p' }, + { moduleId: 'ELIT.5.2.12', label: 'Advance Payments', icon: 'fa fa-money', routerLink: 'advpaymntsp2p' }, + { moduleId: 'ELIT.5.2.2', label: 'Receive Items at CS', icon: 'fa fa-reply-all', routerLink: 'stksrcvdatcsp2p' }, + { moduleId: 'ELIT.5.2.3', label: 'Receive Invoices', icon: 'fa fa-money', routerLink: 'invoicesp2p' }, + { moduleId: 'ELIT.5.2.4', label: 'Cash Purchase', icon: 'fa fa-archive', routerLink: 'cashpurchases' }, + { moduleId: 'ELIT.5.2.5', label: 'Payment Against Invoice', icon: 'fa fa-archive', routerLink: 'paymentsp2p' }, + { moduleId: 'ELIT.5.2.6', label: 'Goods Transfer', icon: 'fa fa-archive', routerLink: 'gdstransfers' }, + { moduleId: 'ELIT.5.2.7', label: 'Receive Goods Transfer', icon: 'fa fa-archive', routerLink: 'gdsreceives' }, + { moduleId: 'ELIT.5.2.8', label: 'Fix Transfer Anomalies', icon: 'fa fa-archive', routerLink: 'transferanomalies' }, + { moduleId: 'ELIT.5.2.9', label: 'Consumption of Goods', icon: 'fa fa-archive', routerLink: 'gdscnsmpnsp2p' }, + { moduleId: 'ELIT.5.2.10', label: 'Damage of Goods', icon: 'fa fa-archive', routerLink: 'gdsdamagesp2p' }, + { moduleId: 'ELIT.5.2.11', label: 'Stock Take of Goods', icon: 'fa fa-archive', routerLink: 'stocktakes' } + ] + }, + { + moduleId: 'ELIT.5.3', label: 'Reports', icon: 'fa fa-newspaper-o', + items: + [ + { moduleId: 'ELIT.5.3.1', label: 'GL Vouchers', icon: 'fa fa-th-list', routerLink: 'glvouchersp2p' }, + { moduleId: 'ELIT.5.3.2', label: 'Current Stock', icon: 'fa fa-file-text-o', routerLink: 'currentstockp2p' }, + { moduleId: 'ELIT.5.3.3', label: 'Stock Ledger', icon: 'fa fa-file-text-o', routerLink: 'stockledgerp2p' }, + { moduleId: 'ELIT.5.3.4', label: 'Supplier Ledger', icon: 'fa fa-file-text-o', routerLink: 'supplierledgerp2p' } + ] + } + ] + }, + { + moduleId: 'ELIT.6', label: 'Fixed Assets', icon: 'fa fa-sitemap', + items: + [ + { + moduleId: 'ELIT.6.1', label: 'Basic Setup', icon: 'fa fa-wrench', + items: + [ + { moduleId: 'ELIT.6.1.1', label: 'Asset Types', icon: 'fa fa-address-card-o', routerLink: 'assettypes' }, + { moduleId: 'ELIT.6.1.2', label: 'Asset Categories', icon: 'fa fa-university', routerLink: 'assetcategories' }, + { moduleId: 'ELIT.6.1.3', label: 'Asset Items', icon: 'fa fa-credit-card', routerLink: 'assetitems' }, + { moduleId: 'ELIT.6.1.6', label: 'Asset Hierarchies', icon: 'fa fa-credit-card', routerLink: 'assethierarchies' }, + { moduleId: 'ELIT.6.1.4', label: 'Locations', icon: 'fa fa-pencil-square', routerLink: 'locations' }, + { moduleId: 'ELIT.6.1.5', label: 'VAT & AIT Rates', icon: 'fa fa-user-circle-o', routerLink: 'aitvatratefa' } + ] + }, + { + moduleId: 'ELIT.6.2', label: 'Regular Activities', icon: 'fa fa-building', + items: + [ + { moduleId: 'ELIT.6.2.0', label: 'Initial Fixed Assets', icon: 'fa fa-puzzle-piece', routerLink: 'initstocksfa' }, + { moduleId: 'ELIT.6.2.15', label: 'Purchase Requisitions', icon: 'fa fa-puzzle-piece', routerLink: 'purchrequisitionsfa' }, + { moduleId: 'ELIT.6.2.1', label: 'Issue Work Orders', icon: 'fa fa-smile-o', routerLink: 'workordersfa' }, + { moduleId: 'ELIT.6.2.14', label: 'Advance Payments', icon: 'fa fa-money', routerLink: 'advpaymntsfa' }, + { moduleId: 'ELIT.6.2.2', label: 'Received Items At CS', icon: 'fa fa-reply-all', routerLink: 'stksrcvdatcsfa' }, + { moduleId: 'ELIT.6.2.3', label: 'Receive Invoices', icon: 'fa fa-money', routerLink: 'invoicesfa' }, + { moduleId: 'ELIT.6.2.4', label: 'Payment Against Invoice', icon: 'fa fa-archive', routerLink: 'paymentsfa' }, + { moduleId: 'ELIT.6.2.5', label: 'Item Acquisition', icon: 'fa fa-smile-o', routerLink: 'itemsacquisition' }, + { moduleId: 'ELIT.6.2.5A', label: 'Update Item Data', icon: 'fa fa-smile-o', routerLink: 'updfaitmdescription' }, + { moduleId: 'ELIT.6.2.6', label: 'Item Installation', icon: 'fa fa-share-square', routerLink: 'astinstalns' }, + { moduleId: 'ELIT.6.2.7', label: 'Transfer Fixed Asset', icon: 'fa fa-money', routerLink: 'assetxfers' }, + { moduleId: 'ELIT.6.2.8', label: 'Write Off', icon: 'fa fa-archive', routerLink: 'astwriteoffs' }, + { moduleId: 'ELIT.6.2.9', label: 'Asset Sales', icon: 'fa fa-archive', routerLink: 'astsales' }, + { moduleId: 'ELIT.6.2.10', label: 'Add Asset Value', icon: 'fa fa-archive', routerLink: 'astaddvalues' }, + { moduleId: 'ELIT.6.2.11', label: 'Reduce Asset Value', icon: 'fa fa-archive', routerLink: 'assetreducesvalue' }, + { moduleId: 'ELIT.6.2.12', label: 'No Depreciation Mark', icon: 'fa fa-user-circle-o', routerLink: 'nodeprnmark' }, + { moduleId: 'ELIT.6.2.13', label: 'Depreciation Calculation', icon: 'fa fa-user-circle-o', routerLink: 'calcdepreciation' } + ] + }, + { + moduleId: 'ELIT.6.3', label: 'Reports', icon: 'fa fa-newspaper-o', + items: + [ + { moduleId: 'ELIT.6.3.1', label: 'GL Vouchers', icon: 'fa fa-th-list', routerLink: 'glvouchersfa' }, + { moduleId: 'ELIT.6.3.2', label: 'Current Stock', icon: 'fa fa-file-text-o', routerLink: 'currentstockfa' }, + { moduleId: 'ELIT.6.3.3', label: 'Stock Ledger', icon: 'fa fa-file-text-o', routerLink: 'stockledgerfa' }, + { moduleId: 'ELIT.6.3.4', label: 'Item Details', icon: 'fa fa-file-text-o', routerLink: 'faitemdetail' }, + { moduleId: 'ELIT.6.3.5', label: 'Fixed Asset List', icon: 'fa fa-file-text-o', routerLink: 'faitemsatglance' }, + { moduleId: 'ELIT.6.3.6', label: 'Waiting Acquisition', icon: 'fa fa-file-text-o', routerLink: 'faitemswtngaqstn' }, + { moduleId: 'ELIT.6.3.7', label: 'Acquired Assets', icon: 'fa fa-file-text-o', routerLink: 'faitemsacquire' }, + { moduleId: 'ELIT.6.3.8', label: 'Waiting Installation', icon: 'fa fa-file-text-o', routerLink: 'faitemswtnginstall' }, + { moduleId: 'ELIT.6.3.9', label: 'No Depreciation Marked', icon: 'fa fa-file-text-o', routerLink: 'faitemsnodprn' }, + { moduleId: 'ELIT.6.3.10', label: 'Depreciation Report', icon: 'fa fa-file-text-o', routerLink: 'faitemsdprnrpt' }, + { moduleId: 'ELIT.6.3.11', label: 'Supplier Ledger', icon: 'fa fa-file-text-o', routerLink: 'fasupplierledger' } + ] + } + ] + }, + { + moduleId: 'ELIT.7', label: 'Task Management', icon: 'fa fa-users', + items: + [ + { + moduleId: 'ELIT.7.1', label: 'Basic Setup', icon: 'fa fa-wrench', + items: + [ + { moduleId: 'ELIT.7.1.1', label: 'Developers', icon: 'fa fa-user-circle-o', routerLink: 'developers' }, + { moduleId: 'ELIT.7.1.2', label: 'Support Teams', icon: 'fa fa-user-circle-o', routerLink: 'supportteams' }, + { moduleId: 'ELIT.7.1.2A', label: 'QA Teams', icon: 'fa fa-user-circle-o', routerLink: 'qateams' }, + { moduleId: 'ELIT.7.1.3', label: 'Priorities', icon: 'fa fa-check-square-o', routerLink: 'priorities' }, + { moduleId: 'ELIT.7.1.4', label: 'Task Statuses', icon: 'fa fa-pencil-square', routerLink: 'taskstatuses' }, + { moduleId: 'ELIT.7.1.5', label: 'Task Categories', icon: 'fa fa-check-square-o', routerLink: 'taskcategories' }, + { moduleId: 'ELIT.7.1.6', label: 'Task Types', icon: 'fa fa-check-square-o', routerLink: 'tasktypes' }, + { moduleId: 'ELIT.7.1.7', label: 'Team Spaces', icon: 'fa fa-user-circle-o', routerLink: 'teamspaces' }, + { moduleId: 'ELIT.7.1.9', label: 'User Projects', icon: 'fa fa-product-hunt', routerLink: 'tsuserprojects' } + ] + }, + { + moduleId: 'ELIT.7.2', label: 'Regular Activities', icon: 'fa fa-building', + items: + [ + { moduleId: 'ELIT.7.2.1', label: 'Tasks', icon: 'fa fa-smile-o', routerLink: 'tasks' }, + { moduleId: 'ELIT.7.2.2', label: 'My Tasks', icon: 'fa fa-tasks', routerLink: 'mytasks' } + ] + }, + { + moduleId: 'ELIT.7.3', label: 'Reports', icon: 'fa fa-newspaper-o', + items: [{ moduleId: 'ELIT.7.3.1', label: 'Task Reports', icon: 'fa fa-file-text-o', routerLink: 'taskreport' }] + } + ] + }, + { + moduleId: 'ELIT.8', label: 'Attendance System', icon: 'fa fa-calendar', + items: + [ + { + moduleId: 'ELIT.8.1', label: 'Basic Setup', icon: 'fa fa-wrench', + items: + [ + { moduleId: 'ELIT.8.1.1', label: 'Ip/Mac Entries', icon: 'fa fa-user-circle-o', routerLink: 'ipmacentries' }, + { moduleId: 'ELIT.8.1.2', label: 'Holiday Calendar', icon: 'fa fa-calendar', routerLink: 'calendar' }, + { moduleId: 'ELIT.8.1.3', label: 'Ramadan Calendar', icon: 'fa fa-calendar', routerLink: 'ramadan' } + ] + }, + { + moduleId: 'ELIT.8.2', label: 'Regular Activities', icon: 'fa fa-building', + items: + [ + { moduleId: 'ELIT.8.2.1', label: 'Employee Leaves', icon: 'fa fa-smile-o', routerLink: 'empleaves' }, + { moduleId: 'ELIT.8.2.2', label: 'Late Entries', icon: 'fa fa-tasks', routerLink: 'emplates' }, + { moduleId: 'ELIT.8.2.3', label: 'Client Visits', icon: 'fa fa-tasks', routerLink: 'empclntvsts' }, + { moduleId: 'ELIT.8.2.4', label: 'Home Offices', icon: 'fa fa-tasks', routerLink: 'emphmofcs' }, + { moduleId: 'ELIT.8.2.6', label: 'Asset Bookings', icon: 'fa fa-tasks', routerLink: 'assetbookings' } + ] + }, + { + moduleId: 'ELIT.8.3', label: 'Reports', icon: 'fa fa-newspaper-o', + items: + [ + { moduleId: 'ELIT.8.3.1', label: 'Login History', icon: 'fa fa-tasks', routerLink: 'loginhistory' }, + { moduleId: 'ELIT.8.3.2', label: 'Attendance Reports', icon: 'fa fa-file-text-o', routerLink: 'atnreports' } + ] + } + ] + } + ]; + +/** + * Keeps the nodes the user is permitted to see. + * + * A group is kept only when its own key is permitted and at least one of its children survives, + * a page is kept only when its key is permitted and the route is actually registered in the app, + * so a permitted but not yet implemented page never lands in the sidebar as a dead link. + */ +export function buildPermittedMenu(nodes: MenuNode[], moduleIds: string[], routeExists: (routerLink: string) => boolean): MenuNode[] +{ + const permitted: MenuNode[] = []; + for (const node of nodes ?? []) + { + if (node.moduleId && !moduleIds?.includes(node.moduleId)) + continue; + + if (node.items?.length) + { + const items = buildPermittedMenu(node.items, moduleIds, routeExists); + if (items.length) + permitted.push({ moduleId: node.moduleId, label: node.label, icon: node.icon, items: items }); + } + else if (node.routerLink && routeExists(node.routerLink)) + { + permitted.push({ moduleId: node.moduleId, label: node.label, icon: node.icon, routerLink: node.routerLink }); + } + } + + return permitted; +} diff --git a/App/ClientApp/src/app/components/models/user.model.ts b/App/ClientApp/src/app/components/models/user.model.ts index ff0bfda..bb2165b 100644 --- a/App/ClientApp/src/app/components/models/user.model.ts +++ b/App/ClientApp/src/app/components/models/user.model.ts @@ -1,35 +1,39 @@ export class UserModel { id: number; - logId: number; expires?: Date; loginId: string; userName: string; systemDate: Date; - idleTime: number; - pingTime: number; loginTime: string; logoutTime?: Date; validUser: boolean; - employeeId: number; - timeoutTime: number; loginStatus: number; - bmProcessId: number; - prProcessId: number; - dbOnStartup: boolean; - batchEnabled: boolean; - viewOwnTaskOnly: boolean; + authMethod: number; notificationCount: number; pwdChangeRequired: boolean; authenticationToken?: string; authRequiredAtLogin: boolean; - idsValue: string; - moduleIds: any[] = []; + //Returned by the login endpoint since the token based authentication + refreshToken?: string; + userPlatFormType?: number; + userRoleType?: string; + employeeNumber?: string; + + //Permission keys of the logged in user, "" = select, "_1" = add, "_2" = edit, "_3" = delete + moduleIds: string[] = []; returnStatus: number; returnMessage: any[] = []; validationErrors: any[] = []; + //Not returned by the login endpoint anymore, defaults are applied on the client + logId?: number; + idleTime?: number; + pingTime?: number; + employeeId?: number; + timeoutTime?: number; + dbOnStartup?: boolean; schemeName: string = 'dark'; themeName: string = 'yellow'; menuLayout: string = 'overlay'; diff --git a/App/ClientApp/src/app/login/login.component.ts b/App/ClientApp/src/app/login/login.component.ts index 8c70966..2650fa8 100644 --- a/App/ClientApp/src/app/login/login.component.ts +++ b/App/ClientApp/src/app/login/login.component.ts @@ -67,10 +67,7 @@ export class LoginComponent implements OnInit debugger; if (this.authService.currentUserValue?.id) { - if (this.authService.currentUserValue.dbOnStartup) - this.router.navigate(['/dashboard']); - else - this.router.navigate(['/home']); + this.router.navigate(['/home']); } else { @@ -270,110 +267,107 @@ export class LoginComponent implements OnInit { this.loading = false; this.spinnerService.hide(); + + //The endpoint answers 200 only for a valid user, guard anyway so a partial + //response never leaves a half logged in session behind + if (!data?.validUser || !data.id || !data.authenticationToken) + { + this.abortLogin(this.parseReturnMessage(data) || 'Login failed, please try again.'); + return; + } + if (data.authRequiredAtLogin) { - this.authService.loggedIn(data, false); - const modalOptions: NgbModalOptions = { backdrop: 'static', keyboard: false, backdropClass: 'customBackdrop', centered: true, windowClass: 'misc-width' }; - const modalRef = this.modalService.open(OtpComponent, modalOptions); - - modalRef.componentInstance.fromParent = { userId: data.id, authMethod: data.authMethod }; - modalRef.result.then((result) => - { - if (result) - { - if (data.pwdChangeRequired) - { - this.authService.loggedIn(data, false); - const modalOptions1: NgbModalOptions = { backdrop: 'static', keyboard: false, backdropClass: 'customBackdrop', centered: true }; - const modalRef1 = this.modalService.open(ChangePwdComponent, modalOptions1); - modalRef1.componentInstance.fromParent = { userId: data.id, loginId: data.loginId, userName: data.userName }; - - modalRef1.result.then(() => - { - this.authService.loggedout(); - }); - } - else - { - this.authService.refreshToken().subscribe( - { - next: () => - { - localStorage.setItem('OnlineSalesAutoCrop.sysdate', sysDate); - if (this.f.remberMe.value) - localStorage.setItem('OnlineSalesAutoCrop.rlap', loginInf); - else - localStorage.removeItem('OnlineSalesAutoCrop.rlap'); - - this.idleSvc.startMonitor(); - this.authService.raiseLoginEvent(data.id, true); - const returnUrl = this.authService.currentUserValue?.dbOnStartup ? this.route.snapshot.queryParams['returnUrl'] ?? '/dashboard' : this.route.snapshot.queryParams['returnUrl'] ?? '/home'; - this.router.navigate([returnUrl]); - }, - error: () => - { - this.authService.loggedout(); - } - }); - } - } - else - { - localStorage.removeItem('OnlineSalesAutoCrop.rlap'); - localStorage.removeItem('OnlineSalesAutoCrop.sysdate'); - - this.authService.loggedout(); - } - }); + this.verifyOtp(data, sysDate, loginInf); } else if (data.pwdChangeRequired) { - this.authService.loggedIn(data, false); - const modalOptions: NgbModalOptions = { backdrop: 'static', keyboard: false, backdropClass: 'customBackdrop', centered: true }; - const modalRef = this.modalService.open(ChangePwdComponent, modalOptions); - modalRef.componentInstance.fromParent = { userId: data.id, loginId: data.loginId, userName: data.userName };; - - modalRef.result.then(() => - { - this.authService.loggedout(); - }); + this.changePassword(data); } else { - this.authService.refreshToken().subscribe( - { - next: () => - { - localStorage.setItem('OnlineSalesAutoCrop.sysdate', sysDate); - if (this.f.remberMe.value) - localStorage.setItem('OnlineSalesAutoCrop.rlap', loginInf); - else - localStorage.removeItem('OnlineSalesAutoCrop.rlap'); - - this.idleSvc.startMonitor(); - this.authService.raiseLoginEvent(data.id, true); - const returnUrl = this.authService.currentUserValue?.dbOnStartup ? this.route.snapshot.queryParams['returnUrl'] ?? '/dashboard' : this.route.snapshot.queryParams['returnUrl'] ?? '/home'; - this.router.navigate([returnUrl]); - }, - error: () => - { - localStorage.removeItem('OnlineSalesAutoCrop.rlap'); - localStorage.removeItem('OnlineSalesAutoCrop.sysdate'); - - this.authService.loggedout(); - } - }); + this.completeLogin(data, sysDate, loginInf); } }, error: (errorMessage: string) => { this.loading = false; this.spinnerService.hide(); - localStorage.removeItem('OnlineSalesAutoCrop.rlap'); - localStorage.removeItem('OnlineSalesAutoCrop.sysdate'); - this.alertService.error(errorMessage); + this.abortLogin(errorMessage); } }); } + + /** Second factor, the user is cached already so the otp call travels with the token. */ + private verifyOtp(data: any, sysDate: string, loginInf: string): void + { + this.authService.loggedIn(data, false); + const modalOptions: NgbModalOptions = { backdrop: 'static', keyboard: false, backdropClass: 'customBackdrop', centered: true, windowClass: 'misc-width' }; + const modalRef = this.modalService.open(OtpComponent, modalOptions); + + modalRef.componentInstance.fromParent = { userId: data.id, authMethod: data.authMethod }; + modalRef.result.then((result) => + { + if (!result) + { + this.abortLogin(''); + return; + } + + if (data.pwdChangeRequired) + this.changePassword(data); + else + this.completeLogin(data, sysDate, loginInf); + }, + () => this.abortLogin('')); + } + + /** Expired/first time password, the user has to sign in again with the new one. */ + private changePassword(data: any): void + { + this.authService.loggedIn(data, false); + const modalOptions: NgbModalOptions = { backdrop: 'static', keyboard: false, backdropClass: 'customBackdrop', centered: true }; + const modalRef = this.modalService.open(ChangePwdComponent, modalOptions); + modalRef.componentInstance.fromParent = { userId: data.id, loginId: data.loginId, userName: data.userName }; + + modalRef.result.then(() => this.authService.loggedout(), () => this.authService.loggedout()); + } + + /** + * The login response already carries the authentication token, the refresh token and the + * permission keys (moduleIds), so nothing else has to be fetched before entering the app. + * Raising the login event makes the sidebar rebuild itself from those permission keys. + */ + private completeLogin(data: any, sysDate: string, loginInf: string): void + { + localStorage.setItem('OnlineSalesAutoCrop.sysdate', sysDate); + if (this.f.remberMe.value) + localStorage.setItem('OnlineSalesAutoCrop.rlap', loginInf); + else + localStorage.removeItem('OnlineSalesAutoCrop.rlap'); + + this.idleSvc.startMonitor(); + this.authService.raiseLoginEvent(data.id, true); + + const returnUrl = this.route.snapshot.queryParams['returnUrl'] ?? '/home'; + this.router.navigate([returnUrl]); + } + + private abortLogin(errorMessage: string): void + { + this.loading = false; + this.spinnerService.hide(); + localStorage.removeItem('OnlineSalesAutoCrop.rlap'); + localStorage.removeItem('OnlineSalesAutoCrop.sysdate'); + + this.authService.loggedout(); + if (errorMessage) + this.alertService.error(errorMessage); + } + + private parseReturnMessage(data: any): string + { + return Array.isArray(data?.returnMessage) ? data.returnMessage.join('
') : ''; + } } diff --git a/App/ClientApp/src/providers/authguard.ts b/App/ClientApp/src/providers/authguard.ts index 3c7e292..1329abe 100644 --- a/App/ClientApp/src/providers/authguard.ts +++ b/App/ClientApp/src/providers/authguard.ts @@ -25,7 +25,7 @@ export class AuthGuard let keys: string[] = permissionkey.split(','); for (const key of keys) { - found = currentUser.moduleIds.includes(key); + found = currentUser.moduleIds?.includes(key) === true; if (found) break; } @@ -33,7 +33,7 @@ export class AuthGuard if (found === false) this.router.navigate(['/accessdenied']); - return true; + return found; } } else diff --git a/App/ClientApp/src/providers/user/authentication.service.ts b/App/ClientApp/src/providers/user/authentication.service.ts index 2a406b4..61e97ba 100644 --- a/App/ClientApp/src/providers/user/authentication.service.ts +++ b/App/ClientApp/src/providers/user/authentication.service.ts @@ -59,12 +59,7 @@ export class AuthenticationService return this.api.postAsync('v1/users/login', params) .pipe(map(user => { - user.moduleIds = JSON.parse(this.functionService.decipherData(user.idsValue)); - user.idsValue = ''; - const encString: string = this.functionService.encrypt(JSON.stringify(user)); - localStorage.setItem('OnlineSalesAutoCrop.currentUser', encString); - - this.currentUserSubject.next(user); + this.storeCurrentUser(user); return user; })); } @@ -74,6 +69,31 @@ export class AuthenticationService } } + /** + * The login endpoint returns the permission keys in `moduleIds` and no longer returns the + * theme/idle settings, so the client defaults are applied here before the user is cached. + */ + public storeCurrentUser(user: any): void + { + user.moduleIds = user.moduleIds ?? []; + + const defaults = new UserModel(); + user.themeName = user.themeName || defaults.themeName; + user.schemeName = user.schemeName || defaults.schemeName; + user.menuLayout = user.menuLayout || defaults.menuLayout; + + const encString: string = this.functionService.encrypt(JSON.stringify(user)); + localStorage.setItem('OnlineSalesAutoCrop.currentUser', encString); + + this.currentUserSubject.next(user); + } + + /** True when the permission key is granted, pass "_1|_2|_3" to check add/edit/delete. */ + public hasPermission(moduleId: string): boolean + { + return this.currentUserValue?.moduleIds?.includes(moduleId) === true; + } + public refreshToken() { try @@ -99,13 +119,13 @@ export class AuthenticationService { try { - if (!this.currentUserValue?.logId) + if (!this.currentUserValue?.id) { localStorage.removeItem('OnlineSalesAutoCrop.currentUser'); return; } - const params: any = { logId: this.currentUserValue.logId, attendanceLogout: attendanceLogout, ipAddress: ipAddress, macAddress: macAddress, hostName: hostName, logoutRemarks: logoutRemarks }; + const params: any = { logId: this.currentUserValue.logId ?? 0, attendanceLogout: attendanceLogout, ipAddress: ipAddress, macAddress: macAddress, hostName: hostName, logoutRemarks: logoutRemarks }; return this.api.postAsync('v1/users/logout', params) .pipe(map(value => { @@ -142,7 +162,7 @@ export class AuthenticationService { try { - const params: any = { logId: this.currentUserValue.logId }; + const params: any = { logId: this.currentUserValue?.logId ?? 0 }; return this.api.postAsync('v1/users/sessionExpired', params) .pipe(map(value => {