Add search / filter box (#2170)
* feat: Add search client based on #1978 * moved the filtering to the DB level using zod and tidied up some imports. * minor fix * minor fix * fix typo --------- Co-authored-by: Bernd Storath <999999bst@gmail.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative w-60 md:mr-2">
|
||||||
|
<div class="relative flex h-full items-center">
|
||||||
|
<MagnifyingGlassIcon
|
||||||
|
class="absolute left-2.5 h-4 w-4 text-gray-400 dark:text-neutral-500"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="searchQuery"
|
||||||
|
type="text"
|
||||||
|
:placeholder="$t('client.search')"
|
||||||
|
class="w-full rounded bg-white py-2 pr-8 text-sm text-gray-900 shadow-sm ring-1 ring-gray-300 transition-all placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-red-600 dark:bg-neutral-800 dark:text-white dark:ring-neutral-700 dark:placeholder:text-neutral-500 dark:focus:ring-red-700"
|
||||||
|
@input="updateSearch"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="searchQuery"
|
||||||
|
class="absolute right-2 flex h-5 w-5 items-center justify-center rounded-full bg-gray-200 text-gray-600 hover:bg-gray-300 hover:text-gray-800 dark:bg-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-600 dark:hover:text-neutral-100"
|
||||||
|
aria-label="Clear search"
|
||||||
|
@click="clearSearch"
|
||||||
|
>
|
||||||
|
<IconsClose class="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const clientsStore = useClientsStore();
|
||||||
|
const searchQuery = ref('');
|
||||||
|
|
||||||
|
const updateSearch = useDebounceFn(() => {
|
||||||
|
clientsStore.setSearchQuery(searchQuery.value);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
function clearSearch() {
|
||||||
|
searchQuery.value = '';
|
||||||
|
clientsStore.setSearchQuery('');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-shrink-0 space-x-1 md:block">
|
<div class="flex flex-shrink-0 items-center space-x-2">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<PanelHead>
|
<PanelHead>
|
||||||
<PanelHeadTitle :text="$t('pages.clients')" />
|
<PanelHeadTitle :text="$t('pages.clients')" />
|
||||||
<PanelHeadBoat>
|
<PanelHeadBoat>
|
||||||
|
<ClientsSearch />
|
||||||
<ClientsSort />
|
<ClientsSort />
|
||||||
<ClientsNew />
|
<ClientsNew />
|
||||||
</PanelHeadBoat>
|
</PanelHeadBoat>
|
||||||
|
|||||||
@@ -31,8 +31,13 @@ export const useClientsStore = defineStore('Clients', () => {
|
|||||||
const clients = ref<null | LocalClient[]>(null);
|
const clients = ref<null | LocalClient[]>(null);
|
||||||
const clientsPersist = ref<Record<string, ClientPersist>>({});
|
const clientsPersist = ref<Record<string, ClientPersist>>({});
|
||||||
|
|
||||||
|
const searchParams = ref({
|
||||||
|
filter: undefined as string | undefined,
|
||||||
|
});
|
||||||
|
|
||||||
const { data: _clients, refresh: _refresh } = useFetch('/api/client', {
|
const { data: _clients, refresh: _refresh } = useFetch('/api/client', {
|
||||||
method: 'get',
|
method: 'get',
|
||||||
|
params: searchParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: rewrite
|
// TODO: rewrite
|
||||||
@@ -120,6 +125,7 @@ export const useClientsStore = defineStore('Clients', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: move sort to backend
|
||||||
if (transformedClients !== undefined) {
|
if (transformedClients !== undefined) {
|
||||||
transformedClients = sortByProperty(
|
transformedClients = sortByProperty(
|
||||||
transformedClients,
|
transformedClients,
|
||||||
@@ -130,5 +136,11 @@ export const useClientsStore = defineStore('Clients', () => {
|
|||||||
|
|
||||||
clients.value = transformedClients ?? null;
|
clients.value = transformedClients ?? null;
|
||||||
}
|
}
|
||||||
return { clients, clientsPersist, refresh, _clients };
|
|
||||||
|
function setSearchQuery(filter: string) {
|
||||||
|
clients.value = null;
|
||||||
|
searchParams.value.filter = filter || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { clients, clientsPersist, refresh, _clients, setSearchQuery };
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -116,7 +116,8 @@
|
|||||||
"dnsDesc": "DNS server clients will use (overrides global config)",
|
"dnsDesc": "DNS server clients will use (overrides global config)",
|
||||||
"notConnected": "Client not connected",
|
"notConnected": "Client not connected",
|
||||||
"endpoint": "Endpoint",
|
"endpoint": "Endpoint",
|
||||||
"endpointDesc": "IP of the client from which the WireGuard connection is established"
|
"endpointDesc": "IP of the client from which the WireGuard connection is established",
|
||||||
|
"search": "Search clients..."
|
||||||
},
|
},
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"change": "Change",
|
"change": "Change",
|
||||||
|
|||||||
@@ -112,7 +112,8 @@
|
|||||||
"persistentKeepaliveDesc": "设置保活数据包的发送间隔(秒)。0表示禁用",
|
"persistentKeepaliveDesc": "设置保活数据包的发送间隔(秒)。0表示禁用",
|
||||||
"hooks": "钩子脚本",
|
"hooks": "钩子脚本",
|
||||||
"hooksDescription": "钩子脚本仅在使用wg-quick时有效",
|
"hooksDescription": "钩子脚本仅在使用wg-quick时有效",
|
||||||
"hooksLeaveEmpty": "如果不使用wg-quick,请留空此字段"
|
"hooksLeaveEmpty": "如果不使用wg-quick,请留空此字段",
|
||||||
|
"search": "搜索客户端..."
|
||||||
},
|
},
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"change": "确认修改",
|
"change": "确认修改",
|
||||||
|
|||||||
@@ -113,7 +113,8 @@
|
|||||||
"hooks": "掛鉤",
|
"hooks": "掛鉤",
|
||||||
"hooksDescription": "掛鉤僅適用於wg-quick",
|
"hooksDescription": "掛鉤僅適用於wg-quick",
|
||||||
"hooksLeaveEmpty": "僅適用於wg-quick,否則請留空",
|
"hooksLeaveEmpty": "僅適用於wg-quick,否則請留空",
|
||||||
"dnsDesc": "客戶端使用的域名系統伺服器(取代全局配置)"
|
"dnsDesc": "客戶端使用的域名系統伺服器(取代全局配置)",
|
||||||
|
"search": "搜尋客戶端..."
|
||||||
},
|
},
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"change": "更改",
|
"change": "更改",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default defineNuxtConfig({
|
|||||||
'@pinia/nuxt',
|
'@pinia/nuxt',
|
||||||
'@eschricht/nuxt-color-mode',
|
'@eschricht/nuxt-color-mode',
|
||||||
'radix-vue/nuxt',
|
'radix-vue/nuxt',
|
||||||
|
'@vueuse/nuxt',
|
||||||
'@nuxt/eslint',
|
'@nuxt/eslint',
|
||||||
],
|
],
|
||||||
colorMode: {
|
colorMode: {
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
"@phc/format": "^1.0.0",
|
"@phc/format": "^1.0.0",
|
||||||
"@pinia/nuxt": "^0.11.2",
|
"@pinia/nuxt": "^0.11.2",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
|
"@vueuse/core": "^13.9.0",
|
||||||
|
"@vueuse/nuxt": "^13.9.0",
|
||||||
"apexcharts": "^5.3.5",
|
"apexcharts": "^5.3.5",
|
||||||
"argon2": "^0.44.0",
|
"argon2": "^0.44.0",
|
||||||
"cidr-tools": "^11.0.3",
|
"cidr-tools": "^11.0.3",
|
||||||
|
|||||||
Generated
+54
@@ -35,6 +35,12 @@ importers:
|
|||||||
'@tailwindcss/forms':
|
'@tailwindcss/forms':
|
||||||
specifier: ^0.5.10
|
specifier: ^0.5.10
|
||||||
version: 0.5.10(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))
|
version: 0.5.10(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))
|
||||||
|
'@vueuse/core':
|
||||||
|
specifier: ^13.9.0
|
||||||
|
version: 13.9.0(vue@3.5.22(typescript@5.9.3))
|
||||||
|
'@vueuse/nuxt':
|
||||||
|
specifier: ^13.9.0
|
||||||
|
version: 13.9.0(magicast@0.3.5)(nuxt@3.19.3(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.7.2)(@vue/compiler-sfc@3.5.22)(db0@0.3.4(@libsql/client@0.15.15)(drizzle-orm@0.44.6(@libsql/client@0.15.15)))(drizzle-orm@0.44.6(@libsql/client@0.15.15))(eslint@9.37.0(jiti@1.21.7))(ioredis@5.8.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(vite@7.1.9(@types/node@24.7.2)(jiti@1.21.7)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue-tsc@3.1.1(typescript@5.9.3))(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
|
||||||
apexcharts:
|
apexcharts:
|
||||||
specifier: ^5.3.5
|
specifier: ^5.3.5
|
||||||
version: 5.3.5
|
version: 5.3.5
|
||||||
@@ -1874,6 +1880,9 @@ packages:
|
|||||||
'@types/web-bluetooth@0.0.20':
|
'@types/web-bluetooth@0.0.20':
|
||||||
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
|
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
|
||||||
|
|
||||||
|
'@types/web-bluetooth@0.0.21':
|
||||||
|
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
|
||||||
|
|
||||||
'@types/ws@8.18.1':
|
'@types/ws@8.18.1':
|
||||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||||
|
|
||||||
@@ -2155,12 +2164,31 @@ packages:
|
|||||||
'@vueuse/core@10.11.1':
|
'@vueuse/core@10.11.1':
|
||||||
resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==}
|
resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==}
|
||||||
|
|
||||||
|
'@vueuse/core@13.9.0':
|
||||||
|
resolution: {integrity: sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.5.0
|
||||||
|
|
||||||
'@vueuse/metadata@10.11.1':
|
'@vueuse/metadata@10.11.1':
|
||||||
resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==}
|
resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==}
|
||||||
|
|
||||||
|
'@vueuse/metadata@13.9.0':
|
||||||
|
resolution: {integrity: sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==}
|
||||||
|
|
||||||
|
'@vueuse/nuxt@13.9.0':
|
||||||
|
resolution: {integrity: sha512-n/9BRU3nLl2mVI6rYbB3jOctCmQD0xT799hXPCwCn1PyvK7r6O9Nt1dxfVCMfKCDAiCi8Fz2IqPC6Zs2Dv1pVA==}
|
||||||
|
peerDependencies:
|
||||||
|
nuxt: ^3.0.0 || ^4.0.0-0
|
||||||
|
vue: ^3.5.0
|
||||||
|
|
||||||
'@vueuse/shared@10.11.1':
|
'@vueuse/shared@10.11.1':
|
||||||
resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
|
resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
|
||||||
|
|
||||||
|
'@vueuse/shared@13.9.0':
|
||||||
|
resolution: {integrity: sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.5.0
|
||||||
|
|
||||||
'@yr/monotone-cubic-spline@1.0.3':
|
'@yr/monotone-cubic-spline@1.0.3':
|
||||||
resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==}
|
resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==}
|
||||||
|
|
||||||
@@ -7190,6 +7218,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/web-bluetooth@0.0.20': {}
|
'@types/web-bluetooth@0.0.20': {}
|
||||||
|
|
||||||
|
'@types/web-bluetooth@0.0.21': {}
|
||||||
|
|
||||||
'@types/ws@8.18.1':
|
'@types/ws@8.18.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 24.7.2
|
'@types/node': 24.7.2
|
||||||
@@ -7558,8 +7588,28 @@ snapshots:
|
|||||||
- '@vue/composition-api'
|
- '@vue/composition-api'
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
|
'@vueuse/core@13.9.0(vue@3.5.22(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
'@types/web-bluetooth': 0.0.21
|
||||||
|
'@vueuse/metadata': 13.9.0
|
||||||
|
'@vueuse/shared': 13.9.0(vue@3.5.22(typescript@5.9.3))
|
||||||
|
vue: 3.5.22(typescript@5.9.3)
|
||||||
|
|
||||||
'@vueuse/metadata@10.11.1': {}
|
'@vueuse/metadata@10.11.1': {}
|
||||||
|
|
||||||
|
'@vueuse/metadata@13.9.0': {}
|
||||||
|
|
||||||
|
'@vueuse/nuxt@13.9.0(magicast@0.3.5)(nuxt@3.19.3(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.7.2)(@vue/compiler-sfc@3.5.22)(db0@0.3.4(@libsql/client@0.15.15)(drizzle-orm@0.44.6(@libsql/client@0.15.15)))(drizzle-orm@0.44.6(@libsql/client@0.15.15))(eslint@9.37.0(jiti@1.21.7))(ioredis@5.8.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(vite@7.1.9(@types/node@24.7.2)(jiti@1.21.7)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue-tsc@3.1.1(typescript@5.9.3))(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
'@nuxt/kit': 3.19.3(magicast@0.3.5)
|
||||||
|
'@vueuse/core': 13.9.0(vue@3.5.22(typescript@5.9.3))
|
||||||
|
'@vueuse/metadata': 13.9.0
|
||||||
|
local-pkg: 1.1.2
|
||||||
|
nuxt: 3.19.3(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.7.2)(@vue/compiler-sfc@3.5.22)(db0@0.3.4(@libsql/client@0.15.15)(drizzle-orm@0.44.6(@libsql/client@0.15.15)))(drizzle-orm@0.44.6(@libsql/client@0.15.15))(eslint@9.37.0(jiti@1.21.7))(ioredis@5.8.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(vite@7.1.9(@types/node@24.7.2)(jiti@1.21.7)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue-tsc@3.1.1(typescript@5.9.3))(yaml@2.8.1)
|
||||||
|
vue: 3.5.22(typescript@5.9.3)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- magicast
|
||||||
|
|
||||||
'@vueuse/shared@10.11.1(vue@3.5.22(typescript@5.9.3))':
|
'@vueuse/shared@10.11.1(vue@3.5.22(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
vue-demi: 0.14.10(vue@3.5.22(typescript@5.9.3))
|
vue-demi: 0.14.10(vue@3.5.22(typescript@5.9.3))
|
||||||
@@ -7567,6 +7617,10 @@ snapshots:
|
|||||||
- '@vue/composition-api'
|
- '@vue/composition-api'
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
|
'@vueuse/shared@13.9.0(vue@3.5.22(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
vue: 3.5.22(typescript@5.9.3)
|
||||||
|
|
||||||
'@yr/monotone-cubic-spline@1.0.3': {}
|
'@yr/monotone-cubic-spline@1.0.3': {}
|
||||||
|
|
||||||
abbrev@3.0.1: {}
|
abbrev@3.0.1: {}
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
export default definePermissionEventHandler('clients', 'custom', ({ user }) => {
|
import { ClientQuerySchema } from '#db/repositories/client/types';
|
||||||
if (user.role === roles.ADMIN) {
|
|
||||||
return WireGuard.getAllClients();
|
export default definePermissionEventHandler(
|
||||||
|
'clients',
|
||||||
|
'custom',
|
||||||
|
async ({ event, user }) => {
|
||||||
|
const { filter } = await getValidatedQuery(
|
||||||
|
event,
|
||||||
|
validateZod(ClientQuerySchema, event)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (user.role === roles.ADMIN) {
|
||||||
|
return WireGuard.getAllClients(filter);
|
||||||
|
}
|
||||||
|
return WireGuard.getClientsForUser(user.id, filter);
|
||||||
}
|
}
|
||||||
return WireGuard.getClientsForUser(user.id);
|
);
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { eq, sql } from 'drizzle-orm';
|
import { eq, sql, or, like, and } from 'drizzle-orm';
|
||||||
import { containsCidr, parseCidr } from 'cidr-tools';
|
import { containsCidr, parseCidr } from 'cidr-tools';
|
||||||
import { client } from './schema';
|
import { client } from './schema';
|
||||||
import type {
|
import type {
|
||||||
@@ -42,6 +42,39 @@ function createPreparedStatement(db: DBType) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.prepare(),
|
.prepare(),
|
||||||
|
findAllPublicFiltered: db.query.client
|
||||||
|
.findMany({
|
||||||
|
where: or(
|
||||||
|
like(client.name, sql.placeholder('filter')),
|
||||||
|
like(client.ipv4Address, sql.placeholder('filter')),
|
||||||
|
like(client.ipv6Address, sql.placeholder('filter'))
|
||||||
|
),
|
||||||
|
with: {
|
||||||
|
oneTimeLink: true,
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
privateKey: false,
|
||||||
|
preSharedKey: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.prepare(),
|
||||||
|
findByUserIdFiltered: db.query.client
|
||||||
|
.findMany({
|
||||||
|
where: and(
|
||||||
|
eq(client.userId, sql.placeholder('userId')),
|
||||||
|
or(
|
||||||
|
like(client.name, sql.placeholder('filter')),
|
||||||
|
like(client.ipv4Address, sql.placeholder('filter')),
|
||||||
|
like(client.ipv6Address, sql.placeholder('filter'))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
with: { oneTimeLink: true },
|
||||||
|
columns: {
|
||||||
|
privateKey: false,
|
||||||
|
preSharedKey: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.prepare(),
|
||||||
toggle: db
|
toggle: db
|
||||||
.update(client)
|
.update(client)
|
||||||
.set({ enabled: sql.placeholder('enabled') as never as boolean })
|
.set({ enabled: sql.placeholder('enabled') as never as boolean })
|
||||||
@@ -96,6 +129,41 @@ export class ClientService {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get clients based on user ID and filter conditions
|
||||||
|
*/
|
||||||
|
async getForUserFiltered(userId: ID, filter: string) {
|
||||||
|
const filterPattern = `%${filter.toLowerCase()}%`;
|
||||||
|
|
||||||
|
const result = await this.#statements.findByUserIdFiltered.execute({
|
||||||
|
userId,
|
||||||
|
filter: filterPattern,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.map((row) => ({
|
||||||
|
...row,
|
||||||
|
createdAt: new Date(row.createdAt),
|
||||||
|
updatedAt: new Date(row.updatedAt),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all clients based on filter conditions without sensitive data
|
||||||
|
*/
|
||||||
|
async getAllPublicFiltered(filter: string) {
|
||||||
|
const filterPattern = `%${filter.toLowerCase()}%`;
|
||||||
|
|
||||||
|
const result = await this.#statements.findAllPublicFiltered.execute({
|
||||||
|
filter: filterPattern,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.map((row) => ({
|
||||||
|
...row,
|
||||||
|
createdAt: new Date(row.createdAt),
|
||||||
|
updatedAt: new Date(row.updatedAt),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
get(id: ID) {
|
get(id: ID) {
|
||||||
return this.#statements.findById.execute({ id });
|
return this.#statements.findById.execute({ id });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ const address6 = z
|
|||||||
.min(1, { message: t('zod.client.address6') })
|
.min(1, { message: t('zod.client.address6') })
|
||||||
.pipe(safeStringRefine);
|
.pipe(safeStringRefine);
|
||||||
|
|
||||||
|
const filter = z.string().optional();
|
||||||
|
|
||||||
const serverAllowedIps = z.array(AddressSchema, {
|
const serverAllowedIps = z.array(AddressSchema, {
|
||||||
message: t('zod.client.serverAllowedIps'),
|
message: t('zod.client.serverAllowedIps'),
|
||||||
});
|
});
|
||||||
@@ -50,6 +52,12 @@ export const ClientCreateSchema = z.object({
|
|||||||
|
|
||||||
export type ClientCreateType = z.infer<typeof ClientCreateSchema>;
|
export type ClientCreateType = z.infer<typeof ClientCreateSchema>;
|
||||||
|
|
||||||
|
export const ClientQuerySchema = z.object({
|
||||||
|
filter: filter,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ClientQueryType = z.infer<typeof ClientQuerySchema>;
|
||||||
|
|
||||||
export const ClientUpdateSchema = schemaForType<UpdateClientType>()(
|
export const ClientUpdateSchema = schemaForType<UpdateClientType>()(
|
||||||
z.object({
|
z.object({
|
||||||
name: name,
|
name: name,
|
||||||
|
|||||||
@@ -61,10 +61,15 @@ class WireGuard {
|
|||||||
WG_DEBUG('Config synced successfully.');
|
WG_DEBUG('Config synced successfully.');
|
||||||
}
|
}
|
||||||
|
|
||||||
async getClientsForUser(userId: ID) {
|
async getClientsForUser(userId: ID, filter?: string) {
|
||||||
const wgInterface = await Database.interfaces.get();
|
const wgInterface = await Database.interfaces.get();
|
||||||
|
|
||||||
const dbClients = await Database.clients.getForUser(userId);
|
let dbClients;
|
||||||
|
if (filter?.trim()) {
|
||||||
|
dbClients = await Database.clients.getForUserFiltered(userId, filter);
|
||||||
|
} else {
|
||||||
|
dbClients = await Database.clients.getForUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
const clients = dbClients.map((client) => ({
|
const clients = dbClients.map((client) => ({
|
||||||
...client,
|
...client,
|
||||||
@@ -104,9 +109,16 @@ class WireGuard {
|
|||||||
return clientDump;
|
return clientDump;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllClients() {
|
async getAllClients(filter?: string) {
|
||||||
const wgInterface = await Database.interfaces.get();
|
const wgInterface = await Database.interfaces.get();
|
||||||
const dbClients = await Database.clients.getAllPublic();
|
|
||||||
|
let dbClients;
|
||||||
|
if (filter?.trim()) {
|
||||||
|
dbClients = await Database.clients.getAllPublicFiltered(filter);
|
||||||
|
} else {
|
||||||
|
dbClients = await Database.clients.getAllPublic();
|
||||||
|
}
|
||||||
|
|
||||||
const clients = dbClients.map((client) => ({
|
const clients = dbClients.map((client) => ({
|
||||||
...client,
|
...client,
|
||||||
latestHandshakeAt: null as Date | null,
|
latestHandshakeAt: null as Date | null,
|
||||||
|
|||||||
Reference in New Issue
Block a user