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>
|
</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,
|
||||||
|
|
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'
|
import axios from 'axios'
|
||||||
|
|
||||||
export const HTTP = axios.create({
|
export const HTTPFactory = () => {
|
||||||
|
return axios.create({
|
||||||
baseURL: window.API_URL,
|
baseURL: window.API_URL,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
|
@ -21,3 +21,4 @@
|
||||||
@import 'namespaces';
|
@import 'namespaces';
|
||||||
@import 'legal';
|
@import 'legal';
|
||||||
@import 'keyboard-shortcuts';
|
@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,
|
.input,
|
||||||
.textarea {
|
.textarea {
|
||||||
transition: all $transition;
|
transition: all $transition;
|
||||||
|
|
|
@ -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')
|
||||||
|
|
Loading…
Reference in a new issue