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,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,54 +364,51 @@ 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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user