0
0
mirror of https://github.com/vx3r/wg-gen-web.git synced 2025-09-11 12:24:27 +00:00

oauth2 oidc, vuex store

This commit is contained in:
vx3r
2020-04-28 20:11:49 +09:00
parent f90124afbf
commit 125ddaef0f
36 changed files with 2050 additions and 847 deletions

View File

@ -1,69 +1,36 @@
<template>
<v-app id="inspire">
<Notification v-bind:notification="notification"/>
<div v-if="this.isAuthenticated">
<Header/>
<v-app-bar app>
<img class="mr-3" :src="require('./assets/logo.png')" height="50" alt="Wg Gen Web"/>
<v-toolbar-title to="/">Wg Gen Web</v-toolbar-title>
<v-spacer />
<v-toolbar-items>
<v-btn to="/clients">
Clients
<v-icon right dark>mdi-account-network-outline</v-icon>
</v-btn>
<v-btn to="/server">
Server
<v-icon right dark>mdi-vpn</v-icon>
</v-btn>
</v-toolbar-items>
</v-app-bar>
<v-content>
<v-container>
<router-view />
</v-container>
<Notification v-bind:notification="notification"/>
</v-content>
<v-footer app>
<v-row justify="start" no-gutters>
<v-col cols="12" lg="6" md="12" sm="12">
<div :align="$vuetify.breakpoint.smAndDown ? 'center' : 'left'">
<small>Copyright &copy; {{ new Date().getFullYear() }}, Wg Gen Web.</small>
<small>This work is licensed under a <a class="pr-1 pl-1" href="http://www.wtfpl.net/" target="_blank">WTFPL License.</a></small>
</div>
</v-col>
</v-row>
<v-row justify="end" no-gutters>
<v-col cols="12" lg="6" md="12" sm="12">
<div :align="$vuetify.breakpoint.smAndDown ? 'center' : 'right'">
<small>Created with</small>
<v-icon class="pr-1 pl-1">mdi-heart</v-icon><span>by</span><a class="pr-2 pl-1" href="mailto:vx3r@127-0-0-1.fr">vx3r</a>
<a :href="'https://github.com/vx3r/wg-gen-web/commit/' + version"><kbd>Version: {{ version.substring(0,7) }}</kbd></a>
</div>
</v-col>
</v-row>
</v-footer>
<v-content>
<v-container>
<router-view />
</v-container>
</v-content>
<Footer/>
</div>
</v-app>
</template>
<script>
import {ApiService} from "./services/ApiService";
import Notification from './components/Notification'
import Header from "./components/Header";
import Footer from "./components/Footer";
import {mapActions, mapGetters} from "vuex";
export default {
name: 'App',
components: {
Footer,
Header,
Notification
},
data: () => ({
api: null,
version:'N/A',
notification: {
show: false,
color: '',
@ -71,23 +38,69 @@
},
}),
mounted() {
this.api = new ApiService();
this.getVersion()
computed:{
...mapGetters({
isAuthenticated: 'auth/isAuthenticated',
authStatus: 'auth/authStatus',
authRedirectUrl: 'auth/authRedirectUrl',
authError: 'auth/error',
clientError: 'client/error',
serverError: 'server/error',
})
},
created () {
this.$vuetify.theme.dark = true
},
methods: {
getVersion() {
this.api.get('/server/version').then((res) => {
this.version = res.version;
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
mounted() {
if (this.$route.query.code && this.$route.query.state) {
this.oauth2_exchange({
code: this.$route.query.code,
state: this.$route.query.state
})
} else {
this.oauth2_url()
}
},
watch: {
authError(newValue, oldValue) {
console.log(newValue)
this.notify('error', newValue);
},
clientError(newValue, oldValue) {
console.log(newValue)
this.notify('error', newValue);
},
serverError(newValue, oldValue) {
console.log(newValue)
this.notify('error', newValue);
},
isAuthenticated(newValue, oldValue) {
console.log(`Updating isAuthenticated from ${oldValue} to ${newValue}`);
if (newValue === true) {
this.$router.push('/clients')
}
},
authStatus(newValue, oldValue) {
console.log(`Updating authStatus from ${oldValue} to ${newValue}`);
if (newValue === 'redirect') {
window.location.replace(this.authRedirectUrl)
}
},
},
methods: {
...mapActions('auth', {
oauth2_exchange: 'oauth2_exchange',
oauth2_url: 'oauth2_url',
}),
notify(color, msg) {
this.notification.show = true;
this.notification.color = color;

View File

@ -9,7 +9,7 @@
</v-list-item-content>
<v-btn
color="success"
@click="startAddClient"
@click="startCreate"
>
Add new client
<v-icon right dark>mdi-account-multiple-plus-outline</v-icon>
@ -31,15 +31,15 @@
<v-list-item-content>
<v-list-item-title class="headline">{{ client.name }}</v-list-item-title>
<v-list-item-subtitle>{{ client.email }}</v-list-item-subtitle>
<v-list-item-subtitle>Created: {{ client.created | formatDate }}</v-list-item-subtitle>
<v-list-item-subtitle>Updated: {{ client.updated | formatDate }}</v-list-item-subtitle>
<v-list-item-subtitle>Created: {{ client.created | formatDate }} by {{ client.createdBy }}</v-list-item-subtitle>
<v-list-item-subtitle>Updated: {{ client.updated | formatDate }} by {{ client.updatedBy }}</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-avatar
tile
size="150"
>
<v-img :src="`${apiBaseUrl}/client/${client.id}/config?qrcode=true`"/>
<v-img :src="'data:image/png;base64, ' + getClientQrcode(client.id)"/>
</v-list-item-avatar>
</v-list-item>
@ -55,61 +55,61 @@
</v-chip>
</v-card-text>
<v-card-actions>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn
text
:href="`${apiBaseUrl}/client/${client.id}/config?qrcode=false`"
v-on="on"
>
<span class="d-none d-lg-flex">Download</span>
<v-icon right dark>mdi-cloud-download-outline</v-icon>
</v-btn>
</template>
<span>Download</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn
text
v-on:click="forceFileDownload(client)"
v-on="on"
>
<span class="d-none d-lg-flex">Download</span>
<v-icon right dark>mdi-cloud-download-outline</v-icon>
</v-btn>
</template>
<span>Download</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn
text
@click.stop="startUpdateClient(client)"
v-on="on"
>
<span class="d-none d-lg-flex">Edit</span>
<v-icon right dark>mdi-square-edit-outline</v-icon>
</v-btn>
</template>
<span>Edit</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn
text
@click.stop="startUpdate(client)"
v-on="on"
>
<span class="d-none d-lg-flex">Edit</span>
<v-icon right dark>mdi-square-edit-outline</v-icon>
</v-btn>
</template>
<span>Edit</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn
text
@click="deleteClient(client)"
v-on="on"
>
<span class="d-none d-lg-flex">Delete</span>
<v-icon right dark>mdi-trash-can-outline</v-icon>
</v-btn>
</template>
<span>Delete</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn
text
@click="remove(client)"
v-on="on"
>
<span class="d-none d-lg-flex">Delete</span>
<v-icon right dark>mdi-trash-can-outline</v-icon>
</v-btn>
</template>
<span>Delete</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn
text
@click="sendEmailClient(client.id)"
v-on="on"
>
<span class="d-none d-lg-flex">Send Email</span>
<v-icon right dark>mdi-email-send-outline</v-icon>
</v-btn>
</template>
<span>Send Email</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn
text
@click="email(client)"
v-on="on"
>
<span class="d-none d-lg-flex">Send Email</span>
<v-icon right dark>mdi-email-send-outline</v-icon>
</v-btn>
</template>
<span>Send Email</span>
</v-tooltip>
<v-spacer/>
<v-tooltip right>
<template v-slot:activator="{ on }">
@ -118,7 +118,7 @@
v-on="on"
color="success"
v-model="client.enable"
v-on:change="updateClient(client)"
v-on:change="update(client)"
/>
</template>
<span> {{client.enable ? 'Disable' : 'Enable'}} this client</span>
@ -133,7 +133,7 @@
</v-row>
<v-dialog
v-if="client"
v-model="dialogAddClient"
v-model="dialogCreate"
max-width="550"
>
<v-card>
@ -210,14 +210,14 @@
<v-btn
:disabled="!valid"
color="success"
@click="addClient(client)"
@click="create(client)"
>
Submit
<v-icon right dark>mdi-check-outline</v-icon>
</v-btn>
<v-btn
color="primary"
@click="dialogAddClient = false"
@click="dialogCreate = false"
>
Cancel
<v-icon right dark>mdi-close-circle-outline</v-icon>
@ -227,7 +227,7 @@
</v-dialog>
<v-dialog
v-if="client"
v-model="dialogEditClient"
v-model="dialogUpdate"
max-width="550"
>
<v-card>
@ -308,14 +308,14 @@
<v-btn
:disabled="!valid"
color="success"
@click="updateClient(client)"
@click="update(client)"
>
Submit
<v-icon right dark>mdi-check-outline</v-icon>
</v-btn>
<v-btn
color="primary"
@click="dialogEditClient = false"
@click="dialogUpdate = false"
>
Cancel
<v-icon right dark>mdi-close-circle-outline</v-icon>
@ -323,61 +323,50 @@
</v-card-actions>
</v-card>
</v-dialog>
<Notification v-bind:notification="notification"/>
</v-container>
</template>
<script>
import {ApiService, API_BASE_URL} from '../services/ApiService'
import Notification from '../components/Notification'
import { mapActions, mapGetters } from 'vuex'
export default {
name: 'Clients',
components: {
Notification
},
data: () => ({
api: null,
apiBaseUrl: API_BASE_URL,
clients: [],
notification: {
show: false,
color: '',
text: '',
},
dialogAddClient: false,
dialogEditClient: false,
dialogCreate: false,
dialogUpdate: false,
client: null,
server: null,
valid: false,
}),
computed:{
...mapGetters({
getClientQrcode: 'client/getClientQrcode',
getClientConfig: 'client/getClientConfig',
server: 'server/server',
clients: 'client/clients',
clientQrcodes: 'client/clientQrcodes',
}),
},
mounted () {
this.api = new ApiService();
this.getClients();
this.getServer()
this.readAllClients()
this.readServer()
},
methods: {
getClients() {
this.api.get('/client').then((res) => {
this.clients = res
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
},
...mapActions('client', {
errorClient: 'error',
readAllClients: 'readAll',
creatClient: 'create',
updateClient: 'update',
deleteClient: 'delete',
emailClient: 'email',
}),
...mapActions('server', {
readServer: 'read',
}),
getServer() {
this.api.get('/server').then((res) => {
this.server = res;
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
},
startAddClient() {
this.dialogAddClient = true;
startCreate() {
this.client = {
name: "",
email: "",
@ -385,91 +374,87 @@
allowedIPs: this.server.allowedips,
address: this.server.address,
}
this.dialogCreate = true;
},
addClient(client) {
create(client) {
if (client.allowedIPs.length < 1) {
this.notify('error', 'Please provide at least one valid CIDR address for client allowed IPs');
this.errorClient('Please provide at least one valid CIDR address for client allowed IPs')
return;
}
for (let i = 0; i < client.allowedIPs.length; i++){
if (this.$isCidr(client.allowedIPs[i]) === 0) {
this.notify('error', 'Invalid CIDR detected, please correct before submitting');
this.errorClient('Invalid CIDR detected, please correct before submitting')
return
}
}
this.dialogAddClient = false;
this.api.post('/client', client).then((res) => {
this.notify('success', `Client ${res.name} successfully added`);
this.getClients()
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
this.dialogCreate = false;
this.creatClient(client)
},
deleteClient(client) {
remove(client) {
if(confirm(`Do you really want to delete ${client.name} ?`)){
this.api.delete(`/client/${client.id}`).then((res) => {
this.notify('success', "Client successfully deleted");
this.getClients()
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
this.deleteClient(client)
}
},
sendEmailClient(id) {
this.api.get(`/client/${id}/email`).then((res) => {
this.notify('success', "Email successfully sent");
this.getClients()
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
email(client) {
if (!client.email){
this.errorClient('Client email is not defined')
return
}
if(confirm(`Do you really want to send email to ${client.email} with all configurations ?`)){
this.emailClient(client)
}
},
startUpdateClient(client) {
startUpdate(client) {
this.client = client;
this.dialogEditClient = true;
this.dialogUpdate = true;
},
updateClient(client) {
update(client) {
// check allowed IPs
if (client.allowedIPs.length < 1) {
this.notify('error', 'Please provide at least one valid CIDR address for client allowed IPs');
this.errorClient('Please provide at least one valid CIDR address for client allowed IPs');
return;
}
for (let i = 0; i < client.allowedIPs.length; i++){
if (this.$isCidr(client.allowedIPs[i]) === 0) {
this.notify('error', 'Invalid CIDR detected, please correct before submitting');
this.errorClient('Invalid CIDR detected, please correct before submitting');
return
}
}
// check address
if (client.address.length < 1) {
this.notify('error', 'Please provide at least one valid CIDR address for client');
this.errorClient('Please provide at least one valid CIDR address for client');
return;
}
for (let i = 0; i < client.address.length; i++){
if (this.$isCidr(client.address[i]) === 0) {
this.notify('error', 'Invalid CIDR detected, please correct before submitting');
this.errorClient('Invalid CIDR detected, please correct before submitting');
return
}
}
// all good, submit
this.dialogEditClient = false;
this.api.patch(`/client/${client.id}`, client).then((res) => {
this.notify('success', `Client ${res.name} successfully updated`);
this.getClients()
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
this.dialogUpdate = false;
this.updateClient(client)
},
notify(color, msg) {
this.notification.show = true;
this.notification.color = color;
this.notification.text = msg;
}
forceFileDownload(client){
let config = this.getClientConfig(client.id)
if (!config) {
this.errorClient('Failed to download client config');
return
}
const url = window.URL.createObjectURL(new Blob([config]))
const link = document.createElement('a')
link.href = url
link.setAttribute('download', 'wg0.conf') //or any other extension
document.body.appendChild(link)
link.click()
},
}
};
</script>

View File

@ -0,0 +1,51 @@
<template>
<v-container>
<v-footer app>
<v-row justify="start" no-gutters>
<v-col cols="12" lg="6" md="12" sm="12">
<div :align="$vuetify.breakpoint.smAndDown ? 'center' : 'left'">
<small>Copyright &copy; {{ new Date().getFullYear() }}, Wg Gen Web. </small>
<small>This work is licensed under <a class="pr-1 pl-1" href="http://www.wtfpl.net/" target="_blank">WTFPL License.</a></small>
</div>
</v-col>
</v-row>
<v-row justify="end" no-gutters>
<v-col cols="12" lg="6" md="12" sm="12">
<div :align="$vuetify.breakpoint.smAndDown ? 'center' : 'right'">
<small>Created with</small>
<v-icon class="pr-1 pl-1">mdi-heart</v-icon><span>by</span><a class="pr-2 pl-1" href="mailto:vx3r@127-0-0-1.fr">vx3r</a>
<a :href="'https://github.com/vx3r/wg-gen-web/commit/' + version"><kbd>Version: {{ version.substring(0,7) }}</kbd></a>
</div>
</v-col>
</v-row>
</v-footer>
</v-container>
</template>
<script>
import {mapActions, mapGetters} from "vuex";
export default {
name: 'Footer',
data: () => ({
}),
computed:{
...mapGetters({
version: 'server/version',
}),
},
mounted() {
this.versionServer()
},
methods: {
...mapActions('server', {
versionServer: 'version',
}),
}
}
</script>

View File

@ -0,0 +1,77 @@
<template>
<v-container>
<v-app-bar app>
<img class="mr-3" :src="require('../assets/logo.png')" height="50" alt="Wg Gen Web"/>
<v-toolbar-title to="/">Wg Gen Web</v-toolbar-title>
<v-spacer />
<v-toolbar-items>
<v-btn to="/clients">
Clients
<v-icon right dark>mdi-account-network-outline</v-icon>
</v-btn>
<v-btn to="/server">
Server
<v-icon right dark>mdi-vpn</v-icon>
</v-btn>
</v-toolbar-items>
<v-menu
left
bottom
>
<template v-slot:activator="{ on }">
<v-btn icon v-on="on">
<v-icon>mdi-account-circle</v-icon>
</v-btn>
</template>
<v-card
class="mx-auto"
max-width="344"
outlined
>
<v-list-item three-line>
<v-list-item-content>
<div class="overline mb-4">connected as</div>
<v-list-item-title class="headline mb-1">{{user.name}}</v-list-item-title>
<v-list-item-subtitle>Email: {{user.email}}</v-list-item-subtitle>
<v-list-item-subtitle>Issuer: {{user.issuer}}</v-list-item-subtitle>
<v-list-item-subtitle>Issued at: {{ user.issuedAt | formatDate }}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-card-actions>
<v-btn small
v-on:click="logout()"
>
logout
<v-icon small right dark>mdi-logout</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</v-menu>
</v-app-bar>
</v-container>
</template>
<script>
import {mapActions, mapGetters} from "vuex";
export default {
name: 'Header',
computed:{
...mapGetters({
user: 'auth/user',
}),
},
methods: {
...mapActions('auth', {
logout: 'logout',
}),
}
}
</script>

View File

@ -158,7 +158,7 @@
<v-btn
class="ma-2"
color="success"
:href="`${apiBaseUrl}/server/config`"
v-on:click="forceFileDownload()"
>
Download server configuration
<v-icon right dark>mdi-cloud-download-outline</v-icon>
@ -167,52 +167,44 @@
<v-btn
class="ma-2"
color="warning"
@click="updateServer"
@click="update"
>
Update server configuration
<v-icon right dark>mdi-update</v-icon>
</v-btn>
<v-divider dark/>
</v-row>
<Notification v-bind:notification="notification"/>
</v-container>
</template>
<script>
import {API_BASE_URL, ApiService} from "../services/ApiService";
import Notification from '../components/Notification'
import {mapActions, mapGetters} from "vuex";
export default {
name: 'Server',
components: {
Notification
},
data: () => ({
api: null,
server: null,
apiBaseUrl: API_BASE_URL,
notification: {
show: false,
color: '',
text: '',
},
}),
computed:{
...mapGetters({
server: 'server/server',
config: 'server/config',
}),
},
mounted () {
this.api = new ApiService();
this.getServer()
this.readServer()
},
methods: {
getServer() {
this.api.get('/server').then((res) => {
this.server = res;
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
},
updateServer () {
...mapActions('server', {
errorServer: 'error',
readServer: 'read',
updateServer: 'update',
}),
update() {
// convert int values
this.server.listenPort = parseInt(this.server.listenPort, 10);
this.server.persistentKeepalive = parseInt(this.server.persistentKeepalive, 10);
@ -220,12 +212,12 @@
// check server addresses
if (this.server.address.length < 1) {
this.notify('error', 'Please provide at least one valid CIDR address for server interface');
this.errorServer('Please provide at least one valid CIDR address for server interface');
return;
}
for (let i = 0; i < this.server.address.length; i++){
if (this.$isCidr(this.server.address[i]) === 0) {
this.notify('error', `Invalid CIDR detected, please correct ${this.server.address[i]} before submitting`);
this.errorServer(`Invalid CIDR detected, please correct ${this.server.address[i]} before submitting`);
return
}
}
@ -233,35 +225,34 @@
// check DNS correct
for (let i = 0; i < this.server.dns.length; i++){
if (this.$isCidr(this.server.dns[i] + '/32') === 0) {
this.notify('error', `Invalid IP detected, please correct ${this.server.dns[i]} before submitting`);
this.errorServer(`Invalid IP detected, please correct ${this.server.dns[i]} before submitting`);
return
}
}
// check client AllowedIPs
if (this.server.allowedips.length < 1) {
this.notify('error', 'Please provide at least one valid CIDR address for client allowed IPs');
this.errorServer('Please provide at least one valid CIDR address for client allowed IPs');
return;
}
for (let i = 0; i < this.server.allowedips.length; i++){
if (this.$isCidr(this.server.allowedips[i]) === 0) {
this.notify('error', 'Invalid CIDR detected, please correct before submitting');
this.errorServer('Invalid CIDR detected, please correct before submitting');
return
}
}
this.api.patch('/server', this.server).then((res) => {
this.notify('success', "Server successfully updated");
this.server = res;
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
this.updateServer(this.server)
},
forceFileDownload(){
const url = window.URL.createObjectURL(new Blob([this.config]))
const link = document.createElement('a')
link.href = url
link.setAttribute('download', 'wg0.conf') //or any other extension
document.body.appendChild(link)
link.click()
},
notify(color, msg) {
this.notification.show = true;
this.notification.color = color;
this.notification.text = msg;
}
}
};
</script>

View File

@ -1,14 +1,18 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import vuetify from './plugins/vuetify';
import './plugins/moment';
import './plugins/cidr'
import './plugins/axios'
Vue.config.productionTip = false
// Don't warn about using the dev version of Vue in development.
Vue.config.productionTip = process.env.NODE_ENV === 'production'
new Vue({
router,
store,
vuetify,
render: function (h) { return h(App) }
}).$mount('#app')

26
ui/src/plugins/axios.js Normal file
View File

@ -0,0 +1,26 @@
import Vue from 'vue'
import axios from "axios";
import VueAxios from "vue-axios";
import TokenService from "../services/token.service";
Vue.use(VueAxios, axios);
let baseUrl = "/api/v1.0";
if (process.env.NODE_ENV === "development"){
baseUrl = process.env.VUE_APP_API_BASE_URL;
}
Vue.axios.defaults.baseURL = baseUrl;
Vue.axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
if (401 === error.response.status) {
TokenService.destroyToken();
TokenService.destroyClientId();
window.location = '/';
} else {
return Promise.reject(error);
}
});

View File

@ -1,22 +1,19 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import store from "../store";
Vue.use(VueRouter);
const routes = [
{
path: '/',
name: 'index',
component: function () {
return import(/* webpackChunkName: "Index" */ '../views/Index.vue')
},
},
{
path: '/clients',
name: 'clients',
component: function () {
return import(/* webpackChunkName: "Clients" */ '../views/Clients.vue')
},
meta: {
requiresAuth: true
}
},
{
path: '/server',
@ -24,6 +21,9 @@ const routes = [
component: function () {
return import(/* webpackChunkName: "Server" */ '../views/Server.vue')
},
meta: {
requiresAuth: true
}
}
];
@ -33,4 +33,16 @@ const router = new VueRouter({
routes
});
router.beforeEach((to, from, next) => {
if(to.matched.some(record => record.meta.requiresAuth)) {
if (store.getters["auth/isAuthenticated"]) {
next()
return
}
next('/')
} else {
next()
}
})
export default router

View File

@ -1,40 +0,0 @@
import axios from 'axios'
let baseUrl = "/api/v1.0";
if (process.env.NODE_ENV === "development"){
baseUrl = process.env.VUE_APP_API_BASE_URL
}
export const API_BASE_URL = baseUrl;
export class ApiService {
get(resource) {
return axios
.get(`${API_BASE_URL}${resource}`)
.then(response => response.data)
};
post(resource, data) {
return axios
.post(`${API_BASE_URL}${resource}`, data)
.then(response => response.data)
};
put(resource, data) {
return axios
.put(`${API_BASE_URL}${resource}`, data)
.then(response => response.data)
};
patch(resource, data) {
return axios
.patch(`${API_BASE_URL}${resource}`, data)
.then(response => response.data)
};
delete(resource) {
return axios
.delete(`${API_BASE_URL}${resource}`)
.then(response => response.data)
};
}

View File

@ -0,0 +1,59 @@
import Vue from "vue";
import TokenService from "./token.service";
const ApiService = {
setHeader() {
Vue.axios.defaults.headers.common.Authorization = `${TokenService.getToken()}`;
},
get(resource) {
return Vue.axios.get(resource)
.then(response => response.data)
.catch(error => {
throw new Error(`ApiService: ${error}`)
});
},
post(resource, params) {
return Vue.axios.post(resource, params)
.then(response => response.data)
.catch(error => {
throw new Error(`ApiService: ${error}`)
});
},
put(resource, params) {
return Vue.axios.put(resource, params)
.then(response => response.data)
.catch(error => {
throw new Error(`ApiService: ${error}`)
});
},
patch(resource, params) {
return Vue.axios.patch(resource, params)
.then(response => response.data)
.catch(error => {
throw new Error(`ApiService: ${error}`)
});
},
delete(resource) {
return Vue.axios.delete(resource)
.then(response => response.data)
.catch(error => {
throw new Error(`ApiService: ${error}`)
});
},
getWithConfig(resource, config) {
return Vue.axios.get(resource, config)
.then(response => response.data)
.catch(error => {
throw new Error(`ApiService: ${error}`)
});
},
};
export default ApiService;

View File

@ -0,0 +1,35 @@
const TOKEN_KEY = "token";
const CLIENT_ID_KEY = "client_id";
export const getToken = () => {
return window.localStorage.getItem(TOKEN_KEY);
};
export const saveToken = token => {
window.localStorage.setItem(TOKEN_KEY, token);
};
export const destroyToken = () => {
window.localStorage.removeItem(TOKEN_KEY);
};
export const getClientId = () => {
return window.localStorage.getItem(CLIENT_ID_KEY);
};
export const saveClientId = token => {
window.localStorage.setItem(CLIENT_ID_KEY, token);
};
export const destroyClientId = () => {
window.localStorage.removeItem(CLIENT_ID_KEY);
};
export default {
getToken,
saveToken,
destroyToken,
getClientId,
saveClientId,
destroyClientId
};

19
ui/src/store/index.js Normal file
View File

@ -0,0 +1,19 @@
import Vue from 'vue'
import Vuex from 'vuex'
import auth from "./modules/auth";
import client from "./modules/client";
import server from "./modules/server";
Vue.use(Vuex)
export default new Vuex.Store({
state: {},
getters : {},
mutations: {},
actions:{},
modules: {
auth,
client,
server
}
})

View File

@ -0,0 +1,126 @@
import ApiService from "../../services/api.service";
import TokenService from "../../services/token.service";
const state = {
error: null,
user: null,
authStatus: '',
authRedirectUrl: '',
};
const getters = {
error(state) {
return state.error;
},
user(state) {
return state.user;
},
isAuthenticated(state) {
return state.user !== null;
},
authRedirectUrl(state) {
return state.authRedirectUrl
},
authStatus(state) {
return state.authStatus
},
};
const actions = {
user({ commit }){
ApiService.get("/auth/user")
.then( resp => {
commit('user', resp)
})
.catch(err => {
commit('error', err);
commit('logout')
});
},
oauth2_url({ commit, dispatch }){
if (TokenService.getToken()) {
ApiService.setHeader();
dispatch('user');
return
}
ApiService.get("/auth/oauth2_url")
.then(resp => {
if (resp.codeUrl === '_magic_string_fake_auth_no_redirect_'){
console.log("server report oauth2 is disabled, fake exchange")
commit('authStatus', 'disabled')
TokenService.saveClientId(resp.clientId)
dispatch('oauth2_exchange', {code: "", state: resp.state})
} else {
commit('authStatus', 'redirect')
commit('authRedirectUrl', resp)
}
})
.catch(err => {
commit('authStatus', 'error')
commit('error', err);
commit('logout')
})
},
oauth2_exchange({ commit, dispatch }, data){
data.clientId = TokenService.getClientId()
ApiService.post("/auth/oauth2_exchange", data)
.then(resp => {
commit('authStatus', 'success')
commit('token', resp)
dispatch('user');
})
.catch(err => {
commit('authStatus', 'error')
commit('error', err);
commit('logout')
})
},
logout({ commit }){
ApiService.get("/auth/logout")
.then(resp => {
commit('logout')
})
.catch(err => {
commit('authStatus', '')
commit('error', err);
commit('logout')
})
},
}
const mutations = {
error(state, error) {
state.error = error;
},
authStatus(state, authStatus) {
state.authStatus = authStatus;
},
authRedirectUrl(state, resp) {
state.authRedirectUrl = resp.codeUrl;
TokenService.saveClientId(resp.clientId);
},
token(state, token) {
TokenService.saveToken(token);
ApiService.setHeader();
TokenService.destroyClientId();
},
user(state, user) {
state.user = user;
},
logout(state) {
state.user = null;
TokenService.destroyToken();
TokenService.destroyClientId();
}
};
export default {
namespaced: true,
state,
getters,
actions,
mutations
}

View File

@ -0,0 +1,177 @@
import ApiService from "../../services/api.service";
const state = {
error: null,
clients: [],
clientQrcodes: [],
clientConfigs: []
}
const getters = {
error(state) {
return state.error;
},
clients(state) {
return state.clients;
},
getClientQrcode: (state) => (id) => {
let item = state.clientQrcodes.find(item => item.id === id)
// initial load fails, must wait promise and stuff...
return item ? item.qrcode : "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
},
getClientConfig: (state) => (id) => {
let item = state.clientConfigs.find(item => item.id === id)
return item ? item.config : null
}
}
const actions = {
error({ commit }, error){
commit('error', error)
},
readAll({ commit, dispatch }){
ApiService.get("/client")
.then(resp => {
commit('clients', resp)
dispatch('readQrcodes')
dispatch('readConfigs')
})
.catch(err => {
commit('error', err)
})
},
create({ commit, dispatch }, client){
ApiService.post("/client", client)
.then(resp => {
dispatch('readQrcode', resp)
dispatch('readConfig', resp)
commit('create', resp)
})
.catch(err => {
commit('error', err)
})
},
update({ commit, dispatch }, client){
ApiService.patch(`/client/${client.id}`, client)
.then(resp => {
dispatch('readQrcode', resp)
dispatch('readConfig', resp)
commit('update', resp)
})
.catch(err => {
commit('error', err)
})
},
delete({ commit }, client){
ApiService.delete(`/client/${client.id}`)
.then(() => {
commit('delete', client)
})
.catch(err => {
commit('error', err)
})
},
email({ commit }, client){
ApiService.get(`/client/${client.id}/email`)
.then(() => {
})
.catch(err => {
commit('error', err)
})
},
readQrcode({ state, commit }, client){
ApiService.getWithConfig(`/client/${client.id}/config?qrcode=true`, {responseType: 'arraybuffer'})
.then(resp => {
let image = Buffer.from(resp, 'binary').toString('base64')
commit('clientQrcodes', { client, image })
})
.catch(err => {
commit('error', err)
})
},
readConfig({ state, commit }, client){
ApiService.getWithConfig(`/client/${client.id}/config?qrcode=false`, {responseType: 'arraybuffer'})
.then(resp => {
commit('clientConfigs', { client: client, config: resp })
})
.catch(err => {
commit('error', err)
})
},
readQrcodes({ state, dispatch }){
state.clients.forEach(client => {
dispatch('readQrcode', client)
})
},
readConfigs({ state, dispatch }){
state.clients.forEach(client => {
dispatch('readConfig', client)
})
},
}
const mutations = {
error(state, error) {
state.error = error;
},
clients(state, clients){
state.clients = clients
},
create(state, client){
state.clients.push(client)
},
update(state, client){
let index = state.clients.findIndex(x => x.id === client.id);
if (index !== -1) {
state.clients.splice(index, 1);
state.clients.push(client);
} else {
state.error = "update client failed, not in list"
}
},
delete(state, client){
let index = state.clients.findIndex(x => x.id === client.id);
if (index !== -1) {
state.clients.splice(index, 1);
} else {
state.error = "delete client failed, not in list"
}
},
clientQrcodes(state, { client, image }){
let index = state.clientQrcodes.findIndex(x => x.id === client.id);
if (index !== -1) {
state.clientQrcodes.splice(index, 1);
}
state.clientQrcodes.push({
id: client.id,
qrcode: image
})
},
clientConfigs(state, { client, config }){
let index = state.clientConfigs.findIndex(x => x.id === client.id);
if (index !== -1) {
state.clientConfigs.splice(index, 1);
}
state.clientConfigs.push({
id: client.id,
config: config
})
},
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}

View File

@ -0,0 +1,100 @@
import ApiService from "../../services/api.service";
const state = {
error: null,
server: null,
config: '',
version: '_ci_build_not_run_properly_',
}
const getters = {
error(state) {
return state.error;
},
server(state) {
return state.server;
},
version(state) {
return state.version;
},
config(state) {
return state.config;
},
}
const actions = {
error({ commit }, error){
commit('error', error)
},
read({ commit, dispatch }){
ApiService.get("/server")
.then(resp => {
commit('server', resp)
dispatch('config')
})
.catch(err => {
commit('error', err)
})
},
update({ commit }, server){
ApiService.patch(`/server`, server)
.then(resp => {
commit('server', resp)
})
.catch(err => {
commit('error', err)
})
},
config({ commit }){
ApiService.getWithConfig("/server/config", {responseType: 'arraybuffer'})
.then(resp => {
commit('config', resp)
})
.catch(err => {
commit('error', err)
})
},
version({ commit }){
ApiService.get("/server/version")
.then(resp => {
commit('version', resp.version)
})
.catch(err => {
commit('error', err)
})
},
}
const mutations = {
error(state, error) {
state.error = error;
},
server(state, server){
state.server = server
},
config(state, config){
state.config = config
},
version(state, version){
state.version = version
},
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}

View File

@ -1,10 +0,0 @@
<template>
</template>
<script>
export default {
created () {
this.$router.replace({ name: 'clients' })
}
}
</script>