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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user