aboutsummaryrefslogtreecommitdiff
path: root/ExamTemplate/Web/ExamTemplate.Web
diff options
context:
space:
mode:
Diffstat (limited to 'ExamTemplate/Web/ExamTemplate.Web')
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/Configurations/ControllerUserMappings.cs18
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/Controllers/AccountController.cs146
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/Controllers/HomeController.cs33
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/ExamTemplate.Web.csproj20
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/Program.cs20
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/Properties/launchSettings.json27
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/Startup.cs134
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/Views/Account/Edit.cshtml20
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/Views/Account/Login.cshtml23
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/Views/Account/Profile.cshtml33
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/Views/Account/Register.cshtml27
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/Views/Home/Index.cshtml8
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/Views/Shared/Error.cshtml25
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/Views/Shared/ErrorNotFound.cshtml10
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/Views/Shared/_FooterContent.cshtml8
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/Views/Shared/_Layout.cshtml25
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/Views/Shared/_Navbar.cshtml31
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/Views/_ViewImports.cshtml5
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/Views/_ViewStart.cshtml3
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/appsettings.json18
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/wwwroot/css/site.css79
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/wwwroot/css/styles.css143
-rw-r--r--ExamTemplate/Web/ExamTemplate.Web/wwwroot/favicon.icobin0 -> 32038 bytes
23 files changed, 856 insertions, 0 deletions
diff --git a/ExamTemplate/Web/ExamTemplate.Web/Configurations/ControllerUserMappings.cs b/ExamTemplate/Web/ExamTemplate.Web/Configurations/ControllerUserMappings.cs
new file mode 100644
index 0000000..05c57e2
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/Configurations/ControllerUserMappings.cs
@@ -0,0 +1,18 @@
+using AutoMapper;
+using ExamTemplate.Services.Models.User;
+using ExamTemplate.Web.Models.User;
+
+namespace ExamTemplate.Services.Configurations
+{
+ public class ControllerUserMappings : Profile
+ {
+ public ControllerUserMappings()
+ {
+ CreateMap<RegisterUserWebModel, RegisterUserServiceModel>();
+ CreateMap<LoginUserWebModel, LoginUserServiceModel>();
+ CreateMap<UserServiceModel, UserWebModel>();
+ CreateMap<UserServiceModel, EditUserWebModel>();
+ CreateMap<EditUserWebModel, UserServiceModel>();
+ }
+ }
+}
diff --git a/ExamTemplate/Web/ExamTemplate.Web/Controllers/AccountController.cs b/ExamTemplate/Web/ExamTemplate.Web/Controllers/AccountController.cs
new file mode 100644
index 0000000..2c2eb32
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/Controllers/AccountController.cs
@@ -0,0 +1,146 @@
+using ExamTemplate.Services.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+using ExamTemplate.Web.Models.User;
+using AutoMapper;
+using ExamTemplate.Services.Models.User;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+
+namespace ExamTemplate.Web.Controllers
+{
+ [Authorize]
+ public class AccountController : Controller
+ {
+ private readonly IMapper _autoMapper;
+ private readonly IUserService _userService;
+
+ public AccountController(IMapper autoMapper, IUserService userService)
+ {
+ this._autoMapper = autoMapper;
+ this._userService = userService;
+ }
+
+ [HttpGet]
+ [AllowAnonymous]
+ public IActionResult Register()
+ {
+ return View();
+ }
+
+ [HttpPost]
+ [AllowAnonymous]
+ public async Task<IActionResult> Register(RegisterUserWebModel registerUserWebModel)
+ {
+ if (!ModelState.IsValid)
+ return View(registerUserWebModel);
+
+ RegisterUserServiceModel registerUserServiceModel = this._autoMapper.Map<RegisterUserServiceModel>(registerUserWebModel);
+
+ bool result = await this._userService.RegisterUserAsync(registerUserServiceModel);
+
+ if (result)
+ return await this.Login(new LoginUserWebModel {
+ Username = registerUserServiceModel.Username,
+ Password = registerUserServiceModel.Password
+ });
+ else
+ return View(registerUserWebModel);
+ }
+
+ [HttpGet]
+ [AllowAnonymous]
+ public IActionResult Login()
+ {
+ return View();
+ }
+
+ [HttpPost]
+ [AllowAnonymous]
+ public async Task<IActionResult> Login(LoginUserWebModel loginUserWebModel)
+ {
+ if (!ModelState.IsValid)
+ return View(loginUserWebModel);
+
+ LoginUserServiceModel loginUserServiceModel = this._autoMapper.Map<LoginUserServiceModel>(loginUserWebModel);
+
+ bool result = await this._userService.LoginUserAsync(loginUserServiceModel);
+
+ if (result)
+ return RedirectToAction("Index", "Home");
+ else
+ return View(loginUserWebModel);
+ }
+
+ [HttpPost]
+ public async Task<IActionResult> Logout()
+ {
+ await this._userService.LogoutAsync();
+
+ return RedirectToAction("Login");
+ }
+
+ [HttpGet]
+ [AllowAnonymous]
+ public async Task<IActionResult> Profile(string username)
+ {
+ UserServiceModel userServiceModel = await this._userService.GetUserByUsernameAsync(username);
+
+ if (userServiceModel == default(UserServiceModel))
+ return RedirectToAction("ErrorNotFound", "Home");
+
+ UserWebModel userWebModel = this._autoMapper.Map<UserWebModel>(userServiceModel);
+
+ return View(userWebModel);
+ }
+
+ [HttpGet]
+ public async Task<IActionResult> Edit()
+ {
+ UserServiceModel userServiceModel = await this._userService.GetUserByClaimsAsync(this.HttpContext.User);
+
+ if (userServiceModel == default(UserServiceModel))
+ return RedirectToAction("ErrorNotFound", "Home");
+
+ EditUserWebModel editUserWebModel = this._autoMapper.Map<EditUserWebModel>(userServiceModel);
+
+ return View(editUserWebModel);
+ }
+
+ [HttpPost]
+ public async Task<IActionResult> Edit(EditUserWebModel editUserWebModel)
+ {
+ if (!ModelState.IsValid)
+ return View(editUserWebModel);
+
+ if (!this._userService.IsSignedIn(HttpContext.User))
+ return RedirectToAction("Login");
+
+ UserServiceModel loggedInUser = await this._userService.GetUserByClaimsAsync(HttpContext.User);
+
+ UserServiceModel userServiceModel = this._autoMapper.Map<UserServiceModel>(editUserWebModel);
+ bool result = await this._userService.EditUserAsync(HttpContext.User, userServiceModel);
+
+ if (result)
+ {
+ if (loggedInUser.Username != editUserWebModel.Username)
+ await this._userService.LogoutAsync();
+
+ return RedirectToAction("Profile", new { username = editUserWebModel.Username });
+ }
+ else
+ return RedirectToAction("Profile", new { username = loggedInUser.Username });
+ }
+
+ [HttpPost]
+ public async Task<IActionResult> Delete()
+ {
+ await this._userService.LogoutAsync();
+ bool result = await this._userService.DeleteUserAsync(HttpContext.User);
+
+ if (result)
+ return RedirectToAction("Login");
+ else
+ return RedirectToAction("Index", "Home");
+ }
+ }
+}
diff --git a/ExamTemplate/Web/ExamTemplate.Web/Controllers/HomeController.cs b/ExamTemplate/Web/ExamTemplate.Web/Controllers/HomeController.cs
new file mode 100644
index 0000000..d9cfc45
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/Controllers/HomeController.cs
@@ -0,0 +1,33 @@
+using System.Diagnostics;
+using ExamTemplate.Web.Models;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace ExamTemplate.Web.Controllers
+{
+ public class HomeController : Controller
+ {
+ private readonly ILogger<HomeController> _logger;
+
+ public HomeController(ILogger<HomeController> logger)
+ {
+ _logger = logger;
+ }
+
+ public IActionResult Index()
+ {
+ return View();
+ }
+
+ [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+ public IActionResult Error()
+ {
+ return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+ }
+
+ public IActionResult ErrorNotFound()
+ {
+ return View();
+ }
+ }
+}
diff --git a/ExamTemplate/Web/ExamTemplate.Web/ExamTemplate.Web.csproj b/ExamTemplate/Web/ExamTemplate.Web/ExamTemplate.Web.csproj
new file mode 100644
index 0000000..b3e1542
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/ExamTemplate.Web.csproj
@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <ItemGroup>
+ <ProjectReference Include="..\ExamTemplate.Web.Models\ExamTemplate.Web.Models.csproj" />
+ <ProjectReference Include="..\..\Services\ExamTemplate.Services\ExamTemplate.Services.csproj" />
+ <ProjectReference Include="..\..\Services\ExamTemplate.Services.Models\ExamTemplate.Services.Models.csproj" />
+ <ProjectReference Include="..\..\Common\ExamTemplate.Common\ExamTemplate.Common.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Automapper" Version="10.1.1" />
+ <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />
+ <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="5.0.5.1" />
+ </ItemGroup>
+
+ <PropertyGroup>
+ <TargetFramework>net5.0</TargetFramework>
+ </PropertyGroup>
+
+</Project>
diff --git a/ExamTemplate/Web/ExamTemplate.Web/Program.cs b/ExamTemplate/Web/ExamTemplate.Web/Program.cs
new file mode 100644
index 0000000..be33374
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/Program.cs
@@ -0,0 +1,20 @@
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Hosting;
+
+namespace ExamTemplate.Web
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ CreateHostBuilder(args).Build().Run();
+ }
+
+ public static IHostBuilder CreateHostBuilder(string[] args) =>
+ Host.CreateDefaultBuilder(args)
+ .ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.UseStartup<Startup>();
+ });
+ }
+}
diff --git a/ExamTemplate/Web/ExamTemplate.Web/Properties/launchSettings.json b/ExamTemplate/Web/ExamTemplate.Web/Properties/launchSettings.json
new file mode 100644
index 0000000..080115b
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/Properties/launchSettings.json
@@ -0,0 +1,27 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:36205",
+ "sslPort": 44322
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "Web": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/ExamTemplate/Web/ExamTemplate.Web/Startup.cs b/ExamTemplate/Web/ExamTemplate.Web/Startup.cs
new file mode 100644
index 0000000..c18bca6
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/Startup.cs
@@ -0,0 +1,134 @@
+using System;
+using System.Linq;
+using ExamTemplate.Common;
+using ExamTemplate.Data;
+using ExamTemplate.Data.Models;
+using ExamTemplate.Services.Services;
+using ExamTemplate.Services.Interfaces;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace ExamTemplate.Web
+{
+ public class Startup
+ {
+ public Startup(IConfiguration configuration)
+ {
+ Configuration = configuration;
+ }
+
+ public IConfiguration Configuration { get; }
+
+ // This method gets called by the runtime. Use this method to add services to the container.
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddControllersWithViews();
+ services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
+
+ /*
+ * Dependency Injection configuration
+ */
+
+ services.AddTransient<ICloudinaryService, CloudinaryService>(options =>
+ new CloudinaryService(
+ cloudName: this.Configuration.GetSection("Cloud").GetSection("cloudName").Value,
+ apiKey: this.Configuration.GetSection("Cloud").GetSection("apiKey").Value,
+ apiSecret: this.Configuration.GetSection("Cloud").GetSection("apiSecret").Value));
+ services.AddTransient<IUserService, UserService>();
+
+ /*
+ * Database configuration
+ */
+
+ services.AddDbContext<TemplateContext>(options =>
+ options.UseNpgsql(this.Configuration.GetConnectionString("LocalDBConnection")));
+
+ // Needed for SignInManager and UserManager
+ services.AddIdentity<User, IdentityRole<Guid>>(options =>
+ {
+ options.SignIn.RequireConfirmedAccount = false;
+
+ // Password settings
+ options.Password.RequireDigit = false;
+ options.Password.RequireLowercase = false;
+ options.Password.RequireNonAlphanumeric = false;
+ options.Password.RequireUppercase = false;
+ options.Password.RequiredLength = 3;
+ options.Password.RequiredUniqueChars = 0;
+ }).AddRoles<IdentityRole<Guid>>()
+ .AddEntityFrameworkStores<TemplateContext>();
+ }
+
+ // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+ if (env.IsDevelopment())
+ {
+ app.UseDeveloperExceptionPage();
+ }
+ else
+ {
+ app.UseExceptionHandler("/Home/Error");
+ // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
+ app.UseHsts();
+ }
+
+ app.UseStaticFiles();
+
+ app.UseRouting();
+
+ app.UseAuthentication();
+ app.UseAuthorization();
+
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapControllerRoute(
+ name: "default",
+ pattern: "{controller=Home}/{action=Index}/{id?}");
+ endpoints.MapFallbackToController("ErrorNotFound", "Home");
+ });
+
+ /*
+ * Make sure that the database is migrated
+ * and that the User and Admin role exist in database
+ */
+
+ using var serviceScope = app.ApplicationServices.CreateScope();
+ using var dbContext = serviceScope.ServiceProvider.GetRequiredService<TemplateContext>();
+
+ dbContext.Database.Migrate();
+
+ var roleManager = (RoleManager<IdentityRole<Guid>>)serviceScope.ServiceProvider.GetService(typeof(RoleManager<IdentityRole<Guid>>));
+ foreach (string name in RoleConst.GetAllNames())
+ {
+ if (!dbContext.Roles.Any(x => x.Name == name))
+ {
+ IdentityRole<Guid> role = new IdentityRole<Guid>() { Name = name };
+ roleManager.CreateAsync(role).Wait();
+ }
+ }
+
+ /* If you want to create some custom database values at startup
+ * uncomment the following code
+ * and replace OBJCONST_ with your static class with constants (e.g. RoleConst)
+ * replace OBJS_ with the name of the DbSet of your database model (e.g. Roles)
+ * replace OBJ_ with the name of your database model (e.g. Role)
+
+ foreach (string name in OBJCONST_.GetAllNames())
+ {
+ if (!dbContext.OBJS_.Any(x => x.Name == name))
+ {
+ var entity = new OBJ_() { Id = Guid.NewGuid(), Name = name };
+ dbContext.OBJS_.Add(entity);
+ dbContext.SaveChanges();
+ }
+ }
+ */
+ }
+ }
+}
diff --git a/ExamTemplate/Web/ExamTemplate.Web/Views/Account/Edit.cshtml b/ExamTemplate/Web/ExamTemplate.Web/Views/Account/Edit.cshtml
new file mode 100644
index 0000000..a088742
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/Views/Account/Edit.cshtml
@@ -0,0 +1,20 @@
+@model EditUserWebModel
+@{
+ ViewData["Title"] = "Edit Profile";
+}
+
+<form asp-controller="Account" asp-action="Edit" method="post">
+ <label asp-for="Username">Username:</label>
+ <input type="text" asp-for="Username" placeholder="@Model.Username">
+ <span asp-validation-for="Username" class="form-error"></span>
+
+ <label asp-for="FirstName">First Name:</label>
+ <input type="text" asp-for="FirstName" placeholder="@Model.FirstName">
+ <span asp-validation-for="FirstName" class="form-error"></span>
+
+ <label asp-for="LastName">Last Name:</label>
+ <input type="text" asp-for="LastName" placeholder="@Model.LastName">
+ <span asp-validation-for="LastName" class="form-error"></span>
+
+ <input type="submit" value="Update Profile">
+</form>
diff --git a/ExamTemplate/Web/ExamTemplate.Web/Views/Account/Login.cshtml b/ExamTemplate/Web/ExamTemplate.Web/Views/Account/Login.cshtml
new file mode 100644
index 0000000..daa3f3e
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/Views/Account/Login.cshtml
@@ -0,0 +1,23 @@
+@model LoginUserWebModel
+@{
+ ViewData["Title"] = "Login";
+}
+
+<form asp-controller="Account" asp-action="Login" method="post">
+ <input type="text" asp-for="Username" placeholder="Username">
+ <span asp-validation-for="Username" class="form-error"></span>
+
+ <input type="password" asp-for="Password" placeholder="Password">
+ <span asp-validation-for="Password" class="form-error"></span>
+
+ <input type="submit">
+
+ @if (Model != null)
+ {
+ <p class="form-error">
+ Invalid credentials or account doesn't exist!
+ </p>
+ }
+</form>
+
+
diff --git a/ExamTemplate/Web/ExamTemplate.Web/Views/Account/Profile.cshtml b/ExamTemplate/Web/ExamTemplate.Web/Views/Account/Profile.cshtml
new file mode 100644
index 0000000..33fc882
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/Views/Account/Profile.cshtml
@@ -0,0 +1,33 @@
+@using Microsoft.AspNetCore.Identity
+
+@inject SignInManager<User> SignInManager
+@inject UserManager<User> UserManager
+
+@model UserWebModel
+@{
+ ViewData["Title"] = Model.Username + "'s Profile";
+}
+
+<p>
+ <h2>
+ @Model.FirstName @Model.LastName
+ </h2>
+ <div>
+ @Model.Username
+ </div>
+ @if (SignInManager.IsSignedIn(User))
+ {
+ @if(UserManager.GetUserName(User) == Model.Username)
+ {
+ <br>
+
+ <form asp-controller="Account" asp-action="Edit" method="get">
+ <input type="submit" value="Edit Profile">
+ </form>
+
+ <form asp-controller="Account" asp-action="Delete" method="post">
+ <input type="submit" value="Delete Profile">
+ </form>
+ }
+ }
+</p>
diff --git a/ExamTemplate/Web/ExamTemplate.Web/Views/Account/Register.cshtml b/ExamTemplate/Web/ExamTemplate.Web/Views/Account/Register.cshtml
new file mode 100644
index 0000000..e436d72
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/Views/Account/Register.cshtml
@@ -0,0 +1,27 @@
+@model RegisterUserWebModel
+@{
+ ViewData["Title"] = "Register";
+}
+
+<form asp-controller="Account" asp-action="Register" method="post">
+ <input type="text" asp-for="FirstName" placeholder="FirstName">
+ <span asp-validation-for="FirstName" class="form-error"></span>
+
+ <input type="text" asp-for="LastName" placeholder="LastName">
+ <span asp-validation-for="LastName" class="form-error"></span>
+
+ <input type="text" asp-for="Username" placeholder="Username">
+ <span asp-validation-for="Username" class="form-error"></span>
+
+ <input type="password" asp-for="Password" placeholder="Password">
+ <span asp-validation-for="Password" class="form-error"></span>
+
+ <input type="submit">
+
+ @if (Model != null)
+ {
+ <p class="form-error">
+ Couldn't register account!
+ </p>
+ }
+</form>
diff --git a/ExamTemplate/Web/ExamTemplate.Web/Views/Home/Index.cshtml b/ExamTemplate/Web/ExamTemplate.Web/Views/Home/Index.cshtml
new file mode 100644
index 0000000..56ea950
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/Views/Home/Index.cshtml
@@ -0,0 +1,8 @@
+@{
+ ViewData["Title"] = "Home";
+}
+
+<div class="text-center">
+ <h1 class="display-4">Welcome</h1>
+ <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
+</div>
diff --git a/ExamTemplate/Web/ExamTemplate.Web/Views/Shared/Error.cshtml b/ExamTemplate/Web/ExamTemplate.Web/Views/Shared/Error.cshtml
new file mode 100644
index 0000000..5a7ce95
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/Views/Shared/Error.cshtml
@@ -0,0 +1,25 @@
+@model ErrorViewModel
+@{
+ ViewData["Title"] = "Error";
+}
+
+<h1 class="text-danger">Error.</h1>
+<h2 class="text-danger">An error occurred while processing your request.</h2>
+
+@if (Model.ShowRequestId)
+{
+ <p>
+ <strong>Request ID:</strong> <code>@Model.RequestId</code>
+ </p>
+}
+
+<h3>Development Mode</h3>
+<p>
+ Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
+</p>
+<p>
+ <strong>The Development environment shouldn't be enabled for deployed applications.</strong>
+ It can result in displaying sensitive information from exceptions to end users.
+ For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
+ and restarting the app.
+</p>
diff --git a/ExamTemplate/Web/ExamTemplate.Web/Views/Shared/ErrorNotFound.cshtml b/ExamTemplate/Web/ExamTemplate.Web/Views/Shared/ErrorNotFound.cshtml
new file mode 100644
index 0000000..39fc5ca
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/Views/Shared/ErrorNotFound.cshtml
@@ -0,0 +1,10 @@
+@{
+ ViewData["Title"] = "404: Not found!";
+}
+
+<h2>
+ 404: Not found!
+</h2>
+<p>
+ The page you're looking for couldn't be found!
+</p>
diff --git a/ExamTemplate/Web/ExamTemplate.Web/Views/Shared/_FooterContent.cshtml b/ExamTemplate/Web/ExamTemplate.Web/Views/Shared/_FooterContent.cshtml
new file mode 100644
index 0000000..60a21aa
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/Views/Shared/_FooterContent.cshtml
@@ -0,0 +1,8 @@
+<div class="border-top box-shadow">
+ <div class="footer-content middle-content-container">
+ <section>
+ IT-kariera ExamTemplate - 2021
+ </section>
+ <div class="flex-spacer"></div>
+ </div>
+</div>
diff --git a/ExamTemplate/Web/ExamTemplate.Web/Views/Shared/_Layout.cshtml b/ExamTemplate/Web/ExamTemplate.Web/Views/Shared/_Layout.cshtml
new file mode 100644
index 0000000..dd7bf82
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/Views/Shared/_Layout.cshtml
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>@ViewData["Title"]</title>
+ <link rel="stylesheet" href="~/css/site.css" />
+ <link rel="stylesheet" href="~/css/styles.css" />
+</head>
+<body>
+ <header>
+ <partial name="_Navbar.cshtml" />
+ </header>
+
+ <main class="main">
+ <div class="middle-content-container">
+ @RenderBody()
+ </div>
+ </main>
+
+ <footer class="border-top box-shadow">
+ <partial name="_FooterContent.cshtml" />
+ </footer>
+</body>
+</html>
diff --git a/ExamTemplate/Web/ExamTemplate.Web/Views/Shared/_Navbar.cshtml b/ExamTemplate/Web/ExamTemplate.Web/Views/Shared/_Navbar.cshtml
new file mode 100644
index 0000000..0ec5c4d
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/Views/Shared/_Navbar.cshtml
@@ -0,0 +1,31 @@
+@using Microsoft.AspNetCore.Identity
+
+@inject SignInManager<User> SignInManager
+@inject UserManager<User> UserManager
+
+<nav class="navbar border-bottom box-shadow">
+ <div class="middle-content-container navbar-contents">
+ <section>
+ <b>ExamTemplate</b>
+ <a asp-controller="Home" asp-action="Index">Home</a>
+ </section>
+ <div class="flex-spacer"></div>
+ <section>
+ @if (SignInManager.IsSignedIn(User))
+ {
+ <a asp-controller="Account" asp-action="Profile" asp-route-username="@UserManager.GetUserName(User)">
+ @UserManager.GetUserName(User)
+ </a>
+
+ <form asp-controller="Account" asp-action="Logout" method="post">
+ <input type="submit" value="Logout">
+ </form>
+ }
+ else
+ {
+ <a asp-controller="Account" asp-action="Login">Login</a>
+ <a asp-controller="Account" asp-action="Register">Register</a>
+ }
+ </section>
+ </div>
+</nav>
diff --git a/ExamTemplate/Web/ExamTemplate.Web/Views/_ViewImports.cshtml b/ExamTemplate/Web/ExamTemplate.Web/Views/_ViewImports.cshtml
new file mode 100644
index 0000000..18502e4
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/Views/_ViewImports.cshtml
@@ -0,0 +1,5 @@
+@using ExamTemplate.Web
+@using ExamTemplate.Web.Models
+@using ExamTemplate.Web.Models.User
+@using ExamTemplate.Data.Models
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
diff --git a/ExamTemplate/Web/ExamTemplate.Web/Views/_ViewStart.cshtml b/ExamTemplate/Web/ExamTemplate.Web/Views/_ViewStart.cshtml
new file mode 100644
index 0000000..3a04d05
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/Views/_ViewStart.cshtml
@@ -0,0 +1,3 @@
+@{
+ Layout = "_Layout";
+}
diff --git a/ExamTemplate/Web/ExamTemplate.Web/appsettings.json b/ExamTemplate/Web/ExamTemplate.Web/appsettings.json
new file mode 100644
index 0000000..f1b58be
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/appsettings.json
@@ -0,0 +1,18 @@
+{
+ "ConnectionStrings": {
+ "LocalDBConnection": "Server=localhost;Port=5432;Database=TemplateContext;User Id=;Password=;"
+ },
+ "Cloud": {
+ "cloudName": "ExamTemplate",
+ "apiKey": "",
+ "apiSecret": ""
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/ExamTemplate/Web/ExamTemplate.Web/wwwroot/css/site.css b/ExamTemplate/Web/ExamTemplate.Web/wwwroot/css/site.css
new file mode 100644
index 0000000..5923427
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/wwwroot/css/site.css
@@ -0,0 +1,79 @@
+/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
+for details on configuring this project to bundle and minify static web assets. */
+
+a.navbar-brand {
+ white-space: normal;
+ text-align: center;
+ word-break: break-all;
+}
+
+/* Provide sufficient contrast against white background */
+a {
+ color: #0366d6;
+}
+
+.btn-primary {
+ color: #fff;
+ background-color: #1b6ec2;
+ border-color: #1861ac;
+}
+
+.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
+ color: #fff;
+ background-color: #1b6ec2;
+ border-color: #1861ac;
+}
+
+/* Sticky footer styles
+-------------------------------------------------- */
+html {
+ font-size: 14px;
+}
+@media (min-width: 768px) {
+ html {
+ font-size: 16px;
+ }
+}
+
+.border-top {
+ border-top: 1px solid #e5e5e5;
+}
+.border-bottom {
+ border-bottom: 1px solid #e5e5e5;
+}
+
+.box-shadow {
+ box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
+}
+
+button.accept-policy {
+ font-size: 1rem;
+ line-height: inherit;
+}
+
+/* Sticky footer styles
+-------------------------------------------------- */
+html {
+ position: relative;
+ min-height: 100%;
+ line-height: 1.15;
+}
+
+body {
+ min-height: 100vh;
+ margin: 0;
+ font-size: 1.15em;
+ background-color: white;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+}
+
+/* The following is kinda dirty, that's why it's separated */
+
+body {
+ display: flex;
+ flex-direction: column;
+}
+
+body > main {
+ flex: 1;
+}
diff --git a/ExamTemplate/Web/ExamTemplate.Web/wwwroot/css/styles.css b/ExamTemplate/Web/ExamTemplate.Web/wwwroot/css/styles.css
new file mode 100644
index 0000000..e7fc7b3
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/wwwroot/css/styles.css
@@ -0,0 +1,143 @@
+
+/* Change the maximum width of content (stuff inside pages and navbar),
+ * depending on width of browser window.
+ * Configuration copied from default Bootstrap
+ */
+
+@media (min-width: 576px) {
+ :root {
+ --max-content-width: 540px;
+ }
+}
+
+@media (min-width: 768px) {
+ :root {
+ --max-content-width: 720px;
+ }
+}
+
+@media (min-width: 992px) {
+ :root {
+ --max-content-width: 960px;
+ }
+}
+
+@media (min-width: 1200px) {
+ :root {
+ --max-content-width: 1140px;
+ }
+}
+
+/* Main */
+
+.main {
+ width: 100%;
+ height: 100%;
+}
+
+ /* Stuff that you need in the middle portion
+ * of the screen (like the stuff inside the
+ * navbar and footer) should be inside
+ * an element with this tag
+ */
+.middle-content-container {
+ max-width: var(--max-content-width);
+ margin-left: auto;
+ margin-right: auto;
+}
+
+/* Navbar and footer */
+
+.navbar, .footer-content {
+ width: 100%;
+ min-height: 45px;
+
+ padding-top: 8px;
+ padding-bottom: 8px;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.navbar section > :not(*:first-child) {
+ padding-left: 5px;
+}
+
+.navbar section > :not(*:last-child) {
+ padding-right: 5px;
+}
+
+.navbar a {
+ text-decoration: none;
+ color: #343a40;
+}
+
+.navbar a:hover {
+ color: black;
+}
+
+.navbar-contents {
+ width: 100%;
+ display: flex;
+ box-sizing: border-box;
+}
+
+.navbar-contents > * {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* Forms */
+
+form {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: fit-content;
+}
+
+.main > div > form {
+ margin-top: 10px;
+}
+
+.main > div > form:first-child {
+ width: 100%;
+ max-width: 300px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+form > * {
+ width: 100%;
+ box-sizing: border-box;
+}
+
+input {
+ margin: 5px;
+ padding: 9px;
+ border: 1px solid darkgrey;
+ border-radius: 4px;
+}
+
+input[type="submit"] {
+ color: white;
+ background-color: black;
+}
+
+input[type="submit"]:hover {
+ cursor: pointer;
+}
+
+.form-error {
+ font-size: 0.8em;
+ color: red;
+}
+
+/* Other general stuff */
+
+.flex-spacer {
+ flex: 1;
+}
diff --git a/ExamTemplate/Web/ExamTemplate.Web/wwwroot/favicon.ico b/ExamTemplate/Web/ExamTemplate.Web/wwwroot/favicon.ico
new file mode 100644
index 0000000..a3a7999
--- /dev/null
+++ b/ExamTemplate/Web/ExamTemplate.Web/wwwroot/favicon.ico
Binary files differ