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>
<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" />
<span class="text-sm">{{ $t('client.sort') }}</span>
</BasePrimaryButton>
@ -11,7 +11,7 @@ const globalStore = useGlobalStore();
const clientsStore = useClientsStore();
function toggleSort() {
globalStore.sortClient = !globalStore.sortClient;
globalStore.sortClient = globalStore.sortClient === 'asc' ? 'desc' : 'asc';
clientsStore.refresh().catch(console.error);
}
</script>

24
src/app/stores/clients.ts

@ -31,9 +31,12 @@ export const useClientsStore = defineStore('Clients', () => {
const clients = ref<null | LocalClient[]>(null);
const clientsPersist = ref<Record<string, ClientPersist>>({});
const searchParams = ref({
filter: undefined as string | undefined,
});
const filter = ref<string | undefined>(undefined);
const searchParams = computed(() => ({
filter: filter.value,
sort: globalStore.sortClient,
}));
const { data: _clients, refresh: _refresh } = useFetch('/api/client', {
method: 'get',
@ -43,7 +46,7 @@ export const useClientsStore = defineStore('Clients', () => {
// TODO: rewrite
async function refresh({ updateCharts = false } = {}) {
await _refresh();
let transformedClients = _clients.value?.map((client) => {
const transformedClients = _clients.value?.map((client) => {
let avatar = undefined;
if (client.name.includes('@') && client.name.includes('.')) {
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;
}
function setSearchQuery(filter: string) {
function setSearchQuery(query: string) {
clients.value = null;
searchParams.value.filter = filter || undefined;
filter.value = query || undefined;
}
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', {
default: () => false,

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

@ -4,14 +4,14 @@ export default definePermissionEventHandler(
'clients',
'custom',
async ({ event, user }) => {
const { filter } = await getValidatedQuery(
const { filter, sort } = await getValidatedQuery(
event,
validateZod(ClientQuerySchema, event)
);
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 {
ClientCreateFromExistingType,
ClientCreateType,
ClientQueryType,
UpdateClientType,
} from './types';
import type { DBType } from '#db/sqlite';
@ -18,63 +19,9 @@ function createPreparedStatement(db: DBType) {
},
})
.prepare(),
findAllPublic: db.query.client
.findMany({
with: {
oneTimeLink: true,
},
columns: {
privateKey: false,
preSharedKey: false,
},
})
.prepare(),
findById: db.query.client
.findFirst({ where: eq(client.id, sql.placeholder('id')) })
.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
.update(client)
.set({ enabled: sql.placeholder('enabled') as never as boolean })
@ -96,15 +43,6 @@ export class ClientService {
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.
*/
@ -120,25 +58,40 @@ export class ClientService {
/**
* Returns all clients without sensitive data
*/
async getAllPublic() {
const result = await this.#statements.findAllPublic.execute();
return result.map((row) => ({
...row,
createdAt: new Date(row.createdAt),
updatedAt: new Date(row.updatedAt),
}));
}
async getAllPublic({ filter, sort }: ClientQueryType) {
const filters = [];
/**
* Get clients based on user ID and filter conditions
*/
async getForUserFiltered(userId: ID, filter: string) {
const filterPattern = `%${filter.toLowerCase()}%`;
if (filter?.trim()) {
const filterPattern = `%${filter?.toLowerCase()}%`;
filters.push(
or(
like(client.name, filterPattern),
like(client.ipv4Address, filterPattern),
like(client.ipv6Address, filterPattern)
)
);
}
const result = await this.#statements.findByUserIdFiltered.execute({
userId,
filter: filterPattern,
});
const result = await this.#db.query.client
.findMany({
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) => ({
...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) {
const filterPattern = `%${filter.toLowerCase()}%`;
async getAllForUser(userId: ID, { filter, sort }: ClientQueryType) {
const filters = [];
const result = await this.#statements.findAllPublicFiltered.execute({
filter: filterPattern,
});
if (filter?.trim()) {
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) => ({
...row,

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

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

19
src/server/utils/WireGuard.ts

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

Loading…
Cancel
Save