Browse Source

move sort to backend

pull/2671/head
Bernd Storath 3 weeks ago
parent
commit
a00996da7e
  1. 4
      src/app/components/Clients/Sort.vue
  2. 24
      src/app/stores/clients.ts
  3. 2
      src/app/stores/global.ts
  4. 6
      src/server/api/client/index.get.ts
  5. 151
      src/server/database/repositories/client/service.ts
  6. 9
      src/server/database/repositories/client/types.ts
  7. 19
      src/server/utils/WireGuard.ts

4
src/app/components/Clients/Sort.vue

@ -1,6 +1,6 @@
<template> <template>
<BasePrimaryButton @click="toggleSort"> <BasePrimaryButton @click="toggleSort">
<IconsArrowDown v-if="globalStore.sortClient === true" class="mr-2 w-4" /> <IconsArrowDown v-if="globalStore.sortClient === 'asc'" class="mr-2 w-4" />
<IconsArrowUp v-else class="mr-2 w-4" /> <IconsArrowUp v-else class="mr-2 w-4" />
<span class="text-sm">{{ $t('client.sort') }}</span> <span class="text-sm">{{ $t('client.sort') }}</span>
</BasePrimaryButton> </BasePrimaryButton>
@ -11,7 +11,7 @@ const globalStore = useGlobalStore();
const clientsStore = useClientsStore(); const clientsStore = useClientsStore();
function toggleSort() { function toggleSort() {
globalStore.sortClient = !globalStore.sortClient; globalStore.sortClient = globalStore.sortClient === 'asc' ? 'desc' : 'asc';
clientsStore.refresh().catch(console.error); clientsStore.refresh().catch(console.error);
} }
</script> </script>

24
src/app/stores/clients.ts

@ -31,9 +31,12 @@ 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({ const filter = ref<string | undefined>(undefined);
filter: undefined as string | undefined,
}); const searchParams = computed(() => ({
filter: filter.value,
sort: globalStore.sortClient,
}));
const { data: _clients, refresh: _refresh } = useFetch('/api/client', { const { data: _clients, refresh: _refresh } = useFetch('/api/client', {
method: 'get', method: 'get',
@ -43,7 +46,7 @@ export const useClientsStore = defineStore('Clients', () => {
// TODO: rewrite // TODO: rewrite
async function refresh({ updateCharts = false } = {}) { async function refresh({ updateCharts = false } = {}) {
await _refresh(); await _refresh();
let transformedClients = _clients.value?.map((client) => { const transformedClients = _clients.value?.map((client) => {
let avatar = undefined; let avatar = undefined;
if (client.name.includes('@') && client.name.includes('.')) { if (client.name.includes('@') && client.name.includes('.')) {
avatar = `https://gravatar.com/avatar/${sha256(client.name.toLowerCase().trim())}.jpg`; avatar = `https://gravatar.com/avatar/${sha256(client.name.toLowerCase().trim())}.jpg`;
@ -126,21 +129,12 @@ export const useClientsStore = defineStore('Clients', () => {
}; };
}); });
// TODO: move sort to backend
if (transformedClients !== undefined) {
transformedClients = sortByProperty(
transformedClients,
'name',
globalStore.sortClient
);
}
clients.value = transformedClients ?? null; clients.value = transformedClients ?? null;
} }
function setSearchQuery(filter: string) { function setSearchQuery(query: string) {
clients.value = null; clients.value = null;
searchParams.value.filter = filter || undefined; filter.value = query || undefined;
} }
return { clients, clientsPersist, refresh, _clients, setSearchQuery }; return { clients, clientsPersist, refresh, _clients, setSearchQuery };

2
src/app/stores/global.ts

@ -6,7 +6,7 @@ export const useGlobalStore = defineStore('Global', () => {
} }
); );
const sortClient = ref(true); // Sort clients by name, true = asc, false = desc const sortClient = ref<'asc' | 'desc'>('asc');
const uiShowCharts = useCookie<boolean>('uiShowCharts', { const uiShowCharts = useCookie<boolean>('uiShowCharts', {
default: () => false, default: () => false,

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

@ -4,14 +4,14 @@ export default definePermissionEventHandler(
'clients', 'clients',
'custom', 'custom',
async ({ event, user }) => { async ({ event, user }) => {
const { filter } = await getValidatedQuery( const { filter, sort } = await getValidatedQuery(
event, event,
validateZod(ClientQuerySchema, event) validateZod(ClientQuerySchema, event)
); );
if (user.role === roles.ADMIN) { if (user.role === roles.ADMIN) {
return WireGuard.getAllClients(filter); return WireGuard.getAllClients({ filter, sort });
} }
return WireGuard.getClientsForUser(user.id, filter); return WireGuard.getClientsForUser(user.id, { filter, sort });
} }
); );

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

@ -4,6 +4,7 @@ import { client } from './schema';
import type { import type {
ClientCreateFromExistingType, ClientCreateFromExistingType,
ClientCreateType, ClientCreateType,
ClientQueryType,
UpdateClientType, UpdateClientType,
} from './types'; } from './types';
import type { DBType } from '#db/sqlite'; import type { DBType } from '#db/sqlite';
@ -18,63 +19,9 @@ function createPreparedStatement(db: DBType) {
}, },
}) })
.prepare(), .prepare(),
findAllPublic: db.query.client
.findMany({
with: {
oneTimeLink: true,
},
columns: {
privateKey: false,
preSharedKey: false,
},
})
.prepare(),
findById: db.query.client findById: db.query.client
.findFirst({ where: eq(client.id, sql.placeholder('id')) }) .findFirst({ where: eq(client.id, sql.placeholder('id')) })
.prepare(), .prepare(),
findByUserId: db.query.client
.findMany({
where: eq(client.userId, sql.placeholder('userId')),
with: { oneTimeLink: true },
columns: {
privateKey: false,
preSharedKey: false,
},
})
.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,15 +43,6 @@ export class ClientService {
this.#statements = createPreparedStatement(db); this.#statements = createPreparedStatement(db);
} }
async getForUser(userId: ID) {
const result = await this.#statements.findByUserId.execute({ userId });
return result.map((row) => ({
...row,
createdAt: new Date(row.createdAt),
updatedAt: new Date(row.updatedAt),
}));
}
/** /**
* Never return values directly from this function. Use {@link getAllPublic} instead. * Never return values directly from this function. Use {@link getAllPublic} instead.
*/ */
@ -120,25 +58,40 @@ export class ClientService {
/** /**
* Returns all clients without sensitive data * Returns all clients without sensitive data
*/ */
async getAllPublic() { async getAllPublic({ filter, sort }: ClientQueryType) {
const result = await this.#statements.findAllPublic.execute(); const filters = [];
return result.map((row) => ({
...row,
createdAt: new Date(row.createdAt),
updatedAt: new Date(row.updatedAt),
}));
}
/** if (filter?.trim()) {
* Get clients based on user ID and filter conditions const filterPattern = `%${filter?.toLowerCase()}%`;
*/ filters.push(
async getForUserFiltered(userId: ID, filter: string) { or(
const filterPattern = `%${filter.toLowerCase()}%`; like(client.name, filterPattern),
like(client.ipv4Address, filterPattern),
like(client.ipv6Address, filterPattern)
)
);
}
const result = await this.#statements.findByUserIdFiltered.execute({ const result = await this.#db.query.client
userId, .findMany({
filter: filterPattern, with: {
}); oneTimeLink: true,
},
where: and(...filters),
columns: {
privateKey: false,
preSharedKey: false,
},
orderBy: (t, { asc, desc }) => {
if (sort === 'desc') {
return desc(t.name);
} else {
// default to asc
return asc(t.name);
}
},
})
.execute();
return result.map((row) => ({ return result.map((row) => ({
...row, ...row,
@ -148,14 +101,40 @@ export class ClientService {
} }
/** /**
* Get all clients based on filter conditions without sensitive data * Returns all clients without sensitive data belonging to user
*/ */
async getAllPublicFiltered(filter: string) { async getAllForUser(userId: ID, { filter, sort }: ClientQueryType) {
const filterPattern = `%${filter.toLowerCase()}%`; const filters = [];
const result = await this.#statements.findAllPublicFiltered.execute({ if (filter?.trim()) {
filter: filterPattern, const filterPattern = `%${filter?.toLowerCase()}%`;
}); filters.push(
or(
like(client.name, filterPattern),
like(client.ipv4Address, filterPattern),
like(client.ipv6Address, filterPattern)
)
);
}
const result = await this.#db.query.client
.findMany({
where: and(eq(client.userId, userId), ...filters),
with: { oneTimeLink: true },
columns: {
privateKey: false,
preSharedKey: false,
},
orderBy: (t, { asc, desc }) => {
if (sort === 'desc') {
return desc(t.name);
} else {
// default to asc
return asc(t.name);
}
},
})
.execute();
return result.map((row) => ({ return result.map((row) => ({
...row, ...row,

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

@ -45,8 +45,6 @@ const address6 = z
.pipe(controlStringRefine) .pipe(controlStringRefine)
.refine((v) => isIPv6(v)); .refine((v) => isIPv6(v));
const filter = z.string().pipe(safeStringRefine).optional();
const serverAllowedIps = z.array(AddressSchema, { const serverAllowedIps = z.array(AddressSchema, {
message: t('zod.client.serverAllowedIps'), message: t('zod.client.serverAllowedIps'),
}); });
@ -58,8 +56,13 @@ export const ClientCreateSchema = z.object({
export type ClientCreateType = z.infer<typeof ClientCreateSchema>; export type ClientCreateType = z.infer<typeof ClientCreateSchema>;
const filter = z.string().pipe(safeStringRefine);
const sort = z.enum(['asc', 'desc']);
export const ClientQuerySchema = z.object({ export const ClientQuerySchema = z.object({
filter: filter, filter: filter.optional(),
sort: sort.optional(),
}); });
export type ClientQueryType = z.infer<typeof ClientQuerySchema>; export type ClientQueryType = z.infer<typeof ClientQuerySchema>;

19
src/server/utils/WireGuard.ts

@ -1,6 +1,7 @@
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import { createDebug } from 'obug'; import { createDebug } from 'obug';
import type { InterfaceType } from '#db/repositories/interface/types'; import type { InterfaceType } from '#db/repositories/interface/types';
import type { ClientQueryType } from '#db/repositories/client/types';
const WG_DEBUG = createDebug('WireGuard'); const WG_DEBUG = createDebug('WireGuard');
@ -78,15 +79,10 @@ class WireGuard {
WG_DEBUG('Config synced successfully.'); WG_DEBUG('Config synced successfully.');
} }
async getClientsForUser(userId: ID, filter?: string) { async getClientsForUser(userId: ID, query: ClientQueryType) {
const wgInterface = await Database.interfaces.get(); const wgInterface = await Database.interfaces.get();
let dbClients; const dbClients = await Database.clients.getAllForUser(userId, query);
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,
@ -126,15 +122,10 @@ class WireGuard {
return clientDump; return clientDump;
} }
async getAllClients(filter?: string) { async getAllClients(query: ClientQueryType = {}) {
const wgInterface = await Database.interfaces.get(); const wgInterface = await Database.interfaces.get();
let dbClients; const dbClients = await Database.clients.getAllPublic(query);
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,

Loading…
Cancel
Save