Files

282 lines
9.1 KiB
HTML

{{ define "settings" }}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Settings - ATCR</title>
{{ template "head" . }}
</head>
<body>
{{ template "nav" . }}
<main class="container">
<div class="settings-page">
<h1>Settings</h1>
<!-- Identity Section -->
<section class="settings-section">
<h2>Identity</h2>
<div class="form-group">
<label>Handle:</label>
<span>{{ .Profile.Handle }}</span>
</div>
<div class="form-group">
<label>DID:</label>
<code>{{ .Profile.DID }}</code>
</div>
<div class="form-group">
<label>PDS:</label>
<span>{{ .Profile.PDSEndpoint }}</span>
</div>
</section>
<!-- Default Hold Section -->
<section class="settings-section">
<h2>Default Hold</h2>
<p>Current: <strong id="current-hold">{{ if .Profile.DefaultHold }}{{ .Profile.DefaultHold }}{{ else }}Not set{{ end }}</strong></p>
<form hx-post="/api/profile/default-hold"
hx-target="#hold-status"
hx-swap="innerHTML"
id="hold-form">
<div class="form-group">
<label for="hold-endpoint">Hold Endpoint:</label>
<input type="text"
id="hold-endpoint"
name="hold_endpoint"
value="{{ .Profile.DefaultHold }}"
placeholder="https://hold.example.com" />
<small>Leave empty to use AppView default storage</small>
</div>
<button type="submit" class="btn-primary">Save</button>
</form>
<div id="hold-status"></div>
</section>
<!-- Authorized Devices Section -->
<section class="settings-section devices-section">
<h2>Authorized Devices</h2>
<p>Devices authorized via <code>docker-credential-atcr</code> credential helper.</p>
<!-- Setup Instructions -->
<div class="setup-instructions">
<h3>First Time Setup</h3>
<ol>
<li>Install credential helper:
<pre><code>curl -fsSL atcr.io/static/install.sh | bash</code></pre>
</li>
<li>Configure Docker to use the helper. Add to <code>~/.docker/config.json</code>:
<pre><code>{
"credHelpers": {
"{{ .RegistryURL }}": "atcr"
}
}</code></pre>
</li>
<li>Run any Docker command:
{{ template "docker-command" (print "docker pull " .RegistryURL "/" .Profile.Handle "/myimage") }}
</li>
<li>Browser will open for authorization - click Approve</li>
<li>Done! Device is automatically authorized</li>
</ol>
<div class="fallback-note">
<strong>Fallback:</strong> Use <a href="https://bsky.app/settings/app-passwords" target="_blank">app password</a> with <code>docker login {{ .RegistryURL }}</code> for quick start (no device tracking)
</div>
</div>
<!-- Devices List -->
<div class="devices-list">
<h3>Your Authorized Devices</h3>
<table>
<thead>
<tr>
<th>Device Name</th>
<th>IP Address</th>
<th>Created</th>
<th>Last Used</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="devices-table">
<tr><td colspan="5">Loading...</td></tr>
</tbody>
</table>
</div>
</section>
</div>
</main>
<script>
// Default Hold Update - Dynamic display update
document.addEventListener('DOMContentLoaded', function() {
const holdForm = document.getElementById('hold-form');
holdForm.addEventListener('htmx:afterSwap', function(event) {
// Check if the response contains success indicator
if (event.detail.xhr.status === 200) {
const holdInput = document.getElementById('hold-endpoint');
const currentHoldDisplay = document.getElementById('current-hold');
const newValue = holdInput.value.trim();
// Update the current hold display
currentHoldDisplay.textContent = newValue || 'Not set';
}
});
});
// Device Management JavaScript
(function() {
// Load devices
async function loadDevices() {
try {
const resp = await fetch('/api/devices');
if (!resp.ok) {
throw new Error('Failed to load devices');
}
const devices = await resp.json();
const tbody = document.getElementById('devices-table');
if (devices.length === 0) {
tbody.innerHTML = '<tr><td colspan="5">No authorized devices yet. Follow the setup instructions above!</td></tr>';
return;
}
tbody.innerHTML = devices.map(device => {
const createdDate = new Date(device.created_at).toLocaleDateString();
const lastUsed = device.last_used && device.last_used !== '0001-01-01T00:00:00Z'
? new Date(device.last_used).toLocaleDateString()
: 'Never';
return `
<tr>
<td>${escapeHtml(device.name)}</td>
<td>${escapeHtml(device.ip_address || 'Unknown')}</td>
<td>${createdDate}</td>
<td>${lastUsed}</td>
<td><button class="btn-danger" onclick="revokeDevice('${device.id}')">Revoke</button></td>
</tr>
`;
}).join('');
} catch (err) {
console.error('Error loading devices:', err);
document.getElementById('devices-table').innerHTML =
'<tr><td colspan="5">Error loading devices</td></tr>';
}
}
// Revoke device
window.revokeDevice = async function(id) {
if (!confirm('Are you sure you want to revoke this device? This cannot be undone.')) {
return;
}
try {
const resp = await fetch(`/api/devices/${id}`, { method: 'DELETE' });
if (!resp.ok) {
throw new Error('Failed to revoke device');
}
loadDevices();
} catch (err) {
alert('Error revoking device: ' + err.message);
}
};
// Escape HTML helper
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Load devices on page load
loadDevices();
// Refresh devices every 30 seconds (to show new authorizations)
setInterval(loadDevices, 30000);
})();
</script>
<style>
/* Devices Section Styles */
.devices-section .setup-instructions {
margin: 1rem 0;
padding: 1.5rem;
background: var(--code-bg);
border-radius: 4px;
}
.devices-section .setup-instructions h3 {
margin-top: 0;
}
.devices-section .setup-instructions ol {
margin-left: 1.5rem;
}
.devices-section .setup-instructions li {
margin-bottom: 1rem;
}
.devices-section .setup-instructions pre {
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
padding: 0.75rem;
border-radius: 4px;
overflow-x: auto;
margin: 0.5rem 0;
}
.devices-section .setup-instructions code {
font-family: monospace;
}
.devices-section .fallback-note {
margin-top: 1rem;
padding: 1rem;
background: var(--warning-bg);
border: 1px solid var(--warning);
border-radius: 4px;
}
.devices-section .fallback-note a {
color: var(--warning);
text-decoration: underline;
font-weight: 500;
}
.devices-section .fallback-note a:hover {
color: var(--primary);
}
.devices-section .fallback-note a:visited {
color: var(--warning);
}
.devices-section table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
.devices-section th,
.devices-section td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--border);
}
.devices-section th {
background: var(--code-bg);
font-weight: bold;
}
.devices-section .btn-danger {
background: #dc3545;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
}
.devices-section .btn-danger:hover {
background: #c82333;
}
.devices-list {
margin-top: 2rem;
}
</style>
</body>
</html>
{{ end }}