feat: dark mode (#10)
5
.changeset/yellow-fireants-drop.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"wireadmin": patch
|
||||
---
|
||||
|
||||
feat: dark mode
|
59
README.md
@ -1,4 +1,7 @@
|
||||
# WireGuard GUI (Easy Admin UI)
|
||||
# WireGuard (Easy Admin UI)
|
||||
|
||||
[](https://github.com/shahradelahi/wireadmin/actions/workflows/ci.yml)
|
||||
[](https://opensource.org/licenses/GPL-3.0)
|
||||
|
||||

|
||||
|
||||
@ -8,11 +11,14 @@
|
||||
|
||||
## Features
|
||||
|
||||
- Easy-to-use web-based admin UI
|
||||
- Simple and friendly UI
|
||||
- Support for multiple users and servers
|
||||
- Support for **Tor for anonymized connections**
|
||||
- Server connection statistics
|
||||
- List, create, delete, or modify any server or user
|
||||
- Scan QR codes or easily download the client configurations.
|
||||
- Create QR codes
|
||||
- Easily download the client configurations.
|
||||
- Automatic Light/Dark Mode
|
||||
|
||||
## Installation
|
||||
|
||||
@ -22,20 +28,20 @@
|
||||
|
||||
### 2. Docker Image
|
||||
|
||||
#### Build from source
|
||||
#### Build from source (recommended)
|
||||
|
||||
```bash
|
||||
git clone https://github.com/shahradelahi/wireadmin
|
||||
docker buildx build --tag litehex/wireadmin ./wireadmin
|
||||
```
|
||||
|
||||
#### Pull from Docker Hub (recommended)
|
||||
#### Pull from Docker Hub
|
||||
|
||||
```bash
|
||||
docker pull litehex/wireadmin
|
||||
docker pull litehex/wireadmin # OR ghcr.io/shahradelahi/wireadmin
|
||||
```
|
||||
|
||||
### 4. Persistent Data
|
||||
### 3. Persistent Data
|
||||
|
||||
WireAdmin store configurations at `/data`. It's important to mount a volume at this location to ensure that
|
||||
your data is not lost during container restarts or updates.
|
||||
@ -46,18 +52,19 @@ your data is not lost during container restarts or updates.
|
||||
docker volume create wireadmin-data --driver local
|
||||
```
|
||||
|
||||
### 4. Run
|
||||
### 4. Run WireAdmin
|
||||
|
||||
When creating each server, ensure that you add the port exposure through Docker. In the below command, the port `51820`
|
||||
is added for the WireGuard server.
|
||||
|
||||
**NOTE:** The port `3000` is for the WebUI, and can be changed with `PORT` environment variable, but for security
|
||||
reasons, it's recommended to NOT expose **_any kind of WebUI_** to the public. It's up to you to remove it after
|
||||
configuring
|
||||
the Servers/Peers.
|
||||
> 💡 The port `3000` is for the WebUI, and can be changed with `PORT` environment variable, but for security
|
||||
> reasons, it's recommended to NOT expose **_any kind of WebUI_** to the public. It's up to you to remove it after
|
||||
> configuring
|
||||
> the Servers/Peers.
|
||||
|
||||
```bash
|
||||
docker run --rm \
|
||||
```shell
|
||||
docker run --detach \
|
||||
--name wireadmin \
|
||||
-e WG_HOST=<YOUR_SERVER_IP> \
|
||||
-e UI_PASSWORD=<ADMIN_PASSWORD> \
|
||||
-p "3000:3000/tcp" \
|
||||
@ -70,18 +77,24 @@ docker run --rm \
|
||||
litehex/wireadmin
|
||||
```
|
||||
|
||||
## Environment Options
|
||||
> 💡 Replace `<YOUR_SERVER_IP>` with the IP address of your server.
|
||||
|
||||
> 💡 Replace `<ADMIN_PASSWORD>` with the password for the admin UI.
|
||||
|
||||
The Web UI will now be available on `http://0.0.0.0:3000`.
|
||||
|
||||
## Options
|
||||
|
||||
These options can be configured by setting environment variables using `-e KEY="VALUE"` in the `docker run` command.
|
||||
|
||||
| Option | Description | Optional |
|
||||
| ----------------- | ------------------------------------------------------------------------------- | -------- |
|
||||
| `WG_HOST` | The public IP address of the WireGuard server. | |
|
||||
| `UI_PASSWORD` | The password for the admin UI. | |
|
||||
| `HOST` | The hostname for the WebUI. (default: `127.0.0.1`) | ✔️ |
|
||||
| `PORT` | The port for the WebUI. (default: `3000`) | ✔️ |
|
||||
| `TOR_USE_BRIDGES` | Set this to `1` and then mount the bridges file at `/etc/torrc.d/bridges.conf`. | ✔️ |
|
||||
| `TOR_*` | The `Torrc` proxy configuration. (e.g. `SocksPort` as `TOR_SOCKSPORT="9050"`) | ✔️ |
|
||||
| Option | Description | Default | Optional |
|
||||
| ----------------- | ------------------------------------------------------------------------------- | ------------------- | -------- |
|
||||
| `WG_HOST` | The public IP address of the WireGuard server. | - | |
|
||||
| `UI_PASSWORD` | The password for the admin UI. | `insecure-password` | |
|
||||
| `HOST` | The hostname for the WebUI. | `127.0.0.1` | ✔️ |
|
||||
| `PORT` | The port for the WebUI. | `3000` | ✔️ |
|
||||
| `TOR_USE_BRIDGES` | Set this to `1` and then mount the bridges file at `/etc/torrc.d/bridges.conf`. | - | ✔️ |
|
||||
| `TOR_*` | The `Torrc` proxy configuration. (e.g. `SocksPort` as `TOR_SOCKS_PORT="9050"`) | - | ✔️ |
|
||||
|
||||
## Reporting
|
||||
|
||||
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 215 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 57 KiB |
@ -55,6 +55,7 @@
|
||||
"formsnap": "^1.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-svelte": "^0.330.0",
|
||||
"mode-watcher": "^0.3.0",
|
||||
"node-netkit": "0.1.0-canary.2",
|
||||
"pino": "^8.18.0",
|
||||
"pino-pretty": "^10.3.1",
|
||||
|
@ -4,68 +4,67 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 210 20% 98%;
|
||||
--foreground: 224 71.4% 4.1%;
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
|
||||
--muted: 220 14.3% 95.9%;
|
||||
--muted-foreground: 220 8.9% 46.1%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 224 71.4% 4.1%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 224 71.4% 4.1%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
|
||||
--border: 220 13% 91%;
|
||||
--input: 220 13% 91%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
|
||||
--primary: 358 72% 31%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
--secondary: 220 14.3% 95.9%;
|
||||
--secondary-foreground: 220.9 39.3% 11%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
|
||||
--accent: 220 14.3% 95.9%;
|
||||
--accent-foreground: 220.9 39.3% 11%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 20% 98%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
/*--ring: 224 71.4% 4.1%;*/
|
||||
--ring: var(--primary);
|
||||
--ring: 240 5% 64.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 224 71.4% 4.1%;
|
||||
--foreground: 210 20% 98%;
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
|
||||
--muted: 215 27.9% 16.9%;
|
||||
--muted-foreground: 217.9 10.6% 64.9%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
|
||||
--popover: 224 71.4% 4.1%;
|
||||
--popover-foreground: 210 20% 98%;
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
--card: 224 71.4% 4.1%;
|
||||
--card-foreground: 210 20% 98%;
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
|
||||
--border: 215 27.9% 16.9%;
|
||||
--input: 215 27.9% 16.9%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
|
||||
--primary: 210 20% 98%;
|
||||
--primary-foreground: 220.9 39.3% 11%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
|
||||
--secondary: 215 27.9% 16.9%;
|
||||
--secondary-foreground: 210 20% 98%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
|
||||
--accent: 215 27.9% 16.9%;
|
||||
--accent-foreground: 210 20% 98%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 20% 98%;
|
||||
--destructive-foreground: 0 85.7% 97.3%;
|
||||
|
||||
--ring: 216 12.2% 83.9%;
|
||||
--ring: 240 3.7% 15.9%;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,6 @@
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class={cn('text-black/25', className)} {...$$restProps}>
|
||||
<div class={cn('text-black/50 dark:text-white/50', className)} {...$$restProps}>
|
||||
<slot />
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { mode } from 'mode-watcher';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
type $$Props = HTMLAttributes<SVGImageElement> & {
|
||||
@ -11,7 +12,12 @@
|
||||
let className: $$Props['class'] = undefined;
|
||||
export let borderColor: $$Props['borderColor'] = '#d9d9d9';
|
||||
export let shadowColor: $$Props['shadowColor'] = '#f5f5f5';
|
||||
export let contentColor: $$Props['contentColor'] = '#fafafa';
|
||||
export let contentColor: $$Props['contentColor'] = 'transparent';
|
||||
|
||||
$: $mode, (contentColor = $mode === 'dark' ? 'transparent' : 'transparent');
|
||||
$: $mode, (shadowColor = $mode === 'dark' ? '#1e2021' : '#f5f5f5');
|
||||
$: $mode, (borderColor = $mode === 'dark' ? '#d0ccc6' : '#d9d9d9');
|
||||
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
|
@ -1,4 +1,11 @@
|
||||
<script lang="ts">
|
||||
import Sun from 'lucide-svelte/icons/sun';
|
||||
import Moon from 'lucide-svelte/icons/moon';
|
||||
|
||||
import { toggleMode } from 'mode-watcher';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
export let showLogout: boolean = false;
|
||||
</script>
|
||||
|
||||
@ -9,7 +16,7 @@
|
||||
<h1 class="max-sm:text-lg">WireAdmin</h1>
|
||||
</div>
|
||||
|
||||
<div class={'flex items-center gap-x-8'}>
|
||||
<div class={'flex items-center gap-x-3'}>
|
||||
<a
|
||||
href={'https://github.com/shahradelahi/wireadmin'}
|
||||
title={'Giv me a star on Github'}
|
||||
@ -17,21 +24,35 @@
|
||||
>
|
||||
<img
|
||||
src={'https://img.shields.io/github/stars/shahradelahi/wireadmin.svg?style=social&label=Star'}
|
||||
alt={'Giv me a star on Github'}
|
||||
alt={'Gimme a Star'}
|
||||
/>
|
||||
</a>
|
||||
|
||||
<Button on:click={toggleMode} variant="ghost" size="icon">
|
||||
<Sun
|
||||
class="h-[1rem] w-[1rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
|
||||
/>
|
||||
<Moon
|
||||
class="absolute h-[1rem] w-[1rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
|
||||
/>
|
||||
<span class="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
|
||||
{#if showLogout}
|
||||
<a
|
||||
href="/logout"
|
||||
rel="external"
|
||||
title="Logout"
|
||||
class="group text-sm/2 font-medium text-neutral-700 hover:text-neutral-800"
|
||||
>
|
||||
<i
|
||||
class="far fa-arrow-right-from-arc text-sm text-neutral-500 group-hover:text-neutral-800 mr-0.5"
|
||||
></i>
|
||||
Logout
|
||||
<a href="/logout" rel="external" title="Logout">
|
||||
<Button variant="ghost" class="group text-sm/2 gap-x-2 font-medium">
|
||||
<i
|
||||
class={cn(
|
||||
'far fa-arrow-right-from-arc text-sm mr-0.5',
|
||||
'text-neutral-500 group-hover:text-neutral-800',
|
||||
'dark:text-neutral-400 dark:group-hover:text-neutral-100',
|
||||
)}
|
||||
></i>
|
||||
<span
|
||||
class="text-neutral-700 hover:text-neutral-800 dark:text-neutral-100 dark:hover:text-neutral-100"
|
||||
>Logout</span
|
||||
>
|
||||
</Button>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import { Toaster } from 'svelte-french-toast';
|
||||
import { ModeWatcher } from 'mode-watcher';
|
||||
</script>
|
||||
|
||||
<ModeWatcher />
|
||||
<slot />
|
||||
<Toaster />
|
||||
|
@ -60,6 +60,6 @@
|
||||
</div>
|
||||
|
||||
<a href={`/${server.id}`} title="Manage the Server" class="hidden md:block">
|
||||
<Button variant="ghost" size="sm">Manage</Button>
|
||||
<Button variant="outline" size="sm">Manage</Button>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -45,6 +45,6 @@
|
||||
</div>
|
||||
|
||||
<a href={`/service/${slug}`} title="Manage the Server" class="hidden md:block">
|
||||
<Button variant="ghost" size="sm">Manage</Button>
|
||||
<Button variant="outline" size="sm">Manage</Button>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -7,6 +7,7 @@
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { getPeerConf } from '$lib/wireguard/utils';
|
||||
import { QRCodeDialog } from '$lib/components/qrcode-dialog';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
export let peer: Peer;
|
||||
|
||||
@ -56,7 +57,10 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between p-4 border border-neutral-200/60 rounded-md hover:border-neutral-200"
|
||||
class={cn(
|
||||
'flex items-center justify-between p-4 rounded-md',
|
||||
'border border-input bg-background hover:bg-accent/30 hover:text-accent-foreground',
|
||||
)}
|
||||
>
|
||||
<div class="flex items-center gap-x-2">
|
||||
<div
|
||||
@ -84,18 +88,18 @@
|
||||
<!-- QRCode -->
|
||||
<QRCodeDialog let:builder content={conf}>
|
||||
<PeerActionButton builders={[builder]} disabled={isLoading}>
|
||||
<i class={'fal text-neutral-700 group-hover:text-primary fa-qrcode'} />
|
||||
<i class={'fal fa-qrcode'} />
|
||||
</PeerActionButton>
|
||||
</QRCodeDialog>
|
||||
|
||||
<!-- Download -->
|
||||
<PeerActionButton disabled={isLoading} on:click={handleDownload}>
|
||||
<i class={'fal text-neutral-700 group-hover:text-primary fa-download'} />
|
||||
<i class={'fal fa-download'} />
|
||||
</PeerActionButton>
|
||||
|
||||
<!-- Remove -->
|
||||
<PeerActionButton loading={isLoading} on:click={handleRemove}>
|
||||
<i class={'fal text-neutral-700 group-hover:text-primary text-lg fa-trash-can'} />
|
||||
<i class={'fal fa-trash-can'} />
|
||||
</PeerActionButton>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -25,7 +25,8 @@
|
||||
{...$$restProps}
|
||||
class={cn(
|
||||
'group flex items-center justify-center w-10 aspect-square rounded-md',
|
||||
'bg-gray-200/80 hover:bg-gray-100/50',
|
||||
'bg-gray-200/80 hover:bg-gray-100/50 dark:bg-neutral-800/80 dark:hover:bg-neutral-800/50',
|
||||
'text-neutral-700 dark:text-neutral-300 hover:text-primary dark:hover:text-primary',
|
||||
'border border-transparent hover:border-primary',
|
||||
'transition-colors duration-200 ease-in-out',
|
||||
'cursor-pointer',
|
||||
|
@ -73,7 +73,7 @@
|
||||
<CardTitle>Logs</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="relative">
|
||||
<textarea class="w-full h-80 p-2 bg-gray-100" readonly bind:value={logs} />
|
||||
<textarea class="w-full h-80 p-2" readonly bind:value={logs} />
|
||||
{#if !logs}
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<i class="text-4xl animate-spin fas fa-circle-notch"></i>
|
||||
@ -81,7 +81,7 @@
|
||||
{/if}
|
||||
</CardContent>
|
||||
<CardFooter class="flex justify-end gap-2">
|
||||
<Button on:click={restart}>Restart</Button>
|
||||
<Button on:click={restart} variant="outline">Restart</Button>
|
||||
<Button variant="destructive" on:click={clearLogs} disabled={!logs}>Clear</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|