diff --git a/.DS_Store b/.DS_Store index 950724b..3234296 100755 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/Add.cshtml b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/Add.cshtml new file mode 100644 index 0000000..1526aed --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/Add.cshtml @@ -0,0 +1,31 @@ +@page "/admin/role/updaterole/{handler?}/" +@model Album.Areas.Admin.Pages.Role.AddModel +@{ + var btnText = Model.IsUpdate ? "Cập nhật" : "Tạo mới"; +} + + +

@ViewData["Title"]

+ + +
+
+
+
+ + +
+ + + +
+ + + Danh sách +
+
+
+ +@section Scripts { + +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/Add.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/Add.cshtml.cs new file mode 100644 index 0000000..018317d --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/Add.cshtml.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace Album.Areas.Admin.Pages.Role { + public class AddModel : PageModel { + private readonly RoleManager _roleManager; + + public AddModel (RoleManager roleManager) { + _roleManager = roleManager; + } + + [TempData] // Sử dụng Session + public string StatusMessage { get; set; } + + public class InputModel { + public string ID { set; get; } + + [Required (ErrorMessage = "Phải nhập tên role")] + [Display (Name = "Tên của Role")] + [StringLength (100, ErrorMessage = "{0} dài {2} đến {1} ký tự.", MinimumLength = 3)] + public string Name { set; get; } + + } + [BindProperty] + public InputModel Input { set; get; } + + [BindProperty] + public bool IsUpdate { set; get; } + + public IActionResult OnGet () => NotFound ("Không thấy"); + public IActionResult OnPost () => NotFound ("Không thấy"); + public IActionResult OnPostStartNewRole () { + StatusMessage = "Hãy nhập thông tin để tạo role mới"; + IsUpdate = false; + ModelState.Clear (); + return Page (); + } + public async Task OnPostStartUpdate () { + StatusMessage = null; + IsUpdate = true; + if (Input.ID == null) { + StatusMessage = "Error: Không có thông tin về Role"; + return Page (); + } + var result = await _roleManager.FindByIdAsync (Input.ID); + if (result != null) { + Input.Name = result.Name; + ViewData["Title"] = "Cập nhật role : " + Input.Name; + ModelState.Clear (); + } else { + StatusMessage = "Error: Không có thông tin về Role ID = " + Input.ID; + } + + return Page (); + } + + public async Task OnPostAddOrUpdate () { + + if (!ModelState.IsValid) { + StatusMessage = null; + return Page (); + } + + if (IsUpdate) { + // CẬP NHẬT + if (Input.ID == null) { + ModelState.Clear (); + StatusMessage = "Error: Không có thông tin về role"; + return Page (); + } + var result = await _roleManager.FindByIdAsync (Input.ID); + if (result != null) { + result.Name = Input.Name; + var roleUpdateRs = await _roleManager.UpdateAsync (result); + if (roleUpdateRs.Succeeded) { + StatusMessage = "Đã cập nhật role thành công"; + } else { + StatusMessage = "Error: "; + foreach (var er in roleUpdateRs.Errors) { + StatusMessage += er.Description; + } + } + } else { + StatusMessage = "Error: Không tìm thấy Role cập nhật"; + } + + } else { + // TẠO MỚI + var newRole = new IdentityRole (Input.Name); + var rsNewRole = await _roleManager.CreateAsync (newRole); + if (rsNewRole.Succeeded) { + StatusMessage = $"Đã tạo role mới thành công: {newRole.Name}"; + return RedirectToPage("./Index"); + } else { + StatusMessage = "Error: "; + foreach (var er in rsNewRole.Errors) { + StatusMessage += er.Description; + } + } + } + + return Page (); + + } + } +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/AddUserRole.cshtml b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/AddUserRole.cshtml new file mode 100644 index 0000000..3485b82 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/AddUserRole.cshtml @@ -0,0 +1,43 @@ +@page +@model Album.Areas.Admin.Pages.Role.AddUserRole +@{ + ViewData["Title"] = "Cập nhật role cho User"; +} + +

@ViewData["Title"]

+
+
+

Chọn các role gán cho @Model.Input.Name

+
+ +
+ @Html.LabelFor(x => x.Input.RoleNames) + @Html.ListBoxFor(x => x.Input.RoleNames, + new SelectList( Model.AllRoles ), + new {@class="w-100", id = "selectrole"}) +
+ +
+ + + Danh sách + + +
+
+ +@section Scripts { + + + + + + +} + diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/AddUserRole.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/AddUserRole.cshtml.cs new file mode 100644 index 0000000..d8e246c --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/AddUserRole.cshtml.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Album.Areas.Admin.Pages.Role { + public class AddUserRole : PageModel { + private readonly RoleManager _roleManager; + private readonly UserManager _userManager; + + + public AddUserRole (RoleManager roleManager, + UserManager userManager) { + _roleManager = roleManager; + _userManager = userManager; + } + + public class InputModel { + [Required] + public string ID { set; get; } + public string Name { set; get; } + + public string[] RoleNames {set; get;} + + } + + [BindProperty] + public InputModel Input { set; get; } + + [BindProperty] + public bool isConfirmed { set; get; } + + [TempData] // Sử dụng Session + public string StatusMessage { get; set; } + + public IActionResult OnGet () => NotFound ("Không thấy"); + + public List AllRoles {set; get;} = new List(); + + public async Task OnPost () { + + + var user = await _userManager.FindByIdAsync (Input.ID); + if (user == null) { + return NotFound ("Không thấy role cần xóa"); + } + + var roles = await _userManager.GetRolesAsync(user); + var allroles = await _roleManager.Roles.ToListAsync(); + + allroles.ForEach((r) => { + AllRoles.Add(r.Name); + }); + + if (!isConfirmed) { + Input.RoleNames = roles.ToArray(); + isConfirmed = true; + StatusMessage = ""; + ModelState.Clear(); + } + else { + // Update add and remove + StatusMessage = "Vừa cập nhật"; + if (Input.RoleNames == null) Input.RoleNames = new string[] {}; + foreach (var rolename in Input.RoleNames) + { + if (roles.Contains(rolename)) continue; + await _userManager.AddToRoleAsync(user, rolename); + } + foreach (var rolename in roles) + { + if (Input.RoleNames.Contains(rolename)) continue; + await _userManager.RemoveFromRoleAsync(user, rolename); + } + + } + + Input.Name = user.UserName; + return Page (); + } + } +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/Delete.cshtml b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/Delete.cshtml new file mode 100644 index 0000000..c20ee10 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/Delete.cshtml @@ -0,0 +1,24 @@ +@page +@model Album.Areas.Admin.Pages.Role.DeleteModel +@{ + ViewData["Title"] = "Xóa role"; +} + +

@ViewData["Title"]

+
+
+

Bạn có chăc chắn xóa Role @Model.Input.Name

+
+
+ + + Danh sách + +
+
+
+ +@section Scripts { + +} + diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/Delete.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/Delete.cshtml.cs new file mode 100644 index 0000000..af99b8f --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/Delete.cshtml.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace Album.Areas.Admin.Pages.Role { + public class DeleteModel : PageModel { + private readonly RoleManager _roleManager; + + public DeleteModel (RoleManager roleManager) { + _roleManager = roleManager; + } + + public class InputModel { + [Required] + public string ID { set; get; } + public string Name { set; get; } + + } + + [BindProperty] + public InputModel Input { set; get; } + + [BindProperty] + public bool isConfirmed { set; get; } + + [TempData] // Sử dụng Session + public string StatusMessage { get; set; } + + public IActionResult OnGet () => NotFound ("Không thấy"); + + public async Task OnPost () { + + if (!ModelState.IsValid) { + return NotFound ("Không xóa được"); + } + + var role = await _roleManager.FindByIdAsync (Input.ID); + if (role == null) { + return NotFound ("Không thấy role cần xóa"); + } + + ModelState.Clear (); + + if (isConfirmed) { + //Xóa + await _roleManager.DeleteAsync (role); + StatusMessage = "Đã xóa " + role.Name; + + return RedirectToPage ("Index"); + } else { + Input.Name = role.Name; + isConfirmed = true; + + } + + return Page (); + } + } +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/Index.cshtml b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/Index.cshtml new file mode 100644 index 0000000..8287d7a --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/Index.cshtml @@ -0,0 +1,46 @@ +@page "/admin/role/" +@model IndexModel + +

Danh sách các role

+ + +
+ +
+Gán role cho người dùng + + + + + + + + + @foreach (var role in @Model.roles) + { + + + + + + } +
Role IDTênTác vụ
@role.Id@role.Name +
+ +
+ + + + Claims + +
+ +
+ +
\ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/Index.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/Index.cshtml.cs new file mode 100644 index 0000000..07f2d27 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/Index.cshtml.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Album.Areas.Admin.Pages.Role +{ + + public class IndexModel : PageModel + { + private readonly RoleManager _roleManager; + + public IndexModel(RoleManager roleManager) + { + _roleManager = roleManager; + } + public List roles {set; get;} + + [TempData] // Sử dụng Session lưu thông báo + public string StatusMessage { get; set; } + + public async Task OnGet() + { + roles = await _roleManager.Roles.ToListAsync(); + return Page(); + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/User.cshtml b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/User.cshtml new file mode 100644 index 0000000..cef13eb --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/User.cshtml @@ -0,0 +1,49 @@ +@page "/admin/role/users/" +@model Album.Areas.Admin.Pages.Role.UserModel +@{ + ViewData["Title"] = "DANH SÁCH NGƯỜI DÙNG"; +} + + +

@ViewData["Title"]

+ + + + + + + + + @foreach (var user in @Model.users) + { + + + + + + + } +
UserNameRolesActions
@user.UserName@user.listroles +
+ +
+
+ +@section Scripts { + +} +@{ + + Func generateUrl = (int? _pagenumber) => { + return Url.Page("./User", new {pageNumber = _pagenumber}); + }; + + var datapaging = new { + currentPage = Model.pageNumber, + countPages = Model.totalPages, + generateUrl = generateUrl + }; + +} + diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/User.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/User.cshtml.cs new file mode 100644 index 0000000..750b20c --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/User.cshtml.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using Microsoft.EntityFrameworkCore; + +namespace Album.Areas.Admin.Pages.Role { + public class UserModel : PageModel { + const int USER_PER_PAGE = 10; + private readonly RoleManager _roleManager; + private readonly UserManager _userManager; + + private readonly SignInManager _signManager; + + + public UserModel (RoleManager roleManager, + UserManager userManager, + SignInManager signManager) { + _roleManager = roleManager; + _userManager = userManager; + _signManager = signManager; + } + + public class UserInList : AppUser { + // Liệt kê các Role của User ví dụ: "Admin,Editor" ... + public string listroles {set; get;} + } + + public List users; + public int totalPages {set; get;} + + [TempData] // Sử dụng Session + public string StatusMessage { get; set; } + + [BindProperty(SupportsGet=true)] + public int pageNumber {set;get;} + + public IActionResult OnPost() => NotFound("Cấm post"); + + public async Task OnGet() { + + var cuser = await _userManager.GetUserAsync(User); + + // await _userManager.AddClaimAsync(cuser, new System.Security.Claims.Claim("X", "G")); + var roleeditor = await _roleManager.FindByNameAsync("Editor"); + // await _roleManager.AddClaimAsync(roleeditor, new System.Security.Claims.Claim("X", "Y")); + // await _roleManager.AddClaimAsync(roleeditor, new System.Security.Claims.Claim("X", "Z")); + + // var cls = await _userManager.GetClaimsAsync(cuser); + // foreach(var cl in cls) { + // Console.WriteLine("User Claim" + cl.Type+ " Value:" + cl.Value); + // } + + // cls = await _roleManager.GetClaimsAsync(roleeditor); + // foreach(var cl in cls) { + // Console.WriteLine("Role Claim" + cl.Type+ " Value:" + cl.Value); + // } + + + + + if (pageNumber == 0) + pageNumber = 1; + + var lusers = (from u in _userManager.Users + orderby u.UserName + select new UserInList() { + Id = u.Id, UserName = u.UserName, + }); + + + int totalUsers = await lusers.CountAsync(); + + + totalPages = (int)Math.Ceiling((double)totalUsers / USER_PER_PAGE); + + users = await lusers.Skip(USER_PER_PAGE * (pageNumber - 1)).Take(USER_PER_PAGE).ToListAsync(); + + // users.ForEach(async (user) => { + // var roles = await _userManager.GetRolesAsync(user); + // user.listroles = string.Join(",", roles.ToList()); + // }); + + foreach (var user in users) + { + var roles = await _userManager.GetRolesAsync(user); + user.listroles = string.Join(",", roles.ToList()); + } + + return Page(); + } + } +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/_StatusMessage.cshtml b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/_StatusMessage.cshtml new file mode 100644 index 0000000..208a424 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/_StatusMessage.cshtml @@ -0,0 +1,10 @@ +@model string + +@if (!String.IsNullOrEmpty(Model)) +{ + var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; + +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/_ViewImports.cshtml b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/_ViewImports.cshtml new file mode 100644 index 0000000..9d5fdff --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@using Album.Areas.Admin.Pages.Role +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/_ViewStart.cshtml b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/_ViewStart.cshtml new file mode 100644 index 0000000..a0f2b94 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/Role/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "/Views/Shared/_Layout.cshtml"; +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Create.cshtml b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Create.cshtml new file mode 100644 index 0000000..d2ed277 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Create.cshtml @@ -0,0 +1,39 @@ +@page +@model Album.Pages.Blog.CreateModel + +@{ + ViewData["Title"] = "Tạo Claim"; +} + +

Tạo Claim cho role @Model.role.Name

+ +

Biện tập Claim

+
+
+
+
+
+
+ + + +
+
+ + + +
+
+ +
+
+
+
+ + + +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Create.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Create.cshtml.cs new file mode 100644 index 0000000..107617b --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Create.cshtml.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Mvc.Rendering; +using mvcblog.Models; +using mvcblog.Data; +using Microsoft.AspNetCore.Identity; + +namespace Album.Pages.Blog +{ + public class CreateModel : PageModel + { + private readonly AppDbContext _context; + private readonly RoleManager _roleManager; + + public IdentityRole role {set; get;} + + public CreateModel(AppDbContext context, RoleManager roleManager) + { + _context = context; + _roleManager = roleManager; + } + + async Task GetRole() { + if (string.IsNullOrEmpty(roleid)) return null; + return await _roleManager.FindByIdAsync(roleid); + } + + public async Task OnGet() + { + role = await GetRole(); + if (role == null) + return NotFound("Không thấy Role"); + return Page(); + } + + + + [BindProperty(SupportsGet=true)] + public string roleid {set; get;} + + [BindProperty] + public IdentityRoleClaim EditClaim { get; set; } + + // To protect from overposting attacks, enable the specific properties you want to bind to, for + // more details, see https://aka.ms/RazorPagesCRUD. + public async Task OnPostAsync() + { + role = await GetRole(); + if (role == null) + return NotFound("Không thấy Role"); + + if (!ModelState.IsValid) + { + return Page(); + } + + EditClaim.RoleId = roleid; + + _context.RoleClaims.Add(EditClaim); + await _context.SaveChangesAsync(); + + return RedirectToPage("./Index", new {roleid = roleid}); + } + } +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Delete.cshtml b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Delete.cshtml new file mode 100644 index 0000000..6766040 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Delete.cshtml @@ -0,0 +1,34 @@ +@page +@model Album.Pages.Blog.DeleteModel + +@{ + ViewData["Title"] = "Xóa Claim"; +} + +

Xóa

+ +

Có chắc chắn xóa claim?

+
+

Claim

+
+
+
+ @Html.DisplayNameFor(model => model.EditClaim.ClaimType) +
+
+ @Html.DisplayFor(model => model.EditClaim.ClaimType) +
+
+ @Html.DisplayNameFor(model => model.EditClaim.ClaimValue) +
+
+ @Html.DisplayFor(model => model.EditClaim.ClaimValue) +
+
+ +
+ + | + Quay lại danh sách claim +
+
diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Delete.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Delete.cshtml.cs new file mode 100644 index 0000000..81cd920 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Delete.cshtml.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Album.Areas.Admin.Pages.RoleClaims; +using mvcblog.Data; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; + +namespace Album.Pages.Blog { + public class DeleteModel : PageModel { + private readonly AppDbContext _context; + private readonly RoleManager _roleManager; + + public IdentityRole role { set; get; } + + [BindProperty (SupportsGet = true)] + public string roleid { set; get; } + + public DeleteModel (AppDbContext context, RoleManager roleManager) { + _context = context; + _roleManager = roleManager; + } + + async Task GetRole () { + + if (string.IsNullOrEmpty (roleid)) return null; + return await _roleManager.FindByIdAsync (roleid); + } + + [BindProperty] + public IdentityRoleClaim EditClaim { get; set; } + + public async Task OnGetAsync (int? id) { + role = await GetRole (); + if (role == null) + return NotFound ("Không thấy Role"); + + if (id == null) { + return NotFound (); + } + + EditClaim = await _context.RoleClaims.FirstOrDefaultAsync (m => m.Id == id); + + if (EditClaim == null) { + return NotFound (); + } + return Page (); + } + + public async Task OnPostAsync (int? id) { + + role = await GetRole (); + if (role == null) + return NotFound ("Không thấy Role"); + + if (id == null) { + return NotFound (); + } + + EditClaim = await _context.RoleClaims.FindAsync (id); + + if (EditClaim != null) { + _context.RoleClaims.Remove (EditClaim); + await _context.SaveChangesAsync (); + } + + return RedirectToPage ("./Index", new {roleid = roleid}); + } + } +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Edit.cshtml b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Edit.cshtml new file mode 100644 index 0000000..0767d17 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Edit.cshtml @@ -0,0 +1,40 @@ +@page +@model Album.Pages.Blog.EditModel + +@{ + ViewData["Title"] = "Edit"; +} + +

Edit

+ +

EditClaim

+
+
+
+
+
+ +
+ + + +
+
+ + + +
+
+ +
+
+
+
+ + + +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Edit.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Edit.cshtml.cs new file mode 100644 index 0000000..78e7c18 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Edit.cshtml.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Album.Areas.Admin.Pages.RoleClaims; +using mvcblog.Data; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.EntityFrameworkCore; + +namespace Album.Pages.Blog { + public class EditModel : PageModel { + private readonly AppDbContext _context; + private readonly RoleManager _roleManager; + public IdentityRole role { set; get; } + + [BindProperty (SupportsGet = true)] + public string roleid { set; get; } + public EditModel (AppDbContext context, RoleManager roleManager) { + _context = context; + _roleManager = roleManager; + } + async Task GetRole () { + + if (string.IsNullOrEmpty (roleid)) return null; + return await _roleManager.FindByIdAsync (roleid); + } + + [BindProperty] + public IdentityRoleClaim EditClaim { get; set; } + + public async Task OnGetAsync (int? id) { + role = await GetRole (); + if (role == null) + return NotFound ("Không thấy Role"); + + + if (id == null) { + return NotFound (); + } + + EditClaim = await _context.RoleClaims.FirstOrDefaultAsync (m => m.Id == id); + + if (EditClaim == null) { + return NotFound (); + } + return Page (); + } + + // To protect from overposting attacks, enable the specific properties you want to bind to, for + // more details, see https://aka.ms/RazorPagesCRUD. + public async Task OnPostAsync () { + role = await GetRole (); + if (role == null) + return NotFound ("Không thấy Role"); + + if (!ModelState.IsValid) { + return Page (); + } + + + EditClaim.RoleId = roleid; + + _context.Attach (EditClaim).State = EntityState.Modified; + + try { + await _context.SaveChangesAsync (); + } catch (DbUpdateConcurrencyException) { + if (!EditClaimExists (EditClaim.Id)) { + return NotFound (); + } else { + throw; + } + } + + return RedirectToPage ("./Index", new {roleid = roleid}); + } + + private bool EditClaimExists (int id) { + return _context.RoleClaims.Any (e => e.Id == id); + } + } +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/EditClaim.cs b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/EditClaim.cs new file mode 100644 index 0000000..d6c24fa --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/EditClaim.cs @@ -0,0 +1,9 @@ +namespace Album.Areas.Admin.Pages.RoleClaims +{ + public class EditClaim { + public int Id {set; get;} + public string ClaimType {set; get;} + public string ClaimValue {set; get;} + } + +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Index.cshtml b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Index.cshtml new file mode 100644 index 0000000..7ea3f61 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Index.cshtml @@ -0,0 +1,46 @@ +@page +@model Album.Areas.Admin.Pages.RoleClaims.IndexModel + +@{ + ViewData["Title"] = "Claim cho:" + Model.role.Name; +} + +

Role: @Model.role.Name

+ +

+ Thêm Claim +

+ + + + + + + + + + +@foreach (var item in Model.claims) { + + + + + + +} + +
ID + @Html.DisplayNameFor(model => model.claims[0].ClaimType) + + @Html.DisplayNameFor(model => model.claims[0].ClaimValue) +
+ @Html.DisplayFor(modelItem => item.Id) + + @Html.DisplayFor(modelItem => item.ClaimType) + + @Html.DisplayFor(modelItem => item.ClaimValue) + + Edit | + Delete +
+Danh sách ROLES diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Index.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Index.cshtml.cs new file mode 100644 index 0000000..ee8a952 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/Index.cshtml.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using mvcblog.Data; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Album.Areas.Admin.Pages.RoleClaims +{ + + public class IndexModel : PageModel + { + private readonly RoleManager _roleManager; + private readonly AppDbContext _dbContext; + + public IndexModel(RoleManager roleManager, AppDbContext appDbContext) + { + _dbContext = appDbContext; + _roleManager = roleManager; + } + public List roles {set; get;} + + [BindProperty(SupportsGet = true)] + public string roleid {set; get;} + + public IdentityRole role {set; get;} + + [TempData] // Sử dụng Session lưu thông báo + public string StatusMessage { get; set; } + + + public IList claims { get;set; } + + public async Task OnGet() + { + Console.WriteLine(roleid); + if (string.IsNullOrEmpty(roleid)) + return NotFound("Không có role"); + + role = await _roleManager.FindByIdAsync(roleid); + + if (role == null) + return NotFound("Không có role"); + + + + claims = await (from c in _dbContext.RoleClaims + where c.RoleId == roleid + select new EditClaim() { + Id = c.Id, + ClaimType = c.ClaimType, + ClaimValue = c.ClaimValue + }).ToListAsync(); + + return Page(); + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/_StatusMessage.cshtml b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/_StatusMessage.cshtml new file mode 100644 index 0000000..208a424 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/_StatusMessage.cshtml @@ -0,0 +1,10 @@ +@model string + +@if (!String.IsNullOrEmpty(Model)) +{ + var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; + +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/_ViewImports.cshtml b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/_ViewImports.cshtml new file mode 100644 index 0000000..197f56a --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@using Album.Areas.Admin.Pages.RoleClaims +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/_ViewStart.cshtml b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/_ViewStart.cshtml new file mode 100644 index 0000000..a0f2b94 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Admin/Pages/RoleClaims/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "/Views/Shared/_Layout.cshtml"; +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/IdentityHostingStartup.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/IdentityHostingStartup.cs new file mode 100644 index 0000000..dd4da75 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/IdentityHostingStartup.cs @@ -0,0 +1,22 @@ +using System; +using mvcblog.Data; +using mvcblog.Models; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +[assembly: HostingStartup(typeof(Album.Areas.Identity.IdentityHostingStartup))] +namespace Album.Areas.Identity +{ + public class IdentityHostingStartup : IHostingStartup + { + public void Configure(IWebHostBuilder builder) + { + builder.ConfigureServices((context, services) => { + }); + } + } +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/AccessDenied.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/AccessDenied.cshtml new file mode 100644 index 0000000..b7cd054 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/AccessDenied.cshtml @@ -0,0 +1,10 @@ +@page +@model AccessDeniedModel +@{ + ViewData["Title"] = "Cấm truy cập"; +} + +
+

@ViewData["Title"]

+

Bạn không được phép truy cập vào tài nguyên đã yêu cầu.

+
diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/AccessDenied.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/AccessDenied.cshtml.cs new file mode 100644 index 0000000..200e54f --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/AccessDenied.cshtml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Album.Areas.Identity.Pages.Account +{ + public class AccessDeniedModel : PageModel + { + public void OnGet() + { + + } + } +} + diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ConfirmEmail.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ConfirmEmail.cshtml new file mode 100644 index 0000000..96590cb --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ConfirmEmail.cshtml @@ -0,0 +1,8 @@ +@page "/confirm-email/" +@model ConfirmEmailModel +@{ + ViewData["Title"] = "Xác thực email"; +} + +

@ViewData["Title"]

+

@Model.StatusMessage

\ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs new file mode 100644 index 0000000..a7c1422 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; +using XTLASPNET; + +namespace Album.Areas.Identity.Pages.Account { + [AllowAnonymous] + public class ConfirmEmailModel : PageModel { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public ConfirmEmailModel (UserManager userManager, SignInManager signInManager) { + _userManager = userManager; + _signInManager = signInManager; + } + + [TempData] + public string StatusMessage { get; set; } + + public async Task OnGetAsync (string userId, string code, string returnUrl) { + + if (userId == null || code == null) { + return RedirectToPage ("/Index"); + } + + + var user = await _userManager.FindByIdAsync (userId); + if (user == null) { + return NotFound ($"Không tồn tại User - '{userId}'."); + } + + code = Encoding.UTF8.GetString (WebEncoders.Base64UrlDecode (code)); + // Xác thực email + var result = await _userManager.ConfirmEmailAsync (user, code); + + if (result.Succeeded) { + + // Đăng nhập luôn nếu xác thực email thành công + await _signInManager.SignInAsync(user, false); + + return ViewComponent (MessagePage.COMPONENTNAME, + new MessagePage.Message () { + title = "Xác thực email", + htmlcontent = "Đã xác thực thành công, đang chuyển hướng", + urlredirect = (returnUrl != null) ? returnUrl : Url.Page ("/Index") + } + ); + } else { + StatusMessage = "Lỗi xác nhận email"; + } + return Page (); + } + } +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml new file mode 100644 index 0000000..7222a44 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml @@ -0,0 +1,8 @@ +@page +@model ConfirmEmailChangeModel +@{ + ViewData["Title"] = "Xác nhận email thay đổi"; +} + +

@ViewData["Title"]

+ \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml.cs new file mode 100644 index 0000000..c269fd3 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; + +namespace Album.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ConfirmEmailChangeModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public ConfirmEmailChangeModel(UserManager userManager, SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + [TempData] + public string StatusMessage { get; set; } + + public async Task OnGetAsync(string userId, string email, string code) + { + if (userId == null || email == null || code == null) + { + return RedirectToPage("/Index"); + } + + var user = await _userManager.FindByIdAsync(userId); + if (user == null) + { + return NotFound($"Không nạp được ID '{userId}'."); + } + + code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code)); + var result = await _userManager.ChangeEmailAsync(user, email, code); + if (!result.Succeeded) + { + StatusMessage = "Lỗi đổi email."; + foreach (var item in result.Errors) + { + StatusMessage += item.Description; + } + + + return Page(); + } + + /* + + // In our UI email and user name are one and the same, so when we update the email + // we need to update the user name. + var setUserNameResult = await _userManager.SetUserNameAsync(user, email); + if (!setUserNameResult.Succeeded) + { + StatusMessage = "Error changing user name."; + return Page(); + } + */ + + await _signInManager.RefreshSignInAsync(user); + StatusMessage = "Email đã thay đổi."; + return Page(); + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ExternalLogin.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ExternalLogin.cshtml new file mode 100644 index 0000000..c083169 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ExternalLogin.cshtml @@ -0,0 +1,32 @@ +@page +@model ExternalLoginModel +@{ + ViewData["Title"] = "ĐĂNG KÝ TÀI KHOẢN"; +} + +

@ViewData["Title"]

+

Thực hiện liên kết với tài khoản @Model.ProviderDisplayName.

+
+ +

+ Bạn đã xác thực với tài khoản @Model.ProviderDisplayName. + Vui lòng nhập (hoặc xác nhận) chính xác email để liên kết và đăng nhập. +

+ +
+
+
+
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs new file mode 100644 index 0000000..dad4607 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; +using XTLASPNET; + +namespace Album.Areas.Identity.Pages.Account { + [AllowAnonymous] + public class ExternalLoginModel : PageModel { + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + private readonly IEmailSender _emailSender; + private readonly ILogger _logger; + + public ExternalLoginModel ( + SignInManager signInManager, + UserManager userManager, + ILogger logger, + IEmailSender emailSender) { + _signInManager = signInManager; + _userManager = userManager; + _logger = logger; + _emailSender = emailSender; + } + + [BindProperty] + public InputModel Input { get; set; } + + public string ProviderDisplayName { get; set; } + + public string ReturnUrl { get; set; } + + [TempData] + public string ErrorMessage { get; set; } + + public class InputModel { + [Required] + [EmailAddress] + [Display (Name = "Địa chỉ email")] + public string Email { get; set; } + } + + public IActionResult OnGetAsync () { + return RedirectToPage ("./Login"); + } + + // Post yêu cầu login bằng dịch vụ ngoài + // Provider = Google, Facebook ... + public async Task OnPost (string provider, string returnUrl = null) { + // Kiểm tra yêu cầu dịch vụ provider tồn tại + var listprovider = (await _signInManager.GetExternalAuthenticationSchemesAsync ()).ToList (); + var provider_process = listprovider.Find ((m) => m.Name == provider); + if (provider_process == null) { + return NotFound ("Dịch vụ không chính xác: " + provider); + } + + // redirectUrl - là Url sẽ chuyển hướng đến - sau khi CallbackPath (/dang-nhap-tu-google) thi hành xong + // nó bằng identity/account/externallogin?handler=Callback + // tức là gọi OnGetCallbackAsync + var redirectUrl = Url.Page ("./ExternalLogin", pageHandler: "Callback", values : new { returnUrl }); + + // Cấu hình + var properties = _signInManager.ConfigureExternalAuthenticationProperties (provider, redirectUrl); + + // Chuyển hướng đến dịch vụ ngoài (Googe, Facebook) + return new ChallengeResult (provider, properties); + } + + public async Task OnGetCallbackAsync (string returnUrl = null, string remoteError = null) { + returnUrl = returnUrl ?? Url.Content ("~/"); + if (remoteError != null) { + ErrorMessage = $"Lỗi provider: {remoteError}"; + return RedirectToPage ("./Login", new { ReturnUrl = returnUrl }); + } + + // Lấy thông tin do dịch vụ ngoài chuyển đến + var info = await _signInManager.GetExternalLoginInfoAsync (); + if (info == null) { + ErrorMessage = "Lỗi thông tin từ dịch vụ đăng nhập."; + return RedirectToPage ("./Login", new { ReturnUrl = returnUrl }); + } + + // Đăng nhập bằng thông tin LoginProvider, ProviderKey từ info cung cấp bởi dịch vụ ngoài + // User nào có 2 thông tin này sẽ được đăng nhập - thông tin này lưu tại bảng UserLogins của Database + // Trường LoginProvider và ProviderKey ---> tương ứng UserId + var result = await _signInManager.ExternalLoginSignInAsync (info.LoginProvider, info.ProviderKey, isPersistent : false, bypassTwoFactor : true); + + if (result.Succeeded) { + // User đăng nhập thành công vào hệ thống theo thông tin info + _logger.LogInformation ("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider); + return LocalRedirect (returnUrl); + } + if (result.IsLockedOut) { + // Bị tạm khóa + return RedirectToPage ("./Lockout"); + } else { + + var userExisted = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey); + if (userExisted != null) { + // Đã có Acount, đã liên kết với tài khoản ngoài - nhưng không đăng nhập được + // có thể do chưa kích hoạt email + return RedirectToPage ("./RegisterConfirmation", new { Email = userExisted.Email }); + + } + + // Chưa có Account liên kết với tài khoản ngoài + // Hiện thị form để thực hiện bước tiếp theo ở OnPostConfirmationAsync + ReturnUrl = returnUrl; + ProviderDisplayName = info.ProviderDisplayName; + if (info.Principal.HasClaim (c => c.Type == ClaimTypes.Email)) { + // Có thông tin về email từ info, lấy email này hiện thị ở Form + Input = new InputModel { + Email = info.Principal.FindFirstValue (ClaimTypes.Email) + }; + } + + return Page (); + } + } + + public async Task OnPostConfirmationAsync (string returnUrl = null) { + returnUrl = returnUrl ?? Url.Content ("~/"); + // Lấy lại Info + var info = await _signInManager.GetExternalLoginInfoAsync (); + if (info == null) { + ErrorMessage = "Không có thông tin tài khoản ngoài."; + return RedirectToPage ("./Login", new { ReturnUrl = returnUrl }); + } + + + + if (ModelState.IsValid) { + + string externalMail = null; + if (info.Principal.HasClaim (c => c.Type == ClaimTypes.Email)) { + externalMail = info.Principal.FindFirstValue (ClaimTypes.Email); + } + var userWithexternalMail = (externalMail != null) ? (await _userManager.FindByEmailAsync (externalMail)) : null; + + // Xử lý khi có thông tin về email từ info, đồng thời có user với email đó + // trường hợp này sẽ thực hiện liên kết tài khoản ngoài + xác thực email luôn + if ((userWithexternalMail != null) && (Input.Email == externalMail)) { + // xác nhận email luôn nếu chưa xác nhận + if (!userWithexternalMail.EmailConfirmed) { + var codeactive = await _userManager.GenerateEmailConfirmationTokenAsync (userWithexternalMail); + await _userManager.ConfirmEmailAsync (userWithexternalMail, codeactive); + } + // Thực hiện liên kết info và user + var resultAdd = await _userManager.AddLoginAsync (userWithexternalMail, info); + if (resultAdd.Succeeded) { + // Thực hiện login + await _signInManager.SignInAsync (userWithexternalMail, isPersistent : false); + return ViewComponent (MessagePage.COMPONENTNAME, new MessagePage.Message () { + title = "LIÊN KẾT TÀI KHOẢN", + urlredirect = returnUrl, + htmlcontent = $"Liên kết tài khoản {userWithexternalMail.UserName} với {info.ProviderDisplayName} thành công" + }); + } else { + return ViewComponent (MessagePage.COMPONENTNAME, new MessagePage.Message () { + title = "LIÊN KẾT TÀI KHOẢN", + urlredirect = Url.Page ("Index"), + htmlcontent = $"Liên kết thất bại" + }); + } + } + + // Tài khoản chưa có, tạo tài khoản mới + var user = new AppUser { UserName = Input.Email, Email = Input.Email }; + var result = await _userManager.CreateAsync (user); + if (result.Succeeded) { + + // Liên kết tài khoản ngoài với tài khoản vừa tạo + result = await _userManager.AddLoginAsync (user, info); + if (result.Succeeded) { + _logger.LogInformation ("Đã tạo user mới từ thông tin {Name}.", info.LoginProvider); + // Email tạo tài khoản và email từ info giống nhau -> xác thực email luôn + if (user.Email == externalMail) { + var codeactive = await _userManager.GenerateEmailConfirmationTokenAsync (user); + await _userManager.ConfirmEmailAsync (user, codeactive); + await _signInManager.SignInAsync (user, isPersistent : false, info.LoginProvider); + return ViewComponent (MessagePage.COMPONENTNAME, new MessagePage.Message () { + title = "TẠO VÀ LIÊN KẾT TÀI KHOẢN", + urlredirect = returnUrl, + htmlcontent = $"Đã tạo và liên kết tài khoản, kích hoạt email thành công" + }); + } + + // Trường hợp này Email tạo User khác với Email từ info (hoặc info không có email) + // sẽ gửi email xác để người dùng xác thực rồi mới có thể đăng nhập + var userId = await _userManager.GetUserIdAsync (user); + var code = await _userManager.GenerateEmailConfirmationTokenAsync (user); + code = WebEncoders.Base64UrlEncode (Encoding.UTF8.GetBytes (code)); + var callbackUrl = Url.Page ( + "/Account/ConfirmEmail", + pageHandler : null, + values : new { area = "Identity", userId = userId, code = code }, + protocol : Request.Scheme); + + // Please confirm your account by clicking here. + await _emailSender.SendEmailAsync (Input.Email, "Xác nhận địa chỉ email", + $"Hãy xác nhận địa chỉ email bằng cách bấm vào đây."); + + // Chuyển đến trang thông báo cần kích hoạt tài khoản + if (_userManager.Options.SignIn.RequireConfirmedEmail) { + return RedirectToPage ("./RegisterConfirmation", new { Email = Input.Email }); + } + + // Đăng nhập ngay do không yêu cầu xác nhận email + await _signInManager.SignInAsync (user, isPersistent : false, info.LoginProvider); + + return LocalRedirect (returnUrl); + } + } + foreach (var error in result.Errors) { + ModelState.AddModelError (string.Empty, error.Description); + } + } + + ProviderDisplayName = info.ProviderDisplayName; + ReturnUrl = returnUrl; + return Page (); + } + } +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ForgotPassword.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ForgotPassword.cshtml new file mode 100644 index 0000000..3a02454 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ForgotPassword.cshtml @@ -0,0 +1,26 @@ +@page +@model ForgotPasswordModel +@{ + ViewData["Title"] = "Quyên mật khẩu?"; +} + +

@ViewData["Title"]

+

Nhập email của tài khoản.

+
+
+
+
+
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs new file mode 100644 index 0000000..8d72057 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text.Encodings.Web; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; + +namespace Album.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ForgotPasswordModel : PageModel + { + private readonly UserManager _userManager; + private readonly IEmailSender _emailSender; + + public ForgotPasswordModel(UserManager userManager, IEmailSender emailSender) + { + _userManager = userManager; + _emailSender = emailSender; + } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + [Display(Name = "Nhập chính xác địa chỉ email")] + public string Email { get; set; } + } + + public async Task OnPostAsync() + { + if (ModelState.IsValid) + { + var user = await _userManager.FindByEmailAsync(Input.Email); + if (user == null || !(await _userManager.IsEmailConfirmedAsync(user))) + { + return RedirectToPage("./ForgotPasswordConfirmation"); + } + + // Phát sinh Token để reset password + // Token sẽ được kèm vào link trong email, + // link dẫn đến trang /Account/ResetPassword để kiểm tra và đặt lại mật khẩu + var code = await _userManager.GeneratePasswordResetTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = Url.Page( + "/Account/ResetPassword", + pageHandler: null, + values: new { area = "Identity", code }, + protocol: Request.Scheme); + + // Gửi email + await _emailSender.SendEmailAsync( + Input.Email, + "Đặt lại mật khẩu", + $"Để đặt lại mật khẩu hãy bấm vào đây."); + + // Chuyển đến trang thông báo đã gửi mail để reset password + return RedirectToPage("./ForgotPasswordConfirmation"); + } + + return Page(); + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml new file mode 100644 index 0000000..b90ac1e --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml @@ -0,0 +1,11 @@ +@page +@model ForgotPasswordConfirmation +@{ + ViewData["Title"] = "Xác nhận quyên mật khẩu"; +} + +

@ViewData["Title"]

+

+ Hãy kiểm tra hòm thư email của bạn để thực hiện bước tiếp theo +

+ diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml.cs new file mode 100644 index 0000000..daa1964 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Album.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ForgotPasswordConfirmation : PageModel + { + public void OnGet() + { + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Lockout.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Lockout.cshtml new file mode 100644 index 0000000..728bcea --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Lockout.cshtml @@ -0,0 +1,10 @@ +@page "/lockout/" +@model LockoutModel +@{ + ViewData["Title"] = "Tạm khóa tài khoản"; +} + +
+

@ViewData["Title"]

+

Tài khoản này tạm khóa, vui lòng thử lại sau.

+
diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Lockout.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Lockout.cshtml.cs new file mode 100644 index 0000000..865c537 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Lockout.cshtml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Album.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class LockoutModel : PageModel + { + public void OnGet() + { + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Login.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Login.cshtml new file mode 100644 index 0000000..ed16457 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Login.cshtml @@ -0,0 +1,73 @@ +@page "/login/" +@model LoginModel + +@{ + ViewData["Title"] = "ĐĂNG NHẬP"; +} + +

@ViewData["Title"]

+
+
+
+
+

Điền thông tin để đăng nhập.

+
+
+
+ + + +
+
+ + + +
+
+
+ +
+
+
+ +
+ +
+
+
+ +
+ @if ((Model.ExternalLogins?.Count ?? 0) != 0) { +
+

Sử dụng dịch vụ

+
+
+
+

+ @foreach (var provider in Model.ExternalLogins) + { + + } +

+
+
+
+ } +
+ + +
+ +@section Scripts { + +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Login.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Login.cshtml.cs new file mode 100644 index 0000000..3dd5d7d --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Login.cshtml.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using XTLASPNET; + +namespace Album.Areas.Identity.Pages.Account { + [AllowAnonymous] + public class LoginModel : PageModel { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LoginModel (SignInManager signInManager, + ILogger logger, + UserManager userManager) { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public IList ExternalLogins { get; set; } + + public string ReturnUrl { get; set; } + + [TempData] + public string ErrorMessage { get; set; } + + public class InputModel { + [Required (ErrorMessage = "Không để trống")] + [Display (Name = "Nhập username hoặc email của bạn")] + [StringLength (100, MinimumLength = 1, ErrorMessage = "Nhập đúng thông tin")] + public string UserNameOrEmail { set; get; } + + [Required] + [DataType (DataType.Password)] + [Display(Name = "Mật khẩu")] + public string Password { get; set; } + + [Display (Name = "Nhớ thông tin đăng nhập?")] + public bool RememberMe { get; set; } + } + + public async Task OnGetAsync (string returnUrl = null) { + if (!string.IsNullOrEmpty (ErrorMessage)) { + ModelState.AddModelError (string.Empty, ErrorMessage); + } + + returnUrl = returnUrl ?? Url.Content ("~/"); + + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync (IdentityConstants.ExternalScheme); + + ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync ()).ToList (); + + ReturnUrl = returnUrl; + } + + public async Task OnPostAsync (string returnUrl = null) { + returnUrl = returnUrl ?? Url.Content ("~/"); + // Đã đăng nhập nên chuyển hướng về Index + if (_signInManager.IsSignedIn (User)) return Redirect ("Index"); + + if (ModelState.IsValid) { + + IdentityUser user = await _userManager.FindByEmailAsync (Input.UserNameOrEmail); + if (user == null) + user = await _userManager.FindByNameAsync(Input.UserNameOrEmail); + + if (user == null) + { + ModelState.AddModelError (string.Empty, "Tài khoản không tồn tại."); + return Page (); + } + + var result = await _signInManager.PasswordSignInAsync ( + user.UserName, + Input.Password, + Input.RememberMe, + true + ); + + + if (result.Succeeded) { + _logger.LogInformation ("User đã đăng nhập"); + return ViewComponent(MessagePage.COMPONENTNAME, new MessagePage.Message() { + title = "Đã đăng nhập", + htmlcontent = "Đăng nhập thành công", + urlredirect = returnUrl + }); + } + if (result.RequiresTwoFactor) { + // Nếu cấu hình đăng nhập hai yếu tố thì chuyển hướng đến LoginWith2fa + return RedirectToPage ("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe }); + } + if (result.IsLockedOut) { + _logger.LogWarning ("Tài khoản bí tạm khóa."); + // Chuyển hướng đến trang Lockout - hiện thị thông báo + return RedirectToPage ("./Lockout"); + } else { + ModelState.AddModelError (string.Empty, "Không đăng nhập được."); + return Page (); + } + } + return Page (); + + } + } +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/LoginWith2fa.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/LoginWith2fa.cshtml new file mode 100644 index 0000000..780b4ec --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/LoginWith2fa.cshtml @@ -0,0 +1,41 @@ +@page +@model LoginWith2faModel +@{ + ViewData["Title"] = "Two-factor authentication"; +} + +

@ViewData["Title"]

+
+

Your login is protected with an authenticator app. Enter your authenticator code below.

+
+
+
+ +
+
+ + + +
+
+
+ +
+
+
+ +
+
+
+
+

+ Don't have access to your authenticator device? You can + log in with a recovery code. +

+ +@section Scripts { + +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs new file mode 100644 index 0000000..e48885e --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace Album.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class LoginWith2faModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LoginWith2faModel(SignInManager signInManager, ILogger logger) + { + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public bool RememberMe { get; set; } + + public string ReturnUrl { get; set; } + + public class InputModel + { + [Required] + [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Text)] + [Display(Name = "Authenticator code")] + public string TwoFactorCode { get; set; } + + [Display(Name = "Remember this machine")] + public bool RememberMachine { get; set; } + } + + public async Task OnGetAsync(bool rememberMe, string returnUrl = null) + { + // Ensure the user has gone through the username & password screen first + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + + if (user == null) + { + throw new InvalidOperationException($"Unable to load two-factor authentication user."); + } + + ReturnUrl = returnUrl; + RememberMe = rememberMe; + + return Page(); + } + + public async Task OnPostAsync(bool rememberMe, string returnUrl = null) + { + if (!ModelState.IsValid) + { + return Page(); + } + + returnUrl = returnUrl ?? Url.Content("~/"); + + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + throw new InvalidOperationException($"Unable to load two-factor authentication user."); + } + + var authenticatorCode = Input.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty); + + var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, Input.RememberMachine); + + if (result.Succeeded) + { + _logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", user.Id); + return LocalRedirect(returnUrl); + } + else if (result.IsLockedOut) + { + _logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id); + return RedirectToPage("./Lockout"); + } + else + { + _logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", user.Id); + ModelState.AddModelError(string.Empty, "Invalid authenticator code."); + return Page(); + } + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml new file mode 100644 index 0000000..d866adb --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml @@ -0,0 +1,29 @@ +@page +@model LoginWithRecoveryCodeModel +@{ + ViewData["Title"] = "Recovery code verification"; +} + +

@ViewData["Title"]

+
+

+ You have requested to log in with a recovery code. This login will not be remembered until you provide + an authenticator app code at log in or disable 2FA and log in again. +

+
+
+
+
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml.cs new file mode 100644 index 0000000..28efd1d --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace Album.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class LoginWithRecoveryCodeModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LoginWithRecoveryCodeModel(SignInManager signInManager, ILogger logger) + { + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public string ReturnUrl { get; set; } + + public class InputModel + { + [BindProperty] + [Required] + [DataType(DataType.Text)] + [Display(Name = "Recovery Code")] + public string RecoveryCode { get; set; } + } + + public async Task OnGetAsync(string returnUrl = null) + { + // Ensure the user has gone through the username & password screen first + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + throw new InvalidOperationException($"Unable to load two-factor authentication user."); + } + + ReturnUrl = returnUrl; + + return Page(); + } + + public async Task OnPostAsync(string returnUrl = null) + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + throw new InvalidOperationException($"Unable to load two-factor authentication user."); + } + + var recoveryCode = Input.RecoveryCode.Replace(" ", string.Empty); + + var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); + + if (result.Succeeded) + { + _logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", user.Id); + return LocalRedirect(returnUrl ?? Url.Content("~/")); + } + if (result.IsLockedOut) + { + _logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id); + return RedirectToPage("./Lockout"); + } + else + { + _logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", user.Id); + ModelState.AddModelError(string.Empty, "Invalid recovery code entered."); + return Page(); + } + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Logout.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Logout.cshtml new file mode 100644 index 0000000..9cb0275 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Logout.cshtml @@ -0,0 +1,5 @@ +@page "/logout/" +@model LogoutModel +@{ + ViewData["Title"] = "Log out"; +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Logout.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Logout.cshtml.cs new file mode 100644 index 0000000..2f0b317 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Logout.cshtml.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using XTLASPNET; + +namespace Album.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class LogoutModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LogoutModel(SignInManager signInManager, ILogger logger) + { + _signInManager = signInManager; + _logger = logger; + } + + public async Task OnPost(string returnUrl = null) + { + if (!_signInManager.IsSignedIn(User)) return RedirectToPage("/Index"); + + await _signInManager.SignOutAsync(); + _logger.LogInformation("Người dùng đăng xuất"); + + + return ViewComponent(MessagePage.COMPONENTNAME, + new MessagePage.Message() { + title = "Đã đăng xuất", + htmlcontent = "Đăng xuất thành công", + urlredirect = (returnUrl != null) ? returnUrl : Url.Page("/Index") + } + ); + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml new file mode 100644 index 0000000..27b4c02 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml @@ -0,0 +1,36 @@ +@page +@model ChangePasswordModel +@{ + ViewData["Title"] = "Đổi mật khẩu"; + ViewData["ActivePage"] = ManageNavPages.ChangePassword; +} + +

@ViewData["Title"]

+ +
+
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs new file mode 100644 index 0000000..45e4a25 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +namespace Album.Areas.Identity.Pages.Account.Manage +{ + public class ChangePasswordModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public ChangePasswordModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + public class InputModel + { + [Required] + [DataType(DataType.Password)] + [Display(Name = "Password hiện tại")] + public string OldPassword { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "{0} dài {2} đến {1} ký tự.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Password mới")] + public string NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Nhập lại password mới")] + [Compare("NewPassword", ErrorMessage = "Password phải giống nhau.")] + public string ConfirmPassword { get; set; } + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Lỗi nạp User với ID '{_userManager.GetUserId(User)}'."); + } + + var hasPassword = await _userManager.HasPasswordAsync(user); + if (!hasPassword) + { + return RedirectToPage("./SetPassword"); + } + + return Page(); + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Lỗi nạp User với ID '{_userManager.GetUserId(User)}'."); + } + + var changePasswordResult = await _userManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword); + if (!changePasswordResult.Succeeded) + { + foreach (var error in changePasswordResult.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + return Page(); + } + + await _signInManager.RefreshSignInAsync(user); + _logger.LogInformation("User changed their password successfully."); + StatusMessage = "Đã thay đổi password thành công."; + + return RedirectToPage(); + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml new file mode 100644 index 0000000..c95ab92 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml @@ -0,0 +1,33 @@ +@page +@model DeletePersonalDataModel +@{ + ViewData["Title"] = "Delete Personal Data"; + ViewData["ActivePage"] = ManageNavPages.PersonalData; +} + +

@ViewData["Title"]

+ + + +
+
+
+ @if (Model.RequirePassword) + { +
+ + + +
+ } + +
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs new file mode 100644 index 0000000..038ec17 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs @@ -0,0 +1,84 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace Album.Areas.Identity.Pages.Account.Manage +{ + public class DeletePersonalDataModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public DeletePersonalDataModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel + { + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } + } + + public bool RequirePassword { get; set; } + + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + RequirePassword = await _userManager.HasPasswordAsync(user); + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + RequirePassword = await _userManager.HasPasswordAsync(user); + if (RequirePassword) + { + if (!await _userManager.CheckPasswordAsync(user, Input.Password)) + { + ModelState.AddModelError(string.Empty, "Incorrect password."); + return Page(); + } + } + + var result = await _userManager.DeleteAsync(user); + var userId = await _userManager.GetUserIdAsync(user); + if (!result.Succeeded) + { + throw new InvalidOperationException($"Unexpected error occurred deleting user with ID '{userId}'."); + } + + await _signInManager.SignOutAsync(); + + _logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId); + + return Redirect("~/"); + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml new file mode 100644 index 0000000..96df752 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml @@ -0,0 +1,25 @@ +@page +@model Disable2faModel +@{ + ViewData["Title"] = "Disable two-factor authentication (2FA)"; + ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; +} + + +

@ViewData["Title"]

+ + + +
+
+ +
+
diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs new file mode 100644 index 0000000..fdb8eef --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace Album.Areas.Identity.Pages.Account.Manage +{ + public class Disable2faModel : PageModel + { + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public Disable2faModel( + UserManager userManager, + ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + + [TempData] + public string StatusMessage { get; set; } + + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!await _userManager.GetTwoFactorEnabledAsync(user)) + { + throw new InvalidOperationException($"Cannot disable 2FA for user with ID '{_userManager.GetUserId(User)}' as it's not currently enabled."); + } + + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false); + if (!disable2faResult.Succeeded) + { + throw new InvalidOperationException($"Unexpected error occurred disabling 2FA for user with ID '{_userManager.GetUserId(User)}'."); + } + + _logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", _userManager.GetUserId(User)); + StatusMessage = "2fa has been disabled. You can reenable 2fa when you setup an authenticator app"; + return RedirectToPage("./TwoFactorAuthentication"); + } + } +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml new file mode 100644 index 0000000..87470c2 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml @@ -0,0 +1,12 @@ +@page +@model DownloadPersonalDataModel +@{ + ViewData["Title"] = "Download Your Data"; + ViewData["ActivePage"] = ManageNavPages.PersonalData; +} + +

@ViewData["Title"]

+ +@section Scripts { + +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs new file mode 100644 index 0000000..a51a424 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace Album.Areas.Identity.Pages.Account.Manage +{ + public class DownloadPersonalDataModel : PageModel + { + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public DownloadPersonalDataModel( + UserManager userManager, + ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + _logger.LogInformation("User with ID '{UserId}' asked for their personal data.", _userManager.GetUserId(User)); + + // Only include personal data for download + var personalData = new Dictionary(); + var personalDataProps = typeof(AppUser).GetProperties().Where( + prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute))); + foreach (var p in personalDataProps) + { + personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null"); + } + + var logins = await _userManager.GetLoginsAsync(user); + foreach (var l in logins) + { + personalData.Add($"{l.LoginProvider} external login provider key", l.ProviderKey); + } + + Response.Headers.Add("Content-Disposition", "attachment; filename=PersonalData.json"); + return new FileContentResult(JsonSerializer.SerializeToUtf8Bytes(personalData), "application/json"); + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/Email.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/Email.cshtml new file mode 100644 index 0000000..87fbbbe --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/Email.cshtml @@ -0,0 +1,43 @@ +@page +@model EmailModel +@{ + ViewData["Title"] = "QUẢN LÝ EMAIL"; + ViewData["ActivePage"] = ManageNavPages.Email; +} + +

@ViewData["Title"]

+ +
+
+
+
+
+ + @if (Model.IsEmailConfirmed) + { +
+ +
+ +
+
+ } + else + { + + + } +
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs new file mode 100644 index 0000000..1ddeaaa --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text; +using System.Text.Encodings.Web; +using System.Linq; +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; + +namespace Album.Areas.Identity.Pages.Account.Manage +{ + public partial class EmailModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly IEmailSender _emailSender; + + public EmailModel( + UserManager userManager, + SignInManager signInManager, + IEmailSender emailSender) + { + _userManager = userManager; + _signInManager = signInManager; + _emailSender = emailSender; + } + + public string Username { get; set; } + + public string Email { get; set; } + + public bool IsEmailConfirmed { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + [Display(Name = "Đổi sang email mới")] + public string NewEmail { get; set; } + } + + private async Task LoadAsync(AppUser user) + { + var email = await _userManager.GetEmailAsync(user); + Email = email; + + Input = new InputModel + { + NewEmail = email, + }; + + IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user); + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Khôg nạp được tài khoản ID '{_userManager.GetUserId(User)}'."); + } + + await LoadAsync(user); + return Page(); + } + + public async Task OnPostChangeEmailAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Khôg nạp được tài khoản ID '{_userManager.GetUserId(User)}'."); + } + + if (!ModelState.IsValid) + { + await LoadAsync(user); + return Page(); + } + + var email = await _userManager.GetEmailAsync(user); + if (Input.NewEmail != email) + { + var userId = await _userManager.GetUserIdAsync(user); + var code = await _userManager.GenerateChangeEmailTokenAsync(user, Input.NewEmail); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes (code)); + var callbackUrl = Url.Page( + "/Account/ConfirmEmailChange", + pageHandler: null, + values: new { userId = userId, email = Input.NewEmail, code = code }, + protocol: Request.Scheme); + await _emailSender.SendEmailAsync( + Input.NewEmail, + "Xác nhận", + $"Hãy xác nhận Email của bạn bằng cách bấm vào đây."); + + StatusMessage = "Hãy mở email để xác nhận thay đổi"; + return RedirectToPage(); + } + + StatusMessage = "Bạn đã thay đổi email."; + return RedirectToPage(); + } + + public async Task OnPostSendVerificationEmailAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Khôg nạp được tài khoản ID '{_userManager.GetUserId(User)}'."); + } + + if (!ModelState.IsValid) + { + await LoadAsync(user); + return Page(); + } + + var userId = await _userManager.GetUserIdAsync(user); + var email = await _userManager.GetEmailAsync(user); + var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = Url.Page( + "/Account/ConfirmEmail", + pageHandler: null, + values: new { area = "Identity", userId = userId, code = code }, + protocol: Request.Scheme); + await _emailSender.SendEmailAsync( + email, + "Xác nhận Email", + $"Xác nhận email bấm vào đây."); + + StatusMessage = "Hãy mở email để xác nhận"; + return RedirectToPage(); + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml new file mode 100644 index 0000000..6ea8510 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml @@ -0,0 +1,53 @@ +@page +@model EnableAuthenticatorModel +@{ + ViewData["Title"] = "Configure authenticator app"; + ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; +} + + +

@ViewData["Title"]

+
+

To use an authenticator app go through the following steps:

+
    +
  1. +

    + Download a two-factor authenticator app like Microsoft Authenticator for + Android and + iOS or + Google Authenticator for + Android and + iOS. +

    +
  2. +
  3. +

    Scan the QR Code or enter this key @Model.SharedKey into your two factor authenticator app. Spaces and casing do not matter.

    + +
    +
    +
  4. +
  5. +

    + Once you have scanned the QR code or input the key above, your two factor authentication app will provide you + with a unique code. Enter the code in the confirmation box below. +

    +
    +
    +
    +
    + + + +
    + +
    +
    +
    +
    +
  6. +
+
+ +@section Scripts { + +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs new file mode 100644 index 0000000..922bf25 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs @@ -0,0 +1,157 @@ +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.Text; +using System.Text.Encodings.Web; +using System.Linq; +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace Album.Areas.Identity.Pages.Account.Manage +{ + public class EnableAuthenticatorModel : PageModel + { + private readonly UserManager _userManager; + private readonly ILogger _logger; + private readonly UrlEncoder _urlEncoder; + + private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; + + public EnableAuthenticatorModel( + UserManager userManager, + ILogger logger, + UrlEncoder urlEncoder) + { + _userManager = userManager; + _logger = logger; + _urlEncoder = urlEncoder; + } + + public string SharedKey { get; set; } + + public string AuthenticatorUri { get; set; } + + [TempData] + public string[] RecoveryCodes { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel + { + [Required] + [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Text)] + [Display(Name = "Verification Code")] + public string Code { get; set; } + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + await LoadSharedKeyAndQrCodeUriAsync(user); + + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!ModelState.IsValid) + { + await LoadSharedKeyAndQrCodeUriAsync(user); + return Page(); + } + + // Strip spaces and hypens + var verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty); + + var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync( + user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); + + if (!is2faTokenValid) + { + ModelState.AddModelError("Input.Code", "Verification code is invalid."); + await LoadSharedKeyAndQrCodeUriAsync(user); + return Page(); + } + + await _userManager.SetTwoFactorEnabledAsync(user, true); + var userId = await _userManager.GetUserIdAsync(user); + _logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", userId); + + StatusMessage = "Your authenticator app has been verified."; + + if (await _userManager.CountRecoveryCodesAsync(user) == 0) + { + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + RecoveryCodes = recoveryCodes.ToArray(); + return RedirectToPage("./ShowRecoveryCodes"); + } + else + { + return RedirectToPage("./TwoFactorAuthentication"); + } + } + + private async Task LoadSharedKeyAndQrCodeUriAsync(AppUser user) + { + // Load the authenticator key & QR code URI to display on the form + var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); + if (string.IsNullOrEmpty(unformattedKey)) + { + await _userManager.ResetAuthenticatorKeyAsync(user); + unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); + } + + SharedKey = FormatKey(unformattedKey); + + var email = await _userManager.GetEmailAsync(user); + AuthenticatorUri = GenerateQrCodeUri(email, unformattedKey); + } + + private string FormatKey(string unformattedKey) + { + var result = new StringBuilder(); + int currentPosition = 0; + while (currentPosition + 4 < unformattedKey.Length) + { + result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" "); + currentPosition += 4; + } + if (currentPosition < unformattedKey.Length) + { + result.Append(unformattedKey.Substring(currentPosition)); + } + + return result.ToString().ToLowerInvariant(); + } + + private string GenerateQrCodeUri(string email, string unformattedKey) + { + return string.Format( + AuthenticatorUriFormat, + _urlEncoder.Encode("Album"), + _urlEncoder.Encode(email), + unformattedKey); + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml new file mode 100644 index 0000000..86f87ad --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml @@ -0,0 +1,53 @@ +@page +@model ExternalLoginsModel +@{ + ViewData["Title"] = "Quản lý đăng nhập từ dịch vụ ngoài"; + ViewData["ActivePage"] = ManageNavPages.ExternalLogins; +} + + +@if (Model.CurrentLogins?.Count > 0) +{ +

Dịch vụ đã đăng ký

+ + + @foreach (var login in Model.CurrentLogins) + { + + + + + } + +
@login.ProviderDisplayName + @if (Model.ShowRemoveButton) + { +
+
+ + + +
+
+ } + else + { + @:   + } +
+} +@if (Model.OtherLogins?.Count > 0) +{ +

Thêm liên kết với dịch vụ ngoài.

+
+ +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs new file mode 100644 index 0000000..933ee71 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Album.Areas.Identity.Pages.Account.Manage +{ + public class ExternalLoginsModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public ExternalLoginsModel( + UserManager userManager, + SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + public IList CurrentLogins { get; set; } + + public IList OtherLogins { get; set; } + + public bool ShowRemoveButton { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Không nạp được User 'user.Id'."); + } + + CurrentLogins = await _userManager.GetLoginsAsync(user); + OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()) + .Where(auth => CurrentLogins.All(ul => auth.Name != ul.LoginProvider)) + .ToList(); + ShowRemoveButton = user.PasswordHash != null || CurrentLogins.Count > 1; + return Page(); + } + + public async Task OnPostRemoveLoginAsync(string loginProvider, string providerKey) + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Không nạp được User 'user.Id'."); + } + + var result = await _userManager.RemoveLoginAsync(user, loginProvider, providerKey); + if (!result.Succeeded) + { + StatusMessage = "Không xóa được."; + return RedirectToPage(); + } + + await _signInManager.RefreshSignInAsync(user); + StatusMessage = "Đã hủy liên kết."; + return RedirectToPage(); + } + + public async Task OnPostLinkLoginAsync(string provider) + { + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + // Request a redirect to the external login provider to link a login for the current user + var redirectUrl = Url.Page("./ExternalLogins", pageHandler: "LinkLoginCallback"); + var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User)); + return new ChallengeResult(provider, properties); + } + + public async Task OnGetLinkLoginCallbackAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Không nạp được User 'user.Id'."); + } + + var info = await _signInManager.GetExternalLoginInfoAsync(user.Id); + if (info == null) + { + throw new InvalidOperationException($"Lỗi ID '{user.Id}'."); + } + + var result = await _userManager.AddLoginAsync(user, info); + if (!result.Succeeded) + { + StatusMessage = "Lỗi - dịch vụ đã liên kết với tài khoản khác"; + return RedirectToPage(); + } + + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + StatusMessage = "Đã liên kết"; + return RedirectToPage(); + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml new file mode 100644 index 0000000..284ab59 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml @@ -0,0 +1,27 @@ +@page +@model GenerateRecoveryCodesModel +@{ + ViewData["Title"] = "Generate two-factor authentication (2FA) recovery codes"; + ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; +} + + +

@ViewData["Title"]

+ +
+
+ +
+
\ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs new file mode 100644 index 0000000..634e806 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace Album.Areas.Identity.Pages.Account.Manage +{ + public class GenerateRecoveryCodesModel : PageModel + { + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public GenerateRecoveryCodesModel( + UserManager userManager, + ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + + [TempData] + public string[] RecoveryCodes { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user); + if (!isTwoFactorEnabled) + { + var userId = await _userManager.GetUserIdAsync(user); + throw new InvalidOperationException($"Cannot generate recovery codes for user with ID '{userId}' because they do not have 2FA enabled."); + } + + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user); + var userId = await _userManager.GetUserIdAsync(user); + if (!isTwoFactorEnabled) + { + throw new InvalidOperationException($"Cannot generate recovery codes for user with ID '{userId}' as they do not have 2FA enabled."); + } + + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + RecoveryCodes = recoveryCodes.ToArray(); + + _logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", userId); + StatusMessage = "You have generated new recovery codes."; + return RedirectToPage("./ShowRecoveryCodes"); + } + } +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/Index.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/Index.cshtml new file mode 100644 index 0000000..334b31f --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/Index.cshtml @@ -0,0 +1,47 @@ +@page +@model IndexModel +@{ + ViewData["Title"] = "HỒ SƠ"; + ViewData["ActivePage"] = ManageNavPages.Index; +} + +

@ViewData["Title"]

+ +
+
+
+
+
+ + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + @Html.TextBoxFor(m => m.Input.Birthday, "{0:dd/MM/yyyy}", new {@class = "form-control"}) + @* *@ + +
+ + +
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs new file mode 100644 index 0000000..a2cf112 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +// using Album.Binder; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Authorization; + +namespace Album.Areas.Identity.Pages.Account.Manage { + [Authorize] + public partial class IndexModel : PageModel { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public IndexModel ( + UserManager userManager, + SignInManager signInManager) { + _userManager = userManager; + _signInManager = signInManager; + } + + [Display(Name = "Tên tài khoản")] + public string Username { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel { + [Phone] + [Display (Name = "Số điện thoại")] + public string PhoneNumber { get; set; } + + [MaxLength (100)] + [Display(Name = "Họ tên đầy đủ")] + public string FullName { set; get; } + + [MaxLength (255)] + [Display(Name = "Địa chỉ")] + public string Address { set; get; } + + [DataType (DataType.Date)] + [Display(Name = "Ngày sinh d/m/y")] + // [ModelBinder(BinderType=typeof(DayMonthYearBinder))] + [DisplayFormat(ApplyFormatInEditMode=true, DataFormatString = "{0:dd/MM/yyyy}")] + public DateTime? Birthday { set; get; } + + } + + // Nạp thông tin từ User vào Model + private async Task LoadAsync (AppUser user) { + var userName = await _userManager.GetUserNameAsync (user); + var phoneNumber = await _userManager.GetPhoneNumberAsync (user); + Username = userName; + Input = new InputModel { + PhoneNumber = phoneNumber, + Birthday = user.Birthday, + Address = user.Address, + FullName = user.FullName + }; + } + + public async Task OnGetAsync () { + var user = await _userManager.GetUserAsync (User); + + if (user == null) { + return NotFound ($"Không tải được tài khoản ID = '{_userManager.GetUserId(User)}'."); + } + + await LoadAsync (user); + return Page(); + } + + public async Task OnPostAsync () { + var user = await _userManager.GetUserAsync (User); + + if (user == null) { + return NotFound ($"Không có tài khoản ID: '{_userManager.GetUserId(User)}'."); + } + + if (!ModelState.IsValid) { + await LoadAsync(user); + return Page (); + } + + var phoneNumber = await _userManager.GetPhoneNumberAsync (user); + if (Input.PhoneNumber != phoneNumber) { + var setPhoneResult = await _userManager.SetPhoneNumberAsync (user, Input.PhoneNumber); + if (!setPhoneResult.Succeeded) { + StatusMessage = "Lỗi cập nhật số điện thoại."; + return RedirectToPage (); + } + } + + // Cập nhật các trường bổ sung + user.Address = Input.Address; + user.Birthday = Input.Birthday; + user.FullName = Input.FullName; + await _userManager.UpdateAsync(user); + + // Đăng nhập lại để làm mới Cookie (không nhớ thông tin cũ) + await _signInManager.RefreshSignInAsync (user); + StatusMessage = "Hồ sơ của bạn đã cập nhật"; + return RedirectToPage (); + } + } +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs new file mode 100644 index 0000000..5ddec76 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace Album.Areas.Identity.Pages.Account.Manage +{ + public static class ManageNavPages + { + public static string Index => "Index"; + + public static string Email => "Email"; + + public static string ChangePassword => "ChangePassword"; + + public static string DownloadPersonalData => "DownloadPersonalData"; + + public static string DeletePersonalData => "DeletePersonalData"; + + public static string ExternalLogins => "ExternalLogins"; + + public static string PersonalData => "PersonalData"; + + public static string TwoFactorAuthentication => "TwoFactorAuthentication"; + + public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index); + + public static string EmailNavClass(ViewContext viewContext) => PageNavClass(viewContext, Email); + + public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword); + + public static string DownloadPersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DownloadPersonalData); + + public static string DeletePersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DeletePersonalData); + + public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins); + + public static string PersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, PersonalData); + + public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication); + + // Trả về CSS class: bằng active nếu viewContext.ViewData["ActivePage"] bằng với page + private static string PageNavClass(ViewContext viewContext, string page) + { + var activePage = viewContext.ViewData["ActivePage"] as string; + if (activePage == null) + { + activePage = System.IO.Path.GetFileNameWithoutExtension(viewContext.ActionDescriptor.DisplayName); + } + return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null; + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml new file mode 100644 index 0000000..9174ed1 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml @@ -0,0 +1,27 @@ +@page +@model PersonalDataModel +@{ + ViewData["Title"] = "Personal Data"; + ViewData["ActivePage"] = ManageNavPages.PersonalData; +} + +

@ViewData["Title"]

+ +
+
+

Your account contains personal data that you have given us. This page allows you to download or delete that data.

+

+ Deleting this data will permanently remove your account, and this cannot be recovered. +

+
+ +
+

+ Delete +

+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs new file mode 100644 index 0000000..df95c4a --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace Album.Areas.Identity.Pages.Account.Manage +{ + public class PersonalDataModel : PageModel + { + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public PersonalDataModel( + UserManager userManager, + ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + return Page(); + } + } +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml new file mode 100644 index 0000000..081c824 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml @@ -0,0 +1,24 @@ +@page +@model ResetAuthenticatorModel +@{ + ViewData["Title"] = "Reset authenticator key"; + ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; +} + + +

@ViewData["Title"]

+ +
+
+ +
+
\ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs new file mode 100644 index 0000000..86d8e39 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace Album.Areas.Identity.Pages.Account.Manage +{ + public class ResetAuthenticatorModel : PageModel + { + UserManager _userManager; + private readonly SignInManager _signInManager; + ILogger _logger; + + public ResetAuthenticatorModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + [TempData] + public string StatusMessage { get; set; } + + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + await _userManager.SetTwoFactorEnabledAsync(user, false); + await _userManager.ResetAuthenticatorKeyAsync(user); + _logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", user.Id); + + await _signInManager.RefreshSignInAsync(user); + StatusMessage = "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key."; + + return RedirectToPage("./EnableAuthenticator"); + } + } +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml new file mode 100644 index 0000000..88b6dd6 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml @@ -0,0 +1,34 @@ +@page +@model SetPasswordModel +@{ + ViewData["Title"] = "Cài đặt mật khẩu"; + ViewData["ActivePage"] = ManageNavPages.ChangePassword; +} + +

Set your password

+ +

+ Tài khoản chưa đặt mật khẩu, nên cần đặt mật khẩu cho tài khoản +

+
+
+
+
+
+ + + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs new file mode 100644 index 0000000..e7fbd89 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Album.Areas.Identity.Pages.Account.Manage +{ + public class SetPasswordModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public SetPasswordModel( + UserManager userManager, + SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + [BindProperty] + public InputModel Input { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + public class InputModel + { + [Required] + [StringLength(100, ErrorMessage = "{0} dài {2} đến {1} ký tự.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Mật khẩu mới")] + public string NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Nhập lại mật khẩu")] + [Compare("NewPassword", ErrorMessage = "Mật khẩu phải giống nhau.")] + public string ConfirmPassword { get; set; } + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Lỗi tải User ID '{_userManager.GetUserId(User)}'."); + } + + var hasPassword = await _userManager.HasPasswordAsync(user); + + if (hasPassword) + { + return RedirectToPage("./ChangePassword"); + } + + return Page(); + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Lỗi tải User ID '{_userManager.GetUserId(User)}'."); + } + + var addPasswordResult = await _userManager.AddPasswordAsync(user, Input.NewPassword); + if (!addPasswordResult.Succeeded) + { + foreach (var error in addPasswordResult.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + return Page(); + } + + await _signInManager.RefreshSignInAsync(user); + StatusMessage = "Password đã thiết lập."; + + return RedirectToPage(); + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml new file mode 100644 index 0000000..23fa27b --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml @@ -0,0 +1,25 @@ +@page +@model ShowRecoveryCodesModel +@{ + ViewData["Title"] = "Recovery codes"; + ViewData["ActivePage"] = "TwoFactorAuthentication"; +} + + +

@ViewData["Title"]

+ +
+
+ @for (var row = 0; row < Model.RecoveryCodes.Length; row += 2) + { + @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
+ } +
+
\ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs new file mode 100644 index 0000000..7e8d890 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace Album.Areas.Identity.Pages.Account.Manage +{ + public class ShowRecoveryCodesModel : PageModel + { + [TempData] + public string[] RecoveryCodes { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + public IActionResult OnGet() + { + if (RecoveryCodes == null || RecoveryCodes.Length == 0) + { + return RedirectToPage("./TwoFactorAuthentication"); + } + + return Page(); + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml new file mode 100644 index 0000000..a1729eb --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml @@ -0,0 +1,57 @@ +@page +@model TwoFactorAuthenticationModel +@{ + ViewData["Title"] = "Two-factor authentication (2FA)"; + ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; +} + + +

@ViewData["Title"]

+@if (Model.Is2faEnabled) +{ + if (Model.RecoveryCodesLeft == 0) + { +
+ You have no recovery codes left. +

You must generate a new set of recovery codes before you can log in with a recovery code.

+
+ } + else if (Model.RecoveryCodesLeft == 1) + { +
+ You have 1 recovery code left. +

You can generate a new set of recovery codes.

+
+ } + else if (Model.RecoveryCodesLeft <= 3) + { +
+ You have @Model.RecoveryCodesLeft recovery codes left. +

You should generate a new set of recovery codes.

+
+ } + + if (Model.IsMachineRemembered) + { +
+ +
+ } + Disable 2FA + Reset recovery codes +} + +
Authenticator app
+@if (!Model.HasAuthenticator) +{ + Add authenticator app +} +else +{ + Setup authenticator app + Reset authenticator app +} + +@section Scripts { + +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs new file mode 100644 index 0000000..b083022 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace Album.Areas.Identity.Pages.Account.Manage +{ + public class TwoFactorAuthenticationModel : PageModel + { + private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}"; + + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public TwoFactorAuthenticationModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + public bool HasAuthenticator { get; set; } + + public int RecoveryCodesLeft { get; set; } + + [BindProperty] + public bool Is2faEnabled { get; set; } + + public bool IsMachineRemembered { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null; + Is2faEnabled = await _userManager.GetTwoFactorEnabledAsync(user); + IsMachineRemembered = await _signInManager.IsTwoFactorClientRememberedAsync(user); + RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user); + + return Page(); + } + + public async Task OnPost() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + await _signInManager.ForgetTwoFactorClientAsync(); + StatusMessage = "The current browser has been forgotten. When you login again from this browser you will be prompted for your 2fa code."; + return RedirectToPage(); + } + } +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/_Layout.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/_Layout.cshtml new file mode 100644 index 0000000..e845f4f --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/_Layout.cshtml @@ -0,0 +1,32 @@ +@{ + if (ViewData.TryGetValue("ParentLayout", out var parentLayout)) + { + Layout = (string)parentLayout; + } + else + { + // Layout = "/Areas/Identity/Pages/_Layout.cshtml"; + // Sử dụng tiếp Layout chung /Pages/Shared/_Layout.cshtml + Layout = "/Views/Shared/_Layout.cshtml"; + } + +} + +

QUẢN LÝ TÀI KHOẢN

+ +
+

Cập nhật thông tin tài khoản của bạn

+
+
+
+ +
+
+ @RenderBody() +
+
+
+ +@section Scripts { + @RenderSection("Scripts", required: false) +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml new file mode 100644 index 0000000..82efe9f --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml @@ -0,0 +1,15 @@ +@inject SignInManager SignInManager +@{ + var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); +} + diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/_StatusMessage.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/_StatusMessage.cshtml new file mode 100644 index 0000000..208a424 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/_StatusMessage.cshtml @@ -0,0 +1,10 @@ +@model string + +@if (!String.IsNullOrEmpty(Model)) +{ + var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; + +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml new file mode 100644 index 0000000..a823ddd --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml @@ -0,0 +1 @@ +@using Album.Areas.Identity.Pages.Account.Manage diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/_ViewStart.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/_ViewStart.cshtml new file mode 100644 index 0000000..820a2f6 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Manage/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Register.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Register.cshtml new file mode 100644 index 0000000..de1e5ba --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Register.cshtml @@ -0,0 +1,60 @@ +@page "/register/" +@model RegisterModel +@{ + ViewData["Title"] = "ĐĂNG KÝ TÀI KHOẢN"; +} + +

@ViewData["Title"]

+ +
+
+
+

Điền thông tin để tạo tài khoản mới.

+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+ @if ((Model.ExternalLogins?.Count ?? 0) != 0) { +
+

Sử dụng dịch vụ

+
+
+
+

+ @foreach (var provider in Model.ExternalLogins) + { + + } +

+
+
+
+ } +
+
+ +@section Scripts { + +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Register.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Register.cshtml.cs new file mode 100644 index 0000000..cf1ac23 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/Register.cshtml.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; + +namespace Album.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class RegisterModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + private readonly ILogger _logger; + private readonly IEmailSender _emailSender; + + // Các dịch vụ được Inject vào: UserManger, SignInManager, ILogger, IEmailSender + public RegisterModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger, + IEmailSender emailSender) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + _emailSender = emailSender; + } + + // InputModel được binding khi Form Post tới + + [BindProperty] + public InputModel Input { get; set; } + + public string ReturnUrl { get; set; } + + // Xác thực từ dịch vụ ngoài (Googe, Facebook ... bài này chứa thiết lập) + public IList ExternalLogins { get; set; } + + // Lớp InputModel chứa thông tin Post tới dùng để tạo User + public class InputModel + { + [Required] + [EmailAddress] + [Display(Name = "Địa chỉ Email")] + public string Email { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "{0} dài từ {2} đến {1} ký tự.", MinimumLength = 3)] + [DataType(DataType.Password)] + [Display(Name = "Mật khẩu")] + public string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Nhập lại mật khẩu")] + [Compare("Password", ErrorMessage = "Mật khẩu không giống nhau")] + public string ConfirmPassword { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "{0} dài từ {2} đến {1} ký tự.", MinimumLength = 3)] + [DataType(DataType.Text)] + [Display(Name="Tên tài khoản (viết liền - không dấu)")] + public string UserName {set; get;} + } + + // Đăng ký tài khoản theo dữ liệu form post tới + public async Task OnGetAsync(string returnUrl = null) + { + ReturnUrl = returnUrl; + ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + } + + public async Task OnPostAsync(string returnUrl = null) + { + returnUrl = returnUrl ?? Url.Content("~/"); + ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + if (ModelState.IsValid) + { + // Tạo AppUser sau đó tạo User mới (cập nhật vào db) + var user = new AppUser { UserName = Input.UserName, Email = Input.Email }; + var result = await _userManager.CreateAsync(user, Input.Password); + + if (result.Succeeded) + { + _logger.LogInformation("Vừa tạo mới tài khoản thành công."); + + // phát sinh token theo thông tin user để xác nhận email + // mỗi user dựa vào thông tin sẽ có một mã riêng, mã này nhúng vào link + // trong email gửi đi để người dùng xác nhận + var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + + // callbackUrl = /Account/ConfirmEmail?userId=useridxx&code=codexxxx + // Link trong email người dùng bấm vào, nó sẽ gọi Page: /Acount/ConfirmEmail để xác nhận + var callbackUrl = Url.Page( + "/Account/ConfirmEmail", + pageHandler: null, + values: new { area = "Identity", userId = user.Id, code = code, returnUrl = returnUrl }, + protocol: Request.Scheme); + + // Gửi email + await _emailSender.SendEmailAsync(Input.Email, "Xác nhận địa chỉ email", + $"Hãy xác nhận địa chỉ email bằng cách Bấm vào đây."); + + if (_userManager.Options.SignIn.RequireConfirmedEmail) + { + // Nếu cấu hình phải xác thực email mới được đăng nhập thì chuyển hướng đến trang + // RegisterConfirmation - chỉ để hiện thông báo cho biết người dùng cần mở email xác nhận + return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl }); + } + else + { + // Không cần xác thực - đăng nhập luôn + await _signInManager.SignInAsync(user, isPersistent: false); + return LocalRedirect(returnUrl); + } + } + // Có lỗi, đưa các lỗi thêm user vào ModelState để hiện thị ở html heleper: asp-validation-summary + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + + return Page(); + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml new file mode 100644 index 0000000..aa35b5a --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml @@ -0,0 +1,10 @@ +@page "/RegisterConfirmation/" +@model RegisterConfirmationModel +@{ + ViewData["Title"] = "Xác nhận đăng ký"; +} + +

@ViewData["Title"]

+

Một email đã gửi đến cho bạn, bạn cần mở email làm theo hướng dẫn trong email sau đó + Bấm vào đây để tiếp tục +

diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml.cs new file mode 100644 index 0000000..8be4d51 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml.cs @@ -0,0 +1,66 @@ +using Microsoft.AspNetCore.Authorization; +using System.Text; +using System.Threading.Tasks; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; +using XTLASPNET; + +namespace Album.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class RegisterConfirmationModel : PageModel + { + private readonly UserManager _userManager; + public RegisterConfirmationModel(UserManager userManager) + { + _userManager = userManager; + } + + public string Email { get; set; } + + public string UrlContinue {set; get;} + + + public async Task OnGetAsync(string email, string returnUrl = null) + { + if (email == null) + { + return RedirectToPage("/Index"); + } + + + var user = await _userManager.FindByEmailAsync(email); + if (user == null) + { + return NotFound($"Không có user với email: '{email}'."); + } + + if (user.EmailConfirmed) { + // Tài khoản đã xác thực email + return ViewComponent(MessagePage.COMPONENTNAME, + new MessagePage.Message() { + title = "Thông báo", + htmlcontent = "Tài khoản đã xác thực, chờ chuyển hướng", + urlredirect = (returnUrl != null) ? returnUrl : Url.Page("/Index") + } + + ); + } + + Email = email; + + if (returnUrl != null) { + UrlContinue = Url.Page("RegisterConfirmation", new { email = Email, returnUrl = returnUrl }); + } + else + UrlContinue = Url.Page("RegisterConfirmation", new { email = Email }); + + + return Page(); + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ResetPassword.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ResetPassword.cshtml new file mode 100644 index 0000000..95c82c7 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ResetPassword.cshtml @@ -0,0 +1,37 @@ +@page +@model ResetPasswordModel +@{ + ViewData["Title"] = "Đặt lại mật khẩu"; +} + +

@ViewData["Title"]

+

Đặt lại mật khẩu.

+
+
+
+
+
+ +
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs new file mode 100644 index 0000000..17cdfab --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using mvcblog.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; + +namespace Album.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ResetPasswordModel : PageModel + { + private readonly UserManager _userManager; + + public ResetPasswordModel(UserManager userManager) + { + _userManager = userManager; + } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "{0} dài {2} đến {1} ký tự.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Mật khẩu")] + public string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Nhập lại mật khẩu")] + [Compare("Password", ErrorMessage = "Password phải giống nhau.")] + public string ConfirmPassword { get; set; } + + public string Code { get; set; } + } + + public IActionResult OnGet(string code = null) + { + if (code == null) + { + return BadRequest("Mã token không có."); + } + else + { + Input = new InputModel + { + // Giải mã lại code từ code trong url (do mã này khi gửi mail + // đã thực hiện Encode bằng WebEncoders.Base64UrlEncode) + Code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code)) + }; + return Page(); + } + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + // Tìm User theo email + var user = await _userManager.FindByEmailAsync(Input.Email); + if (user == null) + { + // Không thấy user + return RedirectToPage("./ResetPasswordConfirmation"); + } + // Đặt lại passowrd chu user - có kiểm tra mã token khi đổi + var result = await _userManager.ResetPasswordAsync(user, Input.Code, Input.Password); + + if (result.Succeeded) + { + // Chuyển đến trang thông báo đã reset thành công + return RedirectToPage("./ResetPasswordConfirmation"); + } + + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + return Page(); + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml new file mode 100644 index 0000000..98d80f0 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml @@ -0,0 +1,10 @@ +@page +@model ResetPasswordConfirmationModel +@{ + ViewData["Title"] = "Đặt lại mật khẩu"; +} + +

@ViewData["Title"]

+

+ Đã đặt lại mặt khẩu. Đăng nhập tại đây. +

diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs new file mode 100644 index 0000000..c26bf45 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Album.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ResetPasswordConfirmationModel : PageModel + { + public void OnGet() + { + + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/_StatusMessage.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/_StatusMessage.cshtml new file mode 100644 index 0000000..e996841 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/_StatusMessage.cshtml @@ -0,0 +1,10 @@ +@model string + +@if (!String.IsNullOrEmpty(Model)) +{ + var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; + +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/_ViewImports.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/_ViewImports.cshtml new file mode 100644 index 0000000..027a573 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Account/_ViewImports.cshtml @@ -0,0 +1 @@ +@using Album.Areas.Identity.Pages.Account \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Error.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Error.cshtml new file mode 100644 index 0000000..b1f3143 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Error.cshtml @@ -0,0 +1,23 @@ +@page +@model ErrorModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. +

diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Error.cshtml.cs b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Error.cshtml.cs new file mode 100644 index 0000000..6bda83c --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/Error.cshtml.cs @@ -0,0 +1,21 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Album.Areas.Identity.Pages +{ + [AllowAnonymous] + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public class ErrorModel : PageModel + { + public string RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + public void OnGet() + { + RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml new file mode 100644 index 0000000..9e26f3b --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/_ViewImports.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/_ViewImports.cshtml new file mode 100644 index 0000000..d2bc2be --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/_ViewImports.cshtml @@ -0,0 +1,5 @@ +@using Microsoft.AspNetCore.Identity +@using Album.Areas.Identity +@using Album.Areas.Identity.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@using mvcblog.Models diff --git a/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/_ViewStart.cshtml b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/_ViewStart.cshtml new file mode 100644 index 0000000..a0f2b94 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Areas/Identity/Pages/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "/Views/Shared/_Layout.cshtml"; +} diff --git a/ASP_NET_CORE/mvcblog/Data/AppDbContext.cs b/ASP_NET_CORE/mvcblog/Data/AppDbContext.cs new file mode 100644 index 0000000..435dfd9 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Data/AppDbContext.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using mvcblog.Models; + +namespace mvcblog.Data { + public class AppDbContext : IdentityDbContext { + + public AppDbContext (DbContextOptions options) : base (options) { } + + protected override void OnModelCreating (ModelBuilder builder) { + + base.OnModelCreating (builder); + // Bỏ tiền tố AspNet của các bảng: mặc định + foreach (var entityType in builder.Model.GetEntityTypes ()) { + var tableName = entityType.GetTableName (); + if (tableName.StartsWith ("AspNet")) { + entityType.SetTableName (tableName.Substring (6)); + } + } + } + + } + +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Mail/SendMailService.cs b/ASP_NET_CORE/mvcblog/Mail/SendMailService.cs new file mode 100644 index 0000000..612abe3 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Mail/SendMailService.cs @@ -0,0 +1,69 @@ +using System; +using System.Threading.Tasks; +using MailKit.Security; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MimeKit; + +namespace XTLASPNET { + + // Cấu hình dịch vụ gửi mail, giá trị Inject từ appsettings.json + public class MailSettings { + public string Mail { get; set; } + public string DisplayName { get; set; } + public string Password { get; set; } + public string Host { get; set; } + public int Port { get; set; } + + } + + // Dịch vụ gửi mail + public class SendMailService : IEmailSender { + private readonly MailSettings mailSettings; + + private readonly ILogger logger; + + // mailSetting được Inject qua dịch vụ hệ thống + // Có inject Logger để xuất log + public SendMailService (IOptions _mailSettings, ILogger _logger) { + mailSettings = _mailSettings.Value; + logger = _logger; + logger.LogInformation ("Create SendMailService"); + } + + public async Task SendEmailAsync (string email, string subject, string htmlMessage) { + var message = new MimeMessage (); + message.Sender = new MailboxAddress (mailSettings.DisplayName, mailSettings.Mail); + message.From.Add (new MailboxAddress (mailSettings.DisplayName, mailSettings.Mail)); + message.To.Add (MailboxAddress.Parse (email)); + message.Subject = subject; + + var builder = new BodyBuilder (); + builder.HtmlBody = htmlMessage; + message.Body = builder.ToMessageBody (); + + // dùng SmtpClient của MailKit + using var smtp = new MailKit.Net.Smtp.SmtpClient (); + + try { + smtp.Connect (mailSettings.Host, mailSettings.Port, SecureSocketOptions.StartTls); + smtp.Authenticate (mailSettings.Mail, mailSettings.Password); + await smtp.SendAsync (message); + } catch (Exception ex) { + // Gửi mail thất bại, nội dung email sẽ lưu vào thư mục mailssave + System.IO.Directory.CreateDirectory ("mailssave"); + var emailsavefile = string.Format (@"mailssave/{0}.eml", Guid.NewGuid ()); + await message.WriteToAsync (emailsavefile); + + logger.LogInformation ("Lỗi gửi mail, lưu tại - " + emailsavefile); + logger.LogError (ex.Message); + } + + smtp.Disconnect (true); + + logger.LogInformation ("send mail to: " + email); + + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Migrations/20200920144327_Init.Designer.cs b/ASP_NET_CORE/mvcblog/Migrations/20200920144327_Init.Designer.cs new file mode 100644 index 0000000..5c8f658 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Migrations/20200920144327_Init.Designer.cs @@ -0,0 +1,284 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using mvcblog.Data; + +namespace mvcblog.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20200920144327_Init")] + partial class Init + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens"); + }); + + modelBuilder.Entity("mvcblog.Models.AppUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Address") + .HasColumnType("nvarchar(255)") + .HasMaxLength(255); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FullName") + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("mvcblog.Models.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("mvcblog.Models.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("mvcblog.Models.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("mvcblog.Models.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Migrations/20200920144327_Init.cs b/ASP_NET_CORE/mvcblog/Migrations/20200920144327_Init.cs new file mode 100644 index 0000000..5082664 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Migrations/20200920144327_Init.cs @@ -0,0 +1,222 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace mvcblog.Migrations +{ + public partial class Init : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Roles", + columns: table => new + { + Id = table.Column(nullable: false), + Name = table.Column(maxLength: 256, nullable: true), + NormalizedName = table.Column(maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Roles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(nullable: false), + UserName = table.Column(maxLength: 256, nullable: true), + NormalizedUserName = table.Column(maxLength: 256, nullable: true), + Email = table.Column(maxLength: 256, nullable: true), + NormalizedEmail = table.Column(maxLength: 256, nullable: true), + EmailConfirmed = table.Column(nullable: false), + PasswordHash = table.Column(nullable: true), + SecurityStamp = table.Column(nullable: true), + ConcurrencyStamp = table.Column(nullable: true), + PhoneNumber = table.Column(nullable: true), + PhoneNumberConfirmed = table.Column(nullable: false), + TwoFactorEnabled = table.Column(nullable: false), + LockoutEnd = table.Column(nullable: true), + LockoutEnabled = table.Column(nullable: false), + AccessFailedCount = table.Column(nullable: false), + FullName = table.Column(maxLength: 100, nullable: true), + Address = table.Column(maxLength: 255, nullable: true), + Birthday = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "RoleClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + RoleId = table.Column(nullable: false), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_RoleClaims_Roles_RoleId", + column: x => x.RoleId, + principalTable: "Roles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "UserClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(nullable: false), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UserClaims", x => x.Id); + table.ForeignKey( + name: "FK_UserClaims_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "UserLogins", + columns: table => new + { + LoginProvider = table.Column(nullable: false), + ProviderKey = table.Column(nullable: false), + ProviderDisplayName = table.Column(nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_UserLogins_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "UserRoles", + columns: table => new + { + UserId = table.Column(nullable: false), + RoleId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_UserRoles_Roles_RoleId", + column: x => x.RoleId, + principalTable: "Roles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_UserRoles_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "UserTokens", + columns: table => new + { + UserId = table.Column(nullable: false), + LoginProvider = table.Column(nullable: false), + Name = table.Column(nullable: false), + Value = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_UserTokens_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_RoleClaims_RoleId", + table: "RoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "Roles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_UserClaims_UserId", + table: "UserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_UserLogins_UserId", + table: "UserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_UserRoles_RoleId", + table: "UserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "Users", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "Users", + column: "NormalizedUserName", + unique: true, + filter: "[NormalizedUserName] IS NOT NULL"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RoleClaims"); + + migrationBuilder.DropTable( + name: "UserClaims"); + + migrationBuilder.DropTable( + name: "UserLogins"); + + migrationBuilder.DropTable( + name: "UserRoles"); + + migrationBuilder.DropTable( + name: "UserTokens"); + + migrationBuilder.DropTable( + name: "Roles"); + + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Migrations/AppDbContextModelSnapshot.cs b/ASP_NET_CORE/mvcblog/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 0000000..e0e5b77 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,282 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using mvcblog.Data; + +namespace mvcblog.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens"); + }); + + modelBuilder.Entity("mvcblog.Models.AppUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Address") + .HasColumnType("nvarchar(255)") + .HasMaxLength(255); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FullName") + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("mvcblog.Models.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("mvcblog.Models.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("mvcblog.Models.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("mvcblog.Models.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Models/AppUser.cs b/ASP_NET_CORE/mvcblog/Models/AppUser.cs new file mode 100644 index 0000000..5c53c0a --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Models/AppUser.cs @@ -0,0 +1,17 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Identity; + +namespace mvcblog.Models { + public class AppUser : IdentityUser { + [MaxLength (100)] + public string FullName { set; get; } + + [MaxLength (255)] + public string Address { set; get; } + + [DataType (DataType.Date)] + public DateTime? Birthday { set; get; } + + } +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Startup.cs b/ASP_NET_CORE/mvcblog/Startup.cs index 8d294b8..8a6bf58 100644 --- a/ASP_NET_CORE/mvcblog/Startup.cs +++ b/ASP_NET_CORE/mvcblog/Startup.cs @@ -3,84 +3,149 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using mvcblog.Data; +using mvcblog.Models; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Newtonsoft.Json; +using XTLASPNET; +using Microsoft.AspNetCore.Identity.UI.Services; -namespace mvcblog -{ - public class Startup - { - public Startup(IConfiguration configuration) - { +namespace mvcblog { + public class Startup { + public Startup (IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } - public void ConfigureServices(IServiceCollection services) - { + public void ConfigureServices (IServiceCollection services) { + + // Đăng ký AppDbContext, sử dụng kết nối đến MS SQL Server + services.AddDbContext (options => { + string connectstring = Configuration.GetConnectionString ("MyBlogContext"); + options.UseSqlServer (connectstring); + }); + // Đăng ký các dịch vụ của Identity + services.AddIdentity () + .AddEntityFrameworkStores () + .AddDefaultTokenProviders (); + + // Truy cập IdentityOptions + services.Configure (options => { + // Thiết lập về Password + options.Password.RequireDigit = false; // Không bắt phải có số + options.Password.RequireLowercase = false; // Không bắt phải có chữ thường + options.Password.RequireNonAlphanumeric = false; // Không bắt ký tự đặc biệt + options.Password.RequireUppercase = false; // Không bắt buộc chữ in + options.Password.RequiredLength = 3; // Số ký tự tối thiểu của password + options.Password.RequiredUniqueChars = 1; // Số ký tự riêng biệt + + // Cấu hình Lockout - khóa user + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes (5); // Khóa 5 phút + options.Lockout.MaxFailedAccessAttempts = 5; // Thất bại 5 lầ thì khóa + options.Lockout.AllowedForNewUsers = true; + + // Cấu hình về User. + options.User.AllowedUserNameCharacters = // các ký tự đặt tên user + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; + options.User.RequireUniqueEmail = true; // Email là duy nhất + + // Cấu hình đăng nhập. + options.SignIn.RequireConfirmedEmail = true; // Cấu hình xác thực địa chỉ email (email phải tồn tại) + options.SignIn.RequireConfirmedPhoneNumber = false; // Xác thực số điện thoại + + }); + + // Cấu hình Cookie + services.ConfigureApplicationCookie (options => { + // options.Cookie.HttpOnly = true; + options.ExpireTimeSpan = TimeSpan.FromMinutes (30); + options.LoginPath = $"/login/"; // Url đến trang đăng nhập + options.LogoutPath = $"/logout/"; + options.AccessDeniedPath = $"/Identity/Account/AccessDenied"; // Trang khi User bị cấm truy cập + }); + services.Configure (options => { + // Trên 5 giây truy cập lại sẽ nạp lại thông tin User (Role) + // SecurityStamp trong bảng User đổi -> nạp lại thông tinn Security + options.ValidationInterval = TimeSpan.FromSeconds (5); + }); + services.Configure (options => { - options.AppendTrailingSlash = false; // Thêm dấu / vào cuối URL - options.LowercaseUrls = true; // url chữ thường - options.LowercaseQueryStrings = false; // không bắt query trong url phải in thường + options.AppendTrailingSlash = false; // Thêm dấu / vào cuối URL + options.LowercaseUrls = true; // url chữ thường + options.LowercaseQueryStrings = false; // không bắt query trong url phải in thường }); + + services.AddOptions (); // Kích hoạt Options + var mailsettings = Configuration.GetSection ("MailSettings"); // đọc config + services.Configure (mailsettings); // đăng ký để Inject + services.AddTransient (); // Đăng ký dịch vụ Mail - services.AddControllersWithViews(); - services.AddRazorPages(); - } - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else + services.AddAuthorization(options => { - app.UseExceptionHandler("/Home/Error"); - app.UseHsts(); + // User thỏa mãn policy khi có roleclaim: permission với giá trị manage.user + options.AddPolicy("AdminDropdown", policy => { + policy.RequireClaim("permission", "manage.user"); + }); + + }); + + + services.AddControllersWithViews (); + services.AddRazorPages (); + } + + public void Configure (IApplicationBuilder app, IWebHostEnvironment env) { + if (env.IsDevelopment ()) { + app.UseDeveloperExceptionPage (); + } else { + app.UseExceptionHandler ("/Home/Error"); + app.UseHsts (); } - app.UseHttpsRedirection(); - app.UseStaticFiles(); + app.UseHttpsRedirection (); + app.UseStaticFiles (); - app.UseRouting(); + app.UseRouting (); - app.UseAuthorization(); + app.UseAuthentication (); // Phục hồi thông tin đăng nhập (xác thực) + app.UseAuthorization (); // Phục hồi thông tinn về quyền của User - app.UseEndpoints(endpoints => - { - endpoints.MapControllerRoute( + app.UseEndpoints (endpoints => { + endpoints.MapControllerRoute ( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); endpoints.MapControllerRoute ( - name: "learnasproute", // đặt tên route + name: "learnasproute", // đặt tên route defaults : new { controller = "LearnAsp", action = "Index" }, pattern: "learn-asp-net/{id:int?}"); // Đến Razor Page - endpoints.MapRazorPages(); + endpoints.MapRazorPages (); }); - app.Map("/testapi", app => { - app.Run(async context => { - context.Response.StatusCode = 500; + app.Map ("/testapi", app => { + app.Run (async context => { + context.Response.StatusCode = 500; context.Response.ContentType = "application/json"; var ob = new { - url = context.Request.GetDisplayUrl(), + url = context.Request.GetDisplayUrl (), content = "Trả về từ testapi" }; - string jsonString = JsonConvert.SerializeObject(ob); - await context.Response.WriteAsync(jsonString, Encoding.UTF8); + string jsonString = JsonConvert.SerializeObject (ob); + await context.Response.WriteAsync (jsonString, Encoding.UTF8); }); }); @@ -91,4 +156,4 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) } } -} +} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/Views/Shared/Components/MessagePage/Default.cshtml b/ASP_NET_CORE/mvcblog/Views/Shared/Components/MessagePage/Default.cshtml new file mode 100644 index 0000000..51c8958 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Views/Shared/Components/MessagePage/Default.cshtml @@ -0,0 +1,16 @@ +@model XTLASPNET.MessagePage.Message +@{ + Layout = "_Layout"; + ViewData["Title"] = @Model.title; +} +
+
+

@Model.title

+
+
+ @Html.Raw(Model.htmlcontent) +
+ +
diff --git a/ASP_NET_CORE/mvcblog/Views/Shared/Components/MessagePage/MessagePage.cs b/ASP_NET_CORE/mvcblog/Views/Shared/Components/MessagePage/MessagePage.cs new file mode 100644 index 0000000..0bec5f6 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Views/Shared/Components/MessagePage/MessagePage.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace XTLASPNET +{ + [ViewComponent] + public class MessagePage : ViewComponent + { + public const string COMPONENTNAME = "MessagePage"; + // Dữ liệu nội dung trang thông báo + public class Message { + public string title {set; get;} = "Thông báo"; // Tiêu đề của Box hiện thị + public string htmlcontent {set; get;} = ""; // Nội dung HTML hiện thị + public string urlredirect {set; get;} = "/"; // Url chuyển hướng đến + public int secondwait {set; get;} = 3; // Sau secondwait giây thì chuyển + } + public MessagePage() {} + public IViewComponentResult Invoke(Message message) { + // Thiết lập Header của HTTP Respone - chuyển hướng về trang đích + this.HttpContext.Response.Headers.Add("REFRESH",$"{message.secondwait};URL={message.urlredirect}"); + return View(message); + } + } +} diff --git a/ASP_NET_CORE/mvcblog/Views/Shared/_AdminDropdownMenu.cshtml b/ASP_NET_CORE/mvcblog/Views/Shared/_AdminDropdownMenu.cshtml new file mode 100644 index 0000000..e0554a8 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Views/Shared/_AdminDropdownMenu.cshtml @@ -0,0 +1,21 @@ +@using Microsoft.AspNetCore.Identity +@using mvcblog.Models +@using Microsoft.AspNetCore.Authorization +@inject SignInManager SignInManager + +@inject Microsoft.AspNetCore.Authorization.IAuthorizationService authorizationService +@if (SignInManager.IsSignedIn(User) + && (await authorizationService.AuthorizeAsync(User, "AdminDropdown")).Succeeded) +{ + +} diff --git a/ASP_NET_CORE/mvcblog/Views/Shared/_Layout.cshtml b/ASP_NET_CORE/mvcblog/Views/Shared/_Layout.cshtml index 94cdb0d..4fc4aee 100644 --- a/ASP_NET_CORE/mvcblog/Views/Shared/_Layout.cshtml +++ b/ASP_NET_CORE/mvcblog/Views/Shared/_Layout.cshtml @@ -26,6 +26,7 @@ + @await Html.PartialAsync("_LoginPartial") diff --git a/ASP_NET_CORE/mvcblog/Views/Shared/_LoginPartial.cshtml b/ASP_NET_CORE/mvcblog/Views/Shared/_LoginPartial.cshtml new file mode 100644 index 0000000..905360b --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Views/Shared/_LoginPartial.cshtml @@ -0,0 +1,34 @@ +@using Microsoft.AspNetCore.Identity +@using mvcblog.Models +@using Microsoft.AspNetCore.Mvc.ViewEngines + +@inject SignInManager SignInManager +@inject UserManager UserManager +@inject ICompositeViewEngine Engine + + + diff --git a/ASP_NET_CORE/mvcblog/Views/Shared/_Paging.cshtml b/ASP_NET_CORE/mvcblog/Views/Shared/_Paging.cshtml new file mode 100644 index 0000000..30a79b5 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/Views/Shared/_Paging.cshtml @@ -0,0 +1,114 @@ +@model dynamic +@{ + int currentPage = Model.currentPage; + int countPages = Model.countPages; + var generateUrl = Model.generateUrl; + + if (currentPage > countPages) + currentPage = countPages; + + if (countPages <= 1) return; + + + int? preview = null; + int? next = null; + + if (currentPage > 1) + preview = currentPage - 1; + + if (currentPage < countPages) + next = currentPage + 1; + + // Các trang hiện thị trong điều hướng + List pagesRanges = new List(); + + + int delta = 5; // Số trang mở rộng về mỗi bên trang hiện tại + int remain = delta * 2; // Số trang hai bên trang hiện tại + pagesRanges.Add(currentPage); + // Các trang phát triển về hai bên trang hiện tại + for (int i = 1; i <= delta; i++) + { + if (currentPage + i <= countPages) { + pagesRanges.Add(currentPage + i); + remain --; + } + + if (currentPage - i >= 1) { + pagesRanges.Insert(0, currentPage - i); + remain --; + } + + } + // Xử lý thêm vào các trang cho đủ remain (xảy ra ở đầu mút của khoảng trang không đủ + // trang chèn vào) + if (remain > 0) { + if (pagesRanges[0] == 1) { + for (int i = 1; i <= remain; i++) + { + if (pagesRanges.Last() + 1 <= countPages) { + pagesRanges.Add(pagesRanges.Last() + 1); + } + } + } + else { + for (int i = 1; i <= remain; i++) + { + if (pagesRanges.First() - 1 > 1) { + pagesRanges.Insert(0, pagesRanges.First() - 1); + } + } + } + } + +} + + + diff --git a/ASP_NET_CORE/mvcblog/appsettings.json b/ASP_NET_CORE/mvcblog/appsettings.json index d9d9a9b..9d1178b 100644 --- a/ASP_NET_CORE/mvcblog/appsettings.json +++ b/ASP_NET_CORE/mvcblog/appsettings.json @@ -6,5 +6,15 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "MailSettings": { + "Mail": "youremail@gmail.com", + "DisplayName": "Tên Hiện Thị (ví dụ XUANTHULAB)", + "Password": "passowrd ở đây", + "Host": "smtp.gmail.com", + "Port": 587 + }, + "ConnectionStrings": { + "MyBlogContext": "Data Source=localhost,1433; Initial Catalog=myblog; User ID=SA;Password=Password123" + } } diff --git a/ASP_NET_CORE/mvcblog/mailssave/c5524bb4-a0b4-414c-b6ea-3fb01be1cd61.eml b/ASP_NET_CORE/mvcblog/mailssave/c5524bb4-a0b4-414c-b6ea-3fb01be1cd61.eml new file mode 100644 index 0000000..944438f --- /dev/null +++ b/ASP_NET_CORE/mvcblog/mailssave/c5524bb4-a0b4-414c-b6ea-3fb01be1cd61.eml @@ -0,0 +1,13 @@ +From: =?utf-8?b?VMOqbiBIaeG7h24gVGjhu4sgKHbDrSBk4bul?= "XUANTHULAB)" + +Date: Wed, 23 Sep 2020 13:12:24 +0700 +Subject: =?utf-8?b?WMOhYyBuaOG6rW4gxJHhu4thIGNo4buJ?= email +Message-Id: +Sender: =?utf-8?b?VMOqbiBIaeG7h24gVGjhu4sgKHbDrSBk4bul?= "XUANTHULAB)" + +To: xt2810@gmail.com +MIME-Version: 1.0 +Content-Type: text/html; charset=utf-8 +Content-Id: + +Hãy xác nhận địa chỉ email bằng cách Bấm vào đây. diff --git a/ASP_NET_CORE/mvcblog/mvcblog.csproj b/ASP_NET_CORE/mvcblog/mvcblog.csproj index 05320b6..1ec198e 100644 --- a/ASP_NET_CORE/mvcblog/mvcblog.csproj +++ b/ASP_NET_CORE/mvcblog/mvcblog.csproj @@ -5,8 +5,24 @@ + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + diff --git a/ASP_NET_CORE/mvcblog/wwwroot/lib/multiple-select/multiple-select.min.css b/ASP_NET_CORE/mvcblog/wwwroot/lib/multiple-select/multiple-select.min.css new file mode 100644 index 0000000..5a6f1b1 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/wwwroot/lib/multiple-select/multiple-select.min.css @@ -0,0 +1,10 @@ +/** + * multiple-select - Multiple select is a jQuery plugin to select multiple elements with checkboxes :). + * + * @version v1.5.2 + * @homepage http://multiple-select.wenzhixin.net.cn + * @author wenzhixin (http://wenzhixin.net.cn/) + * @license MIT + */ + +@charset "UTF-8";.ms-offscreen{clip:rect(0 0 0 0)!important;width:1px!important;height:1px!important;border:0!important;margin:0!important;padding:0!important;overflow:hidden!important;position:absolute!important;outline:0!important;left:auto!important;top:auto!important}.ms-parent{display:inline-block;position:relative;vertical-align:middle}.ms-choice{display:block;width:100%;height:26px;padding:0;overflow:hidden;cursor:pointer;border:1px solid #aaa;text-align:left;white-space:nowrap;line-height:26px;color:#444;text-decoration:none;border-radius:4px;background-color:#fff}.ms-choice.disabled{background-color:#f4f4f4;background-image:none;border:1px solid #ddd;cursor:default}.ms-choice>span{position:absolute;top:0;left:0;right:20px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;padding-left:8px}.ms-choice>span.placeholder{color:#999}.ms-choice>div.icon-close{position:absolute;top:0;right:16px;height:100%;width:16px}.ms-choice>div.icon-close:before{content:'×';color:#888;font-weight:bold;position:absolute;top:50%;margin-top:-14px}.ms-choice>div.icon-close:hover:before{color:#333}.ms-choice>div.icon-caret{position:absolute;width:0;height:0;top:50%;right:8px;margin-top:-2px;border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px}.ms-choice>div.icon-caret.open{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.ms-drop{width:auto;min-width:100%;overflow:hidden;display:none;margin-top:-1px;padding:0;position:absolute;z-index:1000;background:#fff;color:#000;border:1px solid #aaa;border-radius:4px}.ms-drop.bottom{top:100%;box-shadow:0 4px 5px rgba(0,0,0,0.15)}.ms-drop.top{bottom:100%;box-shadow:0 -4px 5px rgba(0,0,0,0.15)}.ms-search{display:inline-block;margin:0;min-height:26px;padding:2px;position:relative;white-space:nowrap;width:100%;z-index:10000;box-sizing:border-box}.ms-search input{width:100%;height:auto!important;min-height:24px;padding:0 5px;margin:0;outline:0;font-family:sans-serif;border:1px solid #aaa;border-radius:5px;box-shadow:none}.ms-drop ul{overflow:auto;margin:0;padding:0}.ms-drop ul>li{list-style:none;display:list-item;background-image:none;position:static;padding:.25rem 8px}.ms-drop ul>li .disabled{font-weight:normal!important;opacity:.35;filter:Alpha(Opacity=35);cursor:default}.ms-drop ul>li.multiple{display:block;float:left}.ms-drop ul>li.group{clear:both}.ms-drop ul>li.multiple label{width:100%;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ms-drop ul>li label{position:relative;padding-left:1.25rem;margin-bottom:0;font-weight:normal;display:block;white-space:nowrap;cursor:pointer}.ms-drop ul>li label.optgroup{font-weight:bold}.ms-drop ul>li.hide-radio{padding:0}.ms-drop ul>li.hide-radio:focus,.ms-drop ul>li.hide-radio:hover{background-color:#f8f9fa}.ms-drop ul>li.hide-radio.selected{color:#fff;background-color:#007bff}.ms-drop ul>li.hide-radio label{margin-bottom:0;padding:5px 8px}.ms-drop ul>li.hide-radio input{display:none}.ms-drop ul>li.option-level-1 label{padding-left:28px}.ms-drop input[type="radio"],.ms-drop input[type="checkbox"]{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.ms-drop .ms-no-results{display:none} \ No newline at end of file diff --git a/ASP_NET_CORE/mvcblog/wwwroot/lib/multiple-select/multiple-select.min.js b/ASP_NET_CORE/mvcblog/wwwroot/lib/multiple-select/multiple-select.min.js new file mode 100644 index 0000000..7bffe99 --- /dev/null +++ b/ASP_NET_CORE/mvcblog/wwwroot/lib/multiple-select/multiple-select.min.js @@ -0,0 +1,10 @@ +/** + * multiple-select - Multiple select is a jQuery plugin to select multiple elements with checkboxes :). + * + * @version v1.5.2 + * @homepage http://multiple-select.wenzhixin.net.cn + * @author wenzhixin (http://wenzhixin.net.cn/) + * @license MIT + */ + +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(require("jquery")):"function"==typeof define&&define.amd?define(["jquery"],e):e((t=t||self).jQuery)}(this,(function(t){"use strict";function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){for(var n=0;n0?ct:at)(t)},ft=Math.min,pt=function(t){return t>0?ft(ht(t),9007199254740991):0},dt=Math.max,vt=Math.min,gt=function(t,e){var n=ht(t);return n<0?dt(n+e,0):vt(n,e)},yt=function(t){return function(e,n,i){var r,u=D(e),o=pt(u.length),s=gt(i,o);if(t&&n!=n){for(;o>s;)if((r=u[s++])!=r)return!0}else for(;o>s;s++)if((t||s in u)&&u[s]===n)return t||s||0;return!t&&-1}},Et={includes:yt(!0),indexOf:yt(!1)},bt=Et.indexOf,mt=function(t,e){var n,i=D(t),r=0,u=[];for(n in i)!$(Q,n)&&$(i,n)&&u.push(n);for(;e.length>r;)$(i,n=e[r++])&&(~bt(u,n)||u.push(n));return u},At=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],Ft=At.concat("length","prototype"),St={f:Object.getOwnPropertyNames||function(t){return mt(t,Ft)}},Ct={f:Object.getOwnPropertySymbols},kt=lt("Reflect","ownKeys")||function(t){var e=St.f(R(t)),n=Ct.f;return n?e.concat(n(t)):e},Dt=function(t,e){for(var n=kt(e),i=P.f,r=I.f,u=0;uu;)P.f(t,n=i[u++],e[n]);return t},Gt=lt("document","documentElement"),Ut=J("IE_PROTO"),Wt=function(){},Vt=function(){var t,e=T("iframe"),n=At.length;for(e.style.display="none",Gt.appendChild(e),e.src=String("javascript:"),(t=e.contentWindow.document).open(),t.write("