diff --git a/src/server/api/client/[clientId]/configuration.get.ts b/src/server/api/client/[clientId]/configuration.get.ts index e20645b5..7faa531a 100644 --- a/src/server/api/client/[clientId]/configuration.get.ts +++ b/src/server/api/client/[clientId]/configuration.get.ts @@ -1,30 +1,36 @@ import { ClientGetSchema } from '#db/repositories/client/types'; export default definePermissionEventHandler( - actions.CLIENT, - async ({ event }) => { + 'clients', + 'view', + async ({ event, checkPermissions }) => { const { clientId } = await getValidatedRouterParams( event, validateZod(ClientGetSchema) ); const client = await Database.clients.get(clientId); + checkPermissions(client); + if (!client) { throw createError({ statusCode: 404, statusMessage: 'Client not found', }); } + const config = await WireGuard.getClientConfiguration({ clientId }); const configName = client.name .replace(/[^a-zA-Z0-9_=+.-]/g, '-') .replace(/(-{2,}|-$)/g, '-') .replace(/-$/, '') .substring(0, 32); + setHeader( event, 'Content-Disposition', `attachment; filename="${configName || clientId}.conf"` ); + setHeader(event, 'Content-Type', 'text/plain'); return config; } diff --git a/src/server/api/client/[clientId]/disable.post.ts b/src/server/api/client/[clientId]/disable.post.ts index d4746e2d..2e1a58c0 100644 --- a/src/server/api/client/[clientId]/disable.post.ts +++ b/src/server/api/client/[clientId]/disable.post.ts @@ -1,12 +1,17 @@ import { ClientGetSchema } from '#db/repositories/client/types'; export default definePermissionEventHandler( - actions.CLIENT, - async ({ event }) => { + 'clients', + 'update', + async ({ event, checkPermissions }) => { const { clientId } = await getValidatedRouterParams( event, validateZod(ClientGetSchema) ); + + const client = await Database.clients.get(clientId); + checkPermissions(client); + await Database.clients.toggle(clientId, false); await WireGuard.saveConfig(); return { success: true }; diff --git a/src/server/api/client/[clientId]/enable.post.ts b/src/server/api/client/[clientId]/enable.post.ts index d4746e2d..2e1a58c0 100644 --- a/src/server/api/client/[clientId]/enable.post.ts +++ b/src/server/api/client/[clientId]/enable.post.ts @@ -1,12 +1,17 @@ import { ClientGetSchema } from '#db/repositories/client/types'; export default definePermissionEventHandler( - actions.CLIENT, - async ({ event }) => { + 'clients', + 'update', + async ({ event, checkPermissions }) => { const { clientId } = await getValidatedRouterParams( event, validateZod(ClientGetSchema) ); + + const client = await Database.clients.get(clientId); + checkPermissions(client); + await Database.clients.toggle(clientId, false); await WireGuard.saveConfig(); return { success: true }; diff --git a/src/server/api/client/[clientId]/generateOneTimeLink.post.ts b/src/server/api/client/[clientId]/generateOneTimeLink.post.ts index dc4287ad..618addf5 100644 --- a/src/server/api/client/[clientId]/generateOneTimeLink.post.ts +++ b/src/server/api/client/[clientId]/generateOneTimeLink.post.ts @@ -1,12 +1,17 @@ import { ClientGetSchema } from '#db/repositories/client/types'; export default definePermissionEventHandler( - actions.CLIENT, - async ({ event }) => { + 'clients', + 'update', + async ({ event, checkPermissions }) => { const { clientId } = await getValidatedRouterParams( event, validateZod(ClientGetSchema) ); + + const client = await Database.clients.get(clientId); + checkPermissions(client); + await Database.oneTimeLinks.generate(clientId); return { success: true }; } diff --git a/src/server/api/client/[clientId]/index.delete.ts b/src/server/api/client/[clientId]/index.delete.ts index 686a39ac..171ad9e2 100644 --- a/src/server/api/client/[clientId]/index.delete.ts +++ b/src/server/api/client/[clientId]/index.delete.ts @@ -1,12 +1,17 @@ import { ClientGetSchema } from '#db/repositories/client/types'; export default definePermissionEventHandler( - actions.CLIENT, - async ({ event }) => { + 'clients', + 'delete', + async ({ event, checkPermissions }) => { const { clientId } = await getValidatedRouterParams( event, validateZod(ClientGetSchema) ); + + const client = await Database.clients.get(clientId); + checkPermissions(client); + await Database.clients.delete(clientId); await WireGuard.saveConfig(); return { success: true }; diff --git a/src/server/api/client/[clientId]/index.get.ts b/src/server/api/client/[clientId]/index.get.ts index 9bdd8302..13941671 100644 --- a/src/server/api/client/[clientId]/index.get.ts +++ b/src/server/api/client/[clientId]/index.get.ts @@ -1,13 +1,17 @@ -import { ClientGetSchema } from '~~/server/database/repositories/client/types'; +import { ClientGetSchema } from '#db/repositories/client/types'; export default definePermissionEventHandler( - actions.CLIENT, - async ({ event }) => { + 'clients', + 'view', + async ({ event, checkPermissions }) => { const { clientId } = await getValidatedRouterParams( event, validateZod(ClientGetSchema, event) ); + const result = await Database.clients.get(clientId); + checkPermissions(result); + if (!result) { throw createError({ statusCode: 404, diff --git a/src/server/api/client/[clientId]/index.post.ts b/src/server/api/client/[clientId]/index.post.ts index 78ff38e3..21bdd4e2 100644 --- a/src/server/api/client/[clientId]/index.post.ts +++ b/src/server/api/client/[clientId]/index.post.ts @@ -4,8 +4,9 @@ import { } from '#db/repositories/client/types'; export default definePermissionEventHandler( - actions.CLIENT, - async ({ event }) => { + 'clients', + 'update', + async ({ event, checkPermissions }) => { const { clientId } = await getValidatedRouterParams( event, validateZod(ClientGetSchema) @@ -16,6 +17,9 @@ export default definePermissionEventHandler( validateZod(ClientUpdateSchema, event) ); + const client = await Database.clients.get(clientId); + checkPermissions(client); + await Database.clients.update(clientId, data); await WireGuard.saveConfig(); diff --git a/src/server/api/client/[clientId]/qrcode.svg.get.ts b/src/server/api/client/[clientId]/qrcode.svg.get.ts index 381054cd..19ad85e6 100644 --- a/src/server/api/client/[clientId]/qrcode.svg.get.ts +++ b/src/server/api/client/[clientId]/qrcode.svg.get.ts @@ -1,12 +1,17 @@ import { ClientGetSchema } from '#db/repositories/client/types'; export default definePermissionEventHandler( - actions.CLIENT, - async ({ event }) => { + 'clients', + 'view', + async ({ event, checkPermissions }) => { const { clientId } = await getValidatedRouterParams( event, validateZod(ClientGetSchema) ); + + const client = await Database.clients.get(clientId); + checkPermissions(client); + const svg = await WireGuard.getClientQRCodeSVG({ clientId }); setHeader(event, 'Content-Type', 'image/svg+xml'); return svg; diff --git a/src/server/api/client/index.get.ts b/src/server/api/client/index.get.ts index 9608b72b..296e0003 100644 --- a/src/server/api/client/index.get.ts +++ b/src/server/api/client/index.get.ts @@ -1,3 +1,6 @@ -export default definePermissionEventHandler(actions.CLIENT, () => { - return WireGuard.getClients(); +export default definePermissionEventHandler('clients', 'custom', ({ user }) => { + if (user.role === roles.ADMIN) { + return WireGuard.getAllClients(); + } + return WireGuard.getClientsForUser(user.id); }); diff --git a/src/server/api/client/index.post.ts b/src/server/api/client/index.post.ts index aa461746..ea4940fe 100644 --- a/src/server/api/client/index.post.ts +++ b/src/server/api/client/index.post.ts @@ -1,12 +1,14 @@ import { ClientCreateSchema } from '#db/repositories/client/types'; export default definePermissionEventHandler( - actions.CLIENT, + 'clients', + 'create', async ({ event }) => { const { name, expiresAt } = await readValidatedBody( event, validateZod(ClientCreateSchema) ); + await Database.clients.create({ name, expiresAt }); await WireGuard.saveConfig(); return { success: true }; diff --git a/src/server/database/migrations/0000_short_skin.sql b/src/server/database/migrations/0000_short_skin.sql index 921c4b5c..38283554 100644 --- a/src/server/database/migrations/0000_short_skin.sql +++ b/src/server/database/migrations/0000_short_skin.sql @@ -1,5 +1,6 @@ CREATE TABLE `clients_table` ( `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` integer NOT NULL, `name` text NOT NULL, `ipv4_address` text NOT NULL, `ipv6_address` text NOT NULL, @@ -14,19 +15,20 @@ CREATE TABLE `clients_table` ( `dns` text NOT NULL, `enabled` integer NOT NULL, `created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, - `updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL + `updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users_table`(`id`) ON UPDATE cascade ON DELETE restrict ); --> statement-breakpoint CREATE UNIQUE INDEX `clients_table_ipv4_address_unique` ON `clients_table` (`ipv4_address`);--> statement-breakpoint CREATE UNIQUE INDEX `clients_table_ipv6_address_unique` ON `clients_table` (`ipv6_address`);--> statement-breakpoint CREATE TABLE `general_table` ( `id` integer PRIMARY KEY DEFAULT 1 NOT NULL, - `setupStep` integer NOT NULL, + `setup_step` integer NOT NULL, `session_password` text NOT NULL, `session_timeout` integer NOT NULL, - `metricsPrometheus` integer NOT NULL, - `metricsJson` integer NOT NULL, - `metricsPassword` text, + `metrics_prometheus` integer NOT NULL, + `metrics_json` integer NOT NULL, + `metrics_password` text, `created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, `updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL ); @@ -61,10 +63,10 @@ CREATE TABLE `one_time_links_table` ( `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `one_time_link` text NOT NULL, `expires_at` text NOT NULL, - `clientId` integer NOT NULL, + `client_id` integer NOT NULL, `created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, `updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, - FOREIGN KEY (`clientId`) REFERENCES `clients_table`(`id`) ON UPDATE cascade ON DELETE cascade + FOREIGN KEY (`client_id`) REFERENCES `clients_table`(`id`) ON UPDATE cascade ON DELETE cascade ); --> statement-breakpoint CREATE UNIQUE INDEX `one_time_links_table_one_time_link_unique` ON `one_time_links_table` (`one_time_link`);--> statement-breakpoint diff --git a/src/server/database/migrations/0001_classy_the_stranger.sql b/src/server/database/migrations/0001_classy_the_stranger.sql index f305f0e7..a4bdf0f1 100644 --- a/src/server/database/migrations/0001_classy_the_stranger.sql +++ b/src/server/database/migrations/0001_classy_the_stranger.sql @@ -1,5 +1,5 @@ PRAGMA journal_mode=WAL;--> statement-breakpoint -INSERT INTO `general_table` (`setupStep`, `session_password`, `session_timeout`, `metricsPrometheus`, `metricsJson`) +INSERT INTO `general_table` (`setup_step`, `session_password`, `session_timeout`, `metrics_prometheus`, `metrics_json`) VALUES (1, hex(randomblob(256)), 3600, 0, 0); --> statement-breakpoint INSERT INTO `interfaces_table` (`name`, `device`, `port`, `private_key`, `public_key`, `ipv4_cidr`, `ipv6_cidr`, `mtu`, `enabled`) diff --git a/src/server/database/migrations/meta/0000_snapshot.json b/src/server/database/migrations/meta/0000_snapshot.json index 4d28e70c..1b217e6c 100644 --- a/src/server/database/migrations/meta/0000_snapshot.json +++ b/src/server/database/migrations/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "2c4694af-5916-430f-96d3-55aac2653e7e", + "id": "b1dde023-d141-4eab-9226-89a832b2ed2b", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "clients_table": { @@ -14,6 +14,13 @@ "notNull": true, "autoincrement": true }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, "name": { "name": "name", "type": "text", @@ -138,7 +145,21 @@ "isUnique": true } }, - "foreignKeys": {}, + "foreignKeys": { + "clients_table_user_id_users_table_id_fk": { + "name": "clients_table_user_id_users_table_id_fk", + "tableFrom": "clients_table", + "tableTo": "users_table", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, "compositePrimaryKeys": {}, "uniqueConstraints": {}, "checkConstraints": {} @@ -154,8 +175,8 @@ "autoincrement": false, "default": 1 }, - "setupStep": { - "name": "setupStep", + "setup_step": { + "name": "setup_step", "type": "integer", "primaryKey": false, "notNull": true, @@ -175,22 +196,22 @@ "notNull": true, "autoincrement": false }, - "metricsPrometheus": { - "name": "metricsPrometheus", + "metrics_prometheus": { + "name": "metrics_prometheus", "type": "integer", "primaryKey": false, "notNull": true, "autoincrement": false }, - "metricsJson": { - "name": "metricsJson", + "metrics_json": { + "name": "metrics_json", "type": "integer", "primaryKey": false, "notNull": true, "autoincrement": false }, - "metricsPassword": { - "name": "metricsPassword", + "metrics_password": { + "name": "metrics_password", "type": "text", "primaryKey": false, "notNull": false, @@ -415,8 +436,8 @@ "notNull": true, "autoincrement": false }, - "clientId": { - "name": "clientId", + "client_id": { + "name": "client_id", "type": "integer", "primaryKey": false, "notNull": true, @@ -449,12 +470,12 @@ } }, "foreignKeys": { - "one_time_links_table_clientId_clients_table_id_fk": { - "name": "one_time_links_table_clientId_clients_table_id_fk", + "one_time_links_table_client_id_clients_table_id_fk": { + "name": "one_time_links_table_client_id_clients_table_id_fk", "tableFrom": "one_time_links_table", "tableTo": "clients_table", "columnsFrom": [ - "clientId" + "client_id" ], "columnsTo": [ "id" diff --git a/src/server/database/migrations/meta/0001_snapshot.json b/src/server/database/migrations/meta/0001_snapshot.json index 07cb02f8..f04104fa 100644 --- a/src/server/database/migrations/meta/0001_snapshot.json +++ b/src/server/database/migrations/meta/0001_snapshot.json @@ -1,6 +1,6 @@ { - "id": "91d39ed5-2c45-4af6-ba39-4cd72ba71f6a", - "prevId": "2c4694af-5916-430f-96d3-55aac2653e7e", + "id": "720d420c-361f-4427-a45b-db0ca613934d", + "prevId": "b1dde023-d141-4eab-9226-89a832b2ed2b", "version": "6", "dialect": "sqlite", "tables": { @@ -14,6 +14,13 @@ "notNull": true, "autoincrement": true }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, "name": { "name": "name", "type": "text", @@ -138,7 +145,21 @@ "isUnique": true } }, - "foreignKeys": {}, + "foreignKeys": { + "clients_table_user_id_users_table_id_fk": { + "name": "clients_table_user_id_users_table_id_fk", + "tableFrom": "clients_table", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users_table", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "restrict" + } + }, "compositePrimaryKeys": {}, "uniqueConstraints": {}, "checkConstraints": {} @@ -154,8 +175,8 @@ "autoincrement": false, "default": 1 }, - "setupStep": { - "name": "setupStep", + "setup_step": { + "name": "setup_step", "type": "integer", "primaryKey": false, "notNull": true, @@ -175,22 +196,22 @@ "notNull": true, "autoincrement": false }, - "metricsPrometheus": { - "name": "metricsPrometheus", + "metrics_prometheus": { + "name": "metrics_prometheus", "type": "integer", "primaryKey": false, "notNull": true, "autoincrement": false }, - "metricsJson": { - "name": "metricsJson", + "metrics_json": { + "name": "metrics_json", "type": "integer", "primaryKey": false, "notNull": true, "autoincrement": false }, - "metricsPassword": { - "name": "metricsPassword", + "metrics_password": { + "name": "metrics_password", "type": "text", "primaryKey": false, "notNull": false, @@ -415,8 +436,8 @@ "notNull": true, "autoincrement": false }, - "clientId": { - "name": "clientId", + "client_id": { + "name": "client_id", "type": "integer", "primaryKey": false, "notNull": true, @@ -449,11 +470,11 @@ } }, "foreignKeys": { - "one_time_links_table_clientId_clients_table_id_fk": { - "name": "one_time_links_table_clientId_clients_table_id_fk", + "one_time_links_table_client_id_clients_table_id_fk": { + "name": "one_time_links_table_client_id_clients_table_id_fk", "tableFrom": "one_time_links_table", "columnsFrom": [ - "clientId" + "client_id" ], "tableTo": "clients_table", "columnsTo": [ diff --git a/src/server/database/migrations/meta/_journal.json b/src/server/database/migrations/meta/_journal.json index 4029303b..3ce83e4a 100644 --- a/src/server/database/migrations/meta/_journal.json +++ b/src/server/database/migrations/meta/_journal.json @@ -5,14 +5,14 @@ { "idx": 0, "version": "6", - "when": 1739191645161, + "when": 1739266828300, "tag": "0000_short_skin", "breakpoints": true }, { "idx": 1, "version": "6", - "when": 1739191678456, + "when": 1739266837347, "tag": "0001_classy_the_stranger", "breakpoints": true } diff --git a/src/server/database/repositories/client/schema.ts b/src/server/database/repositories/client/schema.ts index 6a38667e..882f49e6 100644 --- a/src/server/database/repositories/client/schema.ts +++ b/src/server/database/repositories/client/schema.ts @@ -1,10 +1,16 @@ import { sql, relations } from 'drizzle-orm'; import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; -import { oneTimeLink } from '../../schema'; +import { oneTimeLink, user } from '../../schema'; export const client = sqliteTable('clients_table', { id: int().primaryKey({ autoIncrement: true }), + userId: int('user_id') + .notNull() + .references(() => user.id, { + onDelete: 'restrict', + onUpdate: 'cascade', + }), name: text().notNull(), ipv4Address: text('ipv4_address').notNull().unique(), ipv6Address: text('ipv6_address').notNull().unique(), @@ -34,4 +40,8 @@ export const clientsRelations = relations(client, ({ one }) => ({ fields: [client.id], references: [oneTimeLink.clientId], }), + user: one(user, { + fields: [client.userId], + references: [user.id], + }), })); diff --git a/src/server/database/repositories/client/service.ts b/src/server/database/repositories/client/service.ts index ddd10729..fb9f8af6 100644 --- a/src/server/database/repositories/client/service.ts +++ b/src/server/database/repositories/client/service.ts @@ -18,6 +18,9 @@ function createPreparedStatement(db: DBType) { 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')) }) + .prepare(), toggle: db .update(client) .set({ enabled: sql.placeholder('enabled') as never as boolean }) @@ -39,6 +42,15 @@ 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), + })); + } + async getAll() { const result = await this.#statements.findAll.execute(); return result.map((row) => ({ @@ -97,6 +109,8 @@ export class ClientService { .insert(client) .values({ name, + // TODO: fix + userId: 1, expiresAt: parsedExpiresAt, privateKey, publicKey, diff --git a/src/server/database/repositories/client/types.ts b/src/server/database/repositories/client/types.ts index 68663209..51c759ec 100644 --- a/src/server/database/repositories/client/types.ts +++ b/src/server/database/repositories/client/types.ts @@ -14,7 +14,7 @@ export type CreateClientType = Omit< export type UpdateClientType = Omit< CreateClientType, - 'privateKey' | 'publicKey' | 'preSharedKey' + 'privateKey' | 'publicKey' | 'preSharedKey' | 'userId' >; const name = z diff --git a/src/server/database/repositories/general/schema.ts b/src/server/database/repositories/general/schema.ts index 3945573d..e1f4932e 100644 --- a/src/server/database/repositories/general/schema.ts +++ b/src/server/database/repositories/general/schema.ts @@ -4,14 +4,14 @@ import { sqliteTable, text, int } from 'drizzle-orm/sqlite-core'; export const general = sqliteTable('general_table', { id: int().primaryKey({ autoIncrement: false }).default(1), - setupStep: int().notNull(), + setupStep: int('setup_step').notNull(), sessionPassword: text('session_password').notNull(), sessionTimeout: int('session_timeout').notNull(), - metricsPrometheus: int({ mode: 'boolean' }).notNull(), - metricsJson: int({ mode: 'boolean' }).notNull(), - metricsPassword: text(), + metricsPrometheus: int('metrics_prometheus', { mode: 'boolean' }).notNull(), + metricsJson: int('metrics_json', { mode: 'boolean' }).notNull(), + metricsPassword: text('metrics_password'), createdAt: text('created_at') .notNull() diff --git a/src/server/database/repositories/oneTimeLink/schema.ts b/src/server/database/repositories/oneTimeLink/schema.ts index dd69c2fd..9eab8bce 100644 --- a/src/server/database/repositories/oneTimeLink/schema.ts +++ b/src/server/database/repositories/oneTimeLink/schema.ts @@ -7,7 +7,7 @@ export const oneTimeLink = sqliteTable('one_time_links_table', { id: int().primaryKey({ autoIncrement: true }), oneTimeLink: text('one_time_link').notNull().unique(), expiresAt: text('expires_at').notNull(), - clientId: int() + clientId: int('client_id') .notNull() .references(() => client.id, { onDelete: 'cascade', onUpdate: 'cascade' }), createdAt: text('created_at') diff --git a/src/server/database/repositories/user/schema.ts b/src/server/database/repositories/user/schema.ts index 71da8582..73cac6ef 100644 --- a/src/server/database/repositories/user/schema.ts +++ b/src/server/database/repositories/user/schema.ts @@ -1,6 +1,8 @@ -import { sql } from 'drizzle-orm'; +import { sql, relations } from 'drizzle-orm'; import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; +import { client } from '../../schema'; + export const user = sqliteTable('users_table', { id: int().primaryKey({ autoIncrement: true }), username: text().notNull().unique(), @@ -17,3 +19,7 @@ export const user = sqliteTable('users_table', { .default(sql`(CURRENT_TIMESTAMP)`) .$onUpdate(() => sql`(CURRENT_TIMESTAMP)`), }); + +export const usersRelations = relations(user, ({ many }) => ({ + clients: many(client), +})); diff --git a/src/server/utils/WireGuard.ts b/src/server/utils/WireGuard.ts index 2b37b2d1..8616ba2e 100644 --- a/src/server/utils/WireGuard.ts +++ b/src/server/utils/WireGuard.ts @@ -52,7 +52,39 @@ class WireGuard { WG_DEBUG('Config synced successfully.'); } - async getClients() { + async getClientsForUser(userId: ID) { + const wgInterface = await Database.interfaces.get(); + + const dbClients = await Database.clients.getForUser(userId); + + const clients = dbClients.map((client) => ({ + ...client, + latestHandshakeAt: null as Date | null, + endpoint: null as string | null, + transferRx: null as number | null, + transferTx: null as number | null, + })); + + // Loop WireGuard status + const dump = await wg.dump(wgInterface.name); + dump.forEach( + ({ publicKey, latestHandshakeAt, endpoint, transferRx, transferTx }) => { + const client = clients.find((client) => client.publicKey === publicKey); + if (!client) { + return; + } + + client.latestHandshakeAt = latestHandshakeAt; + client.endpoint = endpoint; + client.transferRx = transferRx; + client.transferTx = transferTx; + } + ); + + return clients; + } + + async getAllClients() { const wgInterface = await Database.interfaces.get(); const dbClients = await Database.clients.getAll(); const clients = dbClients.map((client) => ({ diff --git a/src/server/utils/handler.ts b/src/server/utils/handler.ts index bddf2a45..60167956 100644 --- a/src/server/utils/handler.ts +++ b/src/server/utils/handler.ts @@ -1,32 +1,65 @@ import type { EventHandlerRequest, EventHandlerResponse, H3Event } from 'h3'; import type { UserType } from '#db/repositories/user/types'; -import type { SetupStepType } from '../database/repositories/general/types'; +import type { SetupStepType } from '#db/repositories/general/types'; +import { + type Permissions, + hasPermissionsWithData, +} from '#shared/utils/permissions'; type PermissionHandler< TReq extends EventHandlerRequest, TRes extends EventHandlerResponse, -> = { (params: { event: H3Event; user: UserType }): TRes }; + Resource extends keyof Permissions, +> = { + (params: { + event: H3Event; + user: UserType; + /** + * check if user has permissions to access the resource + * + * see: {@link hasPermissionsWithData} + */ + checkPermissions: (data?: Permissions[Resource]['dataType']) => true; + }): TRes; +}; /** - * check if the user has the permission to perform the action + * get current user */ export const definePermissionEventHandler = < TReq extends EventHandlerRequest, TRes extends EventHandlerResponse, + Resource extends keyof Permissions, >( - action: Action, - handler: PermissionHandler + resource: Resource, + action: Permissions[Resource]['action'], + handler: PermissionHandler ) => { return defineEventHandler(async (event) => { const user = await getCurrentUser(event); - if (!checkPermissions(action, user)) { + + const permissions = hasPermissionsWithData(user, resource, action); + + // if no data is required, check permissions + if (permissions.isBoolean()) { + permissions.check(); + } + + const response = await handler({ + event, + user, + checkPermissions: permissions.check, + }); + + // if data is required, make sure permissions were checked + if (!permissions.checked) { throw createError({ - statusCode: 403, - statusMessage: 'Forbidden', + statusCode: 500, + statusMessage: 'Permission was not checked', }); } - return await handler({ event, user }); + return response; }); }; diff --git a/src/shared/utils/permissions.ts b/src/shared/utils/permissions.ts index 3b7c5762..4c5adac0 100644 --- a/src/shared/utils/permissions.ts +++ b/src/shared/utils/permissions.ts @@ -1,9 +1,5 @@ -// TODO: implement ABAC - -export const actions = { - ADMIN: 'ADMIN', - CLIENT: 'CLIENT', -} as const; +import type { ClientType } from '#db/repositories/client/types'; +import type { UserType } from '#db/repositories/user/types'; export type Role = number & { readonly __role: unique symbol }; @@ -12,20 +8,105 @@ export const roles = { CLIENT: 2 as Role, } as const; -export type Action = keyof typeof actions; +type Roles = keyof typeof roles; + +/** + * convert role to key + * @example roleToKey(roles.ADMIN) => 'ADMIN' + */ +function roleToKey(role: Role) { + const roleKey = Object.keys(roles).find( + (key) => roles[key as Roles] === role + ); + + if (roleKey === undefined) { + throw new Error('Invalid role'); + } -type MATRIX = { - readonly [key in keyof typeof actions]: readonly (typeof roles)[keyof typeof roles][]; + return roleKey as Roles; +} + +type PermissionCheck = + | boolean + | ((user: UserType, data: Permissions[Key]['dataType']) => boolean); + +type RolesWithPermissions = { + [R in Roles]: { + [Key in keyof Permissions]: { + [Action in Permissions[Key]['action']]: PermissionCheck; + }; + }; }; -export const MATRIX: MATRIX = { - [actions.ADMIN]: [roles.ADMIN] as const, - [actions.CLIENT]: [roles.CLIENT, roles.ADMIN] as const, -} as const; +export type Permissions = { + clients: { + dataType: ClientType; + action: 'view' | 'create' | 'update' | 'delete' | 'custom'; + }; +}; -export const checkPermissions = (action: Action, user: { role: Role }) => { - if (!MATRIX[action].includes(user.role)) { +export const ROLES = { + ADMIN: { + clients: { + view: true, + create: true, + update: true, + delete: true, + custom: true, + }, + }, + CLIENT: { + clients: { + view: (user, client) => user.id === client.userId, + create: false, + update: (user, client) => user.id === client.userId, + delete: (user, client) => user.id === client.userId, + custom: true, + }, + }, +} as const satisfies RolesWithPermissions; + +export function hasPermissions( + user: UserType, + resource: Resource, + action: Permissions[Resource]['action'], + data?: Permissions[Resource]['dataType'] +) { + const permission = ROLES[roleToKey(user.role)][resource][action]; + + if (typeof permission === 'boolean') { + return permission; + } + + if (data === undefined) { return false; } - return true; -}; + + return permission(user, data); +} + +export function hasPermissionsWithData( + user: UserType, + resource: Resource, + action: Permissions[Resource]['action'] +) { + let checked = false; + return { + check(data?: Permissions[Resource]['dataType']) { + checked = true; + const isAllowed = hasPermissions(user, resource, action, data); + + if (!isAllowed) { + throw new Error('Permission denied'); + } + + return isAllowed; + }, + isBoolean() { + return typeof ROLES[roleToKey(user.role)][resource][action] === 'boolean'; + }, + get checked() { + return checked; + }, + }; +}