Browse Source

moved the filtering to the DB level using zod and tidied up some imports.

pull/2170/head
yuWorm 10 months ago
committed by Bernd Storath
parent
commit
860f72ece2
  1. 6
      src/app/components/Clients/Search.vue
  2. 14
      src/server/api/client/index.get.ts
  3. 76
      src/server/database/repositories/client/service.ts
  4. 8
      src/server/database/repositories/client/types.ts
  5. 43
      src/server/utils/WireGuard.ts

6
src/app/components/Clients/Search.vue

@ -14,8 +14,8 @@
<button <button
v-if="searchQuery" 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" 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"
@click="clearSearch"
aria-label="Clear search" aria-label="Clear search"
@click="clearSearch"
> >
<IconsClose class="h-3 w-3" /> <IconsClose class="h-3 w-3" />
</button> </button>
@ -24,10 +24,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue';
import { MagnifyingGlassIcon } from '@heroicons/vue/24/outline';
import IconsClose from '../Icons/Close.vue';
const clientsStore = useClientsStore(); const clientsStore = useClientsStore();
const searchQuery = ref(''); const searchQuery = ref('');

14
src/server/api/client/index.get.ts

@ -1,11 +1,17 @@
import { ClientQuerySchema } from '#db/repositories/client/types';
export default definePermissionEventHandler( export default definePermissionEventHandler(
'clients', 'clients',
'custom', 'custom',
({ event, user }) => { async ({ event, user }) => {
const { filter } = getQuery(event); const { filter } = await getValidatedQuery(
event,
validateZod(ClientQuerySchema, event)
);
if (user.role === roles.ADMIN) { if (user.role === roles.ADMIN) {
return WireGuard.filterClients(null, filter as string); return WireGuard.getAllClients(filter);
} }
return WireGuard.filterClients(user.id, filter as string); return WireGuard.getClientsForUser(user.id, filter);
} }
); );

76
src/server/database/repositories/client/service.ts

@ -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,47 @@ export class ClientService {
})); }));
} }
/**
* Get clients based on user ID and filter conditions
*/
async getForUserFiltered(userId: ID, filter: string) {
if (!filter.trim()) {
return this.getForUser(userId);
}
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 public clients based on filter conditions
*/
async getAllPublicFiltered(filter: string) {
if (!filter.trim()) {
return this.getAllPublic();
}
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 });
} }

8
src/server/database/repositories/client/types.ts

@ -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().default('');
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,

43
src/server/utils/WireGuard.ts

@ -61,11 +61,14 @@ 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();
let dbClients;
const dbClients = await Database.clients.getForUser(userId); 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,
latestHandshakeAt: null as Date | null, latestHandshakeAt: null as Date | null,
@ -104,9 +107,14 @@ 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,
@ -134,29 +142,6 @@ class WireGuard {
return clients; return clients;
} }
async filterClients(userId: ID | null, filter: string) {
let clients;
if (userId != null) {
await this.getClientsForUser(userId);
} else {
clients = await this.getAllClients();
}
if (!clients) {
return [];
}
if (!filter.trim()) return clients;
const searchTerm = filter.toLowerCase().trim();
return clients?.filter((client) => {
const nameMatches = client.name.toLowerCase().includes(searchTerm);
const ipv4Matches = client.ipv4Address.toLowerCase().includes(searchTerm);
const ipv6Matches = client.ipv6Address.toLowerCase().includes(searchTerm);
return nameMatches || ipv4Matches || ipv6Matches;
});
}
async getClientConfiguration({ clientId }: { clientId: ID }) { async getClientConfiguration({ clientId }: { clientId: ID }) {
const wgInterface = await Database.interfaces.get(); const wgInterface = await Database.interfaces.get();
const userConfig = await Database.userConfigs.get(); const userConfig = await Database.userConfigs.get();

Loading…
Cancel
Save