+
@@ -14,7 +14,7 @@
:checked="feature.enabled"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-800"
:class="feature.enabled ? 'bg-red-800' : 'bg-gray-200'"
- @update:checked="toggleFeature(feature)"
+ @update:checked="toggleFeature(key)"
>
-
Save
+
Save
+
diff --git a/src/app/utils/api.ts b/src/app/utils/api.ts
index feb21bc7..4994caa5 100644
--- a/src/app/utils/api.ts
+++ b/src/app/utils/api.ts
@@ -145,6 +145,13 @@ class API {
method: 'get',
});
}
+
+ async updateFeatures(features: Record
) {
+ return $fetch('/api/features', {
+ method: 'post',
+ body: { features },
+ });
+ }
}
type WGClientReturn = Awaited<
diff --git a/src/app/utils/math.ts b/src/app/utils/math.ts
index d43ac1b5..9592e333 100644
--- a/src/app/utils/math.ts
+++ b/src/app/utils/math.ts
@@ -21,6 +21,7 @@ export function bytes(
}
export function dateTime(value: Date) {
+ // TODO: results in mismatch because of different locales
return new Intl.DateTimeFormat(undefined, {
year: 'numeric',
month: 'short',
diff --git a/src/server/api/features.post.ts b/src/server/api/features.post.ts
new file mode 100644
index 00000000..f87119aa
--- /dev/null
+++ b/src/server/api/features.post.ts
@@ -0,0 +1,8 @@
+export default defineEventHandler(async (event) => {
+ const { features } = await readValidatedBody(
+ event,
+ validateZod(featuresType)
+ );
+ await Database.system.updateFeatures(features);
+ return { success: true };
+});
diff --git a/src/server/middleware/setup.ts b/src/server/middleware/setup.ts
index 6c28988d..cb115a37 100644
--- a/src/server/middleware/setup.ts
+++ b/src/server/middleware/setup.ts
@@ -2,6 +2,8 @@
export default defineEventHandler(async (event) => {
const url = getRequestURL(event);
+ // TODO: redirect to login page if already set up
+
if (
url.pathname === '/setup' ||
url.pathname === '/api/account/setup' ||
diff --git a/src/server/utils/types.ts b/src/server/utils/types.ts
index 8e701f92..2d3568e6 100644
--- a/src/server/utils/types.ts
+++ b/src/server/utils/types.ts
@@ -58,6 +58,19 @@ const oneTimeLink = z
.min(1, 'oneTimeLink must be at least 1 Character')
.pipe(safeStringRefine);
+const features = z.record(
+ z.string({ message: 'key must be a valid object' }),
+ z.object(
+ {
+ enabled: z.boolean({ message: 'enabled must be a valid boolean' }),
+ },
+ { message: 'value must be a valid object' }
+ ),
+ { message: 'features must be a valid record' }
+);
+
+const objectMessage = 'Body must be a valid object';
+
export const clientIdType = z.object(
{
clientId: id,
@@ -69,28 +82,28 @@ export const address4Type = z.object(
{
address4: address4,
},
- { message: 'Body must be a valid object' }
+ { message: objectMessage }
);
export const nameType = z.object(
{
name: name,
},
- { message: 'Body must be a valid object' }
+ { message: objectMessage }
);
export const expireDateType = z.object(
{
expireDate: expireDate,
},
- { message: 'Body must be a valid object' }
+ { message: objectMessage }
);
export const oneTimeLinkType = z.object(
{
oneTimeLink: oneTimeLink,
},
- { message: 'Body must be a valid object' }
+ { message: objectMessage }
);
export const createType = z.object(
@@ -98,14 +111,14 @@ export const createType = z.object(
name: name,
expireDate: expireDate,
},
- { message: 'Body must be a valid object' }
+ { message: objectMessage }
);
export const fileType = z.object(
{
file: file,
},
- { message: 'Body must be a valid object' }
+ { message: objectMessage }
);
export const credentialsType = z.object(
@@ -114,7 +127,7 @@ export const credentialsType = z.object(
password: password,
remember: remember,
},
- { message: 'Body must be a valid object' }
+ { message: objectMessage }
);
export const passwordType = z.object(
@@ -122,7 +135,14 @@ export const passwordType = z.object(
username: username,
password: password,
},
- { message: 'Body must be a valid object' }
+ { message: objectMessage }
+);
+
+export const featuresType = z.object(
+ {
+ features: features,
+ },
+ { message: objectMessage }
);
export function validateZod(schema: ZodSchema) {
diff --git a/src/services/database/lowdb.ts b/src/services/database/lowdb.ts
index 5645795f..76bc6ab9 100644
--- a/src/services/database/lowdb.ts
+++ b/src/services/database/lowdb.ts
@@ -18,7 +18,11 @@ import {
type NewClient,
type OneTimeLink,
} from './repositories/client';
-import { SystemRepository } from './repositories/system';
+import {
+ Features,
+ SystemRepository,
+ type Feature,
+} from './repositories/system';
const DEBUG = debug('LowDB');
@@ -37,6 +41,17 @@ export class LowDBSystem extends SystemRepository {
}
return system;
}
+
+ async updateFeatures(features: Record) {
+ DEBUG('Update Features');
+ this.#db.update((v) => {
+ for (const key in features) {
+ if (Features.includes(key as Features)) {
+ v.system[key as Features].enabled = features[key]!.enabled;
+ }
+ }
+ });
+ }
}
export class LowDBUser extends UserRepository {
diff --git a/src/services/database/repositories/system.ts b/src/services/database/repositories/system.ts
index cf9c8aab..45c74213 100644
--- a/src/services/database/repositories/system.ts
+++ b/src/services/database/repositories/system.ts
@@ -65,16 +65,25 @@ export type System = {
wgConfigPort: number;
iptables: IpTables;
+
trafficStats: TrafficStats;
+ prometheus: Prometheus;
clientExpiration: Feature;
oneTimeLinks: Feature;
sortClients: Feature;
- prometheus: Prometheus;
sessionConfig: SessionConfig;
};
+export const Features = [
+ 'clientExpiration',
+ 'oneTimeLinks',
+ 'sortClients',
+] as const;
+
+export type Features = (typeof Features)[number];
+
/**
* Interface for system-related database operations.
* This interface provides methods for retrieving system configuration data
@@ -85,4 +94,6 @@ export abstract class SystemRepository {
* Retrieves the system configuration data from the database.
*/
abstract get(): Promise;
+
+ abstract updateFeatures(features: Record): Promise;
}