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