174 lines
9.6 KiB
HTML
174 lines
9.6 KiB
HTML
{{define "title"}}Let's Encrypt - Email Server Management{{end}}
|
|
{{define "page_title"}}Let's Encrypt{{end}}
|
|
|
|
{{define "content"}}
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h2><i class="bi bi-patch-check me-2"></i>Let's Encrypt</h2>
|
|
</div>
|
|
|
|
<div class="card mb-4">
|
|
<div class="card-header"><h5 class="mb-0"><i class="bi bi-shield-check me-2"></i>Status</h5></div>
|
|
<div class="card-body">
|
|
<dl class="row mb-3">
|
|
<dt class="col-sm-3">Mode</dt>
|
|
<dd class="col-sm-9">
|
|
{{if .status.Enabled}}
|
|
<span class="badge bg-success"><i class="bi bi-check-circle me-1"></i>Let's Encrypt {{if .status.Staging}}(staging){{end}}</span>
|
|
{{else}}
|
|
<span class="badge bg-secondary"><i class="bi bi-dash-circle me-1"></i>Self-signed (Let's Encrypt disabled)</span>
|
|
{{end}}
|
|
</dd>
|
|
<dt class="col-sm-3">Domains</dt>
|
|
<dd class="col-sm-9">{{if .status.Domains}}{{range .status.Domains}}<code>{{.}}</code> {{end}}{{else}}<span class="text-muted">none configured</span>{{end}}</dd>
|
|
<dt class="col-sm-3">Provider</dt>
|
|
<dd class="col-sm-9">{{if .status.Provider}}{{.status.Provider}}{{else}}<span class="text-muted">none selected</span>{{end}}</dd>
|
|
<dt class="col-sm-3">Certificate expires</dt>
|
|
<dd class="col-sm-9">{{if .status.NotAfter.IsZero}}<span class="text-muted">unknown</span>{{else}}{{strftime "%Y-%m-%d %H:%M" .status.NotAfter}}{{end}}</dd>
|
|
<dt class="col-sm-3">Last attempt</dt>
|
|
<dd class="col-sm-9">
|
|
{{if .status.LastAttempt.IsZero}}
|
|
<span class="text-muted">none yet this run</span>
|
|
{{else if .status.LastError}}
|
|
<span class="text-danger"><i class="bi bi-exclamation-triangle me-1"></i>{{strftime "%Y-%m-%d %H:%M" .status.LastAttempt}} — {{.status.LastError}}</span>
|
|
{{else}}
|
|
<span class="text-success"><i class="bi bi-check-circle me-1"></i>{{strftime "%Y-%m-%d %H:%M" .status.LastAttempt}} — success</span>
|
|
{{end}}
|
|
</dd>
|
|
</dl>
|
|
<form method="post" action="/pymta-manager/letsencrypt/obtain">
|
|
<button type="submit" class="btn btn-primary" data-confirm="Obtain or renew the certificate now using the saved configuration?"><i class="bi bi-arrow-repeat me-1"></i>Obtain / Renew Now</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<form method="POST" action="/pymta-manager/letsencrypt/save">
|
|
<div class="card mb-4">
|
|
<div class="card-header"><h5 class="mb-0"><i class="bi bi-gear me-2"></i>Configuration</h5></div>
|
|
<div class="card-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Enable Let's Encrypt</label>
|
|
<select class="form-select" name="enabled">
|
|
<option value="false" {{if ne .le.enabled "true"}}selected{{end}}>No — keep the self-signed certificate</option>
|
|
<option value="true" {{if eq .le.enabled "true"}}selected{{end}}>Yes</option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Staging mode</label>
|
|
<select class="form-select" name="staging">
|
|
<option value="false" {{if ne .le.staging "true"}}selected{{end}}>No — request a real, trusted certificate</option>
|
|
<option value="true" {{if eq .le.staging "true"}}selected{{end}}>Yes — untrusted test certificate, no rate limits</option>
|
|
</select>
|
|
<div class="form-text">Recommended while testing a new configuration.</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Contact Email</label>
|
|
<input type="email" class="form-control" name="contact_email" value="{{.le.contact_email}}">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Domains</label>
|
|
<input type="text" class="form-control font-monospace" name="domains" value="{{.le.domains}}" placeholder="mail.example.com,*.mail.example.com">
|
|
<div class="form-text">Comma-separated. Include a wildcard entry (e.g. <code>*.mail.example.com</code>) alongside its bare domain to cover both with one certificate.</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">DNS Provider</label>
|
|
<select class="form-select" name="dns_provider" id="le_provider">
|
|
<option value="">Select a provider...</option>
|
|
<option value="cloudflare" {{if eq .le.dns_provider "cloudflare"}}selected{{end}}>Cloudflare</option>
|
|
<option value="route53" {{if eq .le.dns_provider "route53"}}selected{{end}}>AWS Route53</option>
|
|
<option value="digitalocean" {{if eq .le.dns_provider "digitalocean"}}selected{{end}}>DigitalOcean</option>
|
|
<option value="gcloud" {{if eq .le.dns_provider "gcloud"}}selected{{end}}>Google Cloud DNS</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="provider-fields" id="fields-cloudflare">
|
|
<div class="setting-section mb-3">
|
|
<h6>Cloudflare</h6>
|
|
<div class="mb-3">
|
|
<label class="form-label">API Token</label>
|
|
<input type="password" class="form-control" name="cloudflare_api_token" placeholder="Leave blank to keep the current value">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="provider-fields" id="fields-route53">
|
|
<div class="setting-section mb-3">
|
|
<h6>AWS Route53</h6>
|
|
<div class="mb-3">
|
|
<label class="form-label">Access Key ID</label>
|
|
<input type="password" class="form-control" name="route53_access_key_id" placeholder="Leave blank to keep the current value, or blank both keys to use the host's AWS credential chain">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Secret Access Key</label>
|
|
<input type="password" class="form-control" name="route53_secret_access_key" placeholder="Leave blank to keep the current value">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Region</label>
|
|
<input type="text" class="form-control" name="route53_region" value="{{.le.route53_region}}" placeholder="us-east-1">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Hosted Zone ID (optional)</label>
|
|
<input type="text" class="form-control" name="route53_hosted_zone_id" value="{{.le.route53_hosted_zone_id}}" placeholder="Leave blank to auto-discover">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="provider-fields" id="fields-digitalocean">
|
|
<div class="setting-section mb-3">
|
|
<h6>DigitalOcean</h6>
|
|
<div class="mb-3">
|
|
<label class="form-label">API Token</label>
|
|
<input type="password" class="form-control" name="digitalocean_api_token" placeholder="Leave blank to keep the current value">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="provider-fields" id="fields-gcloud">
|
|
<div class="setting-section mb-3">
|
|
<h6>Google Cloud DNS</h6>
|
|
<div class="mb-3">
|
|
<label class="form-label">Project ID</label>
|
|
<input type="text" class="form-control" name="gcloud_project" value="{{.le.gcloud_project}}">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Service Account Key (optional)</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control font-monospace" name="gcloud_service_account_json_path" id="gcloud_sa_path" placeholder="Leave blank to use Application Default Credentials">
|
|
<input type="file" class="d-none" id="gcloudKeyUpload" accept=".json">
|
|
<button class="btn btn-outline-secondary" type="button" onclick="document.getElementById('gcloudKeyUpload').click()"><i class="bi bi-upload"></i></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-success"><i class="bi bi-check-lg me-1"></i>Save Configuration</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
{{end}}
|
|
|
|
{{define "extra_js"}}
|
|
<script>
|
|
function updateProviderFields() {
|
|
const selected = document.getElementById('le_provider').value;
|
|
document.querySelectorAll('.provider-fields').forEach(function(el) {
|
|
el.style.display = (el.id === 'fields-' + selected) ? '' : 'none';
|
|
});
|
|
}
|
|
document.getElementById('le_provider').addEventListener('change', updateProviderFields);
|
|
updateProviderFields();
|
|
|
|
document.getElementById('gcloudKeyUpload').addEventListener('change', function(e) {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
const formData = new FormData();
|
|
formData.append('gcloud_key_file', file);
|
|
fetch('/pymta-manager/api/letsencrypt/upload_gcloud_key', { method: 'POST', body: formData })
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.status === 'success') { document.getElementById('gcloud_sa_path').value = data.filepath; showToast('Service account key uploaded', 'success'); }
|
|
else { showToast(data.message || 'Failed to upload key', 'danger'); }
|
|
}).catch(() => showToast('Failed to upload key', 'danger'));
|
|
});
|
|
</script>
|
|
{{end}}
|