mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-07 11:16:37 +00:00
Merge branch 'clients-user' into refactor/separate-tables
This commit is contained in:
@@ -2148,7 +2148,7 @@
|
|||||||
"deviceOrganizationsAccess": "Access to all organizations your account has access to",
|
"deviceOrganizationsAccess": "Access to all organizations your account has access to",
|
||||||
"deviceAuthorize": "Authorize {applicationName}",
|
"deviceAuthorize": "Authorize {applicationName}",
|
||||||
"deviceConnected": "Device Connected!",
|
"deviceConnected": "Device Connected!",
|
||||||
"deviceAuthorizedMessage": "Your device is authorized to access your account.",
|
"deviceAuthorizedMessage": "Device is authorized to access your account.",
|
||||||
"pangolinCloud": "Pangolin Cloud",
|
"pangolinCloud": "Pangolin Cloud",
|
||||||
"viewDevices": "View Devices",
|
"viewDevices": "View Devices",
|
||||||
"viewDevicesDescription": "Manage your connected devices",
|
"viewDevicesDescription": "Manage your connected devices",
|
||||||
@@ -2210,5 +2210,5 @@
|
|||||||
"enterIdentifier": "Enter identifier",
|
"enterIdentifier": "Enter identifier",
|
||||||
"identifier": "Identifier",
|
"identifier": "Identifier",
|
||||||
"deviceLoginUseDifferentAccount": "Not you? Use a different account.",
|
"deviceLoginUseDifferentAccount": "Not you? Use a different account.",
|
||||||
"deviceLoginDeviceRequestingAccessToAccount": "Your device is requesting access to this account."
|
"deviceLoginDeviceRequestingAccessToAccount": "A device is requesting access to this account."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,13 +36,15 @@ export async function createSession(
|
|||||||
const sessionId = encodeHexLowerCase(
|
const sessionId = encodeHexLowerCase(
|
||||||
sha256(new TextEncoder().encode(token))
|
sha256(new TextEncoder().encode(token))
|
||||||
);
|
);
|
||||||
const session: Session = {
|
const [session] = await db
|
||||||
sessionId: sessionId,
|
.insert(sessions)
|
||||||
userId,
|
.values({
|
||||||
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(),
|
sessionId: sessionId,
|
||||||
issuedAt: new Date().getTime()
|
userId,
|
||||||
};
|
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(),
|
||||||
await db.insert(sessions).values(session);
|
issuedAt: new Date().getTime()
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,13 +18,19 @@ export async function verifySession(req: Request, forceLogin?: boolean) {
|
|||||||
user: null
|
user: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (res.session.deviceAuthUsed) {
|
||||||
|
return {
|
||||||
|
session: null,
|
||||||
|
user: null
|
||||||
|
};
|
||||||
|
}
|
||||||
if (!res.session.issuedAt) {
|
if (!res.session.issuedAt) {
|
||||||
return {
|
return {
|
||||||
session: null,
|
session: null,
|
||||||
user: null
|
user: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const mins = 3 * 60 * 1000;
|
const mins = 5 * 60 * 1000;
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
if (now - res.session.issuedAt > mins) {
|
if (now - res.session.issuedAt > mins) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
import { InferSelectModel } from "drizzle-orm";
|
import { InferSelectModel } from "drizzle-orm";
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
|
import { alias } from "yargs";
|
||||||
|
|
||||||
export const domains = pgTable("domains", {
|
export const domains = pgTable("domains", {
|
||||||
domainId: varchar("domainId").primaryKey(),
|
domainId: varchar("domainId").primaryKey(),
|
||||||
@@ -40,6 +41,7 @@ export const orgs = pgTable("orgs", {
|
|||||||
orgId: varchar("orgId").primaryKey(),
|
orgId: varchar("orgId").primaryKey(),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
subnet: varchar("subnet"),
|
subnet: varchar("subnet"),
|
||||||
|
utilitySubnet: varchar("utilitySubnet"), // this is the subnet for utility addresses
|
||||||
createdAt: text("createdAt"),
|
createdAt: text("createdAt"),
|
||||||
requireTwoFactor: boolean("requireTwoFactor"),
|
requireTwoFactor: boolean("requireTwoFactor"),
|
||||||
maxSessionLengthHours: integer("maxSessionLengthHours"),
|
maxSessionLengthHours: integer("maxSessionLengthHours"),
|
||||||
@@ -209,7 +211,8 @@ export const siteResources = pgTable("siteResources", {
|
|||||||
destinationPort: integer("destinationPort"), // only for port mode
|
destinationPort: integer("destinationPort"), // only for port mode
|
||||||
destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode
|
destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode
|
||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
alias: varchar("alias")
|
alias: varchar("alias"),
|
||||||
|
aliasAddress: varchar("aliasAddress")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clientSiteResources = pgTable("clientSiteResources", {
|
export const clientSiteResources = pgTable("clientSiteResources", {
|
||||||
@@ -284,7 +287,8 @@ export const sessions = pgTable("session", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.userId, { onDelete: "cascade" }),
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
|
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
|
||||||
issuedAt: bigint("issuedAt", { mode: "number" })
|
issuedAt: bigint("issuedAt", { mode: "number" }),
|
||||||
|
deviceAuthUsed: boolean("deviceAuthUsed").notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const newtSessions = pgTable("newtSession", {
|
export const newtSessions = pgTable("newtSession", {
|
||||||
@@ -661,7 +665,8 @@ export const clientSitesAssociationsCache = pgTable(
|
|||||||
.notNull(),
|
.notNull(),
|
||||||
siteId: integer("siteId").notNull(),
|
siteId: integer("siteId").notNull(),
|
||||||
isRelayed: boolean("isRelayed").notNull().default(false),
|
isRelayed: boolean("isRelayed").notNull().default(false),
|
||||||
endpoint: varchar("endpoint")
|
endpoint: varchar("endpoint"),
|
||||||
|
publicKey: varchar("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
import { InferSelectModel } from "drizzle-orm";
|
import { InferSelectModel } from "drizzle-orm";
|
||||||
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
|
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
|
||||||
|
import { no } from "zod/v4/locales";
|
||||||
|
|
||||||
export const domains = sqliteTable("domains", {
|
export const domains = sqliteTable("domains", {
|
||||||
domainId: text("domainId").primaryKey(),
|
domainId: text("domainId").primaryKey(),
|
||||||
@@ -32,6 +33,7 @@ export const orgs = sqliteTable("orgs", {
|
|||||||
orgId: text("orgId").primaryKey(),
|
orgId: text("orgId").primaryKey(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
subnet: text("subnet"),
|
subnet: text("subnet"),
|
||||||
|
utilitySubnet: text("utilitySubnet"), // this is the subnet for utility addresses
|
||||||
createdAt: text("createdAt"),
|
createdAt: text("createdAt"),
|
||||||
requireTwoFactor: integer("requireTwoFactor", { mode: "boolean" }),
|
requireTwoFactor: integer("requireTwoFactor", { mode: "boolean" }),
|
||||||
maxSessionLengthHours: integer("maxSessionLengthHours"), // hours
|
maxSessionLengthHours: integer("maxSessionLengthHours"), // hours
|
||||||
@@ -230,7 +232,8 @@ export const siteResources = sqliteTable("siteResources", {
|
|||||||
destinationPort: integer("destinationPort"), // only for port mode
|
destinationPort: integer("destinationPort"), // only for port mode
|
||||||
destination: text("destination").notNull(), // ip, cidr, hostname
|
destination: text("destination").notNull(), // ip, cidr, hostname
|
||||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||||
alias: text("alias")
|
alias: text("alias"),
|
||||||
|
aliasAddress: text("aliasAddress")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clientSiteResources = sqliteTable("clientSiteResources", {
|
export const clientSiteResources = sqliteTable("clientSiteResources", {
|
||||||
@@ -370,7 +373,8 @@ export const clientSitesAssociationsCache = sqliteTable(
|
|||||||
isRelayed: integer("isRelayed", { mode: "boolean" })
|
isRelayed: integer("isRelayed", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
endpoint: text("endpoint")
|
endpoint: text("endpoint"),
|
||||||
|
publicKey: text("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -413,7 +417,10 @@ export const sessions = sqliteTable("session", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.userId, { onDelete: "cascade" }),
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
expiresAt: integer("expiresAt").notNull(),
|
expiresAt: integer("expiresAt").notNull(),
|
||||||
issuedAt: integer("issuedAt")
|
issuedAt: integer("issuedAt"),
|
||||||
|
deviceAuthUsed: integer("deviceAuthUsed", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const newtSessions = sqliteTable("newtSession", {
|
export const newtSessions = sqliteTable("newtSession", {
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
import { clients, clientSitesAssociationsCache, db, olms, orgs, roleClients, roles, userClients, userOrgs, Transaction } from "@server/db";
|
import {
|
||||||
|
clients,
|
||||||
|
db,
|
||||||
|
olms,
|
||||||
|
orgs,
|
||||||
|
roleClients,
|
||||||
|
roles,
|
||||||
|
userClients,
|
||||||
|
userOrgs,
|
||||||
|
Transaction
|
||||||
|
} from "@server/db";
|
||||||
import { eq, and, notInArray } from "drizzle-orm";
|
import { eq, and, notInArray } from "drizzle-orm";
|
||||||
import { listExitNodes } from "#dynamic/lib/exitNodes";
|
import { listExitNodes } from "#dynamic/lib/exitNodes";
|
||||||
import { getNextAvailableClientSubnet } from "@server/lib/ip";
|
import { getNextAvailableClientSubnet } from "@server/lib/ip";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
import { rebuildClientAssociationsFromClient } from "./rebuildClientAssociations";
|
||||||
|
import { sendTerminateClient } from "@server/routers/client/terminate";
|
||||||
|
|
||||||
export async function calculateUserClientsForOrgs(
|
export async function calculateUserClientsForOrgs(
|
||||||
userId: string,
|
userId: string,
|
||||||
@@ -88,7 +100,10 @@ export async function calculateUserClientsForOrgs(
|
|||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(roleClients.roleId, adminRole.roleId),
|
eq(roleClients.roleId, adminRole.roleId),
|
||||||
eq(roleClients.clientId, existingClient.clientId)
|
eq(
|
||||||
|
roleClients.clientId,
|
||||||
|
existingClient.clientId
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
@@ -110,7 +125,10 @@ export async function calculateUserClientsForOrgs(
|
|||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(userClients.userId, userId),
|
eq(userClients.userId, userId),
|
||||||
eq(userClients.clientId, existingClient.clientId)
|
eq(
|
||||||
|
userClients.clientId,
|
||||||
|
existingClient.clientId
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
@@ -172,6 +190,11 @@ export async function calculateUserClientsForOrgs(
|
|||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
await rebuildClientAssociationsFromClient(
|
||||||
|
newClient,
|
||||||
|
transaction
|
||||||
|
);
|
||||||
|
|
||||||
// Grant admin role access to the client
|
// Grant admin role access to the client
|
||||||
await transaction.insert(roleClients).values({
|
await transaction.insert(roleClients).values({
|
||||||
roleId: adminRole.roleId,
|
roleId: adminRole.roleId,
|
||||||
@@ -225,15 +248,8 @@ async function cleanupOrphanedClients(
|
|||||||
: and(eq(clients.userId, userId))
|
: and(eq(clients.userId, userId))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Delete client-site associations first, then delete the clients
|
|
||||||
for (const client of clientsToDelete) {
|
|
||||||
await trx
|
|
||||||
.delete(clientSitesAssociationsCache)
|
|
||||||
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clientsToDelete.length > 0) {
|
if (clientsToDelete.length > 0) {
|
||||||
await trx
|
const deletedClients = await trx
|
||||||
.delete(clients)
|
.delete(clients)
|
||||||
.where(
|
.where(
|
||||||
userOrgIds.length > 0
|
userOrgIds.length > 0
|
||||||
@@ -242,7 +258,20 @@ async function cleanupOrphanedClients(
|
|||||||
notInArray(clients.orgId, userOrgIds)
|
notInArray(clients.orgId, userOrgIds)
|
||||||
)
|
)
|
||||||
: and(eq(clients.userId, userId))
|
: and(eq(clients.userId, userId))
|
||||||
);
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// Rebuild associations for each deleted client to clean up related data
|
||||||
|
for (const deletedClient of deletedClients) {
|
||||||
|
await rebuildClientAssociationsFromClient(deletedClient, trx);
|
||||||
|
|
||||||
|
if (deletedClient.olmId) {
|
||||||
|
await sendTerminateClient(
|
||||||
|
deletedClient.clientId,
|
||||||
|
deletedClient.olmId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (userOrgIds.length === 0) {
|
if (userOrgIds.length === 0) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -255,4 +284,3 @@ async function cleanupOrphanedClients(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { defaultRoleAllowedActions } from "@server/routers/role";
|
|||||||
import { FeatureId, limitsService, sandboxLimitSet } from "@server/lib/billing";
|
import { FeatureId, limitsService, sandboxLimitSet } from "@server/lib/billing";
|
||||||
import { createCustomer } from "#dynamic/lib/billing";
|
import { createCustomer } from "#dynamic/lib/billing";
|
||||||
import { usageService } from "@server/lib/billing/usageService";
|
import { usageService } from "@server/lib/billing/usageService";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
|
||||||
export async function createUserAccountOrg(
|
export async function createUserAccountOrg(
|
||||||
userId: string,
|
userId: string,
|
||||||
@@ -76,6 +77,8 @@ export async function createUserAccountOrg(
|
|||||||
.from(domains)
|
.from(domains)
|
||||||
.where(eq(domains.configManaged, true));
|
.where(eq(domains.configManaged, true));
|
||||||
|
|
||||||
|
const utilitySubnet = config.getRawConfig().orgs.utility_subnet_group;
|
||||||
|
|
||||||
const newOrg = await trx
|
const newOrg = await trx
|
||||||
.insert(orgs)
|
.insert(orgs)
|
||||||
.values({
|
.values({
|
||||||
@@ -83,6 +86,7 @@ export async function createUserAccountOrg(
|
|||||||
name,
|
name,
|
||||||
// subnet
|
// subnet
|
||||||
subnet: "100.90.128.0/24", // TODO: this should not be hardcoded - or can it be the same in all orgs?
|
subnet: "100.90.128.0/24", // TODO: this should not be hardcoded - or can it be the same in all orgs?
|
||||||
|
utilitySubnet: utilitySubnet,
|
||||||
createdAt: new Date().toISOString()
|
createdAt: new Date().toISOString()
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { clientSitesAssociationsCache, db, SiteResource, Transaction } from "@server/db";
|
import {
|
||||||
|
clientSitesAssociationsCache,
|
||||||
|
db,
|
||||||
|
SiteResource,
|
||||||
|
siteResources,
|
||||||
|
Transaction
|
||||||
|
} from "@server/db";
|
||||||
import { clients, orgs, sites } from "@server/db";
|
import { clients, orgs, sites } from "@server/db";
|
||||||
import { and, eq, isNotNull } from "drizzle-orm";
|
import { and, eq, isNotNull } from "drizzle-orm";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
@@ -281,6 +287,56 @@ export async function getNextAvailableClientSubnet(
|
|||||||
return subnet;
|
return subnet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getNextAvailableAliasAddress(
|
||||||
|
orgId: string
|
||||||
|
): Promise<string> {
|
||||||
|
const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId));
|
||||||
|
|
||||||
|
if (!org) {
|
||||||
|
throw new Error(`Organization with ID ${orgId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!org.subnet) {
|
||||||
|
throw new Error(`Organization with ID ${orgId} has no subnet defined`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!org.utilitySubnet) {
|
||||||
|
throw new Error(
|
||||||
|
`Organization with ID ${orgId} has no utility subnet defined`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingAddresses = await db
|
||||||
|
.select({
|
||||||
|
aliasAddress: siteResources.aliasAddress
|
||||||
|
})
|
||||||
|
.from(siteResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
isNotNull(siteResources.aliasAddress),
|
||||||
|
eq(siteResources.orgId, orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const addresses = [
|
||||||
|
...existingAddresses.map(
|
||||||
|
(site) => `${site.aliasAddress?.split("/")[0]}/32`
|
||||||
|
),
|
||||||
|
// reserve a /29 for the dns server and other stuff
|
||||||
|
`${org.utilitySubnet.split("/")[0]}/29`
|
||||||
|
].filter((address) => address !== null) as string[];
|
||||||
|
|
||||||
|
let subnet = findNextAvailableCidr(addresses, 32, org.utilitySubnet);
|
||||||
|
if (!subnet) {
|
||||||
|
throw new Error("No available subnets remaining in space");
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the cidr
|
||||||
|
subnet = subnet.split("/")[0];
|
||||||
|
|
||||||
|
return subnet;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getNextAvailableOrgSubnet(): Promise<string> {
|
export async function getNextAvailableOrgSubnet(): Promise<string> {
|
||||||
const existingAddresses = await db
|
const existingAddresses = await db
|
||||||
.select({
|
.select({
|
||||||
@@ -327,9 +383,22 @@ export function generateRemoteSubnets(allSiteResources: SiteResource[]): string[
|
|||||||
return Array.from(new Set(remoteSubnets));
|
return Array.from(new Set(remoteSubnets));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Alias = { alias: string | null; aliasAddress: string | null };
|
||||||
|
|
||||||
|
export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
|
||||||
|
let aliasConfigs = allSiteResources
|
||||||
|
.filter((sr) => sr.alias && sr.aliasAddress && sr.mode == "host")
|
||||||
|
.map((sr) => ({
|
||||||
|
alias: sr.alias,
|
||||||
|
aliasAddress: sr.aliasAddress
|
||||||
|
}));
|
||||||
|
return aliasConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
export type SubnetProxyTarget = {
|
export type SubnetProxyTarget = {
|
||||||
sourcePrefix: string;
|
sourcePrefix: string; // must be a cidr
|
||||||
destPrefix: string;
|
destPrefix: string; // must be a cidr
|
||||||
|
rewriteTo?: string; // must be a cidr
|
||||||
portRange?: {
|
portRange?: {
|
||||||
min: number;
|
min: number;
|
||||||
max: number;
|
max: number;
|
||||||
@@ -372,6 +441,15 @@ export function generateSubnetProxyTargets(
|
|||||||
destPrefix: `${siteResource.destination}/32`
|
destPrefix: `${siteResource.destination}/32`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (siteResource.alias && siteResource.aliasAddress) {
|
||||||
|
// also push a match for the alias address
|
||||||
|
targets.push({
|
||||||
|
sourcePrefix: clientPrefix,
|
||||||
|
destPrefix: `${siteResource.aliasAddress}/32`,
|
||||||
|
rewriteTo: `${siteResource.destination}/32`
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (siteResource.mode == "cidr") {
|
} else if (siteResource.mode == "cidr") {
|
||||||
targets.push({
|
targets.push({
|
||||||
sourcePrefix: clientPrefix,
|
sourcePrefix: clientPrefix,
|
||||||
@@ -386,4 +464,4 @@ export function generateSubnetProxyTargets(
|
|||||||
);
|
);
|
||||||
|
|
||||||
return targets;
|
return targets;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,6 +229,11 @@ export const configSchema = z
|
|||||||
.default(51820)
|
.default(51820)
|
||||||
.transform(stoi)
|
.transform(stoi)
|
||||||
.pipe(portSchema),
|
.pipe(portSchema),
|
||||||
|
clients_start_port: portSchema
|
||||||
|
.optional()
|
||||||
|
.default(21820)
|
||||||
|
.transform(stoi)
|
||||||
|
.pipe(portSchema),
|
||||||
base_endpoint: z
|
base_endpoint: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -249,12 +254,14 @@ export const configSchema = z
|
|||||||
orgs: z
|
orgs: z
|
||||||
.object({
|
.object({
|
||||||
block_size: z.number().positive().gt(0).optional().default(24),
|
block_size: z.number().positive().gt(0).optional().default(24),
|
||||||
subnet_group: z.string().optional().default("100.90.128.0/24")
|
subnet_group: z.string().optional().default("100.90.128.0/24"),
|
||||||
|
utility_subnet_group: z.string().optional().default("100.96.128.0/24") //just hardcode this for now as well
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({
|
.default({
|
||||||
block_size: 24,
|
block_size: 24,
|
||||||
subnet_group: "100.90.128.0/24"
|
subnet_group: "100.90.128.0/24",
|
||||||
|
utility_subnet_group: "100.96.128.0/24"
|
||||||
}),
|
}),
|
||||||
rate_limits: z
|
rate_limits: z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -25,20 +25,22 @@ import {
|
|||||||
deletePeer as newtDeletePeer
|
deletePeer as newtDeletePeer
|
||||||
} from "@server/routers/newt/peers";
|
} from "@server/routers/newt/peers";
|
||||||
import {
|
import {
|
||||||
|
initPeerAddHandshake as holepunchSiteAdd,
|
||||||
addPeer as olmAddPeer,
|
addPeer as olmAddPeer,
|
||||||
deletePeer as olmDeletePeer
|
deletePeer as olmDeletePeer
|
||||||
} from "@server/routers/olm/peers";
|
} from "@server/routers/olm/peers";
|
||||||
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import {
|
import {
|
||||||
|
generateAliasConfig,
|
||||||
generateRemoteSubnets,
|
generateRemoteSubnets,
|
||||||
generateSubnetProxyTargets,
|
generateSubnetProxyTargets,
|
||||||
SubnetProxyTarget
|
SubnetProxyTarget
|
||||||
} from "@server/lib/ip";
|
} from "@server/lib/ip";
|
||||||
import {
|
import {
|
||||||
addRemoteSubnets,
|
addPeerData,
|
||||||
addTargets as addSubnetProxyTargets,
|
addTargets as addSubnetProxyTargets,
|
||||||
removeRemoteSubnets,
|
removePeerData,
|
||||||
removeTargets as removeSubnetProxyTargets
|
removeTargets as removeSubnetProxyTargets
|
||||||
} from "@server/routers/client/targets";
|
} from "@server/routers/client/targets";
|
||||||
|
|
||||||
@@ -128,7 +130,7 @@ export async function getClientSiteResourceAccess(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rebuildClientAssociations(
|
export async function rebuildClientAssociationsFromSiteResource(
|
||||||
siteResource: SiteResource,
|
siteResource: SiteResource,
|
||||||
trx: Transaction | typeof db = db
|
trx: Transaction | typeof db = db
|
||||||
): Promise<{
|
): Promise<{
|
||||||
@@ -463,65 +465,17 @@ async function handleMessagesForSiteClients(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isAdd) {
|
if (isAdd) {
|
||||||
// TODO: WE NEED TO HANDLE THIS BETTER. WE ARE DEFAULTING TO RELAYING FOR NEW SITES
|
await holepunchSiteAdd(
|
||||||
// BUT REALLY WE NEED TO TRACK THE USERS PREFERENCE THAT THEY CHOSE IN THE CLIENTS
|
// this will kick off the add peer process for the client
|
||||||
// AND TRIGGER A HOLEPUNCH OR SOMETHING TO GET THE ENDPOINT AND HP TO THE NEW SITES
|
client.clientId,
|
||||||
const isRelayed = true;
|
{
|
||||||
|
|
||||||
newtJobs.push(
|
|
||||||
newtAddPeer(
|
|
||||||
siteId,
|
siteId,
|
||||||
{
|
exitNode: {
|
||||||
publicKey: client.pubKey,
|
publicKey: exitNode.publicKey,
|
||||||
allowedIps: [`${client.subnet.split("/")[0]}/32`], // we want to only allow from that client
|
endpoint: exitNode.endpoint
|
||||||
// endpoint: isRelayed ? "" : clientSite.endpoint
|
}
|
||||||
endpoint: isRelayed ? "" : "" // we are not HPing yet so no endpoint
|
},
|
||||||
},
|
olm.olmId
|
||||||
newt.newtId
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: should we have this here?
|
|
||||||
const allSiteResources = await db // only get the site resources that this client has access to
|
|
||||||
.select()
|
|
||||||
.from(siteResources)
|
|
||||||
.innerJoin(
|
|
||||||
clientSiteResourcesAssociationsCache,
|
|
||||||
eq(
|
|
||||||
siteResources.siteResourceId,
|
|
||||||
clientSiteResourcesAssociationsCache.siteResourceId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(siteResources.siteId, site.siteId),
|
|
||||||
eq(
|
|
||||||
clientSiteResourcesAssociationsCache.clientId,
|
|
||||||
client.clientId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
olmJobs.push(
|
|
||||||
olmAddPeer(
|
|
||||||
client.clientId,
|
|
||||||
{
|
|
||||||
siteId: site.siteId,
|
|
||||||
endpoint:
|
|
||||||
isRelayed || !site.endpoint
|
|
||||||
? `${exitNode.endpoint}:21820`
|
|
||||||
: site.endpoint,
|
|
||||||
publicKey: site.publicKey,
|
|
||||||
serverIP: site.address,
|
|
||||||
serverPort: site.listenPort,
|
|
||||||
remoteSubnets: generateRemoteSubnets(
|
|
||||||
allSiteResources.map(
|
|
||||||
({ siteResources }) => siteResources
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
olm.olmId
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -703,10 +657,11 @@ async function handleSubnetProxyTargetUpdates(
|
|||||||
|
|
||||||
for (const client of addedClients) {
|
for (const client of addedClients) {
|
||||||
olmJobs.push(
|
olmJobs.push(
|
||||||
addRemoteSubnets(
|
addPeerData(
|
||||||
client.clientId,
|
client.clientId,
|
||||||
siteResource.siteId,
|
siteResource.siteId,
|
||||||
generateRemoteSubnets([siteResource])
|
generateRemoteSubnets([siteResource]),
|
||||||
|
generateAliasConfig([siteResource])
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -738,10 +693,11 @@ async function handleSubnetProxyTargetUpdates(
|
|||||||
|
|
||||||
for (const client of removedClients) {
|
for (const client of removedClients) {
|
||||||
olmJobs.push(
|
olmJobs.push(
|
||||||
removeRemoteSubnets(
|
removePeerData(
|
||||||
client.clientId,
|
client.clientId,
|
||||||
siteResource.siteId,
|
siteResource.siteId,
|
||||||
generateRemoteSubnets([siteResource])
|
generateRemoteSubnets([siteResource]),
|
||||||
|
generateAliasConfig([siteResource])
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -750,3 +706,511 @@ async function handleSubnetProxyTargetUpdates(
|
|||||||
|
|
||||||
await Promise.all(proxyJobs);
|
await Promise.all(proxyJobs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function rebuildClientAssociationsFromClient(
|
||||||
|
client: Client,
|
||||||
|
trx: Transaction | typeof db = db
|
||||||
|
): Promise<void> {
|
||||||
|
let newSiteResourceIds: number[] = [];
|
||||||
|
|
||||||
|
// 1. Direct client associations
|
||||||
|
const directSiteResources = await trx
|
||||||
|
.select({ siteResourceId: clientSiteResources.siteResourceId })
|
||||||
|
.from(clientSiteResources)
|
||||||
|
.where(eq(clientSiteResources.clientId, client.clientId));
|
||||||
|
|
||||||
|
newSiteResourceIds.push(
|
||||||
|
...directSiteResources.map((r) => r.siteResourceId)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. User-based and role-based access (if client has a userId)
|
||||||
|
if (client.userId) {
|
||||||
|
// Direct user associations
|
||||||
|
const userSiteResourceIds = await trx
|
||||||
|
.select({ siteResourceId: userSiteResources.siteResourceId })
|
||||||
|
.from(userSiteResources)
|
||||||
|
.innerJoin(
|
||||||
|
siteResources,
|
||||||
|
eq(
|
||||||
|
siteResources.siteResourceId,
|
||||||
|
userSiteResources.siteResourceId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userSiteResources.userId, client.userId),
|
||||||
|
eq(siteResources.orgId, client.orgId)
|
||||||
|
)
|
||||||
|
); // this needs to be locked onto this org or else cross-org access could happen
|
||||||
|
|
||||||
|
newSiteResourceIds.push(
|
||||||
|
...userSiteResourceIds.map((r) => r.siteResourceId)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Role-based access
|
||||||
|
const roleIds = await trx
|
||||||
|
.select({ roleId: userOrgs.roleId })
|
||||||
|
.from(userOrgs)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userOrgs.userId, client.userId),
|
||||||
|
eq(userOrgs.orgId, client.orgId)
|
||||||
|
)
|
||||||
|
) // this needs to be locked onto this org or else cross-org access could happen
|
||||||
|
.then((rows) => rows.map((row) => row.roleId));
|
||||||
|
|
||||||
|
if (roleIds.length > 0) {
|
||||||
|
const roleSiteResourceIds = await trx
|
||||||
|
.select({ siteResourceId: roleSiteResources.siteResourceId })
|
||||||
|
.from(roleSiteResources)
|
||||||
|
.where(inArray(roleSiteResources.roleId, roleIds));
|
||||||
|
|
||||||
|
newSiteResourceIds.push(
|
||||||
|
...roleSiteResourceIds.map((r) => r.siteResourceId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove duplicates
|
||||||
|
newSiteResourceIds = Array.from(new Set(newSiteResourceIds));
|
||||||
|
|
||||||
|
// Get full siteResource details
|
||||||
|
const newSiteResources =
|
||||||
|
newSiteResourceIds.length > 0
|
||||||
|
? await trx
|
||||||
|
.select()
|
||||||
|
.from(siteResources)
|
||||||
|
.where(
|
||||||
|
inArray(siteResources.siteResourceId, newSiteResourceIds)
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Group by siteId for site-level associations
|
||||||
|
const newSiteIds = Array.from(
|
||||||
|
new Set(newSiteResources.map((sr) => sr.siteId))
|
||||||
|
);
|
||||||
|
|
||||||
|
/////////// Process client-siteResource associations ///////////
|
||||||
|
|
||||||
|
// Get existing resource associations
|
||||||
|
const existingResourceAssociations = await trx
|
||||||
|
.select({
|
||||||
|
siteResourceId: clientSiteResourcesAssociationsCache.siteResourceId
|
||||||
|
})
|
||||||
|
.from(clientSiteResourcesAssociationsCache)
|
||||||
|
.where(
|
||||||
|
eq(clientSiteResourcesAssociationsCache.clientId, client.clientId)
|
||||||
|
);
|
||||||
|
|
||||||
|
const existingSiteResourceIds = existingResourceAssociations.map(
|
||||||
|
(r) => r.siteResourceId
|
||||||
|
);
|
||||||
|
|
||||||
|
const resourcesToAdd = newSiteResourceIds.filter(
|
||||||
|
(id) => !existingSiteResourceIds.includes(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const resourcesToRemove = existingSiteResourceIds.filter(
|
||||||
|
(id) => !newSiteResourceIds.includes(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert new associations
|
||||||
|
if (resourcesToAdd.length > 0) {
|
||||||
|
await trx.insert(clientSiteResourcesAssociationsCache).values(
|
||||||
|
resourcesToAdd.map((siteResourceId) => ({
|
||||||
|
clientId: client.clientId,
|
||||||
|
siteResourceId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old associations
|
||||||
|
if (resourcesToRemove.length > 0) {
|
||||||
|
await trx
|
||||||
|
.delete(clientSiteResourcesAssociationsCache)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(
|
||||||
|
clientSiteResourcesAssociationsCache.clientId,
|
||||||
|
client.clientId
|
||||||
|
),
|
||||||
|
inArray(
|
||||||
|
clientSiteResourcesAssociationsCache.siteResourceId,
|
||||||
|
resourcesToRemove
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////// Process client-site associations ///////////
|
||||||
|
|
||||||
|
// Get existing site associations
|
||||||
|
const existingSiteAssociations = await trx
|
||||||
|
.select({ siteId: clientSitesAssociationsCache.siteId })
|
||||||
|
.from(clientSitesAssociationsCache)
|
||||||
|
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
|
||||||
|
|
||||||
|
const existingSiteIds = existingSiteAssociations.map((s) => s.siteId);
|
||||||
|
|
||||||
|
const sitesToAdd = newSiteIds.filter((id) => !existingSiteIds.includes(id));
|
||||||
|
const sitesToRemove = existingSiteIds.filter(
|
||||||
|
(id) => !newSiteIds.includes(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert new site associations
|
||||||
|
if (sitesToAdd.length > 0) {
|
||||||
|
await trx.insert(clientSitesAssociationsCache).values(
|
||||||
|
sitesToAdd.map((siteId) => ({
|
||||||
|
clientId: client.clientId,
|
||||||
|
siteId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old site associations
|
||||||
|
if (sitesToRemove.length > 0) {
|
||||||
|
await trx
|
||||||
|
.delete(clientSitesAssociationsCache)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(clientSitesAssociationsCache.clientId, client.clientId),
|
||||||
|
inArray(clientSitesAssociationsCache.siteId, sitesToRemove)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////// Send messages ///////////
|
||||||
|
|
||||||
|
// Get the olm for this client
|
||||||
|
const [olm] = await trx
|
||||||
|
.select({ olmId: olms.olmId })
|
||||||
|
.from(olms)
|
||||||
|
.where(eq(olms.clientId, client.clientId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!olm) {
|
||||||
|
logger.warn(
|
||||||
|
`Olm not found for client ${client.clientId}, skipping peer updates`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle messages for sites being added
|
||||||
|
await handleMessagesForClientSites(
|
||||||
|
client,
|
||||||
|
olm.olmId,
|
||||||
|
sitesToAdd,
|
||||||
|
sitesToRemove,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle subnet proxy target updates for resources
|
||||||
|
await handleMessagesForClientResources(
|
||||||
|
client,
|
||||||
|
newSiteResources,
|
||||||
|
resourcesToAdd,
|
||||||
|
resourcesToRemove,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMessagesForClientSites(
|
||||||
|
client: {
|
||||||
|
clientId: number;
|
||||||
|
pubKey: string | null;
|
||||||
|
subnet: string | null;
|
||||||
|
userId: string | null;
|
||||||
|
orgId: string;
|
||||||
|
},
|
||||||
|
olmId: string,
|
||||||
|
sitesToAdd: number[],
|
||||||
|
sitesToRemove: number[],
|
||||||
|
trx: Transaction | typeof db = db
|
||||||
|
): Promise<void> {
|
||||||
|
if (!client.subnet || !client.pubKey) {
|
||||||
|
logger.warn(
|
||||||
|
`Client ${client.clientId} missing subnet or pubKey, skipping peer updates`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allSiteIds = [...sitesToAdd, ...sitesToRemove];
|
||||||
|
if (allSiteIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get site details for all affected sites
|
||||||
|
const sitesData = await trx
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.leftJoin(exitNodes, eq(sites.exitNodeId, exitNodes.exitNodeId))
|
||||||
|
.leftJoin(newts, eq(sites.siteId, newts.siteId))
|
||||||
|
.where(inArray(sites.siteId, allSiteIds));
|
||||||
|
|
||||||
|
let newtJobs: Promise<any>[] = [];
|
||||||
|
let olmJobs: Promise<any>[] = [];
|
||||||
|
let exitNodeJobs: Promise<any>[] = [];
|
||||||
|
|
||||||
|
for (const siteData of sitesData) {
|
||||||
|
const site = siteData.sites;
|
||||||
|
const exitNode = siteData.exitNodes;
|
||||||
|
const newt = siteData.newt;
|
||||||
|
|
||||||
|
if (!site.publicKey) {
|
||||||
|
logger.warn(
|
||||||
|
`Site ${site.siteId} missing publicKey, skipping peer updates`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newt) {
|
||||||
|
logger.warn(
|
||||||
|
`Newt not found for site ${site.siteId}, skipping peer updates`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAdd = sitesToAdd.includes(site.siteId);
|
||||||
|
const isRemove = sitesToRemove.includes(site.siteId);
|
||||||
|
|
||||||
|
if (isRemove) {
|
||||||
|
// Remove peer from newt
|
||||||
|
newtJobs.push(
|
||||||
|
newtDeletePeer(site.siteId, client.pubKey, newt.newtId)
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
// Remove peer from olm
|
||||||
|
olmJobs.push(
|
||||||
|
olmDeletePeer(
|
||||||
|
client.clientId,
|
||||||
|
site.siteId,
|
||||||
|
site.publicKey,
|
||||||
|
olmId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// if the error includes not found then its just because the olm does not exist anymore or yet and its fine if we dont send
|
||||||
|
if (
|
||||||
|
error instanceof Error &&
|
||||||
|
error.message.includes("not found")
|
||||||
|
) {
|
||||||
|
logger.debug(
|
||||||
|
`Olm data not found for client ${client.clientId}, skipping removal`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAdd) {
|
||||||
|
if (!exitNode) {
|
||||||
|
logger.warn(
|
||||||
|
`Exit node not found for site ${site.siteId}, skipping peer add`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await holepunchSiteAdd(
|
||||||
|
// this will kick off the add peer process for the client
|
||||||
|
client.clientId,
|
||||||
|
{
|
||||||
|
siteId: site.siteId,
|
||||||
|
exitNode: {
|
||||||
|
publicKey: exitNode.publicKey,
|
||||||
|
endpoint: exitNode.endpoint
|
||||||
|
}
|
||||||
|
},
|
||||||
|
olmId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update exit node destinations
|
||||||
|
exitNodeJobs.push(
|
||||||
|
updateClientSiteDestinations(
|
||||||
|
{
|
||||||
|
clientId: client.clientId,
|
||||||
|
pubKey: client.pubKey,
|
||||||
|
subnet: client.subnet
|
||||||
|
},
|
||||||
|
trx
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(exitNodeJobs);
|
||||||
|
await Promise.all(newtJobs);
|
||||||
|
await Promise.all(olmJobs);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMessagesForClientResources(
|
||||||
|
client: {
|
||||||
|
clientId: number;
|
||||||
|
pubKey: string | null;
|
||||||
|
subnet: string | null;
|
||||||
|
userId: string | null;
|
||||||
|
orgId: string;
|
||||||
|
},
|
||||||
|
allNewResources: SiteResource[],
|
||||||
|
resourcesToAdd: number[],
|
||||||
|
resourcesToRemove: number[],
|
||||||
|
trx: Transaction | typeof db = db
|
||||||
|
): Promise<void> {
|
||||||
|
// Group resources by site
|
||||||
|
const resourcesBySite = new Map<number, SiteResource[]>();
|
||||||
|
|
||||||
|
for (const resource of allNewResources) {
|
||||||
|
if (!resourcesBySite.has(resource.siteId)) {
|
||||||
|
resourcesBySite.set(resource.siteId, []);
|
||||||
|
}
|
||||||
|
resourcesBySite.get(resource.siteId)!.push(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
let proxyJobs: Promise<any>[] = [];
|
||||||
|
let olmJobs: Promise<any>[] = [];
|
||||||
|
|
||||||
|
// Handle additions
|
||||||
|
if (resourcesToAdd.length > 0) {
|
||||||
|
const addedResources = allNewResources.filter((r) =>
|
||||||
|
resourcesToAdd.includes(r.siteResourceId)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Group by site for proxy updates
|
||||||
|
const addedBySite = new Map<number, SiteResource[]>();
|
||||||
|
for (const resource of addedResources) {
|
||||||
|
if (!addedBySite.has(resource.siteId)) {
|
||||||
|
addedBySite.set(resource.siteId, []);
|
||||||
|
}
|
||||||
|
addedBySite.get(resource.siteId)!.push(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add subnet proxy targets for each site
|
||||||
|
for (const [siteId, resources] of addedBySite.entries()) {
|
||||||
|
const [newt] = await trx
|
||||||
|
.select({ newtId: newts.newtId })
|
||||||
|
.from(newts)
|
||||||
|
.where(eq(newts.siteId, siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!newt) {
|
||||||
|
logger.warn(
|
||||||
|
`Newt not found for site ${siteId}, skipping proxy updates`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const resource of resources) {
|
||||||
|
const targets = generateSubnetProxyTargets(resource, [
|
||||||
|
{
|
||||||
|
clientId: client.clientId,
|
||||||
|
pubKey: client.pubKey,
|
||||||
|
subnet: client.subnet
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (targets.length > 0) {
|
||||||
|
proxyJobs.push(addSubnetProxyTargets(newt.newtId, targets));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Add peer data to olm
|
||||||
|
olmJobs.push(
|
||||||
|
addPeerData(
|
||||||
|
client.clientId,
|
||||||
|
resource.siteId,
|
||||||
|
generateRemoteSubnets([resource]),
|
||||||
|
generateAliasConfig([resource])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// if the error includes not found then its just because the olm does not exist anymore or yet and its fine if we dont send
|
||||||
|
if (
|
||||||
|
error instanceof Error &&
|
||||||
|
error.message.includes("not found")
|
||||||
|
) {
|
||||||
|
logger.debug(
|
||||||
|
`Olm data not found for client ${client.clientId} and site ${resource.siteId}, skipping removal`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle removals
|
||||||
|
if (resourcesToRemove.length > 0) {
|
||||||
|
const removedResources = await trx
|
||||||
|
.select()
|
||||||
|
.from(siteResources)
|
||||||
|
.where(inArray(siteResources.siteResourceId, resourcesToRemove));
|
||||||
|
|
||||||
|
// Group by site for proxy updates
|
||||||
|
const removedBySite = new Map<number, SiteResource[]>();
|
||||||
|
for (const resource of removedResources) {
|
||||||
|
if (!removedBySite.has(resource.siteId)) {
|
||||||
|
removedBySite.set(resource.siteId, []);
|
||||||
|
}
|
||||||
|
removedBySite.get(resource.siteId)!.push(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove subnet proxy targets for each site
|
||||||
|
for (const [siteId, resources] of removedBySite.entries()) {
|
||||||
|
const [newt] = await trx
|
||||||
|
.select({ newtId: newts.newtId })
|
||||||
|
.from(newts)
|
||||||
|
.where(eq(newts.siteId, siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!newt) {
|
||||||
|
logger.warn(
|
||||||
|
`Newt not found for site ${siteId}, skipping proxy updates`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const resource of resources) {
|
||||||
|
const targets = generateSubnetProxyTargets(resource, [
|
||||||
|
{
|
||||||
|
clientId: client.clientId,
|
||||||
|
pubKey: client.pubKey,
|
||||||
|
subnet: client.subnet
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (targets.length > 0) {
|
||||||
|
proxyJobs.push(
|
||||||
|
removeSubnetProxyTargets(newt.newtId, targets)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Remove peer data from olm
|
||||||
|
olmJobs.push(
|
||||||
|
removePeerData(
|
||||||
|
client.clientId,
|
||||||
|
resource.siteId,
|
||||||
|
generateRemoteSubnets([resource]),
|
||||||
|
generateAliasConfig([resource])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// if the error includes not found then its just because the olm does not exist anymore or yet and its fine if we dont send
|
||||||
|
if (
|
||||||
|
error instanceof Error &&
|
||||||
|
error.message.includes("not found")
|
||||||
|
) {
|
||||||
|
logger.debug(
|
||||||
|
`Olm data not found for client ${client.clientId} and site ${resource.siteId}, skipping removal`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([...proxyJobs, ...olmJobs]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1369,7 +1369,7 @@ const updateHolePunchSchema = z.object({
|
|||||||
port: z.number(),
|
port: z.number(),
|
||||||
timestamp: z.number(),
|
timestamp: z.number(),
|
||||||
reachableAt: z.string().optional(),
|
reachableAt: z.string().optional(),
|
||||||
publicKey: z.string().optional()
|
publicKey: z.string() // this is the client public key
|
||||||
});
|
});
|
||||||
hybridRouter.post(
|
hybridRouter.post(
|
||||||
"/gerbil/update-hole-punch",
|
"/gerbil/update-hole-punch",
|
||||||
@@ -1408,7 +1408,7 @@ hybridRouter.post(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { olmId, newtId, ip, port, timestamp, token, reachableAt } =
|
const { olmId, newtId, ip, port, timestamp, token, publicKey, reachableAt } =
|
||||||
parsedParams.data;
|
parsedParams.data;
|
||||||
|
|
||||||
const destinations = await updateAndGenerateEndpointDestinations(
|
const destinations = await updateAndGenerateEndpointDestinations(
|
||||||
@@ -1418,6 +1418,7 @@ hybridRouter.post(
|
|||||||
port,
|
port,
|
||||||
timestamp,
|
timestamp,
|
||||||
token,
|
token,
|
||||||
|
publicKey,
|
||||||
exitNode,
|
exitNode,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ setInterval(async () => {
|
|||||||
await db
|
await db
|
||||||
.delete(webauthnChallenge)
|
.delete(webauthnChallenge)
|
||||||
.where(lt(webauthnChallenge.expiresAt, now));
|
.where(lt(webauthnChallenge.expiresAt, now));
|
||||||
logger.debug("Cleaned up expired security key challenges");
|
// logger.debug("Cleaned up expired security key challenges");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Failed to clean up expired security key challenges", error);
|
logger.error("Failed to clean up expired security key challenges", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export type StartDeviceWebAuthBody = z.infer<typeof bodySchema>;
|
|||||||
|
|
||||||
export type StartDeviceWebAuthResponse = {
|
export type StartDeviceWebAuthResponse = {
|
||||||
code: string;
|
code: string;
|
||||||
expiresAt: number;
|
expiresInSeconds: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to generate device code in format A1AJ-N5JD
|
// Helper function to generate device code in format A1AJ-N5JD
|
||||||
@@ -131,10 +131,13 @@ export async function startDeviceWebAuth(
|
|||||||
createdAt: Date.now()
|
createdAt: Date.now()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// calculate relative expiration in seconds
|
||||||
|
const expiresInSeconds = Math.floor((expiresAt - Date.now()) / 1000);
|
||||||
|
|
||||||
return response<StartDeviceWebAuthResponse>(res, {
|
return response<StartDeviceWebAuthResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
code,
|
code,
|
||||||
expiresAt
|
expiresInSeconds
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { fromError } from "zod-validation-error";
|
|||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { response } from "@server/lib/response";
|
import { response } from "@server/lib/response";
|
||||||
import { db, deviceWebAuthCodes } from "@server/db";
|
import { db, deviceWebAuthCodes, sessions } from "@server/db";
|
||||||
import { eq, and, gt } from "drizzle-orm";
|
import { eq, and, gt } from "drizzle-orm";
|
||||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||||
import { sha256 } from "@oslojs/crypto/sha2";
|
import { sha256 } from "@oslojs/crypto/sha2";
|
||||||
@@ -44,20 +44,36 @@ export async function verifyDeviceWebAuth(
|
|||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const { user, session } = req;
|
const { user, session } = req;
|
||||||
if (!user || !session) {
|
if (!user || !session) {
|
||||||
logger.debug("Unauthorized attempt to verify device web auth code");
|
return next(createHttpError(HttpCode.UNAUTHORIZED, "Unauthorized"));
|
||||||
return next(unauthorized());
|
}
|
||||||
|
|
||||||
|
if (session.deviceAuthUsed) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.UNAUTHORIZED,
|
||||||
|
"Device web auth code already used for this session"
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!session.issuedAt) {
|
if (!session.issuedAt) {
|
||||||
logger.debug("Session missing issuedAt timestamp");
|
return next(
|
||||||
return next(unauthorized());
|
createHttpError(
|
||||||
|
HttpCode.UNAUTHORIZED,
|
||||||
|
"Session issuedAt timestamp missing"
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure sessions is not older than 5 minutes
|
// make sure sessions is not older than 5 minutes
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - session.issuedAt > 3 * 60 * 1000) {
|
if (now - session.issuedAt > 5 * 60 * 1000) {
|
||||||
logger.debug("Session is too old to verify device web auth code");
|
return next(
|
||||||
return next(unauthorized());
|
createHttpError(
|
||||||
|
HttpCode.UNAUTHORIZED,
|
||||||
|
"Session is too old to verify device web auth code"
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedBody = bodySchema.safeParse(req.body);
|
const parsedBody = bodySchema.safeParse(req.body);
|
||||||
@@ -134,6 +150,14 @@ export async function verifyDeviceWebAuth(
|
|||||||
})
|
})
|
||||||
.where(eq(deviceWebAuthCodes.codeId, deviceCode.codeId));
|
.where(eq(deviceWebAuthCodes.codeId, deviceCode.codeId));
|
||||||
|
|
||||||
|
// Also update the session to mark that device auth was used
|
||||||
|
await db
|
||||||
|
.update(sessions)
|
||||||
|
.set({
|
||||||
|
deviceAuthUsed: true
|
||||||
|
})
|
||||||
|
.where(eq(sessions.sessionId, session.sessionId));
|
||||||
|
|
||||||
return response<VerifyDeviceWebAuthResponse>(res, {
|
return response<VerifyDeviceWebAuthResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -24,18 +24,19 @@ import { isIpInCidr } from "@server/lib/ip";
|
|||||||
import { listExitNodes } from "#dynamic/lib/exitNodes";
|
import { listExitNodes } from "#dynamic/lib/exitNodes";
|
||||||
import { generateId } from "@server/auth/sessions/app";
|
import { generateId } from "@server/auth/sessions/app";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
const createClientParamsSchema = z.strictObject({
|
const createClientParamsSchema = z.strictObject({
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
const createClientSchema = z.strictObject({
|
const createClientSchema = z.strictObject({
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
olmId: z.string(),
|
olmId: z.string(),
|
||||||
secret: z.string(),
|
secret: z.string(),
|
||||||
subnet: z.string(),
|
subnet: z.string(),
|
||||||
type: z.enum(["olm"])
|
type: z.enum(["olm"])
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CreateClientBody = z.infer<typeof createClientSchema>;
|
export type CreateClientBody = z.infer<typeof createClientSchema>;
|
||||||
|
|
||||||
@@ -186,6 +187,7 @@ export async function createClient(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let newClient: Client | null = null;
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
// TODO: more intelligent way to pick the exit node
|
// TODO: more intelligent way to pick the exit node
|
||||||
const exitNodesList = await listExitNodes(orgId);
|
const exitNodesList = await listExitNodes(orgId);
|
||||||
@@ -204,7 +206,7 @@ export async function createClient(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [newClient] = await trx
|
[newClient] = await trx
|
||||||
.insert(clients)
|
.insert(clients)
|
||||||
.values({
|
.values({
|
||||||
exitNodeId: randomExitNode.exitNodeId,
|
exitNodeId: randomExitNode.exitNodeId,
|
||||||
@@ -244,13 +246,15 @@ export async function createClient(
|
|||||||
dateCreated: moment().toISOString()
|
dateCreated: moment().toISOString()
|
||||||
});
|
});
|
||||||
|
|
||||||
return response<CreateClientResponse>(res, {
|
await rebuildClientAssociationsFromClient(newClient, trx);
|
||||||
data: newClient,
|
});
|
||||||
success: true,
|
|
||||||
error: false,
|
return response<CreateClientResponse>(res, {
|
||||||
message: "Site created successfully",
|
data: newClient,
|
||||||
status: HttpCode.CREATED
|
success: true,
|
||||||
});
|
error: false,
|
||||||
|
message: "Site created successfully",
|
||||||
|
status: HttpCode.CREATED
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { isValidIP } from "@server/lib/validators";
|
|||||||
import { isIpInCidr } from "@server/lib/ip";
|
import { isIpInCidr } from "@server/lib/ip";
|
||||||
import { listExitNodes } from "#dynamic/lib/exitNodes";
|
import { listExitNodes } from "#dynamic/lib/exitNodes";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
const paramsSchema = z
|
const paramsSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -191,6 +192,7 @@ export async function createUserClient(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let newClient: Client | null = null;
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
// TODO: more intelligent way to pick the exit node
|
// TODO: more intelligent way to pick the exit node
|
||||||
const exitNodesList = await listExitNodes(orgId);
|
const exitNodesList = await listExitNodes(orgId);
|
||||||
@@ -209,7 +211,7 @@ export async function createUserClient(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [newClient] = await trx
|
[newClient] = await trx
|
||||||
.insert(clients)
|
.insert(clients)
|
||||||
.values({
|
.values({
|
||||||
exitNodeId: randomExitNode.exitNodeId,
|
exitNodeId: randomExitNode.exitNodeId,
|
||||||
@@ -232,13 +234,15 @@ export async function createUserClient(
|
|||||||
clientId: newClient.clientId
|
clientId: newClient.clientId
|
||||||
});
|
});
|
||||||
|
|
||||||
return response<CreateClientAndOlmResponse>(res, {
|
await rebuildClientAssociationsFromClient(newClient, trx);
|
||||||
data: newClient,
|
});
|
||||||
success: true,
|
|
||||||
error: false,
|
return response<CreateClientAndOlmResponse>(res, {
|
||||||
message: "Site created successfully",
|
data: newClient,
|
||||||
status: HttpCode.CREATED
|
success: true,
|
||||||
});
|
error: false,
|
||||||
|
message: "Site created successfully",
|
||||||
|
status: HttpCode.CREATED
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db, olms } from "@server/db";
|
||||||
import { clients, clientSitesAssociationsCache } from "@server/db";
|
import { clients, clientSitesAssociationsCache } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
@@ -9,10 +9,12 @@ import createHttpError from "http-errors";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||||
|
import { sendTerminateClient } from "./terminate";
|
||||||
|
|
||||||
const deleteClientSchema = z.strictObject({
|
const deleteClientSchema = z.strictObject({
|
||||||
clientId: z.string().transform(Number).pipe(z.int().positive())
|
clientId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
});
|
});
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "delete",
|
method: "delete",
|
||||||
@@ -68,19 +70,27 @@ export async function deleteClient(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
// Delete the client-site associations first
|
|
||||||
await trx
|
|
||||||
.delete(clientSitesAssociationsCache)
|
|
||||||
.where(eq(clientSitesAssociationsCache.clientId, clientId));
|
|
||||||
|
|
||||||
// Then delete the client itself
|
// Then delete the client itself
|
||||||
await trx.delete(clients).where(eq(clients.clientId, clientId));
|
const [deletedClient] = await trx
|
||||||
|
.delete(clients)
|
||||||
|
.where(eq(clients.clientId, clientId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
// this is a machine client
|
const [olm] = await trx
|
||||||
|
.select()
|
||||||
|
.from(olms)
|
||||||
|
.where(eq(olms.clientId, clientId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
// this is a machine client so we also delete the olm
|
||||||
if (!client.userId && client.olmId) {
|
if (!client.userId && client.olmId) {
|
||||||
await trx
|
await trx.delete(olms).where(eq(olms.olmId, client.olmId));
|
||||||
.delete(clients)
|
}
|
||||||
.where(eq(clients.olmId, client.olmId));
|
|
||||||
|
await rebuildClientAssociationsFromClient(deletedClient, trx);
|
||||||
|
|
||||||
|
if (olm) {
|
||||||
|
await sendTerminateClient(deletedClient.clientId, olm.olmId); // the olmId needs to be provided because it cant look it up after deletion
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { sendToClient } from "#dynamic/routers/ws";
|
import { sendToClient } from "#dynamic/routers/ws";
|
||||||
import { db, olms } from "@server/db";
|
import { db, olms } from "@server/db";
|
||||||
import { SubnetProxyTarget } from "@server/lib/ip";
|
import { Alias, SubnetProxyTarget } from "@server/lib/ip";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
export async function addTargets(newtId: string, targets: SubnetProxyTarget[]) {
|
export async function addTargets(newtId: string, targets: SubnetProxyTarget[]) {
|
||||||
@@ -33,10 +33,11 @@ export async function updateTargets(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addRemoteSubnets(
|
export async function addPeerData(
|
||||||
clientId: number,
|
clientId: number,
|
||||||
siteId: number,
|
siteId: number,
|
||||||
remoteSubnets: string[],
|
remoteSubnets: string[],
|
||||||
|
aliases: Alias[],
|
||||||
olmId?: string
|
olmId?: string
|
||||||
) {
|
) {
|
||||||
if (!olmId) {
|
if (!olmId) {
|
||||||
@@ -52,18 +53,20 @@ export async function addRemoteSubnets(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await sendToClient(olmId, {
|
await sendToClient(olmId, {
|
||||||
type: `olm/wg/peer/add-remote-subnets`,
|
type: `olm/wg/peer/data/add`,
|
||||||
data: {
|
data: {
|
||||||
siteId: siteId,
|
siteId: siteId,
|
||||||
remoteSubnets: remoteSubnets
|
remoteSubnets: remoteSubnets,
|
||||||
|
aliases: aliases
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeRemoteSubnets(
|
export async function removePeerData(
|
||||||
clientId: number,
|
clientId: number,
|
||||||
siteId: number,
|
siteId: number,
|
||||||
remoteSubnets: string[],
|
remoteSubnets: string[],
|
||||||
|
aliases: Alias[],
|
||||||
olmId?: string
|
olmId?: string
|
||||||
) {
|
) {
|
||||||
if (!olmId) {
|
if (!olmId) {
|
||||||
@@ -79,21 +82,26 @@ export async function removeRemoteSubnets(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await sendToClient(olmId, {
|
await sendToClient(olmId, {
|
||||||
type: `olm/wg/peer/remove-remote-subnets`,
|
type: `olm/wg/peer/data/remove`,
|
||||||
data: {
|
data: {
|
||||||
siteId: siteId,
|
siteId: siteId,
|
||||||
remoteSubnets: remoteSubnets
|
remoteSubnets: remoteSubnets,
|
||||||
|
aliases: aliases
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateRemoteSubnets(
|
export async function updatePeerData(
|
||||||
clientId: number,
|
clientId: number,
|
||||||
siteId: number,
|
siteId: number,
|
||||||
remoteSubnets: {
|
remoteSubnets: {
|
||||||
oldRemoteSubnets: string[],
|
oldRemoteSubnets: string[],
|
||||||
newRemoteSubnets: string[]
|
newRemoteSubnets: string[]
|
||||||
},
|
},
|
||||||
|
aliases: {
|
||||||
|
oldAliases: Alias[],
|
||||||
|
newAliases: Alias[]
|
||||||
|
},
|
||||||
olmId?: string
|
olmId?: string
|
||||||
) {
|
) {
|
||||||
if (!olmId) {
|
if (!olmId) {
|
||||||
@@ -109,10 +117,11 @@ export async function updateRemoteSubnets(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await sendToClient(olmId, {
|
await sendToClient(olmId, {
|
||||||
type: `olm/wg/peer/update-remote-subnets`,
|
type: `olm/wg/peer/data/update`,
|
||||||
data: {
|
data: {
|
||||||
siteId: siteId,
|
siteId: siteId,
|
||||||
...remoteSubnets
|
...remoteSubnets,
|
||||||
|
...aliases
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
22
server/routers/client/terminate.ts
Normal file
22
server/routers/client/terminate.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { sendToClient } from "#dynamic/routers/ws";
|
||||||
|
import { db, olms } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
export async function sendTerminateClient(clientId: number, olmId?: string | null) {
|
||||||
|
if (!olmId) {
|
||||||
|
const [olm] = await db
|
||||||
|
.select()
|
||||||
|
.from(olms)
|
||||||
|
.where(eq(olms.clientId, clientId))
|
||||||
|
.limit(1);
|
||||||
|
if (!olm) {
|
||||||
|
throw new Error(`Olm with ID ${clientId} not found`);
|
||||||
|
}
|
||||||
|
olmId = olm.olmId;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendToClient(olmId, {
|
||||||
|
type: `olm/terminate`,
|
||||||
|
data: {}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -19,6 +19,8 @@ import { fromError } from "zod-validation-error";
|
|||||||
import { validateNewtSessionToken } from "@server/auth/sessions/newt";
|
import { validateNewtSessionToken } from "@server/auth/sessions/newt";
|
||||||
import { validateOlmSessionToken } from "@server/auth/sessions/olm";
|
import { validateOlmSessionToken } from "@server/auth/sessions/olm";
|
||||||
import { checkExitNodeOrg } from "#dynamic/lib/exitNodes";
|
import { checkExitNodeOrg } from "#dynamic/lib/exitNodes";
|
||||||
|
import { updatePeer as updateOlmPeer } from "../olm/peers";
|
||||||
|
import { updatePeer as updateNewtPeer } from "../newt/peers";
|
||||||
|
|
||||||
// Define Zod schema for request validation
|
// Define Zod schema for request validation
|
||||||
const updateHolePunchSchema = z.object({
|
const updateHolePunchSchema = z.object({
|
||||||
@@ -28,8 +30,9 @@ const updateHolePunchSchema = z.object({
|
|||||||
ip: z.string(),
|
ip: z.string(),
|
||||||
port: z.number(),
|
port: z.number(),
|
||||||
timestamp: z.number(),
|
timestamp: z.number(),
|
||||||
|
publicKey: z.string(),
|
||||||
reachableAt: z.string().optional(),
|
reachableAt: z.string().optional(),
|
||||||
publicKey: z.string().optional()
|
exitNodePublicKey: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
// New response type with multi-peer destination support
|
// New response type with multi-peer destination support
|
||||||
@@ -63,23 +66,26 @@ export async function updateHolePunch(
|
|||||||
timestamp,
|
timestamp,
|
||||||
token,
|
token,
|
||||||
reachableAt,
|
reachableAt,
|
||||||
publicKey
|
publicKey, // this is the client's current public key for this session
|
||||||
|
exitNodePublicKey
|
||||||
} = parsedParams.data;
|
} = parsedParams.data;
|
||||||
|
|
||||||
let exitNode: ExitNode | undefined;
|
let exitNode: ExitNode | undefined;
|
||||||
if (publicKey) {
|
if (exitNodePublicKey) {
|
||||||
// Get the exit node by public key
|
// Get the exit node by public key
|
||||||
[exitNode] = await db
|
[exitNode] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(exitNodes)
|
.from(exitNodes)
|
||||||
.where(eq(exitNodes.publicKey, publicKey));
|
.where(eq(exitNodes.publicKey, exitNodePublicKey));
|
||||||
} else {
|
} else {
|
||||||
// FOR BACKWARDS COMPATIBILITY IF GERBIL IS STILL =<1.1.0
|
// FOR BACKWARDS COMPATIBILITY IF GERBIL IS STILL =<1.1.0
|
||||||
[exitNode] = await db.select().from(exitNodes).limit(1);
|
[exitNode] = await db.select().from(exitNodes).limit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!exitNode) {
|
if (!exitNode) {
|
||||||
logger.warn(`Exit node not found for publicKey: ${publicKey}`);
|
logger.warn(
|
||||||
|
`Exit node not found for publicKey: ${exitNodePublicKey}`
|
||||||
|
);
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.NOT_FOUND, "Exit node not found")
|
createHttpError(HttpCode.NOT_FOUND, "Exit node not found")
|
||||||
);
|
);
|
||||||
@@ -92,12 +98,13 @@ export async function updateHolePunch(
|
|||||||
port,
|
port,
|
||||||
timestamp,
|
timestamp,
|
||||||
token,
|
token,
|
||||||
|
publicKey,
|
||||||
exitNode
|
exitNode
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.debug(
|
// logger.debug(
|
||||||
`Returning ${destinations.length} peer destinations for olmId: ${olmId} or newtId: ${newtId}: ${JSON.stringify(destinations, null, 2)}`
|
// `Returning ${destinations.length} peer destinations for olmId: ${olmId} or newtId: ${newtId}: ${JSON.stringify(destinations, null, 2)}`
|
||||||
);
|
// );
|
||||||
|
|
||||||
// Return the new multi-peer structure
|
// Return the new multi-peer structure
|
||||||
return res.status(HttpCode.OK).send({
|
return res.status(HttpCode.OK).send({
|
||||||
@@ -121,6 +128,7 @@ export async function updateAndGenerateEndpointDestinations(
|
|||||||
port: number,
|
port: number,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
token: string,
|
token: string,
|
||||||
|
publicKey: string,
|
||||||
exitNode: ExitNode,
|
exitNode: ExitNode,
|
||||||
checkOrg = false
|
checkOrg = false
|
||||||
) {
|
) {
|
||||||
@@ -128,9 +136,9 @@ export async function updateAndGenerateEndpointDestinations(
|
|||||||
const destinations: PeerDestination[] = [];
|
const destinations: PeerDestination[] = [];
|
||||||
|
|
||||||
if (olmId) {
|
if (olmId) {
|
||||||
logger.debug(
|
// logger.debug(
|
||||||
`Got hole punch with ip: ${ip}, port: ${port} for olmId: ${olmId}`
|
// `Got hole punch with ip: ${ip}, port: ${port} for olmId: ${olmId}`
|
||||||
);
|
// );
|
||||||
|
|
||||||
const { session, olm: olmSession } =
|
const { session, olm: olmSession } =
|
||||||
await validateOlmSessionToken(token);
|
await validateOlmSessionToken(token);
|
||||||
@@ -150,7 +158,7 @@ export async function updateAndGenerateEndpointDestinations(
|
|||||||
throw new Error("Olm not found");
|
throw new Error("Olm not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const [client] = await db
|
const [updatedClient] = await db
|
||||||
.update(clients)
|
.update(clients)
|
||||||
.set({
|
.set({
|
||||||
lastHolePunch: timestamp
|
lastHolePunch: timestamp
|
||||||
@@ -158,10 +166,16 @@ export async function updateAndGenerateEndpointDestinations(
|
|||||||
.where(eq(clients.clientId, olm.clientId))
|
.where(eq(clients.clientId, olm.clientId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
if (await checkExitNodeOrg(exitNode.exitNodeId, client.orgId) && checkOrg) {
|
if (
|
||||||
|
(await checkExitNodeOrg(
|
||||||
|
exitNode.exitNodeId,
|
||||||
|
updatedClient.orgId
|
||||||
|
)) &&
|
||||||
|
checkOrg
|
||||||
|
) {
|
||||||
// not allowed
|
// not allowed
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Exit node ${exitNode.exitNodeId} is not allowed for org ${client.orgId}`
|
`Exit node ${exitNode.exitNodeId} is not allowed for org ${updatedClient.orgId}`
|
||||||
);
|
);
|
||||||
throw new Error("Exit node not allowed");
|
throw new Error("Exit node not allowed");
|
||||||
}
|
}
|
||||||
@@ -171,10 +185,15 @@ export async function updateAndGenerateEndpointDestinations(
|
|||||||
.select({
|
.select({
|
||||||
siteId: sites.siteId,
|
siteId: sites.siteId,
|
||||||
subnet: sites.subnet,
|
subnet: sites.subnet,
|
||||||
listenPort: sites.listenPort
|
listenPort: sites.listenPort,
|
||||||
|
publicKey: sites.publicKey,
|
||||||
|
endpoint: clientSitesAssociationsCache.endpoint
|
||||||
})
|
})
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.innerJoin(clientSitesAssociationsCache, eq(sites.siteId, clientSitesAssociationsCache.siteId))
|
.innerJoin(
|
||||||
|
clientSitesAssociationsCache,
|
||||||
|
eq(sites.siteId, clientSitesAssociationsCache.siteId)
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(sites.exitNodeId, exitNode.exitNodeId),
|
eq(sites.exitNodeId, exitNode.exitNodeId),
|
||||||
@@ -184,27 +203,52 @@ export async function updateAndGenerateEndpointDestinations(
|
|||||||
|
|
||||||
// Update clientSites for each site on this exit node
|
// Update clientSites for each site on this exit node
|
||||||
for (const site of sitesOnExitNode) {
|
for (const site of sitesOnExitNode) {
|
||||||
logger.debug(
|
// logger.debug(
|
||||||
`Updating site ${site.siteId} on exit node ${exitNode.exitNodeId}`
|
// `Updating site ${site.siteId} on exit node ${exitNode.exitNodeId}`
|
||||||
);
|
// );
|
||||||
|
|
||||||
await db
|
// if the public key or endpoint has changed, update it otherwise continue
|
||||||
|
if (
|
||||||
|
site.endpoint === `${ip}:${port}` &&
|
||||||
|
site.publicKey === publicKey
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updatedClientSitesAssociationsCache] = await db
|
||||||
.update(clientSitesAssociationsCache)
|
.update(clientSitesAssociationsCache)
|
||||||
.set({
|
.set({
|
||||||
endpoint: `${ip}:${port}`
|
endpoint: `${ip}:${port}`,
|
||||||
|
publicKey: publicKey
|
||||||
})
|
})
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(clientSitesAssociationsCache.clientId, olm.clientId),
|
eq(clientSitesAssociationsCache.clientId, olm.clientId),
|
||||||
eq(clientSitesAssociationsCache.siteId, site.siteId)
|
eq(clientSitesAssociationsCache.siteId, site.siteId)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (
|
||||||
|
updatedClientSitesAssociationsCache.endpoint !==
|
||||||
|
site.endpoint && // this is the endpoint from the join table not the site
|
||||||
|
updatedClient.pubKey === publicKey // only trigger if the client's public key matches the current public key which means it has registered so we dont prematurely send the update
|
||||||
|
) {
|
||||||
|
logger.info(
|
||||||
|
`ClientSitesAssociationsCache for client ${olm.clientId} and site ${site.siteId} endpoint changed from ${site.endpoint} to ${updatedClientSitesAssociationsCache.endpoint}`
|
||||||
);
|
);
|
||||||
|
// Handle any additional logic for endpoint change
|
||||||
|
handleClientEndpointChange(
|
||||||
|
olm.clientId,
|
||||||
|
updatedClientSitesAssociationsCache.endpoint!
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(
|
// logger.debug(
|
||||||
`Updated ${sitesOnExitNode.length} sites on exit node ${exitNode.exitNodeId}`
|
// `Updated ${sitesOnExitNode.length} sites on exit node ${exitNode.exitNodeId}`
|
||||||
);
|
// );
|
||||||
if (!client) {
|
if (!updatedClient) {
|
||||||
logger.warn(`Client not found for olm: ${olmId}`);
|
logger.warn(`Client not found for olm: ${olmId}`);
|
||||||
throw new Error("Client not found");
|
throw new Error("Client not found");
|
||||||
}
|
}
|
||||||
@@ -219,9 +263,9 @@ export async function updateAndGenerateEndpointDestinations(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (newtId) {
|
} else if (newtId) {
|
||||||
logger.debug(
|
// logger.debug(
|
||||||
`Got hole punch with ip: ${ip}, port: ${port} for newtId: ${newtId}`
|
// `Got hole punch with ip: ${ip}, port: ${port} for newtId: ${newtId}`
|
||||||
);
|
// );
|
||||||
|
|
||||||
const { session, newt: newtSession } =
|
const { session, newt: newtSession } =
|
||||||
await validateNewtSessionToken(token);
|
await validateNewtSessionToken(token);
|
||||||
@@ -253,7 +297,10 @@ export async function updateAndGenerateEndpointDestinations(
|
|||||||
.where(eq(sites.siteId, newt.siteId))
|
.where(eq(sites.siteId, newt.siteId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (await checkExitNodeOrg(exitNode.exitNodeId, site.orgId) && checkOrg) {
|
if (
|
||||||
|
(await checkExitNodeOrg(exitNode.exitNodeId, site.orgId)) &&
|
||||||
|
checkOrg
|
||||||
|
) {
|
||||||
// not allowed
|
// not allowed
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Exit node ${exitNode.exitNodeId} is not allowed for org ${site.orgId}`
|
`Exit node ${exitNode.exitNodeId} is not allowed for org ${site.orgId}`
|
||||||
@@ -273,6 +320,18 @@ export async function updateAndGenerateEndpointDestinations(
|
|||||||
.where(eq(sites.siteId, newt.siteId))
|
.where(eq(sites.siteId, newt.siteId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
if (
|
||||||
|
updatedSite.endpoint != site.endpoint &&
|
||||||
|
updatedSite.publicKey == publicKey
|
||||||
|
) {
|
||||||
|
// only trigger if the site's public key matches the current public key which means it has registered so we dont prematurely send the update
|
||||||
|
logger.info(
|
||||||
|
`Site ${newt.siteId} endpoint changed from ${site.endpoint} to ${updatedSite.endpoint}`
|
||||||
|
);
|
||||||
|
// Handle any additional logic for endpoint change
|
||||||
|
handleSiteEndpointChange(newt.siteId, updatedSite.endpoint!);
|
||||||
|
}
|
||||||
|
|
||||||
if (!updatedSite || !updatedSite.subnet) {
|
if (!updatedSite || !updatedSite.subnet) {
|
||||||
logger.warn(`Site not found: ${newt.siteId}`);
|
logger.warn(`Site not found: ${newt.siteId}`);
|
||||||
throw new Error("Site not found");
|
throw new Error("Site not found");
|
||||||
@@ -326,3 +385,143 @@ export async function updateAndGenerateEndpointDestinations(
|
|||||||
}
|
}
|
||||||
return destinations;
|
return destinations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleSiteEndpointChange(siteId: number, newEndpoint: string) {
|
||||||
|
// Alert all clients connected to this site that the endpoint has changed (only if NOT relayed)
|
||||||
|
try {
|
||||||
|
// Get site details
|
||||||
|
const [site] = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.where(eq(sites.siteId, siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!site || !site.publicKey) {
|
||||||
|
logger.warn(`Site ${siteId} not found or has no public key`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all non-relayed clients connected to this site
|
||||||
|
const connectedClients = await db
|
||||||
|
.select({
|
||||||
|
clientId: clients.clientId,
|
||||||
|
olmId: olms.olmId,
|
||||||
|
isRelayed: clientSitesAssociationsCache.isRelayed
|
||||||
|
})
|
||||||
|
.from(clientSitesAssociationsCache)
|
||||||
|
.innerJoin(
|
||||||
|
clients,
|
||||||
|
eq(clientSitesAssociationsCache.clientId, clients.clientId)
|
||||||
|
)
|
||||||
|
.innerJoin(olms, eq(olms.clientId, clients.clientId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(clientSitesAssociationsCache.siteId, siteId),
|
||||||
|
eq(clientSitesAssociationsCache.isRelayed, false)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update each non-relayed client with the new site endpoint
|
||||||
|
for (const client of connectedClients) {
|
||||||
|
try {
|
||||||
|
await updateOlmPeer(
|
||||||
|
client.clientId,
|
||||||
|
{
|
||||||
|
siteId: siteId,
|
||||||
|
publicKey: site.publicKey,
|
||||||
|
endpoint: newEndpoint
|
||||||
|
},
|
||||||
|
client.olmId
|
||||||
|
);
|
||||||
|
logger.debug(
|
||||||
|
`Updated client ${client.clientId} with new site ${siteId} endpoint: ${newEndpoint}`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to update client ${client.clientId} with new site endpoint: ${error}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Error handling site endpoint change for site ${siteId}: ${error}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleClientEndpointChange(
|
||||||
|
clientId: number,
|
||||||
|
newEndpoint: string
|
||||||
|
) {
|
||||||
|
// Alert all sites connected to this client that the endpoint has changed (only if NOT relayed)
|
||||||
|
try {
|
||||||
|
// Get client details
|
||||||
|
const [client] = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.clientId, clientId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!client || !client.pubKey) {
|
||||||
|
logger.warn(`Client ${clientId} not found or has no public key`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all non-relayed sites connected to this client
|
||||||
|
const connectedSites = await db
|
||||||
|
.select({
|
||||||
|
siteId: sites.siteId,
|
||||||
|
newtId: newts.newtId,
|
||||||
|
isRelayed: clientSitesAssociationsCache.isRelayed,
|
||||||
|
subnet: clients.subnet
|
||||||
|
})
|
||||||
|
.from(clientSitesAssociationsCache)
|
||||||
|
.innerJoin(
|
||||||
|
sites,
|
||||||
|
eq(clientSitesAssociationsCache.siteId, sites.siteId)
|
||||||
|
)
|
||||||
|
.innerJoin(newts, eq(newts.siteId, sites.siteId))
|
||||||
|
.innerJoin(
|
||||||
|
clients,
|
||||||
|
eq(clientSitesAssociationsCache.clientId, clients.clientId)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(clientSitesAssociationsCache.clientId, clientId),
|
||||||
|
eq(clientSitesAssociationsCache.isRelayed, false)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update each non-relayed site with the new client endpoint
|
||||||
|
for (const siteData of connectedSites) {
|
||||||
|
try {
|
||||||
|
if (!siteData.subnet) {
|
||||||
|
logger.warn(
|
||||||
|
`Client ${clientId} has no subnet, skipping update for site ${siteData.siteId}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateNewtPeer(
|
||||||
|
siteData.siteId,
|
||||||
|
client.pubKey,
|
||||||
|
{
|
||||||
|
endpoint: newEndpoint
|
||||||
|
},
|
||||||
|
siteData.newtId
|
||||||
|
);
|
||||||
|
logger.debug(
|
||||||
|
`Updated site ${siteData.siteId} with new client ${clientId} endpoint: ${newEndpoint}`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to update site ${siteData.siteId} with new client endpoint: ${error}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Error handling client endpoint change for client ${clientId}: ${error}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -79,12 +79,12 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
|
|||||||
// TODO: somehow we should make sure a recent hole punch has happened if this occurs (hole punch could be from the last restart if done quickly)
|
// TODO: somehow we should make sure a recent hole punch has happened if this occurs (hole punch could be from the last restart if done quickly)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (existingSite.lastHolePunch && now - existingSite.lastHolePunch > 6) {
|
if (existingSite.lastHolePunch && now - existingSite.lastHolePunch > 5) {
|
||||||
// logger.warn(
|
logger.warn(
|
||||||
// `Site ${existingSite.siteId} last hole punch is too old, skipping`
|
`Site ${existingSite.siteId} last hole punch is too old, skipping`
|
||||||
// );
|
);
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// update the endpoint and the public key
|
// update the endpoint and the public key
|
||||||
const [site] = await db
|
const [site] = await db
|
||||||
@@ -275,6 +275,7 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
|
|||||||
resource,
|
resource,
|
||||||
resourceClients
|
resourceClients
|
||||||
);
|
);
|
||||||
|
|
||||||
targetsToSend.push(...resourceTargets);
|
targetsToSend.push(...resourceTargets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { db } from "@server/db";
|
import { Client, db } from "@server/db";
|
||||||
import { olms, clients, clientSitesAssociationsCache } from "@server/db";
|
import { olms, clients, clientSitesAssociationsCache } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -9,6 +9,8 @@ import { z } from "zod";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||||
|
import { sendTerminateClient } from "../client/terminate";
|
||||||
|
|
||||||
const paramsSchema = z
|
const paramsSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -54,20 +56,30 @@ export async function deleteUserOlm(
|
|||||||
.from(clients)
|
.from(clients)
|
||||||
.where(eq(clients.olmId, olmId));
|
.where(eq(clients.olmId, olmId));
|
||||||
|
|
||||||
// Delete client-site associations for each associated client
|
let deletedClient: Client | null = null;
|
||||||
for (const client of associatedClients) {
|
|
||||||
await trx
|
|
||||||
.delete(clientSitesAssociationsCache)
|
|
||||||
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete all associated clients
|
// Delete all associated clients
|
||||||
if (associatedClients.length > 0) {
|
if (associatedClients.length > 0) {
|
||||||
await trx.delete(clients).where(eq(clients.olmId, olmId));
|
[deletedClient] = await trx
|
||||||
|
.delete(clients)
|
||||||
|
.where(eq(clients.olmId, olmId))
|
||||||
|
.returning();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, delete the OLM itself
|
// Finally, delete the OLM itself
|
||||||
await trx.delete(olms).where(eq(olms.olmId, olmId));
|
const [olm] = await trx
|
||||||
|
.delete(olms)
|
||||||
|
.where(eq(olms.olmId, olmId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (deletedClient) {
|
||||||
|
await rebuildClientAssociationsFromClient(deletedClient, trx);
|
||||||
|
if (olm) {
|
||||||
|
await sendTerminateClient(
|
||||||
|
deletedClient.clientId,
|
||||||
|
olm.olmId
|
||||||
|
); // the olmId needs to be provided because it cant look it up after deletion
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import { generateSessionToken } from "@server/auth/sessions/app";
|
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||||
import { db } from "@server/db";
|
import {
|
||||||
|
clients,
|
||||||
|
db,
|
||||||
|
ExitNode,
|
||||||
|
exitNodes,
|
||||||
|
sites,
|
||||||
|
clientSitesAssociationsCache
|
||||||
|
} from "@server/db";
|
||||||
import { olms } from "@server/db";
|
import { olms } from "@server/db";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq, inArray } from "drizzle-orm";
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -15,11 +22,13 @@ import {
|
|||||||
import { verifyPassword } from "@server/auth/password";
|
import { verifyPassword } from "@server/auth/password";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
|
import { listExitNodes } from "#dynamic/lib/exitNodes";
|
||||||
|
|
||||||
export const olmGetTokenBodySchema = z.object({
|
export const olmGetTokenBodySchema = z.object({
|
||||||
olmId: z.string(),
|
olmId: z.string(),
|
||||||
secret: z.string(),
|
secret: z.string(),
|
||||||
token: z.string().optional()
|
token: z.string().optional(),
|
||||||
|
orgId: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type OlmGetTokenBody = z.infer<typeof olmGetTokenBodySchema>;
|
export type OlmGetTokenBody = z.infer<typeof olmGetTokenBodySchema>;
|
||||||
@@ -40,7 +49,7 @@ export async function getOlmToken(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { olmId, secret, token } = parsedBody.data;
|
const { olmId, secret, token, orgId } = parsedBody.data;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (token) {
|
if (token) {
|
||||||
@@ -61,11 +70,12 @@ export async function getOlmToken(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingOlmRes = await db
|
const [existingOlm] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(olms)
|
.from(olms)
|
||||||
.where(eq(olms.olmId, olmId));
|
.where(eq(olms.olmId, olmId));
|
||||||
if (!existingOlmRes || !existingOlmRes.length) {
|
|
||||||
|
if (!existingOlm) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
@@ -74,12 +84,11 @@ export async function getOlmToken(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingOlm = existingOlmRes[0];
|
|
||||||
|
|
||||||
const validSecret = await verifyPassword(
|
const validSecret = await verifyPassword(
|
||||||
secret,
|
secret,
|
||||||
existingOlm.secretHash
|
existingOlm.secretHash
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!validSecret) {
|
if (!validSecret) {
|
||||||
if (config.getRawConfig().app.log_failed_attempts) {
|
if (config.getRawConfig().app.log_failed_attempts) {
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -96,11 +105,113 @@ export async function getOlmToken(
|
|||||||
const resToken = generateSessionToken();
|
const resToken = generateSessionToken();
|
||||||
await createOlmSession(resToken, existingOlm.olmId);
|
await createOlmSession(resToken, existingOlm.olmId);
|
||||||
|
|
||||||
|
let orgIdToUse = orgId;
|
||||||
|
let clientIdToUse;
|
||||||
|
if (!orgIdToUse) {
|
||||||
|
if (!existingOlm.clientId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Olm is not associated with a client, orgId is required"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [client] = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.clientId, existingOlm.clientId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Olm's associated client not found, orgId is required"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
orgIdToUse = client.orgId;
|
||||||
|
clientIdToUse = client.clientId;
|
||||||
|
} else {
|
||||||
|
// we did provide the org
|
||||||
|
const [client] = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.orgId, orgIdToUse))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"No client found for provided orgId"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingOlm.clientId !== client.clientId) {
|
||||||
|
// we only need to do this if the client is changing
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`Switching olm client ${existingOlm.olmId} to org ${orgId} for user ${existingOlm.userId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(olms)
|
||||||
|
.set({
|
||||||
|
clientId: client.clientId
|
||||||
|
})
|
||||||
|
.where(eq(olms.olmId, existingOlm.olmId));
|
||||||
|
}
|
||||||
|
|
||||||
|
clientIdToUse = client.clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all exit nodes from sites where the client has peers
|
||||||
|
const clientSites = await db
|
||||||
|
.select()
|
||||||
|
.from(clientSitesAssociationsCache)
|
||||||
|
.innerJoin(
|
||||||
|
sites,
|
||||||
|
eq(sites.siteId, clientSitesAssociationsCache.siteId)
|
||||||
|
)
|
||||||
|
.where(eq(clientSitesAssociationsCache.clientId, clientIdToUse!));
|
||||||
|
|
||||||
|
// Extract unique exit node IDs
|
||||||
|
const exitNodeIds = Array.from(
|
||||||
|
new Set(
|
||||||
|
clientSites
|
||||||
|
.map(({ sites: site }) => site.exitNodeId)
|
||||||
|
.filter((id): id is number => id !== null)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let allExitNodes: ExitNode[] = [];
|
||||||
|
if (exitNodeIds.length > 0) {
|
||||||
|
allExitNodes = await db
|
||||||
|
.select()
|
||||||
|
.from(exitNodes)
|
||||||
|
.where(inArray(exitNodes.exitNodeId, exitNodeIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitNodesHpData = allExitNodes.map((exitNode: ExitNode) => {
|
||||||
|
return {
|
||||||
|
publicKey: exitNode.publicKey,
|
||||||
|
endpoint: exitNode.endpoint
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
logger.debug("Token created successfully");
|
logger.debug("Token created successfully");
|
||||||
|
|
||||||
return response<{ token: string }>(res, {
|
return response<{
|
||||||
|
token: string;
|
||||||
|
exitNodes: { publicKey: string; endpoint: string }[];
|
||||||
|
}>(res, {
|
||||||
data: {
|
data: {
|
||||||
token: resToken
|
token: resToken,
|
||||||
|
exitNodes: exitNodesHpData
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { olms, clients, clientSites } from "@server/db";
|
import { olms } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { clients, Olm } from "@server/db";
|
|||||||
import { eq, lt, isNull, and, or } from "drizzle-orm";
|
import { eq, lt, isNull, and, or } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { validateSessionToken } from "@server/auth/sessions/app";
|
import { validateSessionToken } from "@server/auth/sessions/app";
|
||||||
|
import { checkOrgAccessPolicy } from "@server/lib/checkOrgAccessPolicy";
|
||||||
|
import { sendTerminateClient } from "../client/terminate";
|
||||||
|
|
||||||
// Track if the offline checker interval is running
|
// Track if the offline checker interval is running
|
||||||
let offlineCheckerInterval: NodeJS.Timeout | null = null;
|
let offlineCheckerInterval: NodeJS.Timeout | null = null;
|
||||||
@@ -57,6 +59,9 @@ export const startOlmOfflineChecker = (): void => {
|
|||||||
|
|
||||||
// Send a disconnect message to the client if connected
|
// Send a disconnect message to the client if connected
|
||||||
try {
|
try {
|
||||||
|
await sendTerminateClient(offlineClient.clientId, offlineClient.olmId); // terminate first
|
||||||
|
// wait a moment to ensure the message is sent
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
await disconnectClient(offlineClient.olmId);
|
await disconnectClient(offlineClient.olmId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -110,6 +115,36 @@ export const handleOlmPingMessage: MessageHandler = async (context) => {
|
|||||||
logger.warn("User ID mismatch for olm ping");
|
logger.warn("User ID mismatch for olm ping");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get the client
|
||||||
|
const [client] = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(clients.olmId, olm.olmId),
|
||||||
|
eq(clients.userId, olm.userId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
logger.warn("Client not found for olm ping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
|
orgId: client.orgId,
|
||||||
|
userId: olm.userId,
|
||||||
|
session: userToken // this is the user token passed in the message
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!policyCheck.allowed) {
|
||||||
|
logger.warn(
|
||||||
|
`Olm user ${olm.userId} does not pass access policies for org ${client.orgId}: ${policyCheck.error}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!olm.clientId) {
|
if (!olm.clientId) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
clientSiteResourcesAssociationsCache,
|
clientSiteResourcesAssociationsCache,
|
||||||
db,
|
db,
|
||||||
ExitNode,
|
ExitNode,
|
||||||
|
Org,
|
||||||
orgs,
|
orgs,
|
||||||
roleClients,
|
roleClients,
|
||||||
roles,
|
roles,
|
||||||
@@ -25,77 +26,88 @@ import { and, eq, inArray, isNull } from "drizzle-orm";
|
|||||||
import { addPeer, deletePeer } from "../newt/peers";
|
import { addPeer, deletePeer } from "../newt/peers";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { listExitNodes } from "#dynamic/lib/exitNodes";
|
import { listExitNodes } from "#dynamic/lib/exitNodes";
|
||||||
import { getNextAvailableClientSubnet } from "@server/lib/ip";
|
import {
|
||||||
|
generateAliasConfig,
|
||||||
|
getNextAvailableClientSubnet
|
||||||
|
} from "@server/lib/ip";
|
||||||
import { generateRemoteSubnets } from "@server/lib/ip";
|
import { generateRemoteSubnets } from "@server/lib/ip";
|
||||||
|
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||||
|
import { checkOrgAccessPolicy } from "@server/lib/checkOrgAccessPolicy";
|
||||||
|
import { validateSessionToken } from "@server/auth/sessions/app";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
|
||||||
export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||||
logger.info("Handling register olm message!");
|
logger.info("Handling register olm message!");
|
||||||
const { message, client: c, sendToClient } = context;
|
const { message, client: c, sendToClient } = context;
|
||||||
const olm = c as Olm;
|
const olm = c as Olm;
|
||||||
|
|
||||||
const now = new Date().getTime() / 1000;
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
if (!olm) {
|
if (!olm) {
|
||||||
logger.warn("Olm not found");
|
logger.warn("Olm not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { publicKey, relay, olmVersion, orgId, doNotCreateNewClient } =
|
const { publicKey, relay, olmVersion, orgId, userToken } = message.data;
|
||||||
message.data;
|
|
||||||
let client: Client;
|
|
||||||
|
|
||||||
if (orgId) {
|
if (!olm.clientId) {
|
||||||
try {
|
logger.warn("Olm client ID not found");
|
||||||
client = await getOrCreateOrgClient(
|
return;
|
||||||
orgId,
|
|
||||||
olm.userId,
|
|
||||||
olm.olmId,
|
|
||||||
olm.name || "User Device",
|
|
||||||
// doNotCreateNewClient ? true : false
|
|
||||||
true // for now never create a new client automatically because we create the users clients when they are added to the org
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(
|
|
||||||
`Error switching olm client ${olm.olmId} to org ${orgId}: ${err}`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!client) {
|
|
||||||
logger.warn("Client not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
`Switching olm client ${olm.olmId} to org ${orgId} for user ${olm.userId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(olms)
|
|
||||||
.set({
|
|
||||||
clientId: client.clientId
|
|
||||||
})
|
|
||||||
.where(eq(olms.olmId, olm.olmId));
|
|
||||||
} else {
|
|
||||||
if (!olm.clientId) {
|
|
||||||
logger.warn("Olm has no client ID!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Using last connected org for client ${olm.clientId}`);
|
|
||||||
|
|
||||||
[client] = await db
|
|
||||||
.select()
|
|
||||||
.from(clients)
|
|
||||||
.where(eq(clients.clientId, olm.clientId))
|
|
||||||
.limit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [client] = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.clientId, olm.clientId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
logger.warn("Client ID not found");
|
logger.warn("Client ID not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [org] = await db
|
||||||
|
.select()
|
||||||
|
.from(orgs)
|
||||||
|
.where(eq(orgs.orgId, client.orgId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!org) {
|
||||||
|
logger.warn("Org not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orgId) {
|
||||||
|
if (!olm.userId) {
|
||||||
|
logger.warn("Olm has no user ID");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { session: userSession, user } =
|
||||||
|
await validateSessionToken(userToken);
|
||||||
|
if (!userSession || !user) {
|
||||||
|
logger.warn("Invalid user session for olm register");
|
||||||
|
return; // by returning here we just ignore the ping and the setInterval will force it to disconnect
|
||||||
|
}
|
||||||
|
if (user.userId !== olm.userId) {
|
||||||
|
logger.warn("User ID mismatch for olm register");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
|
orgId: orgId,
|
||||||
|
userId: olm.userId,
|
||||||
|
session: userToken // this is the user token passed in the message
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!policyCheck.allowed) {
|
||||||
|
logger.warn(
|
||||||
|
`Olm user ${olm.userId} does not pass access policies for org ${orgId}: ${policyCheck.error}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Olm client ID: ${client.clientId}, Public Key: ${publicKey}, Relay: ${relay}`
|
`Olm client ID: ${client.clientId}, Public Key: ${publicKey}, Relay: ${relay}`
|
||||||
);
|
);
|
||||||
@@ -105,41 +117,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client.exitNodeId) {
|
if (olmVersion && olm.version !== olmVersion) {
|
||||||
// TODO: FOR NOW WE ARE JUST HOLEPUNCHING ALL EXIT NODES BUT IN THE FUTURE WE SHOULD HANDLE THIS BETTER
|
|
||||||
|
|
||||||
// Get the exit node
|
|
||||||
const allExitNodes = await listExitNodes(client.orgId, true); // FILTER THE ONLINE ONES
|
|
||||||
|
|
||||||
const exitNodesHpData = allExitNodes.map((exitNode: ExitNode) => {
|
|
||||||
return {
|
|
||||||
publicKey: exitNode.publicKey,
|
|
||||||
endpoint: exitNode.endpoint
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send holepunch message
|
|
||||||
await sendToClient(olm.olmId, {
|
|
||||||
type: "olm/wg/holepunch/all",
|
|
||||||
data: {
|
|
||||||
exitNodes: exitNodesHpData
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!olmVersion) {
|
|
||||||
// THIS IS FOR BACKWARDS COMPATIBILITY
|
|
||||||
// THE OLDER CLIENTS DID NOT SEND THE VERSION
|
|
||||||
await sendToClient(olm.olmId, {
|
|
||||||
type: "olm/wg/holepunch",
|
|
||||||
data: {
|
|
||||||
serverPubKey: allExitNodes[0].publicKey,
|
|
||||||
endpoint: allExitNodes[0].endpoint
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (olmVersion) {
|
|
||||||
await db
|
await db
|
||||||
.update(olms)
|
.update(olms)
|
||||||
.set({
|
.set({
|
||||||
@@ -148,11 +126,6 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
.where(eq(olms.olmId, olm.olmId));
|
.where(eq(olms.olmId, olm.olmId));
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (now - (client.lastHolePunch || 0) > 6) {
|
|
||||||
// logger.warn("Client last hole punch is too old, skipping all sites");
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (client.pubKey !== publicKey) {
|
if (client.pubKey !== publicKey) {
|
||||||
logger.info(
|
logger.info(
|
||||||
"Public key mismatch. Updating public key and clearing session info..."
|
"Public key mismatch. Updating public key and clearing session info..."
|
||||||
@@ -190,15 +163,18 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
`Found ${sitesData.length} sites for client ${client.clientId}`
|
`Found ${sitesData.length} sites for client ${client.clientId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (sitesData.length === 0) {
|
// this prevents us from accepting a register from an olm that has not hole punched yet.
|
||||||
sendToClient(olm.olmId, {
|
// the olm will pump the register so we can keep checking
|
||||||
type: "olm/register/no-sites",
|
// TODO: I still think there is a better way to do this rather than locking it out here but ???
|
||||||
data: {}
|
if (now - (client.lastHolePunch || 0) > 5 && sitesData.length > 0) {
|
||||||
});
|
logger.warn(
|
||||||
|
"Client last hole punch is too old and we have sites to send; skipping this register"
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each site
|
// Process each site
|
||||||
for (const { sites: site } of sitesData) {
|
for (const { sites: site, clientSitesAssociationsCache: association } of sitesData) {
|
||||||
if (!site.exitNodeId) {
|
if (!site.exitNodeId) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Site ${site.siteId} does not have exit node, skipping`
|
`Site ${site.siteId} does not have exit node, skipping`
|
||||||
@@ -261,7 +237,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let endpoint = site.endpoint;
|
let relayEndpoint: string | undefined = undefined;
|
||||||
if (relay) {
|
if (relay) {
|
||||||
const [exitNode] = await db
|
const [exitNode] = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -272,7 +248,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
logger.warn(`Exit node not found for site ${site.siteId}`);
|
logger.warn(`Exit node not found for site ${site.siteId}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
endpoint = `${exitNode.endpoint}:21820`;
|
relayEndpoint = `${exitNode.endpoint}:${config.getRawConfig().gerbil.clients_start_port}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allSiteResources = await db // only get the site resources that this client has access to
|
const allSiteResources = await db // only get the site resources that this client has access to
|
||||||
@@ -298,11 +274,17 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
// Add site configuration to the array
|
// Add site configuration to the array
|
||||||
siteConfigurations.push({
|
siteConfigurations.push({
|
||||||
siteId: site.siteId,
|
siteId: site.siteId,
|
||||||
endpoint: endpoint,
|
relayEndpoint: relayEndpoint, // this can be undefined now if not relayed
|
||||||
|
endpoint: site.endpoint,
|
||||||
publicKey: site.publicKey,
|
publicKey: site.publicKey,
|
||||||
serverIP: site.address,
|
serverIP: site.address,
|
||||||
serverPort: site.listenPort,
|
serverPort: site.listenPort,
|
||||||
remoteSubnets: generateRemoteSubnets(allSiteResources.map(({ siteResources }) => siteResources))
|
remoteSubnets: generateRemoteSubnets(
|
||||||
|
allSiteResources.map(({ siteResources }) => siteResources)
|
||||||
|
),
|
||||||
|
aliases: generateAliasConfig(
|
||||||
|
allSiteResources.map(({ siteResources }) => siteResources)
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,128 +300,11 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
type: "olm/wg/connect",
|
type: "olm/wg/connect",
|
||||||
data: {
|
data: {
|
||||||
sites: siteConfigurations,
|
sites: siteConfigurations,
|
||||||
tunnelIP: client.subnet
|
tunnelIP: client.subnet,
|
||||||
|
utilitySubnet: org.utilitySubnet
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
broadcast: false,
|
broadcast: false,
|
||||||
excludeSender: false
|
excludeSender: false
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getOrCreateOrgClient(
|
|
||||||
orgId: string,
|
|
||||||
userId: string | null,
|
|
||||||
olmId: string,
|
|
||||||
name: string,
|
|
||||||
doNotCreateNewClient: boolean,
|
|
||||||
trx: Transaction | typeof db = db
|
|
||||||
): Promise<Client> {
|
|
||||||
// get the org
|
|
||||||
const [org] = await trx
|
|
||||||
.select()
|
|
||||||
.from(orgs)
|
|
||||||
.where(eq(orgs.orgId, orgId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!org) {
|
|
||||||
throw new Error("Org not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!org.subnet) {
|
|
||||||
throw new Error("Org has no subnet defined");
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the user has a client in the org and if not then create a client for them
|
|
||||||
const [existingClient] = await trx
|
|
||||||
.select()
|
|
||||||
.from(clients)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(clients.orgId, orgId),
|
|
||||||
userId ? eq(clients.userId, userId) : isNull(clients.userId), // we dont check the user id if it is null because the olm is not tied to a user?
|
|
||||||
eq(clients.olmId, olmId)
|
|
||||||
)
|
|
||||||
) // checking the olmid here because we want to create a new client PER OLM PER ORG
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
let client = existingClient;
|
|
||||||
if (!client && !doNotCreateNewClient) {
|
|
||||||
logger.debug(
|
|
||||||
`Client does not exist in org ${orgId}, creating new client for user ${userId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
throw new Error("User ID is required to create client in org");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that the user belongs to the org
|
|
||||||
const [userOrg] = await trx
|
|
||||||
.select()
|
|
||||||
.from(userOrgs)
|
|
||||||
.where(and(eq(userOrgs.orgId, orgId), eq(userOrgs.userId, userId)))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!userOrg) {
|
|
||||||
throw new Error("User does not belong to org");
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: more intelligent way to pick the exit node
|
|
||||||
const exitNodesList = await listExitNodes(orgId);
|
|
||||||
const randomExitNode =
|
|
||||||
exitNodesList[Math.floor(Math.random() * exitNodesList.length)];
|
|
||||||
|
|
||||||
const [adminRole] = await trx
|
|
||||||
.select()
|
|
||||||
.from(roles)
|
|
||||||
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!adminRole) {
|
|
||||||
throw new Error("Admin role not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const newSubnet = await getNextAvailableClientSubnet(orgId);
|
|
||||||
if (!newSubnet) {
|
|
||||||
throw new Error("No available subnet found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const subnet = newSubnet.split("/")[0];
|
|
||||||
const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`; // we want the block size of the whole org
|
|
||||||
|
|
||||||
const [newClient] = await trx
|
|
||||||
.insert(clients)
|
|
||||||
.values({
|
|
||||||
exitNodeId: randomExitNode.exitNodeId,
|
|
||||||
orgId,
|
|
||||||
name,
|
|
||||||
subnet: updatedSubnet,
|
|
||||||
type: "olm",
|
|
||||||
userId: userId,
|
|
||||||
olmId: olmId // to lock this client to the olm even as the olm moves between clients in different orgs
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
await trx.insert(roleClients).values({
|
|
||||||
roleId: adminRole.roleId,
|
|
||||||
clientId: newClient.clientId
|
|
||||||
});
|
|
||||||
|
|
||||||
await trx.insert(userClients).values({
|
|
||||||
// we also want to make sure that the user can see their own client if they are not an admin
|
|
||||||
userId,
|
|
||||||
clientId: newClient.clientId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (userOrg.roleId != adminRole.roleId) {
|
|
||||||
// make sure the user can access the client
|
|
||||||
trx.insert(userClients).values({
|
|
||||||
userId,
|
|
||||||
clientId: newClient.clientId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
client = newClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { db, exitNodes, sites } from "@server/db";
|
|||||||
import { MessageHandler } from "@server/routers/ws";
|
import { MessageHandler } from "@server/routers/ws";
|
||||||
import { clients, clientSitesAssociationsCache, Olm } from "@server/db";
|
import { clients, clientSitesAssociationsCache, Olm } from "@server/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { updatePeer } from "../newt/peers";
|
import { updatePeer as newtUpdatePeer } from "../newt/peers";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
|
||||||
export const handleOlmRelayMessage: MessageHandler = async (context) => {
|
export const handleOlmRelayMessage: MessageHandler = async (context) => {
|
||||||
@@ -79,18 +79,19 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// update the peer on the exit node
|
// update the peer on the exit node
|
||||||
await updatePeer(siteId, client.pubKey, {
|
await newtUpdatePeer(siteId, client.pubKey, {
|
||||||
endpoint: "" // this removes the endpoint
|
endpoint: "" // this removes the endpoint so the exit node knows to relay
|
||||||
});
|
});
|
||||||
|
|
||||||
sendToClient(olm.olmId, {
|
return {
|
||||||
type: "olm/wg/peer/relay",
|
message: {
|
||||||
data: {
|
type: "olm/wg/peer/relay",
|
||||||
siteId: siteId,
|
data: {
|
||||||
endpoint: exitNode.endpoint,
|
siteId: siteId,
|
||||||
publicKey: exitNode.publicKey
|
relayEndpoint: exitNode.endpoint
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
broadcast: false,
|
||||||
return;
|
excludeSender: false
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
187
server/routers/olm/handleOlmServerPeerAddMessage.ts
Normal file
187
server/routers/olm/handleOlmServerPeerAddMessage.ts
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import {
|
||||||
|
Client,
|
||||||
|
clientSiteResourcesAssociationsCache,
|
||||||
|
db,
|
||||||
|
ExitNode,
|
||||||
|
Org,
|
||||||
|
orgs,
|
||||||
|
roleClients,
|
||||||
|
roles,
|
||||||
|
siteResources,
|
||||||
|
Transaction,
|
||||||
|
userClients,
|
||||||
|
userOrgs,
|
||||||
|
users
|
||||||
|
} from "@server/db";
|
||||||
|
import { MessageHandler } from "@server/routers/ws";
|
||||||
|
import {
|
||||||
|
clients,
|
||||||
|
clientSitesAssociationsCache,
|
||||||
|
exitNodes,
|
||||||
|
Olm,
|
||||||
|
olms,
|
||||||
|
sites
|
||||||
|
} from "@server/db";
|
||||||
|
import { and, eq, inArray, isNotNull, isNull } from "drizzle-orm";
|
||||||
|
import { addPeer, deletePeer } from "../newt/peers";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { listExitNodes } from "#dynamic/lib/exitNodes";
|
||||||
|
import {
|
||||||
|
generateAliasConfig,
|
||||||
|
getNextAvailableClientSubnet
|
||||||
|
} from "@server/lib/ip";
|
||||||
|
import { generateRemoteSubnets } from "@server/lib/ip";
|
||||||
|
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||||
|
import { checkOrgAccessPolicy } from "@server/lib/checkOrgAccessPolicy";
|
||||||
|
import { validateSessionToken } from "@server/auth/sessions/app";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
import {
|
||||||
|
addPeer as newtAddPeer,
|
||||||
|
deletePeer as newtDeletePeer
|
||||||
|
} from "@server/routers/newt/peers";
|
||||||
|
|
||||||
|
export const handleOlmServerPeerAddMessage: MessageHandler = async (
|
||||||
|
context
|
||||||
|
) => {
|
||||||
|
logger.info("Handling register olm message!");
|
||||||
|
const { message, client: c, sendToClient } = context;
|
||||||
|
const olm = c as Olm;
|
||||||
|
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
if (!olm) {
|
||||||
|
logger.warn("Olm not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { siteId } = message.data;
|
||||||
|
|
||||||
|
// get the site
|
||||||
|
const [site] = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.where(eq(sites.siteId, siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!site) {
|
||||||
|
logger.error(
|
||||||
|
`handleOlmServerPeerAddMessage: Site with ID ${siteId} not found`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!site.endpoint) {
|
||||||
|
logger.error(
|
||||||
|
`handleOlmServerPeerAddMessage: Site with ID ${siteId} has no endpoint`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the client
|
||||||
|
|
||||||
|
if (!olm.clientId) {
|
||||||
|
logger.error(
|
||||||
|
`handleOlmServerPeerAddMessage: Olm with ID ${olm.olmId} has no clientId`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [client] = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(and(eq(clients.clientId, olm.clientId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
logger.error(
|
||||||
|
`handleOlmServerPeerAddMessage: Client with ID ${olm.clientId} not found`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!client.pubKey) {
|
||||||
|
logger.error(
|
||||||
|
`handleOlmServerPeerAddMessage: Client with ID ${client.clientId} has no public key`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let endpoint: string | null = null;
|
||||||
|
|
||||||
|
|
||||||
|
const currentSessionSiteAssociationCaches = await db
|
||||||
|
.select()
|
||||||
|
.from(clientSitesAssociationsCache)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(clientSitesAssociationsCache.clientId, client.clientId),
|
||||||
|
isNotNull(clientSitesAssociationsCache.endpoint),
|
||||||
|
eq(clientSitesAssociationsCache.publicKey, client.pubKey) // limit it to the current session its connected with otherwise the endpoint could be stale
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// pick an endpoint
|
||||||
|
for (const assoc of currentSessionSiteAssociationCaches) {
|
||||||
|
if (assoc.endpoint) {
|
||||||
|
endpoint = assoc.endpoint;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!endpoint) {
|
||||||
|
logger.error(
|
||||||
|
`handleOlmServerPeerAddMessage: No endpoint found for client ${client.clientId}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: here we are always starting direct to the peer and will relay later
|
||||||
|
|
||||||
|
await newtAddPeer(siteId, {
|
||||||
|
publicKey: client.pubKey,
|
||||||
|
allowedIps: [`${client.subnet.split("/")[0]}/32`], // we want to only allow from that client
|
||||||
|
endpoint: endpoint // this is the client's endpoint with reference to the site's exit node
|
||||||
|
});
|
||||||
|
|
||||||
|
const allSiteResources = await db // only get the site resources that this client has access to
|
||||||
|
.select()
|
||||||
|
.from(siteResources)
|
||||||
|
.innerJoin(
|
||||||
|
clientSiteResourcesAssociationsCache,
|
||||||
|
eq(
|
||||||
|
siteResources.siteResourceId,
|
||||||
|
clientSiteResourcesAssociationsCache.siteResourceId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(siteResources.siteId, site.siteId),
|
||||||
|
eq(
|
||||||
|
clientSiteResourcesAssociationsCache.clientId,
|
||||||
|
client.clientId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return connect message with all site configurations
|
||||||
|
return {
|
||||||
|
message: {
|
||||||
|
type: "olm/wg/peer/add",
|
||||||
|
data: {
|
||||||
|
siteId: site.siteId,
|
||||||
|
endpoint: site.endpoint,
|
||||||
|
publicKey: site.publicKey,
|
||||||
|
serverIP: site.address,
|
||||||
|
serverPort: site.listenPort,
|
||||||
|
remoteSubnets: generateRemoteSubnets(
|
||||||
|
allSiteResources.map(({ siteResources }) => siteResources)
|
||||||
|
),
|
||||||
|
aliases: generateAliasConfig(
|
||||||
|
allSiteResources.map(({ siteResources }) => siteResources)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
broadcast: false,
|
||||||
|
excludeSender: false
|
||||||
|
};
|
||||||
|
};
|
||||||
96
server/routers/olm/handleOlmUnRelayMessage.ts
Normal file
96
server/routers/olm/handleOlmUnRelayMessage.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { db, exitNodes, sites } from "@server/db";
|
||||||
|
import { MessageHandler } from "@server/routers/ws";
|
||||||
|
import { clients, clientSitesAssociationsCache, Olm } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import { updatePeer as newtUpdatePeer } from "../newt/peers";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
|
export const handleOlmUnRelayMessage: MessageHandler = async (context) => {
|
||||||
|
const { message, client: c, sendToClient } = context;
|
||||||
|
const olm = c as Olm;
|
||||||
|
|
||||||
|
logger.info("Handling unrelay olm message!");
|
||||||
|
|
||||||
|
if (!olm) {
|
||||||
|
logger.warn("Olm not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!olm.clientId) {
|
||||||
|
logger.warn("Olm has no site!"); // TODO: Maybe we create the site here?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientId = olm.clientId;
|
||||||
|
|
||||||
|
const [client] = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.clientId, clientId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
logger.warn("Client not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we hand endpoints for both the site and the client and the lastHolePunch is not too old
|
||||||
|
if (!client.pubKey) {
|
||||||
|
logger.warn("Client has no endpoint or listen port");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { siteId } = message.data;
|
||||||
|
|
||||||
|
// Get the site
|
||||||
|
const [site] = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.where(eq(sites.siteId, siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!site) {
|
||||||
|
logger.warn("Site not found or has no exit node");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [clientSiteAssociation] = await db
|
||||||
|
.update(clientSitesAssociationsCache)
|
||||||
|
.set({
|
||||||
|
isRelayed: false
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(clientSitesAssociationsCache.clientId, olm.clientId),
|
||||||
|
eq(clientSitesAssociationsCache.siteId, siteId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (!clientSiteAssociation) {
|
||||||
|
logger.warn("Client-Site association not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clientSiteAssociation.endpoint) {
|
||||||
|
logger.warn("Client-Site association has no endpoint, cannot unrelay");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the peer on the exit node
|
||||||
|
await newtUpdatePeer(siteId, client.pubKey, {
|
||||||
|
endpoint: clientSiteAssociation.endpoint // this is the endpoint of the client to connect directly to the exit node
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: {
|
||||||
|
type: "olm/wg/peer/unrelay",
|
||||||
|
data: {
|
||||||
|
siteId: siteId,
|
||||||
|
endpoint: site.endpoint
|
||||||
|
}
|
||||||
|
},
|
||||||
|
broadcast: false,
|
||||||
|
excludeSender: false
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -7,3 +7,5 @@ export * from "./deleteUserOlm";
|
|||||||
export * from "./listUserOlms";
|
export * from "./listUserOlms";
|
||||||
export * from "./deleteUserOlm";
|
export * from "./deleteUserOlm";
|
||||||
export * from "./getUserOlm";
|
export * from "./getUserOlm";
|
||||||
|
export * from "./handleOlmServerPeerAddMessage";
|
||||||
|
export * from "./handleOlmUnRelayMessage";
|
||||||
@@ -3,6 +3,7 @@ import { clients, olms, newts, sites } from "@server/db";
|
|||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { sendToClient } from "#dynamic/routers/ws";
|
import { sendToClient } from "#dynamic/routers/ws";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
import { exit } from "process";
|
||||||
|
|
||||||
export async function addPeer(
|
export async function addPeer(
|
||||||
clientId: number,
|
clientId: number,
|
||||||
@@ -78,8 +79,8 @@ export async function updatePeer(
|
|||||||
siteId: number;
|
siteId: number;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
serverIP: string | null;
|
serverIP?: string | null;
|
||||||
serverPort: number | null;
|
serverPort?: number | null;
|
||||||
remoteSubnets?: string[] | null; // optional, comma-separated list of subnets that
|
remoteSubnets?: string[] | null; // optional, comma-separated list of subnets that
|
||||||
},
|
},
|
||||||
olmId?: string
|
olmId?: string
|
||||||
@@ -102,6 +103,7 @@ export async function updatePeer(
|
|||||||
siteId: peer.siteId,
|
siteId: peer.siteId,
|
||||||
publicKey: peer.publicKey,
|
publicKey: peer.publicKey,
|
||||||
endpoint: peer.endpoint,
|
endpoint: peer.endpoint,
|
||||||
|
relayEndpoint: peer.serverIP,
|
||||||
serverIP: peer.serverIP,
|
serverIP: peer.serverIP,
|
||||||
serverPort: peer.serverPort,
|
serverPort: peer.serverPort,
|
||||||
remoteSubnets: peer.remoteSubnets
|
remoteSubnets: peer.remoteSubnets
|
||||||
@@ -110,3 +112,40 @@ export async function updatePeer(
|
|||||||
|
|
||||||
logger.info(`Added peer ${peer.publicKey} to olm ${olmId}`);
|
logger.info(`Added peer ${peer.publicKey} to olm ${olmId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function initPeerAddHandshake(
|
||||||
|
clientId: number,
|
||||||
|
peer: {
|
||||||
|
siteId: number;
|
||||||
|
exitNode: {
|
||||||
|
publicKey: string;
|
||||||
|
endpoint: string;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
olmId?: string
|
||||||
|
) {
|
||||||
|
if (!olmId) {
|
||||||
|
const [olm] = await db
|
||||||
|
.select()
|
||||||
|
.from(olms)
|
||||||
|
.where(eq(olms.clientId, clientId))
|
||||||
|
.limit(1);
|
||||||
|
if (!olm) {
|
||||||
|
throw new Error(`Olm with ID ${clientId} not found`);
|
||||||
|
}
|
||||||
|
olmId = olm.olmId;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendToClient(olmId, {
|
||||||
|
type: "olm/wg/peer/holepunch/site/add",
|
||||||
|
data: {
|
||||||
|
siteId: peer.siteId,
|
||||||
|
exitNode: {
|
||||||
|
publicKey: peer.exitNode.publicKey,
|
||||||
|
endpoint: peer.exitNode.endpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`Initiated peer add handshake for site ${peer.siteId} to olm ${olmId}`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ import { FeatureId } from "@server/lib/billing";
|
|||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
const createOrgSchema = z.strictObject({
|
const createOrgSchema = z.strictObject({
|
||||||
orgId: z.string(),
|
orgId: z.string(),
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
subnet: z.string()
|
subnet: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "put",
|
method: "put",
|
||||||
@@ -131,12 +131,16 @@ export async function createOrg(
|
|||||||
.from(domains)
|
.from(domains)
|
||||||
.where(eq(domains.configManaged, true));
|
.where(eq(domains.configManaged, true));
|
||||||
|
|
||||||
|
const utilitySubnet =
|
||||||
|
config.getRawConfig().orgs.utility_subnet_group;
|
||||||
|
|
||||||
const newOrg = await trx
|
const newOrg = await trx
|
||||||
.insert(orgs)
|
.insert(orgs)
|
||||||
.values({
|
.values({
|
||||||
orgId,
|
orgId,
|
||||||
name,
|
name,
|
||||||
subnet,
|
subnet,
|
||||||
|
utilitySubnet,
|
||||||
createdAt: new Date().toISOString()
|
createdAt: new Date().toISOString()
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, domains, orgDomains, resources } from "@server/db";
|
import {
|
||||||
|
clients,
|
||||||
|
clientSiteResourcesAssociationsCache,
|
||||||
|
clientSitesAssociationsCache,
|
||||||
|
db,
|
||||||
|
domains,
|
||||||
|
olms,
|
||||||
|
orgDomains,
|
||||||
|
resources
|
||||||
|
} from "@server/db";
|
||||||
import { newts, newtSessions, orgs, sites, userActions } from "@server/db";
|
import { newts, newtSessions, orgs, sites, userActions } from "@server/db";
|
||||||
import { eq, and, inArray, sql } from "drizzle-orm";
|
import { eq, and, inArray, sql } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
@@ -14,8 +23,8 @@ import { deletePeer } from "../gerbil/peers";
|
|||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
const deleteOrgSchema = z.strictObject({
|
const deleteOrgSchema = z.strictObject({
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type DeleteOrgResponse = {};
|
export type DeleteOrgResponse = {};
|
||||||
|
|
||||||
@@ -69,41 +78,75 @@ export async function deleteOrg(
|
|||||||
.where(eq(sites.orgId, orgId))
|
.where(eq(sites.orgId, orgId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
|
const orgClients = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.orgId, orgId));
|
||||||
|
|
||||||
const deletedNewtIds: string[] = [];
|
const deletedNewtIds: string[] = [];
|
||||||
|
const olmsToTerminate: string[] = [];
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
if (sites) {
|
for (const site of orgSites) {
|
||||||
for (const site of orgSites) {
|
if (site.pubKey) {
|
||||||
if (site.pubKey) {
|
if (site.type == "wireguard") {
|
||||||
if (site.type == "wireguard") {
|
await deletePeer(site.exitNodeId!, site.pubKey);
|
||||||
await deletePeer(site.exitNodeId!, site.pubKey);
|
} else if (site.type == "newt") {
|
||||||
} else if (site.type == "newt") {
|
// get the newt on the site by querying the newt table for siteId
|
||||||
// get the newt on the site by querying the newt table for siteId
|
const [deletedNewt] = await trx
|
||||||
const [deletedNewt] = await trx
|
.delete(newts)
|
||||||
.delete(newts)
|
.where(eq(newts.siteId, site.siteId))
|
||||||
.where(eq(newts.siteId, site.siteId))
|
.returning();
|
||||||
.returning();
|
if (deletedNewt) {
|
||||||
if (deletedNewt) {
|
deletedNewtIds.push(deletedNewt.newtId);
|
||||||
deletedNewtIds.push(deletedNewt.newtId);
|
|
||||||
|
|
||||||
// delete all of the sessions for the newt
|
// delete all of the sessions for the newt
|
||||||
await trx
|
await trx
|
||||||
.delete(newtSessions)
|
.delete(newtSessions)
|
||||||
.where(
|
.where(
|
||||||
eq(
|
eq(newtSessions.newtId, deletedNewt.newtId)
|
||||||
newtSessions.newtId,
|
);
|
||||||
deletedNewt.newtId
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Deleting site ${site.siteId}`);
|
|
||||||
await trx
|
|
||||||
.delete(sites)
|
|
||||||
.where(eq(sites.siteId, site.siteId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(`Deleting site ${site.siteId}`);
|
||||||
|
await trx.delete(sites).where(eq(sites.siteId, site.siteId));
|
||||||
|
}
|
||||||
|
for (const client of orgClients) {
|
||||||
|
const [olm] = await trx
|
||||||
|
.select()
|
||||||
|
.from(olms)
|
||||||
|
.where(eq(olms.clientId, client.clientId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (olm) {
|
||||||
|
olmsToTerminate.push(olm.olmId);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Deleting client ${client.clientId}`);
|
||||||
|
await trx
|
||||||
|
.delete(clients)
|
||||||
|
.where(eq(clients.clientId, client.clientId));
|
||||||
|
|
||||||
|
// also delete the associations
|
||||||
|
await trx
|
||||||
|
.delete(clientSiteResourcesAssociationsCache)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
clientSiteResourcesAssociationsCache.clientId,
|
||||||
|
client.clientId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await trx
|
||||||
|
.delete(clientSitesAssociationsCache)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
clientSitesAssociationsCache.clientId,
|
||||||
|
client.clientId
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const allOrgDomains = await trx
|
const allOrgDomains = await trx
|
||||||
@@ -150,7 +193,7 @@ export async function deleteOrg(
|
|||||||
// Send termination messages outside of transaction to prevent blocking
|
// Send termination messages outside of transaction to prevent blocking
|
||||||
for (const newtId of deletedNewtIds) {
|
for (const newtId of deletedNewtIds) {
|
||||||
const payload = {
|
const payload = {
|
||||||
type: `newt/terminate`,
|
type: `newt/wg/terminate`,
|
||||||
data: {}
|
data: {}
|
||||||
};
|
};
|
||||||
// Don't await this to prevent blocking the response
|
// Don't await this to prevent blocking the response
|
||||||
@@ -162,6 +205,18 @@ export async function deleteOrg(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const olmId of olmsToTerminate) {
|
||||||
|
sendToClient(olmId, {
|
||||||
|
type: "olm/terminate",
|
||||||
|
data: {}
|
||||||
|
}).catch((error) => {
|
||||||
|
logger.error(
|
||||||
|
"Failed to send termination message to olm:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: null,
|
data: null,
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import logger from "@server/logger";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
|
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
const addClientToSiteResourceBodySchema = z
|
const addClientToSiteResourceBodySchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -136,7 +136,7 @@ export async function addClientToSiteResource(
|
|||||||
siteResourceId
|
siteResourceId
|
||||||
});
|
});
|
||||||
|
|
||||||
await rebuildClientAssociations(siteResource, trx);
|
await rebuildClientAssociationsFromSiteResource(siteResource, trx);
|
||||||
});
|
});
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import logger from "@server/logger";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
|
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
const addRoleToSiteResourceBodySchema = z
|
const addRoleToSiteResourceBodySchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -146,7 +146,7 @@ export async function addRoleToSiteResource(
|
|||||||
siteResourceId
|
siteResourceId
|
||||||
});
|
});
|
||||||
|
|
||||||
await rebuildClientAssociations(siteResource, trx);
|
await rebuildClientAssociationsFromSiteResource(siteResource, trx);
|
||||||
});
|
});
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import logger from "@server/logger";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
|
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
const addUserToSiteResourceBodySchema = z
|
const addUserToSiteResourceBodySchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -115,7 +115,7 @@ export async function addUserToSiteResource(
|
|||||||
siteResourceId
|
siteResourceId
|
||||||
});
|
});
|
||||||
|
|
||||||
await rebuildClientAssociations(siteResource, trx);
|
await rebuildClientAssociationsFromSiteResource(siteResource, trx);
|
||||||
});
|
});
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ import { fromError } from "zod-validation-error";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { getUniqueSiteResourceName } from "@server/db/names";
|
import { getUniqueSiteResourceName } from "@server/db/names";
|
||||||
import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
|
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||||
|
import { getNextAvailableAliasAddress } from "@server/lib/ip";
|
||||||
|
|
||||||
const createSiteResourceParamsSchema = z.strictObject({
|
const createSiteResourceParamsSchema = z.strictObject({
|
||||||
siteId: z.string().transform(Number).pipe(z.int().positive()),
|
siteId: z.string().transform(Number).pipe(z.int().positive()),
|
||||||
@@ -193,6 +194,10 @@ export async function createSiteResource(
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
const niceId = await getUniqueSiteResourceName(orgId);
|
const niceId = await getUniqueSiteResourceName(orgId);
|
||||||
|
let aliasAddress: string | null = null;
|
||||||
|
if (mode == "host") { // we can only have an alias on a host
|
||||||
|
aliasAddress = await getNextAvailableAliasAddress(orgId);
|
||||||
|
}
|
||||||
|
|
||||||
let newSiteResource: SiteResource | undefined;
|
let newSiteResource: SiteResource | undefined;
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
@@ -210,7 +215,8 @@ export async function createSiteResource(
|
|||||||
// destinationPort: mode === "port" ? destinationPort : null,
|
// destinationPort: mode === "port" ? destinationPort : null,
|
||||||
destination,
|
destination,
|
||||||
enabled,
|
enabled,
|
||||||
alias: alias || null
|
alias,
|
||||||
|
aliasAddress
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -272,7 +278,7 @@ export async function createSiteResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await rebuildClientAssociations(newSiteResource, trx); // we need to call this because we added to the admin role
|
await rebuildClientAssociationsFromSiteResource(newSiteResource, trx); // we need to call this because we added to the admin role
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!newSiteResource) {
|
if (!newSiteResource) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { eq, and } from "drizzle-orm";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
|
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
const deleteSiteResourceParamsSchema = z.strictObject({
|
const deleteSiteResourceParamsSchema = z.strictObject({
|
||||||
siteResourceId: z.string().transform(Number).pipe(z.int().positive()),
|
siteResourceId: z.string().transform(Number).pipe(z.int().positive()),
|
||||||
@@ -106,7 +106,7 @@ export async function deleteSiteResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await rebuildClientAssociations(removedSiteResource, trx);
|
await rebuildClientAssociationsFromSiteResource(removedSiteResource, trx);
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import logger from "@server/logger";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
|
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
const removeClientFromSiteResourceBodySchema = z
|
const removeClientFromSiteResourceBodySchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -142,7 +142,7 @@ export async function removeClientFromSiteResource(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
await rebuildClientAssociations(siteResource, trx);
|
await rebuildClientAssociationsFromSiteResource(siteResource, trx);
|
||||||
});
|
});
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import logger from "@server/logger";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
|
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
const removeRoleFromSiteResourceBodySchema = z
|
const removeRoleFromSiteResourceBodySchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -151,7 +151,7 @@ export async function removeRoleFromSiteResource(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
await rebuildClientAssociations(siteResource, trx);
|
await rebuildClientAssociationsFromSiteResource(siteResource, trx);
|
||||||
});
|
});
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import logger from "@server/logger";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
|
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
const removeUserFromSiteResourceBodySchema = z
|
const removeUserFromSiteResourceBodySchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -121,7 +121,7 @@ export async function removeUserFromSiteResource(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
await rebuildClientAssociations(siteResource, trx);
|
await rebuildClientAssociationsFromSiteResource(siteResource, trx);
|
||||||
});
|
});
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import logger from "@server/logger";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { eq, inArray } from "drizzle-orm";
|
import { eq, inArray } from "drizzle-orm";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
|
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
const setSiteResourceClientsBodySchema = z
|
const setSiteResourceClientsBodySchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -124,7 +124,7 @@ export async function setSiteResourceClients(
|
|||||||
.values(clientIds.map((clientId) => ({ clientId, siteResourceId })));
|
.values(clientIds.map((clientId) => ({ clientId, siteResourceId })));
|
||||||
}
|
}
|
||||||
|
|
||||||
await rebuildClientAssociations(siteResource, trx);
|
await rebuildClientAssociationsFromSiteResource(siteResource, trx);
|
||||||
});
|
});
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import logger from "@server/logger";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { eq, and, ne, inArray } from "drizzle-orm";
|
import { eq, and, ne, inArray } from "drizzle-orm";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
|
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
const setSiteResourceRolesBodySchema = z
|
const setSiteResourceRolesBodySchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -147,7 +147,7 @@ export async function setSiteResourceRoles(
|
|||||||
.values(roleIds.map((roleId) => ({ roleId, siteResourceId })));
|
.values(roleIds.map((roleId) => ({ roleId, siteResourceId })));
|
||||||
}
|
}
|
||||||
|
|
||||||
await rebuildClientAssociations(siteResource, trx);
|
await rebuildClientAssociationsFromSiteResource(siteResource, trx);
|
||||||
});
|
});
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import logger from "@server/logger";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
|
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
const setSiteResourceUsersBodySchema = z
|
const setSiteResourceUsersBodySchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -102,7 +102,7 @@ export async function setSiteResourceUsers(
|
|||||||
.values(userIds.map((userId) => ({ userId, siteResourceId })));
|
.values(userIds.map((userId) => ({ userId, siteResourceId })));
|
||||||
}
|
}
|
||||||
|
|
||||||
await rebuildClientAssociations(siteResource, trx);
|
await rebuildClientAssociationsFromSiteResource(siteResource, trx);
|
||||||
});
|
});
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -17,17 +17,15 @@ import { eq, and, ne } from "drizzle-orm";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { updatePeerData, updateTargets } from "@server/routers/client/targets";
|
||||||
import {
|
import {
|
||||||
updateRemoteSubnets,
|
generateAliasConfig,
|
||||||
updateTargets
|
|
||||||
} from "@server/routers/client/targets";
|
|
||||||
import {
|
|
||||||
generateRemoteSubnets,
|
generateRemoteSubnets,
|
||||||
generateSubnetProxyTargets
|
generateSubnetProxyTargets
|
||||||
} from "@server/lib/ip";
|
} from "@server/lib/ip";
|
||||||
import {
|
import {
|
||||||
getClientSiteResourceAccess,
|
getClientSiteResourceAccess,
|
||||||
rebuildClientAssociations
|
rebuildClientAssociationsFromSiteResource
|
||||||
} from "@server/lib/rebuildClientAssociations";
|
} from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
const updateSiteResourceParamsSchema = z.strictObject({
|
const updateSiteResourceParamsSchema = z.strictObject({
|
||||||
@@ -51,7 +49,44 @@ const updateSiteResourceSchema = z
|
|||||||
roleIds: z.array(z.int()),
|
roleIds: z.array(z.int()),
|
||||||
clientIds: z.array(z.int())
|
clientIds: z.array(z.int())
|
||||||
})
|
})
|
||||||
.strict();
|
.strict()
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.mode === "host" && data.destination) {
|
||||||
|
// Check if it's a valid IP address using zod (v4 or v6)
|
||||||
|
const isValidIP = z
|
||||||
|
.union([z.ipv4(), z.ipv6()])
|
||||||
|
.safeParse(data.destination).success;
|
||||||
|
|
||||||
|
// Check if it's a valid domain (hostname pattern, TLD not required)
|
||||||
|
const domainRegex =
|
||||||
|
/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
|
||||||
|
const isValidDomain = domainRegex.test(data.destination);
|
||||||
|
|
||||||
|
return isValidIP || isValidDomain;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"Destination must be a valid IP address or domain name for host mode"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.mode === "cidr" && data.destination) {
|
||||||
|
// Check if it's a valid CIDR (v4 or v6)
|
||||||
|
const isValidCIDR = z
|
||||||
|
.union([z.cidrv4(), z.cidrv6()])
|
||||||
|
.safeParse(data.destination).success;
|
||||||
|
return isValidCIDR;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Destination must be a valid CIDR notation for cidr mode"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export type UpdateSiteResourceBody = z.infer<typeof updateSiteResourceSchema>;
|
export type UpdateSiteResourceBody = z.infer<typeof updateSiteResourceSchema>;
|
||||||
export type UpdateSiteResourceResponse = SiteResource;
|
export type UpdateSiteResourceResponse = SiteResource;
|
||||||
@@ -226,16 +261,20 @@ export async function updateSiteResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { mergedAllClients } = await rebuildClientAssociations(
|
const { mergedAllClients } =
|
||||||
existingSiteResource, // we want to rebuild based on the existing resource then we will apply the change to the destination below
|
await rebuildClientAssociationsFromSiteResource(
|
||||||
trx
|
existingSiteResource, // we want to rebuild based on the existing resource then we will apply the change to the destination below
|
||||||
);
|
trx
|
||||||
|
);
|
||||||
|
|
||||||
// after everything is rebuilt above we still need to update the targets and remote subnets if the destination changed
|
// after everything is rebuilt above we still need to update the targets and remote subnets if the destination changed
|
||||||
if (
|
const destinationChanged =
|
||||||
existingSiteResource.destination !==
|
existingSiteResource.destination !==
|
||||||
updatedSiteResource.destination
|
updatedSiteResource.destination;
|
||||||
) {
|
const aliasChanged =
|
||||||
|
existingSiteResource.alias !== updatedSiteResource.alias;
|
||||||
|
|
||||||
|
if (destinationChanged || aliasChanged) {
|
||||||
const [newt] = await trx
|
const [newt] = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(newts)
|
.from(newts)
|
||||||
@@ -248,25 +287,28 @@ export async function updateSiteResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldTargets = generateSubnetProxyTargets(
|
// Only update targets on newt if destination changed
|
||||||
existingSiteResource,
|
if (destinationChanged) {
|
||||||
mergedAllClients
|
const oldTargets = generateSubnetProxyTargets(
|
||||||
);
|
existingSiteResource,
|
||||||
const newTargets = generateSubnetProxyTargets(
|
mergedAllClients
|
||||||
updatedSiteResource,
|
);
|
||||||
mergedAllClients
|
const newTargets = generateSubnetProxyTargets(
|
||||||
);
|
updatedSiteResource,
|
||||||
|
mergedAllClients
|
||||||
|
);
|
||||||
|
|
||||||
await updateTargets(newt.newtId, {
|
await updateTargets(newt.newtId, {
|
||||||
oldTargets: oldTargets,
|
oldTargets: oldTargets,
|
||||||
newTargets: newTargets
|
newTargets: newTargets
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const olmJobs: Promise<void>[] = [];
|
const olmJobs: Promise<void>[] = [];
|
||||||
for (const client of mergedAllClients) {
|
for (const client of mergedAllClients) {
|
||||||
// we also need to update the remote subnets on the olms for each client that has access to this site
|
// we also need to update the remote subnets on the olms for each client that has access to this site
|
||||||
olmJobs.push(
|
olmJobs.push(
|
||||||
updateRemoteSubnets(
|
updatePeerData(
|
||||||
client.clientId,
|
client.clientId,
|
||||||
updatedSiteResource.siteId,
|
updatedSiteResource.siteId,
|
||||||
{
|
{
|
||||||
@@ -276,8 +318,22 @@ export async function updateSiteResource(
|
|||||||
newRemoteSubnets: generateRemoteSubnets([
|
newRemoteSubnets: generateRemoteSubnets([
|
||||||
updatedSiteResource
|
updatedSiteResource
|
||||||
])
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
oldAliases: generateAliasConfig([
|
||||||
|
existingSiteResource
|
||||||
|
]),
|
||||||
|
newAliases: generateAliasConfig([
|
||||||
|
updatedSiteResource
|
||||||
|
])
|
||||||
}
|
}
|
||||||
)
|
).catch((error) => {
|
||||||
|
// this is okay because sometimes the olm is not online to receive the update or associated with the client yet
|
||||||
|
logger.warn(
|
||||||
|
`Error updating peer data for client ${client.clientId}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { clients, db, UserOrg } from "@server/db";
|
||||||
import { userOrgs, roles } from "@server/db";
|
import { userOrgs, roles } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
@@ -10,11 +10,12 @@ import logger from "@server/logger";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import stoi from "@server/lib/stoi";
|
import stoi from "@server/lib/stoi";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
const addUserRoleParamsSchema = z.strictObject({
|
const addUserRoleParamsSchema = z.strictObject({
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
roleId: z.string().transform(stoi).pipe(z.number())
|
roleId: z.string().transform(stoi).pipe(z.number())
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AddUserRoleResponse = z.infer<typeof addUserRoleParamsSchema>;
|
export type AddUserRoleResponse = z.infer<typeof addUserRoleParamsSchema>;
|
||||||
|
|
||||||
@@ -72,7 +73,9 @@ export async function addUserRole(
|
|||||||
const existingUser = await db
|
const existingUser = await db
|
||||||
.select()
|
.select()
|
||||||
.from(userOrgs)
|
.from(userOrgs)
|
||||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, role.orgId)))
|
.where(
|
||||||
|
and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, role.orgId))
|
||||||
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (existingUser.length === 0) {
|
if (existingUser.length === 0) {
|
||||||
@@ -108,14 +111,39 @@ export async function addUserRole(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newUserRole = await db
|
let newUserRole: UserOrg | null = null;
|
||||||
.update(userOrgs)
|
await db.transaction(async (trx) => {
|
||||||
.set({ roleId })
|
[newUserRole] = await trx
|
||||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, role.orgId)))
|
.update(userOrgs)
|
||||||
.returning();
|
.set({ roleId })
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userOrgs.userId, userId),
|
||||||
|
eq(userOrgs.orgId, role.orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// get the client associated with this user in this org
|
||||||
|
const orgClients = await trx
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(clients.userId, userId),
|
||||||
|
eq(clients.orgId, role.orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
for (const orgClient of orgClients) {
|
||||||
|
// we just changed the user's role, so we need to rebuild client associations and what they have access to
|
||||||
|
await rebuildClientAssociationsFromClient(orgClient, trx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: newUserRole[0],
|
data: newUserRole,
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Role added to user successfully",
|
message: "Role added to user successfully",
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import HttpCode from "@server/types/HttpCode";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
|
||||||
|
|
||||||
const removeUserSchema = z.strictObject({
|
const removeUserSchema = z.strictObject({
|
||||||
userId: z.string()
|
userId: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function adminRemoveUser(
|
export async function adminRemoveUser(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -50,7 +51,11 @@ export async function adminRemoveUser(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.delete(users).where(eq(users.userId, userId));
|
await db.transaction(async (trx) => {
|
||||||
|
await trx.delete(users).where(eq(users.userId, userId));
|
||||||
|
|
||||||
|
await calculateUserClientsForOrgs(userId, trx);
|
||||||
|
});
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: null,
|
data: null,
|
||||||
|
|||||||
@@ -11,23 +11,27 @@ import {
|
|||||||
handleOlmRegisterMessage,
|
handleOlmRegisterMessage,
|
||||||
handleOlmRelayMessage,
|
handleOlmRelayMessage,
|
||||||
handleOlmPingMessage,
|
handleOlmPingMessage,
|
||||||
startOlmOfflineChecker
|
startOlmOfflineChecker,
|
||||||
|
handleOlmServerPeerAddMessage,
|
||||||
|
handleOlmUnRelayMessage
|
||||||
} from "../olm";
|
} from "../olm";
|
||||||
import { handleHealthcheckStatusMessage } from "../target";
|
import { handleHealthcheckStatusMessage } from "../target";
|
||||||
import { MessageHandler } from "./types";
|
import { MessageHandler } from "./types";
|
||||||
|
|
||||||
export const messageHandlers: Record<string, MessageHandler> = {
|
export const messageHandlers: Record<string, MessageHandler> = {
|
||||||
"newt/wg/register": handleNewtRegisterMessage,
|
"olm/wg/server/peer/add": handleOlmServerPeerAddMessage,
|
||||||
"olm/wg/register": handleOlmRegisterMessage,
|
"olm/wg/register": handleOlmRegisterMessage,
|
||||||
|
"olm/wg/relay": handleOlmRelayMessage,
|
||||||
|
"olm/wg/unrelay": handleOlmUnRelayMessage,
|
||||||
|
"olm/ping": handleOlmPingMessage,
|
||||||
|
"newt/wg/register": handleNewtRegisterMessage,
|
||||||
"newt/wg/get-config": handleGetConfigMessage,
|
"newt/wg/get-config": handleGetConfigMessage,
|
||||||
"newt/receive-bandwidth": handleReceiveBandwidthMessage,
|
"newt/receive-bandwidth": handleReceiveBandwidthMessage,
|
||||||
"olm/wg/relay": handleOlmRelayMessage,
|
|
||||||
"olm/ping": handleOlmPingMessage,
|
|
||||||
"newt/socket/status": handleDockerStatusMessage,
|
"newt/socket/status": handleDockerStatusMessage,
|
||||||
"newt/socket/containers": handleDockerContainersMessage,
|
"newt/socket/containers": handleDockerContainersMessage,
|
||||||
"newt/ping/request": handleNewtPingRequestMessage,
|
"newt/ping/request": handleNewtPingRequestMessage,
|
||||||
"newt/blueprint/apply": handleApplyBlueprintMessage,
|
"newt/blueprint/apply": handleApplyBlueprintMessage,
|
||||||
"newt/healthcheck/status": handleHealthcheckStatusMessage,
|
"newt/healthcheck/status": handleHealthcheckStatusMessage
|
||||||
};
|
};
|
||||||
|
|
||||||
startOlmOfflineChecker(); // this is to handle the offline check for olms
|
startOlmOfflineChecker(); // this is to handle the offline check for olms
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ export default async function DeviceLoginPage({ searchParams }: Props) {
|
|||||||
const params = await searchParams;
|
const params = await searchParams;
|
||||||
const code = params.code || "";
|
const code = params.code || "";
|
||||||
|
|
||||||
console.log("user", user);
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
const redirectDestination = code
|
const redirectDestination = code
|
||||||
? `/auth/login/device?code=${encodeURIComponent(code)}`
|
? `/auth/login/device?code=${encodeURIComponent(code)}`
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ export default function DeviceLoginForm({
|
|||||||
if (!data.code.includes("-") && data.code.length === 8) {
|
if (!data.code.includes("-") && data.code.length === 8) {
|
||||||
data.code = data.code.slice(0, 4) + "-" + data.code.slice(4);
|
data.code = data.code.slice(0, 4) + "-" + data.code.slice(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
|
|
||||||
// First check - get metadata
|
// First check - get metadata
|
||||||
const res = await api.post(
|
const res = await api.post(
|
||||||
"/device-web-auth/verify?forceLogin=true",
|
"/device-web-auth/verify?forceLogin=true",
|
||||||
@@ -93,8 +96,6 @@ export default function DeviceLoginForm({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500)); // artificial delay for better UX
|
|
||||||
|
|
||||||
if (res.data.success && res.data.data.metadata) {
|
if (res.data.success && res.data.data.metadata) {
|
||||||
setMetadata(res.data.data.metadata);
|
setMetadata(res.data.data.metadata);
|
||||||
setCode(data.code.toUpperCase());
|
setCode(data.code.toUpperCase());
|
||||||
@@ -116,14 +117,14 @@ export default function DeviceLoginForm({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
|
|
||||||
// Final verify
|
// Final verify
|
||||||
await api.post("/device-web-auth/verify", {
|
await api.post("/device-web-auth/verify", {
|
||||||
code: code,
|
code: code,
|
||||||
verify: true
|
verify: true
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500)); // artificial delay for better UX
|
|
||||||
|
|
||||||
// Redirect to success page
|
// Redirect to success page
|
||||||
router.push("/auth/login/device/success");
|
router.push("/auth/login/device/success");
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
Reference in New Issue
Block a user