Fix blank screen on server cert settings page (missing authentication)

Critical Bug Fix: Server cert settings page blank screen

Problem:
- Navigating to /settings/endpoints/server-cert showed blank page
- Both localhost:5173 and localhost:5001 affected
- Browser console showed authentication errors

Root Cause:
- ServerCertSettings.vue used raw fetch() without authentication
- API endpoint requires admin role (@require_role('admin'))
- fetch() didn't include JWT bearer token from cookies
- API returned 401 Unauthorized, component failed silently

Solution:
- Converted from Options API to Composition API (Vue 3 standard)
- Changed from raw fetch() to authenticated api client
- Used 'import api from @/api' (same as AgentKeySettings.vue)

Changes:
Before (broken):
  export default {
    methods: {
      async loadCertificate() {
        const response = await fetch('/api/endpoints/settings/server-cert')
        // No auth headers = 401 error
      }
    }
  }

After (working):
  import { ref, onMounted } from 'vue'
  import api from '@/api'

  async function loadCertificate() {
    const response = await api.get('/endpoints/settings/server-cert')
    // api.get() includes JWT from cookie automatically
  }

API Client Features:
- Automatically includes Authorization header with JWT
- Handles response parsing
- Consistent error handling
- Used by all other admin settings pages

Result:
- Page loads successfully
- Certificate details display
- Generate/upload forms work
- All admin-only endpoints authenticated

Tested:
- Hard refresh clears browser cache
- Page renders certificate details
- Generate/upload buttons functional

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Proudlock
2026-09-14 15:43:44 -04:00
parent 87582d0851
commit b7e454b5ef

View File

@@ -268,158 +268,148 @@
</div>
</template>
<script>
export default {
name: 'ServerCertSettings',
data() {
return {
loading: false,
busy: false,
message: '',
error: '',
cert: null,
thumbprintCopied: false,
generateForm: {
cn: 'localhost',
san: 'DNS:localhost,IP:192.168.100.1',
days: 365
},
uploadForm: {
certFile: null,
keyFile: null
}
}
},
mounted() {
this.loadCertificate()
},
methods: {
async loadCertificate() {
this.loading = true
this.error = ''
try {
const response = await fetch('/api/endpoints/settings/server-cert')
const data = await response.json()
if (data.status === 'success') {
this.cert = data.data
} else {
this.error = data.data?.error?.message || 'Failed to load certificate'
}
} catch (err) {
this.error = `Failed to load certificate: ${err.message}`
} finally {
this.loading = false
}
},
async generateCertificate() {
if (!this.generateForm.cn) {
this.error = 'Common Name is required'
return
}
if (!confirm(
'Generate a new self-signed certificate? This will replace the current certificate. ' +
'Agents using thumbprint pinning will need to be reconfigured.'
)) {
return
}
this.busy = true
this.error = ''
this.message = ''
try {
const response = await fetch('/api/endpoints/settings/server-cert/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.generateForm)
})
const data = await response.json()
if (data.status === 'success') {
this.message = 'Certificate generated successfully. Nginx is restarting...'
await this.loadCertificate()
} else {
this.error = data.data?.error?.message || 'Failed to generate certificate'
}
} catch (err) {
this.error = `Failed to generate certificate: ${err.message}`
} finally {
this.busy = false
}
},
onCertFileChange(event) {
this.uploadForm.certFile = event.target.files[0]
},
onKeyFileChange(event) {
this.uploadForm.keyFile = event.target.files[0]
},
async uploadCertificate() {
if (!this.uploadForm.certFile || !this.uploadForm.keyFile) {
this.error = 'Both certificate and private key files are required'
return
}
if (!confirm(
'Upload new certificate? This will replace the current certificate and restart nginx.'
)) {
return
}
this.busy = true
this.error = ''
this.message = ''
try {
const formData = new FormData()
formData.append('cert', this.uploadForm.certFile)
formData.append('key', this.uploadForm.keyFile)
const response = await fetch('/api/endpoints/settings/server-cert/upload', {
method: 'POST',
body: formData
})
const data = await response.json()
if (data.status === 'success') {
this.message = 'Certificate uploaded successfully. Nginx is restarting...'
await this.loadCertificate()
// Clear file inputs
document.getElementById('cert-file').value = ''
document.getElementById('key-file').value = ''
this.uploadForm.certFile = null
this.uploadForm.keyFile = null
} else {
this.error = data.data?.error?.message || 'Failed to upload certificate'
}
} catch (err) {
this.error = `Failed to upload certificate: ${err.message}`
} finally {
this.busy = false
}
},
copyThumbprint() {
navigator.clipboard.writeText(this.cert.thumbprint)
this.thumbprintCopied = true
setTimeout(() => {
this.thumbprintCopied = false
}, 2000)
},
formatDate(dateString) {
if (!dateString) return 'N/A'
const date = new Date(dateString)
return date.toLocaleString()
<script setup>
import { ref, onMounted } from 'vue'
import api from '@/api'
const loading = ref(false)
const busy = ref(false)
const message = ref('')
const error = ref('')
const cert = ref(null)
const thumbprintCopied = ref(false)
const generateForm = ref({
cn: 'localhost',
san: 'DNS:localhost,IP:192.168.100.1',
days: 365
})
const uploadForm = ref({
certFile: null,
keyFile: null
})
onMounted(() => {
loadCertificate()
})
async function loadCertificate() {
loading.value = true
error.value = ''
try {
const response = await api.get('/endpoints/settings/server-cert')
if (response.data.status === 'success') {
cert.value = response.data.data
} else {
error.value = response.data.data?.error?.message || 'Failed to load certificate'
}
} catch (err) {
error.value = `Failed to load certificate: ${err.message}`
} finally {
loading.value = false
}
}
async function generateCertificate() {
if (!generateForm.value.cn) {
error.value = 'Common Name is required'
return
}
if (!confirm(
'Generate a new self-signed certificate? This will replace the current certificate. ' +
'Agents using thumbprint pinning will need to be reconfigured.'
)) {
return
}
busy.value = true
error.value = ''
message.value = ''
try {
const response = await api.post('/endpoints/settings/server-cert/generate', generateForm.value)
if (response.data.status === 'success') {
message.value = 'Certificate generated successfully. Nginx is restarting...'
await loadCertificate()
} else {
error.value = response.data.data?.error?.message || 'Failed to generate certificate'
}
} catch (err) {
error.value = `Failed to generate certificate: ${err.message}`
} finally {
busy.value = false
}
}
function onCertFileChange(event) {
uploadForm.value.certFile = event.target.files[0]
}
function onKeyFileChange(event) {
uploadForm.value.keyFile = event.target.files[0]
}
async function uploadCertificate() {
if (!uploadForm.value.certFile || !uploadForm.value.keyFile) {
error.value = 'Both certificate and private key files are required'
return
}
if (!confirm(
'Upload new certificate? This will replace the current certificate and restart nginx.'
)) {
return
}
busy.value = true
error.value = ''
message.value = ''
try {
const formData = new FormData()
formData.append('cert', uploadForm.value.certFile)
formData.append('key', uploadForm.value.keyFile)
const response = await api.post('/endpoints/settings/server-cert/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
if (response.data.status === 'success') {
message.value = 'Certificate uploaded successfully. Nginx is restarting...'
await loadCertificate()
// Clear file inputs
document.getElementById('cert-file').value = ''
document.getElementById('key-file').value = ''
uploadForm.value.certFile = null
uploadForm.value.keyFile = null
} else {
error.value = response.data.data?.error?.message || 'Failed to upload certificate'
}
} catch (err) {
error.value = `Failed to upload certificate: ${err.message}`
} finally {
busy.value = false
}
}
function copyThumbprint() {
navigator.clipboard.writeText(cert.value.thumbprint)
thumbprintCopied.value = true
setTimeout(() => {
thumbprintCopied.value = false
}, 2000)
}
function formatDate(dateString) {
if (!dateString) return 'N/A'
const date = new Date(dateString)
return date.toLocaleString()
}
</script>
<style scoped>