diff --git a/src/app/app.vue b/src/app/app.vue index 636ca56d..ef2841fa 100644 --- a/src/app/app.vue +++ b/src/app/app.vue @@ -1,7 +1,12 @@ diff --git a/src/app/components/ui/Toast.vue b/src/app/components/ui/Toast.vue new file mode 100644 index 00000000..d5a3721f --- /dev/null +++ b/src/app/components/ui/Toast.vue @@ -0,0 +1,37 @@ + + + diff --git a/src/app/layouts/Header.vue b/src/app/layouts/Header.vue index 7c9b03a4..1cf31451 100644 --- a/src/app/layouts/Header.vue +++ b/src/app/layouts/Header.vue @@ -2,14 +2,14 @@

- +

@@ -84,7 +84,13 @@ const globalStore = useGlobalStore(); const route = useRoute(); -const isLoginPage = computed(() => route.path == '/login'); +const hasOwnLogo = computed( + () => route.path === '/login' || route.path === '/setup' +); + +const loggedIn = computed( + () => route.path !== '/login' && route.path !== '/setup' +); const theme = useTheme(); const uiShowCharts = ref(getItem('uiShowCharts') === '1'); diff --git a/src/app/pages/admin/features.vue b/src/app/pages/admin/features.vue index 69a85f9b..f84e5f2e 100644 --- a/src/app/pages/admin/features.vue +++ b/src/app/pages/admin/features.vue @@ -1,6 +1,6 @@ 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; }