1<html lang="en"> 2 <head> 3 <meta charset="UTF-8"> 4 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 5 <meta name="theme-color" content="#e8e8e8f5"> 6 7 <!-- Title & Description --> 8 9 <title>nostrcheck.me â Login</title> 10 <meta name="description" content="Self-hosted nostrcheck-server"> 11 12 13 14 15 <!-- Canonical --> 16 <link rel="canonical" href="https://nostrcheck.me/api/v1/login" /> 17 18 <!-- DNS prefetch + preconnect to third-party CDNs we always load. --> 19 <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin> 20 <link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin> 21 <link rel="dns-prefetch" href="https://cdn.jsdelivr.net"> 22 <link rel="dns-prefetch" href="https://cdnjs.cloudflare.com"> 23 24 <!-- Open Graph --> 25 <meta property="og:locale" content="en_US"> 26 <meta property="og:type" content="website"> 27 <meta property="og:site_name" content="nostrcheck.me"> 28 <meta property="og:title" content="nostrcheck.me â Login"> 29 <meta property="og:description" content="Self-hosted nostrcheck-server"> 30 <meta property="og:image" content="https://nostrcheck.me/static/resources/logo-server.webp"> 31 <meta property="og:url" content="https://nostrcheck.me/api/v1/login"> 32 33 <!-- Twitter --> 34 <meta name="twitter:card" content="summary_large_image"> 35 <meta name="twitter:title" content="nostrcheck.me â Login"> 36 <meta name="twitter:description" content="Self-hosted nostrcheck-server"> 37 <meta name="twitter:image" content="https://nostrcheck.me/static/resources/logo-server.webp"> 38 <meta name="twitter:image:alt" content="nostrcheck.me â Login"> 39 40 <!-- JSON-LD structured data: WebSite schema with site-search action. --> 41
41<script type="application/ld+json"> 42 { 43 "@context": "https://schema.org", 44 "@type": "WebSite", 45 "name": "nostrcheck.me", 46 "url": "https://nostrcheck.me/", 47 "potentialAction": { 48 "@type": "SearchAction", 49 "target": "https://nostrcheck.me/directory?q={search_term_string}", 50 "query-input": "required name=search_term_string" 51 } 52 } 53 </script>
53 54 55 <!-- Apply the saved theme before any stylesheet loads: prevents the 56 white flash for dark-mode users (theme-toggle re-applies it later 57 and keeps the icon/logo in sync). --> 58
58<script> 59 (function() { 60 var saved = localStorage.getItem('theme'); 61 var dark = saved === 'dark' || (saved !== 'light' && window.matchMedia('(prefers-color-scheme: dark)').matches); 62 document.documentElement.setAttribute('data-bs-theme', dark ? 'dark' : 'light'); 63 })(); 64 </script>
64 65 66 <!-- CSS & Icons --> 67 <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"> 68 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css"> 69 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"> 70 <link rel="stylesheet" href="/static/css/style.css"> 71 72 <link rel="icon" href="/favicon.ico"> 73 <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> 74 <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> 75 <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> 76 <link rel="manifest" href="/site.webmanifest"> 77 78 79 <!-- Theme logo: driven by data-bs-theme (set by the blocking script 80 above), no document.write needed. --> 81 <style> 82 #server-logo { content: url('/static/resources/appearance-server-logo-light.png'); } 83 html[data-bs-theme="dark"] #server-logo { content: url('/static/resources/appearance-server-logo-dark.png'); } 84 </style> 85 86 <!-- Dynamic theme CSS: plain cacheable link (the server sends 87 Cache-Control + ETag; the old Math.random() buster forced a 88 re-download on every navigation and painted late). --> 89 <link rel="stylesheet" href="/static/css/theme.css"> 90</head> 91 92 <link rel="stylesheet" href="/static/css/form.css"> 93 <body class="d-flex flex-column h-100"> 94 <style> 95 96 html[data-bs-theme="light"] .navbar { 97 background-color: #e8e8e8f5 !important; 98 } 99 100 html[data-bs-theme="dark"] .navbar { 101 background-color:#2b3035 !important; 102 } 103 104 .dropdown-menu { --bs-dropdown-link-active-bg: #a575ff; } 105 106 #profile-dropdown { 107 position: absolute; 108 right: 10; 109 top: auto; 110 } 111 112 .navbar-nav .nav-link { 113 font-size: 1rem; 114 font-weight: 500; 115 text-transform: uppercase; 116 letter-spacing: 0.5px; 117 padding: 0.5rem 0.7rem; 118 } 119 120 .navbar-nav .nav-link.active { 121 color: #ffffff; 122 background-color: #a575ffb0; 123 border-radius: 5px; 124 } 125 126 @media (max-width: 994.98px) { 127 #profile-dropdown { 128 position: absolute; 129 right: 85px; 130 top: 4px; 131 transform: none; 132 } 133 #profile-dropdown .navbar-nav { 134 display: flex; 135 flex-direction: row; 136 } 137 #profile-dropdown .navbar-nav .dropdown-menu { 138 position: absolute; 139 } 140 #server-logo { 141 width: 160px; 142 height: auto; 143 } 144 } 145 146</style> 147<nav class="navbar sticky-top navbar-expand-lg ps-3 pe-3 py-1 shadow-lg"> 148 <a class="navbar-brand" href="/"> 149 <img id="server-logo" src="/static/resources/appearance-server-logo-light.png" 150 alt='Server logo' 151 width='130' 152 height='44'> 153 </a> 154 <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#server-navbar" aria-controls="server-navbar" aria-expanded="false" aria-label="Toggle navigation"> 155 <span class="navbar-toggler-icon"></span> 156 </button> 157 <div id="profile-dropdown"> 158 <ul class="navbar-nav ms-auto align-items-center"> 159 <li id="navbar-search-li" class="nav-item dropdown d-none d-lg-flex"> 160 <button class="nav-link px-2" type="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false" aria-label="Search" title="Search (Ctrl+K · Ctrl+F · /)"> 161 <i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i> 162 </button> 163 <div class="dropdown-menu dropdown-menu-end p-2" style="width: 320px;"> 164 <div class="search-box position-relative search-box-navbar"> 165 <div class="input-group input-group-sm"> 166 <span class="input-group-text border-0"><i class="bi bi-search"></i></span> 167 <input type="search" autocomplete="off" class="form-control search-box-input border-0" 168 placeholder="Search users, media, notesâ¦" 169 aria-label="Search"> 170 </div> 171 <div class="search-box-results d-none"></div> 172</div> 173 174 </div> 175 </li> 176
176<script> 177 document.getElementById("navbar-search-li")?.addEventListener("shown.bs.dropdown", () => { 178 document.querySelector("#navbar-search-li .search-box-input")?.focus(); 179 }); 180 </script>
180 181 <li class="nav-item"> 182 <a id="nav-update-rocket" class="nav-link d-none position-relative" href="" target="_blank" title="New update available"> 183 <i class="fas fa-rocket color-pulse rotate-up"></i><span class="visually-hidden">New update available</span> 184 </a> 185 </li> 186 <li> 187 <span class='dropdown-item'><div class="theme-switch" id="theme-switch"> 188 <i class="bi bi-brightness-high-fill" id="theme-switch-icon" ></i> 189</div> 190 191<style> 192 193 194.theme-switch { 195 width: 40px; 196 height: 58px; 197 display: flex; 198 align-items: center; 199 justify-content: center; 200 cursor: pointer; 201} 202 203#theme-switch-icon{ 204 font-size: 1.4rem; 205 transition: transform 0.5s, color 0.5s; 206 transform: rotate(0deg); 207} 208 209html[data-bs-theme="dark"] #theme-switch-icon { 210 color: rgb(222, 226, 230); 211} 212 213</style> 214
215<script> 216const themeSwitch = document.getElementById('theme-switch'); 217const themeSwitchIcon = document.getElementById('theme-switch-icon'); 218 219 220let serverLogoId = 'server-logo'; 221function applyTheme(theme) { 222 if (theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) { 223 themeSwitchIcon.classList.remove('bi') 224 themeSwitchIcon.classList.remove('bi-brightness-high-fill'); 225 themeSwitchIcon.classList.add('fas'); 226 themeSwitchIcon.classList.add('fa-moon'); 227 document.documentElement.setAttribute('data-bs-theme', 'dark'); 228 document.querySelector('meta[name=theme-color]').setAttribute('content', '#2B3035F5'); 229 const serverLogo = document.getElementById(serverLogoId); 230 serverLogo.src = `/static/resources/appearance-server-logo-dark.png?` + new Date().getTime(); 231 serverLogo.id = Math.random().toString(36).substring(7); 232 serverLogoId = serverLogo.id; 233 } else { 234 themeSwitchIcon.classList.remove('fas'); 235 themeSwitchIcon.classList.remove('fa-moon'); 236 themeSwitchIcon.classList.add('bi'); 237 themeSwitchIcon.classList.add('bi-brightness-high-fill'); 238 document.documentElement.setAttribute('data-bs-theme', 'light'); 239 document.querySelector('meta[name=theme-color]').setAttribute('content', '#e8e8e8f5'); 240 const serverLogo = document.getElementById(serverLogoId); 241 serverLogo.src = `/static/resources/appearance-server-logo-light.png?` + new Date().getTime(); 242 serverLogo.id = Math.random().toString(36).substring(7); 243 serverLogoId = serverLogo.id; 244 } 245} 246 247function setTheme(theme) { 248 localStorage.setItem('theme', theme); 249 applyTheme(theme); 250} 251 252themeSwitch.addEventListener('click', function() { 253 themeSwitchIcon.style.transform == 'rotate(360deg)'? themeSwitchIcon.style.transform = 'rotate(0deg)' : themeSwitchIcon.style.transform = 'rotate(360deg)'; 254 document.documentElement.getAttribute('data-bs-theme') == 'dark'? setTheme('light'): setTheme('dark'); 255}); 256 257const savedTheme = localStorage.getItem('theme') || 'system'; 258applyTheme(savedTheme); 259 260window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { 261 if (localStorage.getItem('theme') === 'system') { 262 applyTheme('system'); 263 } 264}); 265</script>
265</span> 266 </li> 267 <li class="d-none d-lg-flex"> 268 <button 269 class="nav-link" 270 type="button" 271 aria-controls="intro" 272 aria-selected="false" 273 aria-label="Upload file" 274 onclick="initUploaderModal();" 275 > 276 <span class="fs-2"> 277 <i class="bi bi-cloud-arrow-up-fill" aria-hidden="true"></i> 278 <span class="visually-hidden">Upload file</span> 279 </span> 280 </button> 281 </li> 282 <li class="d-none d-lg-flex"> 283 <a class='nav-link d-none d-lg-flex' href="/login"><i class="fa-solid fa-user fa-lg me-1"></i></a> 284 </li> 285 <li class="nav-item dropdown d-none d-lg-flex" style="display: none !important;" > 286 <a class="nav-link dropdown-toggle d-flex align-items-center" href="#" id="navbarDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> 287 <div class="picture-container-navbar" style="width:40px; height:40px;"></div> 288 </a> 289 <div class="dropdown-menu dropdown-menu-end"> 290 <a href="/profile" class='dropdown-item nav-link'>Profile</a> 291 292 <div class="dropdown-divider"></div> 293 <button class="nav-link" type="button" aria-controls="intro" aria-selected="false" aria-label="Logout" onclick="logout()"> 294 <span><i class="fa-solid fa-right-from-bracket ms-1 me-2"></i>Logout</span> 295 </button> 296 </div> 297 </li> 298 </ul> 299 </div> 300 <div class="collapse navbar-collapse" id="server-navbar"> 301 <ul class="nav navbar-nav"> 302 <li id="registerMenu"> 303 <a class='nav-link' href="/register">REGISTER <span class="sr-only">(current)</span></a> 304 </li> 305 <li id="galleryMenu"> 306 <a class='nav-link' href="/gallery">GALLERY <span class="sr-only">(current)</span></a> 307 </li> 308 <li id="cdnMenu"> 309 <a class='nav-link' href="/media">UPLOAD</a> 310 </li> 311 <li id="docsMenu"> 312 <a class='nav-link' href="/documentation">DOCS <span class="sr-only">(current)</span></a> 313 </li> 314 <li id="directoryMenu"> 315 <a class='nav-link' href="/directory">DIRECTORY <span class="sr-only">(current)</span></a> 316 </li> 317 <li id="relayMenu"> 318 <a class='nav-link' href="/relay">RELAY <span class="sr-only">(current)</span></a> 319 </li> 320 <li id="converterMenu"> 321 <a class='nav-link' href="/converter">CONVERTER <span class="sr-only">(current)</span></a> 322 </li> 323 </ul> 324 </div> 325</nav> 326<div class="modal fade" id="uploader-modal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="uploader-modal" aria-hidden="true"> 327 <div class="modal-dialog modal-dialog-centered modal-lg modal-fullscreen-lg-down" > 328 <div class="modal-content"> 329 <div class="modal-header"> 330 <h5 class="modal-title">Upload files</h5> 331 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> 332 </div> 333 <div class="modal-body"> 334 <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/bootstrap-table.min.css" rel="stylesheet"> 335<link rel="stylesheet" href="/static/css/table.css"> 336<link rel="stylesheet" href="/static/css/form.css"> 337<div id="navbar-uploader" class="col-12 text-center mx-auto z-1"> 338 <div id="navbar-drop-zone" class="card text-center p-5 default-bg m-2 lead" role="button"><span>Drag and drop files here or click to browse</span></div> 339 <input type="file" id="navbar-file-input" class="d-none" multiple> 340 <table 341 id="navbar-file-table" 342 class="table d-none"> 343 <thead> 344 <tr> 345 </tr> 346 </thead> 347 <tbody id="navbar-file-table-body"></tbody> 348 </table> 349 <div id="navbar-account-message" class="d-none"> 350 <p class="text-center lead">Don't have an account? <a href="/register" class="link-body-emphasis link-offset-3 link-underline-opacity-25 link-underline-opacity-75-hover">Register</a> to manage your recently uploaded files. 351 </p> 352 </div> 353 <div id="navbar-guest-message" class="d-none"> 354 <p class="text-center lead">Upload files using a <a href="https://nostr.net/#nip-07-browser-extensions" class="link-body-emphasis link-offset-3 link-un
354derline-opacity-25 link-underline-opacity-75-hover" target="_blank">browser extension</a>. You can also <a href="/login" class="link-body-emphasis link-offset-3 link-underline-opacity-25 link-underline-opacity-75-hover">Login</a> or <a href="/register" class="link-body-emphasis link-offset-3 link-underline-opacity-25 link-underline-opacity-75-hover">register</a> to manage your files. 355 </p> 356 </div> 357 <div id="navbar-file-table-btns" class="text-center"> 358 <div id="navbar-guest-buttons" class="mt-3 d-none"> 359 <div class="d-grid w-300 mx-auto mt-3 mb-3"> 360 <button class="btn btn-lg btn-primary" id="navbar-file-table-btn-nip07" type="button"><i class="nostr-logo me-2"></i>Browser extension upload</button> 361 </div> 362 </div> 363 <div id="navbar-logged-buttons" class="d-none"> 364 <div class="d-grid w-300 mx-auto mt-3 mb-3"> 365 <button id="navbar-file-table-btn-upload" class="btn btn-lg btn-primary"><i class="fa-solid fa-cloud-arrow-up me-3"></i>Upload</button> 366 </div> 367 </div> 368 </div> 369 370 <p class="mb-4 mt-3">By using our services you agree the <a id="navbar-tos-link" class=" link-offset-2 link-underline-opacity-25" href='/api/v2/tos'>Terms of Service</a></p> 371 372 373 </div> 374<div class="modal fade" id="navbar-payment-modal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="payment-modal" aria-hidden="true"> 375 <div class="modal-dialog modal-dialog-centered modal-md modal-fullscreen-lg-down" > 376 <div id="navbar-payment-info" class="modal-content"> 377 <div class="modal-header"> 378 <h5 class="modal-title" id="navbar-payment-modal-title"></h5> 379 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> 380 </div> 381 <div class="modal-body text-center" id="navbar-payment-modal-body"> 382 <div id="navbar-payment-qr"></div> 383 <h1 id="navbar-payment-amount" class="lead mt-2"></h1> 384 <div id="navbar-payment-link" class="mt-1 mb-4 lead"></div> 385 <div id="navbar-payment-waiting"> 386 <span id="navbar-payment-spinner"> 387 <div class="spinner-border" role="status"> 388 <span class="visually-hidden">Loading...</span> 389 </div> 390 </span> 391 <span class="mt-3 d-block">Waiting for payment...</span> 392 </div> 393 <div id="navbar-payment-success" class="p-3"> 394 <i class="bi bi-check-circle text-success" style="font-size: 3rem;"></i> 395 <p class="lead mt-4">Payment processed successfully! ð¥³</p> 396 </div> 397 <span id="navbar-payment-request" class="d-none"></span> 398 <span id="navbar-payment-preimage" class="d-none"></span> 399 </div> 400 <div id="navbar-modalSwitch-footer" class="modal-footer"> 401 <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> 402 </div> 403 </div> 404 </div> 405</div>
406<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
vendor: 1 bytes, line 406
406
407<script> 408 409 410(function() { 411 const instance = 'navbar' + '-'; 412 const identifier = ''; 413 const filesArray = []; 414 415 const oneFile = 'false' === "true"; 416 417 const fetchFileServerInfo = async (file, authEvent) => { 418 419 if (!file) { 420 return { "status": "error", "message": "No file selected", "satoshi": 0 }; 421 } 422 423 const fileExtension = file.name.split('.').pop().toLowerCase(); 424 let fileType = file.type; // Usar una variable intermedia para almacenar el tipo 425 426 if (!fileType) { 427 try { 428 429 const res = await fetch('https://cdn.jsdelivr.net/gh/jshttp/mime-db/db.json'); 430 const data = await res.json(); 431 432 if (!data) { 433 showMessage("Failed to load MIME types", "alert-danger", false); 434 return { "status": "error", "message": "Failed to load MIME types", "satoshi": 0 }; 435 } 436 437 const possibleTypes = Object.keys(data).filter(key => data[key].extensions && data[key].extensions.includes(fileExtension)); 438 fileType = getPreferredMimeType(fileExtension,possibleTypes); 439 440 } catch (error) { 441 showMessage("Failed to load MIME types", "alert-danger", false); 442 return { "status": "error", "message": "Failed to load MIME types", "satoshi": 0 }; 443 } 444 } 445 446 if (!fileType) { 447 showMessage("Invalid file type", "alert-danger", false); 448 return { "filename": file.name, "url": "" }; 449 } 450 451 try { 452 453 const response = await fetch(window.location.hostname.includes("cdn") ? '/upload' : '/api/v2/media/upload', { 454 method: "HEAD", 455 headers: { 456 "Authorization": "Nostr " + authEvent, 457 "X-Content-Length": file.size.toString(), 458 "X-Content-Type": fileType, 459 "X-SHA-256": `${file.fileData.hash}`, 460 "X-Content-Transform": file.fileData.transform, // NOT FROM BUD-07 SPEC. 461 }, 462 }); 463 const data = { 464 status : response.status, 465 message : response.headers.get('X-Reason'), 466 payment_request : response.headers.get('X-Lightning'), 467 satoshi : response.headers.get('X-Lightning-amount'), 468 } 469 return data; 470 471 472 } catch (error) { 473 console.error(error.message) 474 return { "status": "error", "message": error.message, "satoshi": 0 }; 475 } 476 477 } 478 479 const fetchFileServer = async (file, authEvent = "", method = "", showMessages = false) => { 480 481 if (!method) { 482 return; 483 } 484 485 if (!file) { 486 showMessage("No file selected", "alert-danger", false); 487 return { "filename": "", "url": "" }; 488 } 489 490 let headers = {}; 491 headers["authorization"] = "Nostr " + authEvent; 492 method == "PUT" ? headers["Content-Type"] = file.type : null; 493 if (file.fileData.invoice != "" && file.fileData.preimage != ""){ 494 headers["X-Lightning"] = `${file.fileData.preimage}`; 495 // headers["X-Lightning"] = `8358ad49931cef976f6ac21f43bdf053c5d71a0d01337845ca582ec6058eb80f`; 496 } 497 let uploadMessage = null 498 499 if (showMessages) uploadMessage = showMessage(`Uploading file... `, "alert-info", true); 500 501 const formData = new FormData(); 502 formData.append('file', file); 503 instance.startsWith('userSettingspicture') ? formData.append('media_type', 'avatar') : null; 504 instance.startsWith('userSettingsbanner') ? formData.append('media_type', 'banner') : null; 505 506 try { 507 const response = await fetch(method == "PUT" ? window.location.hostname.includes("cdn") ? '/upload' : '/api/v2/media/upload' : window.location.hostname.includes("cdn") ? '/': '/api/v2/media', { 508 method: method, 509 headers: headers, 510 body: method == "PUT" ? file : formData 511 }); 512 513 let serverData = await response.json(); 514 515 if (!response.status.toString().startsWith("2") && !response.status.toString() == "402") { 516 if (showMessages) updateMessage(uploadMessage, '<i class="bi bi-exclamation-circle-fill pe-1"></i>' + serverData.message, "alert-danger"); 517 if (showMessages) hideMessage(uploadMessage, 5000); 518 return { "filename": file.name, "url": "", "invoice": response.headers.get('X-Lightning')}; 519 } 520 521 if (serverData.processing_url && serverData.processing_url != "") { 522 authEvent = { 523 kind: 27235, 524 created_at: Math.floor(Date.now() / 1000), 525 tags: [ 526 ["u", serverData.processing_url], 527 ["method", "GET"] 528 ], 529 content: 'Authorization event for file upload status', 530 }; 531 authEvent = btoa(JSON.stringify(await window.nostr.signEvent(authEvent))); 532 } 533 534 while (serverData.processing_url) { 535 536 await new Promise(resolve => setTimeout(resolve, 1000)); 537 headers["authorization"] = "Nostr " + authEvent; 538 539 const processingResponse = await fetch(serverData.processing_url, { 540 method: "GET", 541 headers: headers 542 }); 543 544 const processingData = await processingResponse.json(); 545 546 if (processingData.status === "processing") { 547 if (showMessages) updateMessage(uploadMessage, `Processing file... ${processingData.percentage}`); 548 } else { 549 serverData.processing_url = ""; 550 } 551 } 552 553 const url = serverData.nip94_event ? serverData.nip94_event.tags.find(tag => tag[0] === "url")[1] : serverData.processing_url ? serverData.processing_url : serverData.url; 554 if (showMessages) updateMessage(uploadMessage, 555 `File uploaded successfully! 556 <button class="btn p-0 pb-1" type="button" aria-controls="intro" aria-selected="false" aria-label="View file"> 557 <a href="${url}" target="_blank"><span><i class="fa-solid fa-link ms-2 me-2"></i><span class="visually-hidden">
557View file</span></span></a> 558 </button> 559 <button class="btn p-0 pb-1" type="button" aria-controls="intro" aria-selected="false" aria-label="Copy to clipboard" onclick="copyToClipboard(this,'${url}')"> 560 <span><i class="fa-solid fa-copy"></i><span class="visually-hidden">Copy to clipboard</span></span> 561 </button> 562 ` 563 ); 564 if (showMessages) hideMessage(uploadMessage, 15000); 565 return { "filename": file.name, "url": url, "invoice": response.headers.get('X-Lightning')}; 566 567 } catch (error) { 568 console.error(error.message); 569 if (showMessages) updateMessage(uploadMessage, '<i class="bi bi-exclamation-circle-fill pe-1"></i>' + error, "alert-danger"); 570 if (showMessages) hideMessage(uploadMessage, 5000); 571 return { "filename": file.name, "url": "", "invoice": "" }; 572 } 573 } 574 575 function getPreferredMimeType(extension, possibleTypes) { 576 let preferredMimeType = null; 577 switch (extension) { 578 case 'stl': 579 preferredMimeType = possibleTypes.includes('model/stl') ? 'model/stl' : null; 580 break; 581 case 'jpg': 582 case 'jpeg': 583 preferredMimeType = possibleTypes.includes('image/jpeg') ? 'image/jpeg' : null; 584 break; 585 case 'png': 586 preferredMimeType = possibleTypes.includes('image/png') ? 'image/png' : null; 587 break; 588 default: 589 preferredMimeType = possibleTypes[0]; 590 break; 591 } 592 return preferredMimeType; 593 } 594 595 document.addEventListener('DOMContentLoaded', async () => { 596 597 if (identifier) { 598 document.getElementById(instance + 'logged-buttons').classList.remove('d-none'); 599 } else { 600 document.getElementById(instance + 'guest-message').classList.remove('d-none'); 601 document.getElementById(instance + 'guest-buttons').classList.remove('d-none'); 602 } 603 604 $('#' + instance + 'file-table').bootstrapTable({ 605 idField: 'id', 606 uniqueId: 'id', 607 pagination: oneFile? false : true, 608 pageSize: 4, 609 showHeader: false, 610 paginationParts: ['pageInfoShort', 'pageList'], 611 sortName: 'id', 612 sortOrder: 'desc', 613 columns: [ 614 { 615 field: 'id', 616 title: 'ID', 617 visible: false 618 }, 619 { 620 field: 'fileName', 621 title: 'File Name', 622 formatter: function(value, row, index) { 623 return fileNameFormatter(value, row, index, filesArray); 624 } 625 }, 626 { 627 field: 'fileType', 628 title: 'File Type', 629 visible: false 630 }, 631 { 632 field: 'transform', 633 title: 'Resize', 634 formatter: function(value, row, index) { 635 return transformFormatter(value, row, index, filesArray); 636 } 637 }, 638 { 639 field: 'fileSize', 640 title: 'File Size' 641 }, 642 { 643 field: 'invoice', 644 title: 'Invoice', 645 formatter: function(value, row, index) { 646 return invoiceFormatter(value, row, index, filesArray); 647 } 648 }, 649 { 650 field: 'satoshi', 651 title: 'Satoshi', 652 formatter: function(value, row, index) { 653 return satoshiFormatter(value, row, index); 654 }, 655 class: 'text-end' 656 }, 657 { 658 field: 'isPaid', 659 title: 'isPaid', 660 visible: false 661 }, 662 { 663 field: 'action', 664 title: 'Action', 665 formatter: function(value, row, index) { 666 return actionFormatter(value, row, index); 667 }, 668 class: 'text-center' 669 }, 670 { 671 field: 'accepted', 672 title: 'Accepted', 673 visible: false 674 } 675 ] 676 }); 677 678 if (oneFile) { 679 document.getElementById(instance + 'file-table-btn-upload').classList.add('d-none'); 680 } 681 682 }); 683 684 const dropZone = document.getElementById(instance + 'drop-zone'); 685 const fileInput = document.getElementById(instance + 'file-input'); 686
687 let dropZoneOriginalBackground = dropZone.style.backgroundColor; 688 dropZone.addEventListener('dragover', (e) => { 689 e.preventDefault(); 690 dropZone.style.backgroundColor = 'rgb(248 249 250 / 30%)'; 691 }); 692 693 dropZone.addEventListener('dragleave', () => { 694 dropZone.style.backgroundColor = dropZoneOriginalBackground 695 }); 696 697 dropZone.addEventListener('drop', (e) => { 698 e.preventDefault(); 699 dropZone.style.backgroundColor = dropZoneOriginalBackground; 700 const files = Array.from(e.dataTransfer.files); 701 window[`${instance}handleFiles`](files); 702 }); 703 704 dropZone.addEventListener('click', () => { 705 fileInput.click(); 706 }); 707 708 fileInput.addEventListener('change', (e) => { 709 const files = Array.from(e.target.files); 710 window[`${instance}handleFiles`](files); 711 }); 712 713 document.getElementById('navbar-file-table-btn-upload').addEventListener('click', async function() { 714 await window[`${instance}uploadFiles`](window[`${instance}filesArray`]); 715 }); 716 717 document.getElementById('navbar-file-table-btn-nip07').addEventListener('click', async function() { 718 await window[`${instance}uploadFiles`](window[`${instance}filesArray`]); 719 }); 720 721 let fileCounter = 0; 722 723 async function handleFiles(files) { 724 725 if (files.length === 0) { 726 return; 727 } 728 729 document.getElementById(instance + 'file-table').classList.remove('d-none'); 730 if(!oneFile) document.getElementById(instance + 'file-table-btn-upload').classList.remove('d-none'); 731 if(!oneFile) document.getElementById(instance + 'file-table-btn-nip07').classList.remove('d-none'); 732 733 for (const file of files) { 734 735 if (oneFile && filesArray.length > 0) { 736 filesArray.splice(0, filesArray.length); 737 $('#' + instance + 'file-table').bootstrapTable('load', filesArray.map(file => file.fileData)); 738 } 739 740 if (!filesArray.some(f => f.fileData.originalName === file.name && f.size === file.size)) { 741 const uniqueId = fileCounter++; 742 743 filesArray.push(file); 744 745 const fileData = { 746 id: instance + uniqueId, 747 originalName: file.name, 748 fileName: file.name.length > 30 ? file.name.substring(0, 30) + '...' : file.name, 749 fileType: file.type, 750 transform: file.type.startsWith('image/') || file.type.startsWith('video/') ? 1 : 0, 751 fileSize: (file.size / 1024).toFixed(2) + ' KB', 752 action: 'init', 753 accepted: true, 754 satoshi: -1, 755 invoice: null, 756 isPaid: false, 757 hash: null, 758 }; 759 760 fileData.hash = await crypto.subtle.digest('SHA-256', await file.arrayBuffer()).then(buffer => { 761 return Array.from(new Uint8Array(buffer)).map(b => b.toString(16).padStart(2, '0')).join(''); 762 }); 763 764 file.fileData = fileData; 765 766 const fileServerInfo = await getFileServerInfo(file); 767 if (fileServerInfo == true) { 768 $('#' + instance + 'file-table').bootstrapTable('load', filesArray.map(file => file.fileData)); 769 } 770 if (oneFile && fileServerInfo == true) { 771 if (file.fileData.satoshi > 0) { 772 handlePayment(file.fileData.invoice, file.fileData.satoshi, file.fileData.id, instance).then(async (result) => { 773 if (result) { 774 file.fileData.isPaid = true; 775 $('#' + instance + 'file-table').bootstrapTable('load', filesArray.map(file => file.fileData)); 776 await window[`${instance}uploadFiles`](window[`${instance}filesArray`]); 777 }else{ 778 const fileIndex = filesArray.findIndex(f => f.fileData.id === file.fileData.id); 779 if (fileIndex !== -1) { 780 filesArray.splice(fileIndex, 1); 781 $('#' + instance + 'file-table').bootstrapTable('load', filesArray.map(file => file.fileData)); 782 } 783 } 784 }); 785 }else{ 786 await window[`${instance}uploadFiles`](window[`${instance}filesArray`]); 787 } 788 } 789 790 } 791 } 792 793 $(document).on('click', '.delete-btn', function() { 794 const fileId = $(this).data('file-id'); 795 const fileIndex = filesArray.findIndex(file => file.fileData.id === fileId); 796 if (fileIndex !== -1) { 797 filesArray.splice(fileIndex, 1); 798 $('#' + instance + 'file-table').bootstrapTable('load', filesArray.map(file => file.fileData)); 799 } 800 }); 801 802 $(document).on('change', '.form-check-input',async function() { 803 const fileId = $(this).attr('id').split('-').slice(0, -1).join('-') 804 const fileIndex = filesArray.findIndex(file => file.fileData.id === fileId); 805 if (fileIndex !== -1) { 806 807 $(this).prop('disabled', true); 808 filesArray[fileIndex].fileData.transform = this.checked ? 1 : 0; 809 filesArray[fileIndex].fileData.satoshi = -1 // Reset Satoshi value; 810 $('#' + instance + 'file-table').bootstrapTable('load', filesArray.map(file => file.fileData)); 811 await getFileServerInfo(filesArray[fileIndex]).then(() => { 812 $(this).prop('disabled', false); 813 }); 814 } 815 }); 816 } 817 818 819 async function uploadFiles(f) { 820 821 if (f.length === 0) { 822 showMessage("No files selected to upload ð¤", "alert-warning", false, 500); 823 return; 824 } 825 826 for (const file of f) { 827 828 if (file.fileData.processed || file.fileData.accepted === false) continue; 829 830 file.fileData.action = 'processing'; 831 $('#' + instance + 'file-table').bootstrapTable('load', f.map(file => file.fileData)); 832 833 let authEvent = null; 834 let method = $('#' + instance + 'file-table').bootstrapTable('getRowByUniqueId', file.fileData.id).transform == 1 ? 'POST' : 'PUT'; 835 836 if (!window.nostr) { 837 showMessage("â ï¸ NIP07 browser extension not found", "alert-danger", false); 838 file.fileData.satoshi = -2; 839 file.fileData.action = 'error'; 840 $('#' + instance + 'file-table').bootstrapTable('load', f.map(file => file.fileData)); 841 return; 842 } 843 844 try { 845 const pubKey = await window.nostr.getPublicKey(); 846 847 if (!pubKey){ 848 showMessage("â ï¸ NIP07 browser extension not found", "alert-danger", false);
849 console.debug('NIP07 public key not found'); 850 return; 851 } 852 853 854 if (method == 'POST') { 855 authEvent= { 856 857 kind: 27235, 858 created_at: Math.floor(Date.now() / 1000), 859 tags: [ 860 ["u", window.location.hostname.includes("cdn") 861 ? window.location.protocol + "//" + window.location.hostname + '/api/v2/media/' 862 : window.location.protocol + "//" + window.location.hostname + '/api/v2/media'], 863 ["method", "POST"], 864 ["payload", file.fileData.hash], 865 ], 866 content: 'NIP98 authorization event for file upload', 867 }; 868 869 }else{ 870 871 authEvent = { 872 873 kind: 24242, 874 created_at: Math.floor(Date.now() / 1000), 875 tags: [ 876 ["t", 'upload'], 877 ["x", file.fileData.hash], 878 ["expiration", (Math.floor(Date.now() / 1000) + 3600).toString()], 879 ], 880 content: 'Blossom authorization event for file upload', 881 }; 882 } 883 884 authEvent = btoa(JSON.stringify(await window.nostr.signEvent(authEvent))); 885 886 } catch (error) { 887 console.error(error); 888 } 889 890 const result = await fetchFileServer(file, authEvent, method); 891 if (!result || !result.url) { 892 file.fileData.accepted = false; 893 file.fileData.satoshi = -2; 894 file.fileData.action = 'error'; 895 $('#' + instance + 'file-table').bootstrapTable('load', f.map(file => file.fileData)); 896 continue; 897 } 898 899 result.invoice != null ? file.fileData.invoice = result.invoice : null; 900 file.fileData.action = 'success'; 901 file.fileData.processed = true; 902 file.fileData.fileName = '<a class="link-offset-2 link-underline-opacity-25" href="' + result.url + '" target="_blank">' + "..." + result.url.slice(-30) + '</a>' 903 $('#' + instance + 'file-table').bootstrapTable('load', f.map(file => file.fileData)); 904 905 const uploadEvent = new CustomEvent('fileUploaded', { 906 detail: { 907 url: result.url, 908 file: file, 909 instance: instance 910 } 911 }); 912 document.dispatchEvent(uploadEvent); 913 914 } 915 916 if (!identifier) { 917 document.getElementById(instance + 'guest-message').classList.add('d-none'); 918 document.getElementById(instance + 'account-message').classList.remove('d-none'); 919 } 920 921 } 922 923 function transformFormatter(value, row, index, filesArray) { 924 const fileIndex = filesArray.findIndex(file =>
924 file.fileData.id == row.id); 925 926 return ` 927 <div class="form-check form-switch d-flex"> 928 <input type="checkbox" class="form-check-input" id="${row.id}-transform" 929 ${filesArray[fileIndex].fileData.status == 'error' || 930 filesArray[fileIndex].fileData.processed == true || 931 (row.fileType.startsWith('image/') === false && row.fileType.startsWith('video/') === false) 932 ? 'disabled' : ''} 933 ${filesArray[fileIndex].fileData.transform == true ? 'checked' : ''} control-id="ControlID-5"> 934 <label class="form-check-label mt-1" for="${row.id}-transform">Resize</label> 935 </div>`; 936 } 937 938 function satoshiFormatter(value, row, index) { 939 940 if (value == 0) return 'Free' + '<i class="fa-solid fa-star ms-2" style="color:#a06bc9"></i>' 941 if (value == -1) return '<div class="spinner-border spinner-border-sm" role="status">' 942 '<span class="visually-hidden">Loading...</span>' + 943 '</div>' 944 if (value == -2) return '<span class="text-danger">Failed</span>'; 945 return value + ' <i class="fa-solid fa-bolt text-warning"></i>' 946 } 947 948 function invoiceFormatter(value, row, index) { 949 if (row.isPaid === true) return '<span>Paid<i class="fa-solid fa-check ms-2"></i></span>'; 950 return `<button class="btn btn-sm btn-primary" ${value == null || row.satoshi <= 0 ? 'disabled' : ''} onclick="handlePayment('${value}', ${row.satoshi}, '${row.id}','${instance}')">Pay</button>`; 951 } 952 953 function fileNameFormatter(value, row, index, filesArray) { 954 const fileIndex = filesArray.findIndex(file =>
954 file.fileData.id == row.id); 955 956 if (filesArray[fileIndex].fileData.accepted === false) { 957 return '<span class="text-danger">' + value + '</span>'; 958 } 959 return value; 960 } 961 962 function actionFormatter(value, row, index) { 963 if (value == 'processing') { 964 return '<div class="spinner-border spinner-border-sm" role="status">' + 965 '<span class="visually-hidden">Loading...</span>' + 966 '</div>'; 967 } 968 if (value == 'success') { 969 return '<button class="btn btn-sm text-white" data-file-id="' + row.id + '"><i class="fa-solid fa-check"></i></button>'; 970 } 971 972 return '<button class="btn btn-sm text-danger delete-btn" data-file-id="' + row.id + '"><i class="fa-solid fa-xmark"></i></button>'; 973 } 974 975 function handlePayment(value, satoshi, id, i) { 976 return new Promise((resolve, reject) => { 977 const filesArr = window[`${i}filesArray`]; 978 const row = $('#' + i + 'file-table').bootstrapTable('getRowByUniqueId', id); 979 initPaymentModal(value, row.satoshi, i).then(result => { 980 if (result != "") { 981 row.isPaid = true; 982 row.preimage = result; 983 $('#' + i + 'table').bootstrapTable('updateRow', { 984 index: id, 985 row: row 986 }); 987 $('#' + i + 'file-table').bootstrapTable('load', filesArr.map(file => file.fileData)); 988 resolve(true); 989 }else{ 990 resolve(false); 991 } 992 }); 993 }); 994 } 995 996 const getFileServerInfo = async (file) => { 997 998 if (!window.nostr) { 999 showMessage("â ï¸ NIP07 browser extension not found", "alert-danger", false); 1000 file.fileData.satoshi = -2; 1001 file.fileData.action = 'error'; 1002 $('#' + instance + 'file-table').bootstrapTable('load', f.map(file => file.fileData)); 1003 return; 1004 } 1005 1006 try { 1007 const pubKey = await window.nostr.getPublicKey(); 1008 1009 if (!pubKey){console.debug('NIP07 public key not found'); 1010 return; 1011 } 1012 1013 authEvent = { 1014 1015 kind: 24242, 1016 created_at: Math.floor(Date.now() / 1000), 1017 tags: [ 1018 ["t", 'upload'], 1019 ["x", file.fileData.hash], 1020 ["expiration", (Math.floor(Date.now() / 1000) + 3600).toString()], 1021 ], 1022 content: 'Blossom authorization event for file upload', 1023 }; 1024 1025 authEvent = btoa(JSON.stringify(await window.nostr.signEvent(authEvent))); 1026 1027 } catch (error) { 1028 console.error(error); 1029 } 1030 1031 const serverInfo = await fetchFileServerInfo(file, authEvent); 1032 console.debug(serverInfo) 1033 if (!serverInfo || !serverInfo?.status?.toString().startsWith(2)){ 1034 1035 // 402 Payment Required 1036 if (serverInfo.status == 402) { 1037 file.fileData.accepted = true; 1038 file.fileData.invoice = serverInfo.payment_request 1039 file.fileData.satoshi = serverInfo.satoshi; 1040 $('#' + instance + 'file-table').bootstrapTable('load', filesArray.map(file => file.fileData)); 1041 return true; 1042 }else{ 1043 file.fileData.accepted = false; 1044 file.fileData.satoshi = -2; 1045 console.error(`Failed to upload: ${file.name}`); 1046 $('#' + instance + 'file-table').bootstrapTable('load', filesArray.map(file => file.fileData)); 1047 return false; 1048 } 1049 1050 }else{ 1051 file.fileData.accepted = true; 1052 file.fileData.satoshi = 0; 1053 $('#' + instance + 'file-table').bootstrapTable('load', filesArray.map(file => file.fileData)); 1054 return true; 1055 } 1056 1057 } 1058 1059window.handlePayment = handlePayment; 1060window.transformFormatter = transformFormatter; 1061window.satoshiFormatter = satoshiFormatter; 1062window.invoiceFormatter = invoiceFormatter; 1063window.fileNameFormatter = fileNameFormatter; 1064window.actionFormatter = actionFormatter; 1065window.instance = instance; 1066 1067window[`${instance}uploadFiles`] = uploadFiles; 1068window[`${instance}filesArray`] = filesArray; 1069window[`${instance}handleFiles`] = handleFiles; 1070 1071})(); 1072 1073</script>
1073 1074 1075<style> 1076#cdn-drop-zone { 1077 --bs-text-opacity: 1; 1078 color: rgba(255, 255, 255, .5) !important; 1079} 1080</style> 1081 1082 </div> 1083 </div> 1084 </div> 1085</div> 1086 1087<style> 1088 html[data-bs-theme="light"] { 1089 #drop-zone { 1090 color:rgb(145, 145, 145) !important; 1091 } 1092 } 1093 1094 html[data-bs-theme="dark"] { 1095 #drop-zone { 1096 color:rgb(145, 145, 145) !important; 1097 } 1098 } 1099</style>
1100<script> 1101 1102 1103const activeModules = [{"enabled":true,"path":"/nostraddress","methods":["GET"],"description":"Returns the pubkey of a nostr address name","name":"nostraddress"},{"enabled":true,"path":"/media","methods":["GET","POST","PUT","DELETE"],"description":"This module handles media uploads and downloads.","name":"media"},{"enabled":true,"path":"/lightningaddress","methods":["GET","PUT","DELETE"],"description":"Lightning redirections for nostr addresses.","name":"lightning"},{"enabled":true,"path":"/verify","methods":["POST"],"description":"This module can verify a nostr note integrity.","name":"verify"},{"enabled":true,"path":"/register","methods":["POST"],"description":"Register new usernames from trusted pubkeys.","name":"register"},{"enabled":true,"path":"/domains","methods":["GET"],"description":"Get the list of registered domains.","name":"domains"},{"enabled":true,"path":"/admin","methods":["GET","POST"],"description":"Admin API, reboot, status, etc.","name":"admin"},{"enabled":true,"path":"/","methods":["GET","POST"],"description":"Frontend, login, register, etc.","name":"frontend"},{"name":"relay","enabled":true,"path":"/relay","methods":["WebSocket","GET"],"description":"This module handles the Nostr relay"},{"name":"security","enabled":true,"path":"","methods":[],"description":"This module handles security, ip logs, automatic ban, etc."},{"name":"logger","enabled":true,"path":"/","methods":["Library"],"description":"This module manages the server logs engine."}]; 1104 1105 1106 if (!activeModules.some(mod => mod.name === 'register')) { 1107 document.getElementById('registerMenu').classList.add('d-none'); 1108 document.getElementById('directoryMenu').classList.add('d-none'); 1109 } 1110 if (!activeModules.some(mod => mod.name === 'media')) { 1111 document.getElementById('galleryMenu').classList.add('d-none'); 1112 document.getElementById('cdnMenu').classList.add('d-none'); 1113 } 1114 if (!activeModules.some(mod => mod.name === 'relay')) { 1115 document.getElementById('relayMenu').classList.add('d-none'); 1116 } 1117</script>
1117 1118 <main> 1119 <div class="row"> 1120 <div class="col-sm-9 col-md-8 col-lg-6 col-xl-5 col-xxl-4 mx-auto"> 1121 <div class="text-center"> 1122 <h1 class="display-4 mb-1 pb-2 font-weight-normal gradient-text">Login</h1> 1123 </div> 1124 <div id="otc-login"> 1125 <form class="form" onkeydown=" if (event.keyCode === 13) {event.preventDefault(); logInOtc()}"> 1126 <div class="text-center"> 1127 <p class="lead">Login from nostr using a one-time password or using a <a href="https://nostr.net/#nip-07-browser-extensions" class="link-body-emphasis link-offset-3 link-underline-opacity-25 link-underline-opacity-75-hover" target="_blank">browser extension.</a> 1128 1129 </p> 1130 </div> 1131 <div class="input-group"> 1132 <input type="text" data-otc='pubkey' class="form-control" id="input-otc-login" placeholder="public key (npub or hex)" required> 1133 <div class="input-group-text"> 1134 <div class="form-check form-switch"> 1135 <label for="rememberme" class="ps-1">Stay signed</label> 1136 <input class="form-check-input" type="checkbox" value="true" id="rememberme_otc" name="rememberme"> 1137 </div> 1138 </div> 1139 </div> 1140 <div class="d-grid mb-3 mt-3 w-300 mx-auto"> 1141 <button class="btn btn-lg btn-primary" name="otc_submit" id="otc_submit" type="button" onclick="logInOtc()"><i class="nostr-logo me-2" ></i>One time login-code</button> 1142 </div> 1143 </form> 1144 <div class=" hr-text w-50 mx-auto">or</div> 1145 </div> 1146 <div id="extension-login" class="mt-3"> 1147 <form class="form"> 1148 <div class="d-grid w-300 mx-auto"> 1149 <button class="btn btn-lg btn-primary" name="NIP07_submit" id="NIP07_submit" type="button" onclick="logIn('nostr', $('#rememberme:checked').val(), 'NIP07_submit')"><i class="nostr-logo me-2"></i>Browser extension</button> 1150 </div> 1151 </form> 1152 </div> 1153 <div class="text-center"> 1154 <p id="legacy-login-text" class="lead">Login with <a id="legacy-login-link" class="link-body-emphasis link-offset-3 link-underline-opacity-25 link-underline-opacity-75-hover" href="#">password</a></p> 1155 </div> 1156 <div class="d-grid d-none" id="legacy-login"> 1157 <div class="text-center"> 1158 <p class="lead">Login with your username and password</p> 1159 </div> 1160 <form class="form " onkeydown=" if (event.keyCode === 13) {logIn('legacy', $('#rememberme_legacy:checked').val(),'legacy_submit');}"> 1161 <div class="input-group mb-2"> 1162 <label for="username" class="sr-only">
1162Username</label> 1163 <input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus autocomplete="username"> 1164 <div class="input-group-text"> 1165 <div class="form-check form-switch"> 1166 <label for="rememberme" class="ps-1">Stay signed</label> 1167 <input class="form-check-input" type="checkbox" value="true" id="rememberme_legacy" name="rememberme"> 1168 </div> 1169 </div> 1170 </div> 1171 <label for="password" class="sr-only">Password</label> 1172 <div class="input-group"> 1173 <input type="password" id="password" name="password" class="form-control" placeholder="Password" autocomplete="current-password"> 1174 <button type="button" class="toggle-password d-none" 1175 aria-label="Show password as plain text. Warning: this will display your password on the screen."> 1176 </button> 1177 </div> 1178 <div class="d-grid mb-1 w-300 mt-3 mx-auto"> 1179 <button class="btn btn-lg btn-primary" name="legacy_submit" id="legacy_submit" type="button" onclick="logIn('legacy', $('#rememberme:checked').val(),'legacy_submit')"><i class="bi bi-box-arrow-in-right me-3"></i>Login</button> 1180 </div> 1181 </form> 1182 <div class="text-center"> 1183 <p id="nostr-login-text" class="lead">Login with <a id="nostr-login-link" class="link-body-emphasis link-offset-3 link-underline-opacity-25 link-underline-opacity-75-hover" href="#">nostr</a></p> 1184 </div> 1185 </div> 1186</div>
1187<script src="/static/js/form.js"></script>
vendor: 1 bytes, line 1187
1187
1188<script> 1189 1190const logIn = async (type, rememberMe, buttonId) =>{ 1191 1192 console.debug('logIn', type, rememberMe, buttonId); 1193 const buttonText = document.getElementById(buttonId).innerHTML; 1194 document.getElementById(buttonId).innerHTML = '<span class="spinner-border spinner-border-sm pe-1" role="status" aria-hidden="true"></span> Logging in...'; 1195 1196 if (type === 'legacy') { 1197 await fetchServer(JSON.stringify({username: document.getElementById('username').value, 1198 password: document.getElementById('password').value, 1199 rememberMe: rememberMe} 1200 ), 'login').then(response => { 1201 response.json().then(data => { 1202 if (data.status === "success") { 1203 window.location.href = "/profile"; 1204 } else { 1205 initAlertModal("#login", data.message); 1206 document.getElementById(buttonId).innerHTML = buttonText; 1207 } 1208 }); 1209 }); 1210 } 1211 1212 if (type === 'nostr') { 1213 1214 if (!window.nostr) { 1215 initAlertModal("#login", 'NIP07 browser extension not found'); 1216 document.getElementById(buttonId).innerHTML = buttonText; 1217 return; 1218 } 1219 1220 try { 1221 const pubKey = await window.nostr.getPublicKey(); 1222 1223 if (!pubKey){console.debug('NIP07 public key not found');return;} 1224 1225 let event = { 1226 kind: 27235, 1227 created_at: Math.floor(Date.now() / 1000), 1228 tags: [ 1229 ["u", window.location.href ], 1230 ["method", "POST"], 1231 ["cookie", rememberMe? "true": "false"] 1232 ], 1233 content: '' 1234 }; 1235 const loginEvent = JSON.stringify(await window.nostr.signEvent(event)); 1236 1237 if (loginEvent){ 1238 await fetchServer(loginEvent,'login').then(response => { 1239 response.json().then(data => { 1240 if (data.status === "success") { 1241 window.location.href = "/profile"; 1242 } else { 1243 initAlertModal("#login", data.message); 1244 document.getElementById(buttonId).innerHTML = buttonText; 1245 } 1246 }); 1247 }); 1248 } 1249 } catch (error) { 1250 console.error(error); 1251 document.getElementById(buttonId).innerHTML = buttonText; 1252 } 1253 } 1254 } 1255 1256 const fetchServer = async (data, url) => { 1257 try { 1258 const response = await fetch(url, {method: 'POST',headers: {'Content-Type': 'application/json'},body: data}); 1259 return response; 1260 } catch (error) { 1261 initAlertModal("#login", error); 1262 } 1263 } 1264 1265 1266 const logInOtc = async () => { 1267 if($("#input-otc-login").attr("data-otc") === "pubkey"){ 1268 await fetchServer(JSON.stringify({}), `login/${document.getElementById('input-otc-login').value}`).then(response => { 1269 response.json().then(data => { 1270 if (data.status === 'success') { 1271 document.getElementById('input-otc-login').value = ''; 1272 $("#input-otc-login").attr("data-otc", "code").attr("placeholder", "Enter verification code"); 1273 $("#otc_submit").text("Verify and login"); 1274 } else { 1275 initAlertModal("#login", data.message); 1276 } 1277 }); 1278 }); 1279 } else { 1280 1281 const buttonText = document.getElementById('otc_submit').innerHTML; 1282 document.getElementById('otc_submit').innerHTML = '<span class="spinner-border spinner-border-sm pe-1" role="status" aria-hidden="true"></span> Logging in...'; 1283 1284 let data = { 1285 otc: document.getElementById('input-otc-login').value, 1286 rememberMe: document.getElementById('rememberme_otc').checked 1287 } 1288 1289 await fetchServer(JSON.stringify(data), `login`).then(response => { 1290 response.json().then(data => { 1291 if (data.status === 'success') { 1292 window.location.href = "/profile"; 1293 } else { 1294 initAlertModal("#login", data.message); 1295 document.getElementById('otc_submit').innerHTML = buttonText; 1296 }}); 1297 }); 1298 } 1299 } 1300 1301 // Show legacy login form 1302 document.getElementById('legacy-login-link').addEventListener('click', function(event) { 1303 event.preventDefault(); 1304 document.getElementById('legacy-login').classList.remove('d-none'); 1305 document.getElementById('legacy-login-text').classList.add('d-none'); 1306 document.getElementById('otc-login').classList.add('d-none'); 1307 document.getElementById('extension-login').classList.add('d-none'); 1308 }); 1309 1310 // Show nostr login form 1311 document.getElementById('nostr-login-link').addEventListener('click', function(event) { 1312 event.preventDefault(); 1313 document.getElementById('legacy-login').classList.add('d-none'); 1314 document.getElementById('legacy-login-text').classList.remove('d-none'); 1315 document.getElementById('otc-login').classList.remove('d-none'); 1316 document.getElementById('extension-login').classList.remove('d-none'); 1317 }); 1318 1319</script>
1319 1320 </main> 1321 <footer class="text-center mt-auto position-relative-md z-3 shadow-lg"> 1322 <div class="text-center p-3 d-none d-lg-block"> 1323 <span>v0.7.1.0159 </span> 1324 <span>| Content released under</span> <a class="text-secondary text-decoration-none" href="https://mit-license.org/">MIT license</a> 1325 <span>| <a class="text-secondary text-decoration-none" href="https://github.com/quentintaranpino/nostrcheck-server/"> <i class="fa-brands fa-github ms-1"></i> Source code</a></span> 1326 <span>| <a class="text-secondary text-decoration-none" href="/api/v2/privacy">Privacy</a></span> 1327 <span>- <a class="text-secondary text-decoration-none" href="/api/v2/legal">Legal</a></span> 1328 <span>- <a class="text-secondary text-decoration-none" href="/api/v2/tos">TOS</a></span> 1329 1330 </div> 1331 <nav class="navbar navbar-light navbar-expand fixed-bottom d-lg-none d-xl-none pb-3 shadow-lg"> 1332 <ul class="nav nav-justified w-100 align-items-center" id="myTab" role="tablist"> 1333 <li style="display: none;" class='nav-item' role="presentation"> 1334 <button class="nav-link" type="button" aria-controls="intro" aria-selected="false" aria-label="Dashboard"> 1335 <span class="fs-2"><a href="dashboard" role="button"><i class="bi bi-speedometer" aria-hidden="true"></i><span class="visually-hidden">Dashboard</span></a></span> 1336 </button> 1337 </li> 1338 <li class='nav-item' role="presentation"> 1339 <button class="nav-link" type="button" aria-controls="intro" aria-selected="false" aria-label="Home"> 1340 <span class="fs-2"><a href="/" role="button"><i class="bi bi-house-fill" aria-hidden="true"></i><span class="visually-hidden">Home</span></a></span> 1341 </button> 1342 </li> 1343 <li style="display: none;" class='nav-item' role="presentation"> 1344 <button class="nav-link" type="button" aria-controls="intro" aria-selected="false" aria-label="Settings"> 1345 <span class="fs-2" style="display: none;" ><a href="settings" role="button"><i class="bi bi-gear-fill" aria-hidden="true"></i><span class="visually-hidden">Settings</span></a></span> 1346 <span class="fs-2" ><a href="profile#userSettings" role="button"><i class="bi bi-gear-fill" aria-hidden="true"></i><span class="visually-hidden">Settings</span></a></span> 1347 </button> 1348 </li> 1349 <li class='nav-item' role="presentation"> 1350 <button class="nav-link" type="button" aria-controls="intro" aria-selected="false" aria-label="Login"> 1351 <span class="fs-2"> 1352 <a href="login" role="button"> 1353 <i class="fa-solid fa-user" aria-hidden="true"></i> 1354 <span class="visually-hidden">Login</span> 1355 </a> 1356 </span> 1357 </button> 1358 </li> 1359 <li class="nav-item" role="presentation" style="display: none;" > 1360 <button class="nav-link" type="button" aria-controls="intro" aria-selected="false" aria-label="Profile"> 1361 <span class="fs-2"><a href="profile" role="button"> 1362 <div class="picture-container-navbar overflow-hidden d-flex align-items-center rounded-circle mx-auto" style="width: 60px; height: 60px;"></div> 1363 <span class="visually-hidden">Profile</span></a> 1364 </span> 1365 </button> 1366 </li> 1367 <li class="nav-item" role="presentation"> 1368 <button 1369 class="nav-link" 1370 type="button" 1371 aria-controls="intro" 1372 aria-selected="false" 1373 aria-label="Upload file" 1374 onclick="initUploaderModal()" 1375 > 1376 <span class="fs-2"> 1377 <i class="bi bi-cloud-arrow-up-fill" aria-hidden="true"></i> 1378 <span class="visually-hidden">Upload file</span> 1379 </span> 1380 </button> 1381 </li> 1382 <li style="display: none;" class='nav-item' role="presentation"> 1383 <button class="nav-link" type="button" aria-controls="intro" aria-selected="false" aria-label="Logout" onclick="logout()">
1384 <span class="fs-2"><i class="fa-solid fa-right-from-bracket"></i><span class="visually-hidden">Logout</span></span> 1385 </button> 1386 </li> 1387 </ul> 1388 </nav> 1389</footer> 1390<style> 1391 html[data-bs-theme="light"] { 1392 footer{ 1393 background-color: #e8e8e8f5 !important; 1394 } 1395 } 1396 html[data-bs-theme="dark"] { 1397 footer { 1398 background-color: rgb(43 48 53 / 96%) !important; 1399 } 1400 } 1401 1402 @media (max-width: 994.98px) { 1403 footer { 1404 position: fixed !important; 1405 bottom: 0; 1406 width: 100%; 1407 background-color: transparent; 1408 } 1409 } 1410</style> 1411
1412<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
vendor: 1 bytes, line 1412
1412
1413<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
vendor: 1 bytes, line 1413
1413
1414<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
vendor: 1 bytes, line 1414
1414
1415<script src="https://unpkg.com/[email protected]/dist/bootstrap-table.min.js"></script>
vendor: 1 bytes, line 1415
1415
1416<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/extensions/filter-control/bootstrap-table-filter-control.min.js"></script>
vendor: 1 bytes, line 1416
1416
1417<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.resizableColumns.min.js"></script>
vendor: 1 bytes, line 1417
1417
1418<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/extensions/resizable/bootstrap-table-resizable.min.js"></script>
vendor: 1 bytes, line 1418
1418
1419<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/extensions/mobile/bootstrap-table-mobile.min.js"></script>
vendor: 1 bytes, line 1419
1419
1420<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js"></script>
vendor: 1 bytes, line 1420
1420
1421<script src="/static/js/particles.min.js"></script>
vendor: 1 bytes, line 1421
1421
1422<script src="/static/js/server.js"></script>
vendor: 1 bytes, line 1422
1422
1423<script src="/static/js/modal.js"></script>
vendor: 1 bytes, line 1423
1423
1424<script src="/static/js/modules/nostr-tools/lib/nostr.bundle.js"></script>
vendor: 1 bytes, line 1424
1424
1425<script src="/static/js/nostr.js"></script>
vendor: 1 bytes, line 1425
1425
1426<script src="/static/js/note-card.js"></script>
vendor: 1 bytes, line 1426
1426
1427<script src="/static/js/media-action-bar.js"></script>
vendor: 1 bytes, line 1427
1427
1428<script> 1429 1430// Navbar profile picture 1431if (localStorage.getItem('profileData')) { 1432 loadMedia(JSON.parse(localStorage.getItem('profileData')).picture || '/static/resources/picture-default.webp', document.querySelectorAll('.picture-container-navbar'), ['img-thumbnail', 'border-0', 'object-fit-cover', 'w-100', 'h-100']); 1433}else{ 1434 loadMedia('/static/resources/picture-default.webp', document.querySelectorAll('.picture-container-navbar'), ['img-thumbnail', 'border-0', 'object-fit-cover', 'w-100', 'h-100']); 1435} 1436subscribeRelays(0,[""], "from", Math.floor(Date.now() / 1000) - (730 * 24 * 60 * 60)).then((data) => { 1437 if (!data || data.length == 0) return; 1438 data = JSON.parse(data[0].content); 1439 if (localStorage.getItem('profileData') == null || (localStorage.getItem('profileData') != null && data.picture != JSON.parse(localStorage.getItem('profileData')).picture)) { 1440 loadMedia(data.picture || '/static/resources/picture-default.webp' || '/static/resources/picture-default.webp', document.querySelectorAll('.picture-container-navbar'), ['img-thumbnail', 'border-0', 'object-fit-cover', 'w-100', 'h-100']); 1441 } 1442 localStorage.setItem('profileData', JSON.stringify(data)); 1443}); 1444 1445</script>
1445 1446 <div class="modal fade" id="login-alert-modal" tabindex="-1" aria-labelledby="login-alert-modal" aria-hidden="true"> 1447 <div class="modal-dialog modal-dialog-centered " > 1448 <div class="alert container-fluid text-center" role="alert"> 1449 </div> 1450 </div> 1451</div> 1452 1453
1453<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"c3573fbeb9f24ee2aad46eeed217bfb0","r":1,"spa":2}' crossorigin="anonymous"></script>
1453 1454</body> 1455</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.