Spaces:
Running
Running
Aniket Mishra commited on
Commit ·
f29ccf0
1
Parent(s): 2ed539c
feat: add 4-digit Passcode PIN login and 1-Click Magic Access Link for seamless editor collaboration
Browse files- public/index.html +232 -61
- server.js +77 -0
public/index.html
CHANGED
|
@@ -290,15 +290,15 @@
|
|
| 290 |
<span>Connect Google</span>
|
| 291 |
</button>
|
| 292 |
|
| 293 |
-
<!-- Team Editor Login Button
|
| 294 |
<button
|
| 295 |
type="button"
|
| 296 |
id="navEditorLoginBtn"
|
| 297 |
-
onclick="
|
| 298 |
-
class="flex items-center gap-1.5 px-
|
| 299 |
-
title="Log in with
|
| 300 |
>
|
| 301 |
-
<span>🔑 Team Login</span>
|
| 302 |
</button>
|
| 303 |
|
| 304 |
<!-- Manual Upload Direct Button -->
|
|
@@ -1473,10 +1473,51 @@
|
|
| 1473 |
<p class="text-[9px] text-[var(--text-muted)]">Controls how many Drive-to-YouTube video streams process in parallel using the SQLite queue.</p>
|
| 1474 |
</div>
|
| 1475 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1476 |
<!-- Team & Editor Permissions (Email OTP Access) -->
|
| 1477 |
-
<div class="p-3 bg-sky-500/
|
| 1478 |
<div class="flex items-center justify-between font-bold text-sky-400 uppercase text-[10px]">
|
| 1479 |
-
<span>👥
|
| 1480 |
<span class="text-[9px] bg-sky-500/20 text-sky-300 px-1.5 py-0.5 border border-sky-500/30 font-bold" id="teamEditorCount">0 members</span>
|
| 1481 |
</div>
|
| 1482 |
<p class="text-[10px] text-[var(--text-muted)]">Give team members/editors access to edit titles, subjects, batches, and update live YouTube thumbnails via Email OTP without needing Google login.</p>
|
|
@@ -2002,79 +2043,70 @@
|
|
| 2002 |
</div>
|
| 2003 |
</div>
|
| 2004 |
|
| 2005 |
-
<!-- 8. Team Editor
|
| 2006 |
-
<div id="
|
| 2007 |
-
<div class="bg-[var(--bg-card)] border-2 border-
|
| 2008 |
|
| 2009 |
<!-- Modal Header -->
|
| 2010 |
<div class="flex items-center justify-between border-b border-[var(--border-main)] px-5 py-3.5 bg-[var(--bg-subcard)]">
|
| 2011 |
<div class="flex items-center gap-2">
|
| 2012 |
-
<span class="text-
|
| 2013 |
<div>
|
| 2014 |
-
<h3 class="text-xs font-bold uppercase tracking-wider text-
|
| 2015 |
-
<p class="text-[10px] text-[var(--text-muted)]">
|
| 2016 |
</div>
|
| 2017 |
</div>
|
| 2018 |
-
<button type="button" onclick="
|
| 2019 |
</div>
|
| 2020 |
|
| 2021 |
-
<!--
|
| 2022 |
-
<div
|
| 2023 |
-
<
|
| 2024 |
-
|
|
|
|
| 2025 |
<input
|
| 2026 |
-
type="
|
| 2027 |
-
id="
|
| 2028 |
-
|
| 2029 |
-
|
| 2030 |
-
|
|
|
|
|
|
|
| 2031 |
/>
|
| 2032 |
-
<
|
| 2033 |
-
|
| 2034 |
-
|
| 2035 |
-
|
| 2036 |
-
|
| 2037 |
-
|
| 2038 |
-
|
| 2039 |
-
|
| 2040 |
-
|
| 2041 |
-
<span>SEND 6-DIGIT OTP ➔</span>
|
| 2042 |
-
</button>
|
| 2043 |
-
</div>
|
| 2044 |
-
|
| 2045 |
-
<!-- Step 2: Verify OTP -->
|
| 2046 |
-
<div id="editorOtpStep2" class="p-5 space-y-4 hidden">
|
| 2047 |
-
<div class="p-3 bg-sky-500/10 border border-sky-500/30 text-xs">
|
| 2048 |
-
<div class="flex items-center justify-between text-sky-400 font-bold text-[10px] uppercase">
|
| 2049 |
-
<span>OTP Code Sent</span>
|
| 2050 |
-
<button type="button" onclick="resetEditorOtpFlow()" class="text-[9px] underline cursor-pointer text-sky-300">Change Email</button>
|
| 2051 |
</div>
|
| 2052 |
-
<p class="text-[10px] text-[var(--text-main)] mt-1" id="editorOtpSentHint">Sent to: user@example.com</p>
|
| 2053 |
</div>
|
| 2054 |
|
| 2055 |
<div class="space-y-1">
|
| 2056 |
-
<label class="text-[
|
| 2057 |
<input
|
| 2058 |
type="text"
|
| 2059 |
-
id="
|
| 2060 |
-
|
| 2061 |
-
|
| 2062 |
-
|
| 2063 |
-
onkeydown="if(event.key==='Enter') verifyEditorOtp()"
|
| 2064 |
/>
|
| 2065 |
</div>
|
| 2066 |
|
| 2067 |
<button
|
| 2068 |
type="button"
|
| 2069 |
-
id="
|
| 2070 |
-
onclick="
|
| 2071 |
-
class="w-full py-
|
| 2072 |
>
|
| 2073 |
-
<span>
|
| 2074 |
</button>
|
| 2075 |
|
| 2076 |
-
<div class="text-center">
|
| 2077 |
-
<button type="button" onclick="
|
| 2078 |
</div>
|
| 2079 |
</div>
|
| 2080 |
|
|
@@ -3901,6 +3933,7 @@
|
|
| 3901 |
|
| 3902 |
loadClientIds();
|
| 3903 |
loadServerSettings();
|
|
|
|
| 3904 |
loadTeamEditors();
|
| 3905 |
loadSmtpSettings();
|
| 3906 |
showAuthLanding();
|
|
@@ -4024,7 +4057,7 @@
|
|
| 4024 |
navEditorBadge.classList.remove('hidden');
|
| 4025 |
navEditorBadge.classList.add('flex');
|
| 4026 |
const label = document.getElementById('navEditorEmailLabel');
|
| 4027 |
-
if (label) label.textContent = `Editor (${window.editorEmail || '
|
| 4028 |
}
|
| 4029 |
} else if (isAuth) {
|
| 4030 |
if (navAuthBtn) { navAuthBtn.classList.add('hidden'); navAuthBtn.classList.remove('flex'); }
|
|
@@ -4048,13 +4081,142 @@
|
|
| 4048 |
};
|
| 4049 |
|
| 4050 |
// ══════════════════════════════════════════════════════════════════
|
| 4051 |
-
// TEAM & EDITOR ACCESS ENGINE (
|
| 4052 |
// ══════════════════════════════════════════════════════════════════
|
| 4053 |
|
| 4054 |
-
window.
|
| 4055 |
closeAllModals();
|
| 4056 |
-
|
| 4057 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4058 |
if (modal) modal.classList.remove('hidden');
|
| 4059 |
const emailInput = document.getElementById('editorEmailInput');
|
| 4060 |
if (emailInput) {
|
|
@@ -7236,7 +7398,16 @@
|
|
| 7236 |
// Check for auth callback redirect first
|
| 7237 |
checkServerAuthOnLoad();
|
| 7238 |
|
| 7239 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7240 |
(async () => {
|
| 7241 |
const isEditor = await checkEditorAuthOnLoad();
|
| 7242 |
if (isEditor) {
|
|
|
|
| 290 |
<span>Connect Google</span>
|
| 291 |
</button>
|
| 292 |
|
| 293 |
+
<!-- Team Editor PIN Login Button -->
|
| 294 |
<button
|
| 295 |
type="button"
|
| 296 |
id="navEditorLoginBtn"
|
| 297 |
+
onclick="openPinLoginModal()"
|
| 298 |
+
class="flex items-center gap-1.5 px-3 h-8 border border-sky-500/60 bg-sky-500/15 hover:bg-sky-500/25 text-sky-400 font-bold transition-all cursor-pointer text-[11px] uppercase tracking-wider shadow-xs"
|
| 299 |
+
title="Log in with 4-digit Team PIN / Passcode"
|
| 300 |
>
|
| 301 |
+
<span>🔑 Team PIN Login</span>
|
| 302 |
</button>
|
| 303 |
|
| 304 |
<!-- Manual Upload Direct Button -->
|
|
|
|
| 1473 |
<p class="text-[9px] text-[var(--text-muted)]">Controls how many Drive-to-YouTube video streams process in parallel using the SQLite queue.</p>
|
| 1474 |
</div>
|
| 1475 |
|
| 1476 |
+
<!-- Team Passcode / PIN & 1-Click Magic Access Link (Primary Collaboration) -->
|
| 1477 |
+
<div class="p-3 bg-sky-500/10 border-2 border-sky-500/60 text-xs space-y-2.5">
|
| 1478 |
+
<div class="flex items-center justify-between font-bold text-sky-400 uppercase text-[10px]">
|
| 1479 |
+
<span>🔑 Team Editor Passcode / PIN (Instant Access)</span>
|
| 1480 |
+
<span class="text-[9px] bg-sky-500/20 text-sky-300 px-1.5 py-0.5 border border-sky-500/30 font-bold font-mono" id="currentPinBadge">PIN: 1234</span>
|
| 1481 |
+
</div>
|
| 1482 |
+
<p class="text-[10px] text-[var(--text-muted)]">Set a 4-digit PIN for your team members. Anyone with this PIN can log in instantly and edit video titles, batches, and live YouTube thumbnails with zero friction.</p>
|
| 1483 |
+
|
| 1484 |
+
<div class="flex gap-2 items-center">
|
| 1485 |
+
<div class="relative flex-1">
|
| 1486 |
+
<input
|
| 1487 |
+
type="text"
|
| 1488 |
+
id="teamPinInput"
|
| 1489 |
+
maxlength="10"
|
| 1490 |
+
placeholder="Set 4-digit PIN (e.g. 1234)..."
|
| 1491 |
+
value="1234"
|
| 1492 |
+
class="w-full p-2 text-xs font-mono font-bold tracking-widest border border-sky-500/40 bg-[var(--bg-card)] text-[var(--text-main)] focus:outline-none focus:border-sky-400"
|
| 1493 |
+
/>
|
| 1494 |
+
</div>
|
| 1495 |
+
<button
|
| 1496 |
+
type="button"
|
| 1497 |
+
id="savePinBtn"
|
| 1498 |
+
onclick="saveTeamPin()"
|
| 1499 |
+
class="px-3 py-2 bg-sky-600 hover:bg-sky-500 text-white font-bold text-xs uppercase tracking-wider cursor-pointer whitespace-nowrap"
|
| 1500 |
+
>
|
| 1501 |
+
Save PIN
|
| 1502 |
+
</button>
|
| 1503 |
+
</div>
|
| 1504 |
+
|
| 1505 |
+
<div class="pt-1 flex items-center justify-between bg-[var(--bg-card)] border border-sky-500/30 p-2 text-[10px]">
|
| 1506 |
+
<span class="text-[var(--text-muted)] truncate mr-2">🚀 Share 1-Click Instant Login Link with your editor:</span>
|
| 1507 |
+
<button
|
| 1508 |
+
type="button"
|
| 1509 |
+
onclick="copyMagicEditorLink()"
|
| 1510 |
+
class="px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-[10px] uppercase cursor-pointer whitespace-nowrap shrink-0 shadow-sm"
|
| 1511 |
+
>
|
| 1512 |
+
📋 Copy 1-Click Link
|
| 1513 |
+
</button>
|
| 1514 |
+
</div>
|
| 1515 |
+
</div>
|
| 1516 |
+
|
| 1517 |
<!-- Team & Editor Permissions (Email OTP Access) -->
|
| 1518 |
+
<div class="p-3 bg-sky-500/5 border border-sky-500/30 text-xs space-y-2.5">
|
| 1519 |
<div class="flex items-center justify-between font-bold text-sky-400 uppercase text-[10px]">
|
| 1520 |
+
<span>👥 Optional: Authorized Email List</span>
|
| 1521 |
<span class="text-[9px] bg-sky-500/20 text-sky-300 px-1.5 py-0.5 border border-sky-500/30 font-bold" id="teamEditorCount">0 members</span>
|
| 1522 |
</div>
|
| 1523 |
<p class="text-[10px] text-[var(--text-muted)]">Give team members/editors access to edit titles, subjects, batches, and update live YouTube thumbnails via Email OTP without needing Google login.</p>
|
|
|
|
| 2043 |
</div>
|
| 2044 |
</div>
|
| 2045 |
|
| 2046 |
+
<!-- 8. Team Editor 4-Digit PIN Authentication Modal -->
|
| 2047 |
+
<div id="pinAuthModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4 font-mono-code">
|
| 2048 |
+
<div class="bg-[var(--bg-card)] border-2 border-sky-500/60 max-w-sm w-full shadow-2xl my-4 text-[var(--text-main)] overflow-hidden">
|
| 2049 |
|
| 2050 |
<!-- Modal Header -->
|
| 2051 |
<div class="flex items-center justify-between border-b border-[var(--border-main)] px-5 py-3.5 bg-[var(--bg-subcard)]">
|
| 2052 |
<div class="flex items-center gap-2">
|
| 2053 |
+
<span class="text-xl">🔑</span>
|
| 2054 |
<div>
|
| 2055 |
+
<h3 class="text-xs font-bold uppercase tracking-wider text-sky-400">Team Studio Access</h3>
|
| 2056 |
+
<p class="text-[10px] text-[var(--text-muted)]">Enter 4-Digit Passcode / PIN</p>
|
| 2057 |
</div>
|
| 2058 |
</div>
|
| 2059 |
+
<button type="button" onclick="closePinLoginModal()" class="text-[var(--text-muted)] hover:text-[var(--text-main)] text-xl font-bold cursor-pointer">×</button>
|
| 2060 |
</div>
|
| 2061 |
|
| 2062 |
+
<!-- PIN Input Form -->
|
| 2063 |
+
<div class="p-5 space-y-4">
|
| 2064 |
+
<p class="text-[11px] text-[var(--text-muted)] text-center">Enter your 4-digit Team PIN to access all videos and update live YouTube thumbnails.</p>
|
| 2065 |
+
|
| 2066 |
+
<div class="space-y-1.5 text-center">
|
| 2067 |
<input
|
| 2068 |
+
type="password"
|
| 2069 |
+
id="editorPinInput"
|
| 2070 |
+
maxlength="10"
|
| 2071 |
+
placeholder="••••"
|
| 2072 |
+
class="w-full p-3 text-center tracking-[12px] font-mono font-bold text-2xl border-2 border-sky-500/60 bg-[var(--bg-card)] text-[var(--text-main)] focus:outline-none focus:border-sky-400"
|
| 2073 |
+
onkeydown="if(event.key==='Enter') submitPinLogin()"
|
| 2074 |
+
autocomplete="off"
|
| 2075 |
/>
|
| 2076 |
+
<div class="flex items-center justify-between text-[10px] text-[var(--text-muted)] px-1">
|
| 2077 |
+
<span>Passcode / PIN</span>
|
| 2078 |
+
<button
|
| 2079 |
+
type="button"
|
| 2080 |
+
onclick="const el=document.getElementById('editorPinInput'); el.type = el.type === 'password' ? 'text' : 'password';"
|
| 2081 |
+
class="hover:text-sky-400 underline cursor-pointer"
|
| 2082 |
+
>
|
| 2083 |
+
Show / Hide
|
| 2084 |
+
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2085 |
</div>
|
|
|
|
| 2086 |
</div>
|
| 2087 |
|
| 2088 |
<div class="space-y-1">
|
| 2089 |
+
<label class="text-[9px] font-bold uppercase text-[var(--text-muted)] tracking-wider">Your Name (Optional)</label>
|
| 2090 |
<input
|
| 2091 |
type="text"
|
| 2092 |
+
id="editorNameInput"
|
| 2093 |
+
placeholder="e.g. Editor / Aniket"
|
| 2094 |
+
class="w-full p-2 text-xs border border-[var(--border-main)] bg-[var(--bg-card)] text-[var(--text-main)] focus:outline-none focus:border-sky-400"
|
| 2095 |
+
onkeydown="if(event.key==='Enter') submitPinLogin()"
|
|
|
|
| 2096 |
/>
|
| 2097 |
</div>
|
| 2098 |
|
| 2099 |
<button
|
| 2100 |
type="button"
|
| 2101 |
+
id="pinLoginSubmitBtn"
|
| 2102 |
+
onclick="submitPinLogin()"
|
| 2103 |
+
class="w-full py-3 bg-sky-600 hover:bg-sky-500 text-white font-bold text-xs uppercase tracking-widest cursor-pointer transition-all shadow-md flex items-center justify-center gap-2"
|
| 2104 |
>
|
| 2105 |
+
<span>ENTER STUDIO ➔</span>
|
| 2106 |
</button>
|
| 2107 |
|
| 2108 |
+
<div class="p-2 bg-[var(--bg-subcard)] border border-[var(--border-main)] text-center text-[10px] text-[var(--text-muted)]">
|
| 2109 |
+
<span>Default PIN is <button type="button" onclick="document.getElementById('editorPinInput').value='1234'; submitPinLogin('1234');" class="text-sky-400 font-mono font-bold underline cursor-pointer">1234</button></span>
|
| 2110 |
</div>
|
| 2111 |
</div>
|
| 2112 |
|
|
|
|
| 3933 |
|
| 3934 |
loadClientIds();
|
| 3935 |
loadServerSettings();
|
| 3936 |
+
loadTeamPin();
|
| 3937 |
loadTeamEditors();
|
| 3938 |
loadSmtpSettings();
|
| 3939 |
showAuthLanding();
|
|
|
|
| 4057 |
navEditorBadge.classList.remove('hidden');
|
| 4058 |
navEditorBadge.classList.add('flex');
|
| 4059 |
const label = document.getElementById('navEditorEmailLabel');
|
| 4060 |
+
if (label) label.textContent = `Editor (${window.editorEmail || 'PIN Verified'})`;
|
| 4061 |
}
|
| 4062 |
} else if (isAuth) {
|
| 4063 |
if (navAuthBtn) { navAuthBtn.classList.add('hidden'); navAuthBtn.classList.remove('flex'); }
|
|
|
|
| 4081 |
};
|
| 4082 |
|
| 4083 |
// ══════════════════════════════════════════════════════════════════
|
| 4084 |
+
// TEAM & EDITOR ACCESS ENGINE (4-DIGIT PIN & 1-CLICK MAGIC LINK)
|
| 4085 |
// ══════════════════════════════════════════════════════════════════
|
| 4086 |
|
| 4087 |
+
window.openPinLoginModal = function() {
|
| 4088 |
closeAllModals();
|
| 4089 |
+
const modal = document.getElementById('pinAuthModal');
|
| 4090 |
+
if (modal) modal.classList.remove('hidden');
|
| 4091 |
+
const pinInput = document.getElementById('editorPinInput');
|
| 4092 |
+
if (pinInput) {
|
| 4093 |
+
pinInput.value = '';
|
| 4094 |
+
setTimeout(() => pinInput.focus(), 60);
|
| 4095 |
+
}
|
| 4096 |
+
};
|
| 4097 |
+
|
| 4098 |
+
window.closePinLoginModal = function() {
|
| 4099 |
+
const modal = document.getElementById('pinAuthModal');
|
| 4100 |
+
if (modal) modal.classList.add('hidden');
|
| 4101 |
+
};
|
| 4102 |
+
|
| 4103 |
+
window.submitPinLogin = async function(customPin = null) {
|
| 4104 |
+
const pinInput = document.getElementById('editorPinInput');
|
| 4105 |
+
const nameInput = document.getElementById('editorNameInput');
|
| 4106 |
+
const btn = document.getElementById('pinLoginSubmitBtn');
|
| 4107 |
+
const pin = customPin || pinInput?.value?.trim();
|
| 4108 |
+
const name = nameInput?.value?.trim() || 'Team Editor';
|
| 4109 |
+
|
| 4110 |
+
if (!pin) {
|
| 4111 |
+
showToast('Please enter the 4-digit PIN.', 'error');
|
| 4112 |
+
if (pinInput) pinInput.focus();
|
| 4113 |
+
return;
|
| 4114 |
+
}
|
| 4115 |
+
|
| 4116 |
+
if (btn) {
|
| 4117 |
+
btn.disabled = true;
|
| 4118 |
+
btn.textContent = 'VERIFYING PIN...';
|
| 4119 |
+
}
|
| 4120 |
+
|
| 4121 |
+
try {
|
| 4122 |
+
const res = await fetch('/api/auth/pin-login', {
|
| 4123 |
+
method: 'POST',
|
| 4124 |
+
headers: { 'Content-Type': 'application/json' },
|
| 4125 |
+
body: JSON.stringify({ pin, name })
|
| 4126 |
+
});
|
| 4127 |
+
const data = await res.json();
|
| 4128 |
+
|
| 4129 |
+
if (data.success && data.token) {
|
| 4130 |
+
AppStorage.setItem('editor_token', data.token);
|
| 4131 |
+
AppStorage.setItem('editor_name', data.name || name);
|
| 4132 |
+
AppStorage.setItem('editor_last_pin', pin);
|
| 4133 |
+
|
| 4134 |
+
window.gAccessToken = data.token;
|
| 4135 |
+
window.isAuthenticated = true;
|
| 4136 |
+
window.isEditorSession = true;
|
| 4137 |
+
window.editorEmail = data.name || name;
|
| 4138 |
+
|
| 4139 |
+
closePinLoginModal();
|
| 4140 |
+
checkAuthStatus();
|
| 4141 |
+
connectSSE();
|
| 4142 |
+
showToast(`✔ PIN Verified! Welcome to Studio.`, 'success');
|
| 4143 |
+
loadJobStatus();
|
| 4144 |
+
} else {
|
| 4145 |
+
showToast(data.error || 'Incorrect PIN code. Please try again.', 'error');
|
| 4146 |
+
if (pinInput) {
|
| 4147 |
+
pinInput.value = '';
|
| 4148 |
+
pinInput.focus();
|
| 4149 |
+
}
|
| 4150 |
+
}
|
| 4151 |
+
} catch (err) {
|
| 4152 |
+
showToast('Login error: ' + err.message, 'error');
|
| 4153 |
+
} finally {
|
| 4154 |
+
if (btn) {
|
| 4155 |
+
btn.disabled = false;
|
| 4156 |
+
btn.textContent = 'ENTER STUDIO ➔';
|
| 4157 |
+
}
|
| 4158 |
+
}
|
| 4159 |
+
};
|
| 4160 |
+
|
| 4161 |
+
window.loadTeamPin = async function() {
|
| 4162 |
+
const pinInput = document.getElementById('teamPinInput');
|
| 4163 |
+
const pinBadge = document.getElementById('currentPinBadge');
|
| 4164 |
+
try {
|
| 4165 |
+
const res = await fetch('/api/settings/pin');
|
| 4166 |
+
const data = await res.json();
|
| 4167 |
+
if (data.success && data.pin) {
|
| 4168 |
+
if (pinInput) pinInput.value = data.pin;
|
| 4169 |
+
if (pinBadge) pinBadge.textContent = `PIN: ${data.pin}`;
|
| 4170 |
+
}
|
| 4171 |
+
} catch (e) {}
|
| 4172 |
+
};
|
| 4173 |
+
|
| 4174 |
+
window.saveTeamPin = async function() {
|
| 4175 |
+
const pinInput = document.getElementById('teamPinInput');
|
| 4176 |
+
const btn = document.getElementById('savePinBtn');
|
| 4177 |
+
const pin = pinInput?.value?.trim();
|
| 4178 |
+
|
| 4179 |
+
if (!pin || pin.length < 4) {
|
| 4180 |
+
showToast('PIN must be at least 4 digits.', 'error');
|
| 4181 |
+
if (pinInput) pinInput.focus();
|
| 4182 |
+
return;
|
| 4183 |
+
}
|
| 4184 |
+
|
| 4185 |
+
if (btn) {
|
| 4186 |
+
btn.disabled = true;
|
| 4187 |
+
btn.textContent = 'SAVING...';
|
| 4188 |
+
}
|
| 4189 |
+
|
| 4190 |
+
try {
|
| 4191 |
+
const res = await fetch('/api/settings/pin', {
|
| 4192 |
+
method: 'POST',
|
| 4193 |
+
headers: { 'Content-Type': 'application/json' },
|
| 4194 |
+
body: JSON.stringify({ pin })
|
| 4195 |
+
});
|
| 4196 |
+
const data = await res.json();
|
| 4197 |
+
|
| 4198 |
+
if (data.success) {
|
| 4199 |
+
showToast(`✔ Team PIN updated to: ${data.pin}`, 'success');
|
| 4200 |
+
loadTeamPin();
|
| 4201 |
+
} else {
|
| 4202 |
+
showToast(data.error || 'Failed to update PIN', 'error');
|
| 4203 |
+
}
|
| 4204 |
+
} catch (err) {
|
| 4205 |
+
showToast('Error saving PIN: ' + err.message, 'error');
|
| 4206 |
+
} finally {
|
| 4207 |
+
if (btn) {
|
| 4208 |
+
btn.disabled = false;
|
| 4209 |
+
btn.textContent = 'Save PIN';
|
| 4210 |
+
}
|
| 4211 |
+
}
|
| 4212 |
+
};
|
| 4213 |
+
|
| 4214 |
+
window.copyMagicEditorLink = async function() {
|
| 4215 |
+
const pinInput = document.getElementById('teamPinInput');
|
| 4216 |
+
const pin = pinInput?.value?.trim() || '1234';
|
| 4217 |
+
const magicUrl = `${window.location.origin}/?pin=${encodeURIComponent(pin)}`;
|
| 4218 |
+
await copyToClipboard(magicUrl, '1-Click Team Editor Link');
|
| 4219 |
+
};
|
| 4220 |
if (modal) modal.classList.remove('hidden');
|
| 4221 |
const emailInput = document.getElementById('editorEmailInput');
|
| 4222 |
if (emailInput) {
|
|
|
|
| 7398 |
// Check for auth callback redirect first
|
| 7399 |
checkServerAuthOnLoad();
|
| 7400 |
|
| 7401 |
+
// Check for 1-Click Magic Link (?pin=1234 or ?access=1234)
|
| 7402 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 7403 |
+
const urlPin = urlParams.get('pin') || urlParams.get('access') || urlParams.get('key');
|
| 7404 |
+
if (urlPin) {
|
| 7405 |
+
console.log('[auth] 1-Click PIN detected in URL, auto-logging in...');
|
| 7406 |
+
submitPinLogin(urlPin);
|
| 7407 |
+
try { window.history.replaceState({}, document.title, window.location.pathname); } catch (e) {}
|
| 7408 |
+
}
|
| 7409 |
+
|
| 7410 |
+
// Auth Priority: 1) Team Editor session (4-Digit PIN), 2) Server-side refresh token, 3) localStorage access token, 4) Silent GIS refresh
|
| 7411 |
(async () => {
|
| 7412 |
const isEditor = await checkEditorAuthOnLoad();
|
| 7413 |
if (isEditor) {
|
server.js
CHANGED
|
@@ -2263,6 +2263,83 @@ app.post('/api/auth/verify-otp', async (req, res) => {
|
|
| 2263 |
}
|
| 2264 |
});
|
| 2265 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2266 |
// 6. GET /api/auth/editor-session — Check current editor session
|
| 2267 |
app.get('/api/auth/editor-session', (req, res) => {
|
| 2268 |
const authHeader = req.headers.authorization || '';
|
|
|
|
| 2263 |
}
|
| 2264 |
});
|
| 2265 |
|
| 2266 |
+
// 5B. POST /api/auth/pin-login — Verify 4-digit PIN and issue persistent editor session token
|
| 2267 |
+
app.post('/api/auth/pin-login', async (req, res) => {
|
| 2268 |
+
try {
|
| 2269 |
+
const { pin, name } = req.body || {};
|
| 2270 |
+
const storedPin = db.getSetting('team_pin') || process.env.TEAM_PIN || '1234';
|
| 2271 |
+
|
| 2272 |
+
if (!pin || String(pin).trim() !== String(storedPin).trim()) {
|
| 2273 |
+
return res.status(401).json({
|
| 2274 |
+
success: false,
|
| 2275 |
+
error: 'Incorrect Passcode / PIN. Please enter the valid PIN.'
|
| 2276 |
+
});
|
| 2277 |
+
}
|
| 2278 |
+
|
| 2279 |
+
const editorName = (name || 'Team Editor').trim();
|
| 2280 |
+
|
| 2281 |
+
// Pre-resolve channel title & ID
|
| 2282 |
+
let targetChannelId = null;
|
| 2283 |
+
let channelTitle = 'YouTube Channel';
|
| 2284 |
+
|
| 2285 |
+
try {
|
| 2286 |
+
const creds = db.getActiveCredentials();
|
| 2287 |
+
if (creds && creds.length > 0) {
|
| 2288 |
+
const cred = creds[0];
|
| 2289 |
+
const oauth2Client = new google.auth.OAuth2(cred.clientId, cred.clientSecret);
|
| 2290 |
+
oauth2Client.setCredentials({ refresh_token: cred.refreshToken });
|
| 2291 |
+
const yt = google.youtube({ version: 'v3', auth: oauth2Client });
|
| 2292 |
+
const chRes = await yt.channels.list({ part: ['snippet', 'id'], mine: true });
|
| 2293 |
+
if (chRes.data.items && chRes.data.items.length > 0) {
|
| 2294 |
+
targetChannelId = chRes.data.items[0].id;
|
| 2295 |
+
channelTitle = chRes.data.items[0].snippet?.title || 'YouTube Channel';
|
| 2296 |
+
}
|
| 2297 |
+
}
|
| 2298 |
+
} catch (e) {
|
| 2299 |
+
console.warn('Could not pre-resolve channel for PIN editor session:', e.message);
|
| 2300 |
+
}
|
| 2301 |
+
|
| 2302 |
+
if (!targetChannelId) {
|
| 2303 |
+
const history = db.loadUploadedHistory();
|
| 2304 |
+
if (history && history.length > 0 && history[0].channelId) {
|
| 2305 |
+
targetChannelId = history[0].channelId;
|
| 2306 |
+
channelTitle = history[0].batch || 'YouTube Channel';
|
| 2307 |
+
}
|
| 2308 |
+
}
|
| 2309 |
+
|
| 2310 |
+
const session = db.createEditorSession(editorName, 'editor', targetChannelId, null, 30);
|
| 2311 |
+
|
| 2312 |
+
return res.json({
|
| 2313 |
+
success: true,
|
| 2314 |
+
token: session.token,
|
| 2315 |
+
name: editorName,
|
| 2316 |
+
role: session.role,
|
| 2317 |
+
channelId: targetChannelId,
|
| 2318 |
+
channelTitle,
|
| 2319 |
+
message: `PIN Verified! Welcome ${editorName} to Studio.`
|
| 2320 |
+
});
|
| 2321 |
+
} catch (err) {
|
| 2322 |
+
console.error('PIN Login error:', err);
|
| 2323 |
+
return res.status(500).json({ success: false, error: err.message });
|
| 2324 |
+
}
|
| 2325 |
+
});
|
| 2326 |
+
|
| 2327 |
+
// 5C. GET /api/settings/pin & POST /api/settings/pin
|
| 2328 |
+
app.get('/api/settings/pin', (req, res) => {
|
| 2329 |
+
const currentPin = db.getSetting('team_pin') || process.env.TEAM_PIN || '1234';
|
| 2330 |
+
res.json({ success: true, pin: currentPin });
|
| 2331 |
+
});
|
| 2332 |
+
|
| 2333 |
+
app.post('/api/settings/pin', (req, res) => {
|
| 2334 |
+
const { pin } = req.body || {};
|
| 2335 |
+
if (!pin || String(pin).trim().length < 4) {
|
| 2336 |
+
return res.status(400).json({ success: false, error: 'PIN must be at least 4 digits or characters.' });
|
| 2337 |
+
}
|
| 2338 |
+
const cleanPin = String(pin).trim();
|
| 2339 |
+
db.setSetting('team_pin', cleanPin);
|
| 2340 |
+
res.json({ success: true, pin: cleanPin, message: `Team PIN successfully updated to: ${cleanPin}` });
|
| 2341 |
+
});
|
| 2342 |
+
|
| 2343 |
// 6. GET /api/auth/editor-session — Check current editor session
|
| 2344 |
app.get('/api/auth/editor-session', (req, res) => {
|
| 2345 |
const authHeader = req.headers.authorization || '';
|