Add explicit login functionality
This commit is contained in:
+6
-54
@@ -40,7 +40,6 @@
|
||||
if (!form) return;
|
||||
|
||||
var fileInput = document.getElementById('file');
|
||||
var tokenInput = document.getElementById('token');
|
||||
var drop = document.getElementById('drop');
|
||||
var dropHint = document.getElementById('drop-hint');
|
||||
var progress = document.getElementById('progress');
|
||||
@@ -49,8 +48,9 @@
|
||||
var submit = document.getElementById('submit');
|
||||
var errorBox = document.getElementById('error');
|
||||
|
||||
// Limits as rendered for an anonymous caller; refreshed when a token is typed.
|
||||
var limits = { max_size: null, allow_vanity: false };
|
||||
// The session's limits are rendered by the server, so this script never has
|
||||
// to ask who the visitor is. Zero means unlimited.
|
||||
var maxSize = parseInt(form.dataset.maxSize, 10) || 0;
|
||||
|
||||
function formatSize(n) {
|
||||
if (n === null || n === undefined) return 'unlimited';
|
||||
@@ -64,54 +64,6 @@
|
||||
errorBox.hidden = !msg;
|
||||
}
|
||||
|
||||
// --- credentials --------------------------------------------------------
|
||||
// A token is remembered in an HttpOnly cookie the server sets, not here:
|
||||
// this script cannot read it back, so an injected script cannot steal it
|
||||
// either. The page is told who it is by the server when it renders, and the
|
||||
// limits panel is refreshed from /api/limits, which reads the same cookie.
|
||||
function refreshLimits() {
|
||||
var token = tokenInput ? tokenInput.value.trim() : '';
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', base + 'api/limits');
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
// Sent only when the field holds something; otherwise the cookie answers.
|
||||
if (token) xhr.setRequestHeader('Authorization', 'Bearer ' + token);
|
||||
xhr.onload = function () {
|
||||
if (xhr.status !== 200) {
|
||||
if (xhr.status === 401 && token) showError('That token is not recognised.');
|
||||
return;
|
||||
}
|
||||
showError('');
|
||||
var l;
|
||||
try { l = JSON.parse(xhr.responseText); } catch (e) { return; }
|
||||
limits = l;
|
||||
set('limit-size', l.max_size === null ? 'unlimited' : formatSize(l.max_size));
|
||||
set('limit-expiry', l.max_expiry || 'never');
|
||||
set('limit-default', l.default_expiry || 'never');
|
||||
set('limit-vanity', l.allow_vanity ? 'allowed' : 'requires a token');
|
||||
var vanity = document.getElementById('vanity');
|
||||
if (vanity) vanity.disabled = !l.allow_vanity;
|
||||
var expiry = document.getElementById('expiry');
|
||||
if (expiry) expiry.placeholder = l.default_expiry || 'never';
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function set(id, text) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) el.textContent = text;
|
||||
}
|
||||
|
||||
if (tokenInput) {
|
||||
var debounce;
|
||||
tokenInput.addEventListener('input', function () {
|
||||
clearTimeout(debounce);
|
||||
debounce = setTimeout(refreshLimits, 400);
|
||||
});
|
||||
}
|
||||
refreshLimits();
|
||||
|
||||
// --- drag and drop ------------------------------------------------------
|
||||
function describeSelection() {
|
||||
var f = fileInput.files[0];
|
||||
@@ -144,10 +96,10 @@
|
||||
var file = fileInput.files[0];
|
||||
if (!file) return; // let the browser's own validation speak
|
||||
|
||||
if (limits.max_size && file.size > limits.max_size) {
|
||||
if (maxSize && file.size > maxSize) {
|
||||
e.preventDefault();
|
||||
showError('That file is ' + formatSize(file.size) + '; the limit is ' +
|
||||
formatSize(limits.max_size) + '.');
|
||||
showError('That file is ' + formatSize(file.size) + '; your limit is ' +
|
||||
formatSize(maxSize) + '.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+23
-1
@@ -190,7 +190,17 @@ button.link {
|
||||
|
||||
/* Administration --------------------------------------------------------- */
|
||||
|
||||
header nav { float: right; font-size: .875rem; }
|
||||
header { display: flex; align-items: baseline; gap: 1rem; flex-wrap: wrap; }
|
||||
header nav {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: .875rem;
|
||||
font-size: .875rem;
|
||||
}
|
||||
header nav form { display: inline; }
|
||||
header nav .who { color: var(--muted); }
|
||||
header nav .who::before { content: "\1F511\00a0"; }
|
||||
|
||||
dl.stats { grid-template-columns: auto 1fr; }
|
||||
dl.stats em { font-style: normal; color: var(--muted); }
|
||||
@@ -235,3 +245,15 @@ p.error {
|
||||
|
||||
.card .actions { margin: 1.25rem 0; }
|
||||
.card > .field:last-child { margin-bottom: 0; }
|
||||
|
||||
/* One-off token, tucked away so the common path stays a single button. */
|
||||
.onceoff { margin-bottom: 1rem; }
|
||||
.onceoff summary { font-weight: 400; font-size: .875rem; color: var(--muted); }
|
||||
.onceoff .field { max-width: 24rem; }
|
||||
|
||||
.limits .hint { margin-top: .75rem; }
|
||||
.limits dl, .card dl { row-gap: .375rem; }
|
||||
|
||||
/* Actions that mix a link-button with a form-button. */
|
||||
.actions { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
|
||||
.actions form { margin: 0; }
|
||||
|
||||
+28
-32
@@ -1,45 +1,39 @@
|
||||
{{define "content"}}
|
||||
{{if .Stale}}
|
||||
<p class="notice">The token remembered on this device no longer exists. It has been forgotten.</p>
|
||||
<p class="notice">Your login is no longer valid — that token has been removed. You have been logged out.</p>
|
||||
{{end}}
|
||||
|
||||
{{if .TokenName}}
|
||||
<div class="notice" id="identity">
|
||||
Uploading as <strong>{{.TokenName}}</strong>.
|
||||
<form method="post" action="{{.Base}}api/forget"><button type="submit" class="link">Forget</button></form>
|
||||
</div>
|
||||
{{end}}
|
||||
<form id="upload" class="card" method="post" action="{{.Base}}api/upload"
|
||||
enctype="multipart/form-data" data-max-size="{{.MaxSizeBytes}}">
|
||||
|
||||
<form id="upload" class="card" method="post" action="{{.Base}}api/upload" enctype="multipart/form-data">
|
||||
<!-- Field order is load-bearing: the server streams this body and must know
|
||||
the credentials and options before the file part arrives. -->
|
||||
<label class="field">
|
||||
<span>Token <em>optional</em></span>
|
||||
<input type="password" name="token" id="token" autocomplete="off"
|
||||
placeholder="{{if .TokenName}}remembered — type to replace{{else}}anonymous{{end}}">
|
||||
</label>
|
||||
|
||||
<label class="check">
|
||||
<input type="checkbox" name="remember" id="remember" value="1" checked>
|
||||
<span>Remember this token on this device</span>
|
||||
</label>
|
||||
<div class="drop" id="drop">
|
||||
<input type="file" name="file" id="file" required>
|
||||
<p class="hint" id="drop-hint">Choose a file, or drop one here.</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label class="field">
|
||||
<span>Expires in</span>
|
||||
<input type="text" name="expiry" id="expiry" placeholder="{{.DefaultExpiry}}" autocomplete="off">
|
||||
</label>
|
||||
<label class="field" id="vanity-field">
|
||||
<span>Vanity name <em>token only</em></span>
|
||||
<label class="field">
|
||||
<span>Custom name <em>{{if .AllowVanity}}optional{{else}}needs a token{{end}}</em></span>
|
||||
<input type="text" name="vanity" id="vanity" placeholder="auto" autocomplete="off"
|
||||
pattern="[A-Za-z0-9][A-Za-z0-9._-]{1,63}"{{if not .AllowVanity}} disabled{{end}}>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="drop" id="drop">
|
||||
<input type="file" name="file" id="file" required>
|
||||
<p class="hint" id="drop-hint">Choose a file, or drop one here.</p>
|
||||
</div>
|
||||
<details class="onceoff">
|
||||
<summary>Use a different token for this upload</summary>
|
||||
<p class="hint">
|
||||
Applies to this upload only and does not log you in.
|
||||
{{if not .User}}To keep a token for this browser, <a href="{{.Base}}login">log in</a> instead.{{end}}
|
||||
</p>
|
||||
<label class="field">
|
||||
<span>Token</span>
|
||||
<input type="password" name="token" autocomplete="off">
|
||||
</label>
|
||||
</details>
|
||||
|
||||
<div class="progress" id="progress" hidden>
|
||||
<div class="bar"><div class="fill" id="bar-fill"></div></div>
|
||||
@@ -51,13 +45,15 @@
|
||||
</form>
|
||||
|
||||
<section class="limits">
|
||||
<h2>Current limits</h2>
|
||||
<dl id="limits">
|
||||
<dt>Maximum size</dt><dd id="limit-size">{{.MaxSize}}</dd>
|
||||
<dt>Longest lifetime</dt><dd id="limit-expiry">{{.MaxExpiry}}</dd>
|
||||
<dt>Default lifetime</dt><dd id="limit-default">{{.DefaultExpiry}}</dd>
|
||||
<dt>Vanity names</dt><dd id="limit-vanity">{{if .AllowVanity}}allowed{{else}}requires a token{{end}}</dd>
|
||||
<h2>Your limits</h2>
|
||||
<dl>
|
||||
<dt>Uploading as</dt><dd>{{if .User}}{{.User}}{{else}}<em>anonymous</em>{{end}}</dd>
|
||||
<dt>Maximum size</dt><dd>{{.MaxSize}}</dd>
|
||||
<dt>Longest lifetime</dt><dd>{{.MaxExpiry}}</dd>
|
||||
<dt>Default lifetime</dt><dd>{{.DefaultExpiry}}</dd>
|
||||
<dt>Custom names</dt><dd>{{if .AllowVanity}}allowed{{else}}need a token{{end}}</dd>
|
||||
</dl>
|
||||
{{if not .User}}<p class="hint"><a href="{{.Base}}login">Log in</a> with a token to raise these.</p>{{end}}
|
||||
</section>
|
||||
|
||||
<section class="cli">
|
||||
|
||||
@@ -9,7 +9,15 @@
|
||||
<body data-base="{{.Base}}">
|
||||
<header>
|
||||
<a class="brand" href="{{.Base}}">Uncensored Send</a>
|
||||
{{if .Admin}}<nav><a href="{{.Base}}admin">Administration</a></nav>{{end}}
|
||||
<nav>
|
||||
{{if .User}}
|
||||
{{if .Admin}}<a href="{{.Base}}admin">Administration</a>{{end}}
|
||||
<span class="who">{{.User}}</span>
|
||||
<form method="post" action="{{.Base}}logout"><button type="submit" class="link">Log out</button></form>
|
||||
{{else}}
|
||||
<a href="{{.Base}}login">Log in</a>
|
||||
{{end}}
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
{{template "content" .}}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{{define "content"}}
|
||||
{{if .User}}
|
||||
<section class="card">
|
||||
<h1>Logged in</h1>
|
||||
<p>You are logged in as <strong>{{.User}}</strong>.</p>
|
||||
<dl>
|
||||
<dt>Maximum size</dt><dd>{{.MaxSize}}</dd>
|
||||
<dt>Longest lifetime</dt><dd>{{.MaxExpiry}}</dd>
|
||||
<dt>Custom names</dt><dd>{{if .Vanity}}allowed{{else}}not allowed{{end}}</dd>
|
||||
</dl>
|
||||
<p class="actions">
|
||||
<a class="button" href="{{.Base}}">Upload a file</a>
|
||||
<form method="post" action="{{.Base}}logout"><button type="submit" class="link">Log out</button></form>
|
||||
</p>
|
||||
</section>
|
||||
{{else}}
|
||||
<section class="card">
|
||||
<h1>Log in</h1>
|
||||
<p class="hint">
|
||||
A token raises your size and lifetime limits and lets you choose custom
|
||||
names. Logging in keeps it for this browser, so you do not have to paste it
|
||||
for every upload. Without one you can still upload anonymously.
|
||||
</p>
|
||||
|
||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||
|
||||
<form method="post" action="{{.Base}}login">
|
||||
<input type="hidden" name="next" value="{{.Next}}">
|
||||
<label class="field">
|
||||
<span>Token</span>
|
||||
<input type="password" name="token" autocomplete="current-password" required autofocus>
|
||||
</label>
|
||||
<label class="check">
|
||||
<input type="checkbox" name="persist" value="1" checked>
|
||||
<span>Stay logged in on this device</span>
|
||||
</label>
|
||||
<button type="submit">Log in</button>
|
||||
</form>
|
||||
</section>
|
||||
{{end}}
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user