diff --git a/src/admin/Bootstrap.Admin/Bootstrap.Admin.csproj b/src/admin/Bootstrap.Admin/Bootstrap.Admin.csproj index 10c485b9..82b380c2 100644 --- a/src/admin/Bootstrap.Admin/Bootstrap.Admin.csproj +++ b/src/admin/Bootstrap.Admin/Bootstrap.Admin.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/admin/Bootstrap.Admin/Controllers/AccountController.cs b/src/admin/Bootstrap.Admin/Controllers/AccountController.cs index 5b67ce28..1d87b097 100644 --- a/src/admin/Bootstrap.Admin/Controllers/AccountController.cs +++ b/src/admin/Bootstrap.Admin/Controllers/AccountController.cs @@ -1,11 +1,14 @@ using Bootstrap.Admin.Models; using Bootstrap.DataAccess; +using Longbow.GiteeAuth; +using Longbow.GitHubAuth; using Longbow.Web; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; using System; using System.Linq; using System.Net; @@ -30,7 +33,6 @@ namespace Bootstrap.Admin.Controllers { if (!User.Identity.IsAuthenticated) return Login(); - var user = UserHelper.RetrieveUserByUserName(User.Identity.Name); await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); var urlReferrer = Request.Headers["Referer"].FirstOrDefault(); return View(new LockModel(this) @@ -107,10 +109,11 @@ namespace Bootstrap.Admin.Controllers /// Logout this instance. /// /// The logout. + [HttpGet] public async Task Logout() { - await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); - return Redirect("~" + CookieAuthenticationDefaults.LoginPath); + await HttpContext.SignOutAsync(); + return Redirect(Request.PathBase + CookieAuthenticationDefaults.LoginPath); } /// @@ -118,6 +121,29 @@ namespace Bootstrap.Admin.Controllers /// /// The denied. [ResponseCache(Duration = 600)] + [HttpGet] public ActionResult AccessDenied() => View("Error", ErrorModel.CreateById(403)); + + /// + /// Gitee 认证 + /// + /// + [HttpGet] + public IActionResult Gitee([FromServices]IConfiguration config) + { + var enabled = config.GetValue($"{nameof(GiteeOptions)}:Eanbeld", false); + return Challenge(enabled ? GiteeDefaults.AuthenticationScheme : CookieAuthenticationDefaults.AuthenticationScheme); + } + + /// + /// GitHub 认证 + /// + /// + [HttpGet] + public IActionResult GitHub([FromServices]IConfiguration config) + { + var enabled = config.GetValue($"{nameof(GitHubOptions)}:Eanbeld", false); + return Challenge(enabled ? GitHubDefaults.AuthenticationScheme : CookieAuthenticationDefaults.AuthenticationScheme); + } } } diff --git a/src/admin/Bootstrap.Admin/Controllers/Api/InterfaceController.cs b/src/admin/Bootstrap.Admin/Controllers/Api/InterfaceController.cs index 3ea2f63a..981c4150 100644 --- a/src/admin/Bootstrap.Admin/Controllers/Api/InterfaceController.cs +++ b/src/admin/Bootstrap.Admin/Controllers/Api/InterfaceController.cs @@ -3,6 +3,7 @@ using Bootstrap.Security; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; +using System.Security.Principal; namespace Bootstrap.Admin.Controllers { @@ -48,7 +49,7 @@ namespace Bootstrap.Admin.Controllers [HttpPost] public BootstrapUser RetrieveUserByUserName([FromBody]string userName) { - return UserHelper.RetrieveUserByUserName(userName); + return UserHelper.RetrieveUserByUserName(new GenericIdentity(userName)); } /// /// diff --git a/src/admin/Bootstrap.Admin/Controllers/Api/RegisterController.cs b/src/admin/Bootstrap.Admin/Controllers/Api/RegisterController.cs index 70974075..ebe31122 100644 --- a/src/admin/Bootstrap.Admin/Controllers/Api/RegisterController.cs +++ b/src/admin/Bootstrap.Admin/Controllers/Api/RegisterController.cs @@ -1,9 +1,10 @@ -using Bootstrap.DataAccess; +using Bootstrap.DataAccess; using Longbow.Web.SignalR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using System.Linq; +using System.Security.Principal; using System.Threading.Tasks; namespace Bootstrap.Admin.Controllers.Api @@ -24,7 +25,7 @@ namespace Bootstrap.Admin.Controllers.Api [HttpGet] public bool Get(string userName) { - return UserHelper.RetrieveUserByUserName(userName) == null && !UserHelper.RetrieveNewUsers().Any(u => u.UserName == userName); + return UserHelper.RetrieveUserByUserName(new GenericIdentity(userName)) == null && !UserHelper.RetrieveNewUsers().Any(u => u.UserName == userName); } /// @@ -57,7 +58,7 @@ namespace Bootstrap.Admin.Controllers.Api [HttpPut] public bool Put([FromBody]ResetUser user) { - if (UserHelper.RetrieveUserByUserName(user.UserName) == null) return true; + if (UserHelper.RetrieveUserByUserName(new GenericIdentity(user.UserName)) == null) return true; return UserHelper.ForgotPassword(user); } } diff --git a/src/admin/Bootstrap.Admin/Controllers/HomeController.cs b/src/admin/Bootstrap.Admin/Controllers/HomeController.cs index e2f78e81..ae9b5843 100644 --- a/src/admin/Bootstrap.Admin/Controllers/HomeController.cs +++ b/src/admin/Bootstrap.Admin/Controllers/HomeController.cs @@ -18,6 +18,7 @@ namespace Bootstrap.Admin.Controllers public IActionResult Index() { var model = new HeaderBarModel(User.Identity); + if (string.IsNullOrEmpty(model.UserName)) return Redirect(Request.PathBase + CookieAuthenticationDefaults.LogoutPath); var url = DictHelper.RetrieveHomeUrl(model.AppCode); return url.Equals("~/Home/Index", System.StringComparison.OrdinalIgnoreCase) ? (IActionResult)View(model) : Redirect(url); } @@ -39,4 +40,4 @@ namespace Bootstrap.Admin.Controllers return View(model); } } -} \ No newline at end of file +} diff --git a/src/admin/Bootstrap.Admin/HealthChecks/DBHealthCheck.cs b/src/admin/Bootstrap.Admin/HealthChecks/DBHealthCheck.cs index 069a32e7..16354921 100644 --- a/src/admin/Bootstrap.Admin/HealthChecks/DBHealthCheck.cs +++ b/src/admin/Bootstrap.Admin/HealthChecks/DBHealthCheck.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using System; using System.Collections.Generic; using System.Linq; +using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using Task = System.Threading.Tasks.Task; @@ -68,7 +69,7 @@ namespace Bootstrap.Admin.HealthChecks try { DbContextManager.Exception = null; - var user = UserHelper.RetrieveUserByUserName(userName); + var user = UserHelper.RetrieveUserByUserName(new GenericIdentity(userName)); displayName = user?.DisplayName; roles = string.Join(",", RoleHelper.RetrievesByUserName(userName) ?? new string[0]); menusCount = MenuHelper.RetrieveMenusByUserName(userName)?.Count() ?? 0; diff --git a/src/admin/Bootstrap.Admin/Models/HeaderBarModel.cs b/src/admin/Bootstrap.Admin/Models/HeaderBarModel.cs index 82543c32..aeab1773 100644 --- a/src/admin/Bootstrap.Admin/Models/HeaderBarModel.cs +++ b/src/admin/Bootstrap.Admin/Models/HeaderBarModel.cs @@ -1,4 +1,5 @@ using Bootstrap.DataAccess; +using System; using System.Security.Principal; namespace Bootstrap.Admin.Models @@ -14,13 +15,16 @@ namespace Bootstrap.Admin.Models /// public HeaderBarModel(IIdentity identity) { - var user = UserHelper.RetrieveUserByUserName(identity.Name); - Icon = string.Format("{0}{1}", DictHelper.RetrieveIconFolderPath(), user.Icon); - DisplayName = user.DisplayName; - UserName = user.UserName; - AppCode = user.App; - Css = user.Css; - ActiveCss = string.IsNullOrEmpty(Css) ? Theme : Css; + var user = UserHelper.RetrieveUserByUserName(identity); + if (user != null) + { + Icon = user.Icon.Contains("://", StringComparison.OrdinalIgnoreCase) ? user.Icon : string.Format("{0}{1}", DictHelper.RetrieveIconFolderPath(), user.Icon); + DisplayName = user.DisplayName; + UserName = user.UserName; + AppCode = user.App; + Css = user.Css; + ActiveCss = string.IsNullOrEmpty(Css) ? Theme : Css; + } } /// @@ -53,4 +57,4 @@ namespace Bootstrap.Admin.Models /// public string ActiveCss { get; } } -} \ No newline at end of file +} diff --git a/src/admin/Bootstrap.Admin/Models/LoginModel.cs b/src/admin/Bootstrap.Admin/Models/LoginModel.cs index 1ee1a54f..2db09765 100644 --- a/src/admin/Bootstrap.Admin/Models/LoginModel.cs +++ b/src/admin/Bootstrap.Admin/Models/LoginModel.cs @@ -3,12 +3,12 @@ namespace Bootstrap.Admin.Models { /// - /// + /// 登陆页面 Model /// public class LoginModel : ModelBase { /// - /// + /// 默认构造函数 /// public LoginModel() { @@ -21,7 +21,7 @@ namespace Bootstrap.Admin.Models public string ImageLibUrl { get; protected set; } /// - /// 是否登录认证失败 + /// 是否登录认证失败 为真时客户端弹出滑块验证码 /// public bool AuthFailed { get; set; } } diff --git a/src/admin/Bootstrap.Admin/Models/ProfilesModel.cs b/src/admin/Bootstrap.Admin/Models/ProfilesModel.cs index 7b9b754b..4470c3e5 100644 --- a/src/admin/Bootstrap.Admin/Models/ProfilesModel.cs +++ b/src/admin/Bootstrap.Admin/Models/ProfilesModel.cs @@ -20,6 +20,11 @@ namespace Bootstrap.Admin.Models /// public string FileName { get; } + /// + /// 获得 是否为第三方用户 + /// + public bool External { get; } + /// /// /// @@ -39,6 +44,8 @@ namespace Bootstrap.Admin.Models FileName = Path.GetFileName(fileName); } } + + if (controller.User.Identity.AuthenticationType != Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme) External = true; } } -} \ No newline at end of file +} diff --git a/src/admin/Bootstrap.Admin/Startup.cs b/src/admin/Bootstrap.Admin/Startup.cs index dda51b60..e5432b16 100644 --- a/src/admin/Bootstrap.Admin/Startup.cs +++ b/src/admin/Bootstrap.Admin/Startup.cs @@ -1,4 +1,5 @@ using Bootstrap.DataAccess; +using Longbow.GiteeAuth; using Longbow.Web; using Longbow.Web.SignalR; using Microsoft.AspNetCore.Builder; @@ -60,7 +61,7 @@ namespace Bootstrap.Admin services.AddSignalR().AddJsonProtocalDefault(); services.AddSignalRExceptionFilterHandler((client, ex) => client.SendMessageBody(ex).ConfigureAwait(false)); services.AddResponseCompression(); - services.AddBootstrapAdminAuthentication(); + services.AddBootstrapAdminAuthentication().AddGitee(OAuthHelper.Configure).AddGitHub(OAuthHelper.Configure); services.AddSwagger(); services.AddButtonAuthorization(MenuHelper.AuthorizateButtons); services.AddBootstrapAdminBackgroundTask(); diff --git a/src/admin/Bootstrap.Admin/Views/Account/Login.cshtml b/src/admin/Bootstrap.Admin/Views/Account/Login.cshtml index b5b83eaf..03a81f35 100644 --- a/src/admin/Bootstrap.Admin/Views/Account/Login.cshtml +++ b/src/admin/Bootstrap.Admin/Views/Account/Login.cshtml @@ -43,10 +43,10 @@ }
-
- +
@@ -72,11 +72,43 @@ 记住密码自动登录
- + + +
请完成安全验证 diff --git a/src/admin/Bootstrap.Admin/Views/Admin/Profiles.cshtml b/src/admin/Bootstrap.Admin/Views/Admin/Profiles.cshtml index 7ade48a2..20d02f35 100644 --- a/src/admin/Bootstrap.Admin/Views/Admin/Profiles.cshtml +++ b/src/admin/Bootstrap.Admin/Views/Admin/Profiles.cshtml @@ -53,7 +53,7 @@
-
+
修改密码
/// /// - public static IEnumerable RetrievesByUserName(string userName) => CacheManager.GetOrAdd($"{DbHelper.RetrieveAppsByUserNameDataKey}-{userName}", key => DbContextManager.Create().RetrievesByUserName(userName), DbHelper.RetrieveAppsByUserNameDataKey); + public static IEnumerable RetrievesByUserName(string userName) => CacheManager.GetOrAdd($"{DbHelper.RetrieveAppsByUserNameDataKey}-{userName}", key => DbContextManager.Create().RetrievesByUserName(userName), RetrieveAppsByUserNameDataKey); } } diff --git a/src/admin/Bootstrap.DataAccess/Helper/OAuthHelper.cs b/src/admin/Bootstrap.DataAccess/Helper/OAuthHelper.cs new file mode 100644 index 00000000..072565be --- /dev/null +++ b/src/admin/Bootstrap.DataAccess/Helper/OAuthHelper.cs @@ -0,0 +1,87 @@ +using Bootstrap.Security; +using Longbow.Configuration; +using Longbow.OAuth; +using Longbow.Security.Cryptography; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; + +namespace Bootstrap.DataAccess +{ + /// + /// Gitee 授权帮助类 + /// + public static class OAuthHelper + { + private static readonly ConcurrentDictionary _pool = new ConcurrentDictionary(); + + /// + /// 设置 GiteeOptions.Events.OnCreatingTicket 方法 + /// + /// + public static void Configure(TOptions option) where TOptions : LgbOAuthOptions + { + option.Events.OnCreatingTicket = async context => + { + var user = context.User.ToObject(); + user.Schema = context.Scheme.Name; + _pool.AddOrUpdate(user.Login, userName => user, (userName, u) => { u = user; return user; }); + + // call webhook + var config = context.HttpContext.RequestServices.GetRequiredService(); + var webhookUrl = config.GetValue($"{option.GetType().Name}:StarredUrl", ""); + if (!string.IsNullOrEmpty(webhookUrl)) + { + var webhookParameters = new Dictionary() + { + { "access_token", context.AccessToken } + }; + var url = QueryHelpers.AddQueryString(webhookUrl, webhookParameters); + var requestMessage = new HttpRequestMessage(HttpMethod.Put, url); + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + await context.Backchannel.SendAsync(requestMessage, context.HttpContext.RequestAborted); + } + }; + } + + /// + /// 插入 Gitee 授权用户到数据库中 + /// + /// + /// + public static BootstrapUser RetrieveUserByUserName(string userName) where TOptions : LgbOAuthOptions + { + User ret = null; + var user = _pool.TryGetValue(userName, out var giteeUser) ? giteeUser : null; + if (user != null) + { + var option = ConfigurationManager.Get(); + ret = new User() + { + ApprovedBy = "OAuth", + ApprovedTime = DateTime.Now, + DisplayName = user.Name, + UserName = user.Login, + Password = LgbCryptography.GenerateSalt(), + Icon = user.Avatar_Url, + Description = $"{user.Schema}({user.Id})", + App = option.App + }; + DbContextManager.Create().Save(ret); + CacheCleanUtility.ClearCache(cacheKey: UserHelper.RetrieveUsersDataKey); + + // 根据配置文件设置默认角色 + var usr = UserHelper.Retrieves().First(u => u.UserName == userName); + var roles = RoleHelper.Retrieves().Where(r => option.Roles.Any(rl => rl.Equals(r.RoleName, StringComparison.OrdinalIgnoreCase))).Select(r => r.Id); + RoleHelper.SaveByUserId(usr.Id, roles); + } + return ret; + } + } +} diff --git a/src/admin/Bootstrap.DataAccess/Helper/TraceHelper.cs b/src/admin/Bootstrap.DataAccess/Helper/TraceHelper.cs index 182252ae..ab31ee04 100644 --- a/src/admin/Bootstrap.DataAccess/Helper/TraceHelper.cs +++ b/src/admin/Bootstrap.DataAccess/Helper/TraceHelper.cs @@ -22,7 +22,7 @@ namespace Bootstrap.DataAccess { if (context.User.Identity.IsAuthenticated) { - var user = UserHelper.RetrieveUserByUserName(context.User.Identity.Name); + var user = UserHelper.RetrieveUserByUserName(context.User.Identity); // user == null 以前登录过客户端保留了 Cookie 但是用户名可能被系统删除 // link bug: https://gitee.com/LongbowEnterprise/BootstrapAdmin/issues/I123MH diff --git a/src/admin/Bootstrap.DataAccess/Helper/UserHelper.cs b/src/admin/Bootstrap.DataAccess/Helper/UserHelper.cs index a1eb6605..20766aa6 100644 --- a/src/admin/Bootstrap.DataAccess/Helper/UserHelper.cs +++ b/src/admin/Bootstrap.DataAccess/Helper/UserHelper.cs @@ -1,9 +1,12 @@ using Bootstrap.Security; using Bootstrap.Security.DataAccess; using Longbow.Cache; +using Longbow.GiteeAuth; +using Longbow.GitHubAuth; using System; using System.Collections.Generic; using System.Linq; +using System.Security.Principal; using System.Text.RegularExpressions; namespace Bootstrap.DataAccess @@ -95,7 +98,7 @@ namespace Bootstrap.DataAccess value = value.Where(v => !admins.Any(u => u.Id == v)); if (!value.Any()) return true; var ret = DbContextManager.Create().Delete(value); - if (ret) CacheCleanUtility.ClearCache(userIds: value); + if (ret) CacheCleanUtility.ClearCache(userIds: value, cacheKey: RetrieveUsersByNameDataKey + "*"); return ret; } @@ -307,9 +310,30 @@ namespace Bootstrap.DataAccess /// /// 通过登录名获取登录用户方法 /// - /// + /// /// - public static BootstrapUser RetrieveUserByUserName(string userName) => CacheManager.GetOrAdd(string.Format("{0}-{1}", RetrieveUsersByNameDataKey, userName), k => DbContextManager.Create()?.RetrieveUserByUserName(userName), RetrieveUsersByNameDataKey); + public static BootstrapUser RetrieveUserByUserName(IIdentity identity) => CacheManager.GetOrAdd(string.Format("{0}-{1}", RetrieveUsersByNameDataKey, identity.Name), k => + { + var userName = identity.Name; + var proxyList = new List>(); + + // 本地数据库认证 + proxyList.Add(DbContextManager.Create().RetrieveUserByUserName); + + // Gitee 认证 + if (identity.AuthenticationType == GiteeDefaults.AuthenticationScheme) proxyList.Add(OAuthHelper.RetrieveUserByUserName); + + // GitHub 认证 + if (identity.AuthenticationType == GitHubDefaults.AuthenticationScheme) proxyList.Add(OAuthHelper.RetrieveUserByUserName); + + BootstrapUser user = null; + foreach (var p in proxyList) + { + user = p.Invoke(userName); + if (user != null) break; + } + return user; + }, RetrieveUsersByNameDataKey); /// /// 通过登录账号获得用户信息 diff --git a/src/admin/Bootstrap.DataAccess/User.cs b/src/admin/Bootstrap.DataAccess/User.cs index 13e5a496..6aec30e5 100644 --- a/src/admin/Bootstrap.DataAccess/User.cs +++ b/src/admin/Bootstrap.DataAccess/User.cs @@ -204,12 +204,12 @@ namespace Bootstrap.DataAccess /// public virtual bool Save(User user) { - var ret = false; user.PassSalt = LgbCryptography.GenerateSalt(); user.Password = LgbCryptography.ComputeHash(user.Password, user.PassSalt); user.RegisterTime = DateTime.Now; var db = DbManager.Create(); + bool ret; try { db.BeginTransaction(); diff --git a/src/client/Bootstrap.Client/Bootstrap.Client.csproj b/src/client/Bootstrap.Client/Bootstrap.Client.csproj index 44232d7c..29e182bc 100644 --- a/src/client/Bootstrap.Client/Bootstrap.Client.csproj +++ b/src/client/Bootstrap.Client/Bootstrap.Client.csproj @@ -5,7 +5,7 @@ - + diff --git a/src/client/Bootstrap.Client/Models/HeaderBarModel.cs b/src/client/Bootstrap.Client/Models/HeaderBarModel.cs index 87896541..4873c4ee 100644 --- a/src/client/Bootstrap.Client/Models/HeaderBarModel.cs +++ b/src/client/Bootstrap.Client/Models/HeaderBarModel.cs @@ -33,7 +33,7 @@ namespace Bootstrap.Client.Models // set Icon var icon = $"/{DictHelper.RetrieveIconFolderPath().Trim('~', '/')}/{user.Icon}"; - Icon = string.IsNullOrEmpty(ConfigurationManager.GetValue("SimulateUserName", string.Empty)) ? $"{authHost.TrimEnd('/')}{icon}" : "/images/admin.jpg"; + Icon = user.Icon.Contains("://", StringComparison.OrdinalIgnoreCase) ? user.Icon : (string.IsNullOrEmpty(ConfigurationManager.GetValue("SimulateUserName", string.Empty)) ? $"{authHost.TrimEnd('/')}{icon}" : "/images/admin.jpg"); if (!string.IsNullOrEmpty(user.Css)) Theme = user.Css; } diff --git a/src/client/Bootstrap.Client/Startup.cs b/src/client/Bootstrap.Client/Startup.cs index 5d1276e8..d7671a4a 100644 --- a/src/client/Bootstrap.Client/Startup.cs +++ b/src/client/Bootstrap.Client/Startup.cs @@ -1,8 +1,6 @@ using Bootstrap.Client.DataAccess; -using Longbow.Configuration; using Longbow.Web; using Longbow.Web.SignalR; -using Microsoft.AspNetCore; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -13,7 +11,6 @@ using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System; -using System.Linq; namespace Bootstrap.Client { diff --git a/src/client/Bootstrap.Client/Views/Home/Index.cshtml b/src/client/Bootstrap.Client/Views/Home/Index.cshtml index 68ff055c..eec79ad8 100644 --- a/src/client/Bootstrap.Client/Views/Home/Index.cshtml +++ b/src/client/Bootstrap.Client/Views/Home/Index.cshtml @@ -10,8 +10,14 @@ }

这是开源后台管理框架前台系统首页,欢迎使用

-

点击右上角登录信息下拉菜单中的设置按钮进入 后台管理 或者 直接进入

-

由于本系统为演示系统,内部对一些敏感操作进行了限制操作,如一些特殊用户不能删除

+

点击右上角进入 后台管理 或者 直接进入

+

系统密码

+

+

    +
  1. 管理账号 Admin/123789
  2. +
  3. 普通账号 User/123789
  4. +
+

最新功能更新请查看 更新日志

diff --git a/src/client/Bootstrap.Client/Views/Shared/Footer.cshtml b/src/client/Bootstrap.Client/Views/Shared/Footer.cshtml index 673ad7ad..7b8a5232 100644 --- a/src/client/Bootstrap.Client/Views/Shared/Footer.cshtml +++ b/src/client/Bootstrap.Client/Views/Shared/Footer.cshtml @@ -1,6 +1,6 @@ @model ModelBase