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
+275 -3
View File
@@ -37,6 +37,7 @@
width: 100%;
height: auto;
object-fit: contain;
cursor: zoom-in;
}
.media-arrow {
@@ -61,6 +62,91 @@
.carousel-indicators {
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>
@foreach (var p in Model)
@@ -104,7 +190,9 @@
<div class="carousel-media-iframe">
<iframe src="@m.url"
loading="lazy"
allowfullscreen></iframe>
allowfullscreen
referrerpolicy="no-referrer"
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"></iframe>
@if (hasMultiple)
@@ -130,7 +218,11 @@
else if (m.type == "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)
{
@@ -177,4 +269,184 @@
<p class="lead mb-5 text-center">@Html.Raw(atakanozbancom.Models.classes.DescriptionFormatter.ToSafeHtml(p.description))</p>
</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>