PageSourceSearch

https://nostrcheck.me/api/v1/login

html nostrcheck.me collected 2026-10-04 20:30:37 UTC 65,640 bytes, 1,455 lines download raw bytes

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.