1
0

Redone contacts, gallery, projects and resume pages. Added some admin panel pages

This commit is contained in:
Michael Gordeev
2019-12-11 15:29:40 +03:00
parent 53e95afa2d
commit 59337f17d9
45 changed files with 900 additions and 76 deletions
@@ -1,6 +1,8 @@
using System; using System;
using System.Linq;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MyWebsite.Models; using MyWebsite.Areas.API.Models;
namespace MyWebsite.Areas.API namespace MyWebsite.Areas.API
{ {
@@ -9,6 +11,10 @@ namespace MyWebsite.Areas.API
[ApiController] [ApiController]
public class FoxTubeController : ControllerBase public class FoxTubeController : ControllerBase
{ {
FoxTubeDatabaseContext db;
public FoxTubeController(FoxTubeDatabaseContext context) =>
db = context;
[HttpPost("AddMetrics")] [HttpPost("AddMetrics")]
public string AddMetrics() public string AddMetrics()
{ {
@@ -16,34 +22,88 @@ namespace MyWebsite.Areas.API
{ {
Title = Request.Form["Title"], Title = Request.Form["Title"],
Content = Request.Form["Content"], Content = Request.Form["Content"],
Version = Request.Form["Version"] Version = Request.Form["Version"],
TimeStamp = DateTime.Now
}; };
// TODO: Insert package to database db.Metrics.Add(metrics);
db.SaveChanges();
return metrics.Id.ToString(); return db.Metrics.FirstOrDefault(i => i.TimeStamp == metrics.TimeStamp)?.Id.ToString();
} }
[HttpGet("Messages")] [HttpGet("Messages")]
public IActionResult Messages(bool toast = false, DateTime? publishedAfter = null, string lang = "en-US") public object Messages(bool toast = false, DateTime? publishedAfter = null, string lang = "en-US")
{ {
if(toast) if(toast)
{ {
Message message = publishedAfter.HasValue ?
db.Messages.FirstOrDefault(i => i.PublishedAt >= publishedAfter && i.PublishedAt <= DateTime.Now && i.Language == lang) :
db.Messages.OrderByDescending(i => i.PublishedAt).FirstOrDefault();
} if (message == null)
return NoContent(); return NoContent();
return $@"<toast activationType='foreground' launch='inbox|{message.Id}'>
<visual>
<binding template='ToastGeneric'>
<image placement='hero' src='{message.HeroImage}'/>
<image placement='appLogoOverride' hint-crop='circle' src='{message.Icon}'/>
<text>{message.Title}</text>
<text hint-maxLines='5'>{message.Description}</text>
</binding>
</visual>
</toast>";
}
else
{
List<Message> messages = db.Messages.Where(i => i.PublishedAt <= DateTime.Now).ToList();
return messages;
}
} }
[HttpGet("Changelogs")] [HttpGet("Changelogs")]
public IActionResult Changelogs(bool toast = false, string version = null, string lang = "en-US") public object Changelogs(bool toast = false, string version = null, string lang = "en-US")
{ {
if (string.IsNullOrWhiteSpace(version))
throw new ArgumentNullException("You must specify required version number");
if(toast) if(toast)
{ {
Changelog changelog = db.Changelogs.FirstOrDefault(i => i.Version == version && i.Language == lang);
if (changelog == null)
return NoContent();
return $@"<toast activationType='foreground' launch='changelog|{changelog.Id}'>
<visual>
<binding template='ToastGeneric'>
<image placement='hero' src='{changelog.HeroImage}'/>
<image placement='appLogoOverride' hint-crop='circle' src='{changelog.Icon}'/>
<text>{changelog.Description}</text>
<text>{changelog.Title}</text>
</binding>
</visual>
</toast>";
}
else
{
List<Changelog> changelogs = db.Changelogs.Where(i => !IsVersionGreater(i.Version, version)).ToList();
return changelogs;
}
} }
return NoContent(); private static bool IsVersionGreater(string versionOne, string versionTwo)
{
string[] v1 = versionOne.Split('.');
string[] v2 = versionTwo.Split('.');
for (int i = 0; i < v1.Length && i < v2.Length; i++)
if (byte.Parse(v1[i]) > byte.Parse(v2[i]))
return true;
return false;
} }
} }
} }
@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
namespace MyWebsite.Areas.API.Models
{
public class Changelog : Message
{
[Required]
[Column(TypeName = "varchar(5)")]
public string Version { get; set; }
public Changelog()
{
Icon = "https://xfox111.net/projects-assets/FoxTube/ChangelogIcon.png";
HeroImage = "https://xfox111.net/projects-assets/FoxTube/ChangelogHero.png";
}
}
}
@@ -0,0 +1,16 @@
using Microsoft.EntityFrameworkCore;
namespace MyWebsite.Areas.API.Models
{
public class FoxTubeDatabaseContext : DbContext
{
public DbSet<MetricsPackage> Metrics { get; set; }
public DbSet<Message> Messages { get; set; }
public DbSet<Changelog> Changelogs { get; set; }
public FoxTubeDatabaseContext(DbContextOptions options) : base(options)
{
Database.EnsureCreated();
}
}
}
@@ -0,0 +1,42 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MyWebsite.Areas.API.Models
{
public class InboxItem
{
// Metadata
[Key]
[Required]
[Column(TypeName = "varchae(20)")]
public string Id { get; set; }
[Required]
public DateTime PublishedAt { get; set; } = DateTime.Now;
// Content
[Required]
[Column(TypeName = "varchar(255)")]
public string Title { get; set; }
[Column(TypeName = "varchar(255)")]
public string Description { get; set; }
[Required]
[Column(TypeName = "text")]
public string Content { get; set; }
// Media
[Column(TypeName = "varchar(255")]
public string Icon { get; set; }
[Column(TypeName = "varchar(255")]
public string HeroImage { get; set; }
[Column(TypeName = "varchar(255)")]
public string HtmlEmbed { get; set; }
}
}
@@ -0,0 +1,45 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MyWebsite.Areas.API.Models
{
public class Message
{
// Metadata
[Key]
[Column(TypeName = "varchar(50)")]
public string Id { get; set; }
[Required]
public DateTime PublishedAt { get; set; } = DateTime.Now;
[Required]
[Column(TypeName = "varchar(10)")]
public string Language { get; set; } = "en-US";
// Content
[Required]
[Column(TypeName = "varchar(50)")]
public string Title { get; set; }
[Column(TypeName = "varchar(100)")]
public string Description { get; set; }
[Required]
[Column(TypeName = "text")]
public string Content { get; set; }
// Media
[Column(TypeName = "varchar(255")]
public string Icon { get; set; } = "https://xfox111.net/projects-assets/FoxTube/DefaultIcon.png";
[Column(TypeName = "varchar(255")]
public string HeroImage { get; set; } = "https://xfox111.net/projects-assets/FoxTube/DefaultHero.png";
[Column(TypeName = "varchar(255)")]
public string HtmlEmbed { get; set; }
}
}
@@ -0,0 +1,28 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MyWebsite.Areas.API.Models
{
public class MetricsPackage
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
[Required]
[Column(TypeName = "varchar(100)")]
public string Title { get; set; }
[Required]
[Column(TypeName = "mediumtext")]
public string Content { get; set; }
[Required]
[Column(TypeName = "varchar(5)")]
public string Version { get; set; }
[Required]
public DateTime TimeStamp { get; set; }
}
}
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using MyWebsite.Models;
namespace MyWebsite.Areas.Admin.Controllers
{
[Area("Admin")]
public class ContactsController : Controller
{
public ContactsController(DatabaseContext context) =>
Startup.Database = context;
public IActionResult Index() =>
View(Startup.Database.Links);
[HttpGet]
public IActionResult Edit(string id) =>
View(Startup.Database.Links.Find(id));
[HttpPost]
public IActionResult Edit(Link model)
{
Startup.Database.Links.Update(model);
Startup.Database.SaveChanges();
return RedirectToAction("Index");
}
}
}
@@ -0,0 +1,45 @@
@using MyWebsite.Models;
@model Link;
@{
ViewData["Title"] = "Edit link";
}
<header>
<h1>Edit contact link</h1>
<p>&#xE760; <a asp-action="Index">Back to the list</a></p>
</header>
<article>
<form asp-action="Edit">
<div asp-validation-summary="ModelOnly" class="validation-summary-errors"></div>
<div>
<label asp-for="Name"></label>
<input asp-for="Name" />
<span asp-validation-for="Name"></span>
</div>
<div>
<label asp-for="Title"></label>
<input asp-for="Title" />
<span asp-validation-for="Title"></span>
</div>
<div>
<label asp-for="Username"></label>
<input asp-for="Username" />
<span asp-validation-for="Username"></span>
</div>
<div>
<label asp-for="Url"></label>
<input asp-for="Url" />
<span asp-validation-for="Url"></span>
</div>
<div>
<label asp-for="CanContactMe">@Html.DisplayNameFor(model => model.CanContactMe)</label>
<input style="width:auto" type="checkbox" asp-for="CanContactMe" />
</div>
<div class="form-group">
<input type="submit" value="Save"/>
</div>
</form>
</article>
<link type="text/css" rel="stylesheet" href="~/css/Admin.css" />
@@ -0,0 +1,55 @@
@model IEnumerable<MyWebsite.Models.Link>
@{
ViewData["Title"] = "Contacts - Admin panel";
}
<header>
<h1>Contact links list</h1>
<p>&#xE760; <a asp-action="Index" asp-controller="Admin" asp-area="">Back to admin panel</a></p>
</header>
<article>
<p>
<a class="comment" asp-action="Create">// + Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Title</th>
<th>Username</th>
<th>URL</th>
<th>Contact link</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.OrderByDescending(i => i.CanContactMe))
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Username)
</td>
<td>
@Html.DisplayFor(modelItem => item.Url)
</td>
<td>
@Html.DisplayFor(modelItem => item.CanContactMe)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id = item.Name }) |
@Html.ActionLink("Delete", "Delete", new { id = item.Name })
</td>
</tr>
}
</tbody>
</table>
</article>
<link type="text/css" rel="stylesheet" href="~/css/Admin.css" />
@@ -0,0 +1,3 @@
@using MyWebsite
@using MyWebsite.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}
@@ -0,0 +1,59 @@
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MyWebsite.Helpers;
using MyWebsite.Models;
namespace MyWebsite.Controllers
{
[Authorize]
public class AdminController : Controller
{
public AdminController(DatabaseContext context) =>
Startup.Database = context;
public IActionResult Index() =>
View();
[AllowAnonymous]
[HttpGet]
public IActionResult Login() =>
View();
[AllowAnonymous]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginModel model)
{
if (!ModelState.IsValid)
{
ModelState.AddModelError("Authorization error", "Invalid data");
return View(model);
}
LoginModel user = Startup.Database.Users.FirstOrDefault(i => i.Email == model.Email);
if (user == null || !Cryptography.VerifyHash(model.Password, "SHA512", user.Password))
{
ModelState.AddModelError("Authorization error", "Invaild e-mail or password");
return View(model);
}
Claim claim = new Claim(ClaimsIdentity.DefaultNameClaimType, user.Email);
ClaimsIdentity id = new ClaimsIdentity(new Claim[] { claim }, "ApplicationCookie", ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(id));
return RedirectToAction("Index", "Admin");
}
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return RedirectToAction("Login", "Admin");
}
}
}
@@ -1,6 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MyWebsite.Models; using MyWebsite.Models;
using System.Threading.Tasks;
using System.Linq; using System.Linq;
namespace MyWebsite.Controllers namespace MyWebsite.Controllers
@@ -13,7 +12,7 @@ namespace MyWebsite.Controllers
public IActionResult Index() => public IActionResult Index() =>
View(); View();
public async Task<IActionResult> Details(string id) => public IActionResult Details(string id) =>
View(Startup.Database.Gallery.FirstOrDefault(i => i.FileName == id)); View(Startup.Database.Gallery.FirstOrDefault(i => i.FileName == id));
} }
} }
@@ -1,8 +1,6 @@
using System.Collections.Generic; using System.Diagnostics;
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MyWebsite.Models; using MyWebsite.Models;
using System.Linq;
namespace MyWebsite.Controllers namespace MyWebsite.Controllers
{ {
@@ -18,6 +16,10 @@ namespace MyWebsite.Controllers
public IActionResult Contacts() => public IActionResult Contacts() =>
View(); View();
[Route("Projects")]
public IActionResult Projects() =>
View();
[Route("Error")] [Route("Error")]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error() => public IActionResult Error() =>
@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Mvc;
using MyWebsite.Models;
using SelectPdf;
namespace MyWebsite.Controllers
{
public class ResumeController : Controller
{
public ResumeController(DatabaseContext context) =>
Startup.Database = context;
public IActionResult Index() =>
View(Startup.Database.Resume.Find("en-US"));
public IActionResult Print() =>
View(Startup.Database.Resume.Find("en-US"));
public IActionResult Download()
{
HtmlToPdf converter = new HtmlToPdf();
converter.Options.MarginTop = 25;
converter.Options.MarginBottom = 25;
converter.Options.MarginLeft = 25;
converter.Options.MarginRight = 25;
PdfDocument doc = converter.ConvertHtmlString(
$@"<html style=""margin-top: -50px"">
{Startup.Database.Resume.Find("en-US").Content}
<link rel=""stylesheet"" type=""text/css"" href=""{Request.Scheme}://{Request.Host}/css/Style.css"" />
</html>");
byte[] data = doc.Save();
doc.Close();
return File(data, "application/pdf", "[Michael Gordeev] CV.pdf");
}
}
}
+144
View File
@@ -0,0 +1,144 @@
using System;
using System.Security.Cryptography;
using System.Text;
namespace MyWebsite.Helpers
{
public class Cryptography
{
public static string ComputeHash(string plainText, string hashAlgorithm, byte[] saltBytes)
{
// If salt is not specified, generate it.
if (saltBytes == null)
{
// Define min and max salt sizes.
int minSaltSize = 4;
int maxSaltSize = 8;
// Generate a random number for the size of the salt.
Random random = new Random();
int saltSize = random.Next(minSaltSize, maxSaltSize);
// Allocate a byte array, which will hold the salt.
saltBytes = new byte[saltSize];
// Initialize a random number generator.
RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
// Fill the salt with cryptographically strong byte values.
rng.GetNonZeroBytes(saltBytes);
}
// Convert plain text into a byte array.
byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);
// Allocate array, which will hold plain text and salt.
byte[] plainTextWithSaltBytes =
new byte[plainTextBytes.Length + saltBytes.Length];
// Copy plain text bytes into resulting array.
for (int i = 0; i < plainTextBytes.Length; i++)
plainTextWithSaltBytes[i] = plainTextBytes[i];
// Append salt bytes to the resulting array.
for (int i = 0; i < saltBytes.Length; i++)
plainTextWithSaltBytes[plainTextBytes.Length + i] = saltBytes[i];
HashAlgorithm hash;
// Make sure hashing algorithm name is specified.
if (hashAlgorithm == null)
hashAlgorithm = "";
// Initialize appropriate hashing algorithm class.
switch (hashAlgorithm.ToUpper())
{
case "SHA384":
hash = new SHA384Managed();
break;
case "SHA512":
hash = new SHA512Managed();
break;
default:
hash = new MD5CryptoServiceProvider();
break;
}
// Compute hash value of our plain text with appended salt.
byte[] hashBytes = hash.ComputeHash(plainTextWithSaltBytes);
// Create array which will hold hash and original salt bytes.
byte[] hashWithSaltBytes = new byte[hashBytes.Length +
saltBytes.Length];
// Copy hash bytes into resulting array.
for (int i = 0; i < hashBytes.Length; i++)
hashWithSaltBytes[i] = hashBytes[i];
// Append salt bytes to the result.
for (int i = 0; i < saltBytes.Length; i++)
hashWithSaltBytes[hashBytes.Length + i] = saltBytes[i];
// Convert result into a base64-encoded string.
string hashValue = Convert.ToBase64String(hashWithSaltBytes);
// Return the result.
return hashValue;
}
public static bool VerifyHash(string plainText, string hashAlgorithm, string hashValue)
{
// Convert base64-encoded hash value into a byte array.
byte[] hashWithSaltBytes = Convert.FromBase64String(hashValue);
// We must know size of hash (without salt).
int hashSizeInBits, hashSizeInBytes;
// Make sure that hashing algorithm name is specified.
if (hashAlgorithm == null)
hashAlgorithm = "";
// Size of hash is based on the specified algorithm.
switch (hashAlgorithm.ToUpper())
{
case "SHA384":
hashSizeInBits = 384;
break;
case "SHA512":
hashSizeInBits = 512;
break;
default: // Must be MD5
hashSizeInBits = 128;
break;
}
// Convert size of hash from bits to bytes.
hashSizeInBytes = hashSizeInBits / 8;
// Make sure that the specified hash value is long enough.
if (hashWithSaltBytes.Length < hashSizeInBytes)
return false;
// Allocate array to hold original salt bytes retrieved from hash.
byte[] saltBytes = new byte[hashWithSaltBytes.Length - hashSizeInBytes];
// Copy salt from the end of the hash to the new array.
for (int i = 0; i < saltBytes.Length; i++)
saltBytes[i] = hashWithSaltBytes[hashSizeInBytes + i];
// Compute a new hash string.
string expectedHashString = ComputeHash(plainText, hashAlgorithm, saltBytes);
// If the computed hash matches the specified hash,
// the plain text value must be correct.
return (hashValue == expectedHashString);
}
}
}
+18
View File
@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MyWebsite.Models
{
public class Badge
{
[Key]
[Column(TypeName = "varchar(10)")]
public string Name { get; set; }
[Column(TypeName = "varchar(100)")]
[Required]
public string Description { get; set; }
[Column(TypeName = "varchar(20)")]
[Required]
public string Image { get; set; }
}
}
@@ -7,6 +7,9 @@ namespace MyWebsite.Models
public DbSet<Link> Links { get; set; } public DbSet<Link> Links { get; set; }
public DbSet<Image> Gallery { get; set; } public DbSet<Image> Gallery { get; set; }
public DbSet<Project> Projects { get; set; } public DbSet<Project> Projects { get; set; }
public DbSet<LoginModel> Users { get; set; }
public DbSet<Badge> Badges { get; set; }
public DbSet<Resume> Resume { get; set; }
public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options) public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options)
{ {
+16 -3
View File
@@ -1,14 +1,27 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MyWebsite.Models namespace MyWebsite.Models
{ {
public class Image public class Image
{ {
public string Title { get; set; }
public string Description { get; set; }
public DateTime CreationDate { get; set; }
[Key] [Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
[Required]
[Column(TypeName = "varchar(100)")]
public string Title { get; set; }
[Required]
[Column(TypeName = "varchar(255)")]
public string Description { get; set; }
[Required]
public DateTime CreationDate { get; set; }
[Required]
[Column(TypeName = "varchar(20)")]
public string FileName { get; set; } public string FileName { get; set; }
[Required]
[Column(TypeName = "varchar(10)")]
public string Language { get; set; }
} }
} }
+14 -1
View File
@@ -1,14 +1,27 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MyWebsite.Models namespace MyWebsite.Models
{ {
public class Link public class Link
{ {
[Key] [Key]
public int Id { get; set; }
[Required]
[Column(TypeName = "varchar(20)")]
public string Name { get; set; } public string Name { get; set; }
[Required]
[Column(TypeName = "varchar(20)")]
public string Title { get; set; } public string Title { get; set; }
[Required]
[Column(TypeName = "varchar(50)")]
public string Username { get; set; } public string Username { get; set; }
[Required]
[Column(TypeName = "varchar(255)")]
public string Url { get; set; } public string Url { get; set; }
public bool CanContactMe { get; set; } [Required]
public bool CanContactMe { get; set; } = false;
[Required]
public bool DisplayInFooter { get; set; } = false;
} }
} }
+16
View File
@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
namespace MyWebsite.Models
{
public class LoginModel
{
[Key]
[Required(ErrorMessage = "This field is required")]
[DataType(DataType.EmailAddress)]
public string Email { get; set; }
[Required(ErrorMessage = "This field is required")]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}
@@ -1,12 +0,0 @@
using System;
namespace MyWebsite.Models
{
public class MetricsPackage
{
public Guid Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public string Version { get; set; }
}
}
+12
View File
@@ -1,17 +1,29 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MyWebsite.Models namespace MyWebsite.Models
{ {
public class Project public class Project
{ {
[Key] [Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; } public Guid Id { get; set; }
[Required]
[Column(TypeName = "varchar(100)")]
public string Title { get; set; } public string Title { get; set; }
[Column(TypeName = "text")]
public string Description { get; set; } public string Description { get; set; }
[Column(TypeName = "varchar(50)")]
public string ImageName { get; set; } public string ImageName { get; set; }
[Column(TypeName = "varchar(50)")]
public string Link { get; set; } public string Link { get; set; }
[Column(TypeName = "varchar(50)")]
public string LinkCaption { get; set; } public string LinkCaption { get; set; }
[Column(TypeName = "varchar(100)")]
public string Badges { get; set; } public string Badges { get; set; }
} }
} }
+15
View File
@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MyWebsite.Models
{
public class Resume
{
[Key]
[Column(TypeName = "varchar(10)")]
public string Language { get; set; }
[Required]
[Column(TypeName = "text")]
public string Content { get; set; }
}
}
+10
View File
@@ -4,11 +4,21 @@
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<None Remove="Areas\Admin\NewFile.txt" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" /> <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
<PackageReference Include="Select.HtmlToPdf.NetCore" Version="19.2.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Areas\Admin\Views\Shared\" />
<Folder Include="wwwroot\projects-assets\FoxTube\" />
</ItemGroup> </ItemGroup>
+2 -1
View File
@@ -7,7 +7,8 @@
<WebStackScaffolding_IsLayoutPageSelected>True</WebStackScaffolding_IsLayoutPageSelected> <WebStackScaffolding_IsLayoutPageSelected>True</WebStackScaffolding_IsLayoutPageSelected>
<WebStackScaffolding_IsPartialViewSelected>False</WebStackScaffolding_IsPartialViewSelected> <WebStackScaffolding_IsPartialViewSelected>False</WebStackScaffolding_IsPartialViewSelected>
<WebStackScaffolding_IsReferencingScriptLibrariesSelected>False</WebStackScaffolding_IsReferencingScriptLibrariesSelected> <WebStackScaffolding_IsReferencingScriptLibrariesSelected>False</WebStackScaffolding_IsReferencingScriptLibrariesSelected>
<WebStackScaffolding_LayoutPageFile /> <WebStackScaffolding_LayoutPageFile>
</WebStackScaffolding_LayoutPageFile>
<WebStackScaffolding_IsAsyncSelected>False</WebStackScaffolding_IsAsyncSelected> <WebStackScaffolding_IsAsyncSelected>False</WebStackScaffolding_IsAsyncSelected>
<WebStackScaffolding_ViewDialogWidth>600</WebStackScaffolding_ViewDialogWidth> <WebStackScaffolding_ViewDialogWidth>600</WebStackScaffolding_ViewDialogWidth>
</PropertyGroup> </PropertyGroup>
+15 -2
View File
@@ -5,6 +5,9 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using MyWebsite.Models; using MyWebsite.Models;
using MyWebsite.Areas.API.Models;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
namespace MyWebsite namespace MyWebsite
{ {
@@ -22,9 +25,14 @@ namespace MyWebsite
// This method gets called by the runtime. Use this method to add services to the container. // This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
string connection = Configuration.GetConnectionString("DefaultConnection");
services.AddDbContext<DatabaseContext>(options => services.AddDbContext<DatabaseContext>(options =>
options.UseSqlServer(connection)); options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddDbContext<FoxTubeDatabaseContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("FoxTubeAPI")));
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
options.LoginPath = new PathString("/Admin/Login"));
services.AddControllersWithViews(); services.AddControllersWithViews();
} }
@@ -45,6 +53,7 @@ namespace MyWebsite
app.UseRouting(); app.UseRouting();
app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseEndpoints(endpoints => app.UseEndpoints(endpoints =>
@@ -53,6 +62,10 @@ namespace MyWebsite
name: "projects", name: "projects",
areaName: "Projects", areaName: "Projects",
pattern: "Projects/{controller=Projects}/{action=Index}/{id?}"); pattern: "Projects/{controller=Projects}/{action=Index}/{id?}");
endpoints.MapAreaControllerRoute(
name: "admin",
areaName: "Admin",
pattern: "Admin/{controller}/{action=Index}/{id?}");
endpoints.MapAreaControllerRoute( endpoints.MapAreaControllerRoute(
name: "api", name: "api",
areaName: "API", areaName: "API",
@@ -0,0 +1,23 @@
@{
ViewData["Title"] = "Admin panel";
}
<header>
<h1>Administration</h1>
</header>
<article>
<p class="admin-menu">
<a asp-action="Artworks" class="comment">// Artworks</a><br />
<a asp-action="Projects" class="comment">// Projects</a><br />
<a asp-action="Badges" class="comment">// Badges</a><br />
<a asp-action="Resume" class="comment">// Resume</a><br />
<a asp-action="Contacts" class="comment">// Contact links</a><br />
</p>
<p>
<a asp-action="Logout" class="logout">&#xE875; Logout</a>
</p>
</article>
<link type="text/css" rel="stylesheet" href="~/css/Admin.css" />
@@ -0,0 +1,27 @@
@model LoginModel;
@{
ViewData["Title"] = "Login";
}
<header>
<h1>Administration panel - Login</h1>
</header>
<article>
<form asp-action="Login" asp-antiforgery="true">
<div asp-validation-summary="All" class="validation-error"></div>
<div>
<label asp-for="Email">E-mail</label>
<input type="email" asp-for="Email" />
<span asp-validation-for="Email"></span>
</div>
<div>
<label asp-for="Password">Password</label>
<input type="password" asp-for="Password" />
<span asp-validation-for="Password"></span>
</div>
<input style="margin-top:10px" type="submit" value="Login" />
</form>
</article>
<link type="text/css" rel="stylesheet" href="~/css/Admin.css" />
@@ -1,4 +1,4 @@
@model Image; @model Image
@{ @{
ViewData["Title"] = Model.Title; ViewData["Title"] = Model.Title;
} }
@@ -13,7 +13,9 @@
<div> <div>
<h1>@Model.Title</h1> <h1>@Model.Title</h1>
<span>Creation date: @Model.CreationDate.ToShortDateString()</span> <span>Creation date: @Model.CreationDate.ToShortDateString()</span>
<p>@Model.Description</p> <p>
@Html.Raw(Model.Description)
</p>
</div> </div>
</article> </article>
@@ -6,9 +6,9 @@
<h1>My arts</h1> <h1>My arts</h1>
</header> </header>
<article class="info-block gallery"> <article class="gallery">
@if (Startup.Database.Gallery.Count() > 0) @if (Startup.Database.Gallery.Count() > 0)
foreach (Image image in Startup.Database.Gallery) foreach (Image image in Startup.Database.Gallery.OrderByDescending(i => i.CreationDate))
{ {
<a asp-action="Details" asp-route-id="@image.FileName"><img title="@image.Title" src="~/images/Gallery/@image.FileName" /></a> <a asp-action="Details" asp-route-id="@image.FileName"><img title="@image.Title" src="~/images/Gallery/@image.FileName" /></a>
} }
@@ -0,0 +1,17 @@
@model Resume
@{
ViewData["Title"] = "My resume";
}
<header>
<h1>My resume</h1>
<a class="comment" asp-action="Download">// Download CV (.pdf) &#xE118;</a><br />
<a class="comment" asp-action="Print">// Print CV &#xE749;</a>
</header>
<article>
@Html.Raw(Model.Content)
</article>
<partial name="~/Views/Shared/ContactsBlock.cshtml" />
@@ -0,0 +1,23 @@
@model Resume
@{
Layout = null;
ViewData["Title"] = "Print";
}
@Html.Raw(Model.Content)
<link rel="stylesheet" type="text/css" href="~/css/Style.css" />
<style>
body {
margin-top: 0px;
}
</style>
<script type="text/javascript">
window.print();
window.onfocus = function ()
{
window.location = "/Resume";
}
</script>
@@ -8,13 +8,13 @@
<article> <article>
<p> <p>
@foreach (Link link in Startup.Database.Links.Where(i => i.CanContactMe)) @foreach (Link link in Startup.Database.Links.Where(i => i.CanContactMe).OrderBy(i => i.Id))
{ {
<a class="socicon-@(link.Name)"></a> @(link.Title) <a href="@(link.Url)" target="_blank">@(link.Username)</a><br /> <a class="socicon-@(link.Name)"></a> @(link.Title) <a href="@(link.Url)" target="_blank">@(link.Username)</a><br />
} }
</p> </p>
<p> <p>
@foreach (Link link in Startup.Database.Links.Where(i => !i.CanContactMe)) @foreach (Link link in Startup.Database.Links.Where(i => !i.CanContactMe).OrderBy(i => i.Id))
{ {
<a class="socicon-@(link.Name)"></a> @(link.Title) <a href="@(link.Url)" target="_blank">@(link.Username)</a><br /> <a class="socicon-@(link.Name)"></a> @(link.Title) <a href="@(link.Url)" target="_blank">@(link.Username)</a><br />
} }
+9 -16
View File
@@ -3,23 +3,16 @@
ViewData["Title"] = "Error"; ViewData["Title"] = "Error";
} }
<h1 class="text-danger">Error.</h1> <header>
<h2 class="text-danger">An error occurred while processing your request.</h2> <h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
</header>
@if (Model.ShowRequestId) <article>
{ @if (Model.ShowRequestId)
{
<p> <p>
<strong>Request ID:</strong> <code>@Model.RequestId</code> <strong>Request ID:</strong> <code>@Model.RequestId</code>
</p> </p>
} }
</article>
<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>
@@ -0,0 +1,35 @@
@{
ViewData["Title"] = "My projects";
List<Badge> badges = Startup.Database.Badges.ToList();
}
<header>
<div>
<h1>My projects</h1>
<h3>Here is presented the most of projects I worked on</h3>
</div>
<iframe src="//githubbadge.appspot.com/xfox111" class="github-stats" frameborder="0"></iframe>
</header>
<article>
@foreach (Project project in Startup.Database.Projects)
{
<div class="project-item">
<div>
<h1>@project.Title</h1>
<p class="description">
@Html.Raw(project.Description)
</p>
<a href="@(project.Link)" target="_blank">@project.LinkCaption</a>
</div>
<div>
@foreach (string badge in project.Badges.Split(','))
{
<div class="badge" style="background-image: url(../images/Badges/@(badges.FirstOrDefault(i => i.Name == badge).Image).png)" title="@(badges.FirstOrDefault(i => i.Name == badge).Description)"></div>
}
</div>
</div>
}
</article>
<link rel="stylesheet" type="text/css" href="~/css/Projects.css" />
@@ -29,7 +29,7 @@
<menu type="toolbar" class="main-menu" style="display:none"> <menu type="toolbar" class="main-menu" style="display:none">
<li><a asp-controller="About" asp-action="Index">AboutMe();</a></li> <li><a asp-controller="About" asp-action="Index">AboutMe();</a></li>
<li><a asp-controller="CV" asp-action="Index">CV();</a></li> <li><a asp-controller="Resume" asp-action="Index">CV();</a></li>
<li><a asp-controller="Projects" asp-action="Index">Projects();</a></li> <li><a asp-controller="Projects" asp-action="Index">Projects();</a></li>
<li><a asp-controller="Gallery" asp-action="Index">Arts();</a></li> <li><a asp-controller="Gallery" asp-action="Index">Arts();</a></li>
<li><a asp-controller="Contacts" asp-action="Index">Contacts();</a></li> <li><a asp-controller="Contacts" asp-action="Index">Contacts();</a></li>
@@ -44,9 +44,9 @@
</main> </main>
<footer> <footer>
<span class="comment">// Copyright &copy;@(DateTime.Today.Year) <b>Michael "XFox" Gordeev</b></span> <span class="comment">// Copyright &copy;@(DateTime.Today.Year) Michael "XFox" Gordeev</span>
@foreach (Link link in from i in Startup.Database.Links where new string[] { "outlook", "linkedin", "github" }.Contains(i.Name) orderby i.Name descending select i) @foreach (Link link in Startup.Database.Links.Where(i => i.DisplayInFooter).OrderBy(i => i.Id))
{ {
<a class="socicon-@(link.Name)" href="@(link.Url)" target="_blank" title="@(link.Title)"></a> <a class="socicon-@(link.Name)" href="@(link.Url)" target="_blank" title="@(link.Title)"></a>
} }
@@ -1,2 +0,0 @@
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
+2 -1
View File
@@ -8,6 +8,7 @@
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=xfox111;Trusted_Connection=True;" "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=xfox111;Trusted_Connection=True;",
"FoxTubeAPI": "Server=(localdb)\\mssqllocaldb;Database=foxtube;Trusted_Connection=True;"
} }
} }
+21
View File
@@ -4,6 +4,7 @@
border: 1px solid black; border: 1px solid black;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
margin-bottom: 10px;
} }
select { select {
@@ -55,6 +56,26 @@ form {
max-width: 50%; max-width: 50%;
} }
.admin-menu a {
font-size: 16pt;
}
.logout, .logout:link, .logout:link:visited {
font-size: 16pt;
color: red !important;
}
.validation-summary-errors {
color: red;
}
th {
text-align: left;
}
table {
width: 100%;
}
@media only screen and (max-width: 700px) { @media only screen and (max-width: 700px) {
form { form {
max-width: initial; max-width: initial;
@@ -34,16 +34,6 @@ article {
background-size: contain; background-size: contain;
} }
.csharp { background-image: url(../images/Badges/csharp.png); }
.dotnet { background-image: url("../images/Badges/dotnet.png"); }
.xamarin { background-image: url("../images/Badges/xamarin.png"); }
.unity { background-image: url("../images/Badges/unity.png"); }
.android { background-image: url("../images/Badges/android.png"); }
.uwp { background-image: url("../images/Badges/windows.png"); }
.win32 { background-image: url("../images/Badges/windows.png"); }
.windows { background-image: url("../images/Badges/windows.png"); }
.ios { background-image: url("../images/Badges/ios.png"); }
@media only screen and (max-width: 600px) { @media only screen and (max-width: 600px) {
.github-stats { .github-stats {
grid-row: 2; grid-row: 2;
+7 -2
View File
@@ -72,10 +72,15 @@ footer a:hover {
} }
/*Body styles*/ /*Body styles*/
html {
overflow: hidden;
}
body { body {
overflow: auto;
margin: 0px; margin: 0px;
margin-top: 53px; margin-top: 53px;
font-family: Consolas, 'Segoe MDL2 Assets'; font-family: 'Consolas', 'Segoe MDL2 Assets';
/*This stuff is necessary for sticky footer*/ /*This stuff is necessary for sticky footer*/
display: grid; display: grid;
@@ -100,7 +105,7 @@ article a:visited, article a:link {
} }
.comment, .comment:visited { .comment, .comment:visited {
color: #57a64a; color: #57a64a !important;
} }
/*Adaptive code*/ /*Adaptive code*/
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB