diff --git a/ThuInfoWeb/Bots/FeedbackNoticeBot.cs b/ThuInfoWeb/Bots/FeedbackNoticeBot.cs index 755e4b4..b204d57 100644 --- a/ThuInfoWeb/Bots/FeedbackNoticeBot.cs +++ b/ThuInfoWeb/Bots/FeedbackNoticeBot.cs @@ -1,67 +1,50 @@ -using System.Buffers.Text; -using System.Security.Cryptography; +using System.Security.Cryptography; using System.Text; using System.Text.Json; -namespace ThuInfoWeb.Bots +namespace ThuInfoWeb.Bots; + +public class FeedbackNoticeBot(IConfiguration configuration) { - public class FeedbackNoticeBot - { - private readonly string _url; - private readonly string _secret; - private readonly HttpClient _httpClient; - private readonly bool _internalNetworkMode; + private readonly HttpClient _httpClient = new(); + private readonly bool _internalNetworkMode = bool.Parse(configuration["InternalNetworkMode"] ?? "false"); + private readonly string _secret = configuration["FeishuBots:FeedbackNoticeBot:Secret"] ?? ""; + private readonly string _url = configuration["FeishuBots:FeedbackNoticeBot:Url"] ?? ""; - public FeedbackNoticeBot(IConfiguration configuration) - { - this._url = configuration["FeishuBots:FeedbackNoticeBot:Url"]; - this._secret = configuration["FeishuBots:FeedbackNoticeBot:Secret"]; - this._internalNetworkMode = bool.Parse(configuration["InternalNetworkMode"]); - this._httpClient = new HttpClient(); - } + private string GetSign(long timestamp) + { + var str = $"{timestamp}\n{_secret}"; + using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(str)); + var code = hmac.ComputeHash([]); + var sign = Convert.ToBase64String(code); + return sign; + } - private string GetSign(long timestamp) + public async Task PushNoticeAsync(string content) + { + if (_internalNetworkMode) { - var str = $"{timestamp}\n{_secret}"; - using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(str)); - var code = hmac.ComputeHash(new byte[0]); - var sign = Convert.ToBase64String(code); - return sign; + var resp = await _httpClient.PostAsync("https://stu.cs.tsinghua.edu.cn/thuinfo/botnotice", + JsonContent.Create(new { Content = content, Secret = _secret })); + resp.EnsureSuccessStatusCode(); } - - public async Task PushNoticeAsync(string content) + else { - if (_internalNetworkMode) - { - var resp = await _httpClient.PostAsync("https://stu.cs.tsinghua.edu.cn/thuinfo/botnotice", JsonContent.Create(new - { - Content = content, - Secret = _secret - })); - resp.EnsureSuccessStatusCode(); - } - else - { - var ts = DateTimeOffset.Now.ToUnixTimeSeconds(); - var resp = await _httpClient.PostAsync(_url, JsonContent.Create(new + var ts = DateTimeOffset.Now.ToUnixTimeSeconds(); + var resp = await _httpClient.PostAsync(_url, + JsonContent.Create(new { timestamp = ts.ToString(), sign = GetSign(ts), msg_type = "text", - content = new - { - text = content - } + content = new { text = content } })); - var json = await resp.Content.ReadAsStringAsync(); - var parsed = JsonDocument.Parse(json); - if (!parsed.RootElement.TryGetProperty("StatusCode", out var code)) - throw new Exception("Send error"); - else if (code.GetInt32() != 0) - throw new Exception("Send error"); - else - return; - } + var json = await resp.Content.ReadAsStringAsync(); + var parsed = JsonDocument.Parse(json); + if (!parsed.RootElement.TryGetProperty("StatusCode", out var code)) + throw new Exception("Send error"); + if (code.GetInt32() != 0) + throw new Exception("Send error"); } } -} \ No newline at end of file +} diff --git a/ThuInfoWeb/Controllers/ApiController.cs b/ThuInfoWeb/Controllers/ApiController.cs index d5b864e..7bfb8ee 100644 --- a/ThuInfoWeb/Controllers/ApiController.cs +++ b/ThuInfoWeb/Controllers/ApiController.cs @@ -1,155 +1,149 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using ThuInfoWeb.Bots; using ThuInfoWeb.DBModels; using ThuInfoWeb.Dtos; -namespace ThuInfoWeb.Controllers +namespace ThuInfoWeb.Controllers; + +/// +/// The controller for RESTApi +/// +[Route("[controller]")] +[ApiController] +public class ApiController(Data data, VersionManager versionManager, FeedbackNoticeBot feedbackNoticeBot) + : ControllerBase { /// - /// The controller for RESTApi + /// Get announce, get the latest announce simply by no query string(just get /api/announce). If needed, you should only + /// enter id or page at one time. /// - [Route("[controller]")] - [ApiController] - public class ApiController : ControllerBase + /// if entered, this will return single value + /// if entered, this will return up to 5 values in an array. + /// In json format. + [Route("Announce")] + public async Task Announce([FromQuery] int? id, [FromQuery] int? page) { - private readonly Data _data; - private readonly VersionManager _versionManager; - private readonly FeedbackNoticeBot _feedbackNoticeBot; - - public ApiController(Data data, VersionManager versionManager, FeedbackNoticeBot feedbackNoticeBot) + if (page <= 0) + return BadRequest("page必须是正整数"); + if (page is not null) { - this._data = data; - this._versionManager = versionManager; - this._feedbackNoticeBot = feedbackNoticeBot; + var a = await data.GetActiveAnnouncesAsync((int)page, 5); + return Ok(a); } - - /// - /// Get announce, get the latest announce simply by no query string(just get /api/announce). If needed, you should only enter id or page at one time. - /// - /// if entered, this will return single value - /// if entered, this will return up to 5 values in an array. - /// In json format. - [Route("Announce")] - public async Task Announce([FromQuery] int? id, [FromQuery] int? page) + else { - if (page is not null && page <= 0) - return BadRequest("page必须是正整数"); - if (page is not null) - { - var a = await _data.GetActiveAnnouncesAsync(page ?? 1, 5); - return Ok(a); - } - else - { - var a = await _data.GetActiveAnnounceAsync(id); - return Ok(a); - } + var a = await data.GetActiveAnnounceAsync(id); + return Ok(a); } + } - /// - /// Create a feedback - /// - /// a json, has content, appversion, os, nickname(optional) - /// - [Route("Feedback"), HttpPost] - public async Task Feedback(FeedbackDto dto) + /// + /// Create a feedback + /// + /// a json, has content, appversion, os, nickname(optional) + /// + [Route("Feedback")] + [HttpPost] + public async Task Feedback(FeedbackDto dto) + { + var feedback = new Feedback { - var feedback = new Feedback() - { - AppVersion = dto.AppVersion, - Content = dto.Content, - CreatedTime = DateTime.Now, - OS = dto.OS.ToLower(), - Contact = dto.Contact, - PhoneModel = dto.PhoneModel - }; - var result = await _data.CreateFeedbackAsync(feedback); - if (result != 1) return BadRequest(); - else - { - _ = _feedbackNoticeBot.PushNoticeAsync( - $"收到新反馈\n{dto.Content}\n请前往http://app.cs.tsinghua.edu.cn/Home/Feedback回复"); - return Created("Api/Feedback", null); - } - } - - [Route("RepliedFeedback")] - public async Task RepliedFeedback() + AppVersion = dto.AppVersion, + Content = dto.Content, + CreatedTime = DateTime.Now, + OS = dto.OS.ToLower(), + Contact = dto.Contact, + PhoneModel = dto.PhoneModel + }; + var result = await data.CreateFeedbackAsync(feedback); + if (result != 1) { - return Ok((await _data.GetAllRepliedFeedbacksAsync()) - .Select(x => new - { - content = x.Content, - reply = x.Reply, - replierName = x.ReplierName ?? "", - repliedTime = x.RepliedTime - }).ToList()); + return BadRequest(); } - /// - /// Get the url content of Wechat group QRCode. - /// - /// The url string, NOT in json format. - [Route("QRCode")] - public async Task QRCode() - { - return Ok((await _data.GetMiscAsync()).QrCodeContent); - } + _ = feedbackNoticeBot.PushNoticeAsync( + $"收到新反馈\n{dto.Content}\n请前往http://app.cs.tsinghua.edu.cn/Home/Feedback回复"); + return Created("Api/Feedback", null); + } - /// - /// Redirect to the url ok APK. - /// - /// - [Route("Apk")] - public async Task Apk() - { - // when start for the first time, if the apkurl is null or empty, this will generate an exception, so set an apkurl value as soon as possible. - return Redirect((await _data.GetMiscAsync())?.ApkUrl); - } + [Route("RepliedFeedback")] + public async Task RepliedFeedback() + { + return Ok((await data.GetAllRepliedFeedbacksAsync()) + .Select(x => new + { + content = x.Content, reply = x.Reply, replierName = x.ReplierName, repliedTime = x.RepliedTime + }).ToList()); + } - [Route("Socket")] - public async Task Socket([FromQuery] int? sectionId) + /// + /// Get the url content of Wechat group QRCode. + /// + /// The url string, NOT in json format. + [Route("QRCode")] + public async Task QRCode() + { + return Ok((await data.GetMiscAsync())?.QrCodeContent ?? ""); + } + + /// + /// Redirect to the url ok APK. + /// + /// + [Route("Apk")] + public async Task Apk() + { + // when start for the first time, if the apkurl is null or empty, this will generate an exception, so set an apkurl value as soon as possible. + return Redirect((await data.GetMiscAsync())?.ApkUrl ?? ""); + } + + [Route("Socket")] + public async Task Socket([FromQuery] int? sectionId) + { + if (sectionId is null) + return Ok(new List()); + + return Ok((await data.GetSocketsAsync((int)sectionId)).Select(x => new SocketDto { - if (sectionId is null) - return Ok(new List()); + CreatedTime = x.CreatedTime, + SeatId = x.SeatId, + SectionId = x.SectionId, + UpdatedTime = x.UpdatedTime, + Status = Parse(x.Status) + }).ToList()); - static string parse(Socket.SocketStatus status) => status switch + static string Parse(Socket.SocketStatus status) + { + return status switch { DBModels.Socket.SocketStatus.Available => "available", DBModels.Socket.SocketStatus.Unavailable => "unavailable", - DBModels.Socket.SocketStatus.Unknown => "unknown" + _ => "unknown" }; - - return Ok((await _data.GetSocketsAsync(sectionId ?? 0)).Select(x => new SocketDto() - { - CreatedTime = x.CreatedTime, - SeatId = x.SeatId, - SectionId = x.SectionId, - UpdatedTime = x.UpdatedTime, - Status = parse(x.Status) - }).ToList()); } + } - [HttpPost, Route("Socket")] - public async Task Socket(SocketDto dto) - { - var result = await _data.UpdateSocketAsync(dto.SeatId ?? 0, dto.IsAvailable ?? false); - if (result != 1) return BadRequest(); - else return Ok(); - } + [HttpPost] + [Route("Socket")] + public async Task Socket(SocketDto dto) + { + var result = await data.UpdateSocketAsync(dto.SeatId ?? 0, dto.IsAvailable ?? false); + if (result != 1) + return BadRequest(); + return Ok(); + } - [Route("Version/{os}")] - public IActionResult Version([FromRoute] string os) - { - if (os.ToLower() == "android") return Ok(_versionManager.GetCurrentVersion(VersionManager.OS.Android)); - else return Ok(_versionManager.GetCurrentVersion(VersionManager.OS.IOS)); - } + [Route("Version/{os}")] + public IActionResult Version([FromRoute] string os) + { + return Ok(os.Equals("android", StringComparison.CurrentCultureIgnoreCase) + ? versionManager.GetCurrentVersion(VersionManager.OS.Android) + : versionManager.GetCurrentVersion(VersionManager.OS.IOS)); + } - [Route("CardIVersion")] - public async Task CardIVersion() - { - return Ok(new { Version = (await _data.GetMiscAsync()).CardIVersion }); - } + [Route("CardIVersion")] + public async Task CardIVersion() + { + return Ok(new { Version = (await data.GetMiscAsync())?.CardIVersion ?? -1 }); } -} \ No newline at end of file +} diff --git a/ThuInfoWeb/Controllers/HomeController.cs b/ThuInfoWeb/Controllers/HomeController.cs index 09a8653..3fb4ca6 100644 --- a/ThuInfoWeb/Controllers/HomeController.cs +++ b/ThuInfoWeb/Controllers/HomeController.cs @@ -1,286 +1,299 @@ -using Microsoft.AspNetCore.Authorization; +using System.Diagnostics; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using System.Diagnostics; -using System.Text.RegularExpressions; using ThuInfoWeb.DBModels; using ThuInfoWeb.Models; -namespace ThuInfoWeb.Controllers +namespace ThuInfoWeb.Controllers; + +public class HomeController(ILogger logger, Data data, UserManager userManager, + VersionManager versionManager) + : Controller { - public partial class HomeController : Controller + private readonly ILogger _logger = logger; + + public IActionResult Register() { - private readonly ILogger _logger; - private readonly Data _data; - private readonly UserManager _userManager; - private readonly VersionManager _versionManager; + return View(); + } - [GeneratedRegex(@"^\d+\.\d+\.\d+$")] - private static partial Regex VersionRegex(); + [HttpPost] + public IActionResult Register(RegisterViewModel vm) + { + if (!ModelState.IsValid) + return View(vm); - public HomeController(ILogger logger, Data data, UserManager userManager, VersionManager versionManager) - { - _logger = logger; - _data = data; - _userManager = userManager; - _versionManager = versionManager; - } - public IActionResult Register() - { - return View(); - } - [HttpPost] - public async Task Register(RegisterViewModel vm) - { - if (!ModelState.IsValid) return View(vm); + // Prohibit registration + ModelState.AddModelError(nameof(vm.Name), "禁止注册新用户"); + return View(vm); - // Prohibit registration - ModelState.AddModelError(nameof(vm.Name), "禁止注册新用户"); - return View(vm); + // if (await _data.CheckUserAsync(vm.Name)) + // { + // ModelState.AddModelError(nameof(vm.Name), "用户名已被注册"); + // return View(vm); + // } + // var user = new User() + // { + // Name = vm.Name, + // PasswordHash = vm.Password.ToSHA256Hex(), + // CreatedTime = DateTime.Now, + // IsAdmin = false + // }; + // var result = await _data.CreateUserAsync(user); + // if (result == 1) + // { + // await _userManager.DoLoginAsync(vm.Name, false); + // return RedirectToAction("Index"); + // } + // else + // { + // ModelState.AddModelError(nameof(vm.Name), "发生未知错误"); + // return View(vm); + // } + } - // if (await _data.CheckUserAsync(vm.Name)) - // { - // ModelState.AddModelError(nameof(vm.Name), "用户名已被注册"); - // return View(vm); - // } - // var user = new User() - // { - // Name = vm.Name, - // PasswordHash = vm.Password.ToSHA256Hex(), - // CreatedTime = DateTime.Now, - // IsAdmin = false - // }; - // var result = await _data.CreateUserAsync(user); - // if (result == 1) - // { - // await _userManager.DoLoginAsync(vm.Name, false); - // return RedirectToAction("Index"); - // } - // else - // { - // ModelState.AddModelError(nameof(vm.Name), "发生未知错误"); - // return View(vm); - // } - } - public IActionResult Login() - { - return View(); - } + public IActionResult Login() + { + return View(); + } - [HttpPost] - public async Task Login(LoginViewModel vm) + [HttpPost] + public async Task Login(LoginViewModel vm) + { + if (!ModelState.IsValid) + return View(vm); + // get the user and check if the password is correct + var user = vm.Name != null ? await data.GetUserAsync(vm.Name) : null; + if (user is null || vm.Password?.ToSHA256Hex() != user.PasswordHash) { - if (!ModelState.IsValid) return View(vm); - // get the user and check if the password is correct - var user = await _data.GetUserAsync(vm.Name); - if (user is null || vm.Password.ToSHA256Hex() != user.PasswordHash) - { - ModelState.AddModelError(nameof(vm.Name), "用户名或密码错误"); - ModelState.AddModelError(nameof(vm.Password), "用户名或密码错误"); - return View(vm); - } - await _userManager.DoLoginAsync(user.Name, user.IsAdmin); - return RedirectToAction("Index"); + ModelState.AddModelError(nameof(vm.Name), "用户名或密码错误"); + ModelState.AddModelError(nameof(vm.Password), "用户名或密码错误"); + return View(vm); } - [Authorize(Roles = "admin,guest")] - public async Task Logout() - { - await _userManager.DoLogoutAsync(); - return RedirectToAction("Login"); - } - [Authorize(Roles = "admin,guest")] - public IActionResult ChangePassword() => View(); - [HttpPost, Authorize(Roles = "admin,guest")] - public async Task ChangePassword(ChangePasswordViewModel vm) - { - if (!ModelState.IsValid) return View(vm); - if (HttpContext.User.Identity.Name != vm.Name) BadRequest(); - if (vm.OldPassword.ToSHA256Hex() != (await _data.GetUserAsync(HttpContext.User.Identity.Name)).PasswordHash) - { - ModelState.AddModelError(nameof(vm.OldPassword), "旧密码错误"); - return View(vm); - } - var result = await _data.ChangeUserPasswordAsync(HttpContext.User.Identity.Name, vm.NewPassword.ToSHA256Hex()); - if (result != 1) - { - ModelState.AddModelError(nameof(vm.NewPassword), "发生未知错误"); - return View(vm); - } - else - { - await _userManager.DoLogoutAsync(); - return RedirectToAction(nameof(Login)); - } - } + await userManager.DoLoginAsync(user.Name, user.IsAdmin); + return RedirectToAction("Index"); + } - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] - public IActionResult Error() - { - return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); - } + [Authorize(Roles = "admin,guest")] + public async Task Logout() + { + await userManager.DoLogoutAsync(); + return RedirectToAction("Login"); + } + + [Authorize(Roles = "admin,guest")] + public IActionResult ChangePassword() + { + return View(); + } - [Authorize(Roles = "admin,guest")] - public IActionResult Index() + [HttpPost] + [Authorize(Roles = "admin,guest")] + public async Task ChangePassword(ChangePasswordViewModel vm) + { + if (!ModelState.IsValid) + return View(vm); + if (HttpContext.User.Identity!.Name != vm.Name) + BadRequest(); + if (vm.OldPassword?.ToSHA256Hex() != (await data.GetUserAsync(HttpContext.User.Identity!.Name!))!.PasswordHash) { - return View(); + ModelState.AddModelError(nameof(vm.OldPassword), "旧密码错误"); + return View(vm); } - [Authorize(Roles = "admin")] - public async Task Announce([FromQuery] int page = 1) + var result = await data.ChangeUserPasswordAsync(HttpContext.User.Identity.Name!, vm.NewPassword!.ToSHA256Hex()); + if (result != 1) { - ViewData["page"] = page; - var list = await _data.GetAnnouncesAsync(page, 10); - return View(list.Select(a => new AnnounceViewModel() - { - Id = a.Id, - Content = a.Content, - Title = a.Title, - Author = a.Author, - CreatedTime = a.CreatedTime, - IsActive = a.IsActive, - VisibleNotAfter = a.VisibleNotAfter, - VisibleExact = a.VisibleExact - }).ToList()); + ModelState.AddModelError(nameof(vm.NewPassword), "发生未知错误"); + return View(vm); } - [HttpPost, Authorize(Roles = "admin")] - public async Task CreateAnnounce(AnnounceViewModel vm) - { - if (vm.Title is null || vm.Content is null) return BadRequest("标题或内容为空"); - var visibleNotAfter = vm.VisibleNotAfter?.Trim() ?? "9.9.9"; - var visibleExact = vm.VisibleExact ?? ""; - - if (!VersionRegex().IsMatch(visibleNotAfter)) - { - return BadRequest("\"在不晚于以下版本生效\"中的版本号格式错误"); - } - - var visibleExactList = visibleExact.Split(',') - .Select(x => x.Trim()) - .Where(x => !string.IsNullOrWhiteSpace(x)) - .ToList(); - - if (visibleExactList.Any(x => !VersionRegex().IsMatch(x))) - { - return BadRequest("\"在以下版本生效\"中的版本号格式错误"); - } - - visibleExact = string.Join(',', visibleExactList); + await userManager.DoLogoutAsync(); + return RedirectToAction(nameof(Login)); + } - var user = HttpContext.User.Identity!.Name!; - var a = new Announce - { - Title = vm.Title, - Content = vm.Content, - Author = user, - CreatedTime = DateTime.Now, - IsActive = vm.IsActive, - VisibleNotAfter = visibleNotAfter, - VisibleExact = visibleExact - }; - var result = await _data.CreateAnnounceAsync(a); - if (result != 1) return BadRequest(ModelState); - else return CreatedAtAction(nameof(Announce), null); - } - [Authorize(Roles = "admin")] - public async Task ChangeAnnounceStatus([FromRoute] int id, [FromQuery] int returnpage) - { - var a = await _data.GetAnnounceAsync(id); - if (a is null) return BadRequest("找不到对应公告"); - var result = await _data.UpdateAnnounceStatusAsync(id, !a.IsActive); - if (result != 1) return BadRequest(); - else return RedirectToAction(nameof(Announce), new { page = returnpage == 0 ? 1 : returnpage }); - } - [Authorize(Roles = "admin")] - public async Task DeleteAnnounce([FromRoute] int id, [FromQuery] int returnpage) - { - var result = await _data.DeleteAnnounceAsync(id); - if (result != 1) return NoContent(); - else return RedirectToAction(nameof(Announce), new { page = returnpage }); - } - [Authorize(Roles = "admin")] - public async Task Feedback([FromQuery] int page = 1) - { - var list = (await _data.GetFeedbacksAsync(page, 10)).Select(x => - new FeedbackViewModel() - { - AppVersion = x.AppVersion, - Contact = x.Contact, - Content = x.Content, - CreatedTime = x.CreatedTime, - Id = x.Id, - OS = x.OS, - PhoneModel = x.PhoneModel, - Reply = x.Reply, - ReplyerName = x.ReplierName, - RepliedTime = x.RepliedTime - }).ToList(); - ViewData["page"] = page; - return View(list); - } - [Authorize(Roles = "admin")] - public async Task DeleteFeedback([FromRoute] int id, [FromQuery] int returnpage = 1) - { - var result = await _data.DeleteFeedbackAsync(id); - if (result != 1) return NoContent(); - else return RedirectToAction(nameof(Feedback), new { page = returnpage }); - } - [HttpPost, Authorize(Roles = "admin")] - public async Task ReplyFeedback([FromForm] int id, [FromForm] string reply) - { - if (string.IsNullOrWhiteSpace(reply)) - { - return BadRequest("回复不能为空"); - } - var user = HttpContext.User.Identity.Name; - var result = await _data.ReplyFeedbackAsync(id, reply, user); - if (result != 1) return BadRequest("未知错误"); - else return Ok(); - } - [Authorize(Roles = "admin")] - public async Task Misc() + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public IActionResult Error() + { + return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + } + + [Authorize(Roles = "admin,guest")] + public IActionResult Index() + { + return View(); + } + + [Authorize(Roles = "admin")] + public async Task Announce([FromQuery] int page = 1) + { + ViewData["page"] = page; + var list = await data.GetAnnouncesAsync(page, 10); + return View(list.Select(a => new AnnounceViewModel { - var misc = await _data.GetMiscAsync(); - return View(new MiscViewModel() - { - ApkUrl = misc.ApkUrl, - QrCodeContent = misc.QrCodeContent, - CardIVersion = misc.CardIVersion - }); - } - [HttpPost, Authorize(Roles = "admin")] - public async Task Misc(MiscViewModel vm) + Id = a.Id, + Content = a.Content, + Title = a.Title, + Author = a.Author, + CreatedTime = a.CreatedTime, + IsActive = a.IsActive, + VisibleNotAfter = a.VisibleNotAfter, + VisibleExact = a.VisibleExact + }).ToList()); + } + + [HttpPost] + [Authorize(Roles = "admin")] + public async Task CreateAnnounce(AnnounceViewModel vm) + { + if (vm.Title is null || vm.Content is null) + return BadRequest("标题或内容为空"); + var visibleNotAfter = vm.VisibleNotAfter?.Trim() ?? "9.9.9"; + var visibleExact = vm.VisibleExact ?? ""; + + if (!visibleNotAfter.IsValidVersionNumber()) + return BadRequest("\"在不晚于以下版本生效\"中的版本号格式错误"); + + var visibleExactList = visibleExact.Split(',') + .Select(x => x.Trim()) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .ToList(); + + if (visibleExactList.Any(x => !x.IsValidVersionNumber())) + return BadRequest("\"在以下版本生效\"中的版本号格式错误"); + + visibleExact = string.Join(',', visibleExactList); + + var user = HttpContext.User.Identity!.Name!; + var a = new Announce { - if (!ModelState.IsValid) return View(vm); - var misc = new Misc() + Title = vm.Title, + Content = vm.Content, + Author = user, + CreatedTime = DateTime.Now, + IsActive = vm.IsActive, + VisibleNotAfter = visibleNotAfter, + VisibleExact = visibleExact + }; + var result = await data.CreateAnnounceAsync(a); + if (result != 1) + return BadRequest(ModelState); + return CreatedAtAction(nameof(Announce), null); + } + + [Authorize(Roles = "admin")] + public async Task ChangeAnnounceStatus([FromRoute] int id, [FromQuery] int returnpage) + { + var a = await data.GetAnnounceAsync(id); + if (a is null) + return BadRequest("找不到对应公告"); + var result = await data.UpdateAnnounceStatusAsync(id, !a.IsActive); + if (result != 1) + return BadRequest(); + return RedirectToAction(nameof(Announce), new { page = returnpage == 0 ? 1 : returnpage }); + } + + [Authorize(Roles = "admin")] + public async Task DeleteAnnounce([FromRoute] int id, [FromQuery] int returnpage) + { + var result = await data.DeleteAnnounceAsync(id); + if (result != 1) + return NoContent(); + return RedirectToAction(nameof(Announce), new { page = returnpage }); + } + + [Authorize(Roles = "admin")] + public async Task Feedback([FromQuery] int page = 1) + { + var list = (await data.GetFeedbacksAsync(page, 10)).Select(x => + new FeedbackViewModel { - ApkUrl = vm.ApkUrl, - QrCodeContent = vm.QrCodeContent, - CardIVersion = vm.CardIVersion - }; - var result = await _data.UpdateMiscAsync(misc); - if (result != 1) return BadRequest(); - else return RedirectToAction(nameof(Misc)); - } - [Authorize(Roles = "admin"), Route("Home/CheckUpdate/{os}")] - public IActionResult CheckUpdate([FromRoute] string os) + AppVersion = x.AppVersion, + Contact = x.Contact, + Content = x.Content, + CreatedTime = x.CreatedTime, + Id = x.Id, + OS = x.OS, + PhoneModel = x.PhoneModel, + Reply = x.Reply, + ReplierName = x.ReplierName, + RepliedTime = x.RepliedTime + }).ToList(); + ViewData["page"] = page; + return View(list); + } + + [Authorize(Roles = "admin")] + public async Task DeleteFeedback([FromRoute] int id, [FromQuery] int returnpage = 1) + { + var result = await data.DeleteFeedbackAsync(id); + if (result != 1) + return NoContent(); + return RedirectToAction(nameof(Feedback), new { page = returnpage }); + } + + [HttpPost] + [Authorize(Roles = "admin")] + public async Task ReplyFeedback([FromForm] int id, [FromForm] string reply) + { + if (string.IsNullOrWhiteSpace(reply)) + return BadRequest("回复不能为空"); + var user = HttpContext.User.Identity!.Name!; + var result = await data.ReplyFeedbackAsync(id, reply, user); + if (result != 1) + return BadRequest("未知错误"); + return Ok(); + } + + [Authorize(Roles = "admin")] + public async Task Misc() + { + var misc = await data.GetMiscAsync() ?? new Misc(); + return View(new MiscViewModel { - if (!_versionManager.IsRunning) - _ = _versionManager.CheckUpdateAsync(os.ToLower() == "android" ? VersionManager.OS.Android : VersionManager.OS.IOS); - return RedirectToAction(nameof(Index)); - } - [Authorize(Roles = "admin")] - public IActionResult Stat() + ApkUrl = misc.ApkUrl, QrCodeContent = misc.QrCodeContent, CardIVersion = misc.CardIVersion + }); + } + + [HttpPost] + [Authorize(Roles = "admin")] + public async Task Misc(MiscViewModel vm) + { + if (!ModelState.IsValid) + return View(vm); + var misc = new Misc { - return View(); - } + ApkUrl = vm.ApkUrl ?? "", QrCodeContent = vm.QrCodeContent ?? "", CardIVersion = vm.CardIVersion + }; + var result = await data.UpdateMiscAsync(misc); + if (result != 1) + return BadRequest(); + return RedirectToAction(nameof(Misc)); + } + + [Authorize(Roles = "admin")] + [Route("Home/CheckUpdate/{os}")] + public IActionResult CheckUpdate([FromRoute] string os) + { + if (!versionManager.IsRunning) + _ = versionManager.CheckUpdateAsync(os.Equals("android", StringComparison.CurrentCultureIgnoreCase) + ? VersionManager.OS.Android + : VersionManager.OS.IOS); + return RedirectToAction(nameof(Index)); + } + + [Authorize(Roles = "admin")] + public IActionResult Stat() + { + return View(); + } #if DEBUG - [Route("Home/Exception")] - public IActionResult Exception() - { - throw new Exception("Generated exception in DEBUG build"); - } -#endif + [Route("Home/Exception")] + public IActionResult Exception() + { + throw new Exception("Generated exception in DEBUG build"); } -} \ No newline at end of file +#endif +} diff --git a/ThuInfoWeb/Controllers/StatController.cs b/ThuInfoWeb/Controllers/StatController.cs index b7f2ff8..6862d2e 100644 --- a/ThuInfoWeb/Controllers/StatController.cs +++ b/ThuInfoWeb/Controllers/StatController.cs @@ -2,61 +2,51 @@ using Microsoft.AspNetCore.Mvc; using ThuInfoWeb.DBModels; -namespace ThuInfoWeb.Controllers +namespace ThuInfoWeb.Controllers; + +[Route("[controller]/[action]")] +[ApiController] +public class StatController(Data data) : ControllerBase { - [Route("[controller]/[action]")] - [ApiController] - public class StatController : ControllerBase + [Route("{function:int}")] + public async Task Usage(int function) { - private readonly Data _data; - - public StatController(Data data) - { - _data = data; - } - - [Route("{function:int}")] - public async Task Usage(int function) - { - if (!Enum.IsDefined(typeof(Usage.FunctionType), function)) - return BadRequest("功能不存在"); - var usage = new Usage - { - Function = (Usage.FunctionType)function, - CreatedTime = DateTime.Now - }; - var result = await _data.CreateUsageAsync(usage); - if (result != 1) - return BadRequest(); - return Ok(); - } + if (!Enum.IsDefined(typeof(Usage.FunctionType), function)) + return BadRequest("功能不存在"); + var usage = new Usage { Function = (Usage.FunctionType)function, CreatedTime = DateTime.Now }; + var result = await data.CreateUsageAsync(usage); + if (result != 1) + return BadRequest(); + return Ok(); + } - [Route(""), Authorize(Roles = "admin")] - public async Task UsageData() - { - return Ok(await _data.GetUsageAsync()); - } + [Route("")] + [Authorize(Roles = "admin")] + public async Task UsageData() + { + return Ok(await data.GetUsageAsync()); + } - public async Task Startup() - { - var s = new Startup { CreatedTime = DateTime.Now }; - var result = await _data.CreateStartupAsync(s); - if (result != 1) - return BadRequest(); - return Ok(); - } + public async Task Startup() + { + var s = new Startup { CreatedTime = DateTime.Now }; + var result = await data.CreateStartupAsync(s); + if (result != 1) + return BadRequest(); + return Ok(); + } - [Route(""), Authorize(Roles = "admin")] - public async Task StartupData() - { - return Ok(await _data.GetStartupDataAsync()); - } + [Route("")] + [Authorize(Roles = "admin")] + public async Task StartupData() + { + return Ok(await data.GetStartupDataAsync()); + } #if DEBUG - public async Task GenStartupData() - { - await _data.GenStartupDataAsync(); - return Ok(); - } -#endif + public async Task GenStartupData() + { + await data.GenStartupDataAsync(); + return Ok(); } -} \ No newline at end of file +#endif +} diff --git a/ThuInfoWeb/DBModels/Announce.cs b/ThuInfoWeb/DBModels/Announce.cs index 405b3bf..bf8b97e 100644 --- a/ThuInfoWeb/DBModels/Announce.cs +++ b/ThuInfoWeb/DBModels/Announce.cs @@ -1,29 +1,32 @@ -using FreeSql.DataAnnotations; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; +using FreeSql.DataAnnotations; -namespace ThuInfoWeb.DBModels +namespace ThuInfoWeb.DBModels; + +public class Announce { - public class Announce - { - [Column(IsIdentity = true, IsPrimary = true)] - public int Id { get; set; } + [Column(IsIdentity = true, IsPrimary = true)] + public int Id { get; init; } - public string Title { get; set; } + [Column(StringLength = 50, IsNullable = false)] + public string Title { get; init; } = string.Empty; - [Column(StringLength = -1)] - public string Content { get; set; } + [Column(StringLength = -1, IsNullable = false)] + public string Content { get; init; } = string.Empty; - public string Author { get; set; } + [Column(StringLength = 50, IsNullable = false)] + public string Author { get; init; } = string.Empty; - public DateTime CreatedTime { get; set; } + [Column(IsNullable = false)] + public DateTime CreatedTime { get; init; } - [JsonIgnore] - public bool IsActive { get; set; } + [JsonIgnore] + [Column(IsNullable = false)] + public bool IsActive { get; init; } - [Column(StringLength = 10, IsNullable = false)] - public string VisibleNotAfter { get; set; } + [Column(StringLength = 10, IsNullable = false)] + public string VisibleNotAfter { get; init; } = "9.9.9"; - [Column(StringLength = 30, IsNullable = false)] - public string VisibleExact { get; set; } - } -} \ No newline at end of file + [Column(StringLength = 30, IsNullable = false)] + public string VisibleExact { get; init; } = ""; +} diff --git a/ThuInfoWeb/DBModels/Feedback.cs b/ThuInfoWeb/DBModels/Feedback.cs index d54ac0d..518c78a 100644 --- a/ThuInfoWeb/DBModels/Feedback.cs +++ b/ThuInfoWeb/DBModels/Feedback.cs @@ -1,20 +1,36 @@ using FreeSql.DataAnnotations; -namespace ThuInfoWeb.DBModels +namespace ThuInfoWeb.DBModels; + +public class Feedback { - public class Feedback - { - [Column(IsPrimary = true, IsIdentity = true)] - public int Id { get; set; } - [Column(StringLength = -1)] - public string Content { get; set; } - public string Contact { get; set; } = string.Empty; - public DateTime CreatedTime { get; set; } - public string AppVersion { get; set; } - public string OS { get; set; } - public string PhoneModel { get; set; } - public string Reply { get; set; } = string.Empty; - public string ReplierName { get; set; } = string.Empty; - public DateTime? RepliedTime { get; set; } - } + [Column(IsPrimary = true, IsIdentity = true)] + public int Id { get; set; } + + [Column(StringLength = -1, IsNullable = false)] + public string Content { get; init; } = string.Empty; + + [Column(IsNullable = false)] + public string Contact { get; init; } = string.Empty; + + [Column(IsNullable = false)] + public DateTime CreatedTime { get; init; } + + [Column(IsNullable = false)] + public string AppVersion { get; init; } = "0.0.0"; + + [Column(IsNullable = false)] + public string OS { get; init; } = string.Empty; + + [Column(IsNullable = false)] + public string PhoneModel { get; init; } = string.Empty; + + [Column(IsNullable = false)] + public string Reply { get; init; } = string.Empty; + + [Column(IsNullable = false)] + public string ReplierName { get; init; } = string.Empty; + + [Column(IsNullable = false)] + public DateTime? RepliedTime { get; init; } } diff --git a/ThuInfoWeb/DBModels/Misc.cs b/ThuInfoWeb/DBModels/Misc.cs index 99c7dc7..9a351b8 100644 --- a/ThuInfoWeb/DBModels/Misc.cs +++ b/ThuInfoWeb/DBModels/Misc.cs @@ -1,28 +1,30 @@ using FreeSql.DataAnnotations; -namespace ThuInfoWeb.DBModels +namespace ThuInfoWeb.DBModels; + +/// +/// This should be only one record in database. +/// +public class Misc { + [Column(IsPrimary = true)] + public int Id { get; init; } = 1; + + /// + /// The url data of WeChat group qrcode. + /// + [Column(StringLength = -1, IsNullable = false)] + public string QrCodeContent { get; init; } = string.Empty; + + /// + /// The url of Apk. + /// + [Column(StringLength = -1, IsNullable = false)] + public string ApkUrl { get; init; } = string.Empty; + /// - /// This should be only one record in database. + /// The interface version of new school card. /// - public class Misc - { - [Column(IsPrimary = true)] - public int Id { get; set; } = 1; - /// - /// The url data of wechat group qrcode. - /// - [Column(StringLength = -1)] - public string QrCodeContent { get; set; } - /// - /// The url of Apk. - /// - [Column(StringLength = -1)] - public string ApkUrl { get; set; } - /// - /// The interface version of new school card. - /// - [Column] - public int CardIVersion { get; set; } - } + [Column(IsNullable = false)] + public int CardIVersion { get; init; } } diff --git a/ThuInfoWeb/DBModels/Request.cs b/ThuInfoWeb/DBModels/Request.cs index ccee302..39b4bbd 100644 --- a/ThuInfoWeb/DBModels/Request.cs +++ b/ThuInfoWeb/DBModels/Request.cs @@ -1,14 +1,21 @@ using FreeSql.DataAnnotations; -namespace ThuInfoWeb.DBModels +namespace ThuInfoWeb.DBModels; + +public class Request { - public class Request - { - [Column(IsIdentity = true, IsPrimary = true)] - public int Id { get; set; } - public string Method { get; set; } - public string Path { get; set; } - public DateTime Time { get; set; } - public uint Ip { get; set; } - } + [Column(IsIdentity = true, IsPrimary = true)] + public int Id { get; set; } + + [Column(IsNullable = false)] + public string Method { get; set; } = string.Empty; + + [Column(IsNullable = false)] + public string Path { get; set; } = string.Empty; + + [Column(IsNullable = false)] + public DateTime Time { get; set; } + + [Column(IsNullable = false)] + public uint Ip { get; set; } } diff --git a/ThuInfoWeb/DBModels/Socket.cs b/ThuInfoWeb/DBModels/Socket.cs index 96595f9..7603b66 100644 --- a/ThuInfoWeb/DBModels/Socket.cs +++ b/ThuInfoWeb/DBModels/Socket.cs @@ -1,20 +1,28 @@ using FreeSql.DataAnnotations; -namespace ThuInfoWeb.DBModels +namespace ThuInfoWeb.DBModels; + +public class Socket { - public class Socket + public enum SocketStatus { - [Column(IsPrimary = true)] - public int SeatId { get; set; } - public int SectionId { get; set; } - public SocketStatus Status { get; set; } - public DateTime CreatedTime { get; set; } - public DateTime UpdatedTime { get; set; } - public enum SocketStatus - { - Unknown, - Available, - Unavailable - } + Unknown, + Available, + Unavailable } + + [Column(IsPrimary = true)] + public int SeatId { get; set; } + + [Column(IsNullable = false)] + public int SectionId { get; set; } + + [Column(IsNullable = false)] + public SocketStatus Status { get; set; } + + [Column(IsNullable = false)] + public DateTime CreatedTime { get; set; } + + [Column(IsNullable = false)] + public DateTime UpdatedTime { get; set; } } diff --git a/ThuInfoWeb/DBModels/Startup.cs b/ThuInfoWeb/DBModels/Startup.cs index 154a5bc..529fe7c 100644 --- a/ThuInfoWeb/DBModels/Startup.cs +++ b/ThuInfoWeb/DBModels/Startup.cs @@ -4,8 +4,9 @@ namespace ThuInfoWeb.DBModels; public class Startup { - [Column(IsPrimary = true,IsIdentity = true)] - public int Id { get; set; } + [Column(IsPrimary = true, IsIdentity = true)] + public int Id { get; init; } - public DateTime CreatedTime { get; set; } -} \ No newline at end of file + [Column(IsNullable = false)] + public DateTime CreatedTime { get; init; } +} diff --git a/ThuInfoWeb/DBModels/Usage.cs b/ThuInfoWeb/DBModels/Usage.cs index 5245389..aef16da 100644 --- a/ThuInfoWeb/DBModels/Usage.cs +++ b/ThuInfoWeb/DBModels/Usage.cs @@ -1,33 +1,37 @@ using FreeSql.DataAnnotations; -namespace ThuInfoWeb.DBModels +namespace ThuInfoWeb.DBModels; + +public class Usage { - public class Usage + public enum FunctionType { - [Column(IsPrimary = true,IsIdentity = true)] - public int Id { get; set; } - public FunctionType Function { get; set; } - public DateTime CreatedTime { get; set; } - public enum FunctionType - { - PhysicalExam, - TeachingEvaluation, - Report, - Classrooms, - Library, - GymnasiumReg, - PrivateRooms, - Expenditures, - Bank, - Invoice, - WasherInfo, - QZYQ, - DormScore, - Electricity, - NetworkDetail, - OnlineDevices, - SchoolCalendar, - CampusCard - } + PhysicalExam, + TeachingEvaluation, + Report, + Classrooms, + Library, + GymnasiumReg, + PrivateRooms, + Expenditures, + Bank, + Invoice, + WasherInfo, + QZYQ, + DormScore, + Electricity, + NetworkDetail, + OnlineDevices, + SchoolCalendar, + CampusCard } + + [Column(IsPrimary = true, IsIdentity = true)] + public int Id { get; set; } + + [Column(IsNullable = false)] + public FunctionType Function { get; set; } + + [Column(IsNullable = false)] + public DateTime CreatedTime { get; set; } } diff --git a/ThuInfoWeb/DBModels/User.cs b/ThuInfoWeb/DBModels/User.cs index dddf5ea..149a393 100644 --- a/ThuInfoWeb/DBModels/User.cs +++ b/ThuInfoWeb/DBModels/User.cs @@ -1,14 +1,21 @@ using FreeSql.DataAnnotations; -namespace ThuInfoWeb.DBModels +namespace ThuInfoWeb.DBModels; + +public class User { - public class User - { - [Column(IsIdentity = true, IsPrimary = true)] - public int Id { get; set; } - public string Name { get; set; } - public string PasswordHash { get; set; } - public bool IsAdmin { get; set; } - public DateTime CreatedTime { get; set; } - } + [Column(IsIdentity = true, IsPrimary = true)] + public int Id { get; set; } + + [Column(IsNullable = false)] + public string Name { get; set; } = string.Empty; + + [Column(IsNullable = false)] + public string PasswordHash { get; set; } = string.Empty; + + [Column(IsNullable = false)] + public bool IsAdmin { get; set; } + + [Column(IsNullable = false)] + public DateTime CreatedTime { get; set; } } diff --git a/ThuInfoWeb/DBModels/Version.cs b/ThuInfoWeb/DBModels/Version.cs index 4a98190..0e86342 100644 --- a/ThuInfoWeb/DBModels/Version.cs +++ b/ThuInfoWeb/DBModels/Version.cs @@ -1,15 +1,21 @@ using FreeSql.DataAnnotations; -namespace ThuInfoWeb.DBModels +namespace ThuInfoWeb.DBModels; + +public class Version { - public class Version - { - [Column(IsIdentity = true, IsPrimary = true)] - public int Id { get; set; } - public string VersionName { get; set; } - [Column(StringLength = -1)] - public string ReleaseNote { get; set; } - public DateTime CreatedTime { get; set; } - public bool IsAndroid { get; set; } - } + [Column(IsIdentity = true, IsPrimary = true)] + public int Id { get; init; } + + [Column(IsNullable = false)] + public string VersionName { get; init; } = string.Empty; + + [Column(StringLength = -1, IsNullable = false)] + public string ReleaseNote { get; init; } = string.Empty; + + [Column(IsNullable = false)] + public DateTime CreatedTime { get; init; } + + [Column(IsNullable = false)] + public bool IsAndroid { get; init; } } diff --git a/ThuInfoWeb/Data.cs b/ThuInfoWeb/Data.cs index 3382f56..02b897c 100644 --- a/ThuInfoWeb/Data.cs +++ b/ThuInfoWeb/Data.cs @@ -1,167 +1,211 @@ -using ThuInfoWeb.DBModels; +using FreeSql; +using FreeSql.Internal; +using ThuInfoWeb.DBModels; using Version = ThuInfoWeb.DBModels.Version; -namespace ThuInfoWeb +namespace ThuInfoWeb; + +public class Data { - public class Data - { - private readonly IFreeSql _fsql; - - /// - /// - /// - /// the connection string - /// if env is development, use local sqlite database instead of remote postgresql. The DB file will be created automatically. - public Data(string connectionString, bool isDevelopment) - { - if (isDevelopment) - _fsql = new FreeSql.FreeSqlBuilder() - .UseConnectionString(FreeSql.DataType.Sqlite, "Data Source=test.db") - .UseAutoSyncStructure(true) - .UseNameConvert(FreeSql.Internal.NameConvertType.ToLower) - .UseMonitorCommand(x => Console.WriteLine(x.CommandText)) - .Build(); - else - _fsql = new FreeSql.FreeSqlBuilder() - .UseConnectionString(FreeSql.DataType.PostgreSQL, connectionString) - .UseAutoSyncStructure(true) - .UseNameConvert(FreeSql.Internal.NameConvertType.ToLower) - .Build(); - // Check if there is a misc record in database, create one if not exist. - if (!_fsql.Select().Any()) - _fsql.Insert(new Misc()).ExecuteAffrows(); - } - - public async Task GetMiscAsync() - => await _fsql.Select().FirstAsync(); - - public async Task UpdateMiscAsync(Misc misc) - => await _fsql.Update().SetSource(misc).ExecuteAffrowsAsync(); - - public async Task GetUserAsync(string name) - => await _fsql.Select().Where(x => x.Name == name).ToOneAsync(); - - public async Task CheckUserAsync(string name) - => await _fsql.Select().AnyAsync(x => x.Name == name); - - public async Task CreateUserAsync(User user) - => (await _fsql.Select().AnyAsync(x => x.Name == user.Name)) - ? 0 - : await _fsql.Insert(user).ExecuteAffrowsAsync(); - - public async Task ChangeUserPasswordAsync(string name, string passwordhash) - => await _fsql.Update().Where(x => x.Name == name).Set(x => x.PasswordHash, passwordhash) - .ExecuteAffrowsAsync(); - - public async Task GetAnnounceAsync(int? id = null) - => id is null - ? await _fsql.Select().OrderByDescending(x => x.Id).FirstAsync() - : await _fsql.Select().Where(x => x.Id == id).ToOneAsync(); - - public async Task GetActiveAnnounceAsync(int? id = null) - => id is null - ? await _fsql.Select().OrderByDescending(x => x.Id).Where(x => x.IsActive).FirstAsync() - : await _fsql.Select().Where(x => x.Id == id && x.IsActive).ToOneAsync(); - - public async Task> GetAnnouncesAsync(int page, int pageSize) - => await _fsql.Select().OrderByDescending(x => x.Id).Page(page, pageSize).ToListAsync(); - - public async Task> GetActiveAnnouncesAsync(int page, int pageSize) - => await _fsql.Select().OrderByDescending(x => x.Id).Page(page, pageSize).Where(x => x.IsActive) - .ToListAsync(); - - public async Task CreateAnnounceAsync(Announce a) - => await _fsql.Insert(a).ExecuteAffrowsAsync(); - - public async Task UpdateAnnounceStatusAsync(int id, bool toActive) - => await _fsql.Update().Where(x => x.Id == id).Set(x => x.IsActive, toActive) - .ExecuteAffrowsAsync(); - - public async Task DeleteAnnounceAsync(int id) - => await _fsql.Delete().Where(x => x.Id == id).ExecuteAffrowsAsync(); - - public async Task CreateFeedbackAsync(Feedback feedback) - => await _fsql.Insert(feedback).ExecuteAffrowsAsync(); - - public async Task GetFeedbackAsync(int? id = null) - => id is null - ? await _fsql.Select().OrderByDescending(x => x.Id).FirstAsync() - : await _fsql.Select().Where(x => x.Id == id).ToOneAsync(); - - public async Task> GetFeedbacksAsync(int page, int pageSize) - => await _fsql.Select().OrderByDescending(x => x.Id).Page(page, pageSize).ToListAsync(); - - public async Task> GetAllRepliedFeedbacksAsync() - => await _fsql.Select().Where(x => !string.IsNullOrEmpty(x.Reply)) - .OrderByDescending(x => x.RepliedTime).ToListAsync(); - - public async Task DeleteFeedbackAsync(int id) - => await _fsql.Delete().Where(x => x.Id == id).ExecuteAffrowsAsync(); - - public async Task ReplyFeedbackAsync(int id, string reply, string replyer) - => await _fsql.Update().Where(x => x.Id == id).Set(x => x.Reply, reply) - .Set(x => x.ReplierName, replyer).Set(x => x.RepliedTime, DateTime.Now).ExecuteAffrowsAsync(); - - public async Task> GetSocketsAsync(int sectionId) - => await _fsql.Select().Where(x => x.SectionId == sectionId).ToListAsync(); - - public async Task UpdateSocketAsync(int seatId, bool isAvailable) - => await _fsql.Update().Where(x => x.SeatId == seatId).Set(x => x.Status, - isAvailable ? Socket.SocketStatus.Available : Socket.SocketStatus.Unavailable).ExecuteAffrowsAsync(); - - public async Task CreateVersionAsync(Version version) - => await _fsql.Insert(version).ExecuteAffrowsAsync(); - - public async Task GetVersionAsync(bool isAndroid) - => await _fsql.Select().Where(x => x.IsAndroid == isAndroid).OrderByDescending(x => x.CreatedTime) - .FirstAsync(); - - public async Task CreateHttpRequestLogAsync(Request r) - => await _fsql.Insert(r).ExecuteAffrowsAsync(); + private readonly IFreeSql _fsql; + + /// + /// + /// the connection string + /// + /// if env is development, use local sqlite database instead of remote postgresql. The DB file + /// will be created automatically. + /// + public Data(string connectionString, bool isDevelopment) + { + if (isDevelopment) + _fsql = new FreeSqlBuilder() + .UseConnectionString(DataType.Sqlite, "Data Source=test.db") + .UseAutoSyncStructure(true) + .UseNameConvert(NameConvertType.ToLower) + .UseMonitorCommand(x => Console.WriteLine(x.CommandText)) + .Build(); + else + _fsql = new FreeSqlBuilder() + .UseConnectionString(DataType.PostgreSQL, connectionString) + .UseAutoSyncStructure(true) + .UseNameConvert(NameConvertType.ToLower) + .Build(); + // Check if there is a misc record in database, create one if not exist. + if (!_fsql.Select().Any()) + _fsql.Insert(new Misc()).ExecuteAffrows(); + } + + public async Task GetMiscAsync() + { + return await _fsql.Select().FirstAsync(); + } + + public async Task UpdateMiscAsync(Misc misc) + { + return await _fsql.Update().SetSource(misc).ExecuteAffrowsAsync(); + } + + public async Task GetUserAsync(string name) + { + return await _fsql.Select().Where(x => x.Name == name).ToOneAsync(); + } + + public async Task CheckUserAsync(string name) + { + return await _fsql.Select().AnyAsync(x => x.Name == name); + } + + public async Task CreateUserAsync(User user) + { + return await _fsql.Select().AnyAsync(x => x.Name == user.Name) + ? 0 + : await _fsql.Insert(user).ExecuteAffrowsAsync(); + } + + public async Task ChangeUserPasswordAsync(string name, string passwordHash) + { + return await _fsql.Update().Where(x => x.Name == name).Set(x => x.PasswordHash, passwordHash) + .ExecuteAffrowsAsync(); + } + + public async Task GetAnnounceAsync(int? id = null) + { + return id is null + ? await _fsql.Select().OrderByDescending(x => x.Id).FirstAsync() + : await _fsql.Select().Where(x => x.Id == id).ToOneAsync(); + } + + public async Task GetActiveAnnounceAsync(int? id = null) + { + return id is null + ? await _fsql.Select().OrderByDescending(x => x.Id).Where(x => x.IsActive).FirstAsync() + : await _fsql.Select().Where(x => x.Id == id && x.IsActive).ToOneAsync(); + } + + public async Task> GetAnnouncesAsync(int page, int pageSize) + { + return await _fsql.Select().OrderByDescending(x => x.Id).Page(page, pageSize).ToListAsync(); + } + + public async Task> GetActiveAnnouncesAsync(int page, int pageSize) + { + return await _fsql.Select().OrderByDescending(x => x.Id).Page(page, pageSize).Where(x => x.IsActive) + .ToListAsync(); + } + + public async Task CreateAnnounceAsync(Announce a) + { + return await _fsql.Insert(a).ExecuteAffrowsAsync(); + } + + public async Task UpdateAnnounceStatusAsync(int id, bool toActive) + { + return await _fsql.Update().Where(x => x.Id == id).Set(x => x.IsActive, toActive) + .ExecuteAffrowsAsync(); + } + + public async Task DeleteAnnounceAsync(int id) + { + return await _fsql.Delete().Where(x => x.Id == id).ExecuteAffrowsAsync(); + } + + public async Task CreateFeedbackAsync(Feedback feedback) + { + return await _fsql.Insert(feedback).ExecuteAffrowsAsync(); + } + + public async Task GetFeedbackAsync(int? id = null) + { + return id is null + ? await _fsql.Select().OrderByDescending(x => x.Id).FirstAsync() + : await _fsql.Select().Where(x => x.Id == id).ToOneAsync(); + } + + public async Task> GetFeedbacksAsync(int page, int pageSize) + { + return await _fsql.Select().OrderByDescending(x => x.Id).Page(page, pageSize).ToListAsync(); + } + + public async Task> GetAllRepliedFeedbacksAsync() + { + return await _fsql.Select().Where(x => !string.IsNullOrEmpty(x.Reply)) + .OrderByDescending(x => x.RepliedTime).ToListAsync(); + } + + public async Task DeleteFeedbackAsync(int id) + { + return await _fsql.Delete().Where(x => x.Id == id).ExecuteAffrowsAsync(); + } + + public async Task ReplyFeedbackAsync(int id, string reply, string replier) + { + return await _fsql.Update().Where(x => x.Id == id).Set(x => x.Reply, reply) + .Set(x => x.ReplierName, replier).Set(x => x.RepliedTime, DateTime.Now).ExecuteAffrowsAsync(); + } + + public async Task> GetSocketsAsync(int sectionId) + { + return await _fsql.Select().Where(x => x.SectionId == sectionId).ToListAsync(); + } + + public async Task UpdateSocketAsync(int seatId, bool isAvailable) + { + return await _fsql.Update().Where(x => x.SeatId == seatId).Set(x => x.Status, + isAvailable ? Socket.SocketStatus.Available : Socket.SocketStatus.Unavailable).ExecuteAffrowsAsync(); + } - public async Task CreateUsageAsync(Usage u) - => await _fsql.Insert(u).ExecuteAffrowsAsync(); + public async Task CreateVersionAsync(Version version) + { + return await _fsql.Insert(version).ExecuteAffrowsAsync(); + } + + public async Task GetVersionAsync(bool isAndroid) + { + return await _fsql.Select().Where(x => x.IsAndroid == isAndroid).OrderByDescending(x => x.CreatedTime) + .FirstAsync(); + } + + public async Task CreateHttpRequestLogAsync(Request r) + { + return await _fsql.Insert(r).ExecuteAffrowsAsync(); + } + + public async Task CreateUsageAsync(Usage u) + { + return await _fsql.Insert(u).ExecuteAffrowsAsync(); + } - public async Task> GetUsageAsync() - => await _fsql.Select().GroupBy(x => x.Function).ToDictionaryAsync(x => x.Count()); + public async Task> GetUsageAsync() + { + return await _fsql.Select().GroupBy(x => x.Function).ToDictionaryAsync(x => x.Count()); + } - public async Task CreateStartupAsync(Startup s) - => await _fsql.Insert(s).ExecuteAffrowsAsync(); + public async Task CreateStartupAsync(Startup s) + { + return await _fsql.Insert(s).ExecuteAffrowsAsync(); + } - public async Task> GetStartupDataAsync() - => await _fsql.Select().GroupBy(x => x.CreatedTime.ToString("yyyy MM")) - .OrderBy(x=>x.Key) - .ToDictionaryAsync(x => x.Count()); + public async Task> GetStartupDataAsync() + { + return await _fsql.Select().GroupBy(x => x.CreatedTime.ToString("yyyy MM")) + .OrderBy(x => x.Key) + .ToDictionaryAsync(x => x.Count()); + } #if DEBUG - public async Task GenStartupDataAsync() - { - var s1 = new Startup() - { - CreatedTime = DateTime.Now - TimeSpan.FromDays(30) - }; - for (int i = 0; i < 10; i++) - { - await _fsql.Insert(s1).ExecuteAffrowsAsync(); - } - - var s2 = new Startup() - { - CreatedTime = DateTime.Now - TimeSpan.FromDays(60) - }; - for (int i = 0; i < 20; i++) - { - await _fsql.Insert(s2).ExecuteAffrowsAsync(); - } - - var s3 = new Startup() - { - CreatedTime = DateTime.Now - TimeSpan.FromDays(90) - }; - for (int i = 0; i < 30; i++) - { - await _fsql.Insert(s3).ExecuteAffrowsAsync(); - } - } -#endif + public async Task GenStartupDataAsync() + { + var s1 = new Startup { CreatedTime = DateTime.Now - TimeSpan.FromDays(30) }; + for (var i = 0; i < 10; i++) + await _fsql.Insert(s1).ExecuteAffrowsAsync(); + + var s2 = new Startup { CreatedTime = DateTime.Now - TimeSpan.FromDays(60) }; + for (var i = 0; i < 20; i++) + await _fsql.Insert(s2).ExecuteAffrowsAsync(); + + var s3 = new Startup { CreatedTime = DateTime.Now - TimeSpan.FromDays(90) }; + for (var i = 0; i < 30; i++) + await _fsql.Insert(s3).ExecuteAffrowsAsync(); } -} \ No newline at end of file +#endif +} diff --git a/ThuInfoWeb/Dtos/FeedbackDto.cs b/ThuInfoWeb/Dtos/FeedbackDto.cs index e2e1993..51c0fba 100644 --- a/ThuInfoWeb/Dtos/FeedbackDto.cs +++ b/ThuInfoWeb/Dtos/FeedbackDto.cs @@ -1,19 +1,21 @@ using System.ComponentModel.DataAnnotations; -namespace ThuInfoWeb.Dtos +namespace ThuInfoWeb.Dtos; + +public class FeedbackDto { - public class FeedbackDto - { - [Required] - public string Content { get; set; } - [MaxLength(256)] - public string Contact { get; set; } = string.Empty; - [Required] - public string OS { get; set; } - [Required] - public string AppVersion { get; set; } - [Required] - public string PhoneModel { get; set; } - - } + [Required] + public string Content { get; init; } = string.Empty; + + [MaxLength(256)] + public string Contact { get; init; } = string.Empty; + + [Required] + public string OS { get; init; } = string.Empty; + + [Required] + public string AppVersion { get; init; } = string.Empty; + + [Required] + public string PhoneModel { get; init; } = string.Empty; } diff --git a/ThuInfoWeb/Dtos/LostAndFoundDto.cs b/ThuInfoWeb/Dtos/LostAndFoundDto.cs index 31a2c21..8d48fbd 100644 --- a/ThuInfoWeb/Dtos/LostAndFoundDto.cs +++ b/ThuInfoWeb/Dtos/LostAndFoundDto.cs @@ -1,14 +1,16 @@ using System.ComponentModel.DataAnnotations; -namespace ThuInfoWeb.Dtos +namespace ThuInfoWeb.Dtos; + +public class LostAndFoundDto { - public class LostAndFoundDto - { - public int Id { get; set; } - [Required] - public string Message { get; set; } - [Required] - public int SenderId { get; set; } - public int? TargetId { get; set; } - } + public int Id { get; set; } + + [Required] + public string? Message { get; set; } + + [Required] + public int SenderId { get; set; } + + public int? TargetId { get; set; } } diff --git a/ThuInfoWeb/Dtos/SocketDto.cs b/ThuInfoWeb/Dtos/SocketDto.cs index 0d6880c..3b8fcae 100644 --- a/ThuInfoWeb/Dtos/SocketDto.cs +++ b/ThuInfoWeb/Dtos/SocketDto.cs @@ -1,18 +1,19 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; -namespace ThuInfoWeb.Dtos +namespace ThuInfoWeb.Dtos; + +public class SocketDto { - public class SocketDto - { - [Required] - public int? SeatId { get; set; } - [Required, JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public bool? IsAvailable { get; set; } - public int SectionId { get; set; } - public DateTime CreatedTime { get; set; } - public DateTime UpdatedTime { get; set; } - public string? Status { get; set; } - - } + [Required] + public int? SeatId { get; init; } + + [Required] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool? IsAvailable { get; set; } + + public int SectionId { get; set; } + public DateTime CreatedTime { get; set; } + public DateTime UpdatedTime { get; set; } + public string? Status { get; set; } } diff --git a/ThuInfoWeb/Dtos/VersionDto.cs b/ThuInfoWeb/Dtos/VersionDto.cs index ca9aa0b..37f56c4 100644 --- a/ThuInfoWeb/Dtos/VersionDto.cs +++ b/ThuInfoWeb/Dtos/VersionDto.cs @@ -1,10 +1,12 @@ -namespace ThuInfoWeb.Dtos +namespace ThuInfoWeb.Dtos; + +public class VersionDto { - public class VersionDto - { - public string VersionName { get; set; } - public string ReleaseNote { get; set; } - public DateTime CreatedTime { get; set; } - public string DownloadUrl { get; set; } - } + public string VersionName { get; set; } = string.Empty; + + public string ReleaseNote { get; set; } = string.Empty; + + public DateTime CreatedTime { get; set; } + + public string DownloadUrl { get; set; } = string.Empty; } diff --git a/ThuInfoWeb/Extension.cs b/ThuInfoWeb/Extension.cs index 179f133..7847c02 100644 --- a/ThuInfoWeb/Extension.cs +++ b/ThuInfoWeb/Extension.cs @@ -1,19 +1,22 @@ using System.Security.Cryptography; using System.Text; +using System.Text.RegularExpressions; -namespace ThuInfoWeb +namespace ThuInfoWeb; + +public static partial class Extension { - public static class Extension + [GeneratedRegex(@"^\d+\.\d+\.\d+$")] + private static partial Regex VersionRegex(); + + public static string ToSHA256Hex(this string s) + { + var data = SHA256.HashData(Encoding.ASCII.GetBytes(s)); + return data.Aggregate("", (current, b) => current + b.ToString("x").PadLeft(2, '0')); + } + + public static bool IsValidVersionNumber(this string s) { - public static string ToSHA256Hex(this string s) - { - var data = SHA256.HashData(Encoding.ASCII.GetBytes(s)); - string output = ""; - foreach (var b in data) - { - output += b.ToString("x").PadLeft(2, '0'); - } - return output; - } + return VersionRegex().IsMatch(s); } } diff --git a/ThuInfoWeb/HttpLoggingMiddleware.cs b/ThuInfoWeb/HttpLoggingMiddleware.cs index 8c7bbf2..1d27e66 100644 --- a/ThuInfoWeb/HttpLoggingMiddleware.cs +++ b/ThuInfoWeb/HttpLoggingMiddleware.cs @@ -1,48 +1,44 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using System.Net; -using System.Threading.Tasks; +using System.Net; using ThuInfoWeb.DBModels; -namespace ThuInfoWeb +namespace ThuInfoWeb; + +// You may need to install the Microsoft.AspNetCore.Http.Abstractions package into your project +public class HttpLoggingMiddleware { - // You may need to install the Microsoft.AspNetCore.Http.Abstractions package into your project - public class HttpLoggingMiddleware - { - private readonly RequestDelegate _next; + private readonly RequestDelegate _next; - public HttpLoggingMiddleware(RequestDelegate next) - { - _next = next; - } + public HttpLoggingMiddleware(RequestDelegate next) + { + _next = next; + } - public async Task Invoke(HttpContext context, Data data) + public async Task Invoke(HttpContext context, Data data) + { + var path = context.Request.Path; + if (!path.StartsWithSegments("/api")) { - var path = context.Request.Path; - if (!path.StartsWithSegments("/api")) + var ip = context.Connection.RemoteIpAddress ?? IPAddress.Parse("0.0.0.0"); + var ipBytes = ip.GetAddressBytes().Reverse().ToArray(); + var r = new Request { - var ip = context.Connection.RemoteIpAddress; - if (ip is null) ip = IPAddress.Parse("0.0.0.0"); - var ipBytes = ip.GetAddressBytes().Reverse().ToArray(); - var r = new Request() - { - Method = context.Request.Method, - Path = path, - Ip = BitConverter.ToUInt32(ipBytes), - Time = DateTime.Now - }; - await data.CreateHttpRequestLogAsync(r); - } - await _next(context); + Method = context.Request.Method, + Path = path, + Ip = BitConverter.ToUInt32(ipBytes), + Time = DateTime.Now + }; + await data.CreateHttpRequestLogAsync(r); } + + await _next(context); } +} - // Extension method used to add the middleware to the HTTP request pipeline. - public static class HttpLoggingMiddlewareExtensions +// Extension method used to add the middleware to the HTTP request pipeline. +public static class HttpLoggingMiddlewareExtensions +{ + public static IApplicationBuilder UseHttpLoggingMiddleware(this IApplicationBuilder builder) { - public static IApplicationBuilder UseHttpLoggingMiddleware(this IApplicationBuilder builder) - { - return builder.UseMiddleware(); - } + return builder.UseMiddleware(); } } diff --git a/ThuInfoWeb/Hubs/ScheduleSyncHub.cs b/ThuInfoWeb/Hubs/ScheduleSyncHub.cs index 05a7e4c..db86e5f 100644 --- a/ThuInfoWeb/Hubs/ScheduleSyncHub.cs +++ b/ThuInfoWeb/Hubs/ScheduleSyncHub.cs @@ -5,26 +5,11 @@ namespace ThuInfoWeb.Hubs; public class ScheduleSyncHub : Hub { - private static readonly List SyncClients = new(); - private static readonly List ConfirmSyncClients = new(); + private static readonly List SyncClients = []; + private static readonly List ConfirmSyncClients = []; public void StartMatch(string user, bool isSending) { - static string GenToken() - { - var rand = Random.Shared; - var b = new byte[16]; - rand.NextBytes(b); - var ret = ""; - var hash = MD5.HashData(b); - for (int i = 0; i < 3; i++) - { - ret += hash[i].ToString("x").PadLeft(2,'0'); - } - - return ret; - } - if (SyncClients.Exists(x => x.User == user)) // Existing a user, try to match it. { var token = GenToken(); @@ -32,37 +17,52 @@ static string GenToken() { if (!SyncClients.Exists(x => x.User == user && x.IsSending == false)) return; // no receiver, just return - var target = SyncClients.Where(x => x.User == user).First(); + var target = SyncClients.First(x => x.User == user); SyncClients.Remove(target); _ = Clients.Clients(Context.ConnectionId, target.Id).SendAsync("ConfirmMatch", token); } else // is receiving { - if (!SyncClients.Exists(x => x.User == user && x.IsSending == true)) + if (!SyncClients.Exists(x => x.User == user && x.IsSending)) return; // no sender, just return - var targetId = Context.ConnectionId; - var sender = SyncClients.Where(x => x.User == user).First(); + var sender = SyncClients.First(x => x.User == user); SyncClients.Remove(sender); _ = Clients.Clients(sender.Id, Context.ConnectionId).SendAsync("ConfirmMatch", token); } } else // No user can be matched, waiting { - SyncClients.Add(new(Context.ConnectionId, user, isSending)); + SyncClients.Add(new SyncClient(Context.ConnectionId, user, isSending)); + } + + return; + + static string GenToken() + { + var rand = Random.Shared; + var b = new byte[16]; + rand.NextBytes(b); + var ret = ""; + var hash = MD5.HashData(b); + for (var i = 0; i < 3; i++) + ret += hash[i].ToString("x").PadLeft(2, '0'); + + return ret; } } public void ConfirmMatch(string user, string token, bool isSending) { - ConfirmSyncClients.Add(new(Context.ConnectionId, user, isSending, token)); + ConfirmSyncClients.Add(new ConfirmSyncClient(Context.ConnectionId, user, isSending, token)); var matchedClients = ConfirmSyncClients.FindAll(x => x.Token == token); if (matchedClients.Count != 2) return; if (!matchedClients.TrueForAll(x => x.Token == token)) // code mismatched return; - _ = Clients.Client(matchedClients.Find(x => x.IsSending).Id) - .SendAsync("SetTarget", matchedClients.Find(x => !x.IsSending).Id); - if(ConfirmSyncClients.Count>100) ConfirmSyncClients.Clear(); + _ = Clients.Client(matchedClients.Find(x => x.IsSending)!.Id) + .SendAsync("SetTarget", matchedClients.Find(x => !x.IsSending)!.Id); + if (ConfirmSyncClients.Count > 100) + ConfirmSyncClients.Clear(); } public void SendToTarget(string targetId, string schedulesJson) @@ -79,27 +79,16 @@ public override Task OnDisconnectedAsync(Exception? exception) return base.OnDisconnectedAsync(exception); } - private class SyncClient + private class SyncClient(string id, string user, bool isSending) { - public string Id { get; } - public string User { get; } - public bool IsSending { get; } - - public SyncClient(string id, string user, bool isSending) - { - Id = id; - User = user; - IsSending = isSending; - } + public string Id { get; } = id; + public string User { get; } = user; + public bool IsSending { get; } = isSending; } - private class ConfirmSyncClient : SyncClient + private class ConfirmSyncClient(string id, string user, bool isSending, string token) + : SyncClient(id, user, isSending) { - public string Token { get; } - - public ConfirmSyncClient(string id, string user, bool isSending, string token) : base(id, user, isSending) - { - Token = token; - } + public string Token { get; } = token; } -} \ No newline at end of file +} diff --git a/ThuInfoWeb/Models/AnnounceViewModel.cs b/ThuInfoWeb/Models/AnnounceViewModel.cs index 385c996..ad62271 100644 --- a/ThuInfoWeb/Models/AnnounceViewModel.cs +++ b/ThuInfoWeb/Models/AnnounceViewModel.cs @@ -1,27 +1,28 @@ using System.ComponentModel.DataAnnotations; -namespace ThuInfoWeb.Models +namespace ThuInfoWeb.Models; + +public class AnnounceViewModel { - public class AnnounceViewModel - { - [Required, Display(Name = "标题")] - public string? Title { get; set; } + [Required] + [Display(Name = "标题")] + public string? Title { get; init; } - [Required, Display(Name = "内容")] - public string? Content { get; set; } + [Required] + [Display(Name = "内容")] + public string? Content { get; init; } - public int Id { get; set; } + public int Id { get; init; } - public string? Author { get; set; } + public string? Author { get; init; } - public DateTime CreatedTime { get; set; } + public DateTime CreatedTime { get; init; } - public bool IsActive { get; set; } = true; + public bool IsActive { get; init; } = true; - [Display(Name = "对此版本及之前版本可见")] - public string? VisibleNotAfter { get; set; } + [Display(Name = "对此版本及之前版本可见")] + public string? VisibleNotAfter { get; init; } - [Display(Name = "对这些版本可见 (使用逗号分隔)")] - public string? VisibleExact { get; set; } - } + [Display(Name = "对这些版本可见 (使用逗号分隔)")] + public string? VisibleExact { get; init; } } diff --git a/ThuInfoWeb/Models/ChangePasswordViewModel.cs b/ThuInfoWeb/Models/ChangePasswordViewModel.cs index ff0dd2c..bd73ce7 100644 --- a/ThuInfoWeb/Models/ChangePasswordViewModel.cs +++ b/ThuInfoWeb/Models/ChangePasswordViewModel.cs @@ -1,14 +1,21 @@ using System.ComponentModel.DataAnnotations; -namespace ThuInfoWeb.Models +namespace ThuInfoWeb.Models; + +public class ChangePasswordViewModel { - public class ChangePasswordViewModel - { - [Required, Display(Name = "用户名")] - public string Name { get; set; } - [Required, DataType(DataType.Password), Display(Name = "旧密码")] - public string OldPassword { get; set; } - [Required, DataType(DataType.Password), Display(Name = "新密码"), RegularExpression("^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,20}$", ErrorMessage = "密码必须并只能包含字母和数字,长度为6到20位")] - public string NewPassword { get; set; } - } + [Required] + [Display(Name = "用户名")] + public string? Name { get; init; } + + [Required] + [DataType(DataType.Password)] + [Display(Name = "旧密码")] + public string? OldPassword { get; init; } + + [Required] + [DataType(DataType.Password)] + [Display(Name = "新密码")] + [RegularExpression("^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,20}$", ErrorMessage = "密码必须并只能包含字母和数字,长度为6到20位")] + public string? NewPassword { get; init; } } diff --git a/ThuInfoWeb/Models/ErrorViewModel.cs b/ThuInfoWeb/Models/ErrorViewModel.cs index 4cd00b4..74b8b16 100644 --- a/ThuInfoWeb/Models/ErrorViewModel.cs +++ b/ThuInfoWeb/Models/ErrorViewModel.cs @@ -1,9 +1,8 @@ -namespace ThuInfoWeb.Models +namespace ThuInfoWeb.Models; + +public class ErrorViewModel { - public class ErrorViewModel - { - public string? RequestId { get; set; } + public string? RequestId { get; init; } - public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - } -} \ No newline at end of file + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); +} diff --git a/ThuInfoWeb/Models/FeedbackViewModel.cs b/ThuInfoWeb/Models/FeedbackViewModel.cs index 1cbc1c4..ade4245 100644 --- a/ThuInfoWeb/Models/FeedbackViewModel.cs +++ b/ThuInfoWeb/Models/FeedbackViewModel.cs @@ -1,13 +1,16 @@ using ThuInfoWeb.Dtos; -namespace ThuInfoWeb.Models +namespace ThuInfoWeb.Models; + +public class FeedbackViewModel : FeedbackDto { - public class FeedbackViewModel : FeedbackDto - { - public int Id { get; set; } - public DateTime CreatedTime { get; set; } - public string Reply { get; set; } = string.Empty; - public string ReplyerName { get; set; } = string.Empty; - public DateTime? RepliedTime { get; set; } - } + public int Id { get; init; } + + public DateTime CreatedTime { get; init; } + + public string? Reply { get; init; } + + public string? ReplierName { get; init; } + + public DateTime? RepliedTime { get; init; } } diff --git a/ThuInfoWeb/Models/LoginViewModel.cs b/ThuInfoWeb/Models/LoginViewModel.cs index cd9b1e0..247042c 100644 --- a/ThuInfoWeb/Models/LoginViewModel.cs +++ b/ThuInfoWeb/Models/LoginViewModel.cs @@ -1,12 +1,18 @@ using System.ComponentModel.DataAnnotations; -namespace ThuInfoWeb.Models +namespace ThuInfoWeb.Models; + +public class LoginViewModel { - public class LoginViewModel - { - [DataType(DataType.Text), Display(Name = "用户名"), Required, MaxLength(100)] - public string Name { get; set; } - [DataType(DataType.Password), Display(Name = "密码"), Required, MaxLength(20)] - public string Password { get; set; } - } + [DataType(DataType.Text)] + [Display(Name = "用户名")] + [Required] + [MaxLength(100)] + public string? Name { get; init; } + + [DataType(DataType.Password)] + [Display(Name = "密码")] + [Required] + [MaxLength(20)] + public string? Password { get; init; } } diff --git a/ThuInfoWeb/Models/MiscViewModel.cs b/ThuInfoWeb/Models/MiscViewModel.cs index 456cf9f..e6f59b2 100644 --- a/ThuInfoWeb/Models/MiscViewModel.cs +++ b/ThuInfoWeb/Models/MiscViewModel.cs @@ -1,14 +1,17 @@ using System.ComponentModel.DataAnnotations; -namespace ThuInfoWeb.Models +namespace ThuInfoWeb.Models; + +public class MiscViewModel { - public class MiscViewModel - { - [Required, Url] - public string QrCodeContent { get; set; } - [Required, Url] - public string ApkUrl { get; set; } - [Required] - public int CardIVersion { get; set; } - } + [Required] + [Url] + public string? QrCodeContent { get; init; } + + [Required] + [Url] + public string? ApkUrl { get; init; } + + [Required] + public int CardIVersion { get; init; } } diff --git a/ThuInfoWeb/Models/RegisterViewModel.cs b/ThuInfoWeb/Models/RegisterViewModel.cs index e887c7b..cf43684 100644 --- a/ThuInfoWeb/Models/RegisterViewModel.cs +++ b/ThuInfoWeb/Models/RegisterViewModel.cs @@ -1,12 +1,16 @@ using System.ComponentModel.DataAnnotations; -namespace ThuInfoWeb.Models +namespace ThuInfoWeb.Models; + +public class RegisterViewModel { - public class RegisterViewModel - { - [Required, Display(Name = "用户名")] - public string Name { get; set; } - [Required, DataType(DataType.Password), Display(Name = "密码"), RegularExpression("^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,20}$", ErrorMessage = "密码必须并只能包含字母和数字,长度为6到20位")] - public string Password { get; set; } - } + [Required] + [Display(Name = "用户名")] + public string? Name { get; init; } + + [Required] + [DataType(DataType.Password)] + [Display(Name = "密码")] + [RegularExpression("^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,20}$", ErrorMessage = "密码必须并只能包含字母和数字,长度为6到20位")] + public string? Password { get; init; } } diff --git a/ThuInfoWeb/Models/UserViewModel.cs b/ThuInfoWeb/Models/UserViewModel.cs index 3355c4c..baa77d6 100644 --- a/ThuInfoWeb/Models/UserViewModel.cs +++ b/ThuInfoWeb/Models/UserViewModel.cs @@ -1,8 +1,12 @@ -namespace ThuInfoWeb.Models +using System.ComponentModel.DataAnnotations; + +namespace ThuInfoWeb.Models; + +public class UserViewModel { - public class UserViewModel - { - public string Name { get; set; } - public bool IsAdmin { get; set; } - } + [Required] + public string? Name { get; set; } + + [Required] + public bool IsAdmin { get; set; } = false; } diff --git a/ThuInfoWeb/Program.cs b/ThuInfoWeb/Program.cs index f696adf..9ccbba9 100644 --- a/ThuInfoWeb/Program.cs +++ b/ThuInfoWeb/Program.cs @@ -1,3 +1,4 @@ +using NLog; using NLog.Web; using ThuInfoWeb; using ThuInfoWeb.Bots; @@ -17,7 +18,8 @@ options.LoginPath = new PathString("/Home/Login"); options.AccessDeniedPath = new PathString("/deny"); }); -builder.Services.AddSingleton(new Data(builder.Configuration.GetConnectionString("Test"), builder.Environment.IsDevelopment())); +builder.Services.AddSingleton(new Data(builder.Configuration.GetConnectionString("Test") ?? "", + builder.Environment.IsDevelopment())); // builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); @@ -42,9 +44,8 @@ app.UseAuthentication(); app.UseAuthorization(); -app.MapControllerRoute( - name: "default", - pattern: "{controller}/{action}/{id?}"); +app.MapControllerRoute("default", + "{controller}/{action}/{id?}"); app.MapFallbackToFile("/", "index.html"); app.MapFallbackToFile("/index", "index.html"); @@ -52,8 +53,12 @@ app.MapFallbackToFile("/help", "help.html"); app.MapFallbackToFile("/privacy", "privacy.html"); app.MapFallbackToFile("/privacy-en", "privacy-en.html"); -app.MapFallback("/deny", async r => await r.Response.WriteAsync("access denied")); +app.MapFallback("/deny", async r => +{ + r.Response.StatusCode = 403; + await r.Response.WriteAsync("access denied"); +}); app.MapHub("/schedulesynchub"); app.Run(); -NLog.LogManager.Shutdown(); \ No newline at end of file +LogManager.Shutdown(); diff --git a/ThuInfoWeb/SecretManager.cs b/ThuInfoWeb/SecretManager.cs index 88f47f2..d71bc87 100644 --- a/ThuInfoWeb/SecretManager.cs +++ b/ThuInfoWeb/SecretManager.cs @@ -1,9 +1,3 @@ -namespace ThuInfoWeb -{ - public class SecretManager - { - public SecretManager() - { - } - } -} +namespace ThuInfoWeb; + +public class SecretManager; diff --git a/ThuInfoWeb/ThuInfoWeb.csproj b/ThuInfoWeb/ThuInfoWeb.csproj index 8585971..c7c3346 100644 --- a/ThuInfoWeb/ThuInfoWeb.csproj +++ b/ThuInfoWeb/ThuInfoWeb.csproj @@ -1,20 +1,20 @@ - - net8.0 - enable - enable - + + net8.0 + enable + enable + - - - + + + - - - - - - + + + + + + diff --git a/ThuInfoWeb/UserManager.cs b/ThuInfoWeb/UserManager.cs index 80d4bfc..d92576e 100644 --- a/ThuInfoWeb/UserManager.cs +++ b/ThuInfoWeb/UserManager.cs @@ -1,32 +1,20 @@ -using Microsoft.AspNetCore.Authentication; -using System.Security.Claims; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; -namespace ThuInfoWeb +namespace ThuInfoWeb; + +public class UserManager(IHttpContextAccessor accessor) { - public class UserManager + public async Task DoLoginAsync(string name, bool isAdmin) { - private readonly IHttpContextAccessor _accessor; + var claims = new List { new(ClaimTypes.Name, name), new(ClaimTypes.Role, isAdmin ? "admin" : "guest") }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "any")); + await accessor.HttpContext!.SignInAsync("Cookies", user, + new AuthenticationProperties { ExpiresUtc = DateTime.UtcNow.AddMinutes(20) }); + } - public UserManager(IHttpContextAccessor accessor) - { - this._accessor = accessor; - } - public async Task DoLoginAsync(string name, bool isAdmin) - { - var claims = new List() - { - new Claim(ClaimTypes.Name,name), - new Claim(ClaimTypes.Role,isAdmin?"admin":"guest") - }; - var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "any")); - await _accessor.HttpContext.SignInAsync("Cookies", user, new AuthenticationProperties - { - ExpiresUtc = DateTime.UtcNow.AddMinutes(20) - }); - } - public async Task DoLogoutAsync() - { - await _accessor.HttpContext.SignOutAsync(); - } + public async Task DoLogoutAsync() + { + await accessor.HttpContext!.SignOutAsync(); } } diff --git a/ThuInfoWeb/VersionManager.cs b/ThuInfoWeb/VersionManager.cs index 4df4f05..1ceea89 100644 --- a/ThuInfoWeb/VersionManager.cs +++ b/ThuInfoWeb/VersionManager.cs @@ -3,59 +3,57 @@ using ThuInfoWeb.Dtos; using Version = ThuInfoWeb.DBModels.Version; -namespace ThuInfoWeb +namespace ThuInfoWeb; + +public class VersionManager(ILogger logger, Data data, IConfiguration configuration) { - public class VersionManager + public enum OS { - private readonly ILogger _logger; - private readonly Data _data; - private readonly HttpClient _client = new Func(() => - { - var client = new HttpClient(); - client.DefaultRequestHeaders.Add("user-agent", "aspnetcore/6.0"); - return client; - })(); - private Version _currentVersionOfAndroid; - private Version _currentVersionOfIOS; - private readonly object _lock = new(); - private bool isRunning; - public bool IsRunning + Android, + IOS + } + + private readonly HttpClient _client = new Func(() => + { + var client = new HttpClient(); + client.DefaultRequestHeaders.Add("user-agent", "aspnetcore/6.0"); + return client; + })(); + + private readonly bool _internalNetworkMode = bool.Parse(configuration["InternalNetworkMode"] ?? "false"); + private readonly object _lock = new(); + private Version _currentVersionOfAndroid = data.GetVersionAsync(true).Result ?? new Version(); + private Version _currentVersionOfIOS = data.GetVersionAsync(false).Result ?? new Version(); + private bool _isRunning; + + public bool IsRunning + { + get { - get + lock (_lock) { - lock (_lock) - { - return isRunning; - } + return _isRunning; } - private set + } + private set + { + lock (_lock) { - lock (_lock) - { - isRunning = value; - } + _isRunning = value; } } + } - private readonly bool _internalNetworkMode; - - public VersionManager(ILogger logger, Data data,IConfiguration configuration) - { - this._logger = logger; - this._data = data; - this._internalNetworkMode = bool.Parse(configuration["InternalNetworkMode"]); - // initial current version from database in ctor. - this._currentVersionOfAndroid = data.GetVersionAsync(true).Result ?? new Version(); - this._currentVersionOfIOS = data.GetVersionAsync(false).Result ?? new Version(); - } - public VersionDto GetCurrentVersion(OS os) => os switch + public VersionDto GetCurrentVersion(OS os) + { + return os switch { OS.Android => new VersionDto { CreatedTime = _currentVersionOfAndroid.CreatedTime, DownloadUrl = "https://app.cs.tsinghua.edu.cn/api/apk", ReleaseNote = _currentVersionOfAndroid.ReleaseNote, - VersionName = _currentVersionOfAndroid.VersionName, + VersionName = _currentVersionOfAndroid.VersionName }, OS.IOS => new VersionDto { @@ -63,119 +61,122 @@ public VersionManager(ILogger logger, Data data,IConfiguration c DownloadUrl = "https://apps.apple.com/cn/app/thu-info/id1533968428", ReleaseNote = _currentVersionOfIOS.ReleaseNote, VersionName = _currentVersionOfIOS.VersionName - } + }, + _ => throw new ArgumentOutOfRangeException(nameof(os), os, null) }; - public async Task CheckUpdateAsync(OS os) + } + + public async Task CheckUpdateAsync(OS os) + { + IsRunning = true; + logger.LogInformation("Start checking update for {OS}, current version is {Version}", + os == OS.Android ? "Android" : "iOS", + os == OS.Android ? _currentVersionOfAndroid.VersionName : _currentVersionOfIOS.VersionName); + + try { - IsRunning = true; - _logger.LogInformation($"Start checking update for {(os == OS.Android ? "Android" : "iOS")}, current version is {(os == OS.Android ? _currentVersionOfAndroid.VersionName : _currentVersionOfIOS.VersionName)}"); - try + if (_internalNetworkMode) + { + if (os == OS.Android) + { + var content = await _client.GetStringAsync( + "https://stu.cs.tsinghua.edu.cn/thuinfo/version/android"); + var version = JsonSerializer.Deserialize(content)!; + if (version.VersionName == _currentVersionOfAndroid.VersionName) + logger.LogInformation("No newer version is available for Android (current version is {VersionName})," + + " check update for Android ok", version.VersionName); + + if (await data.CreateVersionAsync(version) != 1) + throw new Exception("Unknown Error"); + logger.LogInformation("Found new version for Android: {VersionName}, check update ok", + version.VersionName); + } + else + { + var content = await _client.GetStringAsync("https://stu.cs.tsinghua.edu.cn/thuinfo/version/ios"); + var version = JsonSerializer.Deserialize(content)!; + if (version.VersionName == _currentVersionOfIOS.VersionName) + logger.LogInformation("No newer version is available for iOS(current version is {VersionName}), check update for iOS ok", version.VersionName); + + if (await data.CreateVersionAsync(version) != 1) + throw new Exception("Unknown Error"); + logger.LogInformation("Found new version for iOS: {VersionName}, check update for iOS ok", version.VersionName); + } + } + else { - if (_internalNetworkMode) + if (os == OS.Android) { - if (os == OS.Android) + const string url = "https://api.github.com/repos/UNIDY2002/THUInfo/releases/latest"; + var content = await _client.GetStringAsync(url); + var json = JsonNode.Parse(content)!; + var versionName = (string)json["name"]!; + if (versionName == _currentVersionOfAndroid.VersionName) { - var content = await _client.GetStringAsync( - "https://stu.cs.tsinghua.edu.cn/thuinfo/version/android"); - var version = JsonSerializer.Deserialize(content)!; - if(version.VersionName == _currentVersionOfAndroid.VersionName) - _logger.LogInformation( - $"No newer version is available for Android(current version is {version.VersionName}), check update for Android ok."); - var result = await _data.CreateVersionAsync(version); - if (result != 1) throw new Exception("Unknown Error"); - else - _logger.LogInformation( - $"Found new version for Android: {version.VersionName}, check update ok"); + logger.LogInformation( + "No newer version is available for Android(current version is {VersionName}), check update for Android ok", + versionName); } else { - var content = await _client.GetStringAsync( - "https://stu.cs.tsinghua.edu.cn/thuinfo/version/ios"); - var version = JsonSerializer.Deserialize(content)!; - if(version.VersionName == _currentVersionOfIOS.VersionName) - _logger.LogInformation( - $"No newer version is available for iOS(current version is {version.VersionName}), check update for iOS ok."); - var result = await _data.CreateVersionAsync(version); - if (result != 1) throw new Exception("Unknown Error"); - else - _logger.LogInformation( - $"Found new version for iOS: {version.VersionName}, check update for iOS ok"); + var publishedAt = DateTime.Parse((string)json["published_at"]!).ToLocalTime(); + var releaseNote = (string)json["body"]!; + var version = new Version + { + CreatedTime = publishedAt, + IsAndroid = true, + ReleaseNote = releaseNote, + VersionName = versionName + }; + var result = await data.CreateVersionAsync(version); + if (result != 1) + throw new Exception("Unknown Error"); + logger.LogInformation("Found new version for Android: {VersionName}, check update ok", versionName); } } - else + else // handle ios { - if (os == OS.Android) + const string url = "https://itunes.apple.com/lookup?id=1533968428"; + var content = await _client.GetStringAsync(url); + var json = JsonNode.Parse(content)!["results"]!.AsArray()[0]!; + var versionName = (string)json["version"]!; + if (versionName == _currentVersionOfIOS.VersionName) { - var url = "https://api.github.com/repos/UNIDY2002/THUInfo/releases/latest"; - var content = await _client.GetStringAsync(url); - var json = JsonNode.Parse(content); - var versionName = (string)json["name"]; - if (versionName == _currentVersionOfAndroid.VersionName) - _logger.LogInformation( - $"No newer version is available for Android(current version is {versionName}), check update for Android ok."); - else - { - var publishedAt = DateTime.Parse((string)json["published_at"]).ToLocalTime(); - var releaseNote = (string)json["body"]; - var version = new Version() - { - CreatedTime = publishedAt, - IsAndroid = true, - ReleaseNote = releaseNote, - VersionName = versionName - }; - var result = await _data.CreateVersionAsync(version); - if (result != 1) throw new Exception("Unknown Error"); - else - _logger.LogInformation( - $"Found new version for Android: {versionName}, check update ok"); - } + logger.LogInformation( + "No newer version is available for iOS(current version is {VersionName}), check update for iOS ok", + versionName); } - else // handle ios + else { - var url = "https://itunes.apple.com/lookup?id=1533968428"; - var content = await _client.GetStringAsync(url); - var json = JsonNode.Parse(content)["results"].AsArray()[0]; - var versionName = (string)json["version"]; - if (versionName == _currentVersionOfIOS.VersionName) - _logger.LogInformation( - $"No newer version is available for iOS(current version is {versionName}), check update for iOS ok."); - else + var publishedAt = DateTime.Parse((string)json["currentVersionReleaseDate"]!).ToLocalTime(); + var releaseNote = (string)json["releaseNotes"]!; + var version = new Version { - var publishedAt = DateTime.Parse((string)json["currentVersionReleaseDate"]).ToLocalTime(); - var releaseNote = (string)json["releaseNotes"]; - var version = new Version() - { - CreatedTime = publishedAt, - IsAndroid = false, - ReleaseNote = releaseNote, - VersionName = versionName - }; - var result = await _data.CreateVersionAsync(version); - if (result != 1) throw new Exception("Unknown Error"); - else - _logger.LogInformation( - $"Found new version for iOS: {versionName}, check update for iOS ok"); - } + CreatedTime = publishedAt, + IsAndroid = false, + ReleaseNote = releaseNote, + VersionName = versionName + }; + var result = await data.CreateVersionAsync(version); + if (result != 1) + throw new Exception("Unknown Error"); + logger.LogInformation("Found new version for iOS: {VersionName}, check update for iOS ok", versionName); } } } - catch (Exception ex) - { - _logger.LogError(ex, $"Checking update for {os} failed."); - } - finally - { - var version = await _data.GetVersionAsync(os == OS.Android); - if (os == OS.Android) _currentVersionOfAndroid = version; - else _currentVersionOfIOS = version; - IsRunning = false; - } } - public enum OS + catch (Exception ex) + { + logger.LogError(ex, "Checking update for {OS} failed", os); + } + finally { - Android, - IOS + var version = await data.GetVersionAsync(os == OS.Android) ?? new Version(); + if (os == OS.Android) + _currentVersionOfAndroid = version; + else + _currentVersionOfIOS = version; + IsRunning = false; } } } diff --git a/ThuInfoWeb/Views/Home/Announce.cshtml b/ThuInfoWeb/Views/Home/Announce.cshtml index c53317e..8d8b67d 100644 --- a/ThuInfoWeb/Views/Home/Announce.cshtml +++ b/ThuInfoWeb/Views/Home/Announce.cshtml @@ -72,11 +72,11 @@ - @if ((int)ViewData["page"] != 1) + @if ((int)ViewData["page"]! != 1) { - 上一页 + 上一页 } - 下一页 + 下一页 - - - - @await RenderSectionAsync("Scripts", required: false) + + + + + +@await RenderSectionAsync("Scripts", false)