Skip to content

Commit

Permalink
Add acme and web server ui, add success dns-01 challenge code
Browse files Browse the repository at this point in the history
  • Loading branch information
NHAS committed Nov 27, 2024
1 parent 3017f67 commit f06cad4
Show file tree
Hide file tree
Showing 14 changed files with 544 additions and 64 deletions.
33 changes: 32 additions & 1 deletion adminui/frontend/src/api/settings.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import type { GenericResponseDTO, MFAMethodDTO, LoginSettingsResponseDTO, GeneralSettingsResponseDTO } from './types'
import type {
GenericResponseDTO,
MFAMethodDTO,
LoginSettingsResponseDTO,
GeneralSettingsResponseDTO,
AcmeDetailsDTO,
WebServerConfigDTO
} from './types'

import { client } from '.'

Expand All @@ -21,3 +28,27 @@ export function updateLoginSettings(settings: LoginSettingsResponseDTO): Promise
export function getMFAMethods(): Promise<MFAMethodDTO[]> {
return client.get('/api/settings/all_mfa_methods').then(res => res.data)
}

export function getWebservers(): Promise<WebServerConfigDTO[]> {
return client.get('/api/settings/webservers').then(res => res.data)
}

export function editWebserver(webserver: WebServerConfigDTO): Promise<GenericResponseDTO> {
return client.put('/api/settings/webserver', webserver).then(res => res.data)
}

export function getAcmeDetails(): Promise<AcmeDetailsDTO> {
return client.get('/api/settings/acme').then(res => res.data)
}

export function setAcmeEmail(email: string): Promise<GenericResponseDTO> {
return client.put('/api/settings/acme/email', { data: email }).then(res => res.data)
}

export function setAcmeProvider(url: string): Promise<GenericResponseDTO> {
return client.put('/api/settings/acme/provider_url', { data: url }).then(res => res.data)
}

export function setAcmeCloudflareDNSKey(cloudflare_api_key: string): Promise<GenericResponseDTO> {
return client.put('/api/settings/acme/cloudflare_api_key', { data: cloudflare_api_key }).then(res => res.data)
}
13 changes: 13 additions & 0 deletions adminui/frontend/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ export interface LoginSettingsResponseDTO {
pam: PamResponseDTO
}

export interface AcmeDetailsDTO {
provider_url: string
email: string
api_token_set: boolean
}

export interface MFAMethodDTO {
friendly_name: string
method: string
Expand Down Expand Up @@ -286,3 +292,10 @@ export enum NodeControlActions {
Stepdown = 'stepdown',
Remove = 'remove'
}

export interface WebServerConfigDTO {
server_name: string
listen_address: string
domain: string
tls: boolean
}
4 changes: 1 addition & 3 deletions adminui/frontend/src/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
import { useRoute, useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
import logo from '../../public/WagLogo.png'
import { useAuthStore } from '@/stores/auth'
import { useInstanceDetailsStore } from '@/stores/serverInfo'
Expand Down Expand Up @@ -70,7 +68,7 @@ async function logout() {
<label for="my-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
<aside class="flex min-h-full w-72 flex-col p-4 bg-neutral text-neutral-content">
<RouterLink to="/dashboard">
<h2 class="btn btn-ghost w-full text-center text-3xl">Wag<img class="h-14" :src="logo" /></h2>
<h2 class="btn btn-ghost w-full text-center text-3xl">Wag<img class="h-14" src="/WagLogo.png" /></h2>
<div class="w-full text-center" v-if="info.serverInfo.version != ''">
<small class="text-center font-mono text-xs">{{ info.serverInfo.version }}</small>
</div>
Expand Down
8 changes: 7 additions & 1 deletion adminui/frontend/src/pages/ClusterEvents.vue
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,19 @@ function openEventInspectionModal(error: GeneralEvent) {
</p>

<label for="members" class="block font-medium text-gray-900 pt-6">New Key Value JSON:</label>
<textarea class="disabled textarea textarea-bordered w-full font-mono" rows="3" v-model="inspectedEvent.state.current"></textarea>
<textarea
class="disabled textarea textarea-bordered w-full font-mono"
disabled
rows="3"
v-model="inspectedEvent.state.current"
></textarea>

<div v-if="inspectedEvent.state.previous.length > 0">
<label for="members" class="block font-medium text-gray-900 pt-6">Previous Key Value JSON:</label>
<textarea
class="disabled textarea textarea-bordered w-full font-mono"
rows="3"
disabled
v-model="inspectedEvent.state.previous"
></textarea>
</div>
Expand Down
234 changes: 228 additions & 6 deletions adminui/frontend/src/pages/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,108 @@ import {
updateLoginSettings,
type GeneralSettingsResponseDTO as GeneralSettingsDTO,
type LoginSettingsResponseDTO as LoginSettingsDTO,
type MFAMethodDTO
type WebServerConfigDTO,
type MFAMethodDTO,
getAcmeDetails,
type AcmeDetailsDTO,
setAcmeCloudflareDNSKey,
setAcmeEmail,
setAcmeProvider,
getWebservers,
editWebserver
} from '@/api'
const toast = useToast()
const { catcher } = useToastError()
const apiTokenSetValue = '**********'
const { data: acme, isLoading: isLoadingAcmeSettings, silentlyRefresh: refreshAcme } = useApi(() => getAcmeDetails())
const { data: general, isLoading: isLoadingGeneralSettings, silentlyRefresh: refreshGeneral } = useApi(() => getGeneralSettings())
const { data: loginSettings, isLoading: isLoadingLoginSettings, silentlyRefresh: refreshLoginSettings } = useApi(() => getLoginSettings())
const { data: webservers, isLoading: isLoadingWebserverSettings, silentlyRefresh: refreshWebservers } = useApi(() => getWebservers())
const { data: mfaTypes, isLoading: isLoadingMFATypes } = useApi(() => getMFAMethods())
const generalData = computed(() => general.value ?? ({} as GeneralSettingsDTO))
const originalAcmeStates = ref<AcmeDetailsDTO>({} as AcmeDetailsDTO)
watch(
acme,
newAcme => {
if (newAcme) {
originalAcmeStates.value = {
api_token_set: newAcme.api_token_set,
email: newAcme.email,
provider_url: newAcme.provider_url
}
}
},
{ immediate: true }
)
const originalServerStates = ref<Record<string, WebServerConfigDTO>>({})
watch(
webservers,
newServers => {
if (newServers) {
originalServerStates.value = newServers.reduce(
(acc, server) => {
acc[server.server_name] = {
server_name: server.server_name,
domain: server.domain,
listen_address: server.listen_address,
tls: server.tls
}
return acc
},
{} as Record<string, WebServerConfigDTO>
)
}
},
{ immediate: true }
)
const getModifiedServers = () => {
if (!webservers.value) return []
return webservers.value.filter(server => {
const original = originalServerStates.value[server.server_name]
if (!original) return true // New server
return original.domain !== server.domain || original.listen_address !== server.listen_address || original.tls !== server.tls
})
}
async function saveServerSettings() {
try {
const updateResults = await Promise.all(getModifiedServers().map(server => editWebserver(server)))
const allSuccessful = updateResults.every(result => result.success) // Adjust based on your API response structure
if (allSuccessful) {
toast.success('updated servers!')
} else {
const failedServers = updateResults.filter(resp => resp.success)
toast.error('failed to save server settings' + failedServers.map(s => s.message))
}
} catch (e) {
catcher(e, 'failed to save acme settings: ')
} finally {
refreshWebservers()
}
}
const generalData = computed(
() =>
general.value ??
({
dns: [] as string[]
} as GeneralSettingsDTO)
)
const loginSettingsData = computed(() => loginSettings.value ?? ({} as LoginSettingsDTO))
const acmeSettingsData = computed(() => acme.value ?? ({} as AcmeDetailsDTO))
const webserversSettingsData = computed(() => webservers.value ?? ([] as WebServerConfigDTO[]))
const textValue = ref(general.value?.dns.join('\n') ?? '')
Expand All @@ -39,10 +128,57 @@ watch(textValue, newValue => {
watch(general, newValue => {
if (newValue) {
textValue.value = newValue.dns.join('\n')
textValue.value = newValue.dns?.join('\n') ?? ''
}
})
const cloudflareApiTokenRef = ref('')
watch(acme, newVal => {
if (newVal?.api_token_set) {
cloudflareApiTokenRef.value = apiTokenSetValue
} else {
cloudflareApiTokenRef.value = ''
}
})
async function saveAcmeSettings() {
try {
let failed = false
if (cloudflareApiTokenRef.value !== apiTokenSetValue) {
const resp = await setAcmeCloudflareDNSKey(cloudflareApiTokenRef.value)
if (!resp.success) {
toast.error('Failed to save cloudflare api token:' + (resp.message ?? 'Unknown Error'))
failed = true
}
}
if (acmeSettingsData.value.email != originalAcmeStates.value.email) {
const resp = await setAcmeEmail(acmeSettingsData.value.email)
if (!resp.success) {
toast.error('Failed to save acme email:' + (resp.message ?? 'Unknown Error'))
failed = true
}
}
if (acmeSettingsData.value.provider_url != originalAcmeStates.value.provider_url) {
const resp = await setAcmeProvider(acmeSettingsData.value.provider_url)
if (!resp.success) {
toast.error('Failed to save acme provider url:' + (resp.message ?? 'Unknown Error'))
failed = true
}
}
if (!failed) {
toast.success('Saved acme settings')
}
} catch (e) {
catcher(e, 'failed to save acme settings: ')
} finally {
refreshAcme()
}
}
async function saveGeneralSettings() {
try {
const resp = await updateGeneralSettings(generalData.value as GeneralSettingsDTO)
Expand Down Expand Up @@ -82,7 +218,9 @@ function filterMfaMethods(enabledMethods: string[], allMethods: MFAMethodDTO[]):

<template>
<main class="w-full p-4">
<PageLoading v-if="isLoadingGeneralSettings || isLoadingLoginSettings || isLoadingMFATypes" />
<PageLoading
v-if="isLoadingGeneralSettings || isLoadingLoginSettings || isLoadingMFATypes || isLoadingAcmeSettings || isLoadingWebserverSettings"
/>

<div v-else>
<h1 class="text-4xl font-bold">Settings</h1>
Expand Down Expand Up @@ -158,7 +296,12 @@ function filterMfaMethods(enabledMethods: string[], allMethods: MFAMethodDTO[]):

<div class="form-control w-full">
<label for="default_method" class="label font-bold">Default MFA Method</label>
<select class="select select-bordered" name="default_method" v-model="loginSettingsData.default_mfa_method">
<select
class="select select-bordered"
name="default_method"
v-model="loginSettingsData.default_mfa_method"
:disabled="loginSettingsData.enabled_mfa_methods.length == 0"
>
<option
v-for="method in filterMfaMethods(loginSettingsData.enabled_mfa_methods, mfaTypes ?? [])"
:selected="method.method == loginSettingsData.default_mfa_method"
Expand All @@ -175,7 +318,11 @@ function filterMfaMethods(enabledMethods: string[], allMethods: MFAMethodDTO[]):
<label :for="method.method" class="label cursor-pointer">
<span class="label-text" :key="method.method">{{ method.friendly_name }}</span>
<span class="flex flex-grow"></span>
<span v-if="method.method == loginSettingsData.default_mfa_method" class="text-gray-400 mr-4">DEFAULT</span>
<span
v-if="method.method == loginSettingsData.default_mfa_method && loginSettingsData.enabled_mfa_methods.length > 0"
class="text-gray-400 mr-4"
>DEFAULT</span
>
<input
:name="method.method"
type="checkbox"
Expand Down Expand Up @@ -277,6 +424,81 @@ function filterMfaMethods(enabledMethods: string[], allMethods: MFAMethodDTO[]):
</div>
</div>
</div>
<div class="card bg-base-100 shadow-xl min-w-[350px] h-max">
<div class="card-body">
<h2 class="card-title">ACME</h2>

<div class="form-control">
<label class="label font-bold">
<span class="label-text">E-Mail</span>
</label>
<input v-model="acmeSettingsData.email" type="email" class="input input-bordered w-full" />
</div>

<div class="form-control">
<label class="label font-bold">
<span class="label-text">Provider</span>
</label>
<input v-model="acmeSettingsData.provider_url" type="url" class="input input-bordered w-full" />
</div>

<div class="form-control">
<label class="label font-bold">
<span class="label-text">Cloudflare API Token</span>
</label>
<input v-model="cloudflareApiTokenRef" type="password" class="input input-bordered w-full" />
</div>

<button type="submit" class="btn btn-primary w-full" @click="() => saveAcmeSettings()">
<span class="loading loading-spinner loading-md" v-if="isLoadingAcmeSettings"></span>
Save
</button>
</div>
</div>
<div class="card bg-base-100 shadow-xl min-w-[350px] h-w max-w-[350px]">
<div class="card-body">
<h2 class="card-title mb-4">Web Servers</h2>
<div class="flex mb-2">
<p>TLS Method:</p>
<div class="flex flex-grow"></div>
<p class="font-bold">
{{
acmeSettingsData.email.length == 0 || acmeSettingsData.provider_url.length == 0
? 'Disabled'
: acmeSettingsData.api_token_set
? 'DNS-01'
: 'HTTP-01'
}}
</p>
</div>
<div role="tablist" class="tabs tabs-bordered">
<template v-for="(server, index) in webserversSettingsData" :key="'webserver-' + server.server_name">
<input type="radio" name="webserver-tabs" role="tab" class="tab" :aria-label="server.server_name" :checked="index == 0" />
<div role="tabpanel" class="tab-content p-10">
<label class="label font-bold">
<span class="label-text">Domain</span>
</label>
<input v-model="server.domain" type="test" class="input input-bordered w-full" />

<label class="label font-bold">
<span class="label-text">Listen Address</span>
</label>
<input v-model="server.listen_address" type="test" class="input input-bordered w-full" />

<label class="label font-bold">
<span class="label-text">TLS</span>
<input v-model="server.tls" type="checkbox" class="toggle toggle-primary" />
</label>
</div>
</template>
</div>

<button type="submit" class="btn btn-primary w-full" @click="saveServerSettings">
<span class="loading loading-spinner loading-md" v-if="isLoadingWebserverSettings"></span>
Save
</button>
</div>
</div>
</div>
</div>
</div>
Expand Down
Loading

0 comments on commit f06cad4

Please sign in to comment.