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:
@@ -13,7 +13,7 @@ namespace atakanozbancom.Controllers
|
||||
private readonly Context db = new Context();
|
||||
|
||||
[Authorize]
|
||||
public ActionResult Index()
|
||||
public ActionResult Index() // admin dashboard landing page
|
||||
{
|
||||
ViewBag.MyProjectsCount = db.MyProjects.Count();
|
||||
ViewBag.AffiliateLinksCount = db.AffiliateLinks.Count();
|
||||
@@ -22,7 +22,7 @@ namespace atakanozbancom.Controllers
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
public ActionResult MyProjects()
|
||||
public ActionResult MyProjects() // shows what we have
|
||||
{
|
||||
var value = db.MyProjects.ToList();
|
||||
return View(value);
|
||||
@@ -53,6 +53,7 @@ namespace atakanozbancom.Controllers
|
||||
DescTR = tr != null ? tr.description : ""
|
||||
};
|
||||
|
||||
// NEW:
|
||||
vm.medias = db.projectmedia
|
||||
.Where(m => m.project_id == p.id)
|
||||
.OrderBy(m => m.sort_order)
|
||||
@@ -183,11 +184,19 @@ namespace atakanozbancom.Controllers
|
||||
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
|
||||
{
|
||||
project_id = projectId,
|
||||
media_type = MediaType.Iframe,
|
||||
url = iframeUrl.Trim(),
|
||||
url = trimmed,
|
||||
sort_order = sortOrder
|
||||
};
|
||||
|
||||
@@ -295,6 +304,9 @@ namespace atakanozbancom.Controllers
|
||||
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 const string AffiliateLogosPrefix = "/Content/uploads/affiliate-logos/";
|
||||
|
||||
@@ -328,6 +340,12 @@ namespace atakanozbancom.Controllers
|
||||
if (!ModelState.IsValid)
|
||||
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);
|
||||
if (logoUrl == null)
|
||||
{
|
||||
@@ -402,6 +420,13 @@ namespace atakanozbancom.Controllers
|
||||
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)
|
||||
{
|
||||
var logoUrl = SaveAffiliateLogo(vm.logoFile);
|
||||
@@ -486,9 +511,12 @@ namespace atakanozbancom.Controllers
|
||||
}
|
||||
catch
|
||||
{
|
||||
// silently ignore, matches existing media-delete behavior
|
||||
}
|
||||
}
|
||||
|
||||
// ================= SOCIAL ICONS (footer) =================
|
||||
|
||||
[Authorize]
|
||||
public ActionResult SocialIcons()
|
||||
{
|
||||
@@ -514,6 +542,12 @@ namespace atakanozbancom.Controllers
|
||||
if (!ModelState.IsValid)
|
||||
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
|
||||
{
|
||||
icon = vm.icon,
|
||||
@@ -557,6 +591,12 @@ namespace atakanozbancom.Controllers
|
||||
if (!ModelState.IsValid)
|
||||
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.url = vm.url;
|
||||
s.sort_order = vm.sort_order;
|
||||
@@ -580,6 +620,8 @@ namespace atakanozbancom.Controllers
|
||||
return RedirectToAction("SocialIcons");
|
||||
}
|
||||
|
||||
// ================= WALLPAPER (site background) =================
|
||||
|
||||
private static readonly string[] AllowedWallpaperExtensions = { ".jpg", ".jpeg", ".png", ".webp", ".gif" };
|
||||
private const string WallpaperPrefix = "/Content/uploads/wallpaper/";
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user