Browse Source

rewrite middleware logic, support basic auth

pull/1397/head
Bernd Storath 9 months ago
parent
commit
32f1e92df0
  1. 9
      CHANGELOG.md
  2. 5
      README.md
  3. 8
      src/app/components/ui/UserMenu.vue
  4. 4
      src/app/pages/admin.vue
  5. 2
      src/app/pages/admin/features.vue
  6. 8
      src/app/pages/me.vue
  7. 2
      src/app/utils/api.ts
  8. 0
      src/server/api/admin/features.post.ts
  9. 18
      src/server/middleware/auth.ts
  10. 79
      src/server/middleware/session.ts
  11. 20
      src/server/middleware/setup.ts
  12. 6
      src/server/utils/session.ts
  13. 2
      src/services/database/migrations/1.ts

9
CHANGELOG.md

@ -14,6 +14,15 @@ This update is an entire rewrite to make it even easier to set up your own VPN.
- Almost all Environment variables removed - Almost all Environment variables removed
- New and Improved UI - New and Improved UI
- API Basic Authentication
- Added Docs
- Incrementing Version -> Semantic Versioning
- CIDR Support
- IPv6 Support
- Changed API Structure
- Changed Database Structure
- Deprecated Dockerless Installations
- Added Docker Volume Mount
## Minor Changes ## Minor Changes

5
README.md

@ -62,6 +62,8 @@ And log in again.
### 2. Run WireGuard Easy ### 2. Run WireGuard Easy
<!-- TODO: prioritize docker compose over docker run -->
To setup the IPv6 Network, simply run once: To setup the IPv6 Network, simply run once:
```bash ```bash
@ -82,6 +84,7 @@ To automatically install & run wg-easy, simply run:
--ip6 fdcc:ad94:bacf:61a3::2a \ --ip6 fdcc:ad94:bacf:61a3::2a \
--ip 10.42.42.42 \ --ip 10.42.42.42 \
-v ~/.wg-easy:/etc/wireguard \ -v ~/.wg-easy:/etc/wireguard \
-v /lib/modules:/lib/modules:ro \
-p 51820:51820/udp \ -p 51820:51820/udp \
-p 51821:51821/tcp \ -p 51821:51821/tcp \
--cap-add NET_ADMIN \ --cap-add NET_ADMIN \
@ -97,7 +100,7 @@ To automatically install & run wg-easy, simply run:
The Web UI will now be available on `http://0.0.0.0:51821`. The Web UI will now be available on `http://0.0.0.0:51821`.
The Prometheus metrics will now be available on `http://0.0.0.0:51821/metrics`. Grafana dashboard [21733](https://grafana.com/grafana/dashboards/21733-wireguard/) The Prometheus metrics will now be available on `http://0.0.0.0:51821/api/metrics`. Grafana dashboard [21733](https://grafana.com/grafana/dashboards/21733-wireguard/)
> 💡 Your configuration files will be saved in `~/.wg-easy` > 💡 Your configuration files will be saved in `~/.wg-easy`

8
src/app/components/ui/UserMenu.vue

@ -36,6 +36,14 @@
Clients Clients
</NuxtLink> </NuxtLink>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem>
<NuxtLink
to="/me"
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
Account
</NuxtLink>
</DropdownMenuItem>
<DropdownMenuItem v-if="authStore.userData?.role === 'ADMIN'"> <DropdownMenuItem v-if="authStore.userData?.role === 'ADMIN'">
<NuxtLink <NuxtLink
to="/admin" to="/admin"

4
src/app/pages/admin.vue

@ -42,8 +42,8 @@ const route = useRoute();
const menuItems = [ const menuItems = [
{ id: 'features', name: 'Features' }, { id: 'features', name: 'Features' },
{ id: 'user', name: 'User' }, { id: 'statistics', name: 'Statistics' },
{ id: 'server', name: 'Server' }, { id: 'metrics', name: 'Metrics' },
]; ];
const activeMenuItem = computed(() => { const activeMenuItem = computed(() => {

2
src/app/pages/admin/features.vue

@ -33,8 +33,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { UiToast } from '#build/components';
const globalStore = useGlobalStore(); const globalStore = useGlobalStore();
const open = ref(false); const open = ref(false);

8
src/app/pages/me.vue

@ -0,0 +1,8 @@
<template>
<main>
<p>Change Username:</p>
<p>Change Name:</p>
<p>Change E-Mail:</p>
<p>Change Password:</p>
</main>
</template>

2
src/app/utils/api.ts

@ -147,7 +147,7 @@ class API {
} }
async updateFeatures(features: Record<string, { enabled: boolean }>) { async updateFeatures(features: Record<string, { enabled: boolean }>) {
return $fetch('/api/features', { return $fetch('/api/admin/features', {
method: 'post', method: 'post',
body: { features }, body: { features },
}); });

0
src/server/api/features.post.ts → src/server/api/admin/features.post.ts

18
src/server/middleware/auth.ts

@ -2,22 +2,26 @@ export default defineEventHandler(async (event) => {
const url = getRequestURL(event); const url = getRequestURL(event);
const session = await useWGSession(event); const session = await useWGSession(event);
// Api handled by session, Setup handled with setup middleware
if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/setup')) {
return;
}
if (url.pathname === '/login') { if (url.pathname === '/login') {
if (session.data.userId) { if (session.data.userId) {
return sendRedirect(event, '/', 302); return sendRedirect(event, '/', 302);
} }
return;
} }
if (url.pathname === '/') { // Require auth for every page other than Login
if (!session.data.userId) { // TODO: investigate /__nuxt_error (error page when unauthenticated)
return sendRedirect(event, '/login', 302); if (!session.data.userId) {
} console.log(url.pathname);
return sendRedirect(event, '/login', 302);
} }
if (url.pathname.startsWith('/admin')) { if (url.pathname.startsWith('/admin')) {
if (!session.data.userId) {
return sendRedirect(event, '/login', 302);
}
const user = await Database.user.findById(session.data.userId); const user = await Database.user.findById(session.data.userId);
if (!user) { if (!user) {
return sendRedirect(event, '/login', 302); return sendRedirect(event, '/login', 302);

79
src/server/middleware/session.ts

@ -1,5 +1,9 @@
import type { User } from '~~/services/database/repositories/user';
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const url = getRequestURL(event); const url = getRequestURL(event);
// If one method of a route is public, every method is public!
// Handle api routes
if ( if (
!url.pathname.startsWith('/api/') || !url.pathname.startsWith('/api/') ||
url.pathname === '/api/account/setup' || url.pathname === '/api/account/setup' ||
@ -13,36 +17,73 @@ export default defineEventHandler(async (event) => {
const system = await Database.system.get(); const system = await Database.system.get();
const session = await getSession<WGSession>(event, system.sessionConfig); const session = await getSession<WGSession>(event, system.sessionConfig);
if (session.id && session.data.userId) {
return;
}
const authorization = getHeader(event, 'Authorization'); const authorization = getHeader(event, 'Authorization');
if (url.pathname.startsWith('/api/') && authorization) {
let user: User | undefined = undefined;
if (session.data.userId) {
// Handle if authenticating using Session
user = await Database.user.findById(session.data.userId);
} else if (authorization) {
// Handle if authenticating using Header
const [method, value] = authorization.split(' ');
// Support Basic Authentication
// TODO: support personal access token or similar
if (method !== 'Basic' || !value) {
throw createError({
statusCode: 401,
statusMessage: 'Session failed',
});
}
const basicValue = Buffer.from(value, 'base64').toString('utf-8');
// Split by first ":"
const index = basicValue.indexOf(':');
const username = basicValue.substring(0, index);
const password = basicValue.substring(index + 1);
if (!username || !password) {
throw createError({
statusCode: 401,
statusMessage: 'Session failed',
});
}
const users = await Database.user.findAll(); const users = await Database.user.findAll();
const user = users.find((user) => user.id == session.data.userId); const foundUser = users.find((v) => v.username === username);
if (!user)
if (!foundUser) {
throw createError({ throw createError({
statusCode: 401, statusCode: 401,
statusMessage: 'Session failed', statusMessage: 'Session failed',
}); });
}
const userHashPassword = foundUser.password;
const passwordValid = await isPasswordValid(password, userHashPassword);
const userHashPassword = user.password; if (!passwordValid) {
const passwordValid = await isPasswordValid( throw createError({
authorization, statusCode: 401,
userHashPassword statusMessage: 'Incorrect Password',
); });
if (passwordValid) {
return;
} }
user = foundUser;
}
if (!user) {
throw createError({ throw createError({
statusCode: 401, statusCode: 401,
statusMessage: 'Incorrect Password', statusMessage: 'Not logged in',
}); });
} }
throw createError({ if (url.pathname.startsWith('/api/admin')) {
statusCode: 401, if (user.role !== 'ADMIN') {
statusMessage: 'Not logged in', throw createError({
}); statusCode: 403,
statusMessage: 'Missing Permissions',
});
}
}
}); });

20
src/server/middleware/setup.ts

@ -2,24 +2,30 @@
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const url = getRequestURL(event); const url = getRequestURL(event);
// TODO: redirect to login page if already set up // User can't be logged in, and public routes can be accessed whenever
if (url.pathname.startsWith('/api/')) {
if (
url.pathname === '/setup' ||
url.pathname === '/api/account/setup' ||
url.pathname === '/api/features'
) {
return; return;
} }
const users = await Database.user.findAll(); const users = await Database.user.findAll();
if (users.length === 0) { if (users.length === 0) {
// If not setup
if (url.pathname.startsWith('/setup')) {
return;
}
if (url.pathname.startsWith('/api/')) { if (url.pathname.startsWith('/api/')) {
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
statusMessage: 'Invalid State', statusMessage: 'Invalid State',
}); });
} }
console.log(url.pathname);
return sendRedirect(event, '/setup', 302); return sendRedirect(event, '/setup', 302);
} else {
// If already set up
if (!url.pathname.startsWith('/setup')) {
return;
}
return sendRedirect(event, '/login', 302);
} }
}); });

6
src/server/utils/session.ts

@ -1,10 +1,10 @@
import type { H3Event } from 'h3'; import type { H3Event } from 'h3';
export type WGSession = { export type WGSession = Partial<{
userId: string; userId: string;
}; }>;
export async function useWGSession(event: H3Event) { export async function useWGSession(event: H3Event) {
const system = await Database.system.get(); const system = await Database.system.get();
return useSession<Partial<WGSession>>(event, system.sessionConfig); return useSession<WGSession>(event, system.sessionConfig);
} }

2
src/services/database/migrations/1.ts

@ -62,6 +62,7 @@ export async function run1(db: Low<Database>) {
password: null, password: null,
}, },
sessionConfig: { sessionConfig: {
// TODO: be able to invalidate all sessions
password: getRandomHex(256), password: getRandomHex(256),
name: 'wg-easy', name: 'wg-easy',
cookie: {}, cookie: {},
@ -71,7 +72,6 @@ export async function run1(db: Low<Database>) {
clients: {}, clients: {},
}; };
// TODO: properly check if ipv6 support
database.system.iptables.PostUp = database.system.iptables.PostUp =
`iptables -t nat -A POSTROUTING -s ${database.system.userConfig.address4Range} -o ${database.system.wgDevice} -j MASQUERADE; `iptables -t nat -A POSTROUTING -s ${database.system.userConfig.address4Range} -o ${database.system.wgDevice} -j MASQUERADE;
iptables -A INPUT -p udp -m udp --dport ${database.system.wgPort} -j ACCEPT; iptables -A INPUT -p udp -m udp --dport ${database.system.wgPort} -j ACCEPT;

Loading…
Cancel
Save