Browse Source

Chore: TODOs (#1695)

* verify setup step

* improve readme

* format todos

* move id

* remove objectMessage

* style array field
pull/1696/head
Bernd Storath 5 months ago
committed by GitHub
parent
commit
3102fa1be9
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 71
      README.md
  2. 31
      src/app/components/Form/ArrayField.vue
  3. 2
      src/app/components/Ui/Footer.vue
  4. 2
      src/app/pages/index.vue
  5. 4
      src/i18n/locales/en.json
  6. 2
      src/server/api/session.post.ts
  7. 5
      src/server/api/setup/2.post.ts
  8. 5
      src/server/api/setup/4.post.ts
  9. 2
      src/server/api/setup/migrate.post.ts
  10. 3
      src/server/database/repositories/client/service.ts
  11. 2
      src/server/database/repositories/general/types.ts
  12. 1
      src/server/database/repositories/oneTimeLink/service.ts
  13. 9
      src/server/database/repositories/oneTimeLink/types.ts
  14. 1
      src/server/database/repositories/user/service.ts
  15. 48
      src/server/database/repositories/user/types.ts
  16. 3
      src/server/database/schema.ts
  17. 2
      src/server/middleware/setup.ts
  18. 1
      src/server/utils/WireGuard.ts
  19. 29
      src/server/utils/handler.ts
  20. 1
      src/server/utils/session.ts
  21. 14
      src/server/utils/types.ts

71
README.md

@ -1,13 +1,16 @@
# WireGuard Easy # WireGuard Easy
[![Build & Publish Docker Image to Docker Hub](https://github.com/wg-easy/wg-easy/actions/workflows/deploy.yml/badge.svg?branch=production)](https://github.com/wg-easy/wg-easy/actions/workflows/deploy.yml) [![Build & Publish latest Image](https://github.com/wg-easy/wg-easy/actions/workflows/deploy.yml/badge.svg?branch=production)](https://github.com/wg-easy/wg-easy/actions/workflows/deploy.yml)
[![Lint](https://github.com/wg-easy/wg-easy/actions/workflows/lint.yml/badge.svg?branch=master)](https://github.com/wg-easy/wg-easy/actions/workflows/lint.yml) [![Lint](https://github.com/wg-easy/wg-easy/actions/workflows/lint.yml/badge.svg?branch=master)](https://github.com/wg-easy/wg-easy/actions/workflows/lint.yml)
![Docker](https://img.shields.io/docker/pulls/weejewel/wg-easy.svg) [![GitHub Stars](https://img.shields.io/github/stars/wg-easy/wg-easy)](https://github.com/wg-easy/wg-easy/stargazers)
[![Sponsor](https://img.shields.io/github/sponsors/weejewel)](https://github.com/sponsors/WeeJeWel) [![License](https://img.shields.io/github/license/wg-easy/wg-easy)](LICENSE)
![GitHub Stars](https://img.shields.io/github/stars/wg-easy/wg-easy) [![GitHub Release](https://img.shields.io/github/v/release/wg-easy/wg-easy)](https://github.com/wg-easy/wg-easy/releases/latest)
[![Image Pulls](https://img.shields.io/badge/image_pulls-11M-blue)](https://github.com/wg-easy/wg-easy/pkgs/container/wg-easy)
You have found the easiest way to install & manage WireGuard on any Linux host! You have found the easiest way to install & manage WireGuard on any Linux host!
<!-- TOOD: update screenshot -->
<p align="center"> <p align="center">
<img src="./assets/screenshot.png" width="802" /> <img src="./assets/screenshot.png" width="802" />
</p> </p>
@ -24,10 +27,12 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
- Gravatar support. - Gravatar support.
- Automatic Light / Dark Mode - Automatic Light / Dark Mode
- Multilanguage Support - Multilanguage Support
- Traffic Stats (default off) - Traffic Stats
- One Time Links (default off) - One Time Links
- Client Expiration (default off) - Client Expiration
- Prometheus metrics support (default off) - Prometheus metrics support
- IPv6 support
- CIDR support
## Requirements ## Requirements
@ -40,14 +45,14 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
We offer multiple Docker image tags to suit your needs. The table below is in a particular order, with the first tag being the most recommended: We offer multiple Docker image tags to suit your needs. The table below is in a particular order, with the first tag being the most recommended:
| tag | Branch | Example | Description | | tag | Branch | Example | Description |
| ------------- | ------------------------------------------------------------------ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | ------------- | ---------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `15` | latest minor for that major tag | `ghcr.io/wg-easy/wg-easy:15` | latest features for specific major versions, no breaking changes | | `15` | latest minor for that major tag | `ghcr.io/wg-easy/wg-easy:15` | latest features for specific major versions, no breaking changes |
| `latest` | latest tag | `ghcr.io/wg-easy/wg-easy:latest` or `ghcr.io/wg-easy/wg-easy` | stable as possible get bug fixes quickly when needed, see Releases for more information. | | `latest` | latest tag | `ghcr.io/wg-easy/wg-easy:latest` or `ghcr.io/wg-easy/wg-easy` | stable as possible get bug fixes quickly when needed, see Releases for more information. |
| `15.0` | latest patch for that minor tag | `ghcr.io/wg-easy/wg-easy:15.0` | latest patches for specific minor version | | `15.0` | latest patch for that minor tag | `ghcr.io/wg-easy/wg-easy:15.0` | latest patches for specific minor version |
| `15.0.0` | specific tag | `ghcr.io/wg-easy/wg-easy:15.0.0` | specific release, don't use this as this will not get updated | | `15.0.0` | specific tag | `ghcr.io/wg-easy/wg-easy:15.0.0` | specific release, don't use this as this will not get updated |
| `nightly` | [`master`](https://github.com/wg-easy/wg-easy/tree/master) | `ghcr.io/wg-easy/wg-easy:nightly` | mostly unstable gets frequent package and code updates, deployed against [`master`](https://github.com/wg-easy/wg-easy/tree/master). | | `nightly` | [`master`](https://github.com/wg-easy/wg-easy/tree/master) | `ghcr.io/wg-easy/wg-easy:nightly` | mostly unstable gets frequent package and code updates, deployed against [`master`](https://github.com/wg-easy/wg-easy/tree/master). |
| `development` | pull requests | `ghcr.io/wg-easy/wg-easy:development` | used for development, testing code from PRs before landing into [`master`](https://github.com/wg-easy/wg-easy/tree/master). | | `development` | pull requests | `ghcr.io/wg-easy/wg-easy:development` | used for development, testing code from PRs before landing into [`master`](https://github.com/wg-easy/wg-easy/tree/master). |
## Installation ## Installation
@ -65,8 +70,16 @@ And log in again.
### 2. Run WireGuard Easy ### 2. Run WireGuard Easy
<!-- TODO: prioritize docker compose over docker run --> The easiest way to run WireGuard Easy is with Docker Compose.
Just download [`docker-compose.yml`](docker-compose.yml), make necessary adjustments and
execute `docker compose up -d`.
The Web UI will now be available on `http://0.0.0.0:51821`.
<!-- TOOD: add to docs: Grafana dashboard [21733](https://grafana.com/grafana/dashboards/21733-wireguard/) -->
<!-- TOOD: add to docs
To setup the IPv6 Network, simply run once: To setup the IPv6 Network, simply run once:
```bash ```bash
@ -107,22 +120,26 @@ The Prometheus metrics will now be available on `http://0.0.0.0:51821/api/metric
> 💡 Your configuration files will be saved in `~/.wg-easy` > 💡 Your configuration files will be saved in `~/.wg-easy`
WireGuard Easy can be launched with Docker Compose as well - just download -->
[`docker-compose.yml`](docker-compose.yml), make necessary adjustments and
execute `docker compose up -d`.
### 3. Sponsor ### 3. Sponsor
Are you enjoying this project? [Buy Emile a beer!](https://github.com/sponsors/WeeJeWel) 🍻 Are you enjoying this project? Consider donating.
Founder: [Buy Emile a beer!](https://github.com/sponsors/WeeJeWel) 🍻
Maintainer: [Buy kaaax0815 a coffee!](https://github.com/sponsors/kaaax0815) ☕
<!-- TOOD: add to docs
## Options ## Options
These options can be configured by setting environment variables using `-e KEY="VALUE"` in the `docker run` command. These options can be configured by setting environment variables using `-e KEY="VALUE"` in the `docker run` command.
| Env | Default | Example | Description | | Env | Default | Example | Description |
| --------- | ----------------- | ------------- | -------------------------------------------- | | ------ | --------- | ----------- | --------------------------- |
| `PORT` | `51821` | `6789` | TCP port for Web UI. | | `PORT` | `51821` | `6789` | TCP port for Web UI. |
| `HOST` | `0.0.0.0` | `localhost` | IP address web UI binds to. | | `HOST` | `0.0.0.0` | `localhost` | IP address web UI binds to. |
## Updating ## Updating
@ -151,9 +168,11 @@ was pulled.
For less common or specific edge-case scenarios, please refer to the detailed information provided in the [Wiki](https://github.com/wg-easy/wg-easy/wiki). For less common or specific edge-case scenarios, please refer to the detailed information provided in the [Wiki](https://github.com/wg-easy/wg-easy/wiki).
-->
## License ## License
This project is licensed under the AGPL-3.0-only License - see the LICENSE file for details This project is licensed under the AGPL-3.0-only License - see the [LICENSE](LICENSE) file for details
This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with Jason A. Donenfeld, ZX2C4 or Edge Security This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with Jason A. Donenfeld, ZX2C4 or Edge Security

31
src/app/components/Form/ArrayField.vue

@ -1,25 +1,32 @@
<template> <template>
<div v-if="data?.length === 0"> <div v-if="data?.length === 0">
{{ emptyText || 'No items' }} {{ emptyText || $t('form.noItems') }}
</div> </div>
<div v-else class="flex flex-col"> <div v-else class="flex flex-col gap-2">
<div v-for="(item, i) in data" :key="i"> <div v-for="(item, i) in data" :key="i">
<input <div class="flex flex-row gap-1">
:value="item" <input
:name="name" :value="item"
type="text" :name="name"
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400" type="text"
@input="update($event, i)" class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400"
@input="update($event, i)"
/>
<BaseButton as="input" type="button" value="-" @click="del(i)" />
</div>
</div>
<div class="mt-2">
<BaseButton
as="input"
type="button"
:value="$t('form.add')"
@click="add"
/> />
<input type="button" value="-" @click="del(i)" />
</div> </div>
</div> </div>
<input type="button" value="Add" @click="add" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
// TODO: style
const data = defineModel<string[]>(); const data = defineModel<string[]>();
defineProps<{ emptyText?: string[]; name: string }>(); defineProps<{ emptyText?: string[]; name: string }>();

2
src/app/components/Ui/Footer.vue

@ -18,7 +18,7 @@
<a <a
class="hover:underline" class="hover:underline"
target="_blank" target="_blank"
href="https://spdx.org/licenses/AGPL-3.0-only.html" href="https://opensource.org/license/agpl-v3"
>AGPL-3.0-only</a >AGPL-3.0-only</a
> >
· ·

2
src/app/pages/index.vue

@ -42,8 +42,6 @@ const intervalId = ref<NodeJS.Timeout | null>(null);
clientsStore.refresh(); clientsStore.refresh();
onMounted(() => { onMounted(() => {
// TODO: remove (to avoid console spam)
return;
// TODO?: replace with websocket or similar // TODO?: replace with websocket or similar
intervalId.value = setInterval(() => { intervalId.value = setInterval(() => {
clientsStore clientsStore

4
src/i18n/locales/en.json

@ -116,7 +116,9 @@
"save": "Save", "save": "Save",
"revert": "Revert", "revert": "Revert",
"sectionGeneral": "General", "sectionGeneral": "General",
"sectionAdvanced": "Advanced" "sectionAdvanced": "Advanced",
"noItems": "No items",
"add": "Add"
}, },
"admin": { "admin": {
"general": { "general": {

2
src/server/api/session.post.ts

@ -28,7 +28,7 @@ export default defineEventHandler(async (event) => {
userId: user.id, userId: user.id,
}); });
// TODO: create audit log? // TODO?: create audit log
SERVER_DEBUG(`New Session: ${data.id} for ${user.id} (${user.username})`); SERVER_DEBUG(`New Session: ${data.id} for ${user.id} (${user.username})`);

5
src/server/api/setup/2.post.ts

@ -1,14 +1,13 @@
import { UserSetupSchema } from '#db/repositories/user/types'; import { UserSetupSchema } from '#db/repositories/user/types';
export default defineSetupEventHandler(async ({ event }) => { export default defineSetupEventHandler(2, async ({ event }) => {
const { username, password } = await readValidatedBody( const { username, password } = await readValidatedBody(
event, event,
validateZod(UserSetupSchema, event) validateZod(UserSetupSchema, event)
); );
// TODO: validate setup step
await Database.users.create(username, password); await Database.users.create(username, password);
await Database.general.setSetupStep(3); await Database.general.setSetupStep(3);
return { success: true }; return { success: true };
}); });

5
src/server/api/setup/4.post.ts

@ -1,12 +1,13 @@
import { UserConfigSetupSchema } from '#db/repositories/userConfig/types'; import { UserConfigSetupSchema } from '#db/repositories/userConfig/types';
export default defineSetupEventHandler(async ({ event }) => { export default defineSetupEventHandler(4, async ({ event }) => {
const { host, port } = await readValidatedBody( const { host, port } = await readValidatedBody(
event, event,
validateZod(UserConfigSetupSchema, event) validateZod(UserConfigSetupSchema, event)
); );
// TODO: validate setup step
await Database.userConfigs.updateHostPort(host, port); await Database.userConfigs.updateHostPort(host, port);
await Database.general.setSetupStep(0); await Database.general.setSetupStep(0);
return { success: true }; return { success: true };
}); });

2
src/server/api/setup/migrate.post.ts

@ -2,7 +2,7 @@ import { parseCidr } from 'cidr-tools';
import { stringifyIp } from 'ip-bigint'; import { stringifyIp } from 'ip-bigint';
import { z } from 'zod'; import { z } from 'zod';
export default defineSetupEventHandler(async ({ event }) => { export default defineSetupEventHandler('migrate', async ({ event }) => {
const { file } = await readValidatedBody( const { file } = await readValidatedBody(
event, event,
validateZod(FileSchema, event) validateZod(FileSchema, event)

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

@ -6,7 +6,6 @@ import type {
ClientCreateType, ClientCreateType,
UpdateClientType, UpdateClientType,
} from './types'; } from './types';
import type { ID } from '#db/schema';
import { wgInterface, userConfig } from '#db/schema'; import { wgInterface, userConfig } from '#db/schema';
import { parseCidr } from 'cidr-tools'; import { parseCidr } from 'cidr-tools';
@ -116,7 +115,7 @@ export class ClientService {
.insert(client) .insert(client)
.values({ .values({
name, name,
// TODO: fix // TODO: properly assign user id
userId: 1, userId: 1,
expiresAt: parsedExpiresAt, expiresAt: parsedExpiresAt,
privateKey, privateKey,

2
src/server/database/repositories/general/types.ts

@ -11,7 +11,7 @@ const metricsEnabled = z.boolean({ message: t('zod.general.metricsEnabled') });
const metricsPassword = z const metricsPassword = z
.string({ message: t('zod.general.metricsPassword') }) .string({ message: t('zod.general.metricsPassword') })
.min(1, { message: t('zod.general.metricsPassword') }) .min(1, { message: t('zod.general.metricsPassword') })
// TODO: validate argon2 regex? // TODO?: validate argon2 regex
.nullable(); .nullable();
export const GeneralUpdateSchema = z.object({ export const GeneralUpdateSchema = z.object({

1
src/server/database/repositories/oneTimeLink/service.ts

@ -1,7 +1,6 @@
import type { DBType } from '#db/sqlite'; import type { DBType } from '#db/sqlite';
import { eq, sql } from 'drizzle-orm'; import { eq, sql } from 'drizzle-orm';
import { oneTimeLink } from './schema'; import { oneTimeLink } from './schema';
import type { ID } from '../../schema';
import CRC32 from 'crc-32'; import CRC32 from 'crc-32';
function createPreparedStatement(db: DBType) { function createPreparedStatement(db: DBType) {

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

@ -9,9 +9,6 @@ const oneTimeLinkType = z
.min(1, t('zod.otl')) .min(1, t('zod.otl'))
.pipe(safeStringRefine); .pipe(safeStringRefine);
export const OneTimeLinkGetSchema = z.object( export const OneTimeLinkGetSchema = z.object({
{ oneTimeLink: oneTimeLinkType,
oneTimeLink: oneTimeLinkType, });
},
{ message: objectMessage }
);

1
src/server/database/repositories/user/service.ts

@ -1,7 +1,6 @@
import type { DBType } from '#db/sqlite'; import type { DBType } from '#db/sqlite';
import { eq, sql } from 'drizzle-orm'; import { eq, sql } from 'drizzle-orm';
import { user } from './schema'; import { user } from './schema';
import type { ID } from '../../schema';
function createPreparedStatement(db: DBType) { function createPreparedStatement(db: DBType) {
return { return {

48
src/server/database/repositories/user/types.ts

@ -20,22 +20,16 @@ const password = z
const remember = z.boolean({ message: t('zod.user.remember') }); const remember = z.boolean({ message: t('zod.user.remember') });
export const UserLoginSchema = z.object( export const UserLoginSchema = z.object({
{ username: username,
username: username, password: password,
password: password, remember: remember,
remember: remember, });
},
{ message: objectMessage }
);
export const UserSetupSchema = z.object( export const UserSetupSchema = z.object({
{ username: username,
username: username, password: password,
password: password, });
},
{ message: objectMessage }
);
const name = z const name = z
.string({ message: t('zod.user.name') }) .string({ message: t('zod.user.name') })
@ -49,23 +43,17 @@ const email = z
.pipe(safeStringRefine) .pipe(safeStringRefine)
.nullable(); .nullable();
export const UserUpdateSchema = z.object( export const UserUpdateSchema = z.object({
{ name: name,
name: name, email: email,
email: email, });
},
{ message: objectMessage }
);
export const UserUpdatePasswordSchema = z export const UserUpdatePasswordSchema = z
.object( .object({
{ currentPassword: password,
currentPassword: password, newPassword: password,
newPassword: password, confirmPassword: password,
confirmPassword: password, })
},
{ message: objectMessage }
)
.refine((val) => val.newPassword === val.confirmPassword, { .refine((val) => val.newPassword === val.confirmPassword, {
message: t('zod.user.passwordMatch'), message: t('zod.user.passwordMatch'),
}); });

3
src/server/database/schema.ts

@ -6,6 +6,3 @@ export * from './repositories/interface/schema';
export * from './repositories/oneTimeLink/schema'; export * from './repositories/oneTimeLink/schema';
export * from './repositories/user/schema'; export * from './repositories/user/schema';
export * from './repositories/userConfig/schema'; export * from './repositories/userConfig/schema';
// TODO: move to types
export type ID = number;

2
src/server/middleware/setup.ts

@ -1,4 +1,4 @@
/* First setup of wg-easy app */ /* First setup of wg-easy */
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const url = getRequestURL(event); const url = getRequestURL(event);

1
src/server/utils/WireGuard.ts

@ -1,7 +1,6 @@
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import debug from 'debug'; import debug from 'debug';
import QRCode from 'qrcode'; import QRCode from 'qrcode';
import type { ID } from '#db/schema';
import type { InterfaceType } from '#db/repositories/interface/types'; import type { InterfaceType } from '#db/repositories/interface/types';
const WG_DEBUG = debug('WireGuard'); const WG_DEBUG = debug('WireGuard');

29
src/server/utils/handler.ts

@ -63,6 +63,17 @@ export const definePermissionEventHandler = <
}); });
}; };
// which api route is allowed for each setup step
// 0 is done, 1 is start
// 3 means step 2 is done
const ValidSetupSteps = {
1: [2] as const,
3: [4, 'migrate'] as const,
} as const;
type ValidSteps =
(typeof ValidSetupSteps)[keyof typeof ValidSetupSteps][number];
type SetupHandler< type SetupHandler<
TReq extends EventHandlerRequest, TReq extends EventHandlerRequest,
TRes extends EventHandlerResponse, TRes extends EventHandlerResponse,
@ -75,6 +86,7 @@ export const defineSetupEventHandler = <
TReq extends EventHandlerRequest, TReq extends EventHandlerRequest,
TRes extends EventHandlerResponse, TRes extends EventHandlerResponse,
>( >(
step: ValidSteps,
handler: SetupHandler<TReq, TRes> handler: SetupHandler<TReq, TRes>
) => { ) => {
return defineEventHandler(async (event) => { return defineEventHandler(async (event) => {
@ -87,6 +99,23 @@ export const defineSetupEventHandler = <
}); });
} }
const validSetupSteps =
ValidSetupSteps[setup.step as keyof typeof ValidSetupSteps];
if (!validSetupSteps) {
throw createError({
statusCode: 500,
statusMessage: 'Invalid setup step',
});
}
if (!validSetupSteps.includes(step as never)) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid step',
});
}
return await handler({ event, setup }); return await handler({ event, setup });
}); });
}; };

1
src/server/utils/session.ts

@ -1,5 +1,4 @@
import type { H3Event } from 'h3'; import type { H3Event } from 'h3';
import type { ID } from '#db/schema';
import type { UserType } from '#db/repositories/user/types'; import type { UserType } from '#db/repositories/user/types';
export type WGSession = Partial<{ export type WGSession = Partial<{

14
src/server/utils/types.ts

@ -2,6 +2,8 @@ import type { ZodSchema } from 'zod';
import z from 'zod'; import z from 'zod';
import type { H3Event, EventHandlerRequest } from 'h3'; import type { H3Event, EventHandlerRequest } from 'h3';
export type ID = number;
/** /**
* return the string as is * return the string as is
* *
@ -9,9 +11,6 @@ import type { H3Event, EventHandlerRequest } from 'h3';
*/ */
export const t = (v: string) => v; export const t = (v: string) => v;
// TODO: use everywhere or remove
export const objectMessage = t('zod.body');
export const safeStringRefine = z export const safeStringRefine = z
.string() .string()
.refine( .refine(
@ -49,12 +48,9 @@ export const AllowedIpsSchema = z
.array(AddressSchema, { message: t('zod.allowedIps') }) .array(AddressSchema, { message: t('zod.allowedIps') })
.min(1, { message: t('zod.allowedIps') }); .min(1, { message: t('zod.allowedIps') });
export const FileSchema = z.object( export const FileSchema = z.object({
{ file: z.string({ message: t('zod.file') }),
file: z.string({ message: t('zod.file') }), });
},
{ message: objectMessage }
);
export const schemaForType = export const schemaForType =
<T>() => <T>() =>

Loading…
Cancel
Save