Allow setting api url from the login screen (#264)
Cleanup Use the http factory everywhere instead of the created element Use the current domain if the api path is relative to the frontend host Format Prevent setting an empty url Fix styling Add changing api url Add change url component Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/264 Co-Authored-By: konrad <konrad@kola-entertainments.de> Co-Committed-By: konrad <konrad@kola-entertainments.de>
This commit is contained in:
parent
9f3d17c3f3
commit
1935af83c3
10 changed files with 214 additions and 10 deletions
|
@ -158,7 +158,6 @@
|
|||
</a>
|
||||
|
||||
<aside class="menu namespaces-lists">
|
||||
<div :class="{ 'is-loading': namespaceService.loading}" class="spinner"></div>
|
||||
<template v-for="n in namespaces">
|
||||
<div :key="n.id">
|
||||
<router-link
|
||||
|
@ -320,7 +319,6 @@
|
|||
import router from './router'
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
import NamespaceService from './services/namespace'
|
||||
import authTypes from './models/authTypes'
|
||||
import Rights from './models/rights.json'
|
||||
|
||||
|
@ -337,7 +335,6 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
namespaceService: NamespaceService,
|
||||
menuActive: true,
|
||||
currentDate: new Date(),
|
||||
userMenuActive: false,
|
||||
|
|
170
src/components/misc/api-config.vue
Normal file
170
src/components/misc/api-config.vue
Normal file
|
@ -0,0 +1,170 @@
|
|||
<template>
|
||||
<div class="api-config">
|
||||
<div v-if="configureApi">
|
||||
<label class="label" for="api-url">Vikunja URL</label>
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">
|
||||
<input
|
||||
class="input" id="api-url"
|
||||
placeholder="eg. https://localhost:3456"
|
||||
required
|
||||
type="url"
|
||||
v-focus
|
||||
v-model="apiUrl"
|
||||
@keyup.enter="setApiUrl"
|
||||
/>
|
||||
</div>
|
||||
<div class="control">
|
||||
<a class="button is-primary" @click="setApiUrl" :disabled="apiUrl === ''">
|
||||
Change
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="api-url-info" v-else>
|
||||
Sign in to your Vikunja account on <span v-tooltip="apiUrl">{{ apiDomain() }}</span><br/>
|
||||
<a @click="() => configureApi = true">change</a>
|
||||
</div>
|
||||
|
||||
<div class="notification is-success mt-2" v-if="successMsg !== '' && errorMsg === ''">
|
||||
{{ successMsg }}
|
||||
</div>
|
||||
<div class="notification is-danger mt-2" v-if="errorMsg !== '' && successMsg === ''">
|
||||
{{ errorMsg }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'apiConfig',
|
||||
data() {
|
||||
return {
|
||||
configureApi: false,
|
||||
apiUrl: '',
|
||||
errorMsg: '',
|
||||
successMsg: '',
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.apiUrl = window.API_URL
|
||||
if (this.apiUrl === '') {
|
||||
this.configureApi = true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
apiDomain() {
|
||||
if (window.API_URL.startsWith('/api/v1')) {
|
||||
return window.location.host
|
||||
}
|
||||
const urlParts = window.API_URL.replace('http://', '').replace('https://', '').split(/[/?#]/)
|
||||
return urlParts[0]
|
||||
},
|
||||
setApiUrl() {
|
||||
if (this.apiUrl === '') {
|
||||
return
|
||||
}
|
||||
|
||||
let urlToCheck = this.apiUrl
|
||||
|
||||
// Check if the url has an http prefix
|
||||
if (!urlToCheck.startsWith('http://') && !urlToCheck.startsWith('https://')) {
|
||||
urlToCheck = `http://${urlToCheck}`
|
||||
}
|
||||
|
||||
urlToCheck = new URL(urlToCheck)
|
||||
const origUrlToCheck = urlToCheck
|
||||
|
||||
const oldUrl = window.API_URL
|
||||
window.API_URL = urlToCheck.toString()
|
||||
|
||||
// Check if the api is reachable at the provided url
|
||||
this.$store.dispatch('config/update')
|
||||
.catch(e => {
|
||||
// Check if it is reachable at /api/v1 and http
|
||||
if (!urlToCheck.pathname.endsWith('/api/v1') && !urlToCheck.pathname.endsWith('/api/v1/')) {
|
||||
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return this.$store.dispatch('config/update')
|
||||
}
|
||||
return Promise.reject(e)
|
||||
})
|
||||
.catch(e => {
|
||||
// Check if it has a port and if not check if it is reachable at https
|
||||
if (urlToCheck.protocol === 'http:') {
|
||||
urlToCheck.protocol = 'https:'
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return this.$store.dispatch('config/update')
|
||||
}
|
||||
return Promise.reject(e)
|
||||
})
|
||||
.catch(e => {
|
||||
// Check if it is reachable at /api/v1 and https
|
||||
urlToCheck.pathname = origUrlToCheck.pathname
|
||||
if (!urlToCheck.pathname.endsWith('/api/v1') && !urlToCheck.pathname.endsWith('/api/v1/')) {
|
||||
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return this.$store.dispatch('config/update')
|
||||
}
|
||||
return Promise.reject(e)
|
||||
})
|
||||
.catch(e => {
|
||||
// Check if it is reachable at port 3456 and https
|
||||
if (urlToCheck.port !== 3456) {
|
||||
urlToCheck.protocol = 'https:'
|
||||
urlToCheck.port = 3456
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return this.$store.dispatch('config/update')
|
||||
}
|
||||
return Promise.reject(e)
|
||||
})
|
||||
.catch(e => {
|
||||
// Check if it is reachable at :3456 and /api/v1 and https
|
||||
urlToCheck.pathname = origUrlToCheck.pathname
|
||||
if (!urlToCheck.pathname.endsWith('/api/v1') && !urlToCheck.pathname.endsWith('/api/v1/')) {
|
||||
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return this.$store.dispatch('config/update')
|
||||
}
|
||||
return Promise.reject(e)
|
||||
})
|
||||
.catch(e => {
|
||||
// Check if it is reachable at port 3456 and http
|
||||
if (urlToCheck.port !== 3456) {
|
||||
urlToCheck.protocol = 'http:'
|
||||
urlToCheck.port = 3456
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return this.$store.dispatch('config/update')
|
||||
}
|
||||
return Promise.reject(e)
|
||||
})
|
||||
.catch(e => {
|
||||
// Check if it is reachable at :3456 and /api/v1 and http
|
||||
urlToCheck.pathname = origUrlToCheck.pathname
|
||||
if (!urlToCheck.pathname.endsWith('/api/v1') && !urlToCheck.pathname.endsWith('/api/v1/')) {
|
||||
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return this.$store.dispatch('config/update')
|
||||
}
|
||||
return Promise.reject(e)
|
||||
})
|
||||
.catch(() => {
|
||||
// Still not found, url is still invalid
|
||||
this.successMsg = ''
|
||||
this.errorMsg = `Could not find or use Vikunja installation at "${this.apiDomain()}".`
|
||||
window.API_URL = oldUrl
|
||||
})
|
||||
.then(r => {
|
||||
if (typeof r !== 'undefined') {
|
||||
// Set it + save it to local storage to save us the hoops
|
||||
this.errorMsg = ''
|
||||
this.successMsg = `Using Vikunja installation at "${this.apiDomain()}".`
|
||||
localStorage.setItem('API_URL', window.API_URL)
|
||||
this.configureApi = false
|
||||
this.apiUrl = window.API_URL
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -1,5 +1,7 @@
|
|||
import axios from 'axios'
|
||||
|
||||
export const HTTP = axios.create({
|
||||
baseURL: window.API_URL,
|
||||
})
|
||||
export const HTTPFactory = () => {
|
||||
return axios.create({
|
||||
baseURL: window.API_URL,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -73,6 +73,12 @@ import {store} from './store'
|
|||
|
||||
console.info(`Vikunja frontend version ${VERSION}`)
|
||||
|
||||
// Check if we have an api url in local storage and use it if that's the case
|
||||
const apiUrlFromStorage = localStorage.getItem('API_URL')
|
||||
if (apiUrlFromStorage !== null) {
|
||||
window.API_URL = apiUrlFromStorage
|
||||
}
|
||||
|
||||
// Make sure the api url does not contain a / at the end
|
||||
if (window.API_URL.substr(window.API_URL.length - 1, window.API_URL.length) === '/') {
|
||||
window.API_URL = window.API_URL.substr(0, window.API_URL.length - 1)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {HTTP} from '@/http-common'
|
||||
import {HTTPFactory} from '@/http-common'
|
||||
import {ERROR_MESSAGE, LOADING} from '../mutation-types'
|
||||
import UserModel from '../../models/user'
|
||||
|
||||
|
@ -32,6 +32,7 @@ export default {
|
|||
actions: {
|
||||
// Logs a user in with a set of credentials.
|
||||
login(ctx, credentials) {
|
||||
const HTTP = HTTPFactory()
|
||||
ctx.commit(LOADING, true, {root: true})
|
||||
|
||||
// Delete an eventually preexisting old token
|
||||
|
@ -78,6 +79,7 @@ export default {
|
|||
// Registers a new user and logs them in.
|
||||
// Not sure if this is the right place to put the logic in, maybe a seperate js component would be better suited.
|
||||
register(ctx, credentials) {
|
||||
const HTTP = HTTPFactory()
|
||||
return HTTP.post('register', {
|
||||
username: credentials.username,
|
||||
email: credentials.email,
|
||||
|
@ -98,6 +100,7 @@ export default {
|
|||
},
|
||||
|
||||
linkShareAuth(ctx, hash) {
|
||||
const HTTP = HTTPFactory()
|
||||
return HTTP.post('/shares/' + hash + '/auth')
|
||||
.then(r => {
|
||||
localStorage.setItem('token', r.data.token)
|
||||
|
@ -128,6 +131,7 @@ export default {
|
|||
},
|
||||
// Renews the api token and saves it to local storage
|
||||
renewToken(ctx) {
|
||||
const HTTP = HTTPFactory()
|
||||
if (!ctx.state.authenticated) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {CONFIG} from '../mutation-types'
|
||||
import {HTTP} from '@/http-common'
|
||||
import {HTTPFactory} from '@/http-common'
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
|
@ -40,10 +40,14 @@ export default {
|
|||
},
|
||||
actions: {
|
||||
update(ctx) {
|
||||
HTTP.get('info')
|
||||
const HTTP = HTTPFactory()
|
||||
|
||||
return HTTP.get('info')
|
||||
.then(r => {
|
||||
ctx.commit(CONFIG, r.data)
|
||||
return Promise.resolve(r)
|
||||
})
|
||||
.catch(e => Promise.reject(e))
|
||||
},
|
||||
},
|
||||
}
|
|
@ -21,3 +21,4 @@
|
|||
@import 'namespaces';
|
||||
@import 'legal';
|
||||
@import 'keyboard-shortcuts';
|
||||
@import 'api-config';
|
||||
|
|
12
src/styles/components/api-config.scss
Normal file
12
src/styles/components/api-config.scss
Normal file
|
@ -0,0 +1,12 @@
|
|||
.api-config {
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
|
||||
.api-url-info {
|
||||
font-size: .9rem;
|
||||
text-align: right;
|
||||
|
||||
span {
|
||||
border-bottom: 1px dashed $primary;
|
||||
}
|
||||
}
|
|
@ -76,6 +76,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.field.has-addons .button {
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.input,
|
||||
.textarea {
|
||||
transition: all $transition;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
<div class="notification is-success has-text-centered" v-if="confirmedEmailSuccess">
|
||||
You successfully confirmed your email! You can log in now.
|
||||
</div>
|
||||
<api-config/>
|
||||
<form @submit.prevent="submit" id="loginform">
|
||||
<div class="field">
|
||||
<label class="label" for="username">Username</label>
|
||||
|
@ -76,13 +77,15 @@
|
|||
import {mapState} from 'vuex'
|
||||
|
||||
import router from '../../router'
|
||||
import {HTTP} from '@/http-common'
|
||||
import {HTTPFactory} from '@/http-common'
|
||||
import message from '../../message'
|
||||
import {ERROR_MESSAGE, LOADING} from '@/store/mutation-types'
|
||||
import legal from '../../components/misc/legal'
|
||||
import ApiConfig from '@/components/misc/api-config'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ApiConfig,
|
||||
legal,
|
||||
},
|
||||
data() {
|
||||
|
@ -91,6 +94,7 @@ export default {
|
|||
}
|
||||
},
|
||||
beforeMount() {
|
||||
const HTTP = HTTPFactory()
|
||||
// Try to verify the email
|
||||
// FIXME: Why is this here? Can we find a better place for this?
|
||||
let emailVerifyToken = localStorage.getItem('emailConfirmToken')
|
||||
|
|
Loading…
Reference in a new issue