Init
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
{% extends "admin_base.html" %}
|
||||
{% block admin_content %}
|
||||
<section class="audit-page">
|
||||
<form class="audit-filters" method="get" action="/admin/audit">
|
||||
<input type="hidden" name="page" value="1" />
|
||||
<label class="audit-field">
|
||||
<span data-i18n="admin.audit.eventType">Event type</span>
|
||||
<select name="event_type" id="audit-event-type">
|
||||
<option value="" {% if not event_type %}selected{% endif %} data-i18n="admin.audit.allEvents">All events</option>
|
||||
{% for t in event_types %}
|
||||
<option value="{{ t }}" {% if event_type == t %}selected{% endif %}>{{ t }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="audit-field audit-field-grow">
|
||||
<span data-i18n="admin.audit.wrapId">Wrap ID</span>
|
||||
<input name="wrap_id" value="{{ wrap_id }}" class="mono" placeholder="…" autocomplete="off" />
|
||||
</label>
|
||||
<button class="btn primary audit-filter-btn" type="submit" data-i18n="admin.audit.filter">Filter</button>
|
||||
</form>
|
||||
|
||||
<div class="audit-meta">
|
||||
{% if total %}
|
||||
<span class="audit-count">{{ from_idx }}–{{ to_idx }}</span>
|
||||
<span data-i18n="admin.audit.of">of</span>
|
||||
<span class="audit-count">{{ total }}</span>
|
||||
{% else %}
|
||||
<span class="audit-count">0</span>
|
||||
{% endif %}
|
||||
<span data-i18n="admin.audit.events">events</span>
|
||||
</div>
|
||||
|
||||
<div class="audit-table-wrap">
|
||||
<table class="audit-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="admin.audit.col.time">Time</th>
|
||||
<th data-i18n="admin.audit.col.event">Event</th>
|
||||
<th data-i18n="admin.audit.col.ok">OK</th>
|
||||
<th data-i18n="admin.audit.col.wrap">Wrap</th>
|
||||
<th>IP</th>
|
||||
<th>UA</th>
|
||||
<th data-i18n="admin.audit.col.details">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for e in events %}
|
||||
<tr class="{% if not e.success %}audit-row-fail{% endif %}">
|
||||
<td class="mono audit-time">{{ e.created_at }}</td>
|
||||
<td><span class="audit-event-pill">{{ e.event_type }}</span></td>
|
||||
<td>
|
||||
{% if e.success %}
|
||||
<span class="audit-status ok"><i class="fa-solid fa-check" aria-hidden="true"></i> <span data-i18n="admin.audit.yes">yes</span></span>
|
||||
{% else %}
|
||||
<span class="audit-status fail"><i class="fa-solid fa-xmark" aria-hidden="true"></i> <span data-i18n="admin.audit.no">no</span></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="mono">{{ e.wrap_id or "—" }}</td>
|
||||
<td class="mono">{{ e.ip or "—" }}</td>
|
||||
<td class="ua" title="{{ e.user_agent or '' }}">{{ e.user_agent or "—" }}</td>
|
||||
<td class="audit-details"><code>{{ e.details }}</code></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="7" class="audit-empty" data-i18n="admin.audit.empty">No events</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if pages > 1 %}
|
||||
<nav class="pagination" aria-label="Pagination">
|
||||
{% if has_prev %}
|
||||
<a class="btn" href="/admin/audit?page={{ page - 1 }}&event_type={{ event_type|urlencode }}&wrap_id={{ wrap_id|urlencode }}">
|
||||
<i class="fa-solid fa-chevron-left" aria-hidden="true"></i>
|
||||
<span data-i18n="admin.audit.prev">Prev</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="btn" aria-disabled="true" disabled>
|
||||
<i class="fa-solid fa-chevron-left" aria-hidden="true"></i>
|
||||
<span data-i18n="admin.audit.prev">Prev</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
<span class="pagination-status">
|
||||
<span data-i18n="admin.audit.page">Page</span>
|
||||
{{ page }}
|
||||
<span data-i18n="admin.audit.of">of</span>
|
||||
{{ pages }}
|
||||
</span>
|
||||
|
||||
{% if has_next %}
|
||||
<a class="btn" href="/admin/audit?page={{ page + 1 }}&event_type={{ event_type|urlencode }}&wrap_id={{ wrap_id|urlencode }}">
|
||||
<span data-i18n="admin.audit.next">Next</span>
|
||||
<i class="fa-solid fa-chevron-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="btn" aria-disabled="true" disabled>
|
||||
<span data-i18n="admin.audit.next">Next</span>
|
||||
<i class="fa-solid fa-chevron-right" aria-hidden="true"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
</section>
|
||||
<script>
|
||||
document.getElementById("audit-event-type")?.addEventListener("change", (e) => {
|
||||
e.target.form?.requestSubmit();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{{ title }} · Wrapped Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="/static/css/app.css" />
|
||||
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="icon" href="/static/favicon.ico" sizes="any" />
|
||||
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
</head>
|
||||
<body class="admin-body">
|
||||
<div class="bg-grid" aria-hidden="true"></div>
|
||||
<header class="admin-top">
|
||||
<a class="brand" href="/admin">
|
||||
<span class="brand-mark" aria-hidden="true">
|
||||
<i class="fa-solid fa-shield-halved"></i>
|
||||
</span>
|
||||
<span class="brand-text">
|
||||
<span class="brand-name">Wrapped</span>
|
||||
<span class="brand-tagline" data-i18n="admin.brand.tagline">Admin console</span>
|
||||
</span>
|
||||
</a>
|
||||
<div class="admin-top-right">
|
||||
<nav class="admin-nav" aria-label="Admin">
|
||||
<a href="/admin/settings" class="admin-nav-link {% if active == 'settings' %}active{% endif %}">
|
||||
<i class="fa-solid fa-sliders" aria-hidden="true"></i>
|
||||
<span data-i18n="admin.nav.settings">Settings</span>
|
||||
</a>
|
||||
<a href="/admin/audit" class="admin-nav-link {% if active == 'audit' %}active{% endif %}">
|
||||
<i class="fa-solid fa-list" aria-hidden="true"></i>
|
||||
<span data-i18n="admin.nav.audit">Audit</span>
|
||||
</a>
|
||||
<a href="/admin/danger" class="admin-nav-link admin-nav-danger {% if active == 'danger' %}active{% endif %}">
|
||||
<i class="fa-solid fa-triangle-exclamation" aria-hidden="true"></i>
|
||||
<span data-i18n="admin.nav.danger">Danger</span>
|
||||
</a>
|
||||
<a href="/" class="admin-nav-link" target="_blank" rel="noopener noreferrer">
|
||||
<i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i>
|
||||
<span data-i18n="admin.nav.site">Site</span>
|
||||
</a>
|
||||
<form method="post" action="/admin/logout" class="inline">
|
||||
<button class="admin-nav-link admin-nav-logout" type="submit">
|
||||
<i class="fa-solid fa-right-from-bracket" aria-hidden="true"></i>
|
||||
<span data-i18n="admin.nav.logout">Logout</span>
|
||||
</button>
|
||||
</form>
|
||||
</nav>
|
||||
<div class="top-actions">
|
||||
<button type="button" class="icon-btn" id="lang-toggle" title="Language" aria-label="Language">
|
||||
<span id="lang-label">RU</span>
|
||||
</button>
|
||||
<button type="button" class="icon-btn" id="theme-toggle" title="Theme" aria-label="Theme">
|
||||
<svg id="theme-icon-sun" class="icon hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
|
||||
<svg id="theme-icon-moon" class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M21 14.5A8.5 8.5 0 1 1 9.5 3a7 7 0 0 0 11.5 11.5z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="admin-shell">
|
||||
<h1 data-i18n="{% if active == 'audit' %}admin.audit.title{% elif active == 'danger' %}admin.danger.title{% else %}admin.settings.title{% endif %}">{{ title }}</h1>
|
||||
{% block admin_content %}{% endblock %}
|
||||
</main>
|
||||
<script src="/static/js/i18n.js"></script>
|
||||
<script src="/static/js/theme.js"></script>
|
||||
<script src="/static/js/modal.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,44 @@
|
||||
{% extends "admin_base.html" %}
|
||||
{% block admin_content %}
|
||||
{% if request.query_params.get('purged') %}
|
||||
<p class="ok">
|
||||
<span data-i18n="admin.flash.purgedPrefix">Purged</span>
|
||||
{{ request.query_params.get('wraps', '0') }}
|
||||
<span data-i18n="admin.flash.purgedWraps">wrap(s) and</span>
|
||||
{{ request.query_params.get('objects', '0') }}
|
||||
<span data-i18n="admin.flash.purgedObjects">object(s).</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if request.query_params.get('purge') == 'confirm' %}
|
||||
<p class="error" data-i18n="admin.flash.purgeConfirm">Type PURGE to confirm forced cleanup.</p>
|
||||
{% endif %}
|
||||
{% if request.query_params.get('purge') == 'error' %}
|
||||
<p class="error" data-i18n="admin.flash.purgeError">Purge failed. Check logs / MinIO connectivity.</p>
|
||||
{% endif %}
|
||||
|
||||
<section class="admin-section danger-zone danger-page">
|
||||
<p class="hint" data-i18n="admin.danger.hint">Force-delete all wraps from the database and all ciphertext objects in MinIO (wraps/). Audit log is kept. This cannot be undone.</p>
|
||||
<form method="post" action="/admin/purge" class="purge-form" id="purge-form">
|
||||
<label>
|
||||
<span data-i18n="admin.danger.confirmLabel">Type PURGE to confirm</span>
|
||||
<input name="confirm" type="text" autocomplete="off" placeholder="PURGE" required />
|
||||
</label>
|
||||
<button class="btn danger" type="submit" data-i18n="admin.danger.submit">Clear all wraps & files</button>
|
||||
</form>
|
||||
</section>
|
||||
<script>
|
||||
document.getElementById("purge-form")?.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const ok = await window.WrappedModal.confirm({
|
||||
title: window.WrappedI18n?.t("admin.danger.confirmTitle") || "Danger zone",
|
||||
message: window.WrappedI18n?.t("admin.danger.confirmDialog")
|
||||
|| "Delete ALL wraps and files? This cannot be undone.",
|
||||
confirmLabel: window.WrappedI18n?.t("admin.danger.confirmOk") || "Delete everything",
|
||||
cancelLabel: window.WrappedI18n?.t("modal.confirm.cancel") || "Cancel",
|
||||
danger: true,
|
||||
});
|
||||
if (ok) form.submit();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{{ title }} · Wrapped</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="/static/css/app.css" />
|
||||
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="icon" href="/static/favicon.ico" sizes="any" />
|
||||
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
</head>
|
||||
<body class="admin-body">
|
||||
<div class="bg-grid" aria-hidden="true"></div>
|
||||
<div class="top-actions admin-login-toggles">
|
||||
<button type="button" class="icon-btn" id="lang-toggle" title="Language" aria-label="Language">
|
||||
<span id="lang-label">RU</span>
|
||||
</button>
|
||||
<button type="button" class="icon-btn" id="theme-toggle" title="Theme" aria-label="Theme">
|
||||
<svg id="theme-icon-sun" class="icon hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
|
||||
<svg id="theme-icon-moon" class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M21 14.5A8.5 8.5 0 1 1 9.5 3a7 7 0 0 0 11.5 11.5z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<main class="admin-login-shell">
|
||||
<form class="admin-card login-card" method="post" action="/admin/login">
|
||||
<div class="login-head">
|
||||
<span class="brand-mark" aria-hidden="true">
|
||||
<i class="fa-solid fa-shield-halved"></i>
|
||||
</span>
|
||||
<div>
|
||||
<div class="brand-name">Wrapped</div>
|
||||
<div class="login-sub" data-i18n="admin.login.sub">Admin</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if error %}<p class="error login-error" {% if error_key %}data-i18n="{{ error_key }}"{% endif %}>{{ error }}</p>{% endif %}
|
||||
<label>
|
||||
<span data-i18n="admin.login.username">Username</span>
|
||||
<input name="username" autocomplete="username" required autofocus />
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="admin.login.password">Password</span>
|
||||
<input name="password" type="password" autocomplete="current-password" required />
|
||||
</label>
|
||||
<button class="btn primary" type="submit" data-i18n="admin.login.submit">Sign in</button>
|
||||
</form>
|
||||
</main>
|
||||
<script src="/static/js/i18n.js"></script>
|
||||
<script src="/static/js/theme.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,140 @@
|
||||
{% extends "admin_base.html" %}
|
||||
{% block admin_content %}
|
||||
{% if request.query_params.get('saved') %}
|
||||
<p class="ok" data-i18n="admin.flash.saved">Settings saved.</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="admin-tabs" id="admin-tabs">
|
||||
<div class="admin-tablist" role="tablist">
|
||||
<button type="button" class="admin-tab active" role="tab" data-tab="limits" data-i18n="admin.tab.limits">Limits</button>
|
||||
<button type="button" class="admin-tab" role="tab" data-tab="rate" data-i18n="admin.tab.rate">Rate limits</button>
|
||||
<button type="button" class="admin-tab" role="tab" data-tab="mime" data-i18n="admin.tab.mime">MIME</button>
|
||||
<button type="button" class="admin-tab" role="tab" data-tab="password" data-i18n="admin.tab.password">Password</button>
|
||||
<button type="button" class="admin-tab" role="tab" data-tab="captcha" data-i18n="admin.tab.captcha">CAPTCHA</button>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/admin/settings" class="admin-form">
|
||||
<section class="admin-tabpanel active" data-panel="limits" role="tabpanel">
|
||||
<h2 data-i18n="admin.limits.title">Limits</h2>
|
||||
<div class="grid-2">
|
||||
<label>
|
||||
<span data-i18n="admin.limits.maxUpload">Max upload (MB)</span>
|
||||
<input name="max_upload_mb" type="number" min="1" step="1"
|
||||
value="{{ (settings.max_upload_bytes / 1048576)|round|int }}" />
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="admin.limits.defaultTtl">Default TTL (hours)</span>
|
||||
<input name="default_ttl_hours" type="number" min="1" step="1"
|
||||
value="{{ (settings.default_ttl_seconds / 3600)|round|int }}" />
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="admin.limits.maxTtl">Max TTL (hours)</span>
|
||||
<input name="max_ttl_hours" type="number" min="1" step="1"
|
||||
value="{{ (settings.max_ttl_seconds / 3600)|round|int }}" />
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="admin.limits.auditRetention">Audit retention (days)</span>
|
||||
<input name="audit_retention_days" type="number" value="{{ settings.audit_retention_days }}" />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="admin-tabpanel" data-panel="rate" role="tabpanel" hidden>
|
||||
<h2 data-i18n="admin.rate.title">Rate limits (per IP / minute)</h2>
|
||||
<div class="grid-3">
|
||||
<label>
|
||||
<span data-i18n="admin.rate.create">Create</span>
|
||||
<input name="rate_limit_create_per_minute" type="number" value="{{ settings.rate_limit_create_per_minute }}" />
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="admin.rate.unwrap">Unwrap</span>
|
||||
<input name="rate_limit_unwrap_per_minute" type="number" value="{{ settings.rate_limit_unwrap_per_minute }}" />
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="admin.rate.adminLogin">Admin login</span>
|
||||
<input name="rate_limit_admin_login_per_minute" type="number" value="{{ settings.rate_limit_admin_login_per_minute }}" />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="admin-tabpanel" data-panel="mime" role="tabpanel" hidden>
|
||||
<h2 data-i18n="admin.mime.title">MIME allowlist</h2>
|
||||
<p class="hint" data-i18n="admin.mime.hint">One pattern per line. Supports exact types and wildcards like image/*.</p>
|
||||
<textarea name="mime_allowlist" rows="14">{% for m in settings.mime_allowlist %}{{ m }}
|
||||
{% endfor %}</textarea>
|
||||
<details class="helper">
|
||||
<summary data-i18n="admin.mime.examples">Examples / suggested defaults</summary>
|
||||
<pre class="mono">{% for m in mime_examples %}{{ m }}
|
||||
{% endfor %}</pre>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="admin-tabpanel" data-panel="password" role="tabpanel" hidden>
|
||||
<h2 data-i18n="admin.passwordMode.title">Password mode</h2>
|
||||
{% for mode in password_modes %}
|
||||
<label class="radio-block">
|
||||
<input type="radio" name="password_mode" value="{{ mode.value }}" {% if settings.password_mode == mode %}checked{% endif %} />
|
||||
<span>
|
||||
<strong>{{ mode.value }}</strong>
|
||||
<small data-i18n="admin.passwordMode.{{ mode.value }}">{{ password_mode_help[mode.value] }}</small>
|
||||
</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<section class="admin-tabpanel" data-panel="captcha" role="tabpanel" hidden>
|
||||
<h2 data-i18n="admin.captcha.title">CAPTCHA</h2>
|
||||
<label>
|
||||
<span data-i18n="admin.captcha.provider">Provider</span>
|
||||
<select name="captcha_provider">
|
||||
{% for p in captcha_providers %}
|
||||
<option value="{{ p.value }}" {% if settings.captcha_provider == p %}selected{% endif %}>{{ p.value }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<div class="grid-2">
|
||||
<label>
|
||||
<span data-i18n="admin.captcha.turnstileSite">Turnstile site key</span>
|
||||
<input name="turnstile_site_key" value="{{ settings.turnstile_site_key }}" />
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="admin.captcha.hcaptchaSite">hCaptcha site key</span>
|
||||
<input name="hcaptcha_site_key" value="{{ settings.hcaptcha_site_key }}" />
|
||||
</label>
|
||||
</div>
|
||||
<p class="hint">
|
||||
<span data-i18n="admin.captcha.secrets">Secrets:</span>
|
||||
Turnstile {% if turnstile_secret_set %}<span class="ok" data-i18n="admin.captcha.set">set</span>{% else %}<span class="error" data-i18n="admin.captcha.missing">missing</span>{% endif %}
|
||||
· hCaptcha {% if hcaptcha_secret_set %}<span class="ok" data-i18n="admin.captcha.set">set</span>{% else %}<span class="error" data-i18n="admin.captcha.missing">missing</span>{% endif %}
|
||||
<span data-i18n="admin.captcha.viaEnv">(via env TURNSTILE_SECRET_KEY / HCAPTCHA_SECRET_KEY)</span>
|
||||
</p>
|
||||
<details class="helper" open>
|
||||
<summary data-i18n="admin.captcha.helperTitle">How to get CAPTCHA tokens</summary>
|
||||
<div class="helper-body">
|
||||
<h3>Cloudflare Turnstile</h3>
|
||||
<ol>
|
||||
<li data-i18n="admin.captcha.turnstile.1">Open Cloudflare Turnstile dashboard</li>
|
||||
<li data-i18n="admin.captcha.turnstile.2">Create a site widget for your domain (use localhost for dev)</li>
|
||||
<li data-i18n="admin.captcha.turnstile.3">Copy Site Key into the field above</li>
|
||||
<li data-i18n="admin.captcha.turnstile.4">Copy Secret Key into env TURNSTILE_SECRET_KEY and restart</li>
|
||||
<li data-i18n="admin.captcha.turnstile.5">Set provider to turnstile</li>
|
||||
</ol>
|
||||
<h3>hCaptcha</h3>
|
||||
<ol>
|
||||
<li data-i18n="admin.captcha.hcaptcha.1">Open hCaptcha dashboard</li>
|
||||
<li data-i18n="admin.captcha.hcaptcha.2">New site → copy Sitekey here</li>
|
||||
<li data-i18n="admin.captcha.hcaptcha.3">Secret into env HCAPTCHA_SECRET_KEY, restart</li>
|
||||
<li data-i18n="admin.captcha.hcaptcha.4">Set provider to hcaptcha</li>
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<div class="admin-tab-actions" id="settings-save-bar">
|
||||
<button class="btn primary" type="submit" data-i18n="admin.settings.save">Save settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/admin-tabs.js"></script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{% block title %}{{ title or "Wrapped" }}{% endblock %}</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="/static/css/app.css" />
|
||||
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="icon" href="/static/favicon.ico" sizes="any" />
|
||||
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-grid" aria-hidden="true"></div>
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/">
|
||||
<span class="brand-mark" aria-hidden="true">
|
||||
<i class="fa-solid fa-shield-halved"></i>
|
||||
</span>
|
||||
<span class="brand-text">
|
||||
<span class="brand-name">Wrapped</span>
|
||||
<span class="brand-tagline" data-i18n="brand.tagline">Упакуй. Отправь. Исчезни.</span>
|
||||
</span>
|
||||
</a>
|
||||
<div class="top-actions">
|
||||
<button type="button" class="icon-btn" id="lang-toggle" title="Language" aria-label="Language">
|
||||
<span id="lang-label">RU</span>
|
||||
</button>
|
||||
<button type="button" class="icon-btn" id="theme-toggle" title="Theme" aria-label="Theme">
|
||||
<svg id="theme-icon-sun" class="icon hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
|
||||
<svg id="theme-icon-moon" class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M21 14.5A8.5 8.5 0 1 1 9.5 3a7 7 0 0 0 11.5 11.5z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main class="shell">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<footer class="site-footer">
|
||||
<ul class="footer-pillars">
|
||||
<li>
|
||||
<i class="fa-solid fa-user-secret" aria-hidden="true"></i>
|
||||
<span>
|
||||
<strong data-i18n="footer.pillar.zk">Zero-knowledge</strong>
|
||||
<small data-i18n="footer.pillar.zk.hint">Сервер не видит содержимое</small>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa-solid fa-fire" aria-hidden="true"></i>
|
||||
<span>
|
||||
<strong data-i18n="footer.pillar.once">Одноразово</strong>
|
||||
<small data-i18n="footer.pillar.once.hint">После открытия — удаление</small>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa-solid fa-lock" aria-hidden="true"></i>
|
||||
<span>
|
||||
<strong data-i18n="footer.pillar.encrypted">Зашифровано</strong>
|
||||
<small data-i18n="footer.pillar.encrypted.hint">Ключ только в вашей ссылке</small>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="footer-copy">
|
||||
© <span data-i18n="footer.copy.author">Сергей Антропов</span>
|
||||
· <a href="https://devops.org.ru" target="_blank" rel="noopener noreferrer">devops.org.ru</a>
|
||||
</p>
|
||||
</footer>
|
||||
<script src="/static/js/i18n.js"></script>
|
||||
<script src="/static/js/theme.js"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,82 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<section class="hero-panel create-panel">
|
||||
<div class="composer">
|
||||
<div class="field-row">
|
||||
<label for="text-language" data-i18n="create.language">Language</label>
|
||||
<select id="text-language">
|
||||
<option value="plaintext" data-i18n="lang.plaintext">Обычный текст</option>
|
||||
<option value="markdown" data-i18n="lang.markdown">Markdown</option>
|
||||
<option value="json" data-i18n="lang.json">JSON</option>
|
||||
<option value="yaml" data-i18n="lang.yaml">YAML</option>
|
||||
<option value="python" data-i18n="lang.python">Python</option>
|
||||
<option value="javascript" data-i18n="lang.javascript">JavaScript</option>
|
||||
<option value="typescript" data-i18n="lang.typescript">TypeScript</option>
|
||||
<option value="go" data-i18n="lang.go">Go</option>
|
||||
<option value="rust" data-i18n="lang.rust">Rust</option>
|
||||
<option value="sql" data-i18n="lang.sql">SQL</option>
|
||||
<option value="bash" data-i18n="lang.bash">Bash</option>
|
||||
<option value="html" data-i18n="lang.html">HTML</option>
|
||||
<option value="css" data-i18n="lang.css">CSS</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<label class="sr-only" for="payload-text" data-i18n="create.text">Text</label>
|
||||
<textarea id="payload-text" class="code-input" spellcheck="false" data-i18n-placeholder="create.textPlaceholder" placeholder="Paste secrets, configs, notes…"></textarea>
|
||||
|
||||
<div id="dropzone" class="dropzone" tabindex="0">
|
||||
<p data-i18n="create.drop">Drop files here, click to browse, or paste a screenshot (⌘V / Ctrl+V)</p>
|
||||
<input id="file-input" type="file" multiple hidden />
|
||||
</div>
|
||||
<ul id="file-list" class="file-list"></ul>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="field">
|
||||
<label for="ttl-seconds" data-i18n="create.ttl">Time to live</label>
|
||||
<select id="ttl-seconds"></select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password" data-i18n="create.password">Password (optional)</label>
|
||||
<div class="input-with-action">
|
||||
<input id="password" type="password" autocomplete="new-password" data-i18n-placeholder="create.passwordPlaceholder" placeholder="Extra unlock secret" />
|
||||
<button type="button" class="field-icon-btn" id="generate-password" aria-describedby="generate-password-tip">
|
||||
<i class="fa-solid fa-wand-magic-sparkles" aria-hidden="true"></i>
|
||||
<span class="sr-only" data-i18n="create.passwordGenerate">Generate password</span>
|
||||
<span class="ui-tooltip" id="generate-password-tip" role="tooltip" data-i18n="create.passwordGenerateTip">Generate a strong password</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="captcha-slot" class="captcha-slot"></div>
|
||||
<p id="form-error" class="error hidden"></p>
|
||||
|
||||
<div class="actions actions-center">
|
||||
<button type="button" id="wrap-btn" class="btn primary">
|
||||
<span data-i18n="create.submit">Create wrapped token</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="success-panel" class="success-panel hidden">
|
||||
<h2 data-i18n="create.successTitle">Token issued</h2>
|
||||
<p class="warn" data-i18n="create.successWarn">One-time unwrap. After someone opens it, ciphertext is destroyed on the server.</p>
|
||||
<label data-i18n="create.shareLink">Share link</label>
|
||||
<div class="copy-row">
|
||||
<input id="share-link" readonly />
|
||||
<button type="button" class="btn" id="copy-link" data-i18n="common.copy">Copy</button>
|
||||
</div>
|
||||
<label data-i18n="create.token">Wrapped token</label>
|
||||
<div class="copy-row">
|
||||
<input id="share-token" class="mono" readonly />
|
||||
<button type="button" class="btn" id="copy-token" data-i18n="common.copy">Copy</button>
|
||||
</div>
|
||||
<p class="meta" id="expires-meta"></p>
|
||||
<button type="button" class="btn ghost" id="create-another" data-i18n="create.another">Create another</button>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="/static/js/crypto.js"></script>
|
||||
<script src="/static/js/create.js"></script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,33 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<section class="hero-panel unwrap-panel">
|
||||
<div class="panel-head">
|
||||
<p class="eyebrow" data-i18n="unwrap.eyebrow">Unwrap</p>
|
||||
<h1 data-i18n="unwrap.title">Reveal once</h1>
|
||||
<p class="lede" data-i18n="unwrap.lede">Paste a wrapped token or open a share link. Ciphertext is fetched once and deleted from the server.</p>
|
||||
</div>
|
||||
|
||||
<div class="composer" id="unwrap-form">
|
||||
<div class="field">
|
||||
<label for="token-input" data-i18n="unwrap.token">Wrapped token or ID</label>
|
||||
<input id="token-input" class="mono" value="{{ wrap_id }}" data-i18n-placeholder="unwrap.tokenPlaceholder" placeholder="wrapped_v1.… или ID" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="unwrap-password" data-i18n="unwrap.password">Password (if set)</label>
|
||||
<input id="unwrap-password" type="password" autocomplete="current-password" />
|
||||
</div>
|
||||
<div id="captcha-slot" class="captcha-slot"></div>
|
||||
<p id="form-error" class="error hidden"></p>
|
||||
<button type="button" id="unwrap-btn" class="btn primary" data-i18n="unwrap.submit">Unwrap</button>
|
||||
</div>
|
||||
|
||||
<div id="result-panel" class="result-panel hidden">
|
||||
<p class="warn" data-i18n="unwrap.destroyed">Server copy destroyed. Preview lives only in this browser session.</p>
|
||||
<div id="items" class="items"></div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="/static/js/crypto.js"></script>
|
||||
<script src="/static/js/unwrap.js"></script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user