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

@@ -25,11 +25,9 @@
"floating-vue": "^5.2",
"ip-num": "1.5.1",
"primeicons": "^7.0.0",
"primevue": "^4.3.9",
"tailwindcss-primeui": "^0.3.4",
"ts-md5": "^1.3.1",
"uuid": "^11.0.2",
"vue": "^3.5.12",
"vue-chartjs": "^5.3.2",
"vue-i18n": "^10.0.4"
},
@@ -46,5 +44,9 @@
"vite": "^5.4.10",
"vite-plugin-dts": "^4.3.0",
"vue-tsc": "^2.1.10"
},
"peerDependencies": {
"vue": "^3.5.12",
"primevue": "^4.3.9"
}
}

View File

@@ -0,0 +1,595 @@
<script setup lang="ts">
import { Button, ConfirmPopup, Divider, IftaLabel, Menu, Select, useConfirm, useToast } from 'primevue';
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import * as Api from '../modules/api';
import { RemoteClient } from '../modules/api';
import * as Utils from '../modules/utils';
import * as NetworkTypes from '../types/network';
const { t } = useI18n()
const props = defineProps<{
api: RemoteClient;
newConfigGenerator?: () => NetworkTypes.NetworkConfig;
}>();
const instanceId = defineModel('instanceId', {
type: String as () => string | undefined,
required: false,
})
const emits = defineEmits(['update']);
const toast = useToast();
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 editingNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
const currentNetworkConfig = ref<NetworkTypes.NetworkConfig | undefined>(undefined);
const listInstanceIdResponse = ref<Api.ListNetworkInstanceIdResponse | undefined>(undefined);
const instanceIdList = computed(() => {
let insts = new Set<string>();
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);
},
set(value: any) {
console.log("set instanceId", value);
instanceId.value = value ? value.uuid : undefined;
}
});
watch(selectedInstanceId, async (newVal, oldVal) => {
if (newVal?.uuid !== oldVal?.uuid && networkIsDisabled.value) {
await loadCurrentNetworkConfig();
}
});
const needShowNetworkStatus = computed(() => {
if (!selectedInstanceId.value) {
// nothing selected
return false;
}
if (networkIsDisabled.value) {
// network is disabled
return false;
}
return true;
})
const networkIsDisabled = computed(() => {
if (!selectedInstanceId.value) {
return false;
}
return listInstanceIdResponse.value?.disabled_inst_ids.map(Utils.UuidToStr).includes(selectedInstanceId.value?.uuid);
});
watch(networkIsDisabled, async (newVal, oldVal) => {
if (newVal !== oldVal && newVal === true) {
await loadCurrentNetworkConfig();
}
});
const loadCurrentNetworkConfig = async () => {
currentNetworkConfig.value = undefined;
if (!selectedInstanceId.value) {
return;
}
let ret = await props.api.get_network_config(selectedInstanceId.value.uuid);
currentNetworkConfig.value = ret;
}
const updateNetworkState = async (disabled: boolean) => {
if (!selectedInstanceId.value) {
return;
}
if (disabled || !currentNetworkConfig.value) {
await props.api.update_network_instance_state(selectedInstanceId.value.uuid, disabled);
} else if (currentNetworkConfig.value) {
await props.api.delete_network(currentNetworkConfig.value.instance_id);
await props.api.run_network(currentNetworkConfig.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(instanceId.value!);
} catch (e) {
console.error(e);
}
emits('update');
},
reject: () => {
return;
}
});
};
const saveAndRunNewNetwork = async () => {
try {
if (isEditing.value) {
await props.api.delete_network(instanceId.value!);
}
let ret = await props.api.run_network(editingNetworkConfig.value);
console.debug("saveAndRunNewNetwork", ret);
selectedInstanceId.value = { uuid: editingNetworkConfig.value.instance_id };
} 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 = () => {
editingNetworkConfig.value = props.newConfigGenerator?.() ?? NetworkTypes.DEFAULT_NETWORK_CONFIG();
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 (!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(instanceId.value!);
console.debug("editNetwork", ret);
editingNetworkConfig.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 () => {
listInstanceIdResponse.value = await props.api.list_network_instance_ids();
console.debug("loadNetworkInstanceIds", listInstanceIdResponse.value);
}
const loadCurrentNetworkInfo = async () => {
if (!instanceId.value) {
return;
}
let ret = await props.api.get_network_info(instanceId.value);
let network_info = ret[instanceId.value];
curNetworkInfo.value = {
instance_id: instanceId.value,
running: network_info.running,
error_msg: network_info.error_msg,
detail: network_info,
} as NetworkTypes.NetworkInstance;
}
const exportConfig = async () => {
if (!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(instanceId.value!);
delete networkConfig.instance_id;
let { toml_config: tomlConfig, error } = await props.api.generate_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(tomlConfig);
if (resp.error) {
throw resp.error;
}
const config = resp.config;
if (!config) return;
config.instance_id = editingNetworkConfig.value?.instance_id ?? config?.instance_id;
Object.assign(editingNetworkConfig.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(tomlConfig);
if (resp.error) {
throw resp.error;
};
const config = resp.config;
if (!config) {
throw new Error("Parsed config is empty");
}
config.instance_id = currentNetworkConfig.value?.instance_id ?? config?.instance_id;
if (networkIsDisabled.value) {
currentNetworkConfig.value = config;
} else {
editingNetworkConfig.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(), loadCurrentNetworkInfo()]);
} 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="editingNetworkConfig" @run-network="saveAndRunNewNetwork"></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="currentNetworkConfig" class="mb-4">
<Config :cur-network="currentNetworkConfig" @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="currentNetworkConfig" :generate-config="generateConfig" :save-config="saveConfig" /> -->
<ConfigEditDialog v-model:visible="showConfigEditDialog" :cur-network="editingNetworkConfig"
:generate-config="generateConfig" :save-config="saveConfig" />
</div>
</template>
<style scoped>
.device-management {
height: 100%;
display: flex;
flex-direction: column;
}
.network-content {
flex: 1;
overflow-y: auto;
}
/* 按钮样式 */
.button-container {
gap: 0.5rem;
}
.create-button {
font-weight: 600;
min-width: 3rem;
}
/* 菜单样式定制 */
:deep(.p-menu) {
min-width: 12rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
padding: 0.25rem;
}
:deep(.p-menu .p-menuitem) {
border-radius: 0.25rem;
}
:deep(.p-menu .p-menuitem-link) {
padding: 0.65rem 1rem;
font-size: 0.9rem;
}
:deep(.p-menu .p-menuitem-icon) {
margin-right: 0.75rem;
}
:deep(.p-menu .p-menuitem.p-error .p-menuitem-text,
.p-menu .p-menuitem.p-error .p-menuitem-icon) {
color: var(--red-500);
}
:deep(.p-menu .p-menuitem:hover.p-error .p-menuitem-link) {
background-color: var(--red-50);
}
/* 按钮图标样式 */
:deep(.p-button-icon-only) {
width: 2.5rem !important;
padding: 0.5rem !important;
}
:deep(.p-button-icon-only .p-button-icon) {
font-size: 1rem;
}
/* 网络选择相关样式 */
.network-label {
white-space: nowrap;
}
:deep(.network-select-container) {
max-width: 100%;
}
/* Dark mode adaptations */
:deep(.bg-surface-50) {
background-color: var(--surface-50, #f8fafc);
}
:deep(.bg-surface-0) {
background-color: var(--surface-card, #ffffff);
}
:deep(.text-primary) {
color: var(--primary-color, #3b82f6);
}
:deep(.text-secondary) {
color: var(--text-color-secondary, #64748b);
}
@media (prefers-color-scheme: dark) {
:deep(.bg-surface-50) {
background-color: var(--surface-ground, #0f172a);
}
:deep(.bg-surface-0) {
background-color: var(--surface-card, #1e293b);
}
}
/* Responsive design for mobile devices */
@media (max-width: 768px) {
.network-header {
padding: 0.75rem;
}
.network-content {
padding: 0.75rem;
}
/* 在小屏幕上缩短网络标签文本 */
.network-label {
font-size: 0.9rem;
}
}
</style>

View File

@@ -1,3 +1,4 @@
export { default as Config } from './Config.vue';
export { default as Status } from './Status.vue';
export { default as ConfigEditDialog } from './ConfigEditDialog.vue';
export { default as RemoteManagement } from './RemoteManagement.vue';

View File

@@ -1,7 +1,7 @@
import './style.css'
import type { App } from 'vue';
import { Config, Status, ConfigEditDialog } from "./components";
import { Config, Status, ConfigEditDialog, RemoteManagement } from "./components";
import Aura from '@primeuix/themes/aura';
import PrimeVue from 'primevue/config'
@@ -44,8 +44,9 @@ export default {
app.component('ConfigEditDialog', ConfigEditDialog);
app.component('Status', Status);
app.component('HumanEvent', HumanEvent);
app.component('RemoteManagement', RemoteManagement);
app.directive('tooltip', vTooltip as any);
}
};
export { Config, ConfigEditDialog, Status, I18nUtils, NetworkTypes, Api, Utils };
export { Config, ConfigEditDialog, RemoteManagement, Status, I18nUtils, NetworkTypes, Api, Utils };

View File

@@ -1,5 +1,3 @@
import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { Md5 } from 'ts-md5'
import { UUID } from './utils';
import { NetworkConfig } from '../types/network';
@@ -7,235 +5,29 @@ 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<UUID>,
disabled_inst_ids: Array<UUID>,
}
export interface GenerateConfigRequest {
config: NetworkConfig;
}
export interface GenerateConfigResponse {
toml_config?: string;
error?: string;
}
export interface ParseConfigRequest {
toml_config: string;
}
export interface ParseConfigResponse {
config?: 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 list_deivce_instance_ids(machine_id: string): Promise<ListNetworkInstanceIdResponse> {
const response = await this.client.get<any, ListNetworkInstanceIdResponse>('/machines/' + machine_id + '/networks');
return response;
}
public async update_device_instance_state(machine_id: string, inst_id: string, disabled: boolean): Promise<undefined> {
await this.client.put<string>('/machines/' + machine_id + '/networks/' + inst_id, {
disabled: disabled,
});
}
public async get_network_info(machine_id: string, inst_id: string): Promise<any> {
const response = await this.client.get<any, Record<string, any>>('/machines/' + machine_id + '/networks/info/' + inst_id);
return response.info.map;
}
public async get_network_config(machine_id: string, inst_id: string): Promise<any> {
const response = await this.client.get<any, Record<string, any>>('/machines/' + machine_id + '/networks/config/' + inst_id);
return response;
}
public async validate_config(machine_id: string, config: any): Promise<ValidateConfigResponse> {
const response = await this.client.post<any, ValidateConfigResponse>(`/machines/${machine_id}/validate-config`, {
config: config,
});
return response;
}
public async run_network(machine_id: string, config: any): Promise<undefined> {
await this.client.post<string>(`/machines/${machine_id}/networks`, {
config: config,
});
}
public async delete_network(machine_id: string, inst_id: string): Promise<undefined> {
await this.client.delete<string>(`/machines/${machine_id}/networks/${inst_id}`);
}
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 async generate_config(config: GenerateConfigRequest): Promise<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 };
}
}
public async parse_config(config: ParseConfigRequest): Promise<ParseConfigResponse> {
try {
const response = await this.client.post<any, ParseConfigResponse>('/parse-config', config);
return response;
} catch (error) {
if (error instanceof AxiosError) {
return { error: error.response?.data };
}
return { error: 'Unknown error: ' + error };
}
}
}
export default ApiClient;
export interface RemoteClient {
validate_config(config: any): Promise<ValidateConfigResponse>;
run_network(config: any): Promise<undefined>;
get_network_info(inst_id: string): Promise<any>;
list_network_instance_ids(): Promise<ListNetworkInstanceIdResponse>;
delete_network(inst_id: string): Promise<undefined>;
update_network_instance_state(inst_id: string, disabled: boolean): Promise<undefined>;
get_network_config(inst_id: string): Promise<any>;
generate_config(config: NetworkConfig): Promise<GenerateConfigResponse>;
parse_config(toml_config: string): Promise<ParseConfigResponse>;
}

View File

@@ -24,12 +24,13 @@ export default defineConfig({
},
// make sure to externalize deps that shouldn't be bundled
// into your library
external: ['vue'],
external: ['vue', 'primevue'],
output: {
// Provide global variables to use in the UMD build
// for externalized deps
globals: {
vue: 'Vue',
primevue: 'primevue',
},
exports: "named"
},

View File

@@ -15,6 +15,7 @@
"easytier-frontend-lib": "workspace:*",
"primevue": "^4.3.9",
"tailwindcss-primeui": "^0.3.4",
"ts-md5": "^1.3.1",
"vue": "^3.5.12",
"vue-i18n": "^9.9.1",
"vue-router": "4"

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;

View File

@@ -7,13 +7,19 @@ use std::sync::{
};
use dashmap::DashMap;
use easytier::{proto::web::HeartbeatRequest, tunnel::TunnelListener};
use easytier::{
proto::{
api::manage::WebClientService, rpc_types::controller::BaseController, web::HeartbeatRequest,
},
rpc_service::remote_client::{self, RemoteClientManager},
tunnel::TunnelListener,
};
use maxminddb::geoip2;
use session::{Location, Session};
use storage::{Storage, StorageToken};
use tokio::task::JoinSet;
use crate::db::{Db, UserIdInDb};
use crate::db::{entity::user_running_network_configs, Db, UserIdInDb};
#[derive(rust_embed::Embed)]
#[folder = "resources/"]
@@ -152,7 +158,7 @@ impl ClientManager {
s.data().read().await.location().cloned()
}
pub fn db(&self) -> &Db {
fn db(&self) -> &Db {
self.storage.db()
}
@@ -245,6 +251,32 @@ impl ClientManager {
}
}
impl
RemoteClientManager<
(UserIdInDb, uuid::Uuid),
user_running_network_configs::Model,
sea_orm::DbErr,
> for ClientManager
{
fn get_rpc_client(
&self,
(user_id, machine_id): (UserIdInDb, uuid::Uuid),
) -> Option<Box<dyn WebClientService<Controller = BaseController> + Send>> {
let s = self.get_session_by_machine_id(user_id, &machine_id)?;
Some(s.scoped_rpc_client())
}
fn get_storage(
&self,
) -> &impl remote_client::Storage<
(UserIdInDb, uuid::Uuid),
user_running_network_configs::Model,
sea_orm::DbErr,
> {
self.storage.db()
}
}
#[cfg(test)]
mod tests {
use std::{sync::Arc, time::Duration};

View File

@@ -12,12 +12,11 @@ use easytier::{
rpc_types::{self, controller::BaseController},
web::{HeartbeatRequest, HeartbeatResponse, WebServerService, WebServerServiceServer},
},
rpc_service::remote_client::{ListNetworkProps, Storage as _},
tunnel::Tunnel,
};
use tokio::sync::{broadcast, RwLock};
use crate::db::ListNetworkProps;
use super::storage::{Storage, StorageToken, WeakRefStorage};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
@@ -224,10 +223,10 @@ impl Session {
}
let req = req.unwrap();
if req.machine_id.is_none() {
let Some(machine_id) = req.machine_id else {
tracing::warn!(?req, "Machine id is not set, ignore");
continue;
}
};
let running_inst_ids = req
.running_network_instances
@@ -257,11 +256,7 @@ impl Session {
let local_configs = match storage
.db
.list_network_configs(
user_id,
Some(req.machine_id.unwrap().into()),
ListNetworkProps::EnabledOnly,
)
.list_network_configs((user_id, machine_id.into()), ListNetworkProps::EnabledOnly)
.await
{
Ok(configs) => configs,

View File

@@ -1,5 +1,6 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
use easytier::rpc_service::remote_client::PersistentConfig;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
@@ -39,3 +40,12 @@ impl Related<super::users::Entity> for Entity {
}
impl ActiveModelBehavior for ActiveModel {}
impl PersistentConfig for Model {
fn get_network_inst_id(&self) -> &str {
&self.network_instance_id
}
fn get_network_config(&self) -> &str {
&self.network_config
}
}

View File

@@ -2,6 +2,7 @@
#[allow(unused_imports)]
pub mod entity;
use easytier::rpc_service::remote_client::{ListNetworkProps, Storage};
use entity::user_running_network_configs;
use sea_orm::{
prelude::Expr, sea_query::OnConflict, ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait,
@@ -9,17 +10,13 @@ use sea_orm::{
};
use sea_orm_migration::MigratorTrait as _;
use sqlx::{migrate::MigrateDatabase as _, types::chrono, Sqlite, SqlitePool};
use uuid::Uuid;
use crate::migrator;
use async_trait::async_trait;
pub type UserIdInDb = i32;
pub enum ListNetworkProps {
All,
EnabledOnly,
DisabledOnly,
}
#[derive(Debug, Clone)]
pub struct Db {
db_path: String,
@@ -68,12 +65,36 @@ impl Db {
&self.orm_db
}
pub async fn insert_or_update_user_network_config<T: ToString>(
pub async fn get_user_id<T: ToString>(
&self,
user_id: UserIdInDb,
device_id: uuid::Uuid,
network_inst_id: uuid::Uuid,
network_config: T,
user_name: T,
) -> Result<Option<UserIdInDb>, DbErr> {
use entity::users as u;
let user = u::Entity::find()
.filter(u::Column::Username.eq(user_name.to_string()))
.one(self.orm_db())
.await?;
Ok(user.map(|u| u.id))
}
// TODO: currently we don't have a token system, so we just use the user name as token
pub async fn get_user_id_by_token<T: ToString>(
&self,
token: T,
) -> Result<Option<UserIdInDb>, DbErr> {
self.get_user_id(token).await
}
}
#[async_trait]
impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for Db {
async fn insert_or_update_user_network_config(
&self,
(user_id, device_id): (UserIdInDb, Uuid),
network_inst_id: Uuid,
network_config: impl ToString + Send,
) -> Result<(), DbErr> {
let txn = self.orm_db().begin().await?;
@@ -105,10 +126,10 @@ impl Db {
txn.commit().await
}
pub async fn delete_network_config(
async fn delete_network_config(
&self,
user_id: UserIdInDb,
network_inst_id: uuid::Uuid,
(user_id, _): (UserIdInDb, Uuid),
network_inst_id: Uuid,
) -> Result<(), DbErr> {
use entity::user_running_network_configs as urnc;
@@ -121,12 +142,12 @@ impl Db {
Ok(())
}
pub async fn update_network_config_state(
async fn update_network_config_state(
&self,
user_id: UserIdInDb,
network_inst_id: uuid::Uuid,
(user_id, _): (UserIdInDb, Uuid),
network_inst_id: Uuid,
disabled: bool,
) -> Result<entity::user_running_network_configs::Model, DbErr> {
) -> Result<user_running_network_configs::Model, DbErr> {
use entity::user_running_network_configs as urnc;
urnc::Entity::update_many()
@@ -151,10 +172,9 @@ impl Db {
)))
}
pub async fn list_network_configs(
async fn list_network_configs(
&self,
user_id: UserIdInDb,
device_id: Option<uuid::Uuid>,
(user_id, device_id): (UserIdInDb, Uuid),
props: ListNetworkProps,
) -> Result<Vec<user_running_network_configs::Model>, DbErr> {
use entity::user_running_network_configs as urnc;
@@ -169,7 +189,7 @@ impl Db {
} else {
configs
};
let configs = if let Some(device_id) = device_id {
let configs = if !device_id.is_nil() {
configs.filter(urnc::Column::DeviceId.eq(device_id.to_string()))
} else {
configs
@@ -180,11 +200,10 @@ impl Db {
Ok(configs)
}
pub async fn get_network_config(
async fn get_network_config(
&self,
user_id: UserIdInDb,
device_id: &uuid::Uuid,
network_inst_id: &String,
(user_id, device_id): (UserIdInDb, Uuid),
network_inst_id: &str,
) -> Result<Option<user_running_network_configs::Model>, DbErr> {
use entity::user_running_network_configs as urnc;
@@ -197,32 +216,11 @@ impl Db {
Ok(config)
}
pub async fn get_user_id<T: ToString>(
&self,
user_name: T,
) -> Result<Option<UserIdInDb>, DbErr> {
use entity::users as u;
let user = u::Entity::find()
.filter(u::Column::Username.eq(user_name.to_string()))
.one(self.orm_db())
.await?;
Ok(user.map(|u| u.id))
}
// TODO: currently we don't have a token system, so we just use the user name as token
pub async fn get_user_id_by_token<T: ToString>(
&self,
token: T,
) -> Result<Option<UserIdInDb>, DbErr> {
self.get_user_id(token).await
}
}
#[cfg(test)]
mod tests {
use easytier::rpc_service::remote_client::Storage;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _};
use crate::db::{entity::user_running_network_configs, Db, ListNetworkProps};
@@ -235,7 +233,7 @@ mod tests {
let inst_id = uuid::Uuid::new_v4();
let device_id = uuid::Uuid::new_v4();
db.insert_or_update_user_network_config(user_id, device_id, inst_id, network_config)
db.insert_or_update_user_network_config((user_id, device_id), inst_id, network_config)
.await
.unwrap();
@@ -250,7 +248,7 @@ mod tests {
// overwrite the config
let network_config = "test_config2";
db.insert_or_update_user_network_config(user_id, device_id, inst_id, network_config)
db.insert_or_update_user_network_config((user_id, device_id), inst_id, network_config)
.await
.unwrap();
@@ -267,14 +265,16 @@ mod tests {
assert_ne!(result.update_time, result2.update_time);
assert_eq!(
db.list_network_configs(user_id, Some(device_id), ListNetworkProps::All)
db.list_network_configs((user_id, device_id), ListNetworkProps::All)
.await
.unwrap()
.len(),
1
);
db.delete_network_config(user_id, inst_id).await.unwrap();
db.delete_network_config((user_id, device_id), inst_id)
.await
.unwrap();
let result3 = user_running_network_configs::Entity::find()
.filter(user_running_network_configs::Column::UserId.eq(user_id))
.one(db.orm_db())

View File

@@ -41,8 +41,7 @@ pub struct RestfulServer {
// serve_task: Option<ScopedTask<()>>,
// delete_task: Option<ScopedTask<tower_sessions::session_store::Result<()>>>,
network_api: NetworkApi,
// network_api: NetworkApi<WebClientManager>,
web_router: Option<Router>,
}
@@ -108,7 +107,7 @@ impl RestfulServer {
) -> anyhow::Result<Self> {
assert!(client_mgr.is_running());
let network_api = NetworkApi::new();
// let network_api = NetworkApi::new();
Ok(RestfulServer {
bind_addr,
@@ -116,7 +115,7 @@ impl RestfulServer {
db,
// serve_task: None,
// delete_task: None,
network_api,
// network_api,
web_router,
})
}
@@ -188,6 +187,7 @@ impl RestfulServer {
}
}
#[allow(unused_mut)]
pub async fn start(
mut self,
) -> Result<
@@ -238,7 +238,7 @@ impl RestfulServer {
let app = Router::new()
.route("/api/v1/summary", get(Self::handle_get_summary))
.route("/api/v1/sessions", get(Self::handle_list_all_sessions))
.merge(self.network_api.build_route())
.merge(NetworkApi::build_route())
.route_layer(login_required!(Backend))
.merge(auth::router())
.with_state(self.client_mgr.clone())

View File

@@ -1,5 +1,3 @@
use std::sync::Arc;
use axum::extract::Path;
use axum::http::StatusCode;
use axum::routing::{delete, post};
@@ -7,12 +5,14 @@ use axum::{extract::State, routing::get, Json, Router};
use axum_login::AuthUser;
use easytier::launcher::NetworkConfig;
use easytier::proto::common::Void;
use easytier::proto::rpc_types::controller::BaseController;
use easytier::proto::{self, api::manage::*, web::*};
use easytier::proto::{api::manage::*, web::*};
use easytier::rpc_service::remote_client::{
ListNetworkInstanceIdsJsonResp, RemoteClientError, RemoteClientManager,
};
use sea_orm::DbErr;
use crate::client_manager::session::{Location, Session};
use crate::client_manager::ClientManager;
use crate::db::{ListNetworkProps, UserIdInDb};
use crate::client_manager::session::Location;
use crate::db::UserIdInDb;
use super::users::AuthSession;
use super::{
@@ -31,6 +31,21 @@ fn convert_rpc_error(e: RpcError) -> (StatusCode, Json<Error>) {
(status_code, Json(error))
}
fn convert_error(e: RemoteClientError<DbErr>) -> (StatusCode, Json<Error>) {
match e {
RemoteClientError::PersistentError(e) => convert_db_error(e),
RemoteClientError::RpcError(e) => convert_rpc_error(e),
RemoteClientError::ClientNotFound => (
StatusCode::NOT_FOUND,
other_error("Client not found").into(),
),
RemoteClientError::NotFound(msg) => (StatusCode::NOT_FOUND, other_error(msg).into()),
RemoteClientError::Other(msg) => {
(StatusCode::INTERNAL_SERVER_ERROR, other_error(msg).into())
}
}
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct ValidateConfigJsonReq {
config: NetworkConfig,
@@ -42,7 +57,7 @@ struct RunNetworkJsonReq {
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct ColletNetworkInfoJsonReq {
struct CollectNetworkInfoJsonReq {
inst_ids: Option<Vec<uuid::Uuid>>,
}
@@ -56,12 +71,6 @@ struct RemoveNetworkJsonReq {
inst_ids: Vec<uuid::Uuid>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct ListNetworkInstanceIdsJsonResp {
running_inst_ids: Vec<easytier::proto::common::Uuid>,
disabled_inst_ids: Vec<easytier::proto::common::Uuid>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct ListMachineItem {
client_url: Option<url::Url>,
@@ -74,13 +83,9 @@ struct ListMachineJsonResp {
machines: Vec<ListMachineItem>,
}
pub struct NetworkApi {}
pub struct NetworkApi;
impl NetworkApi {
pub fn new() -> Self {
Self {}
}
fn get_user_id(auth_session: &AuthSession) -> Result<UserIdInDb, (StatusCode, Json<Error>)> {
let Some(user_id) = auth_session.user.as_ref().map(|x| x.id()) else {
return Err((
@@ -91,63 +96,20 @@ impl NetworkApi {
Ok(user_id)
}
async fn get_session_by_machine_id(
auth_session: &AuthSession,
client_mgr: &ClientManager,
machine_id: &uuid::Uuid,
) -> Result<Arc<Session>, HttpHandleError> {
let user_id = Self::get_user_id(auth_session)?;
let Some(result) = client_mgr.get_session_by_machine_id(user_id, machine_id) else {
return Err((
StatusCode::NOT_FOUND,
other_error(format!("No such session: {}", machine_id)).into(),
));
};
let Some(token) = result.get_token().await else {
return Err((
StatusCode::UNAUTHORIZED,
other_error("No token reported".to_string()).into(),
));
};
if !auth_session
.user
.as_ref()
.map(|x| x.tokens.contains(&token.token))
.unwrap_or(false)
{
return Err((
StatusCode::FORBIDDEN,
other_error("Token mismatch".to_string()).into(),
));
}
Ok(result)
}
async fn handle_validate_config(
auth_session: AuthSession,
State(client_mgr): AppState,
Path(machine_id): Path<uuid::Uuid>,
Json(payload): Json<ValidateConfigJsonReq>,
) -> Result<Json<ValidateConfigResponse>, HttpHandleError> {
let config = payload.config;
let result =
Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?;
let c = result.scoped_rpc_client();
let ret = c
.validate_config(
BaseController::default(),
ValidateConfigRequest {
config: Some(config),
},
Ok(client_mgr
.handle_validate_config(
(Self::get_user_id(&auth_session)?, machine_id),
payload.config,
)
.await
.map_err(convert_rpc_error)?;
Ok(ret.into())
.map_err(convert_error)?
.into())
}
async fn handle_run_network_instance(
@@ -156,33 +118,13 @@ impl NetworkApi {
Path(machine_id): Path<uuid::Uuid>,
Json(payload): Json<RunNetworkJsonReq>,
) -> Result<Json<Void>, HttpHandleError> {
let config = payload.config;
let result =
Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?;
let c = result.scoped_rpc_client();
let resp = c
.run_network_instance(
BaseController::default(),
RunNetworkInstanceRequest {
inst_id: None,
config: Some(config.clone()),
},
)
.await
.map_err(convert_rpc_error)?;
client_mgr
.db()
.insert_or_update_user_network_config(
auth_session.user.as_ref().unwrap().id(),
machine_id,
resp.inst_id.unwrap_or_default().into(),
serde_json::to_string(&config).unwrap(),
.handle_run_network_instance(
(Self::get_user_id(&auth_session)?, machine_id),
payload.config,
)
.await
.map_err(convert_db_error)?;
.map_err(convert_error)?;
Ok(Void::default().into())
}
@@ -191,47 +133,30 @@ impl NetworkApi {
State(client_mgr): AppState,
Path((machine_id, inst_id)): Path<(uuid::Uuid, uuid::Uuid)>,
) -> Result<Json<CollectNetworkInfoResponse>, HttpHandleError> {
let result =
Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?;
let c = result.scoped_rpc_client();
let ret = c
.collect_network_info(
BaseController::default(),
CollectNetworkInfoRequest {
inst_ids: vec![inst_id.into()],
},
Ok(client_mgr
.handle_collect_network_info(
(Self::get_user_id(&auth_session)?, machine_id),
Some(vec![inst_id]),
)
.await
.map_err(convert_rpc_error)?;
Ok(ret.into())
.map_err(convert_error)?
.into())
}
async fn handle_collect_network_info(
auth_session: AuthSession,
State(client_mgr): AppState,
Path(machine_id): Path<uuid::Uuid>,
Json(payload): Json<ColletNetworkInfoJsonReq>,
Json(payload): Json<CollectNetworkInfoJsonReq>,
) -> Result<Json<CollectNetworkInfoResponse>, HttpHandleError> {
let result =
Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?;
let c = result.scoped_rpc_client();
let ret = c
.collect_network_info(
BaseController::default(),
CollectNetworkInfoRequest {
inst_ids: payload
.inst_ids
.unwrap_or_default()
.into_iter()
.map(Into::into)
.collect(),
},
Ok(client_mgr
.handle_collect_network_info(
(Self::get_user_id(&auth_session)?, machine_id),
payload.inst_ids,
)
.await
.map_err(convert_rpc_error)?;
Ok(ret.into())
.map_err(convert_error)?
.into())
}
async fn handle_list_network_instance_ids(
@@ -239,36 +164,11 @@ impl NetworkApi {
State(client_mgr): AppState,
Path(machine_id): Path<uuid::Uuid>,
) -> Result<Json<ListNetworkInstanceIdsJsonResp>, HttpHandleError> {
let result =
Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?;
let c = result.scoped_rpc_client();
let ret = c
.list_network_instance(BaseController::default(), ListNetworkInstanceRequest {})
Ok(client_mgr
.handle_list_network_instance_ids((Self::get_user_id(&auth_session)?, machine_id))
.await
.map_err(convert_rpc_error)?;
let running_inst_ids = ret.inst_ids.clone().into_iter().collect();
// collect networks that are disabled
let disabled_inst_ids = client_mgr
.db()
.list_network_configs(
auth_session.user.unwrap().id(),
Some(machine_id),
ListNetworkProps::DisabledOnly,
)
.await
.map_err(convert_db_error)?
.iter()
.map(|x| Into::<proto::common::Uuid>::into(x.network_instance_id.clone()))
.collect::<Vec<_>>();
Ok(ListNetworkInstanceIdsJsonResp {
running_inst_ids,
disabled_inst_ids,
}
.into())
.map_err(convert_error)?
.into())
}
async fn handle_remove_network_instance(
@@ -276,25 +176,13 @@ impl NetworkApi {
State(client_mgr): AppState,
Path((machine_id, inst_id)): Path<(uuid::Uuid, uuid::Uuid)>,
) -> Result<(), HttpHandleError> {
let result =
Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?;
client_mgr
.db()
.delete_network_config(auth_session.user.as_ref().unwrap().id(), inst_id)
.handle_remove_network_instance(
(Self::get_user_id(&auth_session)?, machine_id),
inst_id,
)
.await
.map_err(convert_db_error)?;
let c = result.scoped_rpc_client();
c.delete_network_instance(
BaseController::default(),
DeleteNetworkInstanceRequest {
inst_ids: vec![inst_id.into()],
},
)
.await
.map_err(convert_rpc_error)?;
Ok(())
.map_err(convert_error)
}
async fn handle_list_machines(
@@ -334,37 +222,14 @@ impl NetworkApi {
));
};
let sess = Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?;
let cfg = client_mgr
.db()
.update_network_config_state(auth_session.user.unwrap().id(), inst_id, payload.disabled)
.await
.map_err(convert_db_error)?;
let c = sess.scoped_rpc_client();
if payload.disabled {
c.delete_network_instance(
BaseController::default(),
DeleteNetworkInstanceRequest {
inst_ids: vec![inst_id.into()],
},
client_mgr
.handle_update_network_state(
(auth_session.user.unwrap().id(), machine_id),
inst_id,
payload.disabled,
)
.await
.map_err(convert_rpc_error)?;
} else {
c.run_network_instance(
BaseController::default(),
RunNetworkInstanceRequest {
inst_id: Some(inst_id.into()),
config: Some(serde_json::from_str(&cfg.network_config).unwrap()),
},
)
.await
.map_err(convert_rpc_error)?;
}
Ok(())
.map_err(convert_error)
}
async fn handle_get_network_config(
@@ -372,31 +237,14 @@ impl NetworkApi {
State(client_mgr): AppState,
Path((machine_id, inst_id)): Path<(uuid::Uuid, uuid::Uuid)>,
) -> Result<Json<NetworkConfig>, HttpHandleError> {
let inst_id = inst_id.to_string();
let db_row = client_mgr
.db()
.get_network_config(auth_session.user.unwrap().id(), &machine_id, &inst_id)
Ok(client_mgr
.handle_get_network_config((auth_session.user.unwrap().id(), machine_id), inst_id)
.await
.map_err(convert_db_error)?
.ok_or((
StatusCode::NOT_FOUND,
other_error(format!("No such network instance: {}", inst_id)).into(),
))?;
Ok(
serde_json::from_str::<NetworkConfig>(&db_row.network_config)
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
other_error(format!("Failed to parse network config: {:?}", e)).into(),
)
})?
.into(),
)
.map_err(convert_error)?
.into())
}
pub fn build_route(&mut self) -> Router<AppStateInner> {
pub fn build_route() -> Router<AppStateInner> {
Router::new()
.route("/api/v1/machines", get(Self::handle_list_machines))
.route(