diff --git a/.env.example b/.env.example
index d3c3a69..4a0393e 100644
--- a/.env.example
+++ b/.env.example
@@ -1,4 +1,4 @@
-DATABASE_URL=local.db
+DATABASE_URL=file:local.db
AUTH_DOMAIN=auth.lab.cazzzer.com
AUTH_CLIENT_ID=
AUTH_CLIENT_SECRET=
diff --git a/bun.lockb b/bun.lockb
index ec7b8fc..e22ff0d 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index 8bc45b9..d64ca26 100644
--- a/package.json
+++ b/package.json
@@ -11,8 +11,10 @@
"format": "prettier --write .",
"lint": "prettier --check . && eslint .",
"db:push": "drizzle-kit push",
+ "db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
- "db:studio": "drizzle-kit studio"
+ "db:studio": "drizzle-kit studio",
+ "db:seed": "bun run ./src/lib/server/db/seed.ts"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
@@ -26,7 +28,7 @@
"autoprefixer": "^10.4.20",
"bits-ui": "^0.21.16",
"clsx": "^2.1.1",
- "drizzle-kit": "^0.22.0",
+ "drizzle-kit": "^0.30.1",
"eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0",
@@ -44,11 +46,11 @@
"vite": "^5.0.3"
},
"dependencies": {
+ "@libsql/client": "^0.14.0",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
"arctic": "^2.2.1",
- "better-sqlite3": "^11.1.2",
- "drizzle-orm": "^0.33.0",
+ "drizzle-orm": "^0.38.2",
"lucide-svelte": "^0.454.0"
}
}
diff --git a/src/lib/opnsense/wg.ts b/src/lib/opnsense/wg.ts
index b463f58..3bef846 100644
--- a/src/lib/opnsense/wg.ts
+++ b/src/lib/opnsense/wg.ts
@@ -59,3 +59,11 @@ export interface OpnsenseWgPeers {
* };
* ```
*/
+
+export interface OpnsenseWgServers {
+ status: "ok" | string | number;
+ rows: {
+ name: string;
+ uuid: string;
+ }[];
+}
diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts
index 667c442..1973590 100644
--- a/src/lib/server/auth.ts
+++ b/src/lib/server/auth.ts
@@ -23,7 +23,7 @@ export async function createSession(userId: string): Promise
{
userId,
expiresAt: new Date(Date.now() + DAY_IN_MS * 30)
};
- await db.insert(table.session).values(session);
+ await db.insert(table.sessions).values(session);
return session;
}
@@ -38,7 +38,7 @@ export function setSessionTokenCookie(event: RequestEvent, sessionId: string, ex
}
export async function invalidateSession(sessionId: string): Promise {
- await db.delete(table.session).where(eq(table.session.id, sessionId));
+ await db.delete(table.sessions).where(eq(table.sessions.id, sessionId));
}
export function deleteSessionTokenCookie(event: RequestEvent) {
@@ -49,12 +49,12 @@ export async function validateSession(sessionId: string) {
const [result] = await db
.select({
// Adjust user table here to tweak returned data
- user: { id: table.user.id, username: table.user.username, name: table.user.name },
- session: table.session
+ user: { id: table.users.id, username: table.users.username, name: table.users.name },
+ session: table.sessions
})
- .from(table.session)
- .innerJoin(table.user, eq(table.session.userId, table.user.id))
- .where(eq(table.session.id, sessionId));
+ .from(table.sessions)
+ .innerJoin(table.users, eq(table.sessions.userId, table.users.id))
+ .where(eq(table.sessions.id, sessionId));
if (!result) {
return { session: null, user: null };
@@ -63,7 +63,7 @@ export async function validateSession(sessionId: string) {
const sessionExpired = Date.now() >= session.expiresAt.getTime();
if (sessionExpired) {
- await db.delete(table.session).where(eq(table.session.id, session.id));
+ await db.delete(table.sessions).where(eq(table.sessions.id, session.id));
return { session: null, user: null };
}
@@ -71,9 +71,9 @@ export async function validateSession(sessionId: string) {
if (renewSession) {
session.expiresAt = new Date(Date.now() + DAY_IN_MS * 30);
await db
- .update(table.session)
+ .update(table.sessions)
.set({ expiresAt: session.expiresAt })
- .where(eq(table.session.id, session.id));
+ .where(eq(table.sessions.id, session.id));
}
return { session, user };
diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts
index d8c8b85..62c6f8e 100644
--- a/src/lib/server/db/index.ts
+++ b/src/lib/server/db/index.ts
@@ -1,8 +1,7 @@
-import { drizzle } from 'drizzle-orm/better-sqlite3';
-import Database from 'better-sqlite3';
-import { env } from '$env/dynamic/private';
+import { drizzle } from 'drizzle-orm/libsql';
import assert from 'node:assert';
+import * as schema from './schema';
+import { DATABASE_URL } from '$env/static/private';
-assert(env.DATABASE_URL, 'DATABASE_URL is not set');
-const client = new Database(env.DATABASE_URL);
-export const db = drizzle(client);
+assert(DATABASE_URL, 'DATABASE_URL is not set');
+export const db= drizzle(DATABASE_URL, { schema });
diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts
index ddcbc75..6b51959 100644
--- a/src/lib/server/db/schema.ts
+++ b/src/lib/server/db/schema.ts
@@ -1,19 +1,59 @@
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
+import { relations } from 'drizzle-orm';
-export const user = sqliteTable('user', {
+export const users = sqliteTable('users', {
id: text('id').primaryKey(),
username: text('username').notNull(),
name: text('name').notNull(),
});
-export const session = sqliteTable('session', {
+export const usersRelations = relations(users, ({ many }) => ({
+ wgClients: many(wgClients),
+}));
+
+export const sessions = sqliteTable('sessions', {
id: text('id').primaryKey(),
userId: text('user_id')
.notNull()
- .references(() => user.id),
- expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull()
+ .references(() => users.id),
+ expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
});
-export type Session = typeof session.$inferSelect;
+export const ipAllocations = sqliteTable('ip_allocations', {
+ // for now, id will be the same as the ipIndex
+ id: integer('id').primaryKey({ autoIncrement: true }),
+ // clientId is nullable because allocations can remain after the client is deleted
+ // unique for now, only allowing one allocation per client
+ clientId: integer('client_id')
+ .unique()
+ .references(() => wgClients.id),
+});
-export type User = typeof user.$inferSelect;
+export const wgClients = sqliteTable('wg_clients', {
+ id: integer().primaryKey({ autoIncrement: true }),
+ userId: text('user_id')
+ .notNull()
+ .references(() => users.id),
+ name: text('name').notNull(),
+ // questioning whether this should be nullable
+ opnsenseId: text('opnsense_id'),
+ publicKey: text('public_key').notNull().unique(),
+ // nullable for the possibility of a client supplying their own private key
+ privateKey: text('private_key'),
+ // nullable for the possibility of no psk
+ preSharedKey: text('pre_shared_key'),
+ // discarded ideas:
+ // (mostly because they make finding the next available ipIndex difficult)
+ // ipIndex: integer('ip_index').notNull().unique(),
+ // allowedIps: text('allowed_ips').notNull(),
+});
+
+export const wgClientsRelations = relations(wgClients, ({ one }) => ({
+ ipAllocation: one(ipAllocations),
+}));
+
+export type WgClient = typeof wgClients.$inferSelect;
+
+export type Session = typeof sessions.$inferSelect;
+
+export type User = typeof users.$inferSelect;
diff --git a/src/lib/server/db/seed.ts b/src/lib/server/db/seed.ts
new file mode 100644
index 0000000..92f1418
--- /dev/null
+++ b/src/lib/server/db/seed.ts
@@ -0,0 +1,29 @@
+import { users, wgClients } from './schema';
+import { eq } from 'drizzle-orm';
+import assert from 'node:assert';
+import { drizzle } from 'drizzle-orm/libsql';
+import * as schema from '$lib/server/db/schema';
+
+assert(process.env.DATABASE_URL, 'DATABASE_URL is not set');
+const db = drizzle(process.env.DATABASE_URL, { schema });
+
+export async function seed() {
+ const user = await db.query.users.findFirst({ where: eq(users.username, 'CaZzzer') });
+ assert(user, 'User not found');
+
+ const clients = [
+ {
+ userId: user.id,
+ name: 'Client1',
+ publicKey: 'BJ5faPVJsDP4CCxNYilmKnwlQXOtXEOJjqIwb4U/CgM=',
+ privateKey: 'KKqsHDu30WCSrVsyzMkOKbE3saQ+wlx0sBwGs61UGXk=',
+ preSharedKey: '0LWopbrISXBNHUxr+WOhCSAg+0hD8j3TLmpyzHkBHCQ=',
+ ipIndex: 1,
+ // allowedIps: '10.18.11.101/32,fd00::1/112',
+ }
+ ]
+
+ await db.insert(wgClients).values(clients);
+}
+
+seed();
diff --git a/src/lib/server/opnsense.ts b/src/lib/server/opnsense.ts
deleted file mode 100644
index 58655b0..0000000
--- a/src/lib/server/opnsense.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { env } from '$env/dynamic/private';
-import assert from 'node:assert';
-import { encodeBasicCredentials } from 'arctic/dist/request';
-import { dev } from '$app/environment';
-
-assert(env.OPNSENSE_API_URL, 'OPNSENSE_API_URL is not set');
-assert(env.OPNSENSE_API_KEY, 'OPNSENSE_API_KEY is not set');
-assert(env.OPNSENSE_API_SECRET, 'OPNSENSE_API_SECRET is not set');
-assert(env.OPNSENSE_WG_IFNAME, 'OPNSENSE_WG_IFNAME is not set');
-
-export const opnsenseUrl = env.OPNSENSE_API_URL;
-export const opnsenseAuth = "Basic " + encodeBasicCredentials(env.OPNSENSE_API_KEY, env.OPNSENSE_API_SECRET);
-export const opnsenseIfname = env.OPNSENSE_WG_IFNAME;
-
-// unset secret for security
-if (!dev) env.OPNSENSE_API_SECRET = "";
diff --git a/src/lib/server/opnsense/index.ts b/src/lib/server/opnsense/index.ts
new file mode 100644
index 0000000..435365b
--- /dev/null
+++ b/src/lib/server/opnsense/index.ts
@@ -0,0 +1,33 @@
+import { env } from '$env/dynamic/private';
+import assert from 'node:assert';
+import { encodeBasicCredentials } from 'arctic/dist/request';
+import { dev } from '$app/environment';
+import type { OpnsenseWgServers } from '$lib/opnsense/wg';
+
+assert(env.OPNSENSE_API_URL, 'OPNSENSE_API_URL is not set');
+assert(env.OPNSENSE_API_KEY, 'OPNSENSE_API_KEY is not set');
+assert(env.OPNSENSE_API_SECRET, 'OPNSENSE_API_SECRET is not set');
+assert(env.OPNSENSE_WG_IFNAME, 'OPNSENSE_WG_IFNAME is not set');
+
+export const opnsenseUrl = env.OPNSENSE_API_URL;
+export const opnsenseAuth = "Basic " + encodeBasicCredentials(env.OPNSENSE_API_KEY, env.OPNSENSE_API_SECRET);
+export const opnsenseIfname = env.OPNSENSE_WG_IFNAME;
+
+// unset secret for security
+if (!dev) env.OPNSENSE_API_SECRET = "";
+
+// this might be pretty bad if the server is down and in a bunch of other cases
+// TODO: write a retry loop later
+const resServers = await fetch(`${opnsenseUrl}/api/wireguard/client/list_servers`, {
+ method: 'GET',
+ headers: {
+ Authorization: opnsenseAuth,
+ Accept: 'application/json',
+ }
+});
+assert(resServers.ok, 'Failed to fetch OPNsense WireGuard servers');
+const servers = await resServers.json() as OpnsenseWgServers;
+assert.equal(servers.status, 'ok', 'Failed to fetch OPNsense WireGuard servers');
+export const serverUuid = servers.rows.find(server => server.name === opnsenseIfname)?.uuid;
+assert(serverUuid, 'Failed to find server UUID for OPNsense WireGuard server');
+console.log('OPNsense WireGuard server UUID:', serverUuid);
diff --git a/src/routes/api/connections/+server.ts b/src/routes/api/connections/+server.ts
index 4b2e343..1167743 100644
--- a/src/routes/api/connections/+server.ts
+++ b/src/routes/api/connections/+server.ts
@@ -1,9 +1,12 @@
import { error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
-import { opnsenseAuth, opnsenseIfname, opnsenseUrl } from '$lib/server/opnsense';
+import { opnsenseAuth, opnsenseUrl } from '$lib/server/opnsense';
import type { OpnsenseWgPeers } from '$lib/opnsense/wg';
-export const GET: RequestHandler = async () => {
+export const GET: RequestHandler = async (event) => {
+ if (!event.locals.user) {
+ return error(401, 'Unauthorized');
+ }
const apiUrl = `${opnsenseUrl}/api/wireguard/service/show`;
const options: RequestInit = {
method: 'POST',
@@ -13,21 +16,24 @@ export const GET: RequestHandler = async () => {
'Content-Type': 'application/json',
},
body: JSON.stringify({
- "current": 1,
+ 'current': 1,
// "rowCount": 7,
- "sort": {},
- "searchPhrase": "",
- "type": ["peer"],
+ 'sort': {},
+ // TODO: use a more unique search phrase
+ // unfortunately 64 character limit,
+ // but it should be fine if users can't change their own username
+ 'searchPhrase': `vpgen-${event.locals.user.username}`,
+ 'type': ['peer'],
}),
};
- console.log("Fetching peers from OPNsense WireGuard API: ", apiUrl, options)
+ console.log('Fetching peers from OPNsense WireGuard API: ', apiUrl, options)
const res = await fetch(apiUrl, options);
const peers = await res.json() as OpnsenseWgPeers;
- peers.rows = peers.rows.filter(peer => peer['latest-handshake'] && peer.ifname === opnsenseIfname)
+ peers.rows = peers.rows.filter(peer => peer['latest-handshake'])
if (!peers) {
- error(500, "Error getting info from OPNsense API");
+ return error(500, 'Error getting info from OPNsense API');
}
return new Response(JSON.stringify(peers), {
headers: {
diff --git a/src/routes/auth/authentik/callback/+server.ts b/src/routes/auth/authentik/callback/+server.ts
index 791e8fa..a150155 100644
--- a/src/routes/auth/authentik/callback/+server.ts
+++ b/src/routes/auth/authentik/callback/+server.ts
@@ -39,7 +39,7 @@ export async function GET(event: RequestEvent): Promise {
const userId: string = claims.sub;
const username: string = claims.preferred_username;
- const [existingUser] = await db.select().from(table.user).where(eq(table.user.id, userId));
+ const existingUser = await db.query.users.findFirst({where: eq(table.users.id, userId)});
if (existingUser) {
const session = await createSession(existingUser.id);
@@ -59,7 +59,7 @@ export async function GET(event: RequestEvent): Promise {
};
try {
- await db.insert(table.user).values(user);
+ await db.insert(table.users).values(user);
const session = await createSession(user.id);
setSessionTokenCookie(event, session.id, session.expiresAt);
} catch (e) {