Add password hashing, user settings with 2FA, and project image lightbox.

Hardens auth with PBKDF2, lockouts, local QR setup, and safer embeds while moving account security under the username menu.
This commit is contained in:
Atakan Doğan Özban
2026-07-17 18:49:34 +02:00
parent 8ec6e3fd50
commit e97a71e5b2
18 changed files with 1253 additions and 62 deletions
+199 -3
View File
@@ -13,7 +13,7 @@ namespace atakanozbancom.Controllers
private readonly Context db = new Context(); private readonly Context db = new Context();
[Authorize] [Authorize]
public ActionResult Index() public ActionResult Index() // admin dashboard landing page
{ {
ViewBag.MyProjectsCount = db.MyProjects.Count(); ViewBag.MyProjectsCount = db.MyProjects.Count();
ViewBag.AffiliateLinksCount = db.AffiliateLinks.Count(); ViewBag.AffiliateLinksCount = db.AffiliateLinks.Count();
@@ -22,7 +22,7 @@ namespace atakanozbancom.Controllers
} }
[Authorize] [Authorize]
public ActionResult MyProjects() public ActionResult MyProjects() // shows what we have
{ {
var value = db.MyProjects.ToList(); var value = db.MyProjects.ToList();
return View(value); return View(value);
@@ -53,6 +53,7 @@ namespace atakanozbancom.Controllers
DescTR = tr != null ? tr.description : "" DescTR = tr != null ? tr.description : ""
}; };
// NEW:
vm.medias = db.projectmedia vm.medias = db.projectmedia
.Where(m => m.project_id == p.id) .Where(m => m.project_id == p.id)
.OrderBy(m => m.sort_order) .OrderBy(m => m.sort_order)
@@ -183,11 +184,19 @@ namespace atakanozbancom.Controllers
return RedirectToAction("myprojectsget", new { id = projectId }); return RedirectToAction("myprojectsget", new { id = projectId });
} }
var trimmed = iframeUrl.Trim();
if (!Uri.TryCreate(trimmed, UriKind.Absolute, out var embedUri)
|| (embedUri.Scheme != Uri.UriSchemeHttps && embedUri.Scheme != Uri.UriSchemeHttp))
{
TempData["ok"] = "Iframe URL must be a valid http(s) address.";
return RedirectToAction("myprojectsget", new { id = projectId });
}
var row = new projectmedia var row = new projectmedia
{ {
project_id = projectId, project_id = projectId,
media_type = MediaType.Iframe, media_type = MediaType.Iframe,
url = iframeUrl.Trim(), url = trimmed,
sort_order = sortOrder sort_order = sortOrder
}; };
@@ -295,6 +304,9 @@ namespace atakanozbancom.Controllers
return RedirectToAction("myprojectsget", new { id = projectId }); return RedirectToAction("myprojectsget", new { id = projectId });
} }
// ================= AFFILIATE LINKS =================
// .svg intentionally excluded: SVGs can embed <script> and execute when opened directly.
private static readonly string[] AllowedLogoExtensions = { ".jpg", ".jpeg", ".png", ".webp", ".gif" }; private static readonly string[] AllowedLogoExtensions = { ".jpg", ".jpeg", ".png", ".webp", ".gif" };
private const string AffiliateLogosPrefix = "/Content/uploads/affiliate-logos/"; private const string AffiliateLogosPrefix = "/Content/uploads/affiliate-logos/";
@@ -328,6 +340,12 @@ namespace atakanozbancom.Controllers
if (!ModelState.IsValid) if (!ModelState.IsValid)
return View(vm); return View(vm);
if (!IsSafeHttpUrl(vm.url))
{
ModelState.AddModelError("url", "URL must be a valid http(s) address.");
return View(vm);
}
var logoUrl = SaveAffiliateLogo(vm.logoFile); var logoUrl = SaveAffiliateLogo(vm.logoFile);
if (logoUrl == null) if (logoUrl == null)
{ {
@@ -402,6 +420,13 @@ namespace atakanozbancom.Controllers
return View(vm); return View(vm);
} }
if (!IsSafeHttpUrl(vm.url))
{
ModelState.AddModelError("url", "URL must be a valid http(s) address.");
vm.existingImage = a.image;
return View(vm);
}
if (vm.logoFile != null && vm.logoFile.ContentLength > 0) if (vm.logoFile != null && vm.logoFile.ContentLength > 0)
{ {
var logoUrl = SaveAffiliateLogo(vm.logoFile); var logoUrl = SaveAffiliateLogo(vm.logoFile);
@@ -486,9 +511,12 @@ namespace atakanozbancom.Controllers
} }
catch catch
{ {
// silently ignore, matches existing media-delete behavior
} }
} }
// ================= SOCIAL ICONS (footer) =================
[Authorize] [Authorize]
public ActionResult SocialIcons() public ActionResult SocialIcons()
{ {
@@ -514,6 +542,12 @@ namespace atakanozbancom.Controllers
if (!ModelState.IsValid) if (!ModelState.IsValid)
return View(vm); return View(vm);
if (!IsSafeHttpUrl(vm.url))
{
ModelState.AddModelError("url", "URL must be a valid http(s) address.");
return View(vm);
}
var icon = new socialicon var icon = new socialicon
{ {
icon = vm.icon, icon = vm.icon,
@@ -557,6 +591,12 @@ namespace atakanozbancom.Controllers
if (!ModelState.IsValid) if (!ModelState.IsValid)
return View(vm); return View(vm);
if (!IsSafeHttpUrl(vm.url))
{
ModelState.AddModelError("url", "URL must be a valid http(s) address.");
return View(vm);
}
s.icon = vm.icon; s.icon = vm.icon;
s.url = vm.url; s.url = vm.url;
s.sort_order = vm.sort_order; s.sort_order = vm.sort_order;
@@ -580,6 +620,8 @@ namespace atakanozbancom.Controllers
return RedirectToAction("SocialIcons"); return RedirectToAction("SocialIcons");
} }
// ================= WALLPAPER (site background) =================
private static readonly string[] AllowedWallpaperExtensions = { ".jpg", ".jpeg", ".png", ".webp", ".gif" }; private static readonly string[] AllowedWallpaperExtensions = { ".jpg", ".jpeg", ".png", ".webp", ".gif" };
private const string WallpaperPrefix = "/Content/uploads/wallpaper/"; private const string WallpaperPrefix = "/Content/uploads/wallpaper/";
private const string DefaultWallpaperUrl = "/web_atakanozbancom/assets/img/wp.jpg"; private const string DefaultWallpaperUrl = "/web_atakanozbancom/assets/img/wp.jpg";
@@ -672,5 +714,159 @@ namespace atakanozbancom.Controllers
} }
} }
[Authorize]
public ActionResult UserSettings()
{
var user = GetCurrentAdmin();
if (user == null)
return RedirectToAction("Index", "login");
ViewBag.TwoFactorEnabled = user.two_factor_enabled;
ViewBag.Username = user.username;
if (!user.two_factor_enabled)
{
var secret = Session["Pending2FaSetupSecret"] as string;
if (string.IsNullOrWhiteSpace(secret))
{
secret = TotpHelper.GenerateSecret();
Session["Pending2FaSetupSecret"] = secret;
}
ViewBag.SetupSecret = secret;
ViewBag.OtpAuthUri = TotpHelper.BuildOtpAuthUri("atakanozban.com", user.username, secret);
}
return View();
}
[Authorize]
public ActionResult Security()
{
return RedirectToAction("UserSettings");
}
[Authorize]
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult ChangePassword(string currentPassword, string newPassword, string confirmPassword)
{
var user = GetCurrentAdmin();
if (user == null)
return RedirectToAction("Index", "login");
if (string.IsNullOrWhiteSpace(currentPassword)
|| string.IsNullOrWhiteSpace(newPassword)
|| string.IsNullOrWhiteSpace(confirmPassword))
{
TempData["pwdError"] = "All password fields are required.";
return RedirectToAction("UserSettings");
}
if (!PasswordHasher.Verify(currentPassword, user.password))
{
TempData["pwdError"] = "Current password is incorrect.";
return RedirectToAction("UserSettings");
}
if (newPassword.Length < 8)
{
TempData["pwdError"] = "New password must be at least 8 characters.";
return RedirectToAction("UserSettings");
}
if (!string.Equals(newPassword, confirmPassword, StringComparison.Ordinal))
{
TempData["pwdError"] = "New password and confirmation do not match.";
return RedirectToAction("UserSettings");
}
user.password = PasswordHasher.Hash(newPassword);
db.SaveChanges();
TempData["pwdOk"] = "Password updated successfully.";
return RedirectToAction("UserSettings");
}
[Authorize]
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult EnableTwoFactor(string code)
{
var user = GetCurrentAdmin();
if (user == null)
return RedirectToAction("Index", "login");
var secret = Session["Pending2FaSetupSecret"] as string;
if (string.IsNullOrWhiteSpace(secret))
{
TempData["tfaError"] = "Setup expired. Please try again.";
return RedirectToAction("UserSettings");
}
if (!TotpHelper.VerifyCode(secret, code))
{
TempData["tfaError"] = "Invalid code. Scan the QR again and enter a fresh code.";
return RedirectToAction("UserSettings");
}
user.two_factor_secret = SecretProtector.Protect(secret);
user.two_factor_enabled = true;
db.SaveChanges();
Session.Remove("Pending2FaSetupSecret");
TempData["tfaOk"] = "Two-factor authentication is now enabled.";
return RedirectToAction("UserSettings");
}
[Authorize]
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult DisableTwoFactor(string code)
{
var user = GetCurrentAdmin();
if (user == null)
return RedirectToAction("Index", "login");
if (!user.two_factor_enabled || string.IsNullOrWhiteSpace(user.two_factor_secret))
{
TempData["tfaError"] = "Two-factor authentication is not enabled.";
return RedirectToAction("UserSettings");
}
var secret = SecretProtector.Unprotect(user.two_factor_secret) ?? user.two_factor_secret;
if (!TotpHelper.VerifyCode(secret, code))
{
TempData["tfaError"] = "Invalid code. 2FA was not disabled.";
return RedirectToAction("UserSettings");
}
user.two_factor_enabled = false;
user.two_factor_secret = null;
db.SaveChanges();
Session.Remove("Pending2FaSetupSecret");
TempData["tfaOk"] = "Two-factor authentication has been disabled.";
return RedirectToAction("UserSettings");
}
private admin GetCurrentAdmin()
{
var username = User?.Identity?.Name;
if (string.IsNullOrWhiteSpace(username))
return null;
return db.admins.FirstOrDefault(x => x.username == username);
}
private static bool IsSafeHttpUrl(string url)
{
if (string.IsNullOrWhiteSpace(url))
return false;
return Uri.TryCreate(url.Trim(), UriKind.Absolute, out var uri)
&& (uri.Scheme == Uri.UriSchemeHttps || uri.Scheme == Uri.UriSchemeHttp);
}
} }
} }
+167 -12
View File
@@ -1,7 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Web;
using System.Web.Mvc; using System.Web.Mvc;
using System.Web.Security; using System.Web.Security;
using atakanozbancom.Models.classes; using atakanozbancom.Models.classes;
@@ -10,33 +8,190 @@ namespace atakanozbancom.Controllers
{ {
public class loginController : Controller public class loginController : Controller
{ {
// GET: Login private readonly Context c = new Context();
Context c = new Context(); private const int MaxPasswordFailures = 8;
private const int MaxTotpFailures = 5;
private const int LockMinutes = 15;
public ActionResult Index() public ActionResult Index()
{ {
return View(); return View();
} }
[HttpPost] [HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Index(admin ad) public ActionResult Index(admin ad)
{ {
var bilgiler = c.admins.FirstOrDefault(x => x.username == ad.username && x.password == ad.password); if (ad == null || string.IsNullOrWhiteSpace(ad.username) || string.IsNullOrWhiteSpace(ad.password))
if (bilgiler != null)
{
FormsAuthentication.SetAuthCookie(bilgiler.username, false);
Session["username"] = bilgiler.username.ToString();
return RedirectToAction("Index", "admin");
}
else
{ {
ViewBag.Error = "Username and password are required.";
return View(); return View();
} }
var user = c.admins.FirstOrDefault(x => x.username == ad.username);
if (user == null)
{
ViewBag.Error = "Invalid username or password.";
return View();
}
if (user.login_lock_until.HasValue && user.login_lock_until.Value > DateTime.UtcNow)
{
ViewBag.Error = "Account temporarily locked. Try again later.";
return View();
}
if (!PasswordHasher.Verify(ad.password, user.password))
{
user.login_failed_count++;
if (user.login_failed_count >= MaxPasswordFailures)
{
user.login_lock_until = DateTime.UtcNow.AddMinutes(LockMinutes);
user.login_failed_count = 0;
}
c.SaveChanges();
ViewBag.Error = "Invalid username or password.";
return View();
}
user.login_failed_count = 0;
user.login_lock_until = null;
if (!PasswordHasher.IsHashed(user.password))
{
user.password = PasswordHasher.Hash(ad.password);
}
c.SaveChanges();
ClearPending2Fa();
if (user.two_factor_enabled && !string.IsNullOrWhiteSpace(user.two_factor_secret))
{
if (user.totp_lock_until.HasValue && user.totp_lock_until.Value > DateTime.UtcNow)
{
ViewBag.Error = "Too many failed 2FA attempts. Try again later.";
return View();
}
Session["Pending2FaUserId"] = user.id;
Session["Pending2FaUsername"] = user.username;
Session["Pending2FaExpiresUtc"] = DateTime.UtcNow.AddMinutes(10);
return RedirectToAction("Verify");
}
CompleteLogin(user.username);
return RedirectToAction("Index", "admin");
}
public ActionResult Verify()
{
if (!HasValidPending2Fa())
{
ClearPending2Fa();
return RedirectToAction("Index");
}
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Verify(string code)
{
if (!HasValidPending2Fa())
{
ClearPending2Fa();
return RedirectToAction("Index");
}
var pendingId = Convert.ToInt32(Session["Pending2FaUserId"]);
var pendingUsername = Session["Pending2FaUsername"].ToString();
var user = c.admins.FirstOrDefault(x => x.id == pendingId && x.username == pendingUsername);
if (user == null || !user.two_factor_enabled || string.IsNullOrWhiteSpace(user.two_factor_secret))
{
ClearPending2Fa();
return RedirectToAction("Index");
}
if (user.totp_lock_until.HasValue && user.totp_lock_until.Value > DateTime.UtcNow)
{
ViewBag.Error = "Too many failed attempts. Try again later.";
return View();
}
var secret = ResolveTotpSecret(user);
if (string.IsNullOrWhiteSpace(secret) || !TotpHelper.VerifyCode(secret, code))
{
user.totp_failed_count++;
if (user.totp_failed_count >= MaxTotpFailures)
{
user.totp_lock_until = DateTime.UtcNow.AddMinutes(LockMinutes);
user.totp_failed_count = 0;
ViewBag.Error = "Too many failed attempts. Try again later.";
}
else
{
ViewBag.Error = "Invalid authentication code.";
}
c.SaveChanges();
return View();
}
user.totp_failed_count = 0;
user.totp_lock_until = null;
c.SaveChanges();
ClearPending2Fa();
CompleteLogin(user.username);
return RedirectToAction("Index", "admin");
} }
public ActionResult logout() public ActionResult logout()
{ {
ClearPending2Fa();
Session.Remove("username");
Session.Abandon();
FormsAuthentication.SignOut(); FormsAuthentication.SignOut();
return RedirectToAction("index", "login"); return RedirectToAction("index", "login");
} }
private void CompleteLogin(string username)
{
FormsAuthentication.SetAuthCookie(username, false);
Session["username"] = username;
}
private bool HasValidPending2Fa()
{
if (Session["Pending2FaUserId"] == null || Session["Pending2FaUsername"] == null)
return false;
var expires = Session["Pending2FaExpiresUtc"] as DateTime?;
if (expires == null || expires.Value < DateTime.UtcNow)
return false;
return true;
}
private static string ResolveTotpSecret(admin user)
{
if (string.IsNullOrWhiteSpace(user.two_factor_secret))
return null;
var unprotected = SecretProtector.Unprotect(user.two_factor_secret);
if (!string.IsNullOrWhiteSpace(unprotected))
return unprotected;
return user.two_factor_secret;
}
private void ClearPending2Fa()
{
Session.Remove("Pending2FaUserId");
Session.Remove("Pending2FaUsername");
Session.Remove("Pending2FaSetupSecret");
Session.Remove("Pending2FaExpiresUtc");
}
} }
} }
+104
View File
@@ -0,0 +1,104 @@
using System;
using System.Security.Cryptography;
using System.Text;
namespace atakanozbancom.Models.classes
{
public static class PasswordHasher
{
private const string Prefix = "pbkdf2";
private const int DefaultIterations = 100000;
private const int SaltSize = 16;
private const int HashSize = 32;
public static string Hash(string password)
{
if (password == null)
throw new ArgumentNullException("password");
var salt = new byte[SaltSize];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(salt);
}
var hash = Pbkdf2(password, salt, DefaultIterations, HashSize);
return string.Format(
"{0}${1}${2}${3}",
Prefix,
DefaultIterations,
Convert.ToBase64String(salt),
Convert.ToBase64String(hash));
}
public static bool IsHashed(string stored)
{
return !string.IsNullOrWhiteSpace(stored)
&& stored.StartsWith(Prefix + "$", StringComparison.Ordinal);
}
public static bool Verify(string password, string stored)
{
if (string.IsNullOrEmpty(password) || string.IsNullOrEmpty(stored))
return false;
if (!IsHashed(stored))
return FixedTimeEquals(password, stored);
var parts = stored.Split('$');
if (parts.Length != 4 || !string.Equals(parts[0], Prefix, StringComparison.Ordinal))
return false;
int iterations;
if (!int.TryParse(parts[1], out iterations) || iterations < 1000)
return false;
byte[] salt;
byte[] expected;
try
{
salt = Convert.FromBase64String(parts[2]);
expected = Convert.FromBase64String(parts[3]);
}
catch
{
return false;
}
var actual = Pbkdf2(password, salt, iterations, expected.Length);
return FixedTimeEquals(actual, expected);
}
private static byte[] Pbkdf2(string password, byte[] salt, int iterations, int length)
{
using (var derive = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA256))
{
return derive.GetBytes(length);
}
}
private static bool FixedTimeEquals(byte[] a, byte[] b)
{
if (a == null || b == null || a.Length != b.Length)
return false;
var diff = 0;
for (var i = 0; i < a.Length; i++)
diff |= a[i] ^ b[i];
return diff == 0;
}
private static bool FixedTimeEquals(string a, string b)
{
if (a == null || b == null || a.Length != b.Length)
return false;
var diff = 0;
for (var i = 0; i < a.Length; i++)
diff |= a[i] ^ b[i];
return diff == 0;
}
}
}
+54
View File
@@ -0,0 +1,54 @@
using System;
using System.Text;
using System.Web.Security;
namespace atakanozbancom.Models.classes
{
public static class SecretProtector
{
private static readonly string[] Purposes = { "atakanozbancom.TotpSecret.v1" };
public static string Protect(string plaintext)
{
if (string.IsNullOrEmpty(plaintext))
return plaintext;
var bytes = Encoding.UTF8.GetBytes(plaintext);
var protectedBytes = MachineKey.Protect(bytes, Purposes);
return Convert.ToBase64String(protectedBytes);
}
public static string Unprotect(string protectedValue)
{
if (string.IsNullOrEmpty(protectedValue))
return protectedValue;
try
{
var protectedBytes = Convert.FromBase64String(protectedValue);
var bytes = MachineKey.Unprotect(protectedBytes, Purposes);
return bytes == null ? null : Encoding.UTF8.GetString(bytes);
}
catch
{
return null;
}
}
public static bool LooksProtected(string value)
{
if (string.IsNullOrWhiteSpace(value))
return false;
try
{
var bytes = Convert.FromBase64String(value);
return bytes.Length > 16;
}
catch
{
return false;
}
}
}
}
+176
View File
@@ -0,0 +1,176 @@
using System;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Web;
namespace atakanozbancom.Models.classes
{
public static class TotpHelper
{
private const string Base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public static string GenerateSecret(int byteLength = 20)
{
var bytes = new byte[byteLength];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(bytes);
}
return ToBase32(bytes);
}
public static bool VerifyCode(string base32Secret, string code, int window = 1)
{
if (string.IsNullOrWhiteSpace(base32Secret) || string.IsNullOrWhiteSpace(code))
return false;
code = code.Trim().Replace(" ", "");
if (code.Length != 6)
return false;
long timestep;
try
{
timestep = GetCurrentTimeStep();
}
catch
{
return false;
}
for (var i = -window; i <= window; i++)
{
var expected = ComputeTotp(base32Secret, timestep + i);
if (FixedTimeEquals(expected, code))
return true;
}
return false;
}
public static string BuildOtpAuthUri(string issuer, string accountName, string base32Secret)
{
var label = HttpUtility.UrlEncode(issuer + ":" + accountName);
var issuerParam = HttpUtility.UrlEncode(issuer);
return string.Format(
CultureInfo.InvariantCulture,
"otpauth://totp/{0}?secret={1}&issuer={2}&digits=6&period=30",
label,
base32Secret,
issuerParam);
}
private static long GetCurrentTimeStep()
{
return (long)Math.Floor((DateTime.UtcNow - UnixEpoch).TotalSeconds / 30.0);
}
private static string ComputeTotp(string base32Secret, long timestep)
{
var key = FromBase32(base32Secret);
var counter = BitConverter.GetBytes(timestep);
if (BitConverter.IsLittleEndian)
Array.Reverse(counter);
byte[] hash;
using (var hmac = new HMACSHA1(key))
{
hash = hmac.ComputeHash(counter);
}
var offset = hash[hash.Length - 1] & 0x0F;
var binary =
((hash[offset] & 0x7F) << 24)
| ((hash[offset + 1] & 0xFF) << 16)
| ((hash[offset + 2] & 0xFF) << 8)
| (hash[offset + 3] & 0xFF);
var otp = binary % 1000000;
return otp.ToString("D6", CultureInfo.InvariantCulture);
}
private static string ToBase32(byte[] data)
{
if (data == null || data.Length == 0)
return string.Empty;
var sb = new StringBuilder((data.Length * 8 + 4) / 5);
int buffer = data[0];
var next = 1;
var bitsLeft = 8;
while (bitsLeft > 0 || next < data.Length)
{
if (bitsLeft < 5)
{
if (next < data.Length)
{
buffer <<= 8;
buffer |= data[next++] & 0xFF;
bitsLeft += 8;
}
else
{
var pad = 5 - bitsLeft;
buffer <<= pad;
bitsLeft += pad;
}
}
var index = (buffer >> (bitsLeft - 5)) & 0x1F;
bitsLeft -= 5;
sb.Append(Base32Alphabet[index]);
}
return sb.ToString();
}
private static byte[] FromBase32(string input)
{
var cleaned = input.Trim().Replace(" ", "").Replace("=", "").ToUpperInvariant();
var output = new byte[cleaned.Length * 5 / 8];
var bitBuffer = 0;
var bitsLeft = 0;
var index = 0;
foreach (var c in cleaned)
{
var val = Base32Alphabet.IndexOf(c);
if (val < 0)
throw new FormatException("Invalid Base32 character.");
bitBuffer = (bitBuffer << 5) | val;
bitsLeft += 5;
if (bitsLeft >= 8)
{
output[index++] = (byte)((bitBuffer >> (bitsLeft - 8)) & 0xFF);
bitsLeft -= 8;
}
}
if (index != output.Length)
{
var trimmed = new byte[index];
Array.Copy(output, trimmed, index);
return trimmed;
}
return output;
}
private static bool FixedTimeEquals(string a, string b)
{
if (a == null || b == null || a.Length != b.Length)
return false;
var diff = 0;
for (var i = 0; i < a.Length; i++)
diff |= a[i] ^ b[i];
return diff == 0;
}
}
}
+23 -1
View File
@@ -1,5 +1,7 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace atakanozbancom.Models.classes namespace atakanozbancom.Models.classes
{ {
@@ -9,6 +11,26 @@ namespace atakanozbancom.Models.classes
public int id { get; set; } public int id { get; set; }
public string username { get; set; } public string username { get; set; }
public string password { get; set; } public string password { get; set; }
[Column("two_factor_enabled")]
public bool two_factor_enabled { get; set; }
[Column("two_factor_secret")]
[StringLength(512)]
public string two_factor_secret { get; set; }
[Column("login_failed_count")]
public int login_failed_count { get; set; }
[Column("login_lock_until")]
public DateTime? login_lock_until { get; set; }
[Column("totp_failed_count")]
public int totp_failed_count { get; set; }
[Column("totp_lock_until")]
public DateTime? totp_lock_until { get; set; }
public List<myprojects> MyProjects { get; set; } public List<myprojects> MyProjects { get; set; }
} }
} }
+13 -3
View File
@@ -9,13 +9,20 @@ wallpaper, plus English/Turkish localization.
- Public site: home, about, my projects (with an image/iframe carousel per project), - Public site: home, about, my projects (with an image/iframe carousel per project),
affiliate links, dynamic social icons in the footer, and a changeable background wallpaper. affiliate links, dynamic social icons in the footer, and a changeable background wallpaper.
- Admin panel (`/admin`) protected by Forms Authentication: - Admin panel (`/admin`) protected by Forms Authentication:
- My Projects (bilingual posts + media carousel)
- Affiliate Links (bilingual cards with logos)
- Social Icons (footer icons)
- Wallpaper (site-wide background image)
- Optional TOTP two-factor authentication (Security page)
- My Projects: create/edit posts with EN/TR translations and a media manager - My Projects: create/edit posts with EN/TR translations and a media manager
(upload images or embed iframes, drag-free reordering via a sort order field). (upload images or embed iframes, drag-free reordering via a sort order field).
- Affiliate Links: create/edit links with a logo, title, description and EN/TR translations. - Affiliate Links: create/edit links with a logo, title, description and EN/TR translations.
- Social Icons: manage the icon row shown in the site footer (Font Awesome classes + URLs). - Social Icons: manage the icon row shown in the site footer (Font Awesome classes + URLs).
- Wallpaper: upload a new background image for the whole site, or reset to the default. - Wallpaper: upload a new background image for the whole site, or reset to the default.
- Security: optional TOTP two-factor authentication (Google Authenticator / Authy / etc.).
- Image uploads everywhere support both the regular file picker **and** pasting an - Image uploads everywhere support both the regular file picker **and** pasting an
image straight from the clipboard (Ctrl+V). image straight from the clipboard (Ctrl+V).
- Public my-projects images open in a larger lightbox when clicked.
- English / Turkish localization via `.resx` resource files and a culture cookie. - English / Turkish localization via `.resx` resource files and a culture cookie.
## Tech stack ## Tech stack
@@ -110,9 +117,12 @@ runtime data, not source code.
This project was open-sourced as a personal portfolio/reference, not as a hardened This project was open-sourced as a personal portfolio/reference, not as a hardened
multi-tenant product. A few things worth knowing if you deploy your own copy or build on it: multi-tenant product. A few things worth knowing if you deploy your own copy or build on it:
- **Passwords are stored and compared in plain text** (`loginController`). If you plan to - Optional **TOTP 2FA** and **password change** live under Admin → username menu → User Settings.
expose this beyond your own local use, replace this with a proper password hash Existing installs can run `database/add-2fa.sql` then `database/harden-auth.sql`.
(e.g. BCrypt/PBKDF2) before going live. - Passwords are stored with **PBKDF2-SHA256** (`PasswordHasher`). On first successful login after
upgrade, any leftover plaintext password is rehashed automatically.
- **Passwords used to be stored in plain text.** If you fork an old clone, force a password change
and rotate machine keys before going live.
- The admin panel already includes `[Authorize]` on every management action and - The admin panel already includes `[Authorize]` on every management action and
`[ValidateAntiForgeryToken]` on every state-changing POST, so it's protected against CSRF `[ValidateAntiForgeryToken]` on every state-changing POST, so it's protected against CSRF
once you're logged in — but access is still gated by a single shared admin login rather once you're logged in — but access is still gated by a single shared admin login rather
+1
View File
@@ -108,6 +108,7 @@
<strong>@(User?.Identity?.Name ?? "Admin")</strong> <strong>@(User?.Identity?.Name ?? "Admin")</strong>
</a> </a>
<ul class="dropdown-menu dropdown-menu-dark text-small shadow"> <ul class="dropdown-menu dropdown-menu-dark text-small shadow">
<li><a class="dropdown-item" href="/admin/usersettings"><i class="fa-solid fa-user-gear me-2"></i>User Settings</a></li>
<li><a class="dropdown-item" href="/" target="_blank"><i class="fa-solid fa-arrow-up-right-from-square me-2"></i>Go to site</a></li> <li><a class="dropdown-item" href="/" target="_blank"><i class="fa-solid fa-arrow-up-right-from-square me-2"></i>Go to site</a></li>
<li><hr class="dropdown-divider" /></li> <li><hr class="dropdown-divider" /></li>
<li><a class="dropdown-item" href="/login/logout"><i class="fa-solid fa-right-from-bracket me-2"></i>Sign out</a></li> <li><a class="dropdown-item" href="/login/logout"><i class="fa-solid fa-right-from-bracket me-2"></i>Sign out</a></li>
+5
View File
@@ -0,0 +1,5 @@
@{
// Kept for old bookmarks; controller redirects to UserSettings.
Layout = null;
Response.Redirect(Url.Action("UserSettings", "admin"));
}
+116
View File
@@ -0,0 +1,116 @@
@{
ViewBag.Title = "User Settings";
Layout = "~/Views/Shared/_AdminLayout.cshtml";
var enabled = ViewBag.TwoFactorEnabled == true;
}
<h2 class="text-white mt-3 mb-3">User Settings</h2>
<p class="text-white-50">Account: <strong class="text-white">@ViewBag.Username</strong></p>
<div class="row">
<div class="col-lg-6 mb-4">
<div class="bg-dark border border-secondary rounded p-4 text-white h-100">
<h4 class="mb-3">Change password</h4>
@if (TempData["pwdOk"] != null)
{
<div class="alert alert-success py-2">@TempData["pwdOk"]</div>
}
@if (TempData["pwdError"] != null)
{
<div class="alert alert-danger py-2">@TempData["pwdError"]</div>
}
@using (Html.BeginForm("ChangePassword", "admin", FormMethod.Post))
{
@Html.AntiForgeryToken()
<div class="mb-3">
<label class="form-label">Current password</label>
<input type="password" name="currentPassword" class="form-control bg-dark text-white" autocomplete="current-password" required />
</div>
<div class="mb-3">
<label class="form-label">New password</label>
<input type="password" name="newPassword" class="form-control bg-dark text-white" autocomplete="new-password" minlength="8" required />
<small class="text-muted">At least 8 characters.</small>
</div>
<div class="mb-3">
<label class="form-label">Confirm new password</label>
<input type="password" name="confirmPassword" class="form-control bg-dark text-white" autocomplete="new-password" minlength="8" required />
</div>
<button type="submit" class="btn btn-primary">Update password</button>
}
</div>
</div>
<div class="col-lg-6 mb-4">
<div class="bg-dark border border-secondary rounded p-4 text-white h-100">
<h4 class="mb-3">
Two-factor authentication
@if (enabled)
{
<span class="badge text-bg-success ms-2">Enabled</span>
}
else
{
<span class="badge text-bg-secondary ms-2">Disabled</span>
}
</h4>
@if (TempData["tfaOk"] != null)
{
<div class="alert alert-success py-2">@TempData["tfaOk"]</div>
}
@if (TempData["tfaError"] != null)
{
<div class="alert alert-danger py-2">@TempData["tfaError"]</div>
}
@if (enabled)
{
<p class="text-white-50">2FA is active. Enter a current authenticator code to disable it.</p>
using (Html.BeginForm("DisableTwoFactor", "admin", FormMethod.Post))
{
@Html.AntiForgeryToken()
<div class="mb-3">
<label class="form-label">Authenticator code</label>
<input type="text" name="code" class="form-control bg-dark text-white" maxlength="6" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code" required />
</div>
<button type="submit" class="btn btn-danger" onclick="return confirm('Disable two-factor authentication?');">Disable 2FA</button>
}
}
else
{
<ol class="text-white-50">
<li>Open your authenticator app and scan the QR code below.</li>
<li>Or enter this secret manually: <code class="text-white">@ViewBag.SetupSecret</code></li>
<li>Enter the 6-digit code to confirm and enable 2FA.</li>
</ol>
<div class="text-center my-3">
<div id="totpQr" class="d-inline-block bg-white p-2 rounded"></div>
</div>
using (Html.BeginForm("EnableTwoFactor", "admin", FormMethod.Post))
{
@Html.AntiForgeryToken()
<div class="mb-3">
<label class="form-label">Authenticator code</label>
<input type="text" name="code" class="form-control bg-dark text-white" maxlength="6" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code" required />
</div>
<button type="submit" class="btn btn-success">Enable 2FA</button>
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<script>
(function () {
var uri = @Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject((string)ViewBag.OtpAuthUri));
var el = document.getElementById('totpQr');
if (el && uri && typeof QRCode !== 'undefined') {
new QRCode(el, { text: uri, width: 200, height: 200 });
}
})();
</script>
}
</div>
</div>
</div>
+10 -21
View File
@@ -1,19 +1,15 @@
@{
@{
Layout = null; Layout = null;
} }
<!doctype html> <!doctype html>
<html lang="en" data-bs-theme="dark"> <html lang="en" data-bs-theme="dark">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sign-in</title> <title>Sign-in</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style> <style>
.form-control { .form-control {
background-color: #212529 !important; background-color: #212529 !important;
@@ -30,16 +26,6 @@
box-shadow: 0 0 5px rgba(13, 110, 253, 0.5); box-shadow: 0 0 5px rgba(13, 110, 253, 0.5);
} }
.form-check-input {
background-color: #343a40 !important;
border-color: #6c757d !important;
}
.form-check-input:checked {
background-color: #0d6efd !important;
border-color: #0d6efd !important;
}
.btn-primary { .btn-primary {
background-color: #0d6efd !important; background-color: #0d6efd !important;
border-color: #0d6efd !important; border-color: #0d6efd !important;
@@ -51,28 +37,31 @@
} }
</style> </style>
</head> </head>
<body class="bg-dark d-flex align-items-center justify-content-center vh-100"> <body class="bg-dark d-flex align-items-center justify-content-center vh-100">
<main class="form-signin w-100" style="max-width: 400px;"> <main class="form-signin w-100" style="max-width: 400px;">
@using (Html.BeginForm("index", "login", FormMethod.Post)) @using (Html.BeginForm("index", "login", FormMethod.Post))
{ {
@Html.AntiForgeryToken()
<h1 class="h3 mb-3 fw-normal text-white text-center">Please sign in</h1> <h1 class="h3 mb-3 fw-normal text-white text-center">Please sign in</h1>
if (ViewBag.Error != null)
{
<div class="alert alert-danger py-2">@ViewBag.Error</div>
}
<div class="form-floating"> <div class="form-floating">
<input type="text" name="username" class="form-control" id="floatingInput" placeholder="Username"> <input type="text" name="username" class="form-control" id="floatingInput" placeholder="Username" autocomplete="username" required>
<label for="floatingInput">Username</label> <label for="floatingInput">Username</label>
</div> </div>
<div class="form-floating mt-2 mb-2"> <div class="form-floating mt-2 mb-3">
<input type="password" name="password" class="form-control" id="floatingPassword" placeholder="Password"> <input type="password" name="password" class="form-control" id="floatingPassword" placeholder="Password" autocomplete="current-password" required>
<label for="floatingPassword">Password</label> <label for="floatingPassword">Password</label>
</div> </div>
<button class="btn btn-primary w-100 py-2" type="submit">Sign in</button> <button class="btn btn-primary w-100 py-2" type="submit">Sign in</button>
} }
</main> </main>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js"></script>
</body> </body>
</html> </html>
+50
View File
@@ -0,0 +1,50 @@
@{
Layout = null;
}
<!doctype html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Two-factor authentication</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css" rel="stylesheet">
<style>
.form-control {
background-color: #212529 !important;
color: white !important;
border: 1px solid #6c757d;
letter-spacing: 0.35rem;
text-align: center;
font-size: 1.4rem;
}
.form-control:focus {
border-color: #0d6efd !important;
box-shadow: 0 0 5px rgba(13, 110, 253, 0.5);
}
</style>
</head>
<body class="bg-dark d-flex align-items-center justify-content-center vh-100">
<main class="w-100" style="max-width: 400px;">
@using (Html.BeginForm("Verify", "login", FormMethod.Post))
{
@Html.AntiForgeryToken()
<h1 class="h3 mb-2 fw-normal text-white text-center">Two-factor authentication</h1>
<p class="text-white-50 text-center mb-3">Enter the 6-digit code from your authenticator app.</p>
if (ViewBag.Error != null)
{
<div class="alert alert-danger py-2">@ViewBag.Error</div>
}
<div class="mb-3">
<input type="text" name="code" class="form-control" maxlength="6" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code" autofocus required />
</div>
<button class="btn btn-primary w-100 py-2" type="submit">Verify</button>
<a href="@Url.Action("Index", "login")" class="btn btn-link text-white-50 w-100 mt-2">Back to sign in</a>
}
</main>
</body>
</html>
+274 -2
View File
@@ -37,6 +37,7 @@
width: 100%; width: 100%;
height: auto; height: auto;
object-fit: contain; object-fit: contain;
cursor: zoom-in;
} }
.media-arrow { .media-arrow {
@@ -61,6 +62,91 @@
.carousel-indicators { .carousel-indicators {
z-index: 6; z-index: 6;
} }
#projectLightboxModal .modal-dialog {
width: auto;
max-width: calc(100vw - 2rem);
margin: 0.5rem auto;
}
#projectLightboxModal .modal-content {
background: transparent;
width: fit-content;
max-width: 100%;
margin: 0 auto;
}
#projectLightboxModal .modal-body {
padding: 0;
}
#projectLightboxModal .lightbox-stage {
position: relative;
display: inline-block;
max-width: calc(100vw - 2rem);
line-height: 0;
background: #000;
border-radius: 0.35rem;
overflow: hidden;
}
#projectLightboxModal .lightbox-stage img {
display: block;
width: auto;
height: auto;
max-width: calc(100vw - 2rem);
max-height: 85vh;
object-fit: contain;
}
#projectLightboxModal .lightbox-arrow {
position: absolute;
top: 50%;
bottom: auto;
transform: translateY(-50%);
width: 3.5rem;
height: 3.5rem;
opacity: 0.9;
z-index: 2;
}
#projectLightboxModal .lightbox-arrow.carousel-control-prev {
left: 0;
}
#projectLightboxModal .lightbox-arrow.carousel-control-next {
right: 0;
}
#projectLightboxModal .lightbox-indicators {
position: absolute;
left: 0;
right: 0;
bottom: 0.75rem;
margin: 0;
z-index: 2;
}
#projectLightboxModal .lightbox-indicators button {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: rgba(255, 255, 255, .5);
border: 0;
margin: 0 4px;
padding: 0;
}
#projectLightboxModal .lightbox-indicators button.active {
background-color: #fff;
}
#projectLightboxModal .lightbox-close {
position: absolute;
top: 0.6rem;
right: 0.6rem;
z-index: 3;
}
</style> </style>
@foreach (var p in Model) @foreach (var p in Model)
@@ -104,7 +190,9 @@
<div class="carousel-media-iframe"> <div class="carousel-media-iframe">
<iframe src="@m.url" <iframe src="@m.url"
loading="lazy" loading="lazy"
allowfullscreen></iframe> allowfullscreen
referrerpolicy="no-referrer"
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"></iframe>
@if (hasMultiple) @if (hasMultiple)
@@ -130,7 +218,11 @@
else if (m.type == "image") else if (m.type == "image")
{ {
<div class="carousel-media-image"> <div class="carousel-media-image">
<img src="@m.url" alt="@p.title" /> <img src="@m.url"
alt="@p.title"
class="project-lightbox-trigger"
data-fullsrc="@m.url"
data-gallery="@carouselId" />
@if (hasMultiple) @if (hasMultiple)
{ {
@@ -178,3 +270,183 @@
</div> </div>
} }
<div class="modal fade" id="projectLightboxModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-none">
<div class="modal-body">
<div class="lightbox-stage">
<button type="button" class="btn-close btn-close-white lightbox-close" data-bs-dismiss="modal" aria-label="Close"></button>
<img id="projectLightboxImg" src="" alt="" />
<button type="button" id="lightboxPrev" class="carousel-control-prev lightbox-arrow" aria-label="Previous">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
</button>
<button type="button" id="lightboxNext" class="carousel-control-next lightbox-arrow" aria-label="Next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
</button>
<div id="lightboxIndicators" class="carousel-indicators lightbox-indicators d-none"></div>
</div>
</div>
</div>
</div>
</div>
<script>
(function () {
var items = [];
var index = 0;
var modalEl = null;
var imgEl = null;
var stageEl = null;
var dialogEl = null;
var indicatorsEl = null;
var prevBtn = null;
var nextBtn = null;
function getEls() {
modalEl = document.getElementById('projectLightboxModal');
imgEl = document.getElementById('projectLightboxImg');
indicatorsEl = document.getElementById('lightboxIndicators');
prevBtn = document.getElementById('lightboxPrev');
nextBtn = document.getElementById('lightboxNext');
stageEl = modalEl ? modalEl.querySelector('.lightbox-stage') : null;
dialogEl = modalEl ? modalEl.querySelector('.modal-dialog') : null;
return modalEl && imgEl && indicatorsEl && prevBtn && nextBtn && stageEl && dialogEl;
}
function fitToImage() {
var maxW = Math.max(window.innerWidth - 32, 200);
var maxH = Math.max(window.innerHeight * 0.85, 200);
var nw = imgEl.naturalWidth || 0;
var nh = imgEl.naturalHeight || 0;
if (!nw || !nh) return;
var scale = Math.min(maxW / nw, maxH / nh, 1);
var w = Math.round(nw * scale);
var h = Math.round(nh * scale);
imgEl.style.width = w + 'px';
imgEl.style.height = h + 'px';
stageEl.style.width = w + 'px';
stageEl.style.height = h + 'px';
dialogEl.style.width = w + 'px';
dialogEl.style.maxWidth = w + 'px';
}
function showSlide(i) {
if (!items.length) return;
index = (i + items.length) % items.length;
var item = items[index];
imgEl.onload = fitToImage;
imgEl.src = item.src;
imgEl.alt = item.alt;
if (imgEl.complete && imgEl.naturalWidth) fitToImage();
var dots = indicatorsEl.querySelectorAll('button');
for (var d = 0; d < dots.length; d++) {
dots[d].classList.toggle('active', d === index);
if (d === index) dots[d].setAttribute('aria-current', 'true');
else dots[d].removeAttribute('aria-current');
}
}
function buildIndicators() {
indicatorsEl.innerHTML = '';
if (items.length < 2) {
indicatorsEl.classList.add('d-none');
prevBtn.classList.add('d-none');
nextBtn.classList.add('d-none');
return;
}
indicatorsEl.classList.remove('d-none');
prevBtn.classList.remove('d-none');
nextBtn.classList.remove('d-none');
for (var i = 0; i < items.length; i++) {
(function (slideIndex) {
var btn = document.createElement('button');
btn.type = 'button';
btn.setAttribute('aria-label', 'Slide ' + (slideIndex + 1));
if (slideIndex === index) {
btn.classList.add('active');
btn.setAttribute('aria-current', 'true');
}
btn.addEventListener('click', function () { showSlide(slideIndex); });
indicatorsEl.appendChild(btn);
})(i);
}
}
document.addEventListener('click', function (e) {
var trigger = e.target.closest('.project-lightbox-trigger');
if (!trigger) return;
if (typeof bootstrap === 'undefined') return;
if (!getEls()) return;
e.preventDefault();
var gallery = trigger.getAttribute('data-gallery');
var galleryImgs = gallery
? document.querySelectorAll('.project-lightbox-trigger[data-gallery="' + gallery + '"]')
: [trigger];
items = [];
index = 0;
for (var i = 0; i < galleryImgs.length; i++) {
var img = galleryImgs[i];
items.push({
src: img.getAttribute('data-fullsrc') || img.src,
alt: img.alt || ''
});
if (img === trigger) index = i;
}
buildIndicators();
showSlide(index);
bootstrap.Modal.getOrCreateInstance(modalEl).show();
});
document.addEventListener('click', function (e) {
if (e.target.closest('#lightboxPrev')) {
e.preventDefault();
showSlide(index - 1);
} else if (e.target.closest('#lightboxNext')) {
e.preventDefault();
showSlide(index + 1);
}
});
document.addEventListener('keydown', function (e) {
if (!modalEl || !modalEl.classList.contains('show')) return;
if (e.key === 'ArrowLeft') showSlide(index - 1);
if (e.key === 'ArrowRight') showSlide(index + 1);
});
window.addEventListener('resize', function () {
if (modalEl && modalEl.classList.contains('show')) fitToImage();
});
document.addEventListener('hidden.bs.modal', function (e) {
if (e.target.id !== 'projectLightboxModal') return;
if (!imgEl) getEls();
if (imgEl) {
imgEl.onload = null;
imgEl.removeAttribute('src');
imgEl.style.width = '';
imgEl.style.height = '';
}
if (stageEl) {
stageEl.style.width = '';
stageEl.style.height = '';
}
if (dialogEl) {
dialogEl.style.width = '';
dialogEl.style.maxWidth = '';
}
items = [];
index = 0;
});
})();
</script>
+9 -12
View File
@@ -13,20 +13,18 @@
<system.web> <system.web>
<httpRuntime maxRequestLength="25600" executionTimeout="600" /> <httpRuntime maxRequestLength="25600" executionTimeout="600" />
<!-- <!-- Generate your own keys for production. Never commit real keys to a public repo. -->
Generate your own machineKey (do not reuse the sample below) and paste it here. <machineKey validationKey="REPLACE_WITH_YOUR_VALIDATION_KEY"
You can generate one at https://www.mvcbuddy.net/tool/machine-key-generator/ or with decryptionKey="REPLACE_WITH_YOUR_DECRYPTION_KEY"
any offline "IIS machineKey generator" utility. This keeps auth cookies / validation="HMACSHA256"
ViewState secure and consistent across app restarts. decryption="AES" />
<customErrors mode="RemoteOnly" />
<machineKey validationKey="..." decryptionKey="..." validation="HMACSHA256" decryption="AES" />
-->
<customErrors mode="Off" />
<compilation targetFramework="4.7.2" debug="true" /> <compilation targetFramework="4.7.2" debug="true" />
<authentication mode="Forms"> <authentication mode="Forms">
<forms loginUrl="/login/index" /> <forms loginUrl="/login/index" requireSSL="true" cookieless="UseCookies" timeout="60" slidingExpiration="true" />
</authentication> </authentication>
<httpCookies httpOnlyCookies="true" requireSSL="true" />
</system.web> </system.web>
<runtime> <runtime>
@@ -88,8 +86,7 @@
</entityFramework> </entityFramework>
<connectionStrings> <connectionStrings>
<!-- Point this at your own SQL Server instance. See README.md for setup instructions. --> <add name="Context" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=atakanozbancomdb;Integrated Security=true;" providerName="System.Data.SqlClient"/>
<add name="Context" connectionString="Data Source=YOUR_SERVER\INSTANCE;Initial Catalog=atakanozbancomdb;Integrated Security=true;" providerName="System.Data.SqlClient" />
</connectionStrings> </connectionStrings>
<!-- IIS 413.1 FIX --> <!-- IIS 413.1 FIX -->
+6
View File
@@ -179,6 +179,9 @@
<Compile Include="Models\classes\socialicon.cs" /> <Compile Include="Models\classes\socialicon.cs" />
<Compile Include="Models\classes\sitesetting.cs" /> <Compile Include="Models\classes\sitesetting.cs" />
<Compile Include="Models\classes\DescriptionFormatter.cs" /> <Compile Include="Models\classes\DescriptionFormatter.cs" />
<Compile Include="Models\classes\TotpHelper.cs" />
<Compile Include="Models\classes\PasswordHasher.cs" />
<Compile Include="Models\classes\SecretProtector.cs" />
<Compile Include="Models\classes\Context.cs" /> <Compile Include="Models\classes\Context.cs" />
<Compile Include="Models\classes\LanguageTR.cs" /> <Compile Include="Models\classes\LanguageTR.cs" />
<Compile Include="Models\classes\myprojects.cs" /> <Compile Include="Models\classes\myprojects.cs" />
@@ -255,7 +258,10 @@
<Content Include="Views\admin\newsocialicon.cshtml" /> <Content Include="Views\admin\newsocialicon.cshtml" />
<Content Include="Views\admin\socialiconget.cshtml" /> <Content Include="Views\admin\socialiconget.cshtml" />
<Content Include="Views\admin\wallpaper.cshtml" /> <Content Include="Views\admin\wallpaper.cshtml" />
<Content Include="Views\admin\usersettings.cshtml" />
<Content Include="Views\admin\security.cshtml" />
<Content Include="Views\login\index.cshtml" /> <Content Include="Views\login\index.cshtml" />
<Content Include="Views\login\verify.cshtml" />
<Content Include="Views\Shared\_PortfolioLayout.cshtml" /> <Content Include="Views\Shared\_PortfolioLayout.cshtml" />
<Content Include="Views\about\index.cshtml" /> <Content Include="Views\about\index.cshtml" />
<Content Include="Views\affiliate\index.cshtml" /> <Content Include="Views\affiliate\index.cshtml" />
+8
View File
@@ -0,0 +1,8 @@
IF COL_LENGTH('dbo.admins', 'two_factor_enabled') IS NULL
BEGIN
ALTER TABLE dbo.admins ADD two_factor_enabled BIT NOT NULL CONSTRAINT DF_admins_two_factor_enabled DEFAULT(0);
END
IF COL_LENGTH('dbo.admins', 'two_factor_secret') IS NULL
BEGIN
ALTER TABLE dbo.admins ADD two_factor_secret NVARCHAR(64) NULL;
END
+24
View File
@@ -0,0 +1,24 @@
IF COL_LENGTH('dbo.admins', 'two_factor_secret') IS NOT NULL
BEGIN
ALTER TABLE dbo.admins ALTER COLUMN two_factor_secret NVARCHAR(512) NULL;
END
IF COL_LENGTH('dbo.admins', 'password') IS NOT NULL
BEGIN
ALTER TABLE dbo.admins ALTER COLUMN password NVARCHAR(255) NOT NULL;
END
IF COL_LENGTH('dbo.admins', 'login_failed_count') IS NULL
BEGIN
ALTER TABLE dbo.admins ADD login_failed_count INT NOT NULL CONSTRAINT DF_admins_login_failed_count DEFAULT(0);
END
IF COL_LENGTH('dbo.admins', 'login_lock_until') IS NULL
BEGIN
ALTER TABLE dbo.admins ADD login_lock_until DATETIME2 NULL;
END
IF COL_LENGTH('dbo.admins', 'totp_failed_count') IS NULL
BEGIN
ALTER TABLE dbo.admins ADD totp_failed_count INT NOT NULL CONSTRAINT DF_admins_totp_failed_count DEFAULT(0);
END
IF COL_LENGTH('dbo.admins', 'totp_lock_until') IS NULL
BEGIN
ALTER TABLE dbo.admins ADD totp_lock_until DATETIME2 NULL;
END
+9 -3
View File
@@ -17,9 +17,15 @@ GO
-- Admin users (used for the /login and /admin panel) -- Admin users (used for the /login and /admin panel)
-- ========================================================= -- =========================================================
CREATE TABLE dbo.admins ( CREATE TABLE dbo.admins (
id INT IDENTITY(1,1) NOT NULL PRIMARY KEY, id INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
username NVARCHAR(100) NOT NULL, username NVARCHAR(100) NOT NULL,
password NVARCHAR(255) NOT NULL password NVARCHAR(255) NOT NULL,
two_factor_enabled BIT NOT NULL CONSTRAINT DF_admins_two_factor_enabled DEFAULT(0),
two_factor_secret NVARCHAR(512) NULL,
login_failed_count INT NOT NULL CONSTRAINT DF_admins_login_failed_count DEFAULT(0),
login_lock_until DATETIME2 NULL,
totp_failed_count INT NOT NULL CONSTRAINT DF_admins_totp_failed_count DEFAULT(0),
totp_lock_until DATETIME2 NULL
); );
GO GO