initial commit
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<v-overlay
|
||||
:model-value="loading"
|
||||
persistent
|
||||
content-class="text-center"
|
||||
class="align-center justify-center"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
size="64"
|
||||
></v-progress-circular>
|
||||
<br />
|
||||
{{ $t('loading') }}
|
||||
</v-overlay>
|
||||
<Message />
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Message from '@/components/message.vue'
|
||||
import { inject, ref, Ref } from 'vue'
|
||||
|
||||
const loading:Ref = inject('loading')?? ref(false)
|
||||
|
||||
// Change page title
|
||||
document.title = "S-UI " + document.location.hostname
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.v-overlay .v-list-item,
|
||||
.v-field__input {
|
||||
direction: ltr;
|
||||
}
|
||||
</style>
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 763 B |
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg viewBox="1.019 0.0225 45.9789 46.9775" width="45.9789" height="46.9775" xmlns="http://www.w3.org/2000/svg">
|
||||
<g featurekey="symbolFeature-0" transform="matrix(0.4545450210571289, 0, 0, 0.4545450210571289, 0.7917079329490662, 1.7009549140930176)" fill="#737373">
|
||||
<g xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path d="M50,99.658L0.5,70.699V29.301L50,0.341l49.5,28.959v41.398L50,99.658z M2.5,69.553L50,97.342l47.5-27.789V30.448L50,2.659 L2.5,30.448V69.553z"/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon points="51,98.376 49,98.376 49,58.822 0.995,30.738 2.005,29.011 50,57.091 97.995,29.011 99.005,30.738 51,58.822 "/>
|
||||
</g>
|
||||
<g>
|
||||
<polyline points="28.494,14.082 76.994,42.457 71.506,45.667 23.006,17.292 "/>
|
||||
<polygon points="71.507,46.246 71.254,46.098 22.754,17.724 23.259,16.861 71.507,45.087 76.003,42.457 28.241,14.514 28.746,13.65 77.983,42.457 "/>
|
||||
</g>
|
||||
<g>
|
||||
<polyline points="71.506,45.667 71.506,57.982 71.51,57.982 76.993,54.775 76.993,42.457 "/>
|
||||
<polyline points="71.006,45.667 72.006,45.667 72.006,57.113 76.493,54.487 76.493,42.457 77.493,42.457 77.493,55.062 71.646,58.482 71,58.85 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g featurekey="nameFeature-0" transform="matrix(1.6160469055175781, 0, 0, 1.6160469055175781, 3.2854819297790527, -21.369783401489258)" fill="#a6a6a6">
|
||||
<path d="M10.904 40.4028 c-5.316 0 -9.2256 -3.048 -9.2256 -6.6192 c0 -1.8592 1.2372 -2.8152 2.5116 -2.8152 c1.0924 0 2.2288 0.7184 2.2288 2.1488 c0 1.4428 -1.4112 1.9972 -1.4112 2.9972 c0 1.782 2.974 3.0552 5.338 3.0552 c3.244 0 6.382 -1.5736 6.382 -5.102 c0 -6.2716 -14.403 -3.6536 -14.403 -13.229 c0 -5.0924 4.3436 -7.6012 9.9036 -7.6012 c4.7364 0 8.9656 2.6496 8.9656 6.1048 c0 1.946 -1.2372 2.9308 -2.4828 2.9308 c-1.1216 0 -2.258 -0.7476 -2.258 -2.178 c0 -1.5584 1.382 -1.8812 1.382 -2.8812 c0 -1.6952 -2.974 -2.7436 -5.3092 -2.7436 c-3.04 0 -5.9296 1.1848 -5.9296 4.6496 c0 5.866 15.193 3.7708 15.193 13.345 c0 4.0208 -3.8304 7.938 -10.886 7.938 z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<v-text-field
|
||||
id="expiry"
|
||||
:label="$t('date.expiry')"
|
||||
v-model="dateFormatted"
|
||||
prepend-inner-icon="mdi-calendar"
|
||||
readonly
|
||||
hide-details
|
||||
></v-text-field>
|
||||
<DatePicker
|
||||
v-model="Input"
|
||||
@input="Input=$event"
|
||||
:locale="$i18n.locale"
|
||||
element="expiry"
|
||||
compact-time
|
||||
type="datetime">
|
||||
<template v-slot:next-month>
|
||||
<v-icon icon="mdi-chevron-right" />
|
||||
</template>
|
||||
<template v-slot:prev-month>
|
||||
<v-icon icon="mdi-chevron-left" />
|
||||
</template>
|
||||
<template #submit-btn="{ submit, canSubmit }">
|
||||
<v-btn
|
||||
:disabled="!canSubmit"
|
||||
@click="submit"
|
||||
>{{ $t('submit') }}</v-btn>
|
||||
</template>
|
||||
<template #cancel-btn="{ vm }">
|
||||
<v-btn
|
||||
@click="reset(vm)"
|
||||
>{{ $t('reset') }}</v-btn>
|
||||
</template>
|
||||
<template #now-btn="{ goToday }">
|
||||
<v-btn
|
||||
@click="goToday"
|
||||
>{{ $t('now') }}</v-btn>
|
||||
</template>
|
||||
</DatePicker>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import DatePicker from 'vue3-persian-datetime-picker'
|
||||
import { i18n } from '@/locales'
|
||||
|
||||
export default {
|
||||
props: ['expiry'],
|
||||
emits: ['submit'],
|
||||
data() {
|
||||
return {
|
||||
menu: false,
|
||||
input: new Date(),
|
||||
}
|
||||
},
|
||||
components: { DatePicker },
|
||||
computed: {
|
||||
dateFormatted() {
|
||||
if (this.expDate == 0) return i18n.global.t('unlimited')
|
||||
const date = new Date(this.expDate*1000)
|
||||
return date.toLocaleString(i18n.global.locale.value)
|
||||
},
|
||||
expDate() {
|
||||
return parseInt(this.expiry?? 0)
|
||||
},
|
||||
Input: {
|
||||
get() { return this.expDate == 0 ? new Date() : new Date(this.expDate*1000) },
|
||||
set(v:string) {
|
||||
this.input = new Date(v)
|
||||
this.submit()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateInput(v:Date) {
|
||||
this.input = v
|
||||
},
|
||||
setNow() {
|
||||
this.input = new Date()
|
||||
},
|
||||
submit() {
|
||||
this.$emit('submit',Math.floor(this.input.getTime()/1000))
|
||||
},
|
||||
reset(vm:any) {
|
||||
this.$emit('submit',0)
|
||||
this.input = new Date()
|
||||
vm.visible = false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
menu(v) {
|
||||
if (v) {
|
||||
this.input = this.expiry == 0 ? new Date() : new Date(this.expDate*1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.vpd-addon-list,
|
||||
.vpd-addon-list-item {
|
||||
background-color: rgb(var(--v-theme-background));
|
||||
border-color: rgb(var(--v-theme-background));
|
||||
}
|
||||
.vpd-content {
|
||||
background-color: rgb(var(--v-theme-background));
|
||||
}
|
||||
.vpd-addon-list-item.vpd-selected,
|
||||
.vpd-addon-list-item:hover {
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
.vpd-close-addon {
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
background-color: transparent;
|
||||
}
|
||||
.vpd-controls {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.vpd-month-label {
|
||||
width: auto;
|
||||
}
|
||||
.vpd-actions button:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
.vpd-wrapper[data-type=datetime].vpd-compact-time .vpd-time {
|
||||
border-top: 0;
|
||||
}
|
||||
.vpd-time .vpd-time-h .vpd-counter-item,
|
||||
.vpd-time .vpd-time-m .vpd-counter-item {
|
||||
vertical-align: top;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<v-card subtitle="Dial" style="background-color: inherit;">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionDetour">
|
||||
<v-text-field
|
||||
label="Forward to Outbound tag"
|
||||
hide-details
|
||||
v-model="dial.detour"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionBind">
|
||||
<v-text-field
|
||||
label="Bind to Network Interface"
|
||||
hide-details
|
||||
v-model="dial.bind_interface"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionIPV4">
|
||||
<v-text-field
|
||||
label="Bind to IPv4"
|
||||
hide-details
|
||||
v-model="dial.inet4_bind_address"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionIPV6">
|
||||
<v-text-field
|
||||
label="Bind to IPv6"
|
||||
hide-details
|
||||
v-model="dial.inet6_bind_address"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionRM">
|
||||
<v-text-field
|
||||
label="Linux Routing Mark"
|
||||
hide-details
|
||||
type="number"
|
||||
min="0"
|
||||
v-model.number="routingMark"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionRA">
|
||||
<v-switch v-model="dial.reuse_addr" color="primary" label="Reuse listener address" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionTCP">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="dial.tcp_fast_open" color="primary" label="TCP Fast Open" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="dial.tcp_multi_path" color="primary" label="TCP Multi Path" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionUDP">
|
||||
<v-switch v-model="dial.udp_fragment" color="primary" label="UDP Fragment" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionCT">
|
||||
<v-text-field
|
||||
label="Connection Timeout"
|
||||
hide-details
|
||||
type="number"
|
||||
min="1"
|
||||
suffix="s"
|
||||
v-model.number="connectTimeout"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionDS">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
clearable
|
||||
@click:clear="delete dial.domain_strategy"
|
||||
width="100"
|
||||
label="Domain to IP Strategy"
|
||||
:items="['prefer_ipv4','prefer_ipv6','ipv4_only','ipv6_only']"
|
||||
v-model="dial.domain_strategy">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Fallback Timeout"
|
||||
hide-details
|
||||
type="number"
|
||||
min="50"
|
||||
step="50"
|
||||
suffix="ms"
|
||||
v-model.number="fallbackDelay"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions class="pt-0">
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details>Dial Options</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionDetour" color="primary" label="Detour" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionBind" color="primary" label="Bind Interface" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionIPV4" color="primary" label="Bind to IPv4" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionIPV6" color="primary" label="Bind to IPv6" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionRM" color="primary" label="Routing Mark" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionRA" color="primary" label="Reuse Address" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionTCP" color="primary" label="TCP Options" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionUDP" color="primary" label="UDP Options" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionCT" color="primary" label="Connection Timeout" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionDS" color="primary" label="Domain Strategy" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: ['dial'],
|
||||
data() {
|
||||
return {
|
||||
menu: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fallbackDelay: {
|
||||
get() { return this.$props.dial.fallback_delay ? parseInt(this.$props.dial.fallback_delay.replace('ms','')) : 300 },
|
||||
set(newValue:number) { this.$props.dial.fallback_delay = newValue > 0 ? newValue + 'ms' : '300ms' }
|
||||
},
|
||||
connectTimeout: {
|
||||
get() { return this.$props.dial.connect_timeout ? parseInt(this.$props.dial.connect_timeout.replace('s','')) : 5 },
|
||||
set(newValue:number) { this.$props.dial.connect_timeout = newValue > 0 ? newValue + 's' : '5s' }
|
||||
},
|
||||
routingMark: {
|
||||
get() { return this.$props.dial.routing_mark?? 0 },
|
||||
set(newValue:number) { this.$props.dial.routing_mark = newValue > 0 ? newValue : 0 }
|
||||
},
|
||||
optionDetour: {
|
||||
get(): boolean { return this.$props.dial.detour != undefined },
|
||||
set(v:boolean) { v ? this.$props.dial.detour = '' : delete this.$props.dial.detour }
|
||||
},
|
||||
optionBind: {
|
||||
get(): boolean { return this.$props.dial.bind_interface != undefined },
|
||||
set(v:boolean) { v ? this.$props.dial.bind_interface = '' : delete this.$props.dial.bind_interface }
|
||||
},
|
||||
optionIPV4: {
|
||||
get(): boolean { return this.$props.dial.inet4_bind_address != undefined },
|
||||
set(v:boolean) { v ? this.$props.dial.inet4_bind_address = '' : delete this.$props.dial.inet4_bind_address }
|
||||
},
|
||||
optionIPV6: {
|
||||
get(): boolean { return this.$props.dial.inet6_bind_address != undefined },
|
||||
set(v:boolean) { v ? this.$props.dial.inet6_bind_address = '' : delete this.$props.dial.inet6_bind_address }
|
||||
},
|
||||
optionRM: {
|
||||
get(): boolean { return this.$props.dial.routing_mark != undefined },
|
||||
set(v:boolean) { v ? this.$props.dial.routing_mark = 0 : delete this.$props.dial.routing_mark }
|
||||
},
|
||||
optionRA: {
|
||||
get(): boolean { return this.$props.dial.reuse_addr != undefined },
|
||||
set(v:boolean) { v ? this.$props.dial.reuse_addr = true : delete this.$props.dial.reuse_addr }
|
||||
},
|
||||
optionTCP: {
|
||||
get(): boolean {
|
||||
return this.$props.dial.tcp_fast_open != undefined &&
|
||||
this.$props.dial.tcp_multi_path != undefined
|
||||
},
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.$props.dial.tcp_fast_open = false
|
||||
this.$props.dial.tcp_multi_path = false
|
||||
} else {
|
||||
delete this.$props.dial.tcp_fast_open
|
||||
delete this.$props.dial.tcp_multi_path
|
||||
}
|
||||
}
|
||||
},
|
||||
optionUDP: {
|
||||
get(): boolean { return this.$props.dial.udp_fragment != undefined },
|
||||
set(v:boolean) { v ? this.$props.dial.udp_fragment = true : delete this.$props.dial.udp_fragment }
|
||||
},
|
||||
optionCT: {
|
||||
get(): boolean { return this.$props.dial.connect_timeout != undefined },
|
||||
set(v:boolean) { v ? this.$props.dial.connect_timeout = '5s' : delete this.$props.dial.connect_timeout }
|
||||
},
|
||||
optionDS: {
|
||||
get(): boolean { return this.$props.dial.domain_strategy != undefined },
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.$props.dial.domain_strategy = 'prefer_ipv4'
|
||||
this.$props.dial.fallback_delay = '300ms'
|
||||
} else {
|
||||
delete this.$props.dial.domain_strategy
|
||||
delete this.$props.dial.fallback_delay
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<v-card :subtitle="$t('in.multiplex')">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" label="Enable Multiplex" v-model="muxEnable" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="mux.enabled">
|
||||
<v-switch color="primary" label="Reject Non-Padded" v-model="mux.padding" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="mux.enabled">
|
||||
<v-switch color="primary" label="Enable Brutal" v-model="burtalEnable" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="mux.brutal?.enabled">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Uplink Bandwidth"
|
||||
hide-details
|
||||
type="number"
|
||||
suffix="Mbps"
|
||||
v-model.number="up_mbps">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Downlink Bandwidth"
|
||||
hide-details
|
||||
type="number"
|
||||
suffix="Mbps"
|
||||
min="0"
|
||||
v-model.number="down_mbps">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { iMultiplex } from '@/types/inMultiplex'
|
||||
export default {
|
||||
props: ['inbound'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
mux(): iMultiplex {
|
||||
return <iMultiplex> this.$props.inbound.multiplex
|
||||
},
|
||||
muxEnable: {
|
||||
get(): boolean { return this.$props.inbound.multiplex ? this.mux.enabled : false },
|
||||
set(newValue:boolean) { this.$props.inbound.multiplex = newValue ? { enabled: newValue } : {} }
|
||||
},
|
||||
burtalEnable: {
|
||||
get(): boolean { return this.mux.brutal ? this.mux.brutal.enabled : false },
|
||||
set(newValue:boolean) { this.mux.brutal = { enabled: newValue, up_mbps: 100, down_mbps: 100 } }
|
||||
},
|
||||
down_mbps: {
|
||||
get() { return this.mux.brutal && this.mux.brutal.down_mbps ? this.mux.brutal.down_mbps : 0 },
|
||||
set(newValue:any) {
|
||||
if (this.mux.brutal){
|
||||
this.mux.brutal.down_mbps = newValue.length != 0 ? newValue : 0
|
||||
}
|
||||
}
|
||||
},
|
||||
up_mbps: {
|
||||
get() { return this.mux.brutal && this.mux.brutal.up_mbps ? this.mux.brutal.up_mbps : 0 },
|
||||
set(newValue:any) {
|
||||
if (this.mux.brutal){
|
||||
this.mux.brutal.up_mbps = newValue.length != 0 ? newValue : 0
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<v-card :subtitle="$t('in.tls')">
|
||||
<v-row v-if="tlsOptional">
|
||||
<v-col cols="auto">
|
||||
<v-switch color="primary" :label="$t('tls.enable')" v-model="tlsEnable" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template v-if="tls.enabled">
|
||||
<v-row>
|
||||
<v-col cols="auto">
|
||||
<v-btn-toggle v-model="usePath"
|
||||
class="rounded-xl"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
shaped
|
||||
mandatory>
|
||||
<v-btn
|
||||
@click="tls.key=undefined; tls.certificate=undefined"
|
||||
>{{ $t('tls.usePath') }}</v-btn>
|
||||
<v-btn
|
||||
@click="tls.key_path=undefined; tls.certificate_path=undefined"
|
||||
>{{ $t('tls.useText') }}</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="usePath == 0">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('tls.certPath')"
|
||||
hide-details
|
||||
v-model="tls.certificate_path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('tls.keyPath')"
|
||||
hide-details
|
||||
v-model="tls.key_path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-textarea
|
||||
:label="$t('tls.cert')"
|
||||
hide-details
|
||||
v-model="certText">
|
||||
</v-textarea>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-textarea
|
||||
:label="$t('tls.key')"
|
||||
hide-details
|
||||
v-model="keyText">
|
||||
</v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="tls.server_name != undefined">
|
||||
<v-text-field
|
||||
label="SNI"
|
||||
hide-details
|
||||
v-model="tls.server_name">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="tls.alpn">
|
||||
<v-select
|
||||
hide-details
|
||||
label="ALPN"
|
||||
multiple
|
||||
:items="alpn"
|
||||
v-model="tls.alpn">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="tls.min_version">
|
||||
<v-select
|
||||
hide-details
|
||||
label="Minimum Version"
|
||||
:items="tlsVersions"
|
||||
v-model="tls.min_version">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="tls.max_version">
|
||||
<v-select
|
||||
hide-details
|
||||
label="Maximum Version"
|
||||
:items="tlsVersions"
|
||||
v-model="tls.max_version">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="8" v-if="tls.cipher_suites != undefined">
|
||||
<v-select
|
||||
hide-details
|
||||
label="Cipher Suites"
|
||||
multiple
|
||||
:items="cipher_suites"
|
||||
v-model="tls.cipher_suites">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start" v-if="tls.enabled">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details>TLS Options</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionSNI" color="primary" label="SNI" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionALPN" color="primary" label="ALPN" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionMinV" color="primary" label="Min Version" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionMaxV" color="primary" label="Max Version" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionCS" color="primary" label="Cipher Suites" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { iTls, defaultInTls } from '@/types/inTls'
|
||||
export default {
|
||||
props: ['inbound'],
|
||||
data() {
|
||||
return {
|
||||
menu: false,
|
||||
usePath: 0,
|
||||
defaults: defaultInTls,
|
||||
alpn: [
|
||||
{ title: "H3", value: 'HTTP/3' },
|
||||
{ title: "H2", value: 'HTTP/2' },
|
||||
{ title: "Http1.1", value: 'HTTP/1.1' },
|
||||
],
|
||||
tlsVersions: [ '1.0', '1.1', '1.2', '1.3' ],
|
||||
cipher_suites: [
|
||||
{ title: "Automatic", value: "" },
|
||||
{ title: "RSA-AES128-CBC-SHA", value: "TLS_RSA_WITH_AES_128_CBC_SHA" },
|
||||
{ title: "RSA-AES256-CBC-SHA", value: "TLS_RSA_WITH_AES_256_CBC_SHA" },
|
||||
{ title: "RSA-AES128-GCM-SHA256", value: "TLS_RSA_WITH_AES_128_GCM_SHA256" },
|
||||
{ title: "RSA-AES256-GCM-SHA384", value: "TLS_RSA_WITH_AES_256_GCM_SHA384" },
|
||||
{ title: "AES128-GCM-SHA256", value: "TLS_AES_128_GCM_SHA256" },
|
||||
{ title: "AES256-GCM-SHA384", value: "TLS_AES_256_GCM_SHA384" },
|
||||
{ title: "CHACHA20-POLY1305-SHA256", value: "TLS_CHACHA20_POLY1305_SHA256" },
|
||||
{ title: "ECDHE-ECDSA-AES128-CBC-SHA", value: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA" },
|
||||
{ title: "ECDHE-ECDSA-AES256-CBC-SHA", value: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA" },
|
||||
{ title: "ECDHE-RSA-AES128-CBC-SHA", value: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" },
|
||||
{ title: "ECDHE-RSA-AES256-CBC-SHA", value: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" },
|
||||
{ title: "ECDHE-ECDSA-AES128-GCM-SHA256", value: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" },
|
||||
{ title: "ECDHE-ECDSA-AES256-GCM-SHA384", value: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" },
|
||||
{ title: "ECDHE-RSA-AES128-GCM-SHA256", value: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" },
|
||||
{ title: "ECDHE-RSA-AES256-GCM-SHA384", value: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" },
|
||||
{ title: "ECDHE-ECDSA-CHACHA20-POLY1305-SHA256", value: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" },
|
||||
{ title: "ECDHE-RSA-CHACHA20-POLY1305-SHA256", value: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tls(): iTls {
|
||||
return <iTls> this.$props.inbound.tls
|
||||
},
|
||||
tlsEnable: {
|
||||
get() { return Object.hasOwn(this.$props.inbound.tls, 'enabled') ? this.tls.enabled : false },
|
||||
set(newValue: boolean) { this.$props.inbound.tls = newValue ? { enabled: true } : {} }
|
||||
},
|
||||
tlsOptional(): boolean {
|
||||
return !['hysteria','hysteria2','tuic','naive'].includes(this.$props.inbound.type)
|
||||
},
|
||||
certText: {
|
||||
get(): string { return this.tls.certificate ? this.tls.certificate.join('\n') : '' },
|
||||
set(newValue:string) { this.tls.certificate = newValue.split('\n') }
|
||||
},
|
||||
keyText: {
|
||||
get(): string { return this.tls.key ? this.tls.key.join('\n') : '' },
|
||||
set(newValue:string) { this.tls.key = newValue.split('\n') }
|
||||
},
|
||||
optionSNI: {
|
||||
get(): boolean { return this.tls.server_name != undefined },
|
||||
set(v:boolean) { this.$props.inbound.tls.server_name = v ? '' : undefined }
|
||||
},
|
||||
optionALPN: {
|
||||
get(): boolean { return this.tls.alpn != undefined },
|
||||
set(v:boolean) { this.$props.inbound.tls.alpn = v ? defaultInTls.alpn : undefined }
|
||||
},
|
||||
optionMinV: {
|
||||
get(): boolean { return this.tls.min_version != undefined },
|
||||
set(v:boolean) { this.$props.inbound.tls.min_version = v ? defaultInTls.min_version : undefined }
|
||||
},
|
||||
optionMaxV: {
|
||||
get(): boolean { return this.tls.max_version != undefined },
|
||||
set(v:boolean) { this.$props.inbound.tls.max_version = v ? defaultInTls.max_version : undefined }
|
||||
},
|
||||
optionCS: {
|
||||
get(): boolean { return this.tls.cipher_suites != undefined },
|
||||
set(v:boolean) { this.$props.inbound.tls.cipher_suites = v ? defaultInTls.cipher_suites : undefined }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<v-card subtitle="Listen">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('in.addr')"
|
||||
hide-details
|
||||
required
|
||||
v-model="inbound.listen">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('in.port')"
|
||||
hide-details
|
||||
type="number"
|
||||
required
|
||||
v-model.number="inbound.listen_port"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionDetour">
|
||||
<v-text-field
|
||||
label="Forward to Inbound tag"
|
||||
hide-details
|
||||
v-model="inbound.detour"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="inbound.sniff" color="primary" :label="$t('in.sniffing')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="inbound.sniff">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="inbound.sniff_override_destination" color="primary" label="Override Sniffed Domain" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Sniffing Timeout"
|
||||
hide-details
|
||||
type="number"
|
||||
min="50"
|
||||
step="50"
|
||||
suffix="ms"
|
||||
v-model.number="sniffTimeout"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionTCP">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="inbound.tcp_fast_open" color="primary" label="TCP Fast Open" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="inbound.tcp_multi_path" color="primary" label="TCP Multi Path" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionUDP">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="inbound.udp_fragment" color="primary" label="UDP Fragment" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="UDP NAT expiration"
|
||||
hide-details
|
||||
type="number"
|
||||
min="1"
|
||||
suffix="Min"
|
||||
v-model.number="udpTimeout"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionDS">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
width="100"
|
||||
label="Domain to IP Strategy"
|
||||
:items="['prefer_ipv4','prefer_ipv6','ipv4_only','ipv6_only']"
|
||||
v-model="inbound.domain_strategy">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions class="pt-0">
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details>Listen Options</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionTCP" color="primary" label="TCP Options" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionUDP" color="primary" label="UDP Options" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionDetour" color="primary" label="Detour" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionDS" color="primary" label="Domain Strategy" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: ['inbound'],
|
||||
data() {
|
||||
return {
|
||||
menu: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
udpTimeout: {
|
||||
get() { return this.$props.inbound.udp_timeout ? parseInt(this.$props.inbound.udp_timeout.replace('m','')) : 5 },
|
||||
set(newValue:number) { this.$props.inbound.udp_timeout = newValue > 0 ? newValue + 'm' : '5m' }
|
||||
},
|
||||
sniffTimeout: {
|
||||
get() { return this.$props.inbound.sniff_timeout ? parseInt(this.$props.inbound.sniff_timeout.replace('ms','')) : 300 },
|
||||
set(newValue:number) { this.$props.inbound.sniff_timeout = newValue > 0 ? newValue + 'ms' : '300ms' }
|
||||
},
|
||||
optionTCP: {
|
||||
get(): boolean {
|
||||
return this.$props.inbound.tcp_fast_open != undefined &&
|
||||
this.$props.inbound.tcp_multi_path != undefined
|
||||
},
|
||||
set(v:boolean) {
|
||||
this.$props.inbound.tcp_fast_open = v ? false : undefined
|
||||
this.$props.inbound.tcp_multi_path = v ? false : undefined
|
||||
}
|
||||
},
|
||||
optionUDP: {
|
||||
get(): boolean {
|
||||
return this.$props.inbound.udp_fragment != undefined &&
|
||||
this.$props.inbound.udp_timeout != undefined
|
||||
},
|
||||
set(v:boolean) {
|
||||
this.$props.inbound.udp_fragment = v ? false : undefined
|
||||
this.$props.inbound.udp_timeout = v ? false : undefined
|
||||
}
|
||||
},
|
||||
optionDetour: {
|
||||
get(): boolean { return this.$props.inbound.detour != undefined },
|
||||
set(v:boolean) { this.$props.inbound.detour = v ? '' : undefined }
|
||||
},
|
||||
optionDS: {
|
||||
get(): boolean { return this.$props.inbound.domain_strategy != undefined },
|
||||
set(v:boolean) { this.$props.inbound.domain_strategy = v ? 'prefer_ipv4' : undefined }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<v-container class="fill-height">
|
||||
<v-responsive :class="reloadItems.length>0 ? 'fill-height text-center' : 'align-center'" >
|
||||
<v-row class="d-flex align-center justify-center">
|
||||
<v-col cols="auto">
|
||||
<v-img src="@/assets/logo.svg" :width="reloadItems.length>0 ? 100 : 200"></v-img>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="d-flex align-center justify-center">
|
||||
<v-col cols="auto">
|
||||
<v-dialog v-model="menu" :close-on-content-click="false" transition="scale-transition" max-width="800">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" variant="tonal">{{ $t('main.tiles') }} <v-icon icon="mdi-star-plus" /></v-btn>
|
||||
</template>
|
||||
<v-card rounded="xl">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>
|
||||
{{ $t('main.tiles') }}
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto"><v-icon icon="mdi-close" @click="menu = false"></v-icon></v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-for="items in menuItems">
|
||||
<v-card variant="flat" :title="items.title">
|
||||
<v-list v-for="item in items.value">
|
||||
<v-list-item>
|
||||
<v-switch
|
||||
v-model="reloadItems"
|
||||
:value="item.value"
|
||||
color="primary"
|
||||
:label="item.title"
|
||||
hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3" v-for="i in reloadItems" :key="i">
|
||||
<v-card class="rounded-lg" variant="outlined" height="200px"
|
||||
:title="menuItems.flatMap(cat => cat.value).find(m => m.value == i)?.title">
|
||||
<v-card-text style="padding: 0 16px;">
|
||||
<Gauge :tilesData="tilesData" :type="i" v-if="i.charAt(0) == 'g'" />
|
||||
<History :tilesData="tilesData" :type="i" v-if="i.charAt(0) == 'h'" />
|
||||
<template v-if="i == 'i-sys'">
|
||||
<v-row>
|
||||
<v-col cols="3">{{ $t('main.info.host') }}</v-col>
|
||||
<v-col cols="9" style="text-wrap: nowrap; overflow: hidden">{{ tilesData.sys?.hostName }}</v-col>
|
||||
<v-col cols="3">{{ $t('main.info.cpu') }}</v-col>
|
||||
<v-col cols="9">
|
||||
<v-chip density="compact" variant="flat">
|
||||
<v-tooltip activator="parent" location="top" style="direction: ltr;">
|
||||
{{ tilesData.sys?.cpuType }}
|
||||
</v-tooltip>
|
||||
{{ tilesData.sys?.cpuCount }} {{ $t('main.info.core') }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col cols="3">IP</v-col>
|
||||
<v-col cols="9">
|
||||
<v-chip density="compact" color="primary" variant="flat" v-if="tilesData.sys?.ipv4?.length>0">
|
||||
<v-tooltip activator="parent" location="top" style="direction: ltr;">
|
||||
<span v-html="tilesData.sys?.ipv4?.join('<br />')"></span>
|
||||
</v-tooltip>
|
||||
IPv4
|
||||
</v-chip>
|
||||
<v-chip density="compact" color="primary" variant="flat" v-if="tilesData.sys?.ipv6?.length>0">
|
||||
<v-tooltip activator="parent" location="top" style="direction: ltr;">
|
||||
<span v-html="tilesData.sys?.ipv6?.join('<br />')"></span>
|
||||
</v-tooltip>
|
||||
IPv6
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col cols="3">S-UI</v-col>
|
||||
<v-col cols="9">
|
||||
<v-chip density="compact" color="primary" variant="flat">
|
||||
<v-tooltip activator="parent" location="top">
|
||||
{{ $t('main.info.threads') }}: {{ tilesData.sys?.appThreads }}<br />
|
||||
{{ $t('main.info.memory') }}: {{ HumanReadable.sizeFormat(tilesData.sys?.appMem) }}
|
||||
</v-tooltip>
|
||||
v{{ tilesData.sys?.appVersion }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col cols="3">{{ $t('main.info.uptime') }}</v-col>
|
||||
<v-col cols="9">{{ HumanReadable.formatSecond(tilesData.uptime) }}</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<template v-if="i == 'i-sbd'">
|
||||
<v-row>
|
||||
<v-col cols="4">{{ $t('main.info.running') }}</v-col>
|
||||
<v-col cols="8">
|
||||
<v-chip density="compact" color="success" variant="flat" v-if="tilesData.sbd?.running">{{ $t('yes') }}</v-chip>
|
||||
<v-chip density="compact" color="error" variant="flat" v-else>{{ $t('no') }}</v-chip>
|
||||
</v-col>
|
||||
<v-col cols="4">{{ $t('main.info.memory') }}</v-col>
|
||||
<v-col cols="8">
|
||||
<v-chip density="compact" color="primary" variant="flat" v-if="tilesData.sbd?.stats?.Alloc">
|
||||
{{ HumanReadable.sizeFormat(tilesData.sbd?.stats?.Alloc) }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col cols="4">{{ $t('main.info.threads') }}</v-col>
|
||||
<v-col cols="8">
|
||||
<v-chip density="compact" color="primary" variant="flat" v-if="tilesData.sbd?.stats?.NumGoroutine">
|
||||
{{ tilesData.sbd?.stats?.NumGoroutine }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col cols="4">{{ $t('main.info.uptime') }}</v-col>
|
||||
<v-col cols="8">{{ HumanReadable.formatSecond(tilesData.sbd?.stats?.Uptime) }}</v-col>
|
||||
<v-col cols="4">{{ $t('online') }}</v-col>
|
||||
<v-col cols="8">
|
||||
<template v-if="tilesData.sbd?.running">
|
||||
<v-chip density="compact" color="primary" variant="flat" v-if="Data().onlines.user">
|
||||
<v-tooltip activator="parent" location="top" :text="$t('pages.clients')" />
|
||||
{{ Data().onlines.user?.length }}
|
||||
</v-chip>
|
||||
<v-chip density="compact" color="success" variant="flat" v-if="Data().onlines.inbound">
|
||||
<v-tooltip activator="parent" location="top" :text="$t('pages.inbounds')" />
|
||||
{{ Data().onlines.inbound?.length }}
|
||||
</v-chip>
|
||||
<v-chip density="compact" color="info" variant="flat" v-if="Data().onlines.outbound">
|
||||
<v-tooltip activator="parent" location="top" :text="$t('pages.outbounds')" />
|
||||
{{ Data().onlines.outbound?.length }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-responsive>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
import { HumanReadable } from '@/plugins/utils'
|
||||
import Data from '@/store/modules/data'
|
||||
import Gauge from '@/components/tiles/Gauge.vue'
|
||||
import History from '@/components/tiles/History.vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { i18n } from '@/locales'
|
||||
|
||||
const menu = ref(false)
|
||||
const menuItems = [
|
||||
{ title: i18n.global.t('main.gauges'), value: [
|
||||
{ title: i18n.global.t('main.gauge.cpu'), value: "g-cpu" },
|
||||
{ title: i18n.global.t('main.gauge.mem'), value: "g-mem" },
|
||||
]
|
||||
},
|
||||
{ title: i18n.global.t('main.charts'), value: [
|
||||
{ title: i18n.global.t('main.chart.cpu'), value: "h-cpu" },
|
||||
{ title: i18n.global.t('main.chart.mem'), value: "h-mem" },
|
||||
{ title: i18n.global.t('main.chart.net'), value: "h-net" },
|
||||
{ title: i18n.global.t('main.chart.pnet'), value: "hp-net" },
|
||||
]
|
||||
},
|
||||
{ title: i18n.global.t('main.infos'), value: [
|
||||
{ title: i18n.global.t('main.info.sys'), value: "i-sys" },
|
||||
{ title: i18n.global.t('main.info.sbd'), value: "i-sbd" },
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
const tilesData = ref(<any>{})
|
||||
|
||||
const reloadItems = computed({
|
||||
get() { return Data().reloadItems },
|
||||
set(v:string[]) {
|
||||
if (Data().reloadItems.length == 0 && v.length>0) startTimer()
|
||||
if (Data().reloadItems.length > 0 && v.length == 0) stopTimer()
|
||||
Data().reloadItems = v
|
||||
v.length>0 ? localStorage.setItem("reloadItems",v.join(',')) : localStorage.removeItem("reloadItems")
|
||||
}
|
||||
})
|
||||
|
||||
const reloadData = async () => {
|
||||
const request = [...new Set(reloadItems.value.map(r => r.split('-')[1]))]
|
||||
const data = await HttpUtils.get('/api/status',{ r: request.join(',')})
|
||||
if (data.success) {
|
||||
tilesData.value = data.obj
|
||||
}
|
||||
}
|
||||
|
||||
let intervalId: NodeJS.Timeout | null = null
|
||||
|
||||
const startTimer = () => {
|
||||
intervalId = setInterval(() => {
|
||||
reloadData()
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const stopTimer = () => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (Data().reloadItems.length != 0) {
|
||||
reloadData()
|
||||
startTimer()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopTimer()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('network')"
|
||||
:items="networks"
|
||||
v-model="Network">
|
||||
</v-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: ['inbound'],
|
||||
data() {
|
||||
return {
|
||||
networks: [
|
||||
{ title: "TCP/UDP", value: '' },
|
||||
{ title: "TCP", value: 'tcp' },
|
||||
{ title: "UDP", value: 'udp' },
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
Network: {
|
||||
get():string { return this.$props.inbound.network?? '' },
|
||||
set(v:string) { this.$props.inbound.network = v != '' ? v : undefined }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<v-card :subtitle="$t('in.transport')">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('transport.enable')" v-model="tpEnable" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="tpEnable">
|
||||
<v-select
|
||||
hide-details
|
||||
width="100"
|
||||
:label="$t('type')"
|
||||
:items="Object.keys(trspTypes).map((key,index) => ({title: key, value: Object.values(trspTypes)[index]}))"
|
||||
v-model="transportType">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Http v-if="Transport.type == trspTypes.HTTP" :transport="Transport" />
|
||||
<WebSocket v-if="Transport.type == trspTypes.WebSocket" :transport="Transport" />
|
||||
<GRPC v-if="Transport.type == trspTypes.gRPC" :transport="Transport" />
|
||||
<HttpUpgrade v-if="Transport.type == trspTypes.HTTPUpgrade" :transport="Transport" />
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { TrspTypes, Transport } from '@/types/transport'
|
||||
import Http from './transports/Http.vue'
|
||||
import WebSocket from './transports/WebSocket.vue'
|
||||
import GRPC from './transports/gRPC.vue'
|
||||
import HttpUpgrade from './transports/HttpUpgrade.vue'
|
||||
export default {
|
||||
props: ['inbound'],
|
||||
data() {
|
||||
return {
|
||||
trspTypes: TrspTypes
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
Transport() {
|
||||
return <Transport>this.$props.inbound.transport
|
||||
},
|
||||
tpEnable: {
|
||||
get() { return Object.hasOwn(this.$props.inbound.transport, 'type') },
|
||||
set(newValue: boolean) { this.$props.inbound.transport = newValue ? { type: 'http' } : {} }
|
||||
},
|
||||
transportType: {
|
||||
get() { return this.Transport.type },
|
||||
set(newValue: string) { this.$props.inbound.transport = { type: newValue } }
|
||||
}
|
||||
},
|
||||
components: { Http, WebSocket, GRPC, HttpUpgrade }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<v-card subtitle="Clients">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch
|
||||
v-model="hasUser"
|
||||
@change="() => {inbound.users = hasUser? [] : undefined}"
|
||||
color="primary"
|
||||
:label="$t('in.clients')"
|
||||
hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
export default {
|
||||
props: ['inbound', 'id'],
|
||||
data() {
|
||||
return {
|
||||
hasUser: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
cardTitle() {
|
||||
this.hasUser = Object.hasOwn(this.$props.inbound,'users')
|
||||
return this.$props.inbound?.type.toUpperCase()
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.hasUser = Object.hasOwn(this.$props.inbound,'users')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<v-snackbar
|
||||
v-model="sb.showMsg"
|
||||
location="top"
|
||||
:color="snackbar.color"
|
||||
:timeout="snackbar.timeout">
|
||||
{{ snackbar.message }}
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import Message from '@/store/modules/message'
|
||||
|
||||
const sb = Message()
|
||||
|
||||
const snackbar = ref(sb.snackbar)
|
||||
</script>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<v-card subtitle="Direct">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<Network :inbound="inbound" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Override Address"
|
||||
hide-details
|
||||
v-model="inbound.override_address">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Override Port"
|
||||
type="number"
|
||||
min="0"
|
||||
hide-details
|
||||
v-model="override_port">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Network from '@/components/Network.vue'
|
||||
|
||||
export default {
|
||||
props: ['inbound'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
override_port: {
|
||||
get() { return this.$props.inbound.override_port ? this.$props.inbound.override_port : ''; },
|
||||
set(newValue: any) { this.$props.inbound.override_port = newValue.length == 0 || newValue == 0 ? undefined : parseInt(newValue); }
|
||||
},
|
||||
},
|
||||
components: { Network }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<v-card subtitle="Hysteria">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Uplink Limit"
|
||||
hide-details
|
||||
type="number"
|
||||
suffix="Mbps"
|
||||
v-model.number="up_mbps">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Downlink Limit"
|
||||
hide-details
|
||||
type="number"
|
||||
suffix="Mbps"
|
||||
min="0"
|
||||
v-model.number="down_mbps">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="obfs Password"
|
||||
hide-details
|
||||
v-model="inbound.obfs">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
export default {
|
||||
props: ['inbound'],
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
down_mbps: {
|
||||
get() { return this.$props.inbound.down_mbps ? this.$props.inbound.down_mbps : 0 },
|
||||
set(newValue:any) {
|
||||
if (newValue.length != 0 ){
|
||||
this.$props.inbound.down_mbps = newValue
|
||||
this.$props.inbound.down = "" + newValue + " Mbps"
|
||||
} else {
|
||||
this.$props.inbound.down_mbps = 0
|
||||
this.$props.inbound.down = "0 Mbps"
|
||||
}
|
||||
}
|
||||
},
|
||||
up_mbps: {
|
||||
get() { return this.$props.inbound.up_mbps ? this.$props.inbound.up_mbps : 0 },
|
||||
set(newValue:number) { this.$props.inbound.up_mbps = newValue > 0 ? newValue : 0 }
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<v-card subtitle="Hysteria2">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Masquerade"
|
||||
hide-details
|
||||
v-model="hysteria2.masquerade"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="hysteria2.ignore_client_bandwidth" color="primary" label="Ignore Client Bandwidth" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="!hysteria2.ignore_client_bandwidth">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Uplink Limit"
|
||||
hide-details
|
||||
type="number"
|
||||
suffix="Mbps"
|
||||
v-model.number="up_mbps">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Downlink Limit"
|
||||
hide-details
|
||||
type="number"
|
||||
suffix="Mbps"
|
||||
min="0"
|
||||
v-model.number="down_mbps">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="hysteria2.obfs">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="obfs Password"
|
||||
hide-details
|
||||
v-model="hysteria2.obfs.password">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details>Options</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionObfs" color="primary" label="Obfs" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Hysteria2, createInbound } from '@/types/inbounds'
|
||||
|
||||
export default {
|
||||
props: ['inbound'],
|
||||
data() {
|
||||
return {
|
||||
menu: false,
|
||||
hysteria2: <Hysteria2> createInbound("hysteria2",{ "tag": "" }),
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
down_mbps: {
|
||||
get() { return this.hysteria2.down_mbps ? this.hysteria2.down_mbps : 0 },
|
||||
set(newValue:any) { this.hysteria2.down_mbps = newValue.length == 0 ? undefined : this.hysteria2.down_mbps }
|
||||
},
|
||||
up_mbps: {
|
||||
get() { return this.hysteria2.up_mbps ? this.hysteria2.up_mbps : 0 },
|
||||
set(newValue:any) { this.hysteria2.up_mbps = newValue.length == 0 ? undefined : this.hysteria2.up_mbps }
|
||||
},
|
||||
optionObfs: {
|
||||
get(): boolean { return this.hysteria2.obfs != undefined },
|
||||
set(v:boolean) { this.$props.inbound.obfs = v ? { type: "salamander", password: ""} : undefined }
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.hysteria2 = <Hysteria2> this.$props.inbound
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<v-card subtitle="Naive">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<Network :inbound="inbound" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Network from '@/components/Network.vue'
|
||||
|
||||
export default {
|
||||
props: ['inbound'],
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
components: { Network }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<v-card subtitle="ShadowTls">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:items="[1,2,3]"
|
||||
label="Version"
|
||||
v-model="version">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="inbound.password != undefined">
|
||||
<v-text-field
|
||||
label="Password"
|
||||
hide-details
|
||||
v-model="inbound.password">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Handshake Server"
|
||||
hide-details
|
||||
v-model="Inbound.handshake.server">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Server Port"
|
||||
type="number"
|
||||
min="0"
|
||||
hide-details
|
||||
v-model="server_port">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Dial :dial="Inbound.handshake" />
|
||||
<v-row v-if="Inbound.handshake_for_server_name != undefined">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Add Hanshake Server"
|
||||
hide-details
|
||||
append-icon="mdi-plus"
|
||||
@click:append="addHandshakeServer()"
|
||||
v-model="handshake_server">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card
|
||||
v-for="(value, key) in Inbound.handshake_for_server_name"
|
||||
border
|
||||
density="compact"
|
||||
style="margin: 5px;"
|
||||
color="background">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>{{ key }}</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col>
|
||||
<v-btn @click="Inbound.handshake_for_server_name ? delete Inbound.handshake_for_server_name[key] : null"
|
||||
icon="mdi-delete"
|
||||
></v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Handshake Server"
|
||||
hide-details
|
||||
v-model="value.server">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Server Port"
|
||||
type="number"
|
||||
min="0"
|
||||
hide-details
|
||||
v-model="value.server_port">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Dial :dial="value" />
|
||||
</v-card>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ShadowTLS } from '@/types/inbounds'
|
||||
import Dial from '../Dial.vue'
|
||||
|
||||
export default {
|
||||
props: ['inbound'],
|
||||
data() {
|
||||
return {
|
||||
handshake_server: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addHandshakeServer() {
|
||||
this.inbound.handshake_for_server_name[this.handshake_server] = {}
|
||||
// Clear the input field after adding the server
|
||||
this.handshake_server = ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.version = this.Inbound.version
|
||||
},
|
||||
computed: {
|
||||
version: {
|
||||
get() { this.version = this.Inbound.version; return this.Inbound.version; },
|
||||
set(newValue: any) {
|
||||
switch (newValue) {
|
||||
case 1:
|
||||
this.Inbound.password = undefined
|
||||
this.Inbound.users = undefined
|
||||
this.Inbound.handshake_for_server_name = undefined
|
||||
break;
|
||||
case 2:
|
||||
if (!this.Inbound.password) {
|
||||
this.Inbound.password = ""
|
||||
}
|
||||
this.Inbound.users = undefined
|
||||
if (!this.Inbound.handshake_for_server_name) {
|
||||
this.Inbound.handshake_for_server_name = {}
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
this.Inbound.password = undefined
|
||||
if (Object.hasOwn(this.Inbound, 'users')) {
|
||||
this.Inbound.users = []
|
||||
}
|
||||
if (!this.Inbound.handshake_for_server_name) {
|
||||
this.Inbound.handshake_for_server_name = {}
|
||||
}
|
||||
break;
|
||||
}
|
||||
this.Inbound.version = newValue;
|
||||
}
|
||||
},
|
||||
Inbound(): ShadowTLS {
|
||||
return <ShadowTLS>this.$props.inbound;
|
||||
},
|
||||
server_port: {
|
||||
get() { return this.Inbound.handshake.server_port ? this.Inbound.handshake.server_port : 443; },
|
||||
set(newValue: any) { this.Inbound.handshake.server_port = newValue.length == 0 || newValue == 0 ? 443 : parseInt(newValue); }
|
||||
},
|
||||
},
|
||||
components: { Dial }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<v-card subtitle="Shadowsocks">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
label="Method"
|
||||
:items="ssMethods"
|
||||
v-model="inbound.method">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="inbound.password" label="Password" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<Network :inbound="inbound" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Network from '@/components/Network.vue'
|
||||
|
||||
export default {
|
||||
props: ['inbound'],
|
||||
data() {
|
||||
return {
|
||||
ssMethods: [
|
||||
"none",
|
||||
"aes-128-gcm",
|
||||
"aes-192-gcm",
|
||||
"aes-256-gcm",
|
||||
"chacha20-ietf-poly1305",
|
||||
"xchacha20-ietf-poly1305",
|
||||
"2022-blake3-aes-128-gcm",
|
||||
"2022-blake3-aes-256-gcm",
|
||||
"2022-blake3-chacha20-poly1305"
|
||||
]
|
||||
}
|
||||
},
|
||||
components: { Network }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<v-card subtitle="TProxy">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<Network :inbound="inbound" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Network from '@/components/Network.vue'
|
||||
|
||||
export default {
|
||||
props: ['inbound'],
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
components: { Network }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<v-card subtitle="TUIC">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
label="Congestion Control"
|
||||
:items="congestion_controls"
|
||||
v-model="inbound.congestion_control">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" label="Zero-RTT Handshake" v-model="inbound.zero_rtt_handshake" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Authentication Timeout"
|
||||
hide-details
|
||||
type="number"
|
||||
suffix="s"
|
||||
min="1"
|
||||
v-model.number="auth_timeout">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Heartbeat"
|
||||
hide-details
|
||||
type="number"
|
||||
suffix="s"
|
||||
min="1"
|
||||
v-model.number="heartbeat">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { TUIC } from '@/types/inbounds'
|
||||
export default {
|
||||
props: ['inbound'],
|
||||
data() {
|
||||
return {
|
||||
congestion_controls: [
|
||||
"cubic","new_reno", "bbr"
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
Inbound(): TUIC {
|
||||
return <TUIC> this.$props.inbound
|
||||
},
|
||||
auth_timeout: {
|
||||
get() { return this.Inbound.auth_timeout ? parseInt(this.Inbound.auth_timeout.replace('s','')) : '' },
|
||||
set(newValue:number) { this.$props.inbound.auth_timeout = newValue ? newValue + 's' : '' }
|
||||
},
|
||||
heartbeat: {
|
||||
get() { return this.Inbound.heartbeat ? parseInt(this.Inbound.heartbeat.replace('s','')) : '' },
|
||||
set(newValue:number) { this.$props.inbound.heartbeat = newValue ? newValue + 's' : '' }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,111 @@
|
||||
<script lang="ts" setup>
|
||||
import { HumanReadable } from '@/plugins/utils';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
tilesData: <any>{},
|
||||
type: String
|
||||
})
|
||||
|
||||
const data = computed(() => {
|
||||
const d = props.tilesData
|
||||
if (!d.mem && !d.cpu) return { percent: 0, text: '-' }
|
||||
switch (props.type) {
|
||||
case 'g-cpu':
|
||||
return { percent: d.cpu, text: Math.ceil(d.cpu) + "%" }
|
||||
case 'g-mem':
|
||||
const curr = HumanReadable.sizeFormat(d.mem.current,0).split(' ')
|
||||
const total = HumanReadable.sizeFormat(d.mem.total,0).split(' ')
|
||||
if (curr[1] == total[1]) curr[1] = ''
|
||||
return {
|
||||
percent: Math.ceil(d.mem.current*100/d.mem.total),
|
||||
text: curr[0] + "<sup>" + (curr[1]?? ' ') + "</sup>/" + total[0] + "<sup>" + (total[1]?? '') + "</sup>"
|
||||
}
|
||||
}
|
||||
return { percent: 0, text: '-'}
|
||||
})
|
||||
|
||||
const cssTransformRotateValue = computed(() => {
|
||||
const percentageAsFraction = data.value.percent / 100
|
||||
const halfPercentage = percentageAsFraction / 2
|
||||
|
||||
return `${halfPercentage}turn`
|
||||
})
|
||||
|
||||
const gaugeColor = computed(() => {
|
||||
if (data.value.percent > 90) return 'error'
|
||||
if (data.value.percent > 70) return 'warning'
|
||||
return 'primary'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gauge__outer">
|
||||
<div class="gauge__inner">
|
||||
<div
|
||||
class="gauge__fill"
|
||||
:style="{
|
||||
transform: `rotate(${cssTransformRotateValue})`,
|
||||
background: `rgb(var(--v-theme-${gaugeColor}))`
|
||||
}">
|
||||
</div>
|
||||
<span class="gauge__cover" dir="ltr" v-html="data.text">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.gauge__outer {
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.gauge__inner {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
padding-bottom: 50%;
|
||||
background: rgb(var(--v-theme-surface));
|
||||
position: relative;
|
||||
border-top-left-radius: 100% 200%;
|
||||
border-top-right-radius: 100% 200%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gauge__fill {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: inherit;
|
||||
height: 100%;
|
||||
background: rgb(var(--v-theme-primary));
|
||||
transform-origin: center top;
|
||||
transform: rotate(0turn);
|
||||
transition: transform 0.2s ease-out;
|
||||
}
|
||||
|
||||
.gauge__cover {
|
||||
width: 75%;
|
||||
height: 150%;
|
||||
background: rgb(var(--v-theme-background));
|
||||
position: absolute;
|
||||
top: 25%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-radius: 50%;
|
||||
|
||||
/* Text */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 25%;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Lexend', sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
sup {
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<Line v-if="loaded" :data="data" :options="<any>options" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Line } from 'vue-chartjs'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Filler,
|
||||
} from 'chart.js'
|
||||
import { HumanReadable } from '@/plugins/utils'
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Filler
|
||||
)
|
||||
ChartJS.defaults.font.family = 'Vazirmatn'
|
||||
export default {
|
||||
components: {
|
||||
Line
|
||||
},
|
||||
props: ['tilesData','type'],
|
||||
data() {
|
||||
return {
|
||||
loaded: false,
|
||||
labels: new Array(20).fill(''),
|
||||
oldValues: <any>{},
|
||||
options1: {
|
||||
animation: false,
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
enabled: false
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
grid: {
|
||||
color: () => { return this.$vuetify.theme.current.colors.secondary },
|
||||
},
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
steps: 10,
|
||||
stepValue: 5,
|
||||
max: 100
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
optionsNet: {
|
||||
animation: false,
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
enabled: false
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
grid: {
|
||||
color: () => { return this.$vuetify.theme.current.colors.secondary },
|
||||
},
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: (label:any, index: number) => { return parseInt(label).toString() },
|
||||
count: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data: ref(<any>{})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
options() {
|
||||
switch (this.$props.type){
|
||||
case "h-net":
|
||||
this.optionsNet.scales.y.ticks.callback = (label:any, index: number) => {
|
||||
return label == 0 ? "0" : HumanReadable.sizeFormat(label,0)
|
||||
}
|
||||
return this.optionsNet
|
||||
case "hp-net":
|
||||
this.optionsNet.scales.y.ticks.callback = (label:any, index: number) => {
|
||||
return label == 0 ? "0" : HumanReadable.packetFormat(label,0)
|
||||
}
|
||||
return this.optionsNet
|
||||
}
|
||||
return this.options1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateData1(value1: number) {
|
||||
const newData = <number[]>[]
|
||||
if (this.data.datasets){
|
||||
newData.push(...this.data.datasets[0].data,value1)
|
||||
}
|
||||
if (newData.length>20) newData.shift()
|
||||
this.data = {
|
||||
labels: this.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: '',
|
||||
backgroundColor: 'rgba(255, 165, 0, 0.2)',
|
||||
borderColor: 'rgba(255, 165, 0,0.8)',
|
||||
fill: true,
|
||||
data: newData
|
||||
}
|
||||
],
|
||||
}
|
||||
this.loaded = true
|
||||
},
|
||||
updateData2(value1: number, value2:number) {
|
||||
const newData1 = <number[]>[]
|
||||
const newData2 = <number[]>[]
|
||||
if (this.data.datasets){
|
||||
newData1.push(...this.data.datasets[0].data,value1)
|
||||
newData2.push(...this.data.datasets[1].data,value2)
|
||||
}
|
||||
if (newData1.length>20) newData1.shift()
|
||||
if (newData2.length>20) newData2.shift()
|
||||
this.data = {
|
||||
labels: this.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: '',
|
||||
backgroundColor: 'rgba(255, 165, 0, 0.2)',
|
||||
borderColor: 'rgba(255, 165, 0,0.8)',
|
||||
fill: true,
|
||||
data: newData1
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
backgroundColor: 'rgba(0, 128, 0, 0.1)',
|
||||
borderColor: 'rgba(0, 128, 0,0.8)',
|
||||
fill: true,
|
||||
data: newData2
|
||||
}
|
||||
],
|
||||
}
|
||||
this.loaded = true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
tilesData(v:any) {
|
||||
switch (this.$props.type) {
|
||||
case 'h-cpu':
|
||||
this.updateData1(v.cpu)
|
||||
break
|
||||
case 'h-mem':
|
||||
this.updateData1(v.mem.current*100/v.mem.total)
|
||||
break
|
||||
case 'h-net':
|
||||
if (this.oldValues.sent) {
|
||||
const downSpeed = (v.net.recv-this.oldValues.recv)/2 // Each 2 sec
|
||||
const upSpeed = (v.net.sent-this.oldValues.sent)/2 // Each 2 sec
|
||||
this.updateData2(upSpeed,downSpeed)
|
||||
}
|
||||
this.oldValues = v.net
|
||||
break
|
||||
case 'hp-net':
|
||||
if (this.oldValues.psent) {
|
||||
const downSpeed = (v.net.precv-this.oldValues.precv)/2 // Each 2 sec
|
||||
const upSpeed = (v.net.psent-this.oldValues.psent)/2 // Each 2 sec
|
||||
this.updateData2(upSpeed,downSpeed)
|
||||
}
|
||||
this.oldValues = v.net
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('transport.hosts')"
|
||||
hide-details
|
||||
v-model="hosts">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('transport.path')"
|
||||
hide-details
|
||||
v-model="transport.path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Method"
|
||||
hide-details
|
||||
v-model="transport.method">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Idle Timeout"
|
||||
hide-details
|
||||
type="number"
|
||||
suffix="s"
|
||||
min="1"
|
||||
v-model.number="idle_timeout">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Ping Timeout"
|
||||
hide-details
|
||||
type="number"
|
||||
suffix="s"
|
||||
min="1"
|
||||
v-model.number="ping_timeout">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { HTTP } from '../../types/transport'
|
||||
export default {
|
||||
props: ['transport'],
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
Http(): HTTP {
|
||||
return <HTTP> this.$props.transport?? {}
|
||||
},
|
||||
hosts: {
|
||||
get() { return this.Http.host ? this.Http.host.join(',') : '' },
|
||||
set(newValue:string) { this.$props.transport.host = newValue.length>0 ? newValue.split(',') : [] }
|
||||
},
|
||||
idle_timeout: {
|
||||
get() { return this.Http.idle_timeout ? parseInt(this.Http.idle_timeout.replace('s','')) : '' },
|
||||
set(newValue:number) { this.$props.transport.idle_timeout = newValue ? newValue + 's' : '' }
|
||||
},
|
||||
ping_timeout: {
|
||||
get() { return this.Http.ping_timeout ? parseInt(this.Http.ping_timeout.replace('s','')) : '' },
|
||||
set(newValue:number) { this.$props.transport.ping_timeout = newValue ? newValue + 's' : '' }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('transport.hosts')"
|
||||
hide-details
|
||||
v-model="transport.host">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('transport.path')"
|
||||
hide-details
|
||||
v-model="transport.path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: ['transport'],
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('transport.path')"
|
||||
hide-details
|
||||
v-model="transport.path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Max Early Data"
|
||||
hide-details
|
||||
type="number"
|
||||
min="0"
|
||||
v-model.number="max_early_data">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Early Data Header Name"
|
||||
hide-details
|
||||
v-model="transport.early_data_header_name">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { WebSocket } from '../../types/transport'
|
||||
export default {
|
||||
props: ['transport'],
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
WS(): WebSocket {
|
||||
return <WebSocket> this.$props.transport
|
||||
},
|
||||
max_early_data: {
|
||||
get() { return this.WS.max_early_data ? this.WS.max_early_data : '' },
|
||||
set(newValue:number) { this.$props.transport.max_early_data = newValue != 0 ? newValue : undefined }
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.WS.early_data_header_name = 'Sec-WebSocket-Protocol'
|
||||
this.WS.path = '/'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Service Name"
|
||||
hide-details
|
||||
v-model="transport.service_name">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch
|
||||
color="primary"
|
||||
v-model="transport.permit_without_stream"
|
||||
label="Permit Without Stream"
|
||||
hide-details>
|
||||
</v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Idle Timeout"
|
||||
hide-details
|
||||
type="number"
|
||||
suffix="s"
|
||||
min="1"
|
||||
v-model.number="idle_timeout">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Ping Timeout"
|
||||
hide-details
|
||||
type="number"
|
||||
suffix="s"
|
||||
min="1"
|
||||
v-model.number="ping_timeout">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { gRPC } from '../../types/transport'
|
||||
export default {
|
||||
props: ['transport'],
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
GRPC(): gRPC {
|
||||
return <gRPC> this.$props.transport?? {}
|
||||
},
|
||||
idle_timeout: {
|
||||
get() { return this.GRPC.idle_timeout ? parseInt(this.GRPC.idle_timeout.replace('s','')) : '' },
|
||||
set(newValue:number) { this.$props.transport.idle_timeout = newValue ? newValue + 's' : '' }
|
||||
},
|
||||
ping_timeout: {
|
||||
get() { return this.GRPC.ping_timeout ? parseInt(this.GRPC.ping_timeout.replace('s','')) : '' },
|
||||
set(newValue:number) { this.$props.transport.ping_timeout = newValue ? newValue + 's' : '' }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<v-app-bar :elevation="5">
|
||||
<v-icon v-if="isMobile" icon="mdi-menu" @click="$emit('toggleDrawer')" />
|
||||
<v-app-bar-title :text="$t(<string>$router.currentRoute.value.name)" class="align-center text-center " />
|
||||
<v-btn prepend-icon="mdi-content-save" v-if="stateChange" :text="$t('actions.save')" @click="saveChanges"></v-btn>
|
||||
<v-icon icon="mdi-theme-light-dark" @click="toggleTheme()" style="margin: 0 10px;"></v-icon>
|
||||
</v-app-bar>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref,watch } from "vue"
|
||||
import { useTheme } from "vuetify"
|
||||
import { FindDiff } from "@/plugins/utils"
|
||||
import Data from "@/store/modules/data"
|
||||
|
||||
defineProps(['isMobile'])
|
||||
|
||||
const theme = useTheme()
|
||||
const darkMode = ref(localStorage.getItem('theme') == "dark")
|
||||
|
||||
const store = Data()
|
||||
|
||||
const toggleTheme = () => {
|
||||
darkMode.value = !darkMode.value
|
||||
theme.global.name.value = darkMode.value ? "dark" : "light"
|
||||
localStorage.setItem('theme', theme.global.name.value)
|
||||
}
|
||||
|
||||
const saveChanges = () => {
|
||||
store.pushData()
|
||||
}
|
||||
|
||||
const oldData = computed((): any => {
|
||||
return {config: store.oldData.config, clients: store.oldData.clients}
|
||||
})
|
||||
|
||||
const newData = computed((): any => {
|
||||
return {config: store.config, clients: store.clients}
|
||||
})
|
||||
|
||||
const stateChange = computed((): any => {
|
||||
return !FindDiff.deepCompare(newData.value,oldData.value)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<v-app style="overflow: auto;">
|
||||
<drawer :isMobile="isMobile" :displayDrawer="displayDrawer" @toggleDrawer="toggleDrawer" />
|
||||
<default-bar :isMobile="isMobile" @toggleDrawer="toggleDrawer" />
|
||||
<default-view />
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import DefaultBar from './AppBar.vue'
|
||||
import Drawer from './Drawer.vue'
|
||||
import DefaultView from './View.vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
const { smAndDown } = useDisplay()
|
||||
const displayDrawer = ref(false)
|
||||
|
||||
const toggleDrawer = () => {
|
||||
displayDrawer.value = !displayDrawer.value
|
||||
}
|
||||
|
||||
const isMobile = computed( ():boolean =>{
|
||||
displayDrawer.value = !smAndDown.value
|
||||
return smAndDown.value
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<v-navigation-drawer
|
||||
v-model="showDrawer"
|
||||
:temporary="isMobile"
|
||||
:expand-on-hover="!isMobile"
|
||||
:rail="!isMobile"
|
||||
:permanent="!isMobile"
|
||||
@click="isMobile ? $emit('toggleDrawer') : null"
|
||||
>
|
||||
<v-list-item
|
||||
height="63"
|
||||
prepend-avatar="@/assets/logo.svg"
|
||||
title="S-UI"
|
||||
>
|
||||
<template v-slot:append v-if="isMobile">
|
||||
<v-icon icon="mdi-close" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-list density="compact" nav>
|
||||
<v-list-item link
|
||||
v-for="item in menu"
|
||||
:key="item.title"
|
||||
:to="item.path"
|
||||
:active="router.currentRoute.value.path == item.path">
|
||||
<template v-slot:prepend>
|
||||
<v-icon :icon="item.icon"></v-icon>
|
||||
</template>
|
||||
<v-list-item-title v-text="$t(item.title)"></v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<template v-slot:append>
|
||||
<v-list-item prepend-icon="mdi-logout" :title="$t('menu.logout')" @click="logout"></v-list-item>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import router from '@/router'
|
||||
import HttpUtil from '@/plugins/httputil'
|
||||
|
||||
const props = defineProps(['isMobile','displayDrawer'])
|
||||
|
||||
const showDrawer = computed((): boolean => {
|
||||
return props.displayDrawer
|
||||
})
|
||||
|
||||
const menu = [
|
||||
{ title: 'pages.home', icon: 'mdi-home', path: '/' },
|
||||
{ title: 'pages.inbounds', icon: 'mdi-cloud-download', path: '/inbounds' },
|
||||
{ title: 'pages.clients', icon: 'mdi-account-multiple', path: '/clients' },
|
||||
{ title: 'pages.outbounds', icon: 'mdi-cloud-upload', path: '/outbounds' },
|
||||
{ title: 'pages.rules', icon: 'mdi-routes', path: '/rules' },
|
||||
{ title: 'pages.basics', icon: 'mdi-application-cog', path: '/basics' },
|
||||
{ title: 'pages.settings', icon: 'mdi-cog', path: '/settings' },
|
||||
]
|
||||
|
||||
const logout = async () => {
|
||||
const response = await HttpUtil.get('/api/logout')
|
||||
if(response.success){
|
||||
router.push('/login')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<v-main>
|
||||
<router-view />
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.v-main {
|
||||
margin: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
{{ $t('actions.' + title) + " " + $t('objects.client') }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="padding: 0 16px;">
|
||||
<v-container style="padding: 0;">
|
||||
<v-tabs
|
||||
v-model="tab"
|
||||
align-tabs="center"
|
||||
>
|
||||
<v-tab value="t1">{{ $t('client.basics') }}</v-tab>
|
||||
<v-tab value="t2">{{ $t('client.config') }}</v-tab>
|
||||
<v-tab value="t3">{{ $t('client.links') }}</v-tab>
|
||||
</v-tabs>
|
||||
<v-window v-model="tab">
|
||||
<v-window-item value="t1">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" v-model="client.enable" :label="$t('enable')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="client.name" :label="$t('client.name')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model.number="Volume" type="number" min="0" :label="$t('stats.volume')" suffix="GiB" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<DatePick :expiry="expDate" @submit="setDate" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-combobox
|
||||
v-model="clientInbounds"
|
||||
:items="inboundTags"
|
||||
:label="$t('client.inboundTags')"
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="auto">
|
||||
<v-switch v-model="clientStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
<v-window-item value="t2">
|
||||
<v-row v-for="(value, key) in clientConfig" :key="key">
|
||||
<v-col cols="12" md="3" align="end" align-self="center">
|
||||
{{ key }}
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-if="value.password != undefined"
|
||||
label="Password"
|
||||
v-model="value.password"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
v-if="value.uuid != undefined"
|
||||
label="UUID"
|
||||
v-model="value.uuid"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
v-if="value.flow != undefined"
|
||||
label="Flow"
|
||||
v-model="value.flow"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
v-if="value.auth_str != undefined"
|
||||
label="Auth"
|
||||
v-model="value.auth_str"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
<v-window-item value="t3">
|
||||
<v-row v-for="(lnk, index) in links">
|
||||
<v-col cols="auto">{{ index + 1 }}</v-col>
|
||||
<v-col style="direction: ltr; overflow-y: hidden;">{{ lnk.uri }}</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn color="primary" @click="extLinks.push({ type: 'external', uri: ''})">{{ $t('actions.add') }} {{ $t('client.external') }}</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-for="(lnk, index) in extLinks">
|
||||
<v-col>
|
||||
<v-text-field
|
||||
dir="ltr"
|
||||
:label="$t('client.external') + ' ' + (index+1)"
|
||||
append-icon="mdi-delete"
|
||||
@click:append="extLinks.splice(index,1)"
|
||||
placeholder="<protocol>://<data>"
|
||||
v-model="lnk.uri" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn color="primary" @click="subLinks.push({ type: 'sub', uri: ''})">{{ $t('actions.add') }} {{ $t('client.sub') }}</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-for="(lnk, index) in subLinks">
|
||||
<v-col>
|
||||
<v-text-field
|
||||
dir="ltr"
|
||||
:label="$t('client.sub') + ' ' + (index+1)"
|
||||
append-icon="mdi-delete"
|
||||
@click:append="subLinks.splice(index,1)"
|
||||
placeholder="http[s]://<domain>[:]<port>/<path>"
|
||||
v-model="lnk.uri" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="blue-darken-1"
|
||||
variant="outlined"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('actions.close') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue-darken-1"
|
||||
variant="tonal"
|
||||
@click="saveChanges"
|
||||
>
|
||||
{{ $t('actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Link } from '@/plugins/link'
|
||||
import { createClient, randomConfigs, updateConfigs } from '@/types/clients'
|
||||
import DatePick from '@/components/DateTime.vue'
|
||||
|
||||
export default {
|
||||
props: ['visible', 'data', 'index', 'inboundTags', 'stats'],
|
||||
emits: ['close', 'save'],
|
||||
data() {
|
||||
return {
|
||||
client: createClient(),
|
||||
title: "add",
|
||||
clientStats: false,
|
||||
tab: "t1",
|
||||
clientConfig: <any>[],
|
||||
links: <Link[]>[],
|
||||
extLinks: <Link[]>[],
|
||||
subLinks: <Link[]>[],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateData() {
|
||||
if (this.$props.index != -1) {
|
||||
const newData = JSON.parse(this.$props.data)
|
||||
this.client = createClient(newData)
|
||||
this.title = "edit"
|
||||
this.clientConfig = JSON.parse(this.client.config)
|
||||
}
|
||||
else {
|
||||
this.client = createClient()
|
||||
this.title = "add"
|
||||
this.clientConfig = randomConfigs('client')
|
||||
}
|
||||
this.clientStats = this.$props.stats
|
||||
const allLinks = <Link[]>JSON.parse(this.client.links)
|
||||
this.links = allLinks.filter(l => l.type == 'local')
|
||||
this.extLinks = allLinks.filter(l => l.type == 'external')
|
||||
this.subLinks = allLinks.filter(l => l.type == 'sub')
|
||||
this.tab = "t1"
|
||||
},
|
||||
closeModal() {
|
||||
this.updateData() // reset
|
||||
this.$emit('close')
|
||||
},
|
||||
saveChanges() {
|
||||
this.client.config = updateConfigs(JSON.stringify(this.clientConfig), this.client.name)
|
||||
this.client.links = JSON.stringify([
|
||||
...this.links,
|
||||
...this.extLinks.filter(l => l.uri != ''),
|
||||
...this.subLinks.filter(l => l.uri != '')])
|
||||
this.$emit('save', this.client, this.clientStats)
|
||||
},
|
||||
setDate(newDate:number){
|
||||
this.client.expiry = newDate
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
clientInbounds: {
|
||||
get() { return this.client.inbounds == "" ? [] : this.client.inbounds.split(',') },
|
||||
set(newValue:string[]) { this.client.inbounds = newValue.length == 0 ? "" : newValue.join(',') }
|
||||
},
|
||||
expDate: {
|
||||
get() { return this.client.expiry},
|
||||
set(v:any) { this.client.expiry = v }
|
||||
},
|
||||
Volume: {
|
||||
get() { return this.client.volume == 0 ? 0 : (this.client.volume / (1024 ** 3)) },
|
||||
set(v:number) { this.client.volume = v > 0 ? v*(1024 ** 3) : 0 }
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(newValue) { if (newValue) {
|
||||
this.updateData()
|
||||
}
|
||||
},
|
||||
},
|
||||
components: { DatePick },
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
{{ $t('actions.' + title) + " " + $t('objects.inbound') }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
width="100"
|
||||
:label="$t('type')"
|
||||
:items="Object.keys(inTypes).map((key,index) => ({title: key, value: Object.values(inTypes)[index]}))"
|
||||
v-model="inbound.type"
|
||||
@update:modelValue="changeType">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="inbound.tag" :label="$t('in.tag')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Listen :inbound="inbound" />
|
||||
<Direct v-if="inbound.type == inTypes.Direct" :inbound="inbound" />
|
||||
<Shadowsocks v-if="inbound.type == inTypes.Shadowsocks" :inbound="inbound" />
|
||||
<Hysteria v-if="inbound.type == inTypes.Hysteria" :inbound="inbound" />
|
||||
<Hysteria2 v-if="inbound.type == inTypes.Hysteria2" :inbound="inbound" />
|
||||
<Naive v-if="inbound.type == inTypes.Naive" :inbound="inbound" />
|
||||
<ShadowTls v-if="inbound.type == inTypes.ShadowTLS" :inbound="inbound" />
|
||||
<Tuic v-if="inbound.type == inTypes.TUIC" :inbound="inbound" />
|
||||
<TProxy v-if="inbound.type == inTypes.TProxy" :inbound="inbound" />
|
||||
<Transport v-if="Object.hasOwn(inbound,'transport')" :inbound="inbound" />
|
||||
<Users v-if="HasOptionalUser.includes(inbound.type)" :inbound="inbound" :id="id" />
|
||||
<InTls v-if="Object.hasOwn(inbound,'tls')" :inbound="inbound" />
|
||||
<InMulitiplex v-if="Object.hasOwn(inbound,'multiplex')" :inbound="inbound" />
|
||||
<v-switch v-model="inboundStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="blue-darken-1"
|
||||
variant="text"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('actions.close') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue-darken-1"
|
||||
variant="text"
|
||||
@click="saveChanges"
|
||||
>
|
||||
{{ $t('actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { InTypes, createInbound } from '@/types/inbounds'
|
||||
import Listen from '@/components/Listen.vue'
|
||||
import Direct from '@/components/protocols/Direct.vue'
|
||||
import Users from '@/components/Users.vue'
|
||||
import Shadowsocks from '@/components/protocols/Shadowsocks.vue'
|
||||
import Hysteria from '@/components/protocols/Hysteria.vue'
|
||||
import Hysteria2 from '@/components/protocols/Hysteria2.vue'
|
||||
import Naive from '@/components/protocols/Naive.vue'
|
||||
import ShadowTls from '@/components/protocols/ShadowTls.vue'
|
||||
import Tuic from '@/components/protocols/Tuic.vue'
|
||||
import InTls from '@/components/InTLS.vue'
|
||||
import TProxy from '@/components/protocols/TProxy.vue'
|
||||
import RandomUtil from '@/plugins/randomUtil'
|
||||
import InMulitiplex from '@/components/InMulitiplex.vue'
|
||||
import Transport from '@/components/Transport.vue'
|
||||
export default {
|
||||
props: ['visible', 'data', 'id', 'stats'],
|
||||
emits: ['close', 'save'],
|
||||
data() {
|
||||
return {
|
||||
inbound: createInbound("direct",{ "tag": "" }),
|
||||
title: "add",
|
||||
inTypes: InTypes,
|
||||
inboundStats: false,
|
||||
HasOptionalUser: [InTypes.Mixed,InTypes.SOCKS,InTypes.HTTP,InTypes.Shadowsocks],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateData() {
|
||||
if (this.$props.id != -1) {
|
||||
const newData = JSON.parse(this.$props.data)
|
||||
this.inbound = createInbound(newData.type, newData)
|
||||
this.title = "edit"
|
||||
}
|
||||
else {
|
||||
const port = RandomUtil.randomIntRange(10000, 60000)
|
||||
this.inbound = createInbound("mixed",{ tag: "in-"+port ,listen: "::", listen_port: port })
|
||||
this.title = "add"
|
||||
}
|
||||
this.inboundStats = this.$props.stats
|
||||
},
|
||||
changeType() {
|
||||
const prevConfig = { tag: this.inbound.tag ,listen: this.inbound.listen, listen_port: this.inbound.listen_port }
|
||||
this.inbound = createInbound(this.inbound.type, prevConfig)
|
||||
},
|
||||
closeModal() {
|
||||
this.updateData() // reset
|
||||
this.$emit('close')
|
||||
},
|
||||
saveChanges() {
|
||||
this.$emit('save', this.inbound, this.inboundStats)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(newValue) {
|
||||
if (newValue) {
|
||||
this.updateData()
|
||||
}
|
||||
},
|
||||
},
|
||||
components: { Listen, InTls, Hysteria2, Naive, Direct, Shadowsocks, Users, Hysteria, ShadowTls, TProxy, InMulitiplex, Tuic, Transport }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.v-card-subtitle {
|
||||
text-align: center;
|
||||
border-bottom: 1px solid gray;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="400">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>QrCode</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="1"><v-icon icon="mdi-close-box" @click="$emit('close')" ></v-icon></v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col style="text-align: center;" @click="copyToClipboard(clientSub)">
|
||||
<v-chip>{{ $t('setting.sub') }}</v-chip>
|
||||
<QrcodeVue :value="clientSub" :size="300" :margin="1" style="border-radius: 1rem;" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-for="l in clientLinks">
|
||||
<v-col style="text-align: center;" @click="copyToClipboard(l.uri)">
|
||||
<v-chip>{{ l.remark }}</v-chip><br />
|
||||
<QrcodeVue :value="l.uri" :size="300" :margin="1" style="border-radius: 1rem;" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import Data from '@/store/modules/data'
|
||||
import Clipboard from 'clipboard'
|
||||
import Message from '@/store/modules/message'
|
||||
import { i18n } from '@/locales'
|
||||
|
||||
export default {
|
||||
props: ['index'],
|
||||
data() {
|
||||
return {
|
||||
msg: Message(),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard(txt:string) {
|
||||
|
||||
const clipboard = new Clipboard('.clipboard-btn', {
|
||||
text: () => txt
|
||||
});
|
||||
|
||||
clipboard.on('success', () => {
|
||||
clipboard.destroy()
|
||||
this.msg.showMessage(i18n.global.t('copyToClipboard') + " : " + i18n.global.t('success'),'success',5000)
|
||||
})
|
||||
|
||||
clipboard.on('error', () => {
|
||||
clipboard.destroy()
|
||||
this.msg.showMessage(i18n.global.t('copyToClipboard') + " : " + i18n.global.t('failed'),'error',5000)
|
||||
})
|
||||
|
||||
// Perform click on hidden button to trigger copy
|
||||
const hiddenButton = document.createElement('button');
|
||||
hiddenButton.className = 'clipboard-btn';
|
||||
document.body.appendChild(hiddenButton);
|
||||
hiddenButton.click();
|
||||
document.body.removeChild(hiddenButton);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
clients() { return Data().clients },
|
||||
client() {
|
||||
if ( typeof this.$props.index != 'number' ) return <any>{}
|
||||
return this.clients[this.$props.index]
|
||||
},
|
||||
clientSub() {
|
||||
return Data().subURI + this.client.name
|
||||
},
|
||||
clientLinks() {
|
||||
return JSON.parse(this.client.links?? "[]")
|
||||
}
|
||||
},
|
||||
components: { QrcodeVue }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg" :loading="loading" color="background">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>
|
||||
{{ $t('stats.graphTitle') + " - " + $t('objects.' + resource) + " : " + tag }}
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto"><v-icon icon="mdi-close" @click="$emit('close')"></v-icon></v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="padding: 0 16px;">
|
||||
<v-container id="container">
|
||||
<v-alert :text="$t('noData')" type="warning" variant="outlined" v-if="alert"></v-alert>
|
||||
<Line v-if="loaded" :data="usage" :options="<any>options" />
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { i18n } from '@/locales'
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
import { HumanReadable } from '@/plugins/utils'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler,
|
||||
} from 'chart.js'
|
||||
import { ref } from 'vue'
|
||||
import { Line } from 'vue-chartjs'
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler
|
||||
)
|
||||
ChartJS.defaults.font.family = 'Vazirmatn'
|
||||
export default {
|
||||
components: {
|
||||
Line
|
||||
},
|
||||
props: ['visible','resource','tag'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
loaded: false,
|
||||
alert: false,
|
||||
intervalId: <any>0,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
text: (ctx:any) => {
|
||||
const {axis = 'xy', intersect, mode} = ctx.chart.options.interaction;
|
||||
return 'Mode: ' + mode + ', axis: ' + axis + ', intersect: ' + intersect;
|
||||
},
|
||||
footer: (items:any[]) => {
|
||||
return HumanReadable.sizeFormat(items.reduce((acc, c) => acc + c.raw, 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
grid: {
|
||||
color: () => { return this.$vuetify.theme.current.colors.secondary },
|
||||
},
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: function(label:any, index: number) {
|
||||
return label == 0 ? 0 : HumanReadable.sizeFormat(label,0)
|
||||
},
|
||||
count: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
usage: ref(<any>{}),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadData(limit: number) {
|
||||
this.loading = true
|
||||
const data = await HttpUtils.get('/api/stats', { resource: this.resource, tag: this.tag, limit: limit })
|
||||
if (data.success && data.obj) {
|
||||
const obj = <any[]>data.obj
|
||||
const l = String(i18n.global.locale) == 'fa' ? "fa-IR" : "en-US"
|
||||
const oneStep = limit * 3600 * 1000 / 360 // Each 10 sec
|
||||
const now = new Date().getTime()
|
||||
const steps = <number[]>[]
|
||||
for (let i = 360; i >= 0; i--) {
|
||||
steps.push(now - (oneStep * i))
|
||||
}
|
||||
const labels = <string[]>[]
|
||||
const uplinkData = <number[]>[]
|
||||
const downlinkData = <number[]>[]
|
||||
for (let i = 1; i<360; i++) {
|
||||
labels.push(this.genLable(steps[i],l))
|
||||
let upSum:number
|
||||
let downSum:number
|
||||
const upTraffics = obj.filter(o => o.direction && o.dateTime*1000 < steps[i] && o.dateTime*1000 > steps[i-1]).map((o:any) => o.traffic)
|
||||
upSum = upTraffics.length>0 ? upTraffics.reduce(u => u) : null
|
||||
const downTraffics = obj.filter(o => !o.direction && o.dateTime*1000 < steps[i] && o.dateTime*1000 > steps[i-1]).map((o:any) => o.traffic)
|
||||
downSum = downTraffics.length>0 ? downTraffics.reduce(d => d) : null
|
||||
uplinkData.push(upSum)
|
||||
downlinkData.push(downSum)
|
||||
}
|
||||
this.usage = {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: i18n.global.t('stats.upload'),
|
||||
backgroundColor: 'rgba(255, 165, 0, 0.4)',
|
||||
borderColor: 'rgba(255, 165, 0)',
|
||||
fill: true,
|
||||
data: uplinkData
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('stats.download'),
|
||||
backgroundColor: 'rgba(0, 128, 0, 0.2)',
|
||||
borderColor: 'rgba(0, 128, 0)',
|
||||
fill: true,
|
||||
data: downlinkData
|
||||
}
|
||||
],
|
||||
}
|
||||
this.loaded = true
|
||||
} else {
|
||||
this.alert = true
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
genLable(step:number, locale: string) {
|
||||
return new Date(step).toLocaleString(locale,{
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(v) {
|
||||
if (v) {
|
||||
const limit = 1
|
||||
this.loadData(limit)
|
||||
this.intervalId = setInterval(() => {
|
||||
this.loadData(limit)
|
||||
}, 10000)
|
||||
} else {
|
||||
this.loaded = false
|
||||
this.alert = false
|
||||
this.usage.labels = []
|
||||
if (this.usage.datasets) {
|
||||
this.usage.datasets[0].data = []
|
||||
this.usage.datasets[1].data = []
|
||||
}
|
||||
if (this.intervalId && this.intervalId != 0) {
|
||||
clearInterval(this.intervalId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,160 @@
|
||||
export default {
|
||||
message: "Welcome",
|
||||
success: "success",
|
||||
failed: "failed",
|
||||
enable: "Enable",
|
||||
disable: "Disable",
|
||||
loading: "Loading...",
|
||||
confirm: "Are you sure ?",
|
||||
yes: "yes",
|
||||
no: "no",
|
||||
unlimited: "infinite",
|
||||
remained: "Remained",
|
||||
type: "Type",
|
||||
submit: "Submit",
|
||||
reset: "Reset",
|
||||
now: "Now",
|
||||
network: "Network",
|
||||
copyToClipboard: "Copy to clipboard",
|
||||
noData: "No data!",
|
||||
online: "Online",
|
||||
pages: {
|
||||
login: "Login",
|
||||
home: "Home",
|
||||
inbounds: "Inbounds",
|
||||
outbounds: "Outbounds",
|
||||
clients: "Clients",
|
||||
rules: "Rules",
|
||||
basics: "Basics",
|
||||
settings: "Settings",
|
||||
},
|
||||
main: {
|
||||
tiles: "Tiles",
|
||||
gauges: "Gauges",
|
||||
charts: "Charts",
|
||||
infos: "Information",
|
||||
gauge: {
|
||||
cpu: "CPU Gauge",
|
||||
mem: "RAM Gauge",
|
||||
},
|
||||
chart: {
|
||||
cpu: "CPU Monitor",
|
||||
mem: "RAM Monitor",
|
||||
net: "Network Bandwidth",
|
||||
pnet: "Network Packets",
|
||||
},
|
||||
info: {
|
||||
sys: "System Info",
|
||||
sbd: "Sing-Box Info",
|
||||
host: "Host",
|
||||
cpu: "CPU",
|
||||
core: "Core",
|
||||
uptime: "Uptime",
|
||||
threads: "Threads",
|
||||
memory: "Memory",
|
||||
running: "Running"
|
||||
}
|
||||
},
|
||||
objects: {
|
||||
inbound: "Inbound",
|
||||
client: "Client",
|
||||
outbound: "Outbound",
|
||||
rule: "Rule",
|
||||
user: "User",
|
||||
},
|
||||
actions: {
|
||||
action: "Action",
|
||||
add: "Add",
|
||||
edit: "Edit",
|
||||
del: "Delete",
|
||||
save: "Save",
|
||||
update: "Update",
|
||||
close: "Close",
|
||||
restartApp: "Restart App",
|
||||
},
|
||||
login: {
|
||||
username: "Username",
|
||||
unRules: "Username can not be empty",
|
||||
password: "Password",
|
||||
pwRules: "Password can not be empty",
|
||||
},
|
||||
menu: {
|
||||
logout: "Logout",
|
||||
},
|
||||
setting: {
|
||||
interface: "Interface",
|
||||
sub: "Subscription",
|
||||
addr: "Address",
|
||||
port: "Port",
|
||||
domain: "Domain",
|
||||
sslKey: "SSL Key Path",
|
||||
sslCert: "SSL Certificate Path",
|
||||
sessionAge: "Session Maximum Age",
|
||||
timeLoc: "Timezone Location",
|
||||
subEncode: "Enable Encoding",
|
||||
subInfo: "Enable Client Info",
|
||||
path: "Default Path",
|
||||
update: "Automatic Update Time",
|
||||
subUri: "Subscription URI",
|
||||
},
|
||||
client: {
|
||||
name: "Name",
|
||||
inboundTags: "Inbound Tags",
|
||||
basics: "Basics",
|
||||
config: "Config",
|
||||
links: "Links",
|
||||
external: "External Link",
|
||||
sub: "External Subscription",
|
||||
},
|
||||
in: {
|
||||
tag: "Tag",
|
||||
addr: "Address",
|
||||
port: "Port",
|
||||
sniffing: "Sniffing",
|
||||
tls: "TLS",
|
||||
clients: "Enable Clients",
|
||||
multiplex: "Multiplex",
|
||||
transport: "Transport",
|
||||
},
|
||||
transport: {
|
||||
enable: "Enable Transport",
|
||||
host: "Host",
|
||||
hosts: "Hosts",
|
||||
path: "Path",
|
||||
},
|
||||
tls : {
|
||||
enable: "Enable TLS",
|
||||
usePath: "Use Path",
|
||||
useText: "Use Text",
|
||||
certPath: "Certificate File Path",
|
||||
keyPath: "Key File Path",
|
||||
cert: "Certificate",
|
||||
key: "Key",
|
||||
},
|
||||
stats: {
|
||||
upload: "Upload",
|
||||
download: "Download",
|
||||
volume: "Volume",
|
||||
usage: "Usage",
|
||||
enable: "Enable Statistics",
|
||||
graphTitle: "Traffic Chart",
|
||||
B: "B",
|
||||
KB: "KB",
|
||||
MB: "MB",
|
||||
GB: "GB",
|
||||
TB: "TB",
|
||||
PB: "PB",
|
||||
p: "p",
|
||||
Kp: "Kp",
|
||||
Mp: "Mp",
|
||||
Gb: "Gb",
|
||||
},
|
||||
date: {
|
||||
expiry: "Expiry",
|
||||
expired: "Expired",
|
||||
d: "d",
|
||||
h: "h",
|
||||
m: "m",
|
||||
s: "s",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
export default {
|
||||
message: "خوش آمدید",
|
||||
success: "موفق",
|
||||
failed: "خطا",
|
||||
enable: "فعال",
|
||||
disable: "غیرفعال",
|
||||
loading: "در حال بارگذاری...",
|
||||
confirm: "آیا مطمئن هستید ؟",
|
||||
yes: "بله",
|
||||
no: "خیر",
|
||||
unlimited: "نامحدود",
|
||||
remained: "باقیمانده",
|
||||
type: "مدل",
|
||||
submit: "تایید",
|
||||
reset: "ریست",
|
||||
now: "اکنون",
|
||||
network: "شبکه",
|
||||
copyToClipboard: "کپی در حافظه",
|
||||
noData: "بدون داده!",
|
||||
online: "آنلاین",
|
||||
pages: {
|
||||
login: "ورود",
|
||||
home: "خانه",
|
||||
inbounds: "ورودیها",
|
||||
outbounds: "خروجیها",
|
||||
clients: "کاربران",
|
||||
rules: "قوانین",
|
||||
basics: "ترازها",
|
||||
settings: "پیکربندی",
|
||||
},
|
||||
main: {
|
||||
tiles: "کاشیها",
|
||||
gauges: "سنجشها",
|
||||
charts: "نمودارها",
|
||||
infos: "دادهها",
|
||||
gauge: {
|
||||
cpu: "سنجش پردازنده",
|
||||
mem: "سنجش حافظه",
|
||||
},
|
||||
chart: {
|
||||
cpu: "نمودار پردازنده",
|
||||
mem: "نمودار حافظه",
|
||||
net: "ترافیک شبکه",
|
||||
pnet: "بستههای شبکه",
|
||||
},
|
||||
info: {
|
||||
sys: "دادههای سیستم",
|
||||
sbd: "دادههای سینگباکس",
|
||||
host: "نام",
|
||||
cpu: "پردازنده",
|
||||
core: "هسته",
|
||||
uptime: "مدت",
|
||||
threads: "نخها",
|
||||
memory: "حافظه",
|
||||
running: "اجرا"
|
||||
}
|
||||
},
|
||||
objects: {
|
||||
inbound: "ورودی",
|
||||
client: "کاربر",
|
||||
outbound: "خروجی",
|
||||
rule: "قانون",
|
||||
user: "کاربر",
|
||||
},
|
||||
actions: {
|
||||
action: "فرمان",
|
||||
add: "ایجاد",
|
||||
edit: "ویرایش",
|
||||
del: "حذف",
|
||||
save: "ذخیره",
|
||||
update: "بروزرسانی",
|
||||
close: "بستن",
|
||||
restartApp: "ریستارت پنل",
|
||||
},
|
||||
login: {
|
||||
username: "نام کاربری",
|
||||
unRules: "نام کاربری نمیتواند خالی باشد",
|
||||
password: "کلمه عبور",
|
||||
pwRules: "کلمه عبور نمیتواند خالی باشد",
|
||||
},
|
||||
menu: {
|
||||
logout: "خروج",
|
||||
},
|
||||
setting: {
|
||||
interface: "نما",
|
||||
sub: "سابسکریپشن",
|
||||
addr: "آدرس",
|
||||
port: "پورت",
|
||||
domain: "دامنه",
|
||||
sslKey: "مسیر فایل کلید",
|
||||
sslCert: "مسیر فایل گواهی",
|
||||
sessionAge: "بیشینه زمان لاگین ماندن",
|
||||
timeLoc: "منطقه زمانی",
|
||||
subEncode: "رمزگذاری",
|
||||
subInfo: "نمایش اطلاعات کاربر",
|
||||
path: "مسیر پیشفرض",
|
||||
update: "زمان بروزرسانی خودکار",
|
||||
subUri: "آدرس نهایی سابسکریپشن",
|
||||
},
|
||||
client: {
|
||||
name: "نام",
|
||||
inboundTags: "برچسبهای ورودی",
|
||||
basics: "پایه",
|
||||
config: "تنظیم",
|
||||
links: "لینکها",
|
||||
external: "لینک خارجی",
|
||||
sub: "سابسکریپشن خارجی",
|
||||
},
|
||||
in: {
|
||||
tag: "برچسب",
|
||||
addr: "آدرس",
|
||||
port: "پورت",
|
||||
sniffing: "مبدل آدرس",
|
||||
tls: "رمزنگاری",
|
||||
clients: "فعالسازی کاربران",
|
||||
multiplex: "تسهیم",
|
||||
transport: "انتقال",
|
||||
},
|
||||
transport: {
|
||||
enable: "فعالسازی انتقال",
|
||||
host: "دامنه",
|
||||
hosts: "دامنهها",
|
||||
path: "مسیر",
|
||||
},
|
||||
tls : {
|
||||
enable: "فعالسازی رمزنگاری",
|
||||
usePath: "مسیر فایل",
|
||||
useText: "متن گواهی",
|
||||
certPath: "مسیر فایل گواهی",
|
||||
keyPath: "مسیر فایل کلید",
|
||||
cert: "گواهی",
|
||||
key: "کلید",
|
||||
},
|
||||
stats: {
|
||||
upload: "آپلود",
|
||||
download: "دانلود",
|
||||
volume: "حجم",
|
||||
usage: "استفاده",
|
||||
enable: "فعال سازی کنترل ترافیک",
|
||||
graphTitle: "نمودار ترافیک",
|
||||
B: "ب",
|
||||
KB: "کب",
|
||||
MB: "مب",
|
||||
GB: "گب",
|
||||
TB: "تب",
|
||||
PB: "پب",
|
||||
p: "پ",
|
||||
Kp: "کپ",
|
||||
Mp: "مپ",
|
||||
Gp: "گپ",
|
||||
},
|
||||
date: {
|
||||
expiry: "انقضا",
|
||||
expired: "منقضی",
|
||||
d: "ر",
|
||||
h: "س",
|
||||
m: "د",
|
||||
s: "ث",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import en from './en'
|
||||
import fa from './fa'
|
||||
|
||||
|
||||
export const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: localStorage.getItem("locale") ?? 'en',
|
||||
fallbackLocale: 'en',
|
||||
messages: {
|
||||
en,
|
||||
fa,
|
||||
},
|
||||
})
|
||||
|
||||
export const languages = [
|
||||
{ title: 'English', value: 'en' },
|
||||
{ title: 'فارسی', value: 'fa' },
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* main.ts
|
||||
*
|
||||
* Bootstraps Vuetify and other plugins then mounts the App`
|
||||
*/
|
||||
|
||||
// Composables
|
||||
import { createApp, ref } from 'vue'
|
||||
|
||||
// Components
|
||||
import App from './App.vue'
|
||||
|
||||
// Use router
|
||||
import router from './router'
|
||||
|
||||
// Store
|
||||
import store from './store'
|
||||
|
||||
// Plugins
|
||||
import { registerPlugins } from '@/plugins'
|
||||
|
||||
// Locale
|
||||
import { i18n } from '@/locales'
|
||||
import Vue3PersianDatetimePicker from 'vue3-persian-datetime-picker'
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const app = createApp(App)
|
||||
app.provide('loading', loading)
|
||||
|
||||
registerPlugins(app)
|
||||
|
||||
app
|
||||
.use(router)
|
||||
.use(store)
|
||||
.use(i18n)
|
||||
.component('DatePicker', Vue3PersianDatetimePicker)
|
||||
.mount('#app')
|
||||
@@ -0,0 +1,18 @@
|
||||
import axios from 'axios'
|
||||
|
||||
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
|
||||
|
||||
axios.interceptors.request.use(
|
||||
(config) => {
|
||||
if (config.data instanceof FormData) {
|
||||
config.headers['Content-Type'] = 'multipart/form-data'
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => Promise.reject(error),
|
||||
)
|
||||
|
||||
const api = axios.create()
|
||||
|
||||
export default api
|
||||
@@ -0,0 +1,66 @@
|
||||
import api from './api'
|
||||
import { i18n } from '@/locales'
|
||||
import Message from "@/store/modules/message"
|
||||
|
||||
export interface Msg {
|
||||
success: boolean
|
||||
msg: string
|
||||
obj: any | null
|
||||
}
|
||||
|
||||
function _handleMsg(msg: any): void {
|
||||
const sb = Message()
|
||||
if (!isMsg(msg)) {
|
||||
return
|
||||
}
|
||||
if(msg.msg){
|
||||
const message = msg.success ? i18n.global.t('success') + ": " + i18n.global.t('actions.' + msg.msg) : i18n.global.t('failed') + ": " + msg.msg
|
||||
sb.showMessage(message, msg.success ? 'success' : 'error', 5000)
|
||||
}
|
||||
}
|
||||
|
||||
function _respToMsg(resp: any): Msg {
|
||||
const data = resp.data
|
||||
if (data == null) {
|
||||
return { success: true, msg: "", obj: null }
|
||||
} else if (isMsg(data)) {
|
||||
if (data.hasOwnProperty('success')) {
|
||||
return { success: data.success, msg: data.msg, obj: data.obj || null }
|
||||
} else {
|
||||
return data
|
||||
}
|
||||
} else {
|
||||
return { success: false, msg: `unknown data: ${data}`, obj: null }
|
||||
}
|
||||
}
|
||||
|
||||
function isMsg(obj: any): obj is Msg {
|
||||
return 'success' in obj && 'msg' in obj && 'obj' in obj
|
||||
}
|
||||
|
||||
const HttpUtils = {
|
||||
async get(url: string, data: object = {}, options: any[] = []): Promise<Msg> {
|
||||
let msg: Msg
|
||||
try {
|
||||
const resp = await api.get(url, { params: data, ...options })
|
||||
msg = _respToMsg(resp)
|
||||
} catch (e: any) {
|
||||
msg = { success: false, msg: e.toString(), obj: null }
|
||||
}
|
||||
_handleMsg(msg)
|
||||
return msg
|
||||
},
|
||||
async post(url: string, data: object | null, options: any = undefined): Promise<Msg> {
|
||||
let msg: Msg
|
||||
try {
|
||||
const resp = await api.post(url, data, options)
|
||||
msg = _respToMsg(resp)
|
||||
} catch (e: any) {
|
||||
msg = { success: false, msg: e.toString(), obj: null }
|
||||
}
|
||||
_handleMsg(msg)
|
||||
return msg
|
||||
},
|
||||
}
|
||||
|
||||
export default HttpUtils;
|
||||
@@ -0,0 +1,10 @@
|
||||
// Plugins
|
||||
import vuetify from './vuetify'
|
||||
|
||||
// Types
|
||||
import type { App } from 'vue'
|
||||
|
||||
export function registerPlugins (app: App) {
|
||||
app
|
||||
.use(vuetify)
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
import { Hysteria, Hysteria2, InTypes, Inbound, Naive, Shadowsocks, TUIC, Trojan, VLESS, VMess } from "@/types/inbounds"
|
||||
import { HTTP, WebSocket, QUIC, gRPC, HTTPUpgrade, Transport, TrspTypes } from "@/types/transport";
|
||||
|
||||
export interface Link {
|
||||
type: "local" | "external" | "sub"
|
||||
remark?: string
|
||||
uri: string
|
||||
}
|
||||
|
||||
function utf8ToBase64(utf8String: string): string {
|
||||
const encodedUtf8 = encodeURIComponent(utf8String).replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(parseInt(p1, 16)));
|
||||
return btoa(encodedUtf8);
|
||||
}
|
||||
|
||||
export namespace LinkUtil {
|
||||
export function linkGenerator(user: string, inbound: Inbound): string {
|
||||
const addr = location.hostname
|
||||
switch(inbound.type){
|
||||
case InTypes.Shadowsocks:
|
||||
return shadowsocksLink(user,<Shadowsocks>inbound,addr)
|
||||
case InTypes.Naive:
|
||||
return naiveLink(user,<Naive>inbound,addr)
|
||||
case InTypes.Hysteria:
|
||||
return hysteriaLink(user,<Hysteria>inbound,addr)
|
||||
case InTypes.Hysteria2:
|
||||
return hysteria2Link(user,<Hysteria2>inbound,addr)
|
||||
case InTypes.TUIC:
|
||||
return tuicLink(user,<TUIC>inbound,addr)
|
||||
case InTypes.VLESS:
|
||||
return vlessLink(user,<VLESS>inbound,addr)
|
||||
case InTypes.Trojan:
|
||||
return trojanLink(user,<Trojan>inbound,addr)
|
||||
case InTypes.VMess:
|
||||
return vmessLink(user,<VMess>inbound,addr)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function shadowsocksLink(user: string, inbound: Shadowsocks, addr: string): string {
|
||||
const userPass = inbound.users?.find(i => i.name == user)?.password
|
||||
const password = [userPass]
|
||||
if (inbound.method.startsWith('2022')) password.push(inbound.password)
|
||||
|
||||
const params = {
|
||||
tfo: inbound.tcp_fast_open? 1 : null,
|
||||
network: inbound.network?? null
|
||||
}
|
||||
|
||||
const uri = new URL(`ss://${utf8ToBase64(inbound.method + ':' + password.join(':'))}@${addr}:${inbound.listen_port}`)
|
||||
for (const [key, value] of Object.entries(params)){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
}
|
||||
}
|
||||
uri.hash = encodeURIComponent(inbound.tag)
|
||||
return uri.toString()
|
||||
}
|
||||
|
||||
function hysteriaLink(user: string, inbound: Hysteria, addr: string): string {
|
||||
const auth = inbound.users.find(i => i.name == user)?.auth_str
|
||||
const params = {
|
||||
upmbps: inbound.up_mbps?? null,
|
||||
downmbps: inbound.down_mbps?? null,
|
||||
auth: auth?? null,
|
||||
peer: inbound.tls.server_name?? null,
|
||||
alpn: inbound.tls.alpn?.join(',')?? null,
|
||||
obfsParam: inbound.obfs?? null,
|
||||
fastopen: inbound.tcp_fast_open? 1 : 0
|
||||
}
|
||||
const uri = new URL(`hysteria://${addr}:${inbound.listen_port}`)
|
||||
for (const [key, value] of Object.entries(params)){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
}
|
||||
}
|
||||
uri.hash = encodeURIComponent(inbound.tag)
|
||||
return uri.toString()
|
||||
}
|
||||
|
||||
function hysteria2Link(user: string, inbound: Hysteria2, addr: string): string {
|
||||
const password = inbound.users.find(i => i.name == user)?.password
|
||||
const params = {
|
||||
upmbps: inbound.up_mbps?? null,
|
||||
downmbps: inbound.down_mbps?? null,
|
||||
sni: inbound.tls.server_name?? null,
|
||||
alpn: inbound.tls.alpn?.join(',')?? null,
|
||||
obfs: inbound.obfs?.type?? null,
|
||||
'obfs-password': inbound.obfs?.password?? null,
|
||||
fastopen: inbound.tcp_fast_open? 1 : 0
|
||||
}
|
||||
const uri = new URL(`hysteria2://${password}@${addr}:${inbound.listen_port}`)
|
||||
for (const [key, value] of Object.entries(params)){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
}
|
||||
}
|
||||
uri.hash = encodeURIComponent(inbound.tag)
|
||||
return uri.toString()
|
||||
}
|
||||
|
||||
function naiveLink(user: string, inbound: Naive, addr: string): string {
|
||||
const password = inbound.users.find(i => i.username == user)?.password
|
||||
const params = {
|
||||
padding: 1,
|
||||
peer: inbound.tls.server_name?? null,
|
||||
alpn: inbound.tls.alpn?.join(',')?? null,
|
||||
tfo: inbound.tcp_fast_open? 1 : 0
|
||||
}
|
||||
const uri = `http2://${utf8ToBase64(user + ":" + password + "@" + addr + ":" + inbound.listen_port)}`
|
||||
const paramsArray = []
|
||||
for (const [key, value] of Object.entries(params)){
|
||||
if (value) {
|
||||
paramsArray.push(`${key}=${encodeURIComponent(value.toString())}`)
|
||||
}
|
||||
}
|
||||
return uri.toString() + "?" + paramsArray.join('&') + "#" + inbound.tag
|
||||
}
|
||||
|
||||
function tuicLink(user: string, inbound: TUIC, addr: string): string {
|
||||
const u = inbound.users.find(i => i.name == user)
|
||||
const params = {
|
||||
sni: inbound.tls.server_name?? null,
|
||||
alpn: inbound.tls.alpn?.join(',')?? null,
|
||||
congestion_control: inbound.congestion_control?? null
|
||||
}
|
||||
const uri = new URL(`tuic://${u?.uuid}:${u?.password}@${addr}:${inbound.listen_port}`)
|
||||
for (const [key, value] of Object.entries(params)){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
}
|
||||
}
|
||||
uri.hash = encodeURIComponent(inbound.tag)
|
||||
return uri.toString()
|
||||
}
|
||||
|
||||
function getTransportParams(t:Transport): any {
|
||||
if (Object.keys(t).length == 0) return {}
|
||||
|
||||
const params = {
|
||||
host: <string|null>'',
|
||||
path: <string|null>'',
|
||||
serviceName: <string|null>'',
|
||||
}
|
||||
switch (t.type){
|
||||
case TrspTypes.HTTP:
|
||||
const th = <HTTP>t
|
||||
params.host = th.host?.join(',')?? null
|
||||
params.path = th.path?? null
|
||||
break
|
||||
case TrspTypes.WebSocket:
|
||||
const tw = <WebSocket>t
|
||||
params.path = tw.path?? null
|
||||
break
|
||||
case TrspTypes.gRPC:
|
||||
const tg = <gRPC>t
|
||||
params.serviceName = tg.service_name?? null
|
||||
break
|
||||
case TrspTypes.HTTPUpgrade:
|
||||
const tu = <HTTPUpgrade>t
|
||||
params.host = tu.host?? null
|
||||
params.path = tu.path?? null
|
||||
break
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
function vlessLink(user: string, inbound: VLESS, addr: string): string {
|
||||
const u = inbound.users.find(i => i.name == user)
|
||||
const transport = <Transport>inbound.transport
|
||||
|
||||
const tParams = getTransportParams(transport)
|
||||
|
||||
const params = {
|
||||
type: transport?.type?? 'none',
|
||||
security: inbound.tls?.enabled? 'tls' : null,
|
||||
alpn: inbound.tls?.alpn?.join(',')?? null,
|
||||
sni: inbound.tls?.server_name?? null,
|
||||
flow: inbound.tls?.enabled ? u?.flow?? null : null
|
||||
}
|
||||
const uri = new URL(`vless://${u?.uuid}@${addr}:${inbound.listen_port}`)
|
||||
for (const [key, value] of Object.entries({...params, ...tParams})){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
}
|
||||
}
|
||||
uri.hash = encodeURIComponent(inbound.tag)
|
||||
return uri.toString()
|
||||
}
|
||||
|
||||
function trojanLink(user: string, inbound: Trojan, addr: string): string {
|
||||
const u = inbound.users.find(i => i.name == user)
|
||||
const transport = <Transport>inbound.transport
|
||||
|
||||
const tParams = getTransportParams(transport)
|
||||
|
||||
const params = {
|
||||
type: transport?.type?? 'none',
|
||||
security: inbound.tls?.enabled? 'tls' : null,
|
||||
alpn: inbound.tls?.alpn?.join(',')?? null,
|
||||
sni: inbound.tls?.server_name?? null,
|
||||
}
|
||||
const uri = new URL(`trojan://${u?.password}@${addr}:${inbound.listen_port}`)
|
||||
for (const [key, value] of Object.entries({...params, ...tParams})){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
}
|
||||
}
|
||||
uri.hash = encodeURIComponent(inbound.tag)
|
||||
return uri.toString()
|
||||
}
|
||||
|
||||
function vmessLink(user: string, inbound: VMess, addr: string): string {
|
||||
const u = inbound.users.find(i => i.name == user)
|
||||
const transport = <Transport>inbound.transport
|
||||
|
||||
const tParams = getTransportParams(transport)
|
||||
if (transport.type == TrspTypes.gRPC) tParams.path = tParams.serviceName
|
||||
|
||||
const params = {
|
||||
v: 2,
|
||||
add: addr,
|
||||
aid: u?.alterId,
|
||||
host: tParams.host,
|
||||
id: u?.uuid,
|
||||
net: transport.type,
|
||||
path: tParams.path,
|
||||
port: inbound.listen_port,
|
||||
ps: inbound.tag,
|
||||
sni: inbound.tls.server_name?? '',
|
||||
tls: Object.keys(inbound.tls).length>0? 'tls' : 'none'
|
||||
}
|
||||
return 'vmess://' + utf8ToBase64(JSON.stringify(params))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
const seq = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
|
||||
|
||||
const RandomUtil = {
|
||||
randomIntRange(min: number, max: number): number {
|
||||
return parseInt((Math.random() * (max - min) + min).toString(), 10)
|
||||
},
|
||||
randomInt(n: number) {
|
||||
return this.randomIntRange(0, n)
|
||||
},
|
||||
randomSeq(count: number): string {
|
||||
let str = ''
|
||||
for (let i = 0; i < count; ++i) {
|
||||
str += seq[this.randomInt(62)]
|
||||
}
|
||||
return str
|
||||
},
|
||||
randomLowerAndNum(count: number): string {
|
||||
let str = ''
|
||||
for (let i = 0; i < count; ++i) {
|
||||
str += seq[this.randomInt(36)]
|
||||
}
|
||||
return str
|
||||
},
|
||||
randomUUID(): string {
|
||||
let d = new Date().getTime()
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
let r = (d + Math.random() * 16) % 16 | 0
|
||||
d = Math.floor(d / 16)
|
||||
return (c === 'x' ? r : (r & 0x7 | 0x8)).toString(16)
|
||||
})
|
||||
},
|
||||
randomShadowsocksPassword(n: number): string {
|
||||
const array = new Uint8Array(n)
|
||||
window.crypto.getRandomValues(array)
|
||||
return btoa(String.fromCharCode(...array))
|
||||
},
|
||||
randomShortId(): string[] {
|
||||
let shortIds = ['','','','']
|
||||
for (var ii = 0; ii < 4; ii++) {
|
||||
for (var jj = 0; jj < this.randomInt(8); jj++){
|
||||
let randomNum = this.randomInt(256)
|
||||
shortIds[ii] += ('0' + randomNum.toString(16)).slice(-2)
|
||||
}
|
||||
}
|
||||
return shortIds
|
||||
}
|
||||
}
|
||||
|
||||
export default RandomUtil
|
||||
@@ -0,0 +1,153 @@
|
||||
import { i18n } from "@/locales"
|
||||
|
||||
type OBJ = {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export const FindDiff = {
|
||||
Config(obj1: OBJ, obj2: OBJ): any[] {
|
||||
const differences: any[] = []
|
||||
|
||||
if(!obj2){
|
||||
return [ { key: "all", obj: obj1 } ]
|
||||
}
|
||||
|
||||
for (const key in obj1) {
|
||||
if (obj2.hasOwnProperty(key)) {
|
||||
const value1 = obj1[key]
|
||||
const value2 = obj2[key]
|
||||
|
||||
if (Array.isArray(value1)){
|
||||
value1.forEach((v1,index) => {
|
||||
if(index >= value2.length){
|
||||
differences.push({key: key, action: "new", index: index, obj: v1})
|
||||
} else if(!this.deepCompare(v1,value2[index])) {
|
||||
differences.push({key: key, action: "edit", index: index, obj: v1})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if (!this.deepCompare(value1,value2)) {
|
||||
differences.push({ key: key, action: "set", obj: value1})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
differences.push({ key: key, action: "set", obj: obj1[key]})
|
||||
}
|
||||
}
|
||||
|
||||
return differences
|
||||
},
|
||||
Clients(value1: any[], value2: any[]): any {
|
||||
const differences: any[] = []
|
||||
value1.forEach((v1,index) => {
|
||||
if(index >= value2.length) differences.push({key: "clients", action: "new", obj: v1})
|
||||
else if(!this.deepCompare(v1,value2[index])) differences.push({key: "clients", action: "edit", obj: v1})
|
||||
})
|
||||
return differences
|
||||
},
|
||||
Settings(obj1: OBJ, obj2: OBJ): any {
|
||||
const differences: any[] = []
|
||||
for (const key in obj1) {
|
||||
if (obj1[key] != obj2[key]) {
|
||||
differences.push({ key: key, action: "set", obj: obj1[key]})
|
||||
}
|
||||
}
|
||||
return differences
|
||||
},
|
||||
deepCompare(obj1: any, obj2: any): boolean {
|
||||
// Check if the types of both objects are the same
|
||||
if (typeof obj1 !== typeof obj2) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if both objects are arrays
|
||||
if (Array.isArray(obj1) && Array.isArray(obj2)) {
|
||||
if (obj1.length !== obj2.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (let i = 0; i < obj1.length; i++) {
|
||||
if (!this.deepCompare(obj1[i], obj2[i])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if both objects are plain objects
|
||||
if (typeof obj1 === 'object' && typeof obj2 === 'object' && obj1 !== null && obj2 !== null) {
|
||||
const keys1 = Object.keys(obj1)
|
||||
const keys2 = Object.keys(obj2)
|
||||
|
||||
if (keys1.length !== keys2.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (const key of keys1) {
|
||||
if (!keys2.includes(key) || !this.deepCompare(obj1[key], obj2[key])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Check primitive values
|
||||
return obj1 === obj2
|
||||
}
|
||||
}
|
||||
|
||||
const ONE_KB = 1024
|
||||
const ONE_MB = ONE_KB * 1024
|
||||
const ONE_GB = ONE_MB * 1024
|
||||
const ONE_TB = ONE_GB * 1024
|
||||
const ONE_PB = ONE_TB * 1024
|
||||
|
||||
export const HumanReadable = {
|
||||
sizeFormat(size:number, fix:number=2) {
|
||||
if (!size || size<0) return "-"
|
||||
if (size < ONE_KB) {
|
||||
return size.toFixed(0) + " " + i18n.global.t('stats.B')
|
||||
} else if (size < ONE_MB) {
|
||||
return (size / ONE_KB).toFixed(fix) + " " + i18n.global.t('stats.KB')
|
||||
} else if (size < ONE_GB) {
|
||||
return (size / ONE_MB).toFixed(fix) + " " + i18n.global.t('stats.MB')
|
||||
} else if (size < ONE_TB) {
|
||||
return (size / ONE_GB).toFixed(fix) + " " + i18n.global.t('stats.GB')
|
||||
} else if (size < ONE_PB) {
|
||||
return (size / ONE_TB).toFixed(fix) + " " + i18n.global.t('stats.TB')
|
||||
} else {
|
||||
return (size / ONE_PB).toFixed(fix) + " " + i18n.global.t('stats.PB')
|
||||
}
|
||||
},
|
||||
packetFormat(size:number, fix:number=2) {
|
||||
if (!size || size<0) return "-"
|
||||
if (size < 1000) {
|
||||
return size.toFixed(0) + " " + i18n.global.t('stats.p')
|
||||
} else if (size < 1000000) {
|
||||
return (size / 1000).toFixed(fix) + " " + i18n.global.t('stats.Kp')
|
||||
} else if (size < 1000000000) {
|
||||
return (size / 1000000).toFixed(fix) + " " + i18n.global.t('stats.Mp')
|
||||
} else {
|
||||
return (size / 1000000000).toFixed(fix) + " " + i18n.global.t('stats.Gp')
|
||||
}
|
||||
},
|
||||
formatSecond(second:number): string {
|
||||
if (!second || second<0) return "-"
|
||||
if (second < 60) {
|
||||
return second.toFixed(0) + i18n.global.t('date.s')
|
||||
} else if (second < 3600) {
|
||||
return (second / 60).toFixed(0) + i18n.global.t('date.m')
|
||||
} else if (second < 3600 * 24) {
|
||||
return (second / 3600).toFixed(0) + i18n.global.t('date.h')
|
||||
}
|
||||
const day = Math.floor(second / 3600 / 24)
|
||||
const remain = Math.floor((second/3600) - (day*24))
|
||||
return day + i18n.global.t('date.d') + (remain > 0 ? ' ' + remain + i18n.global.t('date.h') : '')
|
||||
},
|
||||
remainedDays(exp:number): number|null {
|
||||
if (exp == 0) return -1
|
||||
const now = Date.now()/1000
|
||||
if (exp < now) return null
|
||||
return Math.floor((exp - now) / (3600*24))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* plugins/vuetify.ts
|
||||
*
|
||||
* Framework documentation: https://vuetifyjs.com`
|
||||
*/
|
||||
|
||||
// Styles
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
import 'vuetify/styles'
|
||||
|
||||
import colors from 'vuetify/util/colors'
|
||||
import { fa, en } from 'vuetify/locale'
|
||||
|
||||
// Composables
|
||||
import { createVuetify } from 'vuetify'
|
||||
|
||||
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
|
||||
export default createVuetify({
|
||||
defaults: {
|
||||
VRow: { dense: true } // Apply dense to v-row as default
|
||||
},
|
||||
theme: {
|
||||
defaultTheme: localStorage.getItem('theme') ?? 'light',
|
||||
themes: {
|
||||
light: {
|
||||
colors: {
|
||||
primary: '#1867C0',
|
||||
secondary: '#5CBBF6',
|
||||
tertiary: '#E57373',
|
||||
accent: '#005CAF',
|
||||
error: colors.red.accent3,
|
||||
warning: colors.amber.base,
|
||||
info: colors.teal.darken1,
|
||||
success: colors.green.base,
|
||||
background: colors.grey.lighten4,
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
colors: {
|
||||
primary: colors.blue.darken4,
|
||||
secondary: colors.grey.darken3,
|
||||
accent: colors.pink.darken3,
|
||||
error: colors.red.accent3,
|
||||
warning: colors.amber.darken3,
|
||||
info: colors.teal.lighten1,
|
||||
success: colors.green.darken2,
|
||||
surface: colors.grey.darken3,
|
||||
background: colors.grey.darken4,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
locale: {
|
||||
locale: localStorage.getItem("locale") ?? 'en',
|
||||
fallback: 'en',
|
||||
messages: { en, fa },
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,98 @@
|
||||
// Composables
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Login from '@/views/Login.vue'
|
||||
import Data from '@/store/modules/data'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'pages.login',
|
||||
component: Login,
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/layouts/default/Default.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'pages.home',
|
||||
component: () => import('@/views/Home.vue'),
|
||||
},
|
||||
{
|
||||
path: '/inbounds',
|
||||
name: 'pages.inbounds',
|
||||
component: () => import('@/views/Inbounds.vue'),
|
||||
},
|
||||
{
|
||||
path: '/clients',
|
||||
name: 'pages.clients',
|
||||
component: () => import('@/views/Clients.vue'),
|
||||
},
|
||||
{
|
||||
path: '/outbounds',
|
||||
name: 'pages.outbounds',
|
||||
component: () => import('@/views/Outbounds.vue'),
|
||||
},
|
||||
{
|
||||
path: '/rules',
|
||||
name: 'pages.rules',
|
||||
component: () => import('@/views/Rules.vue'),
|
||||
},
|
||||
{
|
||||
path: '/basics',
|
||||
name: 'pages.basics',
|
||||
component: () => import('@/views/Basics.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'pages.settings',
|
||||
component: () => import('@/views/Settings.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
routes,
|
||||
})
|
||||
|
||||
const DEFAULT_TITLE = 'S-UI'
|
||||
let intervalId:any
|
||||
|
||||
// Navigation guard to check authentication state
|
||||
router.beforeEach((to, from, next) => {
|
||||
// Check the session cookie
|
||||
const sessionCookie = document.cookie.split(';').find(cookie => cookie.trim().startsWith('session='))
|
||||
const isAuthenticated = !!sessionCookie
|
||||
|
||||
// If the route requires authentication and the user is not authenticated, redirect to /login
|
||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||
next('/login')
|
||||
} else if (to.path === '/login' && isAuthenticated) {
|
||||
// If already authenticated and visiting /route, redirect to '/'
|
||||
next('/')
|
||||
} else {
|
||||
// Load default data
|
||||
if(to.path != '/login'){
|
||||
loadDataInterval()
|
||||
} else {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
intervalId = undefined
|
||||
}
|
||||
}
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
const loadDataInterval = () => {
|
||||
if (intervalId) return
|
||||
Data().loadData()
|
||||
intervalId = setInterval(() => {
|
||||
Data().loadData()
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
export default pinia
|
||||
@@ -0,0 +1,67 @@
|
||||
import { FindDiff } from '@/plugins/utils'
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
import { defineStore } from 'pinia'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const Data = defineStore('Data', {
|
||||
state: () => ({
|
||||
lastLoad: 0,
|
||||
reloadItems: localStorage.getItem("reloadItems")?.split(',')?? <string[]>[],
|
||||
subURI: "",
|
||||
onlines: {inbound: <string[]>[], outbound: <string[]>[], user: <string[]>[]},
|
||||
oldData: <{config: any, clients: any[]}>{},
|
||||
config: {},
|
||||
clients: [],
|
||||
}),
|
||||
actions: {
|
||||
async loadData() {
|
||||
const msg = await HttpUtils.get('/api/load', this.lastLoad >0 ? {lu: this.lastLoad} : {} )
|
||||
if(msg.success) {
|
||||
this.lastLoad = Math.floor((new Date()).getTime()/1000)
|
||||
|
||||
// Set new data
|
||||
const data = JSON.parse(msg.obj)
|
||||
if (data.config) this.config = data.config
|
||||
if (data.clients) this.clients = data.clients
|
||||
if (data.subURI) this.subURI = data.subURI
|
||||
this.onlines = data.onlines
|
||||
|
||||
// To avoid ref copy
|
||||
if (data.config) this.oldData.config = { ...JSON.parse(msg.obj).config }
|
||||
if (data.clients) this.oldData.clients = [ ...JSON.parse(msg.obj).clients ]
|
||||
}
|
||||
},
|
||||
async pushData() {
|
||||
const diff = {
|
||||
config: JSON.stringify(FindDiff.Config(this.config,this.oldData.config)),
|
||||
clients: JSON.stringify(FindDiff.Clients(this.clients,this.oldData.clients)),
|
||||
}
|
||||
const msg = await HttpUtils.post('/api/save',diff)
|
||||
if(msg.success) {
|
||||
this.loadData()
|
||||
}
|
||||
},
|
||||
async delInbound(index: number) {
|
||||
const diff = {
|
||||
config: JSON.stringify([{key: "inbounds", action: "del", index: index, obj: null}]),
|
||||
clients: JSON.stringify(FindDiff.Clients(this.clients,this.oldData.clients)),
|
||||
}
|
||||
const msg = await HttpUtils.post('/api/save',diff)
|
||||
if(msg.success) {
|
||||
this.loadData()
|
||||
}
|
||||
},
|
||||
async delClient(id: number) {
|
||||
const diff = {
|
||||
config: JSON.stringify(FindDiff.Config(this.config,this.oldData.config)),
|
||||
clients:JSON.stringify([{key: "clients", action: "del", index: id, obj: null}]),
|
||||
}
|
||||
const msg = await HttpUtils.post('/api/save',diff)
|
||||
if(msg.success) {
|
||||
this.loadData()
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export default Data
|
||||
@@ -0,0 +1,22 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
const Message = defineStore('msg', {
|
||||
state: () => ({
|
||||
showMsg: false,
|
||||
snackbar: {
|
||||
message: '',
|
||||
timeout: 5000,
|
||||
color: '',
|
||||
}
|
||||
}),
|
||||
actions: {
|
||||
showMessage(message:string, color='success',timeout=5000) {
|
||||
this.snackbar.message = message
|
||||
this.snackbar.color = color
|
||||
this.snackbar.timeout = timeout
|
||||
this.showMsg = true
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export default Message
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* src/styles/settings.scss
|
||||
*
|
||||
* Configures SASS variables and Vuetify overwrites
|
||||
*/
|
||||
|
||||
// https://vuetifyjs.com/features/sass-variables/`
|
||||
// @use 'vuetify/settings' with (
|
||||
// $color-pack: false
|
||||
// );
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Vazirmatn';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('@/assets/Vazirmatn-UI-NL-Regular.woff2') format('woff2');
|
||||
unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC, U+0030-0039;
|
||||
}
|
||||
$body-font-family: "Vazirmatn";
|
||||
|
||||
$typoOptions: text-h1, text-sm-h1, text-md-h1, text-lg-h1, text-h2, text-sm-h2,
|
||||
text-md-h2, text-lg-h2, text-h3, text-sm-h3, text-md-h3, text-lg-h3, text-h4,
|
||||
text-sm-h4, text-md-h4, text-lg-h4, text-h5, text-sm-h5, text-md-h5,
|
||||
text-lg-h5, text-h6, text-sm-h6, text-md-h6, text-lg-h6, headline, title,
|
||||
subtitle-1, subtitle-2, text-body-1, text-sm-body-1, text-md-body-1,
|
||||
text-lg-body-1, text-body-2, text-sm-body-2, text-md-body-2, text-lg-body-2,
|
||||
text-caption;
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, $body-font-family, sans-serif !important;
|
||||
@each $typoOption in $typoOptions {
|
||||
.#{$typoOption} {
|
||||
font-family: -apple-system, BlinkMacSystemFont, $body-font-family, sans-serif !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-btn {
|
||||
letter-spacing: 0;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import RandomUtil from "@/plugins/randomUtil"
|
||||
|
||||
export interface Client {
|
||||
id?: number
|
||||
enable: boolean
|
||||
name: string
|
||||
config: string
|
||||
inbounds: string
|
||||
links: string
|
||||
volume: number
|
||||
expiry: number
|
||||
up: number
|
||||
down: number
|
||||
}
|
||||
|
||||
const defaultClient: Client = {
|
||||
enable: true,
|
||||
name: "",
|
||||
config: "[]",
|
||||
inbounds: "",
|
||||
links: "[]",
|
||||
volume: 0,
|
||||
expiry: 0,
|
||||
up: 0,
|
||||
down: 0,
|
||||
}
|
||||
|
||||
type Config = {
|
||||
[key: string]: {
|
||||
name?: string
|
||||
username?: string
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
|
||||
export function updateConfigs(configs: string, newUserName: string): string {
|
||||
const updatedConfigs: Config = JSON.parse(configs)
|
||||
|
||||
for (const key in updatedConfigs) {
|
||||
if (updatedConfigs.hasOwnProperty(key)) {
|
||||
const config = updatedConfigs[key]
|
||||
if (config.hasOwnProperty("name")) {
|
||||
config.name = newUserName
|
||||
} else if (config.hasOwnProperty("username")) {
|
||||
config.username = newUserName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(updatedConfigs)
|
||||
}
|
||||
|
||||
export function randomConfigs(user: string): Config {
|
||||
const mixedPassword = RandomUtil.randomSeq(10)
|
||||
const ssPassword = RandomUtil.randomShadowsocksPassword(32)
|
||||
const uuid = RandomUtil.randomUUID()
|
||||
return {
|
||||
mixed: {
|
||||
username: user,
|
||||
password: mixedPassword,
|
||||
},
|
||||
socks: {
|
||||
username: user,
|
||||
password: mixedPassword,
|
||||
},
|
||||
http: {
|
||||
username: user,
|
||||
password: mixedPassword,
|
||||
},
|
||||
shadowsocks: {
|
||||
name: user,
|
||||
password: ssPassword,
|
||||
},
|
||||
shadowtls: {
|
||||
name: user,
|
||||
password: ssPassword,
|
||||
},
|
||||
vmess: {
|
||||
name: user,
|
||||
uuid: uuid,
|
||||
alterId: 0,
|
||||
},
|
||||
vless: {
|
||||
name: user,
|
||||
uuid: uuid,
|
||||
flow: "xtls-rprx-vision",
|
||||
},
|
||||
trojan: {
|
||||
name: user,
|
||||
password: mixedPassword,
|
||||
},
|
||||
naive: {
|
||||
username: user,
|
||||
password: mixedPassword,
|
||||
},
|
||||
hysteria: {
|
||||
name: user,
|
||||
auth_str: mixedPassword,
|
||||
},
|
||||
tuic: {
|
||||
name: user,
|
||||
uuid: uuid,
|
||||
password: mixedPassword,
|
||||
},
|
||||
hysteria2: {
|
||||
name: user,
|
||||
password: mixedPassword,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function createClient<T extends Client>(json?: Partial<T>): Client {
|
||||
const defaultObject: Client = { ...defaultClient, ...(json || {}) }
|
||||
return defaultObject
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Inbound } from './inbounds'
|
||||
import { Dial, Outbound } from './outbounds'
|
||||
|
||||
interface Log {
|
||||
disabled?: boolean
|
||||
level?: string
|
||||
output?: string
|
||||
timestamp?: boolean
|
||||
}
|
||||
|
||||
interface Dns {
|
||||
servers: DnsServer[]
|
||||
final?: string
|
||||
strategy?: string
|
||||
}
|
||||
|
||||
interface DnsServer{
|
||||
tag?: string,
|
||||
address: string,
|
||||
address_resolver?: string,
|
||||
address_strategy?: string,
|
||||
strategy?: string,
|
||||
detour?: string
|
||||
}
|
||||
|
||||
export interface Ntp extends Dial{
|
||||
enabled?: boolean
|
||||
server: string
|
||||
server_port?: number
|
||||
interval?: string
|
||||
}
|
||||
|
||||
interface Route {
|
||||
rules: RouteRule[] | RouteRuleLogical[]
|
||||
rule_set: RouteRuleSet[]
|
||||
final?: string,
|
||||
auto_detect_interface?: boolean
|
||||
default_interface?: string
|
||||
default_mark?: number
|
||||
}
|
||||
|
||||
interface RouteRule {
|
||||
inbound?: string[] | string
|
||||
ip_version?: 4 | 6,
|
||||
network?: "tcp" | "udp"
|
||||
auth_user?: string[]
|
||||
protocol?: string[] | string
|
||||
domain?: string[] | string
|
||||
domain_suffix?: string[] | string
|
||||
domain_keyword?: string[] | string
|
||||
domain_regex?: string[] | string
|
||||
source_ip_cidr?: string[] | string
|
||||
source_ip_is_private?: boolean
|
||||
ip_cidr?: string[] | string
|
||||
ip_is_private?: boolean
|
||||
source_port?: number[] | number
|
||||
source_port_range?: string[] | string
|
||||
port?: number[] | number
|
||||
port_range?: string[] | string
|
||||
clash_mode?: string
|
||||
rule_set?: string[] | string
|
||||
invert?: boolean
|
||||
outbound: string
|
||||
}
|
||||
|
||||
interface RouteRuleLogical {
|
||||
type: "logical"
|
||||
mode: "and" | "or"
|
||||
rules: RouteRule[]
|
||||
invert?: boolean
|
||||
outbound: string
|
||||
}
|
||||
|
||||
interface RouteRuleSet {
|
||||
type: string
|
||||
tag: string
|
||||
format: string
|
||||
path?: string
|
||||
url?: string
|
||||
download_detour?: string
|
||||
update_interval?: string
|
||||
}
|
||||
|
||||
interface Experimental {
|
||||
cache_file?: CacheFile
|
||||
clash_api?: ClashApi
|
||||
v2ray_api: V2rayApi
|
||||
}
|
||||
|
||||
interface CacheFile {
|
||||
enabled?: boolean
|
||||
path?: string
|
||||
cache_id?: string
|
||||
store_fakeip?: boolean
|
||||
}
|
||||
|
||||
interface V2rayApi {
|
||||
listen: string
|
||||
stats: V2rayApiStats
|
||||
}
|
||||
|
||||
export interface V2rayApiStats {
|
||||
enabled: boolean
|
||||
inbounds: string[]
|
||||
outbounds: string[]
|
||||
users: string[]
|
||||
}
|
||||
|
||||
interface ClashApi {
|
||||
external_controller?: string
|
||||
external_ui?: string
|
||||
external_ui_download_url?: string
|
||||
external_ui_download_detour?: string
|
||||
secret?: string
|
||||
default_mode?: string
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
log: Log
|
||||
dns: Dns
|
||||
ntp?: Ntp
|
||||
inbounds: Inbound[]
|
||||
outbounds: Outbound[]
|
||||
route: Route
|
||||
experimental: Experimental
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export interface Dial {
|
||||
detour?: string
|
||||
bind_interface?: string
|
||||
inet4_bind_address?: string
|
||||
inet6_bind_address?:string
|
||||
routing_mark?: number
|
||||
reuse_addr?: boolean
|
||||
connect_timeout?: string
|
||||
tcp_fast_open?: boolean
|
||||
tcp_multi_path?: boolean
|
||||
udp_fragment?: boolean
|
||||
domain_strategy?: string
|
||||
fallback_delay?: string
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
interface Brutal {
|
||||
enabled: boolean
|
||||
up_mbps: number
|
||||
down_mbps: number
|
||||
}
|
||||
|
||||
export interface iMultiplex{
|
||||
enabled: boolean
|
||||
padding?: boolean
|
||||
brutal?: Brutal
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export interface iTls {
|
||||
enabled?: boolean
|
||||
server_name?: string
|
||||
alpn?: string[]
|
||||
min_version?: string
|
||||
max_version?: string
|
||||
cipher_suites?: string[]
|
||||
certificate?: string[]
|
||||
certificate_path?: string
|
||||
key?: string[]
|
||||
key_path?: string
|
||||
}
|
||||
|
||||
export const defaultInTls: iTls = {
|
||||
alpn: ['HTTP/3', 'HTTP/2', 'HTTP/1.1'],
|
||||
min_version: "1.2",
|
||||
max_version: "1.3",
|
||||
cipher_suites: [""],
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
import { iMultiplex } from "./inMultiplex"
|
||||
import { iTls } from "./inTls"
|
||||
import { Dial } from "./outbounds"
|
||||
import { Transport } from "./transport"
|
||||
|
||||
export const InTypes = {
|
||||
Direct: 'direct',
|
||||
Mixed: 'mixed',
|
||||
SOCKS: 'socks',
|
||||
HTTP: 'http',
|
||||
Shadowsocks: 'shadowsocks',
|
||||
VMess: 'vmess',
|
||||
Trojan: 'trojan',
|
||||
Naive: 'naive',
|
||||
Hysteria: 'hysteria',
|
||||
ShadowTLS: 'shadowtls',
|
||||
TUIC: 'tuic',
|
||||
Hysteria2: 'hysteria2',
|
||||
VLESS: 'vless',
|
||||
// Tun: 'tun',
|
||||
Redirect: 'redirect',
|
||||
TProxy: 'tproxy',
|
||||
}
|
||||
|
||||
type InType = typeof InTypes[keyof typeof InTypes]
|
||||
|
||||
export interface Listen {
|
||||
listen: string
|
||||
listen_port: number
|
||||
tcp_fast_open?: boolean
|
||||
tcp_multi_path?: boolean
|
||||
udp_fragment?: boolean
|
||||
udp_timeout?: string
|
||||
detour?: string
|
||||
sniff?: boolean
|
||||
sniff_override_destination?: boolean
|
||||
sniff_timeout?: string
|
||||
domain_strategy?: string
|
||||
}
|
||||
|
||||
interface InboundBasics extends Listen {
|
||||
type: InType
|
||||
tag: string
|
||||
}
|
||||
|
||||
interface UsernamePass {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
interface NamePass {
|
||||
name: string
|
||||
password: string
|
||||
}
|
||||
interface NameUUID {
|
||||
name: string
|
||||
uuid: string
|
||||
}
|
||||
interface NameAuth {
|
||||
name: string
|
||||
auth_str: string
|
||||
}
|
||||
interface VmessUser extends NameUUID {
|
||||
alterId: number
|
||||
}
|
||||
interface VlessUser extends NameUUID {
|
||||
flow: string
|
||||
}
|
||||
interface TuicUser extends NameUUID {
|
||||
password?: string
|
||||
}
|
||||
|
||||
interface ShadowTLSHandShake extends Dial {
|
||||
server: string
|
||||
server_port: number
|
||||
}
|
||||
|
||||
export interface Direct extends InboundBasics {
|
||||
network?: "udp" | "tcp"
|
||||
override_address?: string
|
||||
override_port?: number
|
||||
}
|
||||
export interface Mixed extends InboundBasics {
|
||||
users?: UsernamePass[]
|
||||
}
|
||||
export interface SOCKS extends InboundBasics {
|
||||
users?: UsernamePass[]
|
||||
}
|
||||
export interface HTTP extends InboundBasics {
|
||||
users?: UsernamePass[]
|
||||
tls?: iTls,
|
||||
}
|
||||
export interface Shadowsocks extends InboundBasics {
|
||||
method: string
|
||||
password: string
|
||||
network?: "udp" | "tcp"
|
||||
users?: NamePass[]
|
||||
multiplex?: iMultiplex
|
||||
}
|
||||
export interface VMess extends InboundBasics {
|
||||
users: VmessUser[]
|
||||
tls: iTls
|
||||
multiplex?: iMultiplex
|
||||
transport?: Transport
|
||||
}
|
||||
export interface Trojan extends InboundBasics {
|
||||
users: NamePass[]
|
||||
tls: iTls
|
||||
fallback?: {
|
||||
server: string
|
||||
server_port: number
|
||||
}
|
||||
multiplex?: iMultiplex
|
||||
transport?: Transport
|
||||
}
|
||||
export interface Naive extends InboundBasics {
|
||||
users: UsernamePass[]
|
||||
tls: iTls,
|
||||
}
|
||||
export interface Hysteria extends InboundBasics {
|
||||
up_mbps: number
|
||||
down_mbps: number
|
||||
obfs?: {
|
||||
type?: "salamander"
|
||||
password?: string
|
||||
}
|
||||
users: NameAuth[]
|
||||
recv_window_conn?: number
|
||||
recv_window_client?: number
|
||||
max_conn_client?: number
|
||||
disable_mtu_discovery?: boolean
|
||||
tls: iTls
|
||||
}
|
||||
export interface ShadowTLS extends InboundBasics {
|
||||
version: 1|2|3
|
||||
password?: string
|
||||
users?: NamePass[]
|
||||
handshake: ShadowTLSHandShake
|
||||
handshake_for_server_name?: {
|
||||
[server_name: string]: ShadowTLSHandShake
|
||||
}
|
||||
strict_mode?: boolean
|
||||
}
|
||||
export interface VLESS extends InboundBasics {
|
||||
users: VlessUser[]
|
||||
tls?: iTls
|
||||
multiplex?: iMultiplex
|
||||
transport?: Transport
|
||||
}
|
||||
export interface TUIC extends InboundBasics {
|
||||
users: TuicUser[]
|
||||
congestion_control: ""|"cubic"|"new_reno"|"bbr"
|
||||
auth_timeout?: string
|
||||
zero_rtt_handshake?: boolean
|
||||
heartbeat?: string
|
||||
tls: iTls
|
||||
}
|
||||
export interface Hysteria2 extends InboundBasics {
|
||||
up_mbps?: number
|
||||
down_mbps?: number
|
||||
obfs?: {
|
||||
type?: "salamander"
|
||||
password: string
|
||||
}
|
||||
users: NamePass[]
|
||||
ignore_client_bandwidth?: boolean
|
||||
tls: iTls
|
||||
masquerade?: string
|
||||
brutal_debug?: boolean
|
||||
}
|
||||
export interface Tun extends InboundBasics {
|
||||
[otherProperties: string]: any
|
||||
}
|
||||
export interface Redirect extends InboundBasics {}
|
||||
export interface TProxy extends InboundBasics {
|
||||
network?: "udp" | "tcp"
|
||||
}
|
||||
|
||||
// Create interfaces dynamically based on InTypes keys
|
||||
type InterfaceMap = {
|
||||
direct: Direct
|
||||
mixed: Mixed
|
||||
socks: SOCKS
|
||||
http: SOCKS
|
||||
shadowsocks: Shadowsocks
|
||||
vmess: VMess
|
||||
trojan: Trojan
|
||||
naive: Naive
|
||||
hysteria: Hysteria
|
||||
shadowtls: ShadowTLS
|
||||
tuic: TUIC
|
||||
hysteria2: Hysteria2
|
||||
vless: VLESS
|
||||
// tun: Tun
|
||||
redirect: Redirect
|
||||
tproxy: TProxy
|
||||
}
|
||||
|
||||
// Create union type from InterfaceMap
|
||||
export type Inbound = InterfaceMap[keyof InterfaceMap]
|
||||
|
||||
type userEnabledTypes = {
|
||||
mixed: Mixed
|
||||
socks: SOCKS
|
||||
http: SOCKS
|
||||
shadowsocks: Shadowsocks
|
||||
vmess: VMess
|
||||
trojan: Trojan
|
||||
naive: Naive
|
||||
hysteria: Hysteria
|
||||
shadowtls: ShadowTLS
|
||||
tuic: TUIC
|
||||
hysteria2: Hysteria2
|
||||
vless: VLESS
|
||||
}
|
||||
|
||||
// Create union type from userEnabledTypes
|
||||
export type InboundWithUser = userEnabledTypes[keyof userEnabledTypes]
|
||||
|
||||
// Create defaultValues object dynamically
|
||||
const defaultValues: Record<InType, Inbound> = {
|
||||
direct: <Direct>{ type: InTypes.Direct },
|
||||
mixed: <Mixed>{ type: InTypes.Mixed },
|
||||
socks: <SOCKS>{ type: InTypes.SOCKS },
|
||||
http: <HTTP>{ type: InTypes.HTTP, tls: {} },
|
||||
shadowsocks: <Shadowsocks>{ type: InTypes.Shadowsocks, method: 'none', multiplex: {} },
|
||||
vmess: <VMess>{ type: InTypes.VMess, users: <VmessUser[]>[], tls: {}, multiplex: {}, transport: {} },
|
||||
trojan: <Trojan>{ type: InTypes.Trojan, users: <NamePass[]>[], tls: {}, multiplex: {}, transport: {} },
|
||||
naive: <Naive>{ type: InTypes.Naive, users: <UsernamePass[]>[], tls: { enabled: true } },
|
||||
hysteria: <Hysteria>{ type: InTypes.Hysteria, users: <NameAuth[]>[], up_mbps: 100, down_mbps: 100, tls: { enabled: true } },
|
||||
shadowtls: <ShadowTLS>{ type: InTypes.ShadowTLS, version: 3, users: <NamePass[]>[], handshake: {}, handshake_for_server_name: {} },
|
||||
tuic: <TUIC>{ type: InTypes.TUIC, users: <TuicUser[]>[], congestion_control: "cubic", tls: { enabled: true } },
|
||||
hysteria2: <Hysteria2>{ type: InTypes.Hysteria2, users: <NamePass[]>[], tls: { enabled: true } },
|
||||
vless: <VLESS>{ type: InTypes.VLESS, users: <VlessUser[]>[], tls: {}, multiplex: {}, transport: {} },
|
||||
// tun: <Tun>{ type: InTypes.Tun },
|
||||
redirect: <Redirect>{ type: InTypes.Redirect },
|
||||
tproxy: <TProxy>{ type: InTypes.TProxy },
|
||||
}
|
||||
|
||||
export function createInbound<T extends Inbound>(type: InType,json?: Partial<T>): Inbound {
|
||||
const defaultObject: Inbound = { ...defaultValues[type] ?? {}, ...(json ?? {}) }
|
||||
return defaultObject
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface oTls {
|
||||
enabled?: boolean
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { oTls } from "./outTls"
|
||||
|
||||
export const OutTypes = {
|
||||
Direct: 'direct',
|
||||
Block: 'block',
|
||||
SOCKS: 'socks',
|
||||
HTTP: 'http',
|
||||
Shadowsocks: 'shadowsocks',
|
||||
VMess: 'vmess',
|
||||
Trojan: 'trojan',
|
||||
Wireguard: 'wireguard',
|
||||
Hysteria: 'hysteria',
|
||||
VLESS: 'vless',
|
||||
ShadowTLS: 'shadowtls',
|
||||
TUIC: 'tuic',
|
||||
Hysteria2: 'hysteria2',
|
||||
Tur: 'tur',
|
||||
SSH: 'ssh',
|
||||
DNS: 'dns',
|
||||
Selector: 'selector',
|
||||
URLTest: 'urltest',
|
||||
}
|
||||
|
||||
type OutType = typeof OutTypes[keyof typeof OutTypes]
|
||||
|
||||
export interface Dial {
|
||||
detour?: string
|
||||
bind_interface?: string
|
||||
inet4_bind_address?: string
|
||||
inet6_bind_address?: string
|
||||
routing_mark?: number
|
||||
reuse_addr?: boolean
|
||||
connect_timeout?: string
|
||||
tcp_fast_open?: boolean
|
||||
tcp_multi_path?: boolean
|
||||
udp_fragment?: boolean
|
||||
domain_strategy?: string
|
||||
fallback_delay?: string
|
||||
}
|
||||
|
||||
interface OutboundBasics {
|
||||
type: OutType
|
||||
tag: string
|
||||
}
|
||||
|
||||
export interface Direct extends OutboundBasics, Dial {
|
||||
override_address?: string
|
||||
override_port?: number
|
||||
proxy_protocol?: 0 | 1 | 2
|
||||
}
|
||||
|
||||
// Create interfaces dynamically based on OutTypes keys
|
||||
type InterfaceMap = {
|
||||
[Key in keyof typeof OutTypes]: {
|
||||
type: string
|
||||
[otherProperties: string]: any; // You can add other properties as needed
|
||||
}
|
||||
}
|
||||
|
||||
// Create union type from InterfaceMap
|
||||
export type Outbound = InterfaceMap[keyof InterfaceMap]
|
||||
|
||||
// Create defaultValues object dynamically
|
||||
const defaultValues: Record<OutType, Outbound> = {
|
||||
direct: { type: OutTypes.Direct },
|
||||
block: { type: OutTypes.Block },
|
||||
socks: { type: OutTypes.SOCKS },
|
||||
http: { type: OutTypes.HTTP },
|
||||
shadowsocks: { type: OutTypes.Shadowsocks },
|
||||
vmess: { type: OutTypes.VMess, tls: { enabled: true } },
|
||||
trojan: { type: OutTypes.Trojan },
|
||||
wireguard: { type: OutTypes.Wireguard },
|
||||
hysteria: { type: OutTypes.Hysteria },
|
||||
vless: { type: OutTypes.VLESS },
|
||||
shadowtls: { type: OutTypes.ShadowTLS },
|
||||
tuic: { type: OutTypes.TUIC },
|
||||
hysteria2: { type: OutTypes.Hysteria2, users: [], tls: {} },
|
||||
tur: { type: OutTypes.Tur },
|
||||
ssh: { type: OutTypes.SSH },
|
||||
dns: { type: OutTypes.DNS },
|
||||
selector: { type: OutTypes.Selector },
|
||||
urltest: { type: OutTypes.URLTest },
|
||||
}
|
||||
|
||||
export function createOutbound<T extends Outbound>(type: string,json?: Partial<T>): Outbound {
|
||||
const defaultObject: Outbound = { ...defaultValues[type], ...(json || {}) }
|
||||
return defaultObject
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
export const TrspTypes = {
|
||||
HTTP: 'http',
|
||||
WebSocket: 'ws',
|
||||
QUIC: 'quic',
|
||||
gRPC: 'grpc',
|
||||
HTTPUpgrade: "httpupgrade"
|
||||
}
|
||||
|
||||
export type TrspType = typeof TrspTypes[keyof typeof TrspTypes]
|
||||
|
||||
export type Transport = HTTP|WebSocket|QUIC|gRPC|HTTPUpgrade
|
||||
|
||||
interface TransportBasics {
|
||||
type: TrspType
|
||||
}
|
||||
|
||||
export interface HTTP extends TransportBasics {
|
||||
host?: string[]
|
||||
path?: string
|
||||
method?: string
|
||||
headers?: {}
|
||||
idle_timeout?: string
|
||||
ping_timeout?: string
|
||||
}
|
||||
|
||||
export interface WebSocket extends TransportBasics {
|
||||
path: string
|
||||
headers?: {}
|
||||
max_early_data?: number
|
||||
early_data_header_name?: string
|
||||
}
|
||||
|
||||
export interface QUIC extends TransportBasics {}
|
||||
|
||||
export interface gRPC extends TransportBasics {
|
||||
service_name?: string
|
||||
idle_timeout?: string
|
||||
ping_timeout?: string
|
||||
permit_without_stream?: boolean
|
||||
}
|
||||
|
||||
export interface HTTPUpgrade extends TransportBasics {
|
||||
host?: string
|
||||
path?: string
|
||||
headers?: {}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
<template>
|
||||
<v-expansion-panels>
|
||||
<v-expansion-panel title="Log">
|
||||
<v-expansion-panel-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-switch v-model="appConfig.log.disabled" color="primary" :label="$t('disable')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-select
|
||||
hide-details
|
||||
label="Level"
|
||||
:items="levels"
|
||||
v-model="appConfig.log.level">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-text-field
|
||||
v-model="appConfig.log.output"
|
||||
hide-details
|
||||
label="Output"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-switch v-model="appConfig.log.timestamp" color="primary" label="Timestamp" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
<v-expansion-panel title="DNS">
|
||||
<v-expansion-panel-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-select
|
||||
hide-details
|
||||
label="Final"
|
||||
:items="[ {title: 'First Server', value: ''}, ...dnsServersTags]"
|
||||
v-model="finalDns">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-select
|
||||
hide-details
|
||||
label="Domain to IP Strategy"
|
||||
clearable
|
||||
@click:clear="delete appConfig.dns.strategy"
|
||||
:items="['prefer_ipv4','prefer_ipv6','ipv4_only','ipv6_only']"
|
||||
v-model="appConfig.dns.strategy">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" align-self="center">
|
||||
<v-btn @click="addDnsServer" rounded>
|
||||
<v-icon icon="mdi-plus" />Server
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template v-for="(s, index) in appConfig.dns.servers">
|
||||
Server {{ index+1 }} <v-icon icon="mdi-delete" @click="appConfig.dns.servers.splice(index,1)" />
|
||||
<v-divider></v-divider>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-text-field
|
||||
v-model="s.tag"
|
||||
hide-details
|
||||
clearable
|
||||
@click:clear="delete s.tag"
|
||||
label="Tag"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-text-field
|
||||
v-model="s.address"
|
||||
hide-details
|
||||
label="Address"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-select
|
||||
hide-details
|
||||
label="Outbound"
|
||||
clearable
|
||||
@click:clear="delete s.detour"
|
||||
:items="outboundTags"
|
||||
v-model="s.detour">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-select
|
||||
hide-details
|
||||
label="Domain Strategy"
|
||||
clearable
|
||||
@click:clear="delete s.strategy"
|
||||
:items="['prefer_ipv4','prefer_ipv6','ipv4_only','ipv6_only']"
|
||||
v-model="s.strategy">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
<v-expansion-panel title="NTP">
|
||||
<v-expansion-panel-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-switch v-model="enableNtp" color="primary" :label="$t('enable')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.ntp?.enabled">
|
||||
<v-text-field
|
||||
v-model="appConfig.ntp.server"
|
||||
hide-details
|
||||
label="Server"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.ntp?.enabled">
|
||||
<v-text-field
|
||||
v-model="appConfig.ntp.server_port"
|
||||
hide-details
|
||||
type="number"
|
||||
clearable
|
||||
@click:clear="delete appConfig.ntp.server_port"
|
||||
label="Server Port"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.ntp?.enabled">
|
||||
<v-text-field
|
||||
v-model="ntpInterval"
|
||||
hide-details
|
||||
suffix="m"
|
||||
min="0"
|
||||
type="number"
|
||||
label="Interval"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Dial :dial="appConfig.ntp" v-if="appConfig.ntp?.enabled" />
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
<v-expansion-panel title="Experimental">
|
||||
<v-expansion-panel-text>
|
||||
Cache File
|
||||
<v-divider></v-divider>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-switch v-model="enableCacheFile" color="primary" :label="$t('enable')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.cache_file">
|
||||
<v-text-field
|
||||
v-model="appConfig.experimental.cache_file.path"
|
||||
hide-details
|
||||
label="Path"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.cache_file">
|
||||
<v-text-field
|
||||
v-model="appConfig.experimental.cache_file.cache_id"
|
||||
hide-details
|
||||
label="Cache ID"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.cache_file">
|
||||
<v-switch v-model="appConfig.experimental.cache_file.store_fakeip"
|
||||
color="primary"
|
||||
label="Store Fake IP"
|
||||
hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
Clash API
|
||||
<v-divider></v-divider>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-switch v-model="enableClashApi" color="primary" :label="$t('enable')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.clash_api">
|
||||
<v-text-field
|
||||
v-model="appConfig.experimental.clash_api.external_controller"
|
||||
hide-details
|
||||
label="External Controller"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.clash_api">
|
||||
<v-text-field
|
||||
v-model="appConfig.experimental.clash_api.external_ui"
|
||||
hide-details
|
||||
label="External UI"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.clash_api">
|
||||
<v-text-field
|
||||
v-model="appConfig.experimental.clash_api.external_ui_download_url"
|
||||
hide-details
|
||||
label="UI Download URL"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.clash_api">
|
||||
<v-text-field
|
||||
v-model="appConfig.experimental.clash_api.external_ui_download_detour"
|
||||
hide-details
|
||||
label="UI Download detour"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.clash_api">
|
||||
<v-text-field
|
||||
v-model="appConfig.experimental.clash_api.secret"
|
||||
hide-details
|
||||
label="Secret"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.clash_api">
|
||||
<v-text-field
|
||||
v-model="appConfig.experimental.clash_api.default_mode"
|
||||
hide-details
|
||||
label="Default Mode"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
V2Ray API
|
||||
<v-divider></v-divider>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-text-field
|
||||
v-model="appConfig.experimental.v2ray_api.listen"
|
||||
hide-details
|
||||
label="Listen"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-switch v-model="appConfig.experimental.v2ray_api.stats.enabled"
|
||||
color="primary"
|
||||
:label="$t('stats.enable')"
|
||||
hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="appConfig.experimental.v2ray_api.stats.enabled">
|
||||
<v-col cols="12" sm="6">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('pages.inbounds')"
|
||||
multiple chips closable-chips
|
||||
:items="inboundTags"
|
||||
v-model="appConfig.experimental.v2ray_api.stats.inbounds">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('pages.outbounds')"
|
||||
multiple chips closable-chips
|
||||
:items="outboundTags"
|
||||
v-model="appConfig.experimental.v2ray_api.stats.outbounds">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('pages.clients')"
|
||||
multiple chips closable-chips
|
||||
:items="clientNames"
|
||||
v-model="appConfig.experimental.v2ray_api.stats.users">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Data from '@/store/modules/data';
|
||||
import Dial from '@/components/Dial.vue';
|
||||
import { computed } from 'vue';
|
||||
import { Config, Ntp } from '@/types/config';
|
||||
import { Client } from '@/types/clients';
|
||||
|
||||
const appConfig = computed((): Config => {
|
||||
return <Config> Data().config
|
||||
})
|
||||
|
||||
const inboundTags = computed((): string[] => {
|
||||
return appConfig.value.inbounds.map(i => i.tag)
|
||||
})
|
||||
|
||||
const outboundTags = computed((): string[] => {
|
||||
return appConfig.value.outbounds.map(o => o.tag)
|
||||
})
|
||||
|
||||
const clientNames = computed((): string[] => {
|
||||
const clients = <Client[]>Data().clients
|
||||
return clients?.map(c => c.name)
|
||||
})
|
||||
|
||||
const levels = ["trace", "debug", "info", "warn", "error", "fatal", "panic"]
|
||||
|
||||
const dnsServersTags = computed((): string[] => {
|
||||
const s = <string[]>appConfig.value.dns.servers?.filter(s => s.tag && s.tag != "")?.map(s => s.tag)
|
||||
return s?? <string[]>[]
|
||||
})
|
||||
|
||||
const finalDns = computed({
|
||||
get() { return appConfig.value.dns.final?? '' },
|
||||
set(v:string) { appConfig.value.dns.final = v.length>0 ? v : undefined }
|
||||
})
|
||||
|
||||
const addDnsServer = () => {
|
||||
if (!appConfig.value.dns.servers) appConfig.value.dns.servers = []
|
||||
appConfig.value.dns.servers.push({address: 'local'})
|
||||
}
|
||||
|
||||
const enableNtp = computed({
|
||||
get() { return appConfig.value.ntp?.enabled?? false },
|
||||
set(v:boolean) {
|
||||
if (v){
|
||||
appConfig.value.ntp = <Ntp>{ enabled: true, server: 'time.apple.com', server_port: 123, interval: '30m'}
|
||||
} else { appConfig.value.ntp = <Ntp>{} }
|
||||
}
|
||||
})
|
||||
|
||||
const ntpInterval = computed({
|
||||
get():any { return appConfig.value.ntp?.interval? parseInt(appConfig.value.ntp?.interval.replace('m','')) : null },
|
||||
set(v:number) { if (appConfig.value.ntp) v>0 ? appConfig.value.ntp.interval = v + 'm' : delete appConfig.value.ntp.interval }
|
||||
})
|
||||
|
||||
const enableCacheFile = computed({
|
||||
get() { return appConfig.value.experimental.cache_file?.enabled?? false },
|
||||
set(v:boolean) {
|
||||
if (v){
|
||||
appConfig.value.experimental.cache_file = { enabled: true }
|
||||
} else { delete appConfig.value.experimental.cache_file }
|
||||
}
|
||||
})
|
||||
|
||||
const enableClashApi = computed({
|
||||
get() { return appConfig.value.experimental.clash_api != undefined },
|
||||
set(v:boolean) {
|
||||
if (v){
|
||||
appConfig.value.experimental.clash_api = {}
|
||||
} else { delete appConfig.value.experimental.clash_api }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.v-card-subtitle {
|
||||
text-align: center;
|
||||
border-bottom: 1px solid gray;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,303 @@
|
||||
|
||||
<template>
|
||||
<ClientModal
|
||||
v-model="modal.visible"
|
||||
:visible="modal.visible"
|
||||
:index="modal.index"
|
||||
:data="modal.data"
|
||||
:stats="modal.stats"
|
||||
:inboundTags="inboundTags"
|
||||
@close="closeModal"
|
||||
@save="saveModal"
|
||||
/>
|
||||
<QrCode
|
||||
v-model="qrcode.visible"
|
||||
:visible="qrcode.visible"
|
||||
:index="qrcode.index"
|
||||
@close="closeQrCode"
|
||||
/>
|
||||
<Stats
|
||||
v-model="stats.visible"
|
||||
:visible="stats.visible"
|
||||
:resource="stats.resource"
|
||||
:tag="stats.tag"
|
||||
@close="closeStats"
|
||||
/>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-btn color="primary" @click="showModal(-1)">{{ $t('actions.add') }}</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in clients" :key="item.id">
|
||||
<v-card rounded="xl" elevation="5" min-width="200">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>{{ item.name }}</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto">
|
||||
<v-switch color="primary" v-model="clients[index].enable" hideDetails density="compact" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col>{{ $t('pages.inbounds') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
<v-tooltip activator="parent" dir="ltr" location="bottom">
|
||||
<span v-for="i in item.inbounds.split(',')">{{ i }}<br /></span>
|
||||
</v-tooltip>
|
||||
{{ item.inbounds.split(',').length }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>{{ $t('stats.volume') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
{{ item.volume == 0 ? $t('unlimited') : HumanReadable.sizeFormat(item.volume) }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>{{ $t('date.expiry') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
{{ item.expiry == 0 ? $t('unlimited') : HumanReadable.remainedDays(item.expiry)?? $t('date.expired') }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>{{ $t('stats.usage') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
<v-tooltip activator="parent" location="bottom">
|
||||
{{ $t('stats.upload') }}:{{ HumanReadable.sizeFormat(item.up) }}<br />
|
||||
{{ $t('stats.download') }}:{{ HumanReadable.sizeFormat(item.down) }}<br />
|
||||
<template v-if="item.volume>0">
|
||||
{{ $t('remained') }}: {{ HumanReadable.sizeFormat(item.volume - (item.up + item.down)) }}
|
||||
</template>
|
||||
</v-tooltip>
|
||||
{{ item.volume>0 ? HumanReadable.sizeFormat(item.up + item.down) : $t('unlimited') }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>{{ $t('online') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
<template v-if="onlines[index]">
|
||||
<v-chip density="comfortable" size="small" color="success" variant="flat">{{ $t('online') }}</v-chip>
|
||||
</template>
|
||||
<template v-else>-</template>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions style="padding: 0;">
|
||||
<v-btn icon="mdi-account-edit" @click="showModal(index)">
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top" :text="$t('actions.edit')"></v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn style="margin-inline-start:0;" icon="mdi-account-minus" color="warning" @click="delOverlay[index] = true">
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top" :text="$t('actions.del')"></v-tooltip>
|
||||
</v-btn>
|
||||
<v-overlay
|
||||
v-model="delOverlay[index]"
|
||||
contained
|
||||
class="align-center justify-center"
|
||||
>
|
||||
<v-card :title="$t('actions.del')" rounded="lg">
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>{{ $t('confirm') }}</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn color="error" variant="outlined" @click="delClient(index)">{{ $t('yes') }}</v-btn>
|
||||
<v-btn color="success" variant="outlined" @click="delOverlay[index] = false">{{ $t('no') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-overlay>
|
||||
<v-btn icon="mdi-qrcode" @click="showQrCode(index)" />
|
||||
<v-btn icon="mdi-chart-line" @click="showStats(item.name)" />
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import Data from '@/store/modules/data'
|
||||
import ClientModal from '@/layouts/modals/Client.vue'
|
||||
import QrCode from '@/layouts/modals/QrCode.vue'
|
||||
import Stats from '@/layouts/modals/Stats.vue'
|
||||
import { Client, createClient } from '@/types/clients'
|
||||
import { computed, ref } from 'vue'
|
||||
import { Config, V2rayApiStats } from '@/types/config'
|
||||
import { InTypes, Inbound,InboundWithUser, ShadowTLS, VLESS } from '@/types/inbounds'
|
||||
import { Link, LinkUtil } from '@/plugins/link'
|
||||
import { HumanReadable } from '@/plugins/utils'
|
||||
|
||||
const clients = computed((): any[] => {
|
||||
return Data().clients
|
||||
})
|
||||
|
||||
const onlines = computed(() => {
|
||||
return Data().onlines.user ? clients.value.map(c => Data().onlines.user.includes(c.name)) : []
|
||||
})
|
||||
|
||||
const appConfig = computed((): Config => {
|
||||
return <Config> Data().config
|
||||
})
|
||||
|
||||
const v2rayStats = computed((): V2rayApiStats => {
|
||||
return <V2rayApiStats> appConfig.value.experimental.v2ray_api.stats
|
||||
})
|
||||
|
||||
const inbounds = computed((): Inbound[] => {
|
||||
return <Inbound[]> appConfig.value.inbounds
|
||||
})
|
||||
|
||||
const inboundTags = computed((): string[] => {
|
||||
if (!inbounds.value) return []
|
||||
return inbounds.value.filter(i => i.tag != "" && Object.hasOwn(i,'users')).map(i => i.tag)
|
||||
})
|
||||
|
||||
const modal = ref({
|
||||
visible: false,
|
||||
index: -1,
|
||||
data: "",
|
||||
stats: false,
|
||||
})
|
||||
|
||||
const delOverlay = ref(new Array<boolean>(clients.value.length).fill(false))
|
||||
|
||||
const showModal = (index: number) => {
|
||||
modal.value.index = index
|
||||
modal.value.data = index == -1 ? '' : JSON.stringify(clients.value[index])
|
||||
modal.value.stats = index == -1 ? false : v2rayStats.value.users.includes(clients.value[index].name)
|
||||
modal.value.visible = true
|
||||
}
|
||||
const closeModal = () => {
|
||||
modal.value.visible = false
|
||||
}
|
||||
const saveModal = (data:any, stats:boolean) => {
|
||||
const inboundTags: string[] = data.inbounds.split(',')?? []
|
||||
let oldName:string = ""
|
||||
if(modal.value.index == -1) {
|
||||
clients.value.push(data)
|
||||
} else {
|
||||
const oldData = createClient(clients.value[modal.value.index])
|
||||
oldName = oldData.name
|
||||
oldData.inbounds.split(',').forEach((i:string) => {
|
||||
if (!inboundTags.includes(i)) inboundTags.push(i)
|
||||
})
|
||||
clients.value[modal.value.index] = data
|
||||
}
|
||||
|
||||
// Rebuild affected inbounds
|
||||
buildInboundsUsers(inboundTags)
|
||||
|
||||
// Rebuild links
|
||||
data.links = updateLinks(data)
|
||||
|
||||
// Set Client Stats
|
||||
const sIndex = v2rayStats.value.users.findIndex(i => i == data.name) // Find if new user exists
|
||||
|
||||
if (oldName != data.name) {
|
||||
v2rayStats.value.users = v2rayStats.value.users.filter(item => item != oldName)
|
||||
}
|
||||
|
||||
if (stats) {
|
||||
// Add if dos not exist
|
||||
if (data.name.length>0 && sIndex == -1) v2rayStats.value.users.push(data.name)
|
||||
} else {
|
||||
// Delete if exists
|
||||
if (sIndex != -1) v2rayStats.value.users.splice(sIndex,1)
|
||||
}
|
||||
|
||||
modal.value.visible = false
|
||||
}
|
||||
const buildInboundsUsers = (inboundTags:string[]) => {
|
||||
inboundTags.forEach(tag => {
|
||||
const inbound_index = inbounds.value.findIndex(i => i.tag == tag)
|
||||
if (inbound_index != -1){
|
||||
const users = <any>[]
|
||||
const newInbound = <InboundWithUser>inbounds.value[inbound_index]
|
||||
const inboundClients = clients.value.filter(c => c.enable && c.inbounds.split(',').includes(tag))
|
||||
inboundClients.forEach(c => {
|
||||
const clientConfig = JSON.parse(c.config)
|
||||
// Remove flow in non tls VLESS
|
||||
if (newInbound.type == InTypes.VLESS) {
|
||||
const vlessInbound = <VLESS>newInbound
|
||||
if (!vlessInbound.tls?.enabled) delete(clientConfig["vless"].flow)
|
||||
}
|
||||
users.push(clientConfig[newInbound.type])
|
||||
})
|
||||
newInbound.users = users
|
||||
|
||||
// Exceptions for Naive and ShadowTLSv3
|
||||
if (users.length == 0){
|
||||
if (newInbound.type == InTypes.Naive) {
|
||||
newInbound.users = <any>[{}]
|
||||
} else {
|
||||
if (newInbound.type == InTypes.ShadowTLS){
|
||||
const ssTls = <ShadowTLS>newInbound
|
||||
if (ssTls.version == 3) newInbound.users = <any>[{}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inbounds.value[inbound_index] = newInbound
|
||||
}
|
||||
})
|
||||
}
|
||||
const updateLinks = (c:Client):string => {
|
||||
const clientInbounds = <Inbound[]>inbounds.value.filter(i => c.inbounds.split(',').includes(i.tag))
|
||||
const newLinks = <Link[]>[]
|
||||
clientInbounds.forEach(i =>{
|
||||
const uri = LinkUtil.linkGenerator(c.name,i)
|
||||
if (uri.length>0){
|
||||
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
|
||||
}
|
||||
})
|
||||
let links = c.links && c.links.length>0? <Link[]>JSON.parse(c.links) : <Link[]>[]
|
||||
links = [...newLinks, ...links.filter(l => l.type != 'local')]
|
||||
|
||||
return JSON.stringify(links)
|
||||
}
|
||||
const delClient = (clientIndex: number) => {
|
||||
const id = clients.value[clientIndex].id
|
||||
const oldData = createClient(clients.value[clientIndex])
|
||||
|
||||
// Delete stats if exists and will be orphaned
|
||||
const tagCounts = clients.value.filter(i => i.name == oldData.name).length
|
||||
const sIndex = v2rayStats.value.users.findIndex(i => i == oldData.name)
|
||||
if (tagCounts == 1 && sIndex != -1){
|
||||
v2rayStats.value.users.splice(sIndex,1)
|
||||
}
|
||||
|
||||
clients.value.splice(clientIndex,1)
|
||||
buildInboundsUsers(oldData.inbounds.split(','))
|
||||
if (id>0) Data().delClient(id)
|
||||
delOverlay.value[clientIndex] = false
|
||||
}
|
||||
|
||||
const qrcode = ref({
|
||||
visible: false,
|
||||
index: 0,
|
||||
})
|
||||
|
||||
const showQrCode = (index: number) => {
|
||||
qrcode.value.index = index
|
||||
qrcode.value.visible = true
|
||||
}
|
||||
const closeQrCode = () => {
|
||||
qrcode.value.visible = false
|
||||
}
|
||||
|
||||
const stats = ref({
|
||||
visible: false,
|
||||
resource: "user",
|
||||
tag: "",
|
||||
})
|
||||
|
||||
const showStats = (tag: string) => {
|
||||
stats.value.tag = tag
|
||||
stats.value.visible = true
|
||||
}
|
||||
const closeStats = () => {
|
||||
stats.value.visible = false
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<Main />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Main from '@/components/Main.vue'
|
||||
</script>
|
||||
@@ -0,0 +1,289 @@
|
||||
<template>
|
||||
<InboundVue
|
||||
v-model="modal.visible"
|
||||
:visible="modal.visible"
|
||||
:id="modal.id"
|
||||
:stats="modal.stats"
|
||||
:data="modal.data"
|
||||
@close="closeModal"
|
||||
@save="saveModal"
|
||||
/>
|
||||
<Stats
|
||||
v-model="stats.visible"
|
||||
:visible="stats.visible"
|
||||
:resource="stats.resource"
|
||||
:tag="stats.tag"
|
||||
@close="closeStats"
|
||||
/>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-btn color="primary" @click="showModal(-1)">{{ $t('actions.add') }}</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in <any[]>inbounds" :key="item.tag">
|
||||
<v-card rounded="xl" elevation="5" min-width="200" :title="item.tag">
|
||||
<v-card-subtitle>
|
||||
<v-row>
|
||||
<v-col>{{ item.type }}</v-col>
|
||||
</v-row>
|
||||
</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col>{{ $t('in.addr') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
{{ item.listen }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>{{ $t('in.port') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
{{ item.listen_port }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>{{ $t('in.tls') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
{{ Object.hasOwn(item,'tls') ? $t(item.tls?.enabled ? 'enable' : 'disable') : '-' }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>{{ $t('pages.clients') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
<v-tooltip activator="parent" dir="ltr" location="bottom" v-if="Object.hasOwn(item,'users')">
|
||||
<span v-for="u in findInbounsUsers(item)">{{ u }}<br /></span>
|
||||
</v-tooltip>
|
||||
{{ Object.hasOwn(item,'users') ? item.users.length : '-' }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>{{ $t('online') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
<template v-if="onlines[index]">
|
||||
<v-chip density="comfortable" size="small" color="success" variant="flat">{{ $t('online') }}</v-chip>
|
||||
</template>
|
||||
<template v-else>-</template>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions style="padding: 0;">
|
||||
<v-btn icon="mdi-file-edit" @click="showModal(index)">
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top" :text="$t('actions.edit')"></v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn icon="mdi-file-remove" style="margin-inline-start:0;" color="warning" @click="delOverlay[index] = true">
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top" :text="$t('actions.del')"></v-tooltip>
|
||||
</v-btn>
|
||||
<v-overlay
|
||||
v-model="delOverlay[index]"
|
||||
contained
|
||||
class="align-center justify-center"
|
||||
>
|
||||
<v-card :title="$t('actions.del')" rounded="lg">
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>{{ $t('confirm') }}</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn color="error" variant="outlined" @click="delInbound(index)">{{ $t('yes') }}</v-btn>
|
||||
<v-btn color="success" variant="outlined" @click="delOverlay[index] = false">{{ $t('no') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-overlay>
|
||||
<v-btn icon="mdi-chart-line" @click="showStats(item.tag)" />
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Data from '@/store/modules/data'
|
||||
import InboundVue from '@/layouts/modals/Inbound.vue'
|
||||
import Stats from '@/layouts/modals/Stats.vue'
|
||||
import { Config, V2rayApiStats } from '@/types/config'
|
||||
import { computed, ref } from 'vue'
|
||||
import { InTypes, Inbound, InboundWithUser, ShadowTLS, VLESS } from '@/types/inbounds'
|
||||
import { Client } from '@/types/clients'
|
||||
import { Link, LinkUtil } from '@/plugins/link'
|
||||
|
||||
const appConfig = computed((): Config => {
|
||||
return <Config> Data().config
|
||||
})
|
||||
|
||||
const inbounds = computed((): Inbound[] => {
|
||||
return <Inbound[]> appConfig.value.inbounds
|
||||
})
|
||||
|
||||
const clients = computed((): Client[] => {
|
||||
return <Client[]> Data().clients
|
||||
})
|
||||
|
||||
const onlines = computed(() => {
|
||||
return Data().onlines.inbound ? inbounds.value.map(i => Data().onlines.inbound.includes(i.tag)) : []
|
||||
})
|
||||
|
||||
const v2rayStats = computed((): V2rayApiStats => {
|
||||
return <V2rayApiStats> appConfig.value.experimental.v2ray_api.stats
|
||||
})
|
||||
|
||||
const modal = ref({
|
||||
visible: false,
|
||||
id: -1,
|
||||
data: "",
|
||||
stats: false,
|
||||
})
|
||||
|
||||
let delOverlay = ref(new Array<boolean>)
|
||||
|
||||
const showModal = (id: number) => {
|
||||
modal.value.id = id
|
||||
modal.value.data = id == -1 ? '' : JSON.stringify(inbounds.value[id])
|
||||
modal.value.stats = id == -1 ? false : v2rayStats.value.inbounds.includes(inbounds.value[id].tag)
|
||||
modal.value.visible = true
|
||||
}
|
||||
const closeModal = () => {
|
||||
modal.value.visible = false
|
||||
}
|
||||
const saveModal = (data:Inbound, stats: boolean) => {
|
||||
// New or Edit
|
||||
if (modal.value.id == -1) {
|
||||
inbounds.value.push(data)
|
||||
if (stats && data.tag.length>0) {
|
||||
v2rayStats.value.inbounds.push(data.tag)
|
||||
}
|
||||
} else {
|
||||
const oldTag = inbounds.value[modal.value.id].tag
|
||||
const sIndex = v2rayStats.value.inbounds.findIndex(i => i == data.tag) // Find if new tag exists
|
||||
|
||||
if (oldTag != data.tag) {
|
||||
v2rayStats.value.inbounds = v2rayStats.value.inbounds.filter(item => item != oldTag)
|
||||
changeClientInboundsTag(oldTag,data.tag)
|
||||
}
|
||||
|
||||
if (stats) {
|
||||
// Add if dos not exist
|
||||
if (data.tag.length>0 && sIndex == -1) v2rayStats.value.inbounds.push(data.tag)
|
||||
} else {
|
||||
// Delete if exists
|
||||
if (sIndex != -1) v2rayStats.value.inbounds.splice(sIndex,1)
|
||||
}
|
||||
|
||||
inbounds.value[modal.value.id] = data
|
||||
}
|
||||
// Set users
|
||||
data = buildInboundsUsers(data)
|
||||
// Update links
|
||||
if (Object.hasOwn(data,'users')) updateLinks(data)
|
||||
modal.value.visible = false
|
||||
}
|
||||
const updateLinks = (i: InboundWithUser) => {
|
||||
if(i.users && i.users.length>0){
|
||||
i.users.forEach((u:any) => {
|
||||
const client = clients.value.find(c => u.username? c.name == u.username : c.name == u.name)
|
||||
if (client){
|
||||
const clientInbounds = <Inbound[]>inbounds.value.filter(i => client?.inbounds.split(',').includes(i.tag))
|
||||
const newLinks = <Link[]>[]
|
||||
clientInbounds.forEach(i =>{
|
||||
const uri = LinkUtil.linkGenerator(client.name,i)
|
||||
if (uri.length>0){
|
||||
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
|
||||
}
|
||||
})
|
||||
let links = client.links && client.links.length>0? <Link[]>JSON.parse(client.links) : <Link[]>[]
|
||||
links = [...newLinks, ...links.filter(l => l.type != 'local')]
|
||||
|
||||
client.links = JSON.stringify(links)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
const delInbound = (index: number) => {
|
||||
const inb = inbounds.value[index]
|
||||
inbounds.value.splice(index,1)
|
||||
const tag = inb.tag
|
||||
|
||||
if (Object.hasOwn(inb,'users')){
|
||||
const inbU = <InboundWithUser>inb
|
||||
if (inbU.users && inbU.users.length>0){
|
||||
inbU.users.forEach((u:any) => {
|
||||
const c_index = clients.value.findIndex(c => u.username? u.username == c.name : u.user == c.name)
|
||||
if (c_index != -1) {
|
||||
const clientInbounds = clients.value[c_index].inbounds.split(',').filter((x:string) => x!=tag)
|
||||
clients.value[c_index].inbounds = clientInbounds.join(',')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Delete stats if exists and will be orphaned
|
||||
const tagCounts = inbounds.value.filter(i => i.tag == inb.tag).length
|
||||
const sIndex = v2rayStats.value.inbounds.findIndex(i => i == inb.tag)
|
||||
if (tagCounts == 1 && sIndex != -1){
|
||||
v2rayStats.value.inbounds.splice(sIndex,1)
|
||||
}
|
||||
if (index < Data().oldData.config.inbounds.length){
|
||||
Data().delInbound(index)
|
||||
}
|
||||
delOverlay.value[index] = false
|
||||
}
|
||||
const buildInboundsUsers = (inbound:InboundWithUser):Inbound => {
|
||||
const users = <any>[]
|
||||
const inboundClients = clients.value.filter(c => c.enable && c.inbounds.split(',').includes(inbound.tag))
|
||||
inboundClients.forEach(c => {
|
||||
const clientConfig = JSON.parse(c.config)
|
||||
// Remove flow in non tls VLESS
|
||||
if (inbound.type == InTypes.VLESS) {
|
||||
const vlessInbound = <VLESS>inbound
|
||||
if (!vlessInbound.tls?.enabled) clientConfig["vless"].flow = undefined
|
||||
}
|
||||
users.push(clientConfig[inbound.type])
|
||||
})
|
||||
inbound.users = users
|
||||
|
||||
// Exceptions for Naive and ShadowTLSv3
|
||||
if (users.length == 0){
|
||||
if (inbound.type == InTypes.Naive){
|
||||
inbound.users = <any>[{}]
|
||||
} else {
|
||||
if (inbound.type == InTypes.ShadowTLS){
|
||||
const ssTls = <ShadowTLS>inbound
|
||||
if (ssTls.version == 3) inbound.users = <any>[{}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <Inbound>inbound
|
||||
}
|
||||
const changeClientInboundsTag = (oldtag: string, newTag:string) => {
|
||||
clients.value.forEach((c, c_index) => {
|
||||
const inboundsArray = c.inbounds.split(',')
|
||||
const inbound_index = inboundsArray.findIndex(i => i == oldtag)
|
||||
if (inbound_index != -1) {
|
||||
inboundsArray[inbound_index] = newTag
|
||||
clients.value[c_index].inbounds = inboundsArray.join(',')
|
||||
}
|
||||
})
|
||||
}
|
||||
const findInbounsUsers = (inbound: InboundWithUser): string[] => {
|
||||
if (inbound.users === null || !Array.isArray(inbound.users) || inbound.users.length == 0) return []
|
||||
|
||||
const users = inbound.users.map(user => "username" in user ? user.username : user.name)
|
||||
return users
|
||||
}
|
||||
|
||||
const stats = ref({
|
||||
visible: false,
|
||||
resource: "inbound",
|
||||
tag: "",
|
||||
})
|
||||
|
||||
const showStats = (tag: string) => {
|
||||
stats.value.tag = tag
|
||||
stats.value.visible = true
|
||||
}
|
||||
const closeStats = () => {
|
||||
stats.value.visible = false
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<v-container class="fill-height" style="margin-top: 100px;">
|
||||
<v-row justify="center" align="center">
|
||||
<v-col cols="12" sm="8" md="4">
|
||||
<v-card>
|
||||
<v-card-title class="headline">Login</v-card-title>
|
||||
<v-card-text>
|
||||
<v-form @submit.prevent="login" ref="form">
|
||||
<v-text-field v-model="username" :label="$t('login.username')" :rules="usernameRules" required></v-text-field>
|
||||
<v-text-field v-model="password" :label="$t('login.password')" :rules="passwordRules" type="password" required></v-text-field>
|
||||
<v-btn :loading="loading" type="submit" color="primary" block class="mt-2">Login</v-btn>
|
||||
</v-form>
|
||||
<v-select
|
||||
density="compact"
|
||||
class="mt-2"
|
||||
hide-details
|
||||
variant="solo"
|
||||
:items="languages"
|
||||
v-model="$i18n.locale"
|
||||
@update:modelValue="changeLocale">
|
||||
<template v-slot:append>
|
||||
<v-icon icon="mdi-theme-light-dark" @click="toggleTheme()"></v-icon>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue"
|
||||
import { useLocale,useTheme } from 'vuetify'
|
||||
import { i18n, languages } from '@/locales'
|
||||
import { useRouter } from 'vue-router'
|
||||
import HttpUtil from '@/plugins/httputil'
|
||||
|
||||
|
||||
const theme = useTheme()
|
||||
const locale = useLocale()
|
||||
const darkMode = ref(localStorage.getItem('theme') == "dark")
|
||||
|
||||
const username = ref('')
|
||||
const usernameRules = [
|
||||
(value: string) => {
|
||||
if (value?.length > 0) return true
|
||||
return i18n.global.t('login.unRules')
|
||||
},
|
||||
]
|
||||
|
||||
const password = ref('')
|
||||
const passwordRules = [
|
||||
(value: string) => {
|
||||
if (value?.length > 0) return true
|
||||
return i18n.global.t('login.pwRules')
|
||||
},
|
||||
]
|
||||
|
||||
const loading = ref(false)
|
||||
const router = useRouter()
|
||||
|
||||
const login = async () => {
|
||||
if (username.value == '' || password.value == '') return
|
||||
loading.value=true
|
||||
const response = await HttpUtil.post('/api/login',{user: username.value, pass: password.value})
|
||||
if(response.success){
|
||||
setTimeout(() => {
|
||||
loading.value=false
|
||||
router.push('/')
|
||||
}, 500)
|
||||
} else {
|
||||
loading.value=false
|
||||
}
|
||||
}
|
||||
const changeLocale = (l: any) => {
|
||||
locale.current.value = l ?? 'en'
|
||||
localStorage.setItem('locale', locale.current.value)
|
||||
}
|
||||
const toggleTheme = () => {
|
||||
darkMode.value = !darkMode.value
|
||||
theme.global.name.value = darkMode.value ? "dark" : "light"
|
||||
localStorage.setItem('theme', theme.global.name.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in <any[]>outbounds" :key="item.tag">
|
||||
<v-card rounded="xl" elevation="5" min-width="200" :title="item.tag">
|
||||
<v-card-subtitle>
|
||||
<v-row>
|
||||
<v-col>{{ item.type }}</v-col>
|
||||
</v-row>
|
||||
</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col>Server</v-col>
|
||||
<v-col dir="ltr">
|
||||
{{ (item.server ?? '') + ' ' + (item.server_port ?? '') }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Data from '@/store/modules/data'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const appConfig = Data().config
|
||||
const outbounds = computed((): any[] => {
|
||||
if (!appConfig || !('outbounds' in appConfig) || !Array.isArray(appConfig.outbounds)) {
|
||||
return []
|
||||
}
|
||||
return appConfig.outbounds
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in <any[]>rules" :key="item.name">
|
||||
<v-card rounded="xl" elevation="5" min-width="200" :title="index">
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col>Type</v-col>
|
||||
<v-col dir="ltr">
|
||||
{{ item.type }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>Mode</v-col>
|
||||
<v-col dir="ltr">
|
||||
{{ item.mode }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>{{ $t('objects.outbound') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
{{ item.outbound }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>{{ $t('pages.rules') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
{{ item.rules ? item.rules.length : 0 }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>Invert</v-col>
|
||||
<v-col dir="ltr">
|
||||
{{ item.invert }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Data from '@/store/modules/data'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const appConfig = Data().config
|
||||
|
||||
const route = computed((): any => {
|
||||
if (!appConfig || !('route' in appConfig)) {
|
||||
return []
|
||||
}
|
||||
return appConfig.route
|
||||
})
|
||||
|
||||
const rules = computed((): any[] => {
|
||||
const data = route.value
|
||||
if (!route || !('rules' in data) || !Array.isArray(data.rules)){
|
||||
return []
|
||||
}
|
||||
return data.rules
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<v-card :loading="loading">
|
||||
<v-tabs
|
||||
v-model="tab"
|
||||
align-tabs="center"
|
||||
bg-color="secondary"
|
||||
show-arrows
|
||||
>
|
||||
<v-tab value="t1">{{ $t('setting.interface') }}</v-tab>
|
||||
<v-tab value="t2">{{ $t('setting.sub') }}</v-tab>
|
||||
<v-tab value="t3">Language</v-tab>
|
||||
</v-tabs>
|
||||
<v-card-text>
|
||||
<v-row align="center" justify="center" style="margin-bottom: 10px;">
|
||||
<v-col cols="auto">
|
||||
<v-btn color="primary" @click="saveChanges" :loading="loading" :disabled="!stateChange">
|
||||
{{ $t('actions.save') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="auto">
|
||||
<v-btn variant="outlined" color="warning" @click="restartApp" :loading="loading" :disabled="stateChange">
|
||||
{{ $t('actions.restartApp') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-window v-model="tab">
|
||||
<v-window-item value="t1">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="settings.webListen" :label="$t('setting.addr')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="webPort" :label="$t('setting.port')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="settings.webDomain" :label="$t('setting.domain')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="settings.webKeyFile" :label="$t('setting.sslKey')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="settings.webCertFile" :label="$t('setting.sslCert')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
type="number"
|
||||
v-model.number="sessionMaxAge"
|
||||
:label="$t('setting.sessionAge')"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="settings.timeLocation" :label="$t('setting.timeLoc')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
|
||||
<v-window-item value="t2">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" v-model="subEncode" :label="$t('setting.subEncode')" hide-details />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" v-model="subShowInfo" :label="$t('setting.subInfo')" hide-details />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="settings.subListen" :label="$t('setting.addr')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
type="number"
|
||||
v-model="subPort"
|
||||
min="1"
|
||||
:label="$t('setting.port')"
|
||||
hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="settings.subKeyFile" :label="$t('setting.sslKey')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="settings.subCertFile" :label="$t('setting.sslCert')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="settings.subDomain" :label="$t('setting.domain')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="settings.subPath" :label="$t('setting.path')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
type="number"
|
||||
v-model.number="subUpdates"
|
||||
:label="$t('setting.update')"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="settings.subURI" :label="$t('setting.subUri')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
|
||||
<v-window-item value="t3">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
label="Language"
|
||||
:items="languages"
|
||||
v-model="$i18n.locale"
|
||||
@update:modelValue="changeLocale">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useLocale } from "vuetify"
|
||||
import { languages } from '@/locales'
|
||||
import { Ref, computed, inject, onMounted, ref } from "vue"
|
||||
import HttpUtils from "@/plugins/httputil"
|
||||
import { FindDiff } from "@/plugins/utils"
|
||||
const locale = useLocale()
|
||||
const tab = ref("t1")
|
||||
const loading:Ref = inject('loading')?? ref(false)
|
||||
const oldSettings = ref({})
|
||||
|
||||
const settings = ref({
|
||||
webListen: "",
|
||||
webDomain: "",
|
||||
webPort: "2095",
|
||||
webCertFile: "",
|
||||
webKeyFile: "",
|
||||
sessionMaxAge: "0",
|
||||
timeLocation: "Asia/Tehran",
|
||||
subListen: "",
|
||||
subPort: "2096",
|
||||
subPath: "/sub/",
|
||||
subDomain: "",
|
||||
subCertFile: "",
|
||||
subKeyFile: "",
|
||||
subUpdates: "12",
|
||||
subEncode: "true",
|
||||
subShowInfo: "false",
|
||||
subURI: "",
|
||||
})
|
||||
|
||||
onMounted(async () => {loadData()})
|
||||
|
||||
const changeLocale = (l: any) => {
|
||||
locale.current.value = l ?? 'en'
|
||||
localStorage.setItem('locale', locale.current.value)
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
const msg = await HttpUtils.get('/api/setting')
|
||||
loading.value = false
|
||||
if (msg.success) {
|
||||
settings.value = msg.obj
|
||||
oldSettings.value = { ...msg.obj }
|
||||
}
|
||||
}
|
||||
|
||||
const saveChanges = async () => {
|
||||
loading.value = true
|
||||
const diff = {
|
||||
settings: JSON.stringify(FindDiff.Settings(settings.value,oldSettings.value)),
|
||||
}
|
||||
const msg = await HttpUtils.post('/api/save', diff)
|
||||
if (msg.success) {
|
||||
loadData()
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
const restartApp = async () => {
|
||||
loading.value = true
|
||||
const msg = await HttpUtils.post('/api/restartApp',{})
|
||||
if (msg.success) {
|
||||
const isTLS = settings.value.webCertFile !== "" || settings.value.webKeyFile !== ""
|
||||
const url = buildURL(settings.value.webDomain,settings.value.webPort.toString(),isTLS)
|
||||
await sleep(3000)
|
||||
window.location.replace(url)
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const buildURL = (host: string, port: string, isTLS: boolean ) => {
|
||||
if (!host || host.length == 0) host = window.location.hostname
|
||||
if (!port || port.length == 0) port = window.location.port
|
||||
|
||||
const protocol = isTLS ? "https:" : "http:"
|
||||
|
||||
if (port === "" || (isTLS && port === "443") || (!isTLS && port === "80")) {
|
||||
port = ""
|
||||
} else {
|
||||
port = `:${port}`
|
||||
}
|
||||
|
||||
return `${protocol}//${host}${port}/settings`
|
||||
}
|
||||
|
||||
const subEncode = computed({
|
||||
get: () => { return settings.value.subEncode == "true" },
|
||||
set: (v:boolean) => { settings.value.subEncode = v ? "true" : "false" }
|
||||
})
|
||||
|
||||
const subShowInfo = computed({
|
||||
get: () => { return settings.value.subShowInfo == "true" },
|
||||
set: (v:boolean) => { settings.value.subShowInfo = v ? "true" : "false" }
|
||||
})
|
||||
|
||||
const webPort = computed({
|
||||
get: () => { return parseInt(settings.value.webPort) },
|
||||
set: (v:number) => { settings.value.webPort = v.toString() }
|
||||
})
|
||||
|
||||
const sessionMaxAge = computed({
|
||||
get: () => { return parseInt(settings.value.sessionMaxAge) },
|
||||
set: (v:number) => { settings.value.sessionMaxAge = v.toString() }
|
||||
})
|
||||
|
||||
const subPort = computed({
|
||||
get: () => { return parseInt(settings.value.subPort) },
|
||||
set: (v:number) => { settings.value.subPort = v.toString() }
|
||||
})
|
||||
|
||||
const subUpdates = computed({
|
||||
get: () => { return parseInt(settings.value.subUpdates) },
|
||||
set: (v:number) => { settings.value.subUpdates = v.toString() }
|
||||
})
|
||||
|
||||
|
||||
const stateChange = computed(() => {
|
||||
return !FindDiff.deepCompare(settings.value,oldSettings.value)
|
||||
})
|
||||
</script>
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
Reference in New Issue
Block a user