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:
@@ -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>
|
||||
Reference in New Issue
Block a user