1071 lines
29 KiB
HTML
1071 lines
29 KiB
HTML
|
|
{{template "base.html" .}}
|
|||
|
|
|
|||
|
|
{{define "title"}}Password Pusher - Secure Text Sharing{{end}}
|
|||
|
|
|
|||
|
|
{{define "head"}}
|
|||
|
|
{{if .Success}}
|
|||
|
|
<meta name="success-data" content='{"id":"{{.ID}}","url":"{{.PushURL}}","expiresAt":"{{.ExpiresAt}}"}'>
|
|||
|
|
{{end}}
|
|||
|
|
<style>
|
|||
|
|
:root {
|
|||
|
|
--bg-color: #1e1e1e;
|
|||
|
|
--text-color: #e0e0e0;
|
|||
|
|
--section-bg: #2d2d2d;
|
|||
|
|
--border-color: #404040;
|
|||
|
|
--highlight: #3c5c7c;
|
|||
|
|
--success: #4caf50;
|
|||
|
|
--warning: #ff9800;
|
|||
|
|
--error: #f44336;
|
|||
|
|
--danger: #f44336;
|
|||
|
|
--info: #2196f3;
|
|||
|
|
--muted-color: #999;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
body {
|
|||
|
|
background-color: var(--bg-color);
|
|||
|
|
color: var(--text-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.container {
|
|||
|
|
background-color: var(--bg-color);
|
|||
|
|
color: var(--text-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
input, textarea {
|
|||
|
|
background-color: var(--section-bg);
|
|||
|
|
color: var(--text-color);
|
|||
|
|
border: 1px solid var(--border-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.push-form, .info-section, .history-section {
|
|||
|
|
background-color: var(--section-bg);
|
|||
|
|
border: 1px solid var(--border-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-header {
|
|||
|
|
background-color: #333;
|
|||
|
|
border-bottom: 1px solid var(--border-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-stats {
|
|||
|
|
background-color: #333;
|
|||
|
|
border-bottom: 1px solid var(--border-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-badge {
|
|||
|
|
background-color: var(--section-bg);
|
|||
|
|
border: 1px solid var(--border-color);
|
|||
|
|
color: var(--text-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-table th {
|
|||
|
|
background-color: #333;
|
|||
|
|
color: var(--text-color);
|
|||
|
|
border-bottom: 2px solid var(--border-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-table td {
|
|||
|
|
border-bottom: 1px solid var(--border-color);
|
|||
|
|
color: var(--text-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-table tr:hover:not(.empty-row) {
|
|||
|
|
background-color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.preview-text {
|
|||
|
|
background-color: #333;
|
|||
|
|
color: var(--text-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-danger {
|
|||
|
|
background: var(--error);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-sm {
|
|||
|
|
font-size: 12px;
|
|||
|
|
padding: 6px 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notes-cell {
|
|||
|
|
max-width: 120px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notes-text {
|
|||
|
|
color: #aaa;
|
|||
|
|
font-style: italic;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.container {
|
|||
|
|
max-width: 800px;
|
|||
|
|
margin: 0 auto;
|
|||
|
|
padding: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.alert {
|
|||
|
|
padding: 20px;
|
|||
|
|
margin: 20px 0;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
border-left: 4px solid var(--success);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.alert-success {
|
|||
|
|
background-color: var(--section-bg);
|
|||
|
|
color: var(--success);
|
|||
|
|
border-color: var(--success);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.link-container {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 10px;
|
|||
|
|
margin: 15px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.link-container input {
|
|||
|
|
flex: 1;
|
|||
|
|
padding: 10px;
|
|||
|
|
border: 2px solid var(--border-color);
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-family: monospace;
|
|||
|
|
background: var(--section-bg);
|
|||
|
|
color: var(--text-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.copy-btn {
|
|||
|
|
padding: 10px 15px;
|
|||
|
|
background: var(--highlight);
|
|||
|
|
color: white;
|
|||
|
|
border: none;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.push-form {
|
|||
|
|
background: var(--section-bg);
|
|||
|
|
padding: 30px;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
margin: 20px 0;
|
|||
|
|
border: 1px solid var(--border-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-group {
|
|||
|
|
margin-bottom: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-group label {
|
|||
|
|
display: block;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
font-weight: bold;
|
|||
|
|
color: var(--text-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-group textarea {
|
|||
|
|
width: 100%;
|
|||
|
|
min-height: 120px;
|
|||
|
|
padding: 12px;
|
|||
|
|
border: 2px solid var(--border-color);
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-family: monospace;
|
|||
|
|
resize: vertical;
|
|||
|
|
background: var(--section-bg);
|
|||
|
|
color: var(--text-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-row {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 1fr 1fr;
|
|||
|
|
gap: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-group input[type="range"] {
|
|||
|
|
width: 100%;
|
|||
|
|
margin: 10px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.range-display {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
margin-top: 5px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.range-display span {
|
|||
|
|
font-weight: bold;
|
|||
|
|
color: var(--highlight);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.checkbox-group {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.checkbox-label {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
gap: 10px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.checkbox-label input[type="checkbox"] {
|
|||
|
|
margin-top: 2px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.checkbox-label small {
|
|||
|
|
display: block;
|
|||
|
|
color: #aaa;
|
|||
|
|
margin-top: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn {
|
|||
|
|
display: inline-block;
|
|||
|
|
padding: 12px 24px;
|
|||
|
|
border: none;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
text-decoration: none;
|
|||
|
|
font-weight: bold;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-primary {
|
|||
|
|
background: var(--highlight);
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-primary:hover {
|
|||
|
|
background: #2a4c6c;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-secondary {
|
|||
|
|
background: #6c757d;
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-large {
|
|||
|
|
padding: 15px 30px;
|
|||
|
|
font-size: 18px;
|
|||
|
|
width: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-section {
|
|||
|
|
background: var(--section-bg);
|
|||
|
|
padding: 25px;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
margin: 20px 0;
|
|||
|
|
border: 1px solid var(--border-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-section h3 {
|
|||
|
|
margin-top: 0;
|
|||
|
|
color: var(--text-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-section ul {
|
|||
|
|
list-style: none;
|
|||
|
|
padding: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-section li {
|
|||
|
|
padding: 8px 0;
|
|||
|
|
border-bottom: 1px solid var(--border-color);
|
|||
|
|
color: var(--text-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-section li:last-child {
|
|||
|
|
border-bottom: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.actions {
|
|||
|
|
margin-top: 20px;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* History Section Styles */
|
|||
|
|
.history-section {
|
|||
|
|
margin-top: 40px;
|
|||
|
|
background: var(--section-bg);
|
|||
|
|
border-radius: 8px;
|
|||
|
|
border: 1px solid var(--border-color);
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-header {
|
|||
|
|
background: var(--bg-color);
|
|||
|
|
padding: 20px;
|
|||
|
|
border-bottom: 1px solid var(--border-color);
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-header h2 {
|
|||
|
|
margin: 0;
|
|||
|
|
color: var(--text-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-controls {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-stats {
|
|||
|
|
padding: 15px 20px;
|
|||
|
|
background: var(--bg-color);
|
|||
|
|
border-bottom: 1px solid var(--border-color);
|
|||
|
|
display: flex;
|
|||
|
|
gap: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-badge {
|
|||
|
|
background: var(--section-bg);
|
|||
|
|
padding: 6px 12px;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
border: 1px solid var(--border-color);
|
|||
|
|
font-size: 14px;
|
|||
|
|
font-weight: bold;
|
|||
|
|
color: var(--text-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-badge.active {
|
|||
|
|
background: var(--success);
|
|||
|
|
color: white;
|
|||
|
|
border-color: var(--success);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-badge.expired {
|
|||
|
|
background: var(--danger);
|
|||
|
|
color: white;
|
|||
|
|
border-color: var(--danger);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-table-container {
|
|||
|
|
overflow-x: auto;
|
|||
|
|
max-width: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-table {
|
|||
|
|
width: 100%;
|
|||
|
|
border-collapse: collapse;
|
|||
|
|
font-size: 13px;
|
|||
|
|
min-width: 600px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-table th {
|
|||
|
|
background: var(--bg-color);
|
|||
|
|
padding: 10px 6px;
|
|||
|
|
text-align: left;
|
|||
|
|
font-weight: bold;
|
|||
|
|
color: var(--text-color);
|
|||
|
|
border-bottom: 2px solid var(--border-color);
|
|||
|
|
white-space: nowrap;
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-table td {
|
|||
|
|
padding: 10px 6px;
|
|||
|
|
border-bottom: 1px solid var(--border-color);
|
|||
|
|
vertical-align: middle;
|
|||
|
|
color: var(--text-color);
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-table tr.expired {
|
|||
|
|
background: var(--bg-color);
|
|||
|
|
opacity: 0.7;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-table tr:hover:not(.empty-row) {
|
|||
|
|
background: var(--section-bg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.empty-row td {
|
|||
|
|
text-align: center;
|
|||
|
|
padding: 40px 20px;
|
|||
|
|
color: var(--muted-color);
|
|||
|
|
font-style: italic;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.date-cell {
|
|||
|
|
white-space: nowrap;
|
|||
|
|
color: var(--muted-color);
|
|||
|
|
min-width: 110px;
|
|||
|
|
font-size: 11px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.views-cell {
|
|||
|
|
text-align: center;
|
|||
|
|
font-weight: bold;
|
|||
|
|
min-width: 60px;
|
|||
|
|
font-size: 11px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notes-cell {
|
|||
|
|
max-width: 150px;
|
|||
|
|
font-size: 11px;
|
|||
|
|
word-break: break-word;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-cell {
|
|||
|
|
text-align: center;
|
|||
|
|
min-width: 80px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-badge {
|
|||
|
|
padding: 3px 6px;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-size: 10px;
|
|||
|
|
font-weight: bold;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-badge.active {
|
|||
|
|
background: var(--success);
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-badge.expired {
|
|||
|
|
background: var(--danger);
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-badge.deleted {
|
|||
|
|
background: #6c757d;
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.actions-cell {
|
|||
|
|
text-align: center;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
min-width: 140px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-xs {
|
|||
|
|
padding: 3px 6px;
|
|||
|
|
font-size: 10px;
|
|||
|
|
margin: 0 1px;
|
|||
|
|
border-radius: 3px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.expired-text {
|
|||
|
|
color: var(--muted-color);
|
|||
|
|
font-size: 12px;
|
|||
|
|
font-style: italic;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 768px) {
|
|||
|
|
.form-row {
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.link-container {
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-header {
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 15px;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-stats {
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
gap: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-table {
|
|||
|
|
font-size: 10px;
|
|||
|
|
min-width: 500px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-table th,
|
|||
|
|
.history-table td {
|
|||
|
|
padding: 6px 3px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notes-cell {
|
|||
|
|
max-width: 100px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.date-cell {
|
|||
|
|
font-size: 9px;
|
|||
|
|
min-width: 90px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.actions-cell {
|
|||
|
|
min-width: 120px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-xs {
|
|||
|
|
padding: 2px 4px;
|
|||
|
|
font-size: 9px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
{{end}}
|
|||
|
|
|
|||
|
|
{{define "content"}}
|
|||
|
|
<div class="container">
|
|||
|
|
<h1>🔐 Password Pusher</h1>
|
|||
|
|
<p>Share sensitive text securely with automatic expiration and view limits.</p>
|
|||
|
|
|
|||
|
|
{{if .Success}}
|
|||
|
|
<div class="alert alert-success">
|
|||
|
|
<h3>✅ Secure Link Created!</h3>
|
|||
|
|
<p>Your text has been encrypted and stored securely. Share this link:</p>
|
|||
|
|
<div class="link-container">
|
|||
|
|
<input type="text" id="pushUrl" value="{{.PushURL}}" readonly onclick="this.select()">
|
|||
|
|
<button onclick="copyToClipboard()" class="copy-btn">📋 Copy</button>
|
|||
|
|
</div>
|
|||
|
|
<p><small>🕒 Expires: {{.ExpiresAt}} | 🆔 ID: {{.ID}}</small></p>
|
|||
|
|
<p><strong>⚠️ Important:</strong> This link will only work once if auto-delete is enabled!</p>
|
|||
|
|
<a href="/pwpush" class="btn btn-primary">Create Another Link</a>
|
|||
|
|
</div>
|
|||
|
|
{{else}}
|
|||
|
|
|
|||
|
|
<form id="pushForm" method="post" class="push-form">
|
|||
|
|
{{if .CSRFToken}}
|
|||
|
|
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
|||
|
|
{{end}}
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label for="text">📝 Text to Share:</label>
|
|||
|
|
<textarea name="text" id="text" placeholder="Enter your password, secret, or sensitive text here..." required></textarea>
|
|||
|
|
<small>Enter any sensitive information you want to share securely.</small>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label for="protection_password">🔑 Password Protection (Optional):</label>
|
|||
|
|
<input type="password" name="password" id="protection_password"
|
|||
|
|
placeholder="Leave empty for no password protection"
|
|||
|
|
style="width: 75%; padding: 15px 20px; border: 2px solid var(--border-color);
|
|||
|
|
border-radius: 8px; background-color: var(--section-bg); color: var(--text-color);
|
|||
|
|
font-size: 13px; font-family: monospace; margin-top: 5px;"><br>
|
|||
|
|
<small>If set, viewers must enter this password to access the content. This bypasses the click-to-reveal feature.</small>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-row">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label for="expiry_days">⏰ Expires After:</label>
|
|||
|
|
<input type="range" name="expiry_days" id="expiry_days" min="1" max="90" value="7"
|
|||
|
|
oninput="updateExpiryDisplay(this.value)">
|
|||
|
|
<div class="range-display">
|
|||
|
|
<span id="expiryDisplay">7 days</span>
|
|||
|
|
<small>Max: 3 months</small>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label for="max_views">👁️ Maximum Views:</label>
|
|||
|
|
<input type="range" name="max_views" id="max_views" min="1" max="100" value="10"
|
|||
|
|
oninput="updateViewsDisplay(this.value)">
|
|||
|
|
<div class="range-display">
|
|||
|
|
<span id="viewsDisplay">10 views</span>
|
|||
|
|
<small>Max: 100 views</small>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-group">
|
|||
|
|
<div class="checkbox-group">
|
|||
|
|
<label class="checkbox-label">
|
|||
|
|
<input type="checkbox" name="require_click" checked>
|
|||
|
|
<span class="checkmark"></span>
|
|||
|
|
🛡️ Require click to reveal (recommended)
|
|||
|
|
<small>Hides content from web crawlers and requires user interaction</small>
|
|||
|
|
</label>
|
|||
|
|
|
|||
|
|
<label class="checkbox-label">
|
|||
|
|
<input type="checkbox" name="auto_delete">
|
|||
|
|
<span class="checkmark"></span>
|
|||
|
|
🗑️ Allow manual deletion after viewing
|
|||
|
|
<small>Adds a delete button when content is viewed (viewer can choose to delete)</small>
|
|||
|
|
</label>
|
|||
|
|
|
|||
|
|
<label class="checkbox-label">
|
|||
|
|
<input type="checkbox" name="track_history" id="track_history">
|
|||
|
|
<span class="checkmark"></span>
|
|||
|
|
📚 Save to my history
|
|||
|
|
<small>Keep a record of your created links (stored in browser)</small>
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<button type="submit" class="btn btn-primary btn-large">
|
|||
|
|
🔒 Create Secure Link
|
|||
|
|
</button>
|
|||
|
|
</form>
|
|||
|
|
|
|||
|
|
<div class="info-section">
|
|||
|
|
<h3>🔒 How it works:</h3>
|
|||
|
|
<ul>
|
|||
|
|
<li><strong>Encryption:</strong> Your text is encrypted before storage</li>
|
|||
|
|
<li><strong>Automatic Expiry:</strong> Links expire after set time or view limit</li>
|
|||
|
|
<li><strong>One-time Use:</strong> Optional auto-delete after viewing</li>
|
|||
|
|
<li><strong>No Registration:</strong> No account required</li>
|
|||
|
|
<li><strong>Browser History:</strong> Optionally track your links locally</li>
|
|||
|
|
</ul>
|
|||
|
|
</div>
|
|||
|
|
{{end}}
|
|||
|
|
|
|||
|
|
<!-- History Section -->
|
|||
|
|
<div class="history-section">
|
|||
|
|
<div class="history-header">
|
|||
|
|
<h2>📚 My Recent Links</h2>
|
|||
|
|
<div class="history-controls">
|
|||
|
|
<button onclick="refreshHistory()" class="btn btn-secondary btn-sm">🔄 Refresh</button>
|
|||
|
|
<button onclick="clearHistory()" class="btn btn-danger btn-sm">🗑️ Clear All</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div id="historyContainer">
|
|||
|
|
<div class="history-stats">
|
|||
|
|
<span class="stat-badge">Total: <span id="totalCount">0</span></span>
|
|||
|
|
<span class="stat-badge active">Active: <span id="activeCount">0</span></span>
|
|||
|
|
<span class="stat-badge expired">Expired: <span id="expiredCount">0</span></span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="history-table-container">
|
|||
|
|
<table id="historyTable" class="history-table">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>📅 Created</th>
|
|||
|
|
<th>⏰ Expires</th>
|
|||
|
|
<th><EFBFBD>️ Views</th>
|
|||
|
|
<th>📝 Local Notes</th>
|
|||
|
|
<th>🔗 Status</th>
|
|||
|
|
<th>⚡ Actions</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody id="historyTableBody">
|
|||
|
|
<tr class="empty-row">
|
|||
|
|
<td colspan="6" class="empty-message">
|
|||
|
|
📭 No history found. Enable "Save to my history" when creating links.
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
function updateExpiryDisplay(days) {
|
|||
|
|
const display = document.getElementById('expiryDisplay');
|
|||
|
|
if (days == 1) {
|
|||
|
|
display.textContent = '1 day';
|
|||
|
|
} else if (days <= 7) {
|
|||
|
|
display.textContent = days + ' days';
|
|||
|
|
} else if (days <= 30) {
|
|||
|
|
const weeks = Math.floor(days / 7);
|
|||
|
|
const remainingDays = days % 7;
|
|||
|
|
if (remainingDays === 0) {
|
|||
|
|
display.textContent = weeks + (weeks === 1 ? ' week' : ' weeks');
|
|||
|
|
} else {
|
|||
|
|
display.textContent = weeks + 'w ' + remainingDays + 'd';
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
const months = Math.floor(days / 30);
|
|||
|
|
const remainingDays = days % 30;
|
|||
|
|
if (remainingDays === 0) {
|
|||
|
|
display.textContent = months + (months === 1 ? ' month' : ' months');
|
|||
|
|
} else {
|
|||
|
|
display.textContent = months + 'm ' + remainingDays + 'd';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateViewsDisplay(views) {
|
|||
|
|
const display = document.getElementById('viewsDisplay');
|
|||
|
|
display.textContent = views + (views === '1' ? ' view' : ' views');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function copyToClipboard() {
|
|||
|
|
const urlInput = document.getElementById('pushUrl');
|
|||
|
|
urlInput.select();
|
|||
|
|
document.execCommand('copy');
|
|||
|
|
|
|||
|
|
const btn = document.querySelector('.copy-btn');
|
|||
|
|
const originalText = btn.textContent;
|
|||
|
|
btn.textContent = '✅ Copied!';
|
|||
|
|
btn.style.background = '#4caf50';
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
btn.textContent = originalText;
|
|||
|
|
btn.style.background = '';
|
|||
|
|
}, 2000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Save settings to cookies when form is submitted
|
|||
|
|
document.getElementById('pushForm').addEventListener('submit', function() {
|
|||
|
|
const expiryDays = document.getElementById('expiry_days').value;
|
|||
|
|
const maxViews = document.getElementById('max_views').value;
|
|||
|
|
const requireClick = document.querySelector('input[name="require_click"]').checked;
|
|||
|
|
const autoDelete = document.querySelector('input[name="auto_delete"]').checked;
|
|||
|
|
const trackHistory = document.querySelector('input[name="track_history"]').checked;
|
|||
|
|
|
|||
|
|
setCookie('pwpush_expiry_days', expiryDays, 30);
|
|||
|
|
setCookie('pwpush_max_views', maxViews, 30);
|
|||
|
|
setCookie('pwpush_require_click', requireClick, 30);
|
|||
|
|
setCookie('pwpush_auto_delete', autoDelete, 30);
|
|||
|
|
setCookie('pwpush_track_history', trackHistory, 30);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// History functionality
|
|||
|
|
function refreshHistory() {
|
|||
|
|
// Show loading state
|
|||
|
|
const refreshBtn = document.querySelector('button[onclick="refreshHistory()"]');
|
|||
|
|
const originalText = refreshBtn.textContent;
|
|||
|
|
refreshBtn.textContent = '🔄 Validating...';
|
|||
|
|
refreshBtn.disabled = true;
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
loadHistory();
|
|||
|
|
refreshBtn.textContent = originalText;
|
|||
|
|
refreshBtn.disabled = false;
|
|||
|
|
}, 100);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function validateLinksWithServer(history) {
|
|||
|
|
// Only validate links that are not already marked as expired/deleted
|
|||
|
|
const activeLinks = history.filter(item => !item.isDeleted && new Date(item.expiresAt) > new Date());
|
|||
|
|
|
|||
|
|
if (activeLinks.length === 0) {
|
|||
|
|
return Promise.resolve(history); // No active links to validate
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const ids = activeLinks.map(item => item.id);
|
|||
|
|
|
|||
|
|
return fetch('/pwpush/api/status/', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json',
|
|||
|
|
},
|
|||
|
|
body: JSON.stringify({ ids: ids })
|
|||
|
|
})
|
|||
|
|
.then(response => {
|
|||
|
|
if (!response.ok) {
|
|||
|
|
throw new Error('Failed to validate links');
|
|||
|
|
}
|
|||
|
|
return response.json();
|
|||
|
|
})
|
|||
|
|
.then(statuses => {
|
|||
|
|
// Update history items with server status
|
|||
|
|
const statusMap = {};
|
|||
|
|
statuses.forEach(status => {
|
|||
|
|
statusMap[status.id] = status;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return history.map(item => {
|
|||
|
|
const serverStatus = statusMap[item.id];
|
|||
|
|
if (serverStatus) {
|
|||
|
|
// Update with server data
|
|||
|
|
item.exists = serverStatus.exists;
|
|||
|
|
item.isDeleted = serverStatus.is_deleted;
|
|||
|
|
item.viewCount = serverStatus.current_views;
|
|||
|
|
item.maxViews = serverStatus.max_views;
|
|||
|
|
|
|||
|
|
// Mark as expired if server says so
|
|||
|
|
if (serverStatus.is_expired || !serverStatus.exists) {
|
|||
|
|
item.isExpired = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// If link doesn't exist on server, mark for removal
|
|||
|
|
if (!serverStatus.exists) {
|
|||
|
|
item.shouldRemove = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return item;
|
|||
|
|
}).filter(item => !item.shouldRemove); // Remove non-existent links
|
|||
|
|
})
|
|||
|
|
.catch(error => {
|
|||
|
|
console.warn('Failed to validate links with server:', error);
|
|||
|
|
return history; // Return original history if validation fails
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function loadHistory() {
|
|||
|
|
let history = getHistoryFromCookie();
|
|||
|
|
|
|||
|
|
// Only validate if "track history" is enabled
|
|||
|
|
const trackHistory = getCookie('pwpush_track_history');
|
|||
|
|
if (trackHistory === 'true' && history.length > 0) {
|
|||
|
|
// Validate with server and update display
|
|||
|
|
validateLinksWithServer(history).then(validatedHistory => {
|
|||
|
|
// Save updated history back to cookies
|
|||
|
|
if (validatedHistory.length !== history.length) {
|
|||
|
|
saveHistoryToCookie(validatedHistory);
|
|||
|
|
history = validatedHistory;
|
|||
|
|
}
|
|||
|
|
displayHistory(history);
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
displayHistory(history);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function displayHistory(history) {
|
|||
|
|
const tbody = document.getElementById('historyTableBody');
|
|||
|
|
const emptyRow = tbody.querySelector('.empty-row');
|
|||
|
|
|
|||
|
|
if (history.length === 0) {
|
|||
|
|
if (emptyRow) emptyRow.style.display = '';
|
|||
|
|
updateStats(0, 0, 0);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (emptyRow) emptyRow.style.display = 'none';
|
|||
|
|
|
|||
|
|
// Clear existing rows except empty row
|
|||
|
|
const existingRows = tbody.querySelectorAll('tr:not(.empty-row)');
|
|||
|
|
existingRows.forEach(row => row.remove());
|
|||
|
|
|
|||
|
|
let activeCount = 0;
|
|||
|
|
let expiredCount = 0;
|
|||
|
|
|
|||
|
|
history.forEach(item => {
|
|||
|
|
const isExpired = item.isExpired || item.isDeleted || new Date(item.expiresAt) < new Date();
|
|||
|
|
if (isExpired) expiredCount++;
|
|||
|
|
else activeCount++;
|
|||
|
|
|
|||
|
|
const row = createHistoryRow(item, isExpired);
|
|||
|
|
tbody.appendChild(row);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
updateStats(history.length, activeCount, expiredCount);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function createHistoryRow(item, isExpired) {
|
|||
|
|
const row = document.createElement('tr');
|
|||
|
|
row.className = isExpired ? 'expired' : 'active';
|
|||
|
|
|
|||
|
|
// Determine status text and actions based on server state
|
|||
|
|
let statusText = '✅ Active';
|
|||
|
|
let statusClass = 'active';
|
|||
|
|
let actions = '';
|
|||
|
|
|
|||
|
|
if (item.isDeleted) {
|
|||
|
|
statusText = '🗑️ Deleted';
|
|||
|
|
statusClass = 'deleted';
|
|||
|
|
actions = '<span class="expired-text">Deleted on server</span>';
|
|||
|
|
} else if (isExpired) {
|
|||
|
|
statusText = '❌ Expired';
|
|||
|
|
statusClass = 'expired';
|
|||
|
|
actions = '<span class="expired-text">Expired</span>';
|
|||
|
|
} else {
|
|||
|
|
actions = `<button onclick="copyLink('${item.url}')" class="btn btn-secondary btn-xs" title="Copy Link">📋</button>
|
|||
|
|
<a href="${item.url}" class="btn btn-primary btn-xs" target="_blank" title="View">👁️</a>
|
|||
|
|
<button onclick="editNotes('${item.id}')" class="btn btn-secondary btn-xs" title="Edit Notes">✏️</button>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Always show remove button for cleaning up history
|
|||
|
|
actions += `<button onclick="removeFromHistory('${item.id}')" class="btn btn-danger btn-xs" title="Remove">🗑️</button>`;
|
|||
|
|
|
|||
|
|
row.innerHTML = `
|
|||
|
|
<td class="date-cell">
|
|||
|
|
${formatDate(item.createdAt)}
|
|||
|
|
</td>
|
|||
|
|
<td class="date-cell">
|
|||
|
|
${formatDate(item.expiresAt)}
|
|||
|
|
</td>
|
|||
|
|
<td class="views-cell">
|
|||
|
|
${item.viewCount || 0}/${item.maxViews}
|
|||
|
|
</td>
|
|||
|
|
<td class="notes-cell">
|
|||
|
|
<span class="notes-text">${item.notes ? item.notes.substring(0, 30) + (item.notes.length > 30 ? '...' : '') : '-'}</span>
|
|||
|
|
</td>
|
|||
|
|
<td class="status-cell">
|
|||
|
|
<span class="status-badge ${statusClass}">
|
|||
|
|
${statusText}
|
|||
|
|
</span>
|
|||
|
|
</td>
|
|||
|
|
<td class="actions-cell">
|
|||
|
|
${actions}
|
|||
|
|
</td>
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
return row;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateStats(total, active, expired) {
|
|||
|
|
document.getElementById('totalCount').textContent = total;
|
|||
|
|
document.getElementById('activeCount').textContent = active;
|
|||
|
|
document.getElementById('expiredCount').textContent = expired;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function formatDate(dateString) {
|
|||
|
|
const date = new Date(dateString);
|
|||
|
|
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function copyLink(url) {
|
|||
|
|
const textArea = document.createElement('textarea');
|
|||
|
|
textArea.value = url;
|
|||
|
|
document.body.appendChild(textArea);
|
|||
|
|
textArea.select();
|
|||
|
|
document.execCommand('copy');
|
|||
|
|
document.body.removeChild(textArea);
|
|||
|
|
|
|||
|
|
// Show success feedback
|
|||
|
|
const btn = event.target;
|
|||
|
|
const originalText = btn.textContent;
|
|||
|
|
btn.textContent = '✅';
|
|||
|
|
btn.style.background = '#4caf50';
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
btn.textContent = originalText;
|
|||
|
|
btn.style.background = '';
|
|||
|
|
}, 1500);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function removeFromHistory(pushId) {
|
|||
|
|
if (confirm('Remove this item from your history?')) {
|
|||
|
|
const history = getHistoryFromCookie();
|
|||
|
|
const updatedHistory = history.filter(h => h.id !== pushId);
|
|||
|
|
saveHistoryToCookie(updatedHistory);
|
|||
|
|
loadHistory();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function editNotes(pushId) {
|
|||
|
|
const history = getHistoryFromCookie();
|
|||
|
|
const item = history.find(h => h.id === pushId);
|
|||
|
|
if (!item) return;
|
|||
|
|
|
|||
|
|
const currentNotes = item.notes || '';
|
|||
|
|
const newNotes = prompt('Add notes for this link (for your reference only):', currentNotes);
|
|||
|
|
|
|||
|
|
if (newNotes !== null) { // User didn't cancel
|
|||
|
|
item.notes = newNotes;
|
|||
|
|
saveHistoryToCookie(history);
|
|||
|
|
loadHistory();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function clearHistory() {
|
|||
|
|
if (confirm('Clear all history? This cannot be undone.')) {
|
|||
|
|
// Show loading state
|
|||
|
|
const clearBtn = document.querySelector('button[onclick="clearHistory()"]');
|
|||
|
|
const originalText = clearBtn.textContent;
|
|||
|
|
clearBtn.textContent = '🗑️ Clearing...';
|
|||
|
|
clearBtn.disabled = true;
|
|||
|
|
|
|||
|
|
// Clear the cookie
|
|||
|
|
document.cookie = "pwpush_history=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
|||
|
|
|
|||
|
|
// Refresh the display and restore button
|
|||
|
|
setTimeout(() => {
|
|||
|
|
loadHistory(); // Refresh the display to show cleared state
|
|||
|
|
clearBtn.textContent = '✅ Cleared!';
|
|||
|
|
|
|||
|
|
// Restore original text after showing success
|
|||
|
|
setTimeout(() => {
|
|||
|
|
clearBtn.textContent = originalText;
|
|||
|
|
clearBtn.disabled = false;
|
|||
|
|
}, 1500);
|
|||
|
|
}, 100);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getHistoryFromCookie() {
|
|||
|
|
const cookie = document.cookie
|
|||
|
|
.split('; ')
|
|||
|
|
.find(row => row.startsWith('pwpush_history='));
|
|||
|
|
|
|||
|
|
if (!cookie) return [];
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
return JSON.parse(decodeURIComponent(cookie.split('=')[1]));
|
|||
|
|
} catch (e) {
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function saveHistoryToCookie(history) {
|
|||
|
|
const expires = new Date();
|
|||
|
|
expires.setTime(expires.getTime() + (30 * 24 * 60 * 60 * 1000)); // 30 days
|
|||
|
|
document.cookie = `pwpush_history=${encodeURIComponent(JSON.stringify(history))};expires=${expires.toUTCString()};path=/`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getCookie(name) {
|
|||
|
|
const value = "; " + document.cookie;
|
|||
|
|
const parts = value.split("; " + name + "=");
|
|||
|
|
if (parts.length === 2) return parts.pop().split(";").shift();
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setCookie(name, value, days) {
|
|||
|
|
const expires = new Date();
|
|||
|
|
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
|
|||
|
|
document.cookie = name + "=" + value + ";expires=" + expires.toUTCString() + ";path=/";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Load history on page load
|
|||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|||
|
|
loadHistory();
|
|||
|
|
|
|||
|
|
// Check if this is a success page with a new link created
|
|||
|
|
const successData = document.querySelector('meta[name="success-data"]');
|
|||
|
|
if (successData) {
|
|||
|
|
const trackHistory = getCookie('pwpush_track_history');
|
|||
|
|
if (trackHistory === 'true') {
|
|||
|
|
// Add the new link to history
|
|||
|
|
const data = JSON.parse(successData.content);
|
|||
|
|
const newItem = {
|
|||
|
|
id: data.id,
|
|||
|
|
url: data.url,
|
|||
|
|
createdAt: new Date().toISOString(),
|
|||
|
|
expiresAt: data.expiresAt,
|
|||
|
|
maxViews: parseInt(getCookie('pwpush_max_views')) || 10,
|
|||
|
|
viewCount: 0,
|
|||
|
|
previewText: 'Content hidden for security'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let history = getHistoryFromCookie();
|
|||
|
|
history.unshift(newItem); // Add to beginning
|
|||
|
|
|
|||
|
|
// Keep only last 50 items
|
|||
|
|
if (history.length > 50) {
|
|||
|
|
history = history.slice(0, 50);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
saveHistoryToCookie(history);
|
|||
|
|
loadHistory(); // Refresh the display
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Load saved preferences
|
|||
|
|
const savedExpiry = getCookie('pwpush_expiry_days');
|
|||
|
|
const savedViews = getCookie('pwpush_max_views');
|
|||
|
|
const savedRequireClick = getCookie('pwpush_require_click');
|
|||
|
|
const savedAutoDelete = getCookie('pwpush_auto_delete');
|
|||
|
|
const savedTrackHistory = getCookie('pwpush_track_history');
|
|||
|
|
|
|||
|
|
if (savedExpiry) {
|
|||
|
|
document.getElementById('expiry_days').value = savedExpiry;
|
|||
|
|
updateExpiryDisplay(savedExpiry);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (savedViews) {
|
|||
|
|
document.getElementById('max_views').value = savedViews;
|
|||
|
|
updateViewsDisplay(savedViews);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (savedRequireClick !== null) {
|
|||
|
|
document.querySelector('input[name="require_click"]').checked = savedRequireClick === 'true';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (savedAutoDelete !== null) {
|
|||
|
|
document.querySelector('input[name="auto_delete"]').checked = savedAutoDelete === 'true';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (savedTrackHistory !== null) {
|
|||
|
|
document.querySelector('input[name="track_history"]').checked = savedTrackHistory === 'true';
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
|
|||
|
|
{{end}}
|