refactor(web): Refactor web logic to extract reusable remote client management module (#1465)

This commit is contained in:
Mg Pig
2025-10-13 23:59:46 +08:00
committed by GitHub
parent 999a486928
commit 87b7b7ed7c
24 changed files with 1382 additions and 995 deletions

View File

@@ -1,11 +1,11 @@
<script lang="ts" setup>
import { computed, inject, ref } from 'vue';
import { Card, Password, Button } from 'primevue';
import { Api } from 'easytier-frontend-lib';
import ApiClient from '../modules/api';
const dialogRef = inject<any>('dialogRef');
const api = computed<Api.ApiClient>(() => dialogRef.value.data.api);
const api = computed<ApiClient>(() => dialogRef.value.data.api);
const password = ref('');

View File

@@ -1,11 +1,11 @@
<script setup lang="ts">
import { NetworkTypes } from 'easytier-frontend-lib';
import { computed, ref } from 'vue';
import { Api } from 'easytier-frontend-lib'
import { AutoComplete, Divider, Button, Textarea } from "primevue";
import { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost } from "../modules/api-host"
import ApiClient from '../modules/api';
const api = computed<Api.ApiClient>(() => new Api.ApiClient(apiHost.value));
const api = computed<ApiClient>(() => new ApiClient(apiHost.value));
const apiHost = ref<string>(getInitialApiHost())
const apiHostSuggestions = ref<Array<string>>([])
@@ -27,9 +27,7 @@ const errorMessage = ref<string>("");
const generateConfig = (config: NetworkTypes.NetworkConfig) => {
saveApiHost(apiHost.value)
errorMessage.value = "";
api.value?.generate_config({
config: config
}).then((res) => {
api.value?.get_remote_client("").generate_config(config).then((res) => {
if (res.error) {
errorMessage.value = "Generation failed: " + res.error;
} else if (res.toml_config) {
@@ -45,9 +43,7 @@ const generateConfig = (config: NetworkTypes.NetworkConfig) => {
const parseConfig = async () => {
try {
errorMessage.value = "";
const res = await api.value?.parse_config({
toml_config: toml_config.value
});
const res = await api.value?.get_remote_client("").parse_config(toml_config.value);
if (res.error) {
errorMessage.value = "Parse failed: " + res.error;

View File

@@ -1,15 +1,16 @@
<script setup lang="ts">
import { Card, useToast } from 'primevue';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { Api, Utils } from 'easytier-frontend-lib';
import { Utils } from 'easytier-frontend-lib';
import ApiClient, { Summary } from '../modules/api';
const props = defineProps({
api: Api.ApiClient,
api: ApiClient,
});
const toast = useToast();
const summary = ref<Api.Summary | undefined>(undefined);
const summary = ref<Summary | undefined>(undefined);
const loadSummary = async () => {
const resp = await props.api?.get_summary();

View File

@@ -3,9 +3,10 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { Button, Drawer, ProgressSpinner, useToast, InputSwitch, Popover, Dropdown, Toolbar } from 'primevue';
import Tooltip from 'primevue/tooltip';
import { useRoute, useRouter } from 'vue-router';
import { Api, Utils } from 'easytier-frontend-lib';
import { Utils } from 'easytier-frontend-lib';
import DeviceDetails from './DeviceDetails.vue';
import { useI18n } from 'vue-i18n'
import ApiClient from '../modules/api';
const { t } = useI18n()
@@ -15,7 +16,7 @@ declare const window: Window & typeof globalThis;
const vTooltip = Tooltip;
const props = defineProps({
api: Api.ApiClient,
api: ApiClient,
});
const detailPopover = ref();

View File

@@ -1,14 +1,12 @@
<script setup lang="ts">
import { IftaLabel, Select, Button, ConfirmPopup, useConfirm, useToast, Divider, Menu } from 'primevue';
import { NetworkTypes, Status, Utils, Api, ConfigEditDialog } from 'easytier-frontend-lib';
import { watch, computed, onMounted, onUnmounted, ref } from 'vue';
import { NetworkTypes, Utils, Api, RemoteManagement } from 'easytier-frontend-lib';
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'
import ApiClient from '../modules/api';
const { t } = useI18n()
const props = defineProps<{
api: Api.ApiClient;
api: ApiClient;
deviceList: Array<Utils.DeviceInfo> | undefined;
}>();
@@ -16,7 +14,6 @@ const emits = defineEmits(['update']);
const route = useRoute();
const router = useRouter();
const toast = useToast();
const deviceId = computed<string>(() => {
return route.params.deviceId as string;
@@ -30,469 +27,29 @@ const deviceInfo = computed<Utils.DeviceInfo | undefined | null>(() => {
return deviceId.value ? props.deviceList?.find((device) => device.machine_id === deviceId.value) : null;
});
const configFile = ref();
const curNetworkInfo = ref<NetworkTypes.NetworkInstance | null>(null);
const isEditing = ref(false);
const showCreateNetworkDialog = ref(false);
const showConfigEditDialog = ref(false);
const isCreatingNetwork = ref(false); // Flag to indicate if we're in network creation mode
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
const listInstanceIdResponse = ref<Api.ListNetworkInstanceIdResponse | undefined>(undefined);
const instanceIdList = computed(() => {
let insts = new Set(deviceInfo.value?.running_network_instances || []);
let t = listInstanceIdResponse.value;
if (t) {
t.running_inst_ids.forEach((u) => insts.add(Utils.UuidToStr(u)));
t.disabled_inst_ids.forEach((u) => insts.add(Utils.UuidToStr(u)));
}
let options = Array.from(insts).map((instance: string) => {
return { uuid: instance };
});
return options;
});
const selectedInstanceId = computed({
get() {
return instanceIdList.value.find((instance) => instance.uuid === instanceId.value);
return instanceId.value;
},
set(value: any) {
console.log("set instanceId", value);
router.push({ name: 'deviceManagement', params: { deviceId: deviceId.value, instanceId: value.uuid } });
set(value: string) {
console.log("selectedInstanceId", value);
router.push({ name: 'deviceManagement', params: { deviceId: deviceId.value, instanceId: value } });
}
});
const needShowNetworkStatus = computed(() => {
if (!selectedInstanceId.value) {
// nothing selected
return false;
}
if (networkIsDisabled.value) {
// network is disabled
return false;
}
return true;
})
const remoteClient = computed<Api.RemoteClient>(() => props.api.get_remote_client(deviceId.value));
const networkIsDisabled = computed(() => {
if (!selectedInstanceId.value) {
return false;
}
return listInstanceIdResponse.value?.disabled_inst_ids.map(Utils.UuidToStr).includes(selectedInstanceId.value?.uuid);
});
watch(selectedInstanceId, async (newVal, oldVal) => {
if (newVal?.uuid !== oldVal?.uuid && networkIsDisabled.value) {
await loadDisabledNetworkConfig();
}
});
const disabledNetworkConfig = ref<NetworkTypes.NetworkConfig | undefined>(undefined);
const loadDisabledNetworkConfig = async () => {
disabledNetworkConfig.value = undefined;
if (!deviceId.value || !selectedInstanceId.value) {
return;
}
let ret = await props.api?.get_network_config(deviceId.value, selectedInstanceId.value.uuid);
disabledNetworkConfig.value = ret;
const newConfigGenerator = () => {
const config = NetworkTypes.DEFAULT_NETWORK_CONFIG();
config.hostname = deviceInfo.value?.hostname;
return config;
}
const updateNetworkState = async (disabled: boolean) => {
if (!deviceId.value || !selectedInstanceId.value) {
return;
}
if (disabled || !disabledNetworkConfig.value) {
await props.api?.update_device_instance_state(deviceId.value, selectedInstanceId.value.uuid, disabled);
} else if (disabledNetworkConfig.value) {
await props.api?.delete_network(deviceId.value, disabledNetworkConfig.value.instance_id);
await props.api?.run_network(deviceId.value, disabledNetworkConfig.value);
}
await loadNetworkInstanceIds();
}
const confirm = useConfirm();
const confirmDeleteNetwork = (event: any) => {
confirm.require({
target: event.currentTarget,
message: 'Do you want to delete this network?',
icon: 'pi pi-info-circle',
rejectProps: {
label: 'Cancel',
severity: 'secondary',
outlined: true
},
acceptProps: {
label: 'Delete',
severity: 'danger'
},
accept: async () => {
try {
await props.api?.delete_network(deviceId.value, instanceId.value);
} catch (e) {
console.error(e);
}
emits('update');
},
reject: () => {
return;
}
});
};
// const verifyNetworkConfig = async (): Promise<ValidateConfigResponse | undefined> => {
// let ret = await props.api?.validate_config(deviceId.value, newNetworkConfig.value);
// console.log("verifyNetworkConfig", ret);
// return ret;
// }
const createNewNetwork = async () => {
try {
if (isEditing.value) {
await props.api?.delete_network(deviceId.value, instanceId.value);
}
let ret = await props.api?.run_network(deviceId.value, newNetworkConfig.value);
console.debug("createNewNetwork", ret);
} catch (e: any) {
console.error(e);
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to create network, error: ' + JSON.stringify(e.response.data), life: 2000 });
return;
}
emits('update');
showCreateNetworkDialog.value = false;
isCreatingNetwork.value = false; // Exit creation mode after successful network creation
}
const newNetwork = () => {
newNetworkConfig.value = NetworkTypes.DEFAULT_NETWORK_CONFIG();
newNetworkConfig.value.hostname = deviceInfo.value?.hostname;
isEditing.value = false;
// showCreateNetworkDialog.value = true; // Old dialog approach
isCreatingNetwork.value = true; // Switch to creation mode instead
}
const cancelNetworkCreation = () => {
isCreatingNetwork.value = false;
}
const editNetwork = async () => {
if (!deviceId.value || !instanceId.value) {
toast.add({ severity: 'error', summary: 'Error', detail: 'No network instance selected', life: 2000 });
return;
}
isEditing.value = true;
try {
let ret = await props.api?.get_network_config(deviceId.value, instanceId.value);
console.debug("editNetwork", ret);
newNetworkConfig.value = ret;
// showCreateNetworkDialog.value = true; // Old dialog approach
isCreatingNetwork.value = true; // Switch to creation mode instead
} catch (e: any) {
console.error(e);
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to edit network, error: ' + JSON.stringify(e.response.data), life: 2000 });
return;
}
}
const loadNetworkInstanceIds = async () => {
if (!deviceId.value) {
return;
}
listInstanceIdResponse.value = await props.api?.list_deivce_instance_ids(deviceId.value);
console.debug("loadNetworkInstanceIds", listInstanceIdResponse.value);
}
const loadDeviceInfo = async () => {
if (!deviceId.value || !instanceId.value) {
return;
}
let ret = await props.api?.get_network_info(deviceId.value, instanceId.value);
let device_info = ret[instanceId.value];
curNetworkInfo.value = {
instance_id: instanceId.value,
running: device_info.running,
error_msg: device_info.error_msg,
detail: device_info,
} as NetworkTypes.NetworkInstance;
}
const exportConfig = async () => {
if (!deviceId.value || !instanceId.value) {
toast.add({ severity: 'error', summary: 'Error', detail: 'No network instance selected', life: 2000 });
return;
}
try {
let networkConfig = await props.api?.get_network_config(deviceId.value, instanceId.value);
delete networkConfig.instance_id;
let { toml_config: tomlConfig, error } = await props.api?.generate_config({
config: networkConfig
});
if (error) {
throw { response: { data: error } };
}
exportTomlFile(tomlConfig ?? '', instanceId.value + '.toml');
} catch (e: any) {
console.error(e);
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to export network config, error: ' + JSON.stringify(e.response.data), life: 2000 });
return;
}
}
const importConfig = () => {
configFile.value.click();
}
const handleFileUpload = (event: Event) => {
const files = (event.target as HTMLInputElement).files;
const file = files ? files[0] : null;
if (!file) return;
const reader = new FileReader();
reader.onload = async (e) => {
try {
let tomlConfig = e.target?.result?.toString();
if (!tomlConfig) return;
const resp = await props.api?.parse_config({ toml_config: tomlConfig });
if (resp.error) {
throw resp.error;
}
const config = resp.config;
if (!config) return;
config.instance_id = newNetworkConfig.value?.instance_id ?? config?.instance_id;
Object.assign(newNetworkConfig.value, resp.config);
toast.add({ severity: 'success', summary: 'Import Success', detail: "Config file import success", life: 2000 });
} catch (error) {
toast.add({ severity: 'error', summary: 'Error', detail: 'Config file parse error: ' + error, life: 2000 });
}
configFile.value.value = null;
}
reader.readAsText(file);
}
const exportTomlFile = (context: string, name: string) => {
let url = window.URL.createObjectURL(new Blob([context], { type: 'application/toml' }));
let link = document.createElement('a');
link.style.display = 'none';
link.href = url;
link.setAttribute('download', name);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}
const generateConfig = async (config: NetworkTypes.NetworkConfig): Promise<string> => {
let { toml_config: tomlConfig, error } = await props.api?.generate_config({ config });
if (error) {
throw error;
}
return tomlConfig ?? '';
}
const saveConfig = async (tomlConfig: string): Promise<void> => {
let resp = await props.api?.parse_config({ toml_config: tomlConfig });
if (resp.error) {
throw resp.error;
};
const config = resp.config;
if (!config) {
throw new Error("Parsed config is empty");
}
config.instance_id = disabledNetworkConfig.value?.instance_id ?? config?.instance_id;
if (networkIsDisabled.value) {
disabledNetworkConfig.value = config;
} else {
newNetworkConfig.value = config;
}
}
// 响应式屏幕宽度
const screenWidth = ref(window.innerWidth);
const updateScreenWidth = () => {
screenWidth.value = window.innerWidth;
};
// 菜单引用和菜单项
const menuRef = ref();
const actionMenu = ref([
{
label: t('web.device_management.edit_network'),
icon: 'pi pi-pencil',
command: () => editNetwork()
},
{
label: t('web.device_management.export_config'),
icon: 'pi pi-download',
command: () => exportConfig()
},
{
label: t('web.device_management.delete_network'),
icon: 'pi pi-trash',
class: 'p-error',
command: () => confirmDeleteNetwork(new Event('click'))
}
]);
let periodFunc = new Utils.PeriodicTask(async () => {
try {
await Promise.all([loadNetworkInstanceIds(), loadDeviceInfo()]);
} catch (e) {
console.debug(e);
}
}, 1000);
onMounted(async () => {
periodFunc.start();
// 添加屏幕尺寸监听
window.addEventListener('resize', updateScreenWidth);
});
onUnmounted(() => {
periodFunc.stop();
// 移除屏幕尺寸监听
window.removeEventListener('resize', updateScreenWidth);
});
</script>
<template>
<div class="device-management">
<input type="file" @change="handleFileUpload" class="hidden" accept="application/toml" ref="configFile" />
<ConfirmPopup></ConfirmPopup>
<!-- 网络选择和操作按钮始终在同一行 -->
<div class="network-header bg-surface-50 p-3 rounded-lg shadow-sm mb-1">
<div class="flex flex-row justify-between items-center gap-2" style="align-items: center;">
<!-- 网络选择 -->
<div class="flex-1 min-w-0">
<IftaLabel class="w-full">
<Select v-model="selectedInstanceId" :options="instanceIdList" optionLabel="uuid" class="w-full"
inputId="dd-inst-id" :placeholder="t('web.device_management.select_network')"
:pt="{ root: { class: 'network-select-container' } }" />
<label class="network-label mr-2 font-medium" for="dd-inst-id">{{
t('web.device_management.network') }}</label>
</IftaLabel>
</div>
<!-- 简化的按钮区域 - 无论屏幕大小都显示 -->
<div class="flex gap-2 shrink-0 button-container items-center">
<!-- Create/Cancel button based on state -->
<Button v-if="!isCreatingNetwork" @click="newNetwork" icon="pi pi-plus"
:label="screenWidth > 640 ? t('web.device_management.create_new') : undefined"
:class="['create-button', screenWidth <= 640 ? 'p-button-icon-only' : '']"
:style="screenWidth <= 640 ? 'width: 3rem !important; height: 3rem !important; font-size: 1.2rem' : ''"
:tooltip="screenWidth <= 640 ? t('web.device_management.create_network') : undefined"
tooltipOptions="{ position: 'bottom' }" severity="primary" />
<Button v-else @click="cancelNetworkCreation" icon="pi pi-times"
:label="screenWidth > 640 ? t('web.device_management.cancel_creation') : undefined"
:class="['cancel-button', screenWidth <= 640 ? 'p-button-icon-only' : '']"
:style="screenWidth <= 640 ? 'width: 3rem !important; height: 3rem !important; font-size: 1.2rem' : ''"
:tooltip="screenWidth <= 640 ? t('web.device_management.cancel_creation') : undefined"
tooltipOptions="{ position: 'bottom' }" severity="secondary" />
<!-- More actions menu -->
<Menu ref="menuRef" :model="actionMenu" :popup="true" />
<Button v-if="!isCreatingNetwork && selectedInstanceId" icon="pi pi-ellipsis-v"
class="p-button-rounded flex items-center justify-center" severity="help"
style="width: 3rem !important; height: 3rem !important; font-size: 1.2rem"
@click="menuRef.toggle($event)" :aria-label="t('web.device_management.more_actions')"
:tooltip="t('web.device_management.more_actions')" tooltipOptions="{ position: 'bottom' }" />
</div>
</div>
</div>
<!-- Main Content Area -->
<div class="network-content bg-surface-0 p-4 rounded-lg shadow-sm">
<!-- Network Creation Form -->
<div v-if="isCreatingNetwork" class="network-creation-container">
<div class="network-creation-header flex items-center gap-2 mb-3">
<i class="pi pi-plus-circle text-primary text-xl"></i>
<h2 class="text-xl font-medium">{{ isEditing ? t('web.device_management.edit_network') :
t('web.device_management.create_network') }}</h2>
</div>
<div class="w-full flex gap-2 flex-wrap justify-start mb-3">
<Button @click="showConfigEditDialog = true" icon="pi pi-file-edit"
:label="t('web.device_management.edit_as_file')" iconPos="left" severity="secondary" />
<Button @click="importConfig" icon="pi pi-upload" :label="t('web.device_management.import_config')"
iconPos="left" severity="help" />
</div>
<Divider />
<Config :cur-network="newNetworkConfig" @run-network="createNewNetwork"></Config>
</div>
<!-- Network Status (for running networks) -->
<div v-else-if="needShowNetworkStatus" class="network-status-container">
<div class="network-status-header flex items-center gap-2 mb-3">
<i class="pi pi-chart-line text-primary text-xl"></i>
<h2 class="text-xl font-medium">{{ t('web.device_management.network_status') }}</h2>
</div>
<Status v-bind:cur-network-inst="curNetworkInfo" class="mb-4"></Status>
<div class="text-center mt-4">
<Button @click="updateNetworkState(true)" :label="t('web.device_management.disable_network')"
severity="warning" icon="pi pi-power-off" iconPos="left" />
</div>
</div>
<!-- Network Configuration (for disabled networks) -->
<div v-else-if="networkIsDisabled" class="network-config-container">
<div class="network-config-header flex items-center gap-2 mb-3">
<i class="pi pi-cog text-secondary text-xl"></i>
<h2 class="text-xl font-medium">{{ t('web.device_management.network_configuration') }}</h2>
</div>
<div v-if="disabledNetworkConfig" class="mb-4">
<Config :cur-network="disabledNetworkConfig" @run-network="updateNetworkState(false)" />
</div>
<div v-else class="network-loading-placeholder text-center py-8">
<i class="pi pi-spin pi-spinner text-3xl text-primary mb-3"></i>
<div class="text-xl text-secondary">{{ t('web.device_management.loading_network_configuration') }}
</div>
</div>
</div>
<!-- Empty State -->
<div v-else class="empty-state flex flex-col items-center py-12">
<i class="pi pi-sitemap text-5xl text-secondary mb-4 opacity-50"></i>
<div class="text-xl text-center font-medium mb-3">{{ t('web.device_management.no_network_selected') }}
</div>
<p class="text-secondary text-center mb-6 max-w-md">
{{ t('web.device_management.select_existing_network_or_create_new') }}
</p>
<Button @click="newNetwork" :label="t('web.device_management.create_network')" icon="pi pi-plus"
iconPos="left" />
</div>
</div>
<!-- Keep only the config edit dialogs -->
<ConfigEditDialog v-if="networkIsDisabled" v-model:visible="showCreateNetworkDialog"
:cur-network="disabledNetworkConfig" :generate-config="generateConfig" :save-config="saveConfig" />
<ConfigEditDialog v-else v-model:visible="showConfigEditDialog" :cur-network="newNetworkConfig"
:generate-config="generateConfig" :save-config="saveConfig" />
</div>
<RemoteManagement :api="remoteClient" v-model:instance-id="selectedInstanceId"
:new-config-generator="newConfigGenerator" />
</template>
<style scoped>

View File

@@ -3,9 +3,10 @@ import { computed, onMounted, ref } from 'vue';
import { Card, InputText, Password, Button, AutoComplete } from 'primevue';
import { useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast';
import { Api, I18nUtils } from 'easytier-frontend-lib';
import { I18nUtils } from 'easytier-frontend-lib';
import { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost } from "../modules/api-host"
import { useI18n } from 'vue-i18n'
import ApiClient, { Credential, RegisterData } from '../modules/api';
const { t } = useI18n()
@@ -13,7 +14,7 @@ defineProps<{
isRegistering: boolean;
}>();
const api = computed<Api.ApiClient>(() => new Api.ApiClient(apiHost.value));
const api = computed<ApiClient>(() => new ApiClient(apiHost.value));
const router = useRouter();
const toast = useToast();
@@ -28,7 +29,7 @@ const captchaSrc = computed(() => api.value.captcha_url());
const onSubmit = async () => {
// Add your login logic here
saveApiHost(apiHost.value);
const credential: Api.Credential = { username: username.value, password: password.value, };
const credential: Credential = { username: username.value, password: password.value, };
let ret = await api.value?.login(credential);
if (ret.success) {
localStorage.setItem('apiHost', btoa(apiHost.value));
@@ -43,8 +44,8 @@ const onSubmit = async () => {
const onRegister = async () => {
saveApiHost(apiHost.value);
const credential: Api.Credential = { username: registerUsername.value, password: registerPassword.value };
const registerReq: Api.RegisterData = { credentials: credential, captcha: captcha.value };
const credential: Credential = { username: registerUsername.value, password: registerPassword.value };
const registerReq: RegisterData = { credentials: credential, captcha: captcha.value };
let ret = await api.value?.register(registerReq);
if (ret.success) {
toast.add({ severity: 'success', summary: 'Register Success', detail: ret.message, life: 2000 });
@@ -108,12 +109,12 @@ onMounted(() => {
<form v-else @submit.prevent="onRegister" class="space-y-4">
<div class="p-field">
<label for="register-username" class="block text-sm font-medium">{{ t('web.login.username')
}}</label>
}}</label>
<InputText id="register-username" v-model="registerUsername" required class="w-full" />
</div>
<div class="p-field">
<label for="register-password" class="block text-sm font-medium">{{ t('web.login.password')
}}</label>
}}</label>
<Password id="register-password" v-model="registerPassword" required toggleMask
:feedback="false" class="w-full" />
</div>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Api, I18nUtils } from 'easytier-frontend-lib'
import { I18nUtils } from 'easytier-frontend-lib'
import { computed, onMounted, ref, onUnmounted, nextTick } from 'vue';
import { Button, TieredMenu } from 'primevue';
import { useRoute, useRouter } from 'vue-router';
@@ -7,13 +7,14 @@ import { useDialog } from 'primevue/usedialog';
import ChangePassword from './ChangePassword.vue';
import Icon from '../assets/easytier.png'
import { useI18n } from 'vue-i18n'
import ApiClient from '../modules/api';
const { t } = useI18n()
const route = useRoute();
const router = useRouter();
const api = computed<Api.ApiClient | undefined>(() => {
const api = computed<ApiClient | undefined>(() => {
try {
return new Api.ApiClient(atob(route.params.apiHost as string), () => {
return new ApiClient(atob(route.params.apiHost as string), () => {
router.push({ name: 'login' });
})
} catch (e) {

View File

@@ -0,0 +1,246 @@
import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { Md5 } from 'ts-md5'
import { Api, Utils } from 'easytier-frontend-lib';
import { NetworkTypes } from 'easytier-frontend-lib';
export interface ValidateConfigResponse {
toml_config: string;
}
// 定义接口返回的数据结构
export interface LoginResponse {
success: boolean;
message: string;
}
export interface RegisterResponse {
success: boolean;
message: string;
}
// 定义请求体数据结构
export interface Credential {
username: string;
password: string;
}
export interface RegisterData {
credentials: Credential;
captcha: string;
}
export interface Summary {
device_count: number;
}
export interface ListNetworkInstanceIdResponse {
running_inst_ids: Array<Utils.UUID>,
disabled_inst_ids: Array<Utils.UUID>,
}
export interface GenerateConfigRequest {
config: NetworkTypes.NetworkConfig;
}
export interface GenerateConfigResponse {
toml_config?: string;
error?: string;
}
export interface ParseConfigRequest {
toml_config: string;
}
export interface ParseConfigResponse {
config?: NetworkTypes.NetworkConfig;
error?: string;
}
export class ApiClient {
private client: AxiosInstance;
private authFailedCb: Function | undefined;
constructor(baseUrl: string, authFailedCb: Function | undefined = undefined) {
this.client = axios.create({
baseURL: baseUrl + '/api/v1',
withCredentials: true, // 如果需要支持跨域携带cookie
headers: {
'Content-Type': 'application/json',
},
});
this.authFailedCb = authFailedCb;
// 添加请求拦截器
this.client.interceptors.request.use((config: InternalAxiosRequestConfig) => {
return config;
}, (error: any) => {
return Promise.reject(error);
});
// 添加响应拦截器
this.client.interceptors.response.use((response: AxiosResponse) => {
console.debug('Axios Response:', response);
return response.data; // 假设服务器返回的数据都在data属性中
}, (error: any) => {
if (error.response) {
let response: AxiosResponse = error.response;
if (response.status == 401 && this.authFailedCb) {
console.error('Unauthorized:', response.data);
this.authFailedCb();
} else {
// 请求已发出但是服务器响应的状态码不在2xx范围
console.error('Response Error:', error.response.data);
}
} else if (error.request) {
// 请求已发出,但是没有收到响应
console.error('Request Error:', error.request);
} else {
// 发生了一些问题导致请求未发出
console.error('Error:', error.message);
}
return Promise.reject(error);
});
}
// 注册
public async register(data: RegisterData): Promise<RegisterResponse> {
try {
data.credentials.password = Md5.hashStr(data.credentials.password);
const response = await this.client.post<RegisterResponse>('/auth/register', data);
console.log("register response:", response);
return { success: true, message: 'Register success', };
} catch (error) {
if (error instanceof AxiosError) {
return { success: false, message: 'Failed to register, error: ' + JSON.stringify(error.response?.data), };
}
return { success: false, message: 'Unknown error, error: ' + error, };
}
}
// 登录
public async login(data: Credential): Promise<LoginResponse> {
try {
data.password = Md5.hashStr(data.password);
const response = await this.client.post<any>('/auth/login', data);
console.log("login response:", response);
return { success: true, message: 'Login success', };
} catch (error) {
if (error instanceof AxiosError) {
if (error.response?.status === 401) {
return { success: false, message: 'Invalid username or password', };
} else {
return { success: false, message: 'Unknown error, status code: ' + error.response?.status, };
}
}
return { success: false, message: 'Unknown error, error: ' + error, };
}
}
public async logout() {
await this.client.get('/auth/logout');
if (this.authFailedCb) {
this.authFailedCb();
}
}
public async change_password(new_password: string) {
await this.client.put('/auth/password', { new_password: Md5.hashStr(new_password) });
}
public async check_login_status() {
try {
await this.client.get('/auth/check_login_status');
return true;
} catch (error) {
return false;
}
}
public async list_session() {
const response = await this.client.get('/sessions');
return response;
}
public async list_machines(): Promise<Array<any>> {
const response = await this.client.get<any, Record<string, Array<any>>>('/machines');
return response.machines;
}
public async get_summary(): Promise<Summary> {
const response = await this.client.get<any, Summary>('/summary');
return response;
}
public captcha_url() {
return this.client.defaults.baseURL + '/auth/captcha';
}
public get_remote_client(machine_id: string): Api.RemoteClient {
return new WebRemoteClient(machine_id, this.client);
}
}
class WebRemoteClient implements Api.RemoteClient {
private machine_id: string;
private client: AxiosInstance;
constructor(machine_id: string, client: AxiosInstance) {
this.machine_id = machine_id;
this.client = client;
}
async validate_config(config: any): Promise<Api.ValidateConfigResponse> {
const response = await this.client.post<any, ValidateConfigResponse>(`/machines/${this.machine_id}/validate-config`, {
config: config,
});
return response;
}
async run_network(config: any): Promise<undefined> {
await this.client.post<string>(`/machines/${this.machine_id}/networks`, {
config: config,
});
}
async get_network_info(inst_id: string): Promise<any> {
const response = await this.client.get<any, Record<string, any>>('/machines/' + this.machine_id + '/networks/info/' + inst_id);
return response.info.map;
}
async list_network_instance_ids(): Promise<Api.ListNetworkInstanceIdResponse> {
const response = await this.client.get<any, ListNetworkInstanceIdResponse>('/machines/' + this.machine_id + '/networks');
return response;
}
async delete_network(inst_id: string): Promise<undefined> {
await this.client.delete<string>(`/machines/${this.machine_id}/networks/${inst_id}`);
}
async update_network_instance_state(inst_id: string, disabled: boolean): Promise<undefined> {
await this.client.put<string>('/machines/' + this.machine_id + '/networks/' + inst_id, {
disabled: disabled,
});
}
async get_network_config(inst_id: string): Promise<any> {
const response = await this.client.get<any, Record<string, any>>('/machines/' + this.machine_id + '/networks/config/' + inst_id);
return response;
}
async generate_config(config: NetworkTypes.NetworkConfig): Promise<Api.GenerateConfigResponse> {
try {
const response = await this.client.post<any, GenerateConfigResponse>('/generate-config', config);
return response;
} catch (error) {
if (error instanceof AxiosError) {
return { error: error.response?.data };
}
return { error: 'Unknown error: ' + error };
}
}
async parse_config(toml_config: string): Promise<Api.ParseConfigResponse> {
try {
const response = await this.client.post<any, ParseConfigResponse>('/parse-config', toml_config);
return response;
} catch (error) {
if (error instanceof AxiosError) {
return { error: error.response?.data };
}
return { error: 'Unknown error: ' + error };
}
}
}
export default ApiClient;