diff --git a/client/components/app/ConfigSideNav.vue b/client/components/app/ConfigSideNav.vue
index 32e7e694a..50fa7a06f 100644
--- a/client/components/app/ConfigSideNav.vue
+++ b/client/components/app/ConfigSideNav.vue
@@ -70,11 +70,6 @@ export default {
title: this.$strings.HeaderUsers,
path: '/config/users'
},
- {
- id: 'config-api-keys',
- title: this.$strings.HeaderApiKeys,
- path: '/config/api-keys'
- },
{
id: 'config-sessions',
title: this.$strings.HeaderListeningSessions,
diff --git a/client/components/app/LazyBookshelf.vue b/client/components/app/LazyBookshelf.vue
index 854b61b24..61331fb9e 100644
--- a/client/components/app/LazyBookshelf.vue
+++ b/client/components/app/LazyBookshelf.vue
@@ -778,6 +778,10 @@ export default {
windowResize() {
this.executeRebuild()
},
+ socketInit() {
+ // Server settings are set on socket init
+ this.executeRebuild()
+ },
initListeners() {
window.addEventListener('resize', this.windowResize)
@@ -790,6 +794,7 @@ export default {
})
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
+ this.$eventBus.$on('socket_init', this.socketInit)
this.$eventBus.$on('user-settings', this.settingsUpdated)
if (this.$root.socket) {
@@ -821,6 +826,7 @@ export default {
}
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
+ this.$eventBus.$off('socket_init', this.socketInit)
this.$eventBus.$off('user-settings', this.settingsUpdated)
if (this.$root.socket) {
diff --git a/client/components/cards/AuthorCard.vue b/client/components/cards/AuthorCard.vue
index 053473934..82645c570 100644
--- a/client/components/cards/AuthorCard.vue
+++ b/client/components/cards/AuthorCard.vue
@@ -71,6 +71,9 @@ export default {
coverHeight() {
return this.cardHeight
},
+ userToken() {
+ return this.store.getters['user/getToken']
+ },
_author() {
return this.author || {}
},
diff --git a/client/components/covers/AuthorImage.vue b/client/components/covers/AuthorImage.vue
index 084492b0c..e320e552e 100644
--- a/client/components/covers/AuthorImage.vue
+++ b/client/components/covers/AuthorImage.vue
@@ -39,6 +39,9 @@ export default {
}
},
computed: {
+ userToken() {
+ return this.$store.getters['user/getToken']
+ },
_author() {
return this.author || {}
},
diff --git a/client/components/modals/AccountModal.vue b/client/components/modals/AccountModal.vue
index 6f4b7b674..71ac81550 100644
--- a/client/components/modals/AccountModal.vue
+++ b/client/components/modals/AccountModal.vue
@@ -309,9 +309,9 @@ export default {
} else {
console.log('Account updated', data.user)
- if (data.user.id === this.user.id && data.user.accessToken !== this.user.accessToken) {
- console.log('Current user access token was updated')
- this.$store.commit('user/setAccessToken', data.user.accessToken)
+ if (data.user.id === this.user.id && data.user.token !== this.user.token) {
+ console.log('Current user token was updated')
+ this.$store.commit('user/setUserToken', data.user.token)
}
this.$toast.success(this.$strings.ToastAccountUpdateSuccess)
@@ -351,6 +351,9 @@ export default {
this.$toast.error(errMsg || 'Failed to create account')
})
},
+ toggleActive() {
+ this.newUser.isActive = !this.newUser.isActive
+ },
userTypeUpdated(type) {
this.newUser.permissions = {
download: type !== 'guest',
diff --git a/client/components/modals/ApiKeyCreatedModal.vue b/client/components/modals/ApiKeyCreatedModal.vue
deleted file mode 100644
index 96442a17f..000000000
--- a/client/components/modals/ApiKeyCreatedModal.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/client/components/modals/ApiKeyModal.vue b/client/components/modals/ApiKeyModal.vue
deleted file mode 100644
index b347abd00..000000000
--- a/client/components/modals/ApiKeyModal.vue
+++ /dev/null
@@ -1,198 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/client/components/modals/Modal.vue b/client/components/modals/Modal.vue
index 31ea1e610..a7d9c0ae2 100644
--- a/client/components/modals/Modal.vue
+++ b/client/components/modals/Modal.vue
@@ -23,7 +23,7 @@ export default {
processing: Boolean,
persistent: {
type: Boolean,
- default: false
+ default: true
},
width: {
type: [String, Number],
@@ -99,7 +99,7 @@ export default {
this.preventClickoutside = false
return
}
- if (this.processing || this.persistent) return
+ if (this.processing && this.persistent) return
if (ev.srcElement && ev.srcElement.classList.contains('modal-bg')) {
this.show = false
}
diff --git a/client/components/modals/item/tabs/Files.vue b/client/components/modals/item/tabs/Files.vue
index 15c442619..7be286fef 100644
--- a/client/components/modals/item/tabs/Files.vue
+++ b/client/components/modals/item/tabs/Files.vue
@@ -29,6 +29,9 @@ export default {
media() {
return this.libraryItem.media || {}
},
+ userToken() {
+ return this.$store.getters['user/getToken']
+ },
userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate']
},
diff --git a/client/components/player/PlayerUi.vue b/client/components/player/PlayerUi.vue
index f929943c4..82d535528 100644
--- a/client/components/player/PlayerUi.vue
+++ b/client/components/player/PlayerUi.vue
@@ -129,6 +129,9 @@ export default {
return `${hoursRounded}h`
}
},
+ token() {
+ return this.$store.getters['user/getToken']
+ },
timeRemaining() {
if (this.useChapterTrack && this.currentChapter) {
var currChapTime = this.currentTime - this.currentChapter.start
diff --git a/client/components/readers/ComicReader.vue b/client/components/readers/ComicReader.vue
index fce269396..28d79bf2a 100644
--- a/client/components/readers/ComicReader.vue
+++ b/client/components/readers/ComicReader.vue
@@ -104,6 +104,9 @@ export default {
}
},
computed: {
+ userToken() {
+ return this.$store.getters['user/getToken']
+ },
libraryItemId() {
return this.libraryItem?.id
},
@@ -231,7 +234,10 @@ export default {
async extract() {
this.loading = true
var buff = await this.$axios.$get(this.ebookUrl, {
- responseType: 'blob'
+ responseType: 'blob',
+ headers: {
+ Authorization: `Bearer ${this.userToken}`
+ }
})
const archive = await Archive.open(buff)
const originalFilesObject = await archive.getFilesObject()
diff --git a/client/components/readers/EpubReader.vue b/client/components/readers/EpubReader.vue
index ac8e3397a..350d8596c 100644
--- a/client/components/readers/EpubReader.vue
+++ b/client/components/readers/EpubReader.vue
@@ -57,6 +57,9 @@ export default {
}
},
computed: {
+ userToken() {
+ return this.$store.getters['user/getToken']
+ },
/** @returns {string} */
libraryItemId() {
return this.libraryItem?.id
@@ -94,9 +97,9 @@ export default {
},
ebookUrl() {
if (this.fileId) {
- return `/api/items/${this.libraryItemId}/ebook/${this.fileId}`
+ return `${this.$config.routerBasePath}/api/items/${this.libraryItemId}/ebook/${this.fileId}`
}
- return `/api/items/${this.libraryItemId}/ebook`
+ return `${this.$config.routerBasePath}/api/items/${this.libraryItemId}/ebook`
},
themeRules() {
const isDark = this.ereaderSettings.theme === 'dark'
@@ -306,24 +309,14 @@ export default {
/** @type {EpubReader} */
const reader = this
- // Use axios to make request because we have token refresh logic in interceptor
- const customRequest = async (url) => {
- try {
- return this.$axios.$get(url, {
- responseType: 'arraybuffer'
- })
- } catch (error) {
- console.error('EpubReader.initEpub customRequest failed:', error)
- throw error
- }
- }
-
/** @type {ePub.Book} */
reader.book = new ePub(reader.ebookUrl, {
width: this.readerWidth,
height: this.readerHeight - 50,
openAs: 'epub',
- requestMethod: customRequest
+ requestHeaders: {
+ Authorization: `Bearer ${this.userToken}`
+ }
})
/** @type {ePub.Rendition} */
@@ -344,33 +337,29 @@ export default {
this.applyTheme()
})
- reader.book.ready
- .then(() => {
- // set up event listeners
- reader.rendition.on('relocated', reader.relocated)
- reader.rendition.on('keydown', reader.keyUp)
+ reader.book.ready.then(() => {
+ // set up event listeners
+ reader.rendition.on('relocated', reader.relocated)
+ reader.rendition.on('keydown', reader.keyUp)
- reader.rendition.on('touchstart', (event) => {
- this.$emit('touchstart', event)
- })
- reader.rendition.on('touchend', (event) => {
- this.$emit('touchend', event)
- })
+ reader.rendition.on('touchstart', (event) => {
+ this.$emit('touchstart', event)
+ })
+ reader.rendition.on('touchend', (event) => {
+ this.$emit('touchend', event)
+ })
- // load ebook cfi locations
- const savedLocations = this.loadLocations()
- if (savedLocations) {
- reader.book.locations.load(savedLocations)
- } else {
- reader.book.locations.generate().then(() => {
- this.checkSaveLocations(reader.book.locations.save())
- })
- }
- this.getChapters()
- })
- .catch((error) => {
- console.error('EpubReader.initEpub failed:', error)
- })
+ // load ebook cfi locations
+ const savedLocations = this.loadLocations()
+ if (savedLocations) {
+ reader.book.locations.load(savedLocations)
+ } else {
+ reader.book.locations.generate().then(() => {
+ this.checkSaveLocations(reader.book.locations.save())
+ })
+ }
+ this.getChapters()
+ })
},
getChapters() {
// Load the list of chapters in the book. See https://github.com/futurepress/epub.js/issues/759
diff --git a/client/components/readers/MobiReader.vue b/client/components/readers/MobiReader.vue
index 459ae55bf..3e784f775 100644
--- a/client/components/readers/MobiReader.vue
+++ b/client/components/readers/MobiReader.vue
@@ -26,6 +26,9 @@ export default {
return {}
},
computed: {
+ userToken() {
+ return this.$store.getters['user/getToken']
+ },
libraryItemId() {
return this.libraryItem?.id
},
@@ -93,8 +96,11 @@ export default {
},
async initMobi() {
// Fetch mobi file as blob
- const buff = await this.$axios.$get(this.ebookUrl, {
- responseType: 'blob'
+ var buff = await this.$axios.$get(this.ebookUrl, {
+ responseType: 'blob',
+ headers: {
+ Authorization: `Bearer ${this.userToken}`
+ }
})
var reader = new FileReader()
reader.onload = async (event) => {
diff --git a/client/components/readers/PdfReader.vue b/client/components/readers/PdfReader.vue
index d9459d768..c05f459c7 100644
--- a/client/components/readers/PdfReader.vue
+++ b/client/components/readers/PdfReader.vue
@@ -55,8 +55,7 @@ export default {
loadedRatio: 0,
page: 1,
numPages: 0,
- pdfDocInitParams: null,
- isRefreshing: false
+ pdfDocInitParams: null
}
},
computed: {
@@ -153,34 +152,7 @@ export default {
this.page++
this.updateProgress()
},
- async refreshToken() {
- if (this.isRefreshing) return
- this.isRefreshing = true
- const newAccessToken = await this.$store.dispatch('user/refreshToken').catch((error) => {
- console.error('Failed to refresh token', error)
- return null
- })
- if (!newAccessToken) {
- // Redirect to login on failed refresh
- this.$router.push('/login')
- return
- }
-
- // Force Vue to re-render the PDF component by creating a new object
- this.pdfDocInitParams = {
- url: this.ebookUrl,
- httpHeaders: {
- Authorization: `Bearer ${newAccessToken}`
- }
- }
- this.isRefreshing = false
- },
- async error(err) {
- if (err && err.status === 401) {
- console.log('Received 401 error, refreshing token')
- await this.refreshToken()
- return
- }
+ error(err) {
console.error(err)
},
resize() {
diff --git a/client/components/readers/Reader.vue b/client/components/readers/Reader.vue
index a7a5ac3d5..c2e5986ec 100644
--- a/client/components/readers/Reader.vue
+++ b/client/components/readers/Reader.vue
@@ -266,6 +266,9 @@ export default {
isComic() {
return this.ebookFormat == 'cbz' || this.ebookFormat == 'cbr'
},
+ userToken() {
+ return this.$store.getters['user/getToken']
+ },
keepProgress() {
return this.$store.state.ereaderKeepProgress
},
diff --git a/client/components/tables/ApiKeysTable.vue b/client/components/tables/ApiKeysTable.vue
deleted file mode 100644
index feab4e681..000000000
--- a/client/components/tables/ApiKeysTable.vue
+++ /dev/null
@@ -1,177 +0,0 @@
-
-
-
-
-
- | {{ $strings.LabelName }} |
- {{ $strings.LabelApiKeyUser }} |
- {{ $strings.LabelExpiresAt }} |
- {{ $strings.LabelCreatedAt }} |
- |
-
-
- |
-
- |
-
-
- {{ apiKey.user.username }}
-
- Error
- |
-
- {{ getExpiresAtText(apiKey) }}
- {{ $strings.LabelExpiresNever }}
- |
-
-
- {{ $formatJsDate(new Date(apiKey.createdAt), dateFormat) }}
-
- |
-
-
-
-
-
-
-
-
-
- |
-
-
-
{{ $strings.LabelNoApiKeys }}
-
-
-
-
-
-
-
diff --git a/client/components/tables/EbookFilesTable.vue b/client/components/tables/EbookFilesTable.vue
index 3ce9d30f0..cc968acd7 100644
--- a/client/components/tables/EbookFilesTable.vue
+++ b/client/components/tables/EbookFilesTable.vue
@@ -49,6 +49,9 @@ export default {
libraryItemId() {
return this.libraryItem.id
},
+ userToken() {
+ return this.$store.getters['user/getToken']
+ },
userCanDownload() {
return this.$store.getters['user/getUserCanDownload']
},
diff --git a/client/components/tables/LibraryFilesTable.vue b/client/components/tables/LibraryFilesTable.vue
index 6f6e74b8c..9be7e2495 100644
--- a/client/components/tables/LibraryFilesTable.vue
+++ b/client/components/tables/LibraryFilesTable.vue
@@ -53,6 +53,9 @@ export default {
libraryItemId() {
return this.libraryItem.id
},
+ userToken() {
+ return this.$store.getters['user/getToken']
+ },
userCanDownload() {
return this.$store.getters['user/getUserCanDownload']
},
diff --git a/client/components/ui/MultiSelectQueryInput.vue b/client/components/ui/MultiSelectQueryInput.vue
index 18abc66e7..6de16bf24 100644
--- a/client/components/ui/MultiSelectQueryInput.vue
+++ b/client/components/ui/MultiSelectQueryInput.vue
@@ -85,6 +85,9 @@ export default {
this.$emit('input', val)
}
},
+ userToken() {
+ return this.$store.getters['user/getToken']
+ },
wrapperClass() {
var classes = []
if (this.disabled) classes.push('bg-black-300')
diff --git a/client/components/ui/SelectInput.vue b/client/components/ui/SelectInput.vue
index f38414ac0..9e0961c10 100644
--- a/client/components/ui/SelectInput.vue
+++ b/client/components/ui/SelectInput.vue
@@ -1,9 +1,9 @@
-
{{ label }}
+
{{ label }}
@@ -21,7 +21,6 @@ export default {
type: String,
default: 'text'
},
- min: [String, Number],
readonly: Boolean,
disabled: Boolean,
inputClass: String,
diff --git a/client/layouts/default.vue b/client/layouts/default.vue
index 9f15af67e..33e7aa15b 100644
--- a/client/layouts/default.vue
+++ b/client/layouts/default.vue
@@ -33,7 +33,6 @@ export default {
return {
socket: null,
isSocketConnected: false,
- isSocketAuthenticated: false,
isFirstSocketConnection: true,
socketConnectionToastId: null,
currentLang: null,
@@ -82,28 +81,9 @@ export default {
document.body.classList.add('app-bar')
}
},
- tokenRefreshed(newAccessToken) {
- if (this.isSocketConnected && !this.isSocketAuthenticated) {
- console.log('[SOCKET] Re-authenticating socket after token refresh')
- this.socket.emit('auth', newAccessToken)
- }
- },
updateSocketConnectionToast(content, type, timeout) {
if (this.socketConnectionToastId !== null && this.socketConnectionToastId !== undefined) {
- const toastUpdateOptions = {
- content: content,
- options: {
- timeout: timeout,
- type: type,
- closeButton: false,
- position: 'bottom-center',
- onClose: () => {
- this.socketConnectionToastId = null
- },
- closeOnClick: timeout !== null
- }
- }
- this.$toast.update(this.socketConnectionToastId, toastUpdateOptions, false)
+ this.$toast.update(this.socketConnectionToastId, { content: content, options: { timeout: timeout, type: type, closeButton: false, position: 'bottom-center', onClose: () => null, closeOnClick: timeout !== null } }, false)
} else {
this.socketConnectionToastId = this.$toast[type](content, { position: 'bottom-center', timeout: timeout, closeButton: false, closeOnClick: timeout !== null })
}
@@ -129,7 +109,7 @@ export default {
this.updateSocketConnectionToast(this.$strings.ToastSocketDisconnected, 'error', null)
},
reconnect() {
- console.log('[SOCKET] reconnected')
+ console.error('[SOCKET] reconnected')
},
reconnectAttempt(val) {
console.log(`[SOCKET] reconnect attempt ${val}`)
@@ -140,10 +120,6 @@ export default {
reconnectFailed() {
console.error('[SOCKET] reconnect failed')
},
- authFailed(payload) {
- console.error('[SOCKET] auth failed', payload.message)
- this.isSocketAuthenticated = false
- },
init(payload) {
console.log('Init Payload', payload)
@@ -151,7 +127,7 @@ export default {
this.$store.commit('users/setUsersOnline', payload.usersOnline)
}
- this.isSocketAuthenticated = true
+ this.$eventBus.$emit('socket_init')
},
streamOpen(stream) {
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamOpen(stream)
@@ -378,15 +354,6 @@ export default {
this.$store.commit('scanners/removeCustomMetadataProvider', provider)
},
initializeSocket() {
- if (this.$root.socket) {
- // Can happen in dev due to hot reload
- console.warn('Socket already initialized')
- this.socket = this.$root.socket
- this.isSocketConnected = this.$root.socket?.connected
- this.isFirstSocketConnection = false
- this.socketConnectionToastId = null
- return
- }
this.socket = this.$nuxtSocket({
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
persist: 'main',
@@ -397,7 +364,6 @@ export default {
path: `${this.$config.routerBasePath}/socket.io`
})
this.$root.socket = this.socket
- this.isSocketAuthenticated = false
console.log('Socket initialized')
// Pre-defined socket events
@@ -411,7 +377,6 @@ export default {
// Event received after authorizing socket
this.socket.on('init', this.init)
- this.socket.on('auth_failed', this.authFailed)
// Stream Listeners
this.socket.on('stream_open', this.streamOpen)
@@ -606,7 +571,6 @@ export default {
this.updateBodyClass()
this.resize()
this.$eventBus.$on('change-lang', this.changeLanguage)
- this.$eventBus.$on('token_refreshed', this.tokenRefreshed)
window.addEventListener('resize', this.resize)
window.addEventListener('keydown', this.keyDown)
@@ -630,7 +594,6 @@ export default {
},
beforeDestroy() {
this.$eventBus.$off('change-lang', this.changeLanguage)
- this.$eventBus.$off('token_refreshed', this.tokenRefreshed)
window.removeEventListener('resize', this.resize)
window.removeEventListener('keydown', this.keyDown)
}
diff --git a/client/nuxt.config.js b/client/nuxt.config.js
index 7219c7847..f54d1cf49 100644
--- a/client/nuxt.config.js
+++ b/client/nuxt.config.js
@@ -73,8 +73,7 @@ module.exports = {
// Axios module configuration: https://go.nuxtjs.dev/config-axios
axios: {
- baseURL: routerBasePath,
- progress: false
+ baseURL: routerBasePath
},
// nuxt/pwa https://pwa.nuxtjs.org
diff --git a/client/package-lock.json b/client/package-lock.json
index 406ef9dbe..db21b43f5 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "audiobookshelf-client",
- "version": "2.26.0",
+ "version": "2.25.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf-client",
- "version": "2.26.0",
+ "version": "2.25.1",
"license": "ISC",
"dependencies": {
"@nuxtjs/axios": "^5.13.6",
diff --git a/client/package.json b/client/package.json
index 5ebaab543..e8823f1be 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
- "version": "2.26.0",
+ "version": "2.25.1",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast client",
"main": "index.js",
diff --git a/client/pages/account.vue b/client/pages/account.vue
index e9b5da3cb..b157f5704 100644
--- a/client/pages/account.vue
+++ b/client/pages/account.vue
@@ -182,19 +182,18 @@ export default {
password: this.password,
newPassword: this.newPassword
})
- .then(() => {
- this.$toast.success(this.$strings.ToastUserPasswordChangeSuccess)
- this.resetForm()
+ .then((res) => {
+ if (res.success) {
+ this.$toast.success(this.$strings.ToastUserPasswordChangeSuccess)
+ this.resetForm()
+ } else {
+ this.$toast.error(res.error || this.$strings.ToastUnknownError)
+ }
+ this.changingPassword = false
})
.catch((error) => {
- console.error('Failed to change password', error)
- let errorMessage = this.$strings.ToastUnknownError
- if (error.response?.data && typeof error.response.data === 'string') {
- errorMessage = error.response.data
- }
- this.$toast.error(errorMessage)
- })
- .finally(() => {
+ console.error(error)
+ this.$toast.error(this.$strings.ToastUnknownError)
this.changingPassword = false
})
},
diff --git a/client/pages/config.vue b/client/pages/config.vue
index c4fe24468..5fa145e55 100644
--- a/client/pages/config.vue
+++ b/client/pages/config.vue
@@ -53,7 +53,6 @@ export default {
else if (pageName === 'sessions') return this.$strings.HeaderListeningSessions
else if (pageName === 'stats') return this.$strings.HeaderYourStats
else if (pageName === 'users') return this.$strings.HeaderUsers
- else if (pageName === 'api-keys') return this.$strings.HeaderApiKeys
else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils
else if (pageName === 'rss-feeds') return this.$strings.HeaderRSSFeeds
else if (pageName === 'email') return this.$strings.HeaderEmail
diff --git a/client/pages/config/api-keys/index.vue b/client/pages/config/api-keys/index.vue
deleted file mode 100644
index 2523feed8..000000000
--- a/client/pages/config/api-keys/index.vue
+++ /dev/null
@@ -1,84 +0,0 @@
-
-
-
-
-
- {{ numApiKeys }}
-
-
-
-
- help_outline
-
-
-
-
-
- {{ $strings.ButtonAddApiKey }}
-
-
- (numApiKeys = count)" />
-
-
-
-
-
-
-
diff --git a/client/pages/config/users/_id/index.vue b/client/pages/config/users/_id/index.vue
index b48147d3d..f0e6647f3 100644
--- a/client/pages/config/users/_id/index.vue
+++ b/client/pages/config/users/_id/index.vue
@@ -13,8 +13,8 @@
{{ username }}
-
-
+
+
@@ -100,11 +100,8 @@ export default {
}
},
computed: {
- legacyToken() {
- return this.user.token
- },
userToken() {
- return this.user.accessToken
+ return this.user.token
},
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
diff --git a/client/pages/login.vue b/client/pages/login.vue
index 01adadcd5..3f48509f2 100644
--- a/client/pages/login.vue
+++ b/client/pages/login.vue
@@ -40,15 +40,6 @@
{{ error }}
-
-