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:
konrad 2020-10-11 10:13:35 +00:00
parent 9f3d17c3f3
commit 1935af83c3
10 changed files with 214 additions and 10 deletions

View file

@ -158,7 +158,6 @@
</a> </a>
<aside class="menu namespaces-lists"> <aside class="menu namespaces-lists">
<div :class="{ 'is-loading': namespaceService.loading}" class="spinner"></div>
<template v-for="n in namespaces"> <template v-for="n in namespaces">
<div :key="n.id"> <div :key="n.id">
<router-link <router-link
@ -320,7 +319,6 @@
import router from './router' import router from './router'
import {mapState} from 'vuex' import {mapState} from 'vuex'
import NamespaceService from './services/namespace'
import authTypes from './models/authTypes' import authTypes from './models/authTypes'
import Rights from './models/rights.json' import Rights from './models/rights.json'
@ -337,7 +335,6 @@ export default {
}, },
data() { data() {
return { return {
namespaceService: NamespaceService,
menuActive: true, menuActive: true,
currentDate: new Date(), currentDate: new Date(),
userMenuActive: false, userMenuActive: false,

View 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>

View file

@ -1,5 +1,7 @@
import axios from 'axios' import axios from 'axios'
export const HTTP = axios.create({ export const HTTPFactory = () => {
return axios.create({
baseURL: window.API_URL, baseURL: window.API_URL,
}) })
}

View file

@ -73,6 +73,12 @@ import {store} from './store'
console.info(`Vikunja frontend version ${VERSION}`) 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 // 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) === '/') { 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) window.API_URL = window.API_URL.substr(0, window.API_URL.length - 1)

View file

@ -1,4 +1,4 @@
import {HTTP} from '@/http-common' import {HTTPFactory} from '@/http-common'
import {ERROR_MESSAGE, LOADING} from '../mutation-types' import {ERROR_MESSAGE, LOADING} from '../mutation-types'
import UserModel from '../../models/user' import UserModel from '../../models/user'
@ -32,6 +32,7 @@ export default {
actions: { actions: {
// Logs a user in with a set of credentials. // Logs a user in with a set of credentials.
login(ctx, credentials) { login(ctx, credentials) {
const HTTP = HTTPFactory()
ctx.commit(LOADING, true, {root: true}) ctx.commit(LOADING, true, {root: true})
// Delete an eventually preexisting old token // Delete an eventually preexisting old token
@ -78,6 +79,7 @@ export default {
// Registers a new user and logs them in. // 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. // 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) { register(ctx, credentials) {
const HTTP = HTTPFactory()
return HTTP.post('register', { return HTTP.post('register', {
username: credentials.username, username: credentials.username,
email: credentials.email, email: credentials.email,
@ -98,6 +100,7 @@ export default {
}, },
linkShareAuth(ctx, hash) { linkShareAuth(ctx, hash) {
const HTTP = HTTPFactory()
return HTTP.post('/shares/' + hash + '/auth') return HTTP.post('/shares/' + hash + '/auth')
.then(r => { .then(r => {
localStorage.setItem('token', r.data.token) localStorage.setItem('token', r.data.token)
@ -128,6 +131,7 @@ export default {
}, },
// Renews the api token and saves it to local storage // Renews the api token and saves it to local storage
renewToken(ctx) { renewToken(ctx) {
const HTTP = HTTPFactory()
if (!ctx.state.authenticated) { if (!ctx.state.authenticated) {
return return
} }

View file

@ -1,5 +1,5 @@
import {CONFIG} from '../mutation-types' import {CONFIG} from '../mutation-types'
import {HTTP} from '@/http-common' import {HTTPFactory} from '@/http-common'
export default { export default {
namespaced: true, namespaced: true,
@ -40,10 +40,14 @@ export default {
}, },
actions: { actions: {
update(ctx) { update(ctx) {
HTTP.get('info') const HTTP = HTTPFactory()
return HTTP.get('info')
.then(r => { .then(r => {
ctx.commit(CONFIG, r.data) ctx.commit(CONFIG, r.data)
return Promise.resolve(r)
}) })
.catch(e => Promise.reject(e))
}, },
}, },
} }

View file

@ -21,3 +21,4 @@
@import 'namespaces'; @import 'namespaces';
@import 'legal'; @import 'legal';
@import 'keyboard-shortcuts'; @import 'keyboard-shortcuts';
@import 'api-config';

View 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;
}
}

View file

@ -76,6 +76,10 @@
} }
} }
.field.has-addons .button {
height: 2.5rem;
}
.input, .input,
.textarea { .textarea {
transition: all $transition; transition: all $transition;

View file

@ -5,6 +5,7 @@
<div class="notification is-success has-text-centered" v-if="confirmedEmailSuccess"> <div class="notification is-success has-text-centered" v-if="confirmedEmailSuccess">
You successfully confirmed your email! You can log in now. You successfully confirmed your email! You can log in now.
</div> </div>
<api-config/>
<form @submit.prevent="submit" id="loginform"> <form @submit.prevent="submit" id="loginform">
<div class="field"> <div class="field">
<label class="label" for="username">Username</label> <label class="label" for="username">Username</label>
@ -76,13 +77,15 @@
import {mapState} from 'vuex' import {mapState} from 'vuex'
import router from '../../router' import router from '../../router'
import {HTTP} from '@/http-common' import {HTTPFactory} from '@/http-common'
import message from '../../message' import message from '../../message'
import {ERROR_MESSAGE, LOADING} from '@/store/mutation-types' import {ERROR_MESSAGE, LOADING} from '@/store/mutation-types'
import legal from '../../components/misc/legal' import legal from '../../components/misc/legal'
import ApiConfig from '@/components/misc/api-config'
export default { export default {
components: { components: {
ApiConfig,
legal, legal,
}, },
data() { data() {
@ -91,6 +94,7 @@ export default {
} }
}, },
beforeMount() { beforeMount() {
const HTTP = HTTPFactory()
// Try to verify the email // Try to verify the email
// FIXME: Why is this here? Can we find a better place for this? // FIXME: Why is this here? Can we find a better place for this?
let emailVerifyToken = localStorage.getItem('emailConfirmToken') let emailVerifyToken = localStorage.getItem('emailConfirmToken')