From 6d70d3ab8006f4ea2562d94ceac605aef6bfc129 Mon Sep 17 00:00:00 2001 From: kimanicodes Date: Tue, 22 Jul 2025 21:06:06 +0300 Subject: [PATCH] Added Login and Signup --- Controllers/AuthController.cs | 211 +++++++++++ Controllers/BooksController.cs | 81 ----- Controllers/UserController.cs | 31 ++ Data/ApplicationDbContext.cs | 8 +- EduCoreSuite.csproj | 5 + Models/Book.cs | 19 - Models/User.cs | 75 ++++ .../Auth/ForgotPasswordViewModel.cs | 10 + Models/ViewModels/Auth/LoginViewModel.cs | 15 + Models/ViewModels/Auth/RegisterViewModel.cs | 27 ++ .../ViewModels/Auth/ResetPasswordViewModel.cs | 15 + Models/ViewModels/Auth/VerifyCodeViewModel.cs | 10 + Models/ViewModels/User/ApprovalViewModel.cs | 11 + Program.cs | 64 +++- Properties/launchSettings.json | 2 +- Properties/serviceDependencies.json | 12 + Properties/serviceDependencies.local.json | 13 + VaultDB.sql | Bin 0 -> 19902 bytes Views/Auth/ForgotPassword.cshtml | 34 ++ Views/Auth/Login.cshtml | 45 +++ Views/Auth/Register.cshtml | 211 +++++++++++ Views/Auth/ResetPassword.cshtml | 34 ++ Views/Auth/VerifyCode.cshtml | 28 ++ {Pages => Views}/Error.cshtml | 0 {Pages => Views}/Error.cshtml.cs | 0 {Pages => Views}/Index.cshtml | 0 {Pages => Views}/Index.cshtml.cs | 0 {Pages => Views}/Privacy.cshtml | 0 {Pages => Views}/Privacy.cshtml.cs | 0 {Pages => Views}/Shared/_Layout.cshtml | 8 +- {Pages => Views}/Shared/_Layout.cshtml.css | 0 .../Shared/_ValidationScriptsPartial.cshtml | 0 Views/User/Approval.cshtml | 60 ++++ {Pages => Views}/_ViewImports.cshtml | 0 {Pages => Views}/_ViewStart.cshtml | 0 appsettings.json | 6 +- wwwroot/css/site.css | 336 +++++++++++++++++- 37 files changed, 1240 insertions(+), 131 deletions(-) create mode 100644 Controllers/AuthController.cs delete mode 100644 Controllers/BooksController.cs create mode 100644 Controllers/UserController.cs delete mode 100644 Models/Book.cs create mode 100644 Models/User.cs create mode 100644 Models/ViewModels/Auth/ForgotPasswordViewModel.cs create mode 100644 Models/ViewModels/Auth/LoginViewModel.cs create mode 100644 Models/ViewModels/Auth/RegisterViewModel.cs create mode 100644 Models/ViewModels/Auth/ResetPasswordViewModel.cs create mode 100644 Models/ViewModels/Auth/VerifyCodeViewModel.cs create mode 100644 Models/ViewModels/User/ApprovalViewModel.cs create mode 100644 Properties/serviceDependencies.json create mode 100644 Properties/serviceDependencies.local.json create mode 100644 VaultDB.sql create mode 100644 Views/Auth/ForgotPassword.cshtml create mode 100644 Views/Auth/Login.cshtml create mode 100644 Views/Auth/Register.cshtml create mode 100644 Views/Auth/ResetPassword.cshtml create mode 100644 Views/Auth/VerifyCode.cshtml rename {Pages => Views}/Error.cshtml (100%) rename {Pages => Views}/Error.cshtml.cs (100%) rename {Pages => Views}/Index.cshtml (100%) rename {Pages => Views}/Index.cshtml.cs (100%) rename {Pages => Views}/Privacy.cshtml (100%) rename {Pages => Views}/Privacy.cshtml.cs (100%) rename {Pages => Views}/Shared/_Layout.cshtml (93%) rename {Pages => Views}/Shared/_Layout.cshtml.css (100%) rename {Pages => Views}/Shared/_ValidationScriptsPartial.cshtml (100%) create mode 100644 Views/User/Approval.cshtml rename {Pages => Views}/_ViewImports.cshtml (100%) rename {Pages => Views}/_ViewStart.cshtml (100%) diff --git a/Controllers/AuthController.cs b/Controllers/AuthController.cs new file mode 100644 index 0000000..f2e6f8a --- /dev/null +++ b/Controllers/AuthController.cs @@ -0,0 +1,211 @@ +using Microsoft.AspNetCore.Mvc; +using EduCoreSuite.Models; +using EduCoreSuite.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using EduCoreSuite.Models.ViewModels.Auth; +using Microsoft.AspNetCore.Authorization; + +[Route("Auth")] +public class AuthController : Controller +{ + private readonly ApplicationDbContext _db; + + public AuthController(ApplicationDbContext db) + { + _db = db; + } + + [HttpGet("Register")] + public IActionResult Register() + { + var viewModel = new RegisterViewModel(); + ViewBag.Roles = _db.Roles.ToList(); + return View(viewModel); + } + + [HttpPost("Register")] + [ValidateAntiForgeryToken] + public async Task Register(RegisterViewModel viewModel) + { + if (!ModelState.IsValid) + { + ViewBag.Roles = _db.Roles.ToList(); + return View(viewModel); + } + + if (await _db.Users.AnyAsync(u => u.Email == viewModel.Email || u.UserName == viewModel.UserName)) + { + ModelState.AddModelError("Email", "User already exists"); + ViewBag.Roles = _db.Roles.ToList(); + return View(viewModel); + } + + var user = new User + { + UserName = viewModel.UserName, + Email = viewModel.Email, + PasswordHash = HashPassword(viewModel.Password), + FirstName = viewModel.FirstName, + LastName = viewModel.LastName, + RoleId = viewModel.RoleId + }; + + _db.Users.Add(user); + await _db.SaveChangesAsync(); + return RedirectToAction("Login"); + } + + [HttpGet("Login")] + [AllowAnonymous] + public IActionResult Login() + { + return View(new LoginViewModel()); + } + + [HttpPost("Login")] + [ValidateAntiForgeryToken] + public async Task Login(LoginViewModel viewModel) + { + if (!ModelState.IsValid) + { + return View(viewModel); + } + + var user = await _db.Users.FirstOrDefaultAsync(u => u.Email == viewModel.Email); + if (user == null || !VerifyPassword(viewModel.Password, user.PasswordHash)) + { + ModelState.AddModelError("", "Invalid credentials"); + return View(viewModel); + } + + var claims = new List + { + new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), + new Claim(ClaimTypes.Name, user.UserName), + new Claim(ClaimTypes.Email, user.Email), + new Claim("CustomUserId", user.CustomUserId), + new Claim(ClaimTypes.Role, user.RoleId.ToString()) + }; + + var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); + var principal = new ClaimsPrincipal(identity); + + await HttpContext.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + principal, + new AuthenticationProperties { IsPersistent = viewModel.RememberMe }); + + return RedirectToAction("Approval", "User"); + } + + [HttpGet("Logout")] + public async Task Logout() + { + await HttpContext.SignOutAsync(); + return RedirectToAction("Login"); + } + + [HttpGet("ForgotPassword")] + public IActionResult ForgotPassword() + { + return View(new ForgotPasswordViewModel()); + } + + [HttpPost("ForgotPassword")] + [ValidateAntiForgeryToken] + public async Task ForgotPassword(ForgotPasswordViewModel viewModel) + { + if (!ModelState.IsValid) + { + return View(viewModel); + } + + var user = await _db.Users.FirstOrDefaultAsync(u => u.Email == viewModel.Email); + if (user == null) + { + ModelState.AddModelError("", "Email not found"); + return View(viewModel); + } + + string code = new Random().Next(100000, 999999).ToString(); + HttpContext.Session.SetString("ResetCode", code); + HttpContext.Session.SetString("ResetEmail", viewModel.Email); + // TODO: Send email using SMTP or SendGrid + + return RedirectToAction("VerifyCode"); + } + + [HttpGet("VerifyCode")] + public IActionResult VerifyCode() + { + return View(new VerifyCodeViewModel()); + } + + [HttpPost("VerifyCode")] + [ValidateAntiForgeryToken] + public IActionResult VerifyCode(VerifyCodeViewModel viewModel) + { + if (!ModelState.IsValid) + { + return View(viewModel); + } + + var sessionCode = HttpContext.Session.GetString("ResetCode"); + if (viewModel.Code != sessionCode) + { + ModelState.AddModelError("", "Invalid code"); + return View(viewModel); + } + + return RedirectToAction("ResetPassword"); + } + + [HttpGet("ResetPassword")] + public IActionResult ResetPassword() + { + return View(new ResetPasswordViewModel()); + } + + [HttpPost("ResetPassword")] + [ValidateAntiForgeryToken] + public async Task ResetPassword(ResetPasswordViewModel viewModel) + { + if (!ModelState.IsValid) + { + return View(viewModel); + } + + var email = HttpContext.Session.GetString("ResetEmail"); + var user = await _db.Users.FirstOrDefaultAsync(u => u.Email == email); + if (user == null) + { + return RedirectToAction("Login"); + } + + user.PasswordHash = HashPassword(viewModel.NewPassword); + await _db.SaveChangesAsync(); + + // Clear the reset session + HttpContext.Session.Remove("ResetCode"); + HttpContext.Session.Remove("ResetEmail"); + + return RedirectToAction("Login"); + } + + private string HashPassword(string password) + { + using var sha = SHA256.Create(); + var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(password)); + return Convert.ToBase64String(bytes); + } + + private bool VerifyPassword(string password, string hash) + { + return HashPassword(password) == hash; + } +} \ No newline at end of file diff --git a/Controllers/BooksController.cs b/Controllers/BooksController.cs deleted file mode 100644 index 6ccaa11..0000000 --- a/Controllers/BooksController.cs +++ /dev/null @@ -1,81 +0,0 @@ -using EduCoreSuite.Data; -using EduCoreSuite.Models; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; - -namespace EduCoreSuite.Controllers -{ - [Route("api/[controller]")] - [ApiController] - public class BooksController : ControllerBase - { - private readonly ApplicationDbContext _context; - - public BooksController(ApplicationDbContext context) - { - _context = context; - } - - [HttpGet] - public async Task>> GetBooks() - { - return await _context.Books.ToListAsync(); - } - - [HttpGet("{id}")] - public async Task> GetBook(int id) - { - var book = await _context.Books.FindAsync(id); - - if (book == null) - return NotFound(); - - return book; - } - - [HttpPost] - public async Task> CreateBook(Book book) - { - _context.Books.Add(book); - await _context.SaveChangesAsync(); - - return CreatedAtAction(nameof(GetBook), new { id = book.Id }, book); - } - - [HttpPut("{id}")] - public async Task UpdateBook(int id, Book book) - { - if (id != book.Id) - return BadRequest(); - - _context.Entry(book).State = EntityState.Modified; - - try - { - await _context.SaveChangesAsync(); - } - catch (DbUpdateConcurrencyException) - { - if (!_context.Books.Any(e => e.Id == id)) - return NotFound(); - - throw; - } - - return NoContent(); - } - - [HttpDelete("{id}")] - public async Task DeleteBook(int id) - { - var book = await _context.Books.FindAsync(id); - if (book == null) - return NotFound(); - - _context.Books.Remove(book); - await _context.SaveChangesAsync(); - - return NoContent(); - } - } -} diff --git a/Controllers/UserController.cs b/Controllers/UserController.cs new file mode 100644 index 0000000..8766ffb --- /dev/null +++ b/Controllers/UserController.cs @@ -0,0 +1,31 @@ +using EduCoreSuite.Data; +using EduCoreSuite.Models.ViewModels.User; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Authorization; + + +namespace EduCoreSuite.Controllers +{ + public class UserController : Controller + { + private readonly ApplicationDbContext _db; + + public UserController(ApplicationDbContext db) + { + _db = db; + } + [Authorize(Roles = "3")] + + [HttpGet] + public async Task Approval() + { + var viewModel = new ApprovalViewModel + { + Users = await _db.Users.ToListAsync(), + Roles = await _db.Roles.ToDictionaryAsync(r => r.Id, r => r.NameOfRole) + }; + return View(viewModel); + } + } +} \ No newline at end of file diff --git a/Data/ApplicationDbContext.cs b/Data/ApplicationDbContext.cs index 7ecbeb4..8ef8fdd 100644 --- a/Data/ApplicationDbContext.cs +++ b/Data/ApplicationDbContext.cs @@ -5,8 +5,12 @@ namespace EduCoreSuite.Data { public class ApplicationDbContext : DbContext { - public DbSet Books { get; set; } - + public DbSet Users { get; set; } + public DbSet Roles { get; set; } + public DbSet Permissions { get; set; } + public DbSet RolesPermissions { get; set; } + public DbSet Courses { get; set; } + public DbSet Registrations { get; set; } public ApplicationDbContext(DbContextOptions options) : base(options) { diff --git a/EduCoreSuite.csproj b/EduCoreSuite.csproj index 50b9dd6..6e346ba 100644 --- a/EduCoreSuite.csproj +++ b/EduCoreSuite.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + fd318432-ae1c-4044-aa3e-05aef00aa902 @@ -17,6 +18,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Models/Book.cs b/Models/Book.cs deleted file mode 100644 index d6f3409..0000000 --- a/Models/Book.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace EduCoreSuite.Models -{ - public class Book - { - public int Id { get; set; } - - [Required] - public string Title { get; set; } = string.Empty; - - [Required] - public string Author { get; set; } = string.Empty; - - public int YearPublished { get; set; } - - public string Genre { get; set; } = string.Empty; - } -} diff --git a/Models/User.cs b/Models/User.cs new file mode 100644 index 0000000..6c4a3cf --- /dev/null +++ b/Models/User.cs @@ -0,0 +1,75 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EduCoreSuite.Models +{ + public class User + { + public long Id { get; set; } + + [Required, StringLength(50)] + public string UserName { get; set; } = string.Empty; + + [Required, EmailAddress] + public string Email { get; set; } = string.Empty; + + [Required] + public string PasswordHash { get; set; } = string.Empty; + + public long RoleId { get; set; } + + public string? FirstName { get; set; } + public string? LastName { get; set; } + + [NotMapped] + public string CustomUserId => Id.ToString("D3"); + } + + public class Role + { + public long Id { get; set; } + + [Required, StringLength(50)] + public string NameOfRole { get; set; } = string.Empty; + } + + public class Permission + { + public long Id { get; set; } + + [Required, StringLength(100)] + public string NameOfPermission { get; set; } = string.Empty; + } + + public class RolePermission + { + public long Id { get; set; } + + [Required] + public long RoleID { get; set; } + + [Required] + public long PermissionID { get; set; } + } + + public class Course + { + public long Id { get; set; } + + [StringLength(100)] + public string? CourseName { get; set; } + + [StringLength(20)] + public string? CourseCode { get; set; } + + public long? InstructorID { get; set; } + } + + public class Registration + { + public long Id { get; set; } + + public long StudentID { get; set; } + public long CourseID { get; set; } + } +} \ No newline at end of file diff --git a/Models/ViewModels/Auth/ForgotPasswordViewModel.cs b/Models/ViewModels/Auth/ForgotPasswordViewModel.cs new file mode 100644 index 0000000..7cc78bc --- /dev/null +++ b/Models/ViewModels/Auth/ForgotPasswordViewModel.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace EduCoreSuite.Models.ViewModels.Auth +{ + public class ForgotPasswordViewModel + { + [Required, EmailAddress] + public string Email { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Models/ViewModels/Auth/LoginViewModel.cs b/Models/ViewModels/Auth/LoginViewModel.cs new file mode 100644 index 0000000..6a0ffeb --- /dev/null +++ b/Models/ViewModels/Auth/LoginViewModel.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace EduCoreSuite.Models.ViewModels.Auth +{ + public class LoginViewModel + { + [Required, EmailAddress] + public string Email { get; set; } = string.Empty; + + [Required, DataType(DataType.Password)] + public string Password { get; set; } = string.Empty; + + public bool RememberMe { get; set; } + } +} \ No newline at end of file diff --git a/Models/ViewModels/Auth/RegisterViewModel.cs b/Models/ViewModels/Auth/RegisterViewModel.cs new file mode 100644 index 0000000..b3e7dc2 --- /dev/null +++ b/Models/ViewModels/Auth/RegisterViewModel.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; + +namespace EduCoreSuite.Models.ViewModels.Auth +{ + public class RegisterViewModel + { + [Required, StringLength(50)] + public string UserName { get; set; } = string.Empty; + + [Required, EmailAddress] + public string Email { get; set; } = string.Empty; + + [Required, DataType(DataType.Password)] + [StringLength(100, MinimumLength = 8)] + public string Password { get; set; } = string.Empty; + + [DataType(DataType.Password)] + [Compare("Password", ErrorMessage = "Passwords don't match")] + public string ConfirmPassword { get; set; } = string.Empty; + + public string? FirstName { get; set; } + public string? LastName { get; set; } + + [Required] + public long RoleId { get; set; } + } +} \ No newline at end of file diff --git a/Models/ViewModels/Auth/ResetPasswordViewModel.cs b/Models/ViewModels/Auth/ResetPasswordViewModel.cs new file mode 100644 index 0000000..dc84b15 --- /dev/null +++ b/Models/ViewModels/Auth/ResetPasswordViewModel.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace EduCoreSuite.Models.ViewModels.Auth +{ + public class ResetPasswordViewModel + { + [Required, DataType(DataType.Password)] + [StringLength(100, MinimumLength = 8)] + public string NewPassword { get; set; } = string.Empty; + + [DataType(DataType.Password)] + [Compare("NewPassword", ErrorMessage = "Passwords don't match")] + public string ConfirmPassword { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Models/ViewModels/Auth/VerifyCodeViewModel.cs b/Models/ViewModels/Auth/VerifyCodeViewModel.cs new file mode 100644 index 0000000..6195330 --- /dev/null +++ b/Models/ViewModels/Auth/VerifyCodeViewModel.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace EduCoreSuite.Models.ViewModels.Auth +{ + public class VerifyCodeViewModel + { + [Required] + public string Code { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Models/ViewModels/User/ApprovalViewModel.cs b/Models/ViewModels/User/ApprovalViewModel.cs new file mode 100644 index 0000000..5a45549 --- /dev/null +++ b/Models/ViewModels/User/ApprovalViewModel.cs @@ -0,0 +1,11 @@ +using EduCoreSuite.Models; +using System.Collections.Generic; + +namespace EduCoreSuite.Models.ViewModels.User +{ + public class ApprovalViewModel + { + public List Users { get; set; } // Fully qualify with Models.User + public Dictionary Roles { get; set; } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs index 2f5d36a..24d1281 100644 --- a/Program.cs +++ b/Program.cs @@ -1,31 +1,65 @@ -using EduCoreSuite.Data; +using EduCoreSuite.Data; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); -// ?? Add services to the container +// ✅ Register MVC controller services +builder.Services.AddControllers(); // <-- Add controllers builder.Services.AddRazorPages(); -// ? Register ApplicationDbContext with SQL Server connection +// ✅ Register your DbContext builder.Services.AddDbContext(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); -var app = builder.Build(); -// ?? Configure the HTTP request pipeline -if (!app.Environment.IsDevelopment()) -{ - app.UseExceptionHandler("/Error"); - app.UseHsts(); -} -app.UseHttpsRedirection(); -app.UseStaticFiles(); +builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie(options => + { + options.LoginPath = "/Auth/Login"; + options.LogoutPath = "/Auth/Logout"; + options.ExpireTimeSpan = TimeSpan.FromDays(7); + options.SlidingExpiration = true; + }); -app.UseRouting(); +builder.Services.AddSession(); +builder.Services.AddHttpContextAccessor(); -app.UseAuthorization(); // If you use authentication, add `app.UseAuthentication()` before this +builder.Services.AddControllersWithViews(); +var app = builder.Build(); + +app.UseStaticFiles(); +app.UseRouting(); +app.UseAuthentication(); +app.UseAuthorization(); +app.UseSession(); +app.MapControllerRoute( + name: "default", + pattern: "{controller=Auth}/{action=Login}/{id?}"); -app.MapRazorPages(); +app.MapFallbackToController("Login", "Auth"); app.Run(); + +//var app = builder.Build(); + +//// ✅ Configure HTTP request pipeline +//if (!app.Environment.IsDevelopment()) +//{ +// app.UseExceptionHandler("/Error"); +// app.UseHsts(); +//} + +//app.UseHttpsRedirection(); +//app.UseStaticFiles(); + +//app.UseRouting(); + +//app.UseAuthorization(); + +//// ✅ Map Razor Pages and API Controllers +//app.MapRazorPages(); +//app.MapControllers(); // <-- Add this line to expose your API endpoints + +//app.Run(); \ No newline at end of file diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index 625109a..664e4ca 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -22,7 +22,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "https://localhost:7065;http://localhost:5138", + "applicationUrl": "https://192.168.100.61;http://localhost:5138", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/Properties/serviceDependencies.json b/Properties/serviceDependencies.json new file mode 100644 index 0000000..1729da0 --- /dev/null +++ b/Properties/serviceDependencies.json @@ -0,0 +1,12 @@ +{ + "dependencies": { + "secrets1": { + "type": "secrets" + }, + "mssql1": { + "type": "mssql", + "connectionId": "ConnectionStrings:DefaultConnection1", + "dynamicId": null + } + } +} \ No newline at end of file diff --git a/Properties/serviceDependencies.local.json b/Properties/serviceDependencies.local.json new file mode 100644 index 0000000..3661c45 --- /dev/null +++ b/Properties/serviceDependencies.local.json @@ -0,0 +1,13 @@ +{ + "dependencies": { + "secrets1": { + "type": "secrets.user" + }, + "mssql1": { + "secretStore": "LocalSecretsFile", + "type": "mssql.local", + "connectionId": "ConnectionStrings:DefaultConnection1", + "dynamicId": null + } + } +} \ No newline at end of file diff --git a/VaultDB.sql b/VaultDB.sql new file mode 100644 index 0000000000000000000000000000000000000000..1051d20a07396e40c224f5c89f5ccd721d19777d GIT binary patch literal 19902 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 hwA3s4Q+oc)8a +

Forgot Password

+ +
+
+ + + +
+ +
+ +
+
+ + + + +@section Scripts { + @{ + await Html.RenderPartialAsync("_ValidationScriptsPartial"); + } +} \ No newline at end of file diff --git a/Views/Auth/Login.cshtml b/Views/Auth/Login.cshtml new file mode 100644 index 0000000..7bff321 --- /dev/null +++ b/Views/Auth/Login.cshtml @@ -0,0 +1,45 @@ +@model EduCoreSuite.Models.ViewModels.Auth.LoginViewModel +@{ + ViewData["Title"] = "Login"; +} + + + +@section Scripts { + @{ + await Html.RenderPartialAsync("_ValidationScriptsPartial"); + } +} \ No newline at end of file diff --git a/Views/Auth/Register.cshtml b/Views/Auth/Register.cshtml new file mode 100644 index 0000000..bd34699 --- /dev/null +++ b/Views/Auth/Register.cshtml @@ -0,0 +1,211 @@ +@model EduCoreSuite.Models.ViewModels.Auth.RegisterViewModel +@{ + ViewData["Title"] = "Register"; + var roles = ViewBag.Roles as List; +} + +
+
+
+ +
+

Sign Up

+

Create new account

+
+ +
+ +
+ + + +
+ + +
+
+ + + +
+
+ + + +
+
+ +
+ +
+ + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ +
+ + +
+ +
+
+ + + @*
+

Password Requirements:

+
+
+ + At least 8 characters long +
+
+ + Contains uppercase letter +
+
+ + Contains number +
+
+
*@ + + +
+ +
+ + +
+

Already have an account?

+ + Sign In + +
+
+
+
+
+ +@section Scripts { + @{ + await Html.RenderPartialAsync("_ValidationScriptsPartial"); + } + + + + + +} \ No newline at end of file diff --git a/Views/Auth/ResetPassword.cshtml b/Views/Auth/ResetPassword.cshtml new file mode 100644 index 0000000..f45b299 --- /dev/null +++ b/Views/Auth/ResetPassword.cshtml @@ -0,0 +1,34 @@ +@model EduCoreSuite.Models.ViewModels.Auth.ResetPasswordViewModel +@{ + ViewData["Title"] = "Reset Password"; +} + +
+

Reset Password

+ +
+
+ + + +
+ +
+ + + +
+ +
+ +
+
+
+ +@section Scripts { + @{ + await Html.RenderPartialAsync("_ValidationScriptsPartial"); + } +} \ No newline at end of file diff --git a/Views/Auth/VerifyCode.cshtml b/Views/Auth/VerifyCode.cshtml new file mode 100644 index 0000000..9650847 --- /dev/null +++ b/Views/Auth/VerifyCode.cshtml @@ -0,0 +1,28 @@ +@model EduCoreSuite.Models.ViewModels.Auth.VerifyCodeViewModel +@{ + ViewData["Title"] = "Verify Code"; +} + +
+

Verify Code

+ +
+
+ + + +
+ +
+ +
+
+
+ +@section Scripts { + @{ + await Html.RenderPartialAsync("_ValidationScriptsPartial"); + } +} \ No newline at end of file diff --git a/Pages/Error.cshtml b/Views/Error.cshtml similarity index 100% rename from Pages/Error.cshtml rename to Views/Error.cshtml diff --git a/Pages/Error.cshtml.cs b/Views/Error.cshtml.cs similarity index 100% rename from Pages/Error.cshtml.cs rename to Views/Error.cshtml.cs diff --git a/Pages/Index.cshtml b/Views/Index.cshtml similarity index 100% rename from Pages/Index.cshtml rename to Views/Index.cshtml diff --git a/Pages/Index.cshtml.cs b/Views/Index.cshtml.cs similarity index 100% rename from Pages/Index.cshtml.cs rename to Views/Index.cshtml.cs diff --git a/Pages/Privacy.cshtml b/Views/Privacy.cshtml similarity index 100% rename from Pages/Privacy.cshtml rename to Views/Privacy.cshtml diff --git a/Pages/Privacy.cshtml.cs b/Views/Privacy.cshtml.cs similarity index 100% rename from Pages/Privacy.cshtml.cs rename to Views/Privacy.cshtml.cs diff --git a/Pages/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml similarity index 93% rename from Pages/Shared/_Layout.cshtml rename to Views/Shared/_Layout.cshtml index e648162..b69c045 100644 --- a/Pages/Shared/_Layout.cshtml +++ b/Views/Shared/_Layout.cshtml @@ -23,7 +23,7 @@ Home @@ -47,5 +47,11 @@ @await RenderSectionAsync("Scripts", required: false) + + @section Scripts { + @{ + await Html.RenderPartialAsync("_ValidationScriptsPartial"); + } + } \ No newline at end of file diff --git a/Pages/Shared/_Layout.cshtml.css b/Views/Shared/_Layout.cshtml.css similarity index 100% rename from Pages/Shared/_Layout.cshtml.css rename to Views/Shared/_Layout.cshtml.css diff --git a/Pages/Shared/_ValidationScriptsPartial.cshtml b/Views/Shared/_ValidationScriptsPartial.cshtml similarity index 100% rename from Pages/Shared/_ValidationScriptsPartial.cshtml rename to Views/Shared/_ValidationScriptsPartial.cshtml diff --git a/Views/User/Approval.cshtml b/Views/User/Approval.cshtml new file mode 100644 index 0000000..2f29192 --- /dev/null +++ b/Views/User/Approval.cshtml @@ -0,0 +1,60 @@ +@model EduCoreSuite.Models.ViewModels.User.ApprovalViewModel +@{ + ViewData["Title"] = "User Approval"; +} + +
+

User Approval

+ +
+ +
+ +
+ + + + + + + + + + + + @foreach (var user in Model.Users) + { + + + + + + + + } + +
User IDNameEmailUser TypeActions
@user.CustomUserId@user.FirstName @user.LastName@user.Email + @(Model.Roles.ContainsKey(user.RoleId) ? Model.Roles[user.RoleId] : "Unknown") + +
+ + +
+
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/Pages/_ViewImports.cshtml b/Views/_ViewImports.cshtml similarity index 100% rename from Pages/_ViewImports.cshtml rename to Views/_ViewImports.cshtml diff --git a/Pages/_ViewStart.cshtml b/Views/_ViewStart.cshtml similarity index 100% rename from Pages/_ViewStart.cshtml rename to Views/_ViewStart.cshtml diff --git a/appsettings.json b/appsettings.json index 30d7bb3..819dfeb 100644 --- a/appsettings.json +++ b/appsettings.json @@ -3,10 +3,10 @@ "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" - }, - "ConnectionStrings": { - "DefaultConnection": "Server=PRONIC;Database=EBUCOREDB;MultipleActiveResultSets=True;TrustServerCertificate=True;" } }, + "ConnectionStrings": { + "DefaultConnection": "Server=localhost;Database=VaultDB;Integrated Security=True;MultipleActiveResultSets=True;TrustServerCertificate=True;" + }, "AllowedHosts": "*" } diff --git a/wwwroot/css/site.css b/wwwroot/css/site.css index f8d98fc..9aafe49 100644 --- a/wwwroot/css/site.css +++ b/wwwroot/css/site.css @@ -1,22 +1,340 @@ html { - font-size: 14px; + font-size: 14px; } @media (min-width: 768px) { - html { - font-size: 16px; - } + html { + font-size: 16px; + } +} + +@media (max-width: 640px) { + .login-container { + margin: 1rem; + max-width: 100%; + } } .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { - box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; + box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; } html { - position: relative; - min-height: 100%; + position: relative; + min-height: 100%; } body { - margin-bottom: 60px; -} \ No newline at end of file + margin-bottom: 60px; +} + +/* Login Page Styles */ +/* Reset and base styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + /* background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + display: flex;*/ + /*align-items: center;*/ + justify-content: center; + /*padding: 20px;*/ +} + +/* Main login container */ +/* Simple centering approach - add this to your existing CSS */ +.login-container { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border-radius: 20px; + padding: 48px 40px; + width: 100%; + max-width: 550px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + margin: 50px auto; + position: relative; +} + +/* Add background to the page */ +/*body { + min-height: 100vh; +}*/ + +/* Header section */ +.login-header { + text-align: center; + margin-bottom: 40px; +} + +.login-title { + font-size: 28px; + font-weight: 700; + color: #2d3748; + margin-bottom: 8px; + letter-spacing: -0.5px; +} + +.login-subtitle { + font-size: 16px; + color: #718096; + font-weight: 400; + line-height: 1.5; +} + +/* Form styling */ +form { + margin-bottom: 32px; +} + +.form-group { + margin-bottom: 24px; +} + +.form-label { + display: block; + font-size: 14px; + font-weight: 600; + color: #4a5568; + margin-bottom: 8px; + letter-spacing: 0.02em; +} + +.form-input { + width: 100%; + padding: 14px 16px; + border: 1.5px solid #e2e8f0; + border-radius: 12px; + font-size: 16px; + background: #ffffff; + transition: all 0.2s ease; + outline: none; + color: #2d3748; +} + + .form-input::placeholder { + color: #a0aec0; + font-size: 15px; + } + + .form-input:focus { + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); + transform: translateY(-1px); + } + + .form-input:hover { + border-color: #cbd5e0; + } + +/* Remember me and forgot password section */ +.remember-forgot { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 32px; + flex-wrap: wrap; + gap: 12px; +} + +.remember-me { + display: flex; + align-items: center; + gap: 8px; +} + + .remember-me input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: #667eea; + border-radius: 4px; + } + + .remember-me label { + font-size: 14px; + color: #4a5568; + cursor: pointer; + user-select: none; + } + +.remember-forgot a { + color: #667eea; + text-decoration: none; + font-size: 14px; + font-weight: 500; + transition: color 0.2s ease; +} + + .remember-forgot a:hover { + color: #5a67d8; + text-decoration: underline; + } + +/* Sign In button */ +.login-button { + width: 100%; + padding: 16px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + border-radius: 12px; + color: white; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + text-transform: none; + letter-spacing: 0.02em; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); +} + + .login-button:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4); + background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%); + } + + .login-button:active { + transform: translateY(0); + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); + } + +/* Create account section */ +.create-account { + text-align: center; + font-size: 14px; + color: #718096; + line-height: 1.5; +} + + .create-account a { + color: #667eea; + text-decoration: none; + font-weight: 500; + transition: color 0.2s ease; + } + + .create-account a:hover { + color: #5a67d8; + text-decoration: underline; + } + +/* Validation errors */ +.validation-error { + display: block; + color: #e53e3e; + font-size: 12px; + margin-top: 6px; + font-weight: 500; +} + +/* Responsive design */ +@media (max-width: 480px) { + .login-container { + padding: 32px 24px; + margin: 16px; + border-radius: 16px; + } + + .login-title { + font-size: 24px; + } + + .login-subtitle { + font-size: 14px; + } + + .form-input { + padding: 12px 14px; + font-size: 15px; + } + + .login-button { + padding: 14px; + font-size: 15px; + } + + .remember-forgot { + flex-direction: column; + align-items: flex-start; + gap: 16px; + } +} + +@media (max-width: 360px) { + .login-container { + padding: 24px 20px; + } + + .login-title { + font-size: 22px; + } +} + +/* Enhanced focus states for accessibility */ +.form-input:focus, +.login-button:focus, +.remember-me input:focus { + outline: 2px solid #667eea; + outline-offset: 2px; +} + +/* Loading state for button (optional) */ +.login-button:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +/* Smooth animations */ +.login-container { + animation: fadeInUp 0.6s ease-out; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Input field animations */ +.form-input { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Hover effects for better UX */ +.form-group:hover .form-input { + border-color: #cbd5e0; +} + +.form-group:hover .form-label { + color: #2d3748; +} + +/* Custom scrollbar for overflow content */ +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; +} + +::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 3px; +} + + ::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; + }