V2 alpha - initial version (#172)

Initial alpha codebase for version 2 of WireGuard Portal.
This version is considered unstable and incomplete (for example, no public REST API)! 
Use with care!


Fixes/Implements the following issues:
 - OAuth support #154, #1 
 - New Web UI with internationalisation support #98, #107, #89, #62
 - Postgres Support #49 
 - Improved Email handling #47, #119 
 - DNS Search Domain support #46 
 - Bugfixes #94, #48 

---------

Co-authored-by: Fabian Wechselberger <wechselbergerf@hotmail.com>
This commit is contained in:
h44z
2023-08-04 13:34:18 +02:00
committed by GitHub
parent b3a5f2ac60
commit 8b820a5adf
788 changed files with 46139 additions and 11281 deletions

125
frontend/src/stores/auth.js Normal file
View File

@@ -0,0 +1,125 @@
import { defineStore } from 'pinia'
import { notify } from "@kyvg/vue3-notification";
import { apiWrapper } from '@/helpers/fetch-wrapper'
import router from '../router'
export const authStore = defineStore({
id: 'auth',
state: () => ({
// initialize state from local storage to enable user to stay logged in
user: JSON.parse(localStorage.getItem('user')),
providers: [],
returnUrl: localStorage.getItem('returnUrl')
}),
getters: {
UserIdentifier: (state) => state.user?.Identifier || 'unknown',
User: (state) => state.user,
LoginProviders: (state) => state.providers,
IsAuthenticated: (state) => state.user != null,
IsAdmin: (state) => state.user?.IsAdmin || false,
ReturnUrl: (state) => state.returnUrl || '/',
},
actions: {
SetReturnUrl(link) {
this.returnUrl = link
localStorage.setItem('returnUrl', link)
},
ResetReturnUrl() {
this.returnUrl = null
localStorage.removeItem('returnUrl')
},
// LoadProviders always returns a fulfilled promise, even if the request failed.
async LoadProviders() {
apiWrapper.get(`/auth/providers`)
.then(providers => this.providers = providers)
.catch(error => {
this.providers = []
console.log("Failed to load auth providers: ", error)
notify({
title: "Backend Connection Failure",
text: "Failed to load external authentication providers!",
})
})
},
// LoadSession returns promise that might have been rejected if the session was not authenticated.
async LoadSession() {
return apiWrapper.get(`/auth/session`)
.then(session => {
if (session.LoggedIn === true) {
this.ResetReturnUrl()
this.setUserInfo(session)
return session.UserIdentifier
} else {
this.setUserInfo(null)
return Promise.reject(new Error('session not authenticated'))
}
})
.catch(err => {
this.setUserInfo(null)
return Promise.reject(err)
})
},
// Login returns promise that might have been rejected if the login attempt was not successful.
async Login(username, password) {
return apiWrapper.post(`/auth/login`, { username, password })
.then(user => {
this.ResetReturnUrl()
this.setUserInfo(user)
return user.Identifier
})
.catch(err => {
console.log("Login failed:", err)
this.setUserInfo(null)
return Promise.reject(new Error("login failed"))
})
},
async Logout() {
this.setUserInfo(null)
this.ResetReturnUrl() // just to be sure^^
try {
await apiWrapper.post(`/auth/logout`)
} catch (e) {
console.log("Logout request failed:", e)
}
notify({
title: "Logged Out",
text: "Logout successful!",
type: "warn",
})
await router.push('/login')
},
// -- internal setters
setUserInfo(userInfo) {
// store user details and jwt in local storage to keep user logged in between page refreshes
if (userInfo) {
if ('UserIdentifier' in userInfo) { // session object
this.user = {
Identifier: userInfo['UserIdentifier'],
Firstname: userInfo['UserFirstname'],
Lastname: userInfo['UserLastname'],
Email: userInfo['UserEmail'],
IsAdmin: userInfo['IsAdmin']
}
} else { // user object
this.user = {
Identifier: userInfo['Identifier'],
Firstname: userInfo['Firstname'],
Lastname: userInfo['Lastname'],
Email: userInfo['Email'],
IsAdmin: userInfo['IsAdmin']
}
}
localStorage.setItem('user', JSON.stringify(this.user))
} else {
this.user = null
localStorage.removeItem('user')
}
},
}
});

View File

@@ -0,0 +1,152 @@
import { defineStore } from 'pinia'
import {apiWrapper} from '@/helpers/fetch-wrapper'
import {notify} from "@kyvg/vue3-notification";
import { freshInterface } from '@/helpers/models';
import { base64_url_encode } from '@/helpers/encoding';
const baseUrl = `/interface`
export const interfaceStore = defineStore({
id: 'interfaces',
state: () => ({
interfaces: [],
prepared: freshInterface(),
configuration: "",
selected: "",
fetching: false,
}),
getters: {
Count: (state) => state.interfaces.length,
Prepared: (state) => {console.log("STATE:", state.prepared); return state.prepared},
All: (state) => state.interfaces,
Find: (state) => {
return (id) => state.interfaces.find((p) => p.Identifier === id)
},
GetSelected: (state) => state.interfaces.find((i) => i.Identifier === state.selected) || state.interfaces[0],
isFetching: (state) => state.fetching,
},
actions: {
setInterfaces(interfaces) {
this.interfaces = interfaces
if (this.interfaces.length > 0) {
this.selected = this.interfaces[0].Identifier
} else {
this.selected = ""
}
this.fetching = false
},
async LoadInterfaces() {
this.fetching = true
return apiWrapper.get(`${baseUrl}/all`)
.then(this.setInterfaces)
.catch(error => {
this.setInterfaces([])
console.log("Failed to load interfaces: ", error)
notify({
title: "Backend Connection Failure",
text: "Failed to load interfaces!",
})
})
},
setPreparedInterface(iface) {
this.prepared = iface;
},
setInterfaceConfig(ifaceConfig) {
this.configuration = ifaceConfig;
},
async PrepareInterface() {
return apiWrapper.get(`${baseUrl}/prepare`)
.then(this.setPreparedInterface)
.catch(error => {
this.prepared = freshInterface()
console.log("Failed to load prepared interface: ", error)
notify({
title: "Backend Connection Failure",
text: "Failed to load prepared interface!",
})
})
},
async LoadInterfaceConfig(id) {
return apiWrapper.get(`${baseUrl}/config/${base64_url_encode(id)}`)
.then(this.setInterfaceConfig)
.catch(error => {
this.configuration = ""
console.log("Failed to load interface configuration: ", error)
notify({
title: "Backend Connection Failure",
text: "Failed to load interface configuration!",
})
})
},
async DeleteInterface(id) {
this.fetching = true
return apiWrapper.delete(`${baseUrl}/${base64_url_encode(id)}`)
.then(() => {
this.interfaces = this.interfaces.filter(i => i.Identifier !== id)
if (this.interfaces.length > 0) {
this.selected = this.interfaces[0].Identifier
} else {
this.selected = ""
}
this.fetching = false
})
.catch(error => {
this.fetching = false
console.log(error)
throw new Error(error)
})
},
async UpdateInterface(id, formData) {
this.fetching = true
return apiWrapper.put(`${baseUrl}/${base64_url_encode(id)}`, formData)
.then(iface => {
let idx = this.interfaces.findIndex((i) => i.Identifier === id)
this.interfaces[idx] = iface
this.fetching = false
})
.catch(error => {
this.fetching = false
console.log(error)
throw new Error(error)
})
},
async CreateInterface(formData) {
this.fetching = true
return apiWrapper.post(`${baseUrl}/new`, formData)
.then(iface => {
this.interfaces.push(iface)
this.fetching = false
})
.catch(error => {
this.fetching = false
console.log(error)
throw new Error(error)
})
},
async ApplyPeerDefaults(id, formData) {
this.fetching = true
return apiWrapper.post(`${baseUrl}/${base64_url_encode(id)}/apply-peer-defaults`, formData)
.then(() => {
this.fetching = false
})
.catch(error => {
this.fetching = false
console.log(error)
throw new Error(error)
})
},
async SaveConfiguration(id) {
this.fetching = true
return apiWrapper.post(`${baseUrl}/${base64_url_encode(id)}/save-config`)
.then(() => {
this.fetching = false
})
.catch(error => {
this.fetching = false
console.log(error)
throw new Error(error)
})
}
}
})

View File

@@ -0,0 +1,258 @@
import { defineStore } from 'pinia'
import {apiWrapper} from "@/helpers/fetch-wrapper";
import {notify} from "@kyvg/vue3-notification";
import {interfaceStore} from "./interfaces";
import {freshPeer, freshStats} from '@/helpers/models';
import { base64_url_encode } from '@/helpers/encoding';
const baseUrl = `/peer`
export const peerStore = defineStore({
id: 'peers',
state: () => ({
peers: [],
stats: {},
statsEnabled: false,
peer: freshPeer(),
prepared: freshPeer(),
configuration: "",
filter: "",
pageSize: 10,
pageOffset: 0,
pages: [],
fetching: false,
}),
getters: {
Find: (state) => {
return (id) => state.peers.find((p) => p.Identifier === id)
},
Count: (state) => state.peers.length,
Prepared: (state) => {console.log("STATE:", state.prepared); return state.prepared},
FilteredCount: (state) => state.Filtered.length,
All: (state) => state.peers,
Filtered: (state) => {
if (!state.filter) {
return state.peers
}
return state.peers.filter((p) => {
return p.DisplayName.includes(state.filter) || p.Identifier.includes(state.filter)
})
},
FilteredAndPaged: (state) => {
return state.Filtered.slice(state.pageOffset, state.pageOffset + state.pageSize)
},
ConfigQrUrl: (state) => {
return (id) => state.peers.find((p) => p.Identifier === id) ? apiWrapper.url(`${baseUrl}/config-qr/${base64_url_encode(id)}`) : ''
},
isFetching: (state) => state.fetching,
hasNextPage: (state) => state.pageOffset < (state.FilteredCount - state.pageSize),
hasPrevPage: (state) => state.pageOffset > 0,
currentPage: (state) => (state.pageOffset / state.pageSize)+1,
Statistics: (state) => {
return (id) => state.statsEnabled && (id in state.stats) ? state.stats[id] : freshStats()
},
hasStatistics: (state) => state.statsEnabled,
},
actions: {
afterPageSizeChange() {
// reset pageOffset to avoid problems with new page sizes
this.pageOffset = 0
this.calculatePages()
},
calculatePages() {
let pageCounter = 1;
this.pages = []
for (let i = 0; i < this.FilteredCount; i+=this.pageSize) {
this.pages.push(pageCounter++)
}
},
gotoPage(page) {
this.pageOffset = (page-1) * this.pageSize
this.calculatePages()
},
nextPage() {
this.pageOffset += this.pageSize
this.calculatePages()
},
previousPage() {
this.pageOffset -= this.pageSize
this.calculatePages()
},
setPeers(peers) {
this.peers = peers
this.calculatePages()
this.fetching = false
},
setPeer(peer) {
this.peer = peer
this.fetching = false
},
setPreparedPeer(peer) {
this.prepared = peer;
},
setPeerConfig(config) {
this.configuration = config;
},
setStats(statsResponse) {
if (!statsResponse) {
this.stats = {}
this.statsEnabled = false
}
this.stats = statsResponse.Stats
this.statsEnabled = statsResponse.Enabled
},
async PreparePeer(interfaceId) {
return apiWrapper.get(`${baseUrl}/iface/${base64_url_encode(interfaceId)}/prepare`)
.then(this.setPreparedPeer)
.catch(error => {
this.prepared = freshPeer()
console.log("Failed to load prepared peer: ", error)
notify({
title: "Backend Connection Failure",
text: "Failed to load prepared peer!",
})
})
},
async MailPeerConfig(linkOnly, ids) {
return apiWrapper.post(`${baseUrl}/config-mail`, {
Identifiers: ids,
LinkOnly: linkOnly
})
.then(() => {
notify({
title: "Peer Configuration sent",
text: "Email sent to linked user!",
})
})
.catch(error => {
console.log("Failed to send peer configuration: ", error)
throw new Error(error)
})
},
async LoadPeerConfig(id) {
return apiWrapper.get(`${baseUrl}/config/${base64_url_encode(id)}`)
.then(this.setPeerConfig)
.catch(error => {
this.configuration = ""
console.log("Failed to load peer configuration: ", error)
notify({
title: "Backend Connection Failure",
text: "Failed to load peer configuration!",
})
})
},
async LoadPeer(id) {
this.fetching = true
return apiWrapper.get(`${baseUrl}/${base64_url_encode(id)}`)
.then(this.setPeer)
.catch(error => {
this.setPeers([])
console.log("Failed to load peer: ", error)
notify({
title: "Backend Connection Failure",
text: "Failed to load peer!",
})
})
},
async LoadStats(interfaceId) {
// if no interfaceId is given, use the currently selected interface
if (!interfaceId) {
interfaceId = interfaceStore().GetSelected.Identifier
if (!interfaceId) {
return // no interface, nothing to load
}
}
this.fetching = true
return apiWrapper.get(`${baseUrl}/iface/${base64_url_encode(interfaceId)}/stats`)
.then(this.setStats)
.catch(error => {
this.setStats(undefined)
console.log("Failed to load peer stats: ", error)
notify({
title: "Backend Connection Failure",
text: "Failed to load peer stats!",
})
})
},
async DeletePeer(id) {
this.fetching = true
return apiWrapper.delete(`${baseUrl}/${base64_url_encode(id)}`)
.then(() => {
this.peers = this.peers.filter(p => p.Identifier !== id)
this.fetching = false
})
.catch(error => {
this.fetching = false
console.log(error)
throw new Error(error)
})
},
async UpdatePeer(id, formData) {
this.fetching = true
return apiWrapper.put(`${baseUrl}/${base64_url_encode(id)}`, formData)
.then(peer => {
let idx = this.peers.findIndex((p) => p.Identifier === id)
this.peers[idx] = peer
this.fetching = false
})
.catch(error => {
this.fetching = false
console.log(error)
throw new Error(error)
})
},
async CreatePeer(interfaceId, formData) {
this.fetching = true
return apiWrapper.post(`${baseUrl}/iface/${base64_url_encode(interfaceId)}/new`, formData)
.then(peer => {
this.peers.push(peer)
this.fetching = false
})
.catch(error => {
this.fetching = false
console.log(error)
throw new Error(error)
})
},
async CreateMultiplePeers(interfaceId, formData) {
this.fetching = true
return apiWrapper.post(`${baseUrl}/iface/${base64_url_encode(interfaceId)}/multiplenew`, formData)
.then(peers => {
this.peers.push(...peers)
this.fetching = false
})
.catch(error => {
this.fetching = false
console.log(error)
throw new Error(error)
})
},
async LoadPeers(interfaceId) {
// if no interfaceId is given, use the currently selected interface
if (!interfaceId) {
interfaceId = interfaceStore().GetSelected.Identifier
if (!interfaceId) {
return // no interface, nothing to load
}
}
this.fetching = true
return apiWrapper.get(`${baseUrl}/iface/${base64_url_encode(interfaceId)}/all`)
.then(this.setPeers)
.catch(error => {
this.setPeers([])
console.log("Failed to load peers: ", error)
notify({
title: "Backend Connection Failure",
text: "Failed to load peers!",
})
})
}
}
})

View File

@@ -0,0 +1,137 @@
import { defineStore } from 'pinia'
import {apiWrapper} from "@/helpers/fetch-wrapper";
import {notify} from "@kyvg/vue3-notification";
import {authStore} from "@/stores/auth";
import { base64_url_encode } from '@/helpers/encoding';
import {freshStats} from "@/helpers/models";
const baseUrl = `/user`
export const profileStore = defineStore({
id: 'profile',
state: () => ({
peers: [],
stats: {},
statsEnabled: false,
user: {},
filter: "",
pageSize: 10,
pageOffset: 0,
pages: [],
fetching: false,
}),
getters: {
FindPeers: (state) => {
return (id) => state.peers.find((p) => p.Identifier === id)
},
CountPeers: (state) => state.peers.length,
FilteredPeerCount: (state) => state.FilteredPeers.length,
Peers: (state) => state.peers,
FilteredPeers: (state) => {
if (!state.filter) {
return state.peers
}
return state.peers.filter((p) => {
return p.DisplayName.includes(state.filter) || p.Identifier.includes(state.filter)
})
},
FilteredAndPagedPeers: (state) => {
return state.FilteredPeers.slice(state.pageOffset, state.pageOffset + state.pageSize)
},
isFetching: (state) => state.fetching,
hasNextPage: (state) => state.pageOffset < (state.FilteredPeerCount - state.pageSize),
hasPrevPage: (state) => state.pageOffset > 0,
currentPage: (state) => (state.pageOffset / state.pageSize)+1,
Statistics: (state) => {
return (id) => state.statsEnabled && (id in state.stats) ? state.stats[id] : freshStats()
},
hasStatistics: (state) => state.statsEnabled,
},
actions: {
afterPageSizeChange() {
// reset pageOffset to avoid problems with new page sizes
this.pageOffset = 0
this.calculatePages()
},
calculatePages() {
let pageCounter = 1;
this.pages = []
for (let i = 0; i < this.FilteredPeerCount; i+=this.pageSize) {
this.pages.push(pageCounter++)
}
},
gotoPage(page) {
this.pageOffset = (page-1) * this.pageSize
this.calculatePages()
},
nextPage() {
this.pageOffset += this.pageSize
this.calculatePages()
},
previousPage() {
this.pageOffset -= this.pageSize
this.calculatePages()
},
setPeers(peers) {
this.peers = peers
this.fetching = false
},
setUser(user) {
this.user = user
this.fetching = false
},
setStats(statsResponse) {
if (!statsResponse) {
this.stats = {}
this.statsEnabled = false
}
this.stats = statsResponse.Stats
this.statsEnabled = statsResponse.Enabled
},
async LoadPeers() {
this.fetching = true
let currentUser = authStore().user.Identifier
return apiWrapper.get(`${baseUrl}/${base64_url_encode(currentUser)}/peers`)
.then(this.setPeers)
.catch(error => {
this.setPeers([])
console.log("Failed to load user peers for ", currentUser, ": ", error)
notify({
title: "Backend Connection Failure",
text: "Failed to load user peers!",
})
})
},
async LoadStats() {
this.fetching = true
let currentUser = authStore().user.Identifier
return apiWrapper.get(`${baseUrl}/${base64_url_encode(currentUser)}/stats`)
.then(this.setStats)
.catch(error => {
this.setStats(undefined)
console.log("Failed to load peer stats: ", error)
notify({
title: "Backend Connection Failure",
text: "Failed to load peer stats!",
})
})
},
async LoadUser() {
this.fetching = true
let currentUser = authStore().user.Identifier
return apiWrapper.get(`${baseUrl}/${base64_url_encode(currentUser)}`)
.then(this.setUser)
.catch(error => {
this.setUser({})
console.log("Failed to load user for ", currentUser, ": ", error)
notify({
title: "Backend Connection Failure",
text: "Failed to load user!",
})
})
},
}
})

View File

@@ -0,0 +1,32 @@
import { defineStore } from 'pinia'
import { notify } from "@kyvg/vue3-notification";
import { apiWrapper } from '@/helpers/fetch-wrapper'
export const securityStore = defineStore({
id: 'security',
state: () => ({
csrfToken: "",
}),
getters: {
CsrfToken: (state) => state.csrfToken,
},
actions: {
SetCsrfToken(token) {
this.csrfToken = token
},
// LoadSecurityProperties always returns a fulfilled promise, even if the request failed.
async LoadSecurityProperties() {
await apiWrapper.get(`/csrf`)
.then(token => this.SetCsrfToken(token))
.catch(error => {
this.SetCsrfToken("");
console.log("Failed to load csrf token: ", error);
notify({
title: "Backend Connection Failure",
text: "Failed to load csrf token!",
});
})
}
}
});

View File

@@ -0,0 +1,36 @@
import { defineStore } from 'pinia'
import { notify } from "@kyvg/vue3-notification";
import { apiWrapper } from '@/helpers/fetch-wrapper'
const baseUrl = `/config`
export const settingsStore = defineStore({
id: 'settings',
state: () => ({
settings: {},
}),
getters: {
Setting: (state) => {
return (key) => (key in state.settings) ? state.settings[key] : undefined
}
},
actions: {
setSettings(settings) {
this.settings = settings
},
// LoadSecurityProperties always returns a fulfilled promise, even if the request failed.
async LoadSettings() {
await apiWrapper.get(`${baseUrl}/settings`)
.then(data => this.setSettings(data))
.catch(error => {
this.setSettings({});
console.log("Failed to load settings: ", error);
notify({
title: "Backend Connection Failure",
text: "Failed to load settings!",
});
})
}
}
});

View File

@@ -0,0 +1,147 @@
import { defineStore } from 'pinia'
import {apiWrapper} from "@/helpers/fetch-wrapper";
import {notify} from "@kyvg/vue3-notification";
import { base64_url_encode } from '@/helpers/encoding';
const baseUrl = `/user`
export const userStore = defineStore({
id: 'users',
state: () => ({
userPeers: [],
users: [],
filter: "",
pageSize: 10,
pageOffset: 0,
pages: [],
fetching: false,
}),
getters: {
Find: (state) => {
return (id) => state.users.find((p) => p.Identifier === id)
},
Count: (state) => state.users.length,
FilteredCount: (state) => state.Filtered.length,
All: (state) => state.users,
Peers: (state) => state.userPeers,
Filtered: (state) => {
if (!state.filter) {
return state.users
}
return state.users.filter((u) => {
return u.Firstname.includes(state.filter) || u.Lastname.includes(state.filter) || u.Email.includes(state.filter) || u.Identifier.includes(state.filter)
})
},
FilteredAndPaged: (state) => {
return state.Filtered.slice(state.pageOffset, state.pageOffset + state.pageSize)
},
isFetching: (state) => state.fetching,
hasNextPage: (state) => state.pageOffset < (state.FilteredCount - state.pageSize),
hasPrevPage: (state) => state.pageOffset > 0,
currentPage: (state) => (state.pageOffset / state.pageSize)+1,
},
actions: {
afterPageSizeChange() {
// reset pageOffset to avoid problems with new page sizes
this.pageOffset = 0
this.calculatePages()
},
calculatePages() {
let pageCounter = 1;
this.pages = []
for (let i = 0; i < this.FilteredCount; i+=this.pageSize) {
this.pages.push(pageCounter++)
}
},
gotoPage(page) {
this.pageOffset = (page-1) * this.pageSize
this.calculatePages()
},
nextPage() {
this.pageOffset += this.pageSize
this.calculatePages()
},
previousPage() {
this.pageOffset -= this.pageSize
this.calculatePages()
},
setUsers(users) {
this.users = users
this.calculatePages()
this.fetching = false
},
setUserPeers(peers) {
this.userPeers = peers
this.fetching = false
},
async LoadUsers() {
this.fetching = true
return apiWrapper.get(`${baseUrl}/all`)
.then(this.setUsers)
.catch(error => {
this.setUsers([])
console.log("Failed to load users: ", error)
notify({
title: "Backend Connection Failure",
text: "Failed to load users!",
})
})
},
async DeleteUser(id) {
this.fetching = true
return apiWrapper.delete(`${baseUrl}/${base64_url_encode(id)}`)
.then(() => {
this.users = this.users.filter(u => u.Identifier !== id)
this.fetching = false
})
.catch(error => {
this.fetching = false
console.log(error)
throw new Error(error)
})
},
async UpdateUser(id, formData) {
this.fetching = true
return apiWrapper.put(`${baseUrl}/${base64_url_encode(id)}`, formData)
.then(user => {
let idx = this.users.findIndex((u) => u.Identifier === id)
this.users[idx] = user
this.fetching = false
})
.catch(error => {
this.fetching = false
console.log(error)
throw new Error(error)
})
},
async CreateUser(formData) {
this.fetching = true
return apiWrapper.post(`${baseUrl}/new`, formData)
.then(user => {
this.users.push(user)
this.fetching = false
})
.catch(error => {
this.fetching = false
console.log(error)
throw new Error(error)
})
},
async LoadUserPeers(id) {
this.fetching = true
return apiWrapper.get(`${baseUrl}/${base64_url_encode(id)}/peers`)
.then(this.setUserPeers)
.catch(error => {
this.setUserPeers([])
console.log("Failed to load user peers for ",id ,": ", error)
notify({
title: "Backend Connection Failure",
text: "Failed to load user peers!",
})
})
},
}
})