From 2c488baa80b25d311512601f402b808a977dd088 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 3 Nov 2025 16:16:19 -0800 Subject: [PATCH] Add name and lock client to specific olm --- server/auth/actions.ts | 1 + server/db/pg/schema/schema.ts | 2 + server/db/sqlite/schema/schema.ts | 3 + server/routers/client/createClient.ts | 12 +- server/routers/external.ts | 14 ++- server/routers/integration.ts | 7 ++ server/routers/olm/createOlm.ts | 118 +++++++++--------- .../routers/olm/handleOlmRegisterMessage.ts | 20 ++- 8 files changed, 95 insertions(+), 82 deletions(-) diff --git a/server/auth/actions.ts b/server/auth/actions.ts index d08457e5..97bee93d 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -85,6 +85,7 @@ export enum ActionsEnum { updateOrgDomain = "updateOrgDomain", getDNSRecords = "getDNSRecords", createNewt = "createNewt", + createOlm = "createOlm", createIdp = "createIdp", updateIdp = "updateIdp", deleteIdp = "deleteIdp", diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 7db4af30..be1ffcb9 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -611,6 +611,7 @@ export const clients = pgTable("clients", { // optionally tied to a user and in this case delete when the user deletes onDelete: "cascade" }), + olmId: text("olmId"), // to lock it to a specific olm optionally name: varchar("name").notNull(), pubKey: varchar("pubKey"), subnet: varchar("subnet").notNull(), @@ -641,6 +642,7 @@ export const olms = pgTable("olms", { secretHash: varchar("secretHash").notNull(), dateCreated: varchar("dateCreated").notNull(), version: text("version"), + name: varchar("name"), clientId: integer("clientId").references(() => clients.clientId, { // we will switch this depending on the current org it wants to connect to onDelete: "set null" diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 63a50154..a5c00350 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -319,8 +319,10 @@ export const clients = sqliteTable("clients", { // optionally tied to a user and in this case delete when the user deletes onDelete: "cascade" }), + name: text("name").notNull(), pubKey: text("pubKey"), + olmId: text("olmId"), // to lock it to a specific olm optionally subnet: text("subnet").notNull(), megabytesIn: integer("bytesIn"), megabytesOut: integer("bytesOut"), @@ -350,6 +352,7 @@ export const olms = sqliteTable("olms", { secretHash: text("secretHash").notNull(), dateCreated: text("dateCreated").notNull(), version: text("version"), + name: text("name"), clientId: integer("clientId").references(() => clients.clientId, { // we will switch this depending on the current org it wants to connect to onDelete: "set null" diff --git a/server/routers/client/createClient.ts b/server/routers/client/createClient.ts index 90445925..50b81184 100644 --- a/server/routers/client/createClient.ts +++ b/server/routers/client/createClient.ts @@ -201,7 +201,8 @@ export async function createClient( orgId, name, subnet: updatedSubnet, - type + type, + olmId // this is to lock it to a specific olm even if the olm moves across clients }) .returning(); @@ -228,15 +229,6 @@ export async function createClient( ); } - const secretHash = await hashPassword(secret); - - await trx.insert(olms).values({ - olmId, - secretHash, - clientId: newClient.clientId, - dateCreated: moment().toISOString() - }); - return response(res, { data: newClient, success: true, diff --git a/server/routers/external.ts b/server/routers/external.ts index 5c235902..b2de8eed 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -16,6 +16,8 @@ import * as idp from "./idp"; import * as blueprints from "./blueprints"; import * as apiKeys from "./apiKeys"; import * as logs from "./auditLogs"; +import * as newt from "./newt"; +import * as olm from "./olm"; import HttpCode from "@server/types/HttpCode"; import { verifyAccessTokenAccess, @@ -40,8 +42,6 @@ import { verifySiteResourceAccess } from "@server/middlewares"; import { ActionsEnum } from "@server/auth/actions"; -import { createNewt, getNewtToken } from "./newt"; -import { getOlmToken } from "./olm"; import rateLimit, { ipKeyGenerator } from "express-rate-limit"; import createHttpError from "http-errors"; import { build } from "@server/build"; @@ -726,6 +726,12 @@ authenticated.delete( // createNewt // ); +authenticated.put( + "/olm", + verifyUserHasAction(ActionsEnum.createOlm), + olm.createOlm +); + authenticated.put( "/idp/oidc", verifyUserIsServerAdmin, @@ -978,7 +984,7 @@ authRouter.post( }, store: createStore() }), - getNewtToken + newt.getNewtToken ); authRouter.post( "/olm/get-token", @@ -993,7 +999,7 @@ authRouter.post( }, store: createStore() }), - getOlmToken + olm.getOlmToken ); authRouter.post( diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 7164c1de..88a8c05e 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -11,6 +11,7 @@ import * as accessToken from "./accessToken"; import * as apiKeys from "./apiKeys"; import * as idp from "./idp"; import * as siteResource from "./siteResource"; +import * as olm from "./olm"; import { verifyApiKey, verifyApiKeyOrgAccess, @@ -556,6 +557,12 @@ authenticated.delete( // newt.createNewt // ); +authenticated.put( + "/olm", + verifyApiKeyHasAction(ActionsEnum.createOlm), + olm.createOlm +); + authenticated.get( `/org/:orgId/api-keys`, verifyApiKeyIsRoot, diff --git a/server/routers/olm/createOlm.ts b/server/routers/olm/createOlm.ts index 3066e4ea..b9cbafa3 100644 --- a/server/routers/olm/createOlm.ts +++ b/server/routers/olm/createOlm.ts @@ -3,41 +3,40 @@ import { db } from "@server/db"; import { hash } from "@node-rs/argon2"; import HttpCode from "@server/types/HttpCode"; import { z } from "zod"; -import { newts } from "@server/db"; +import { olms } from "@server/db"; import createHttpError from "http-errors"; import response from "@server/lib/response"; import { SqliteError } from "better-sqlite3"; import moment from "moment"; -import { generateSessionToken } from "@server/auth/sessions/app"; -import { createNewtSession } from "@server/auth/sessions/newt"; +import { generateId, generateSessionToken } from "@server/auth/sessions/app"; +import { createOlmSession } from "@server/auth/sessions/olm"; import { fromError } from "zod-validation-error"; import { hashPassword } from "@server/auth/password"; -export const createNewtBodySchema = z.object({}); +export const createOlmBodySchema = z.object({}); -export type CreateNewtBody = z.infer; +export type CreateOlmBody = z.infer; -export type CreateNewtResponse = { - token: string; - newtId: string; +export type CreateOlmResponse = { + // token: string; + olmId: string; secret: string; }; -const createNewtSchema = z +const createOlmSchema = z .object({ - newtId: z.string(), - secret: z.string() + userId: z.string().optional(), + name: z.string().min(1).max(255) }) .strict(); -export async function createNewt( +export async function createOlm( req: Request, res: Response, next: NextFunction ): Promise { try { - - const parsedBody = createNewtSchema.safeParse(req.body); + const parsedBody = createOlmSchema.safeParse(req.body); if (!parsedBody.success) { return next( createHttpError( @@ -47,60 +46,55 @@ export async function createNewt( ); } - const { newtId, secret } = parsedBody.data; + const { userId, name } = parsedBody.data; + let userIdFinal = userId; - if (req.user && !req.userOrgRoleId) { - return next( - createHttpError(HttpCode.FORBIDDEN, "User does not have a role") - ); - } - - const secretHash = await hashPassword(secret); - - await db.insert(newts).values({ - newtId: newtId, - secretHash, - dateCreated: moment().toISOString(), - }); - - // give the newt their default permissions: - // await db.insert(newtActions).values({ - // newtId: newtId, - // actionId: ActionsEnum.createOrg, - // orgId: null, - // }); - - const token = generateSessionToken(); - await createNewtSession(token, newtId); - - return response(res, { - data: { - newtId, - secret, - token, - }, - success: true, - error: false, - message: "Newt created successfully", - status: HttpCode.OK, - }); - } catch (e) { - if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") { + if (req.user) { // overwrite the user with the one calling because we want to assign the olm to the user creating it + userIdFinal = req.user.userId; + } else if (!userIdFinal) { return next( createHttpError( HttpCode.BAD_REQUEST, - "A newt with that email address already exists" - ) - ); - } else { - console.error(e); - - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to create newt" + "Either userId must be provided or request must be authenticated" ) ); } + + const olmId = generateId(15); + const secret = generateId(48); + + const secretHash = await hashPassword(secret); + + await db.insert(olms).values({ + olmId: olmId, + userId: userId, + name, + secretHash, + dateCreated: moment().toISOString() + }); + + // const token = generateSessionToken(); + // await createOlmSession(token, olmId); + + return response(res, { + data: { + olmId, + secret + // token, + }, + success: true, + error: false, + message: "Olm created successfully", + status: HttpCode.OK + }); + } catch (e) { + console.error(e); + + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to create olm" + ) + ); } } diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index e9d0ab6f..e2e25875 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -30,7 +30,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { return; } - const { publicKey, relay, olmVersion, orgId, deviceName } = message.data; + const { publicKey, relay, olmVersion, orgId } = message.data; let client: Client; if (orgId) { @@ -40,7 +40,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { } try { - client = await getOrCreateOrgClient(orgId, olm.userId, deviceName); + client = await getOrCreateOrgClient(orgId, olm.userId, olm.olmId, olm.name || "User Device"); } catch (err) { logger.error( `Error switching olm client ${olm.olmId} to org ${orgId}: ${err}` @@ -293,7 +293,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { async function getOrCreateOrgClient( orgId: string, userId: string, - deviceName?: string, + olmId: string, + name: string, trx: Transaction | typeof db = db ): Promise { let client: Client; @@ -328,7 +329,13 @@ async function getOrCreateOrgClient( const [existingClient] = await trx .select() .from(clients) - .where(and(eq(clients.orgId, orgId), eq(clients.userId, userId))) + .where( + and( + eq(clients.orgId, orgId), + eq(clients.userId, userId), + eq(clients.olmId, olmId) + ) + ) // checking the olmid here because we want to create a new client PER OLM PER ORG .limit(1); if (!existingClient) { @@ -364,10 +371,11 @@ async function getOrCreateOrgClient( .values({ exitNodeId: randomExitNode.exitNodeId, orgId, - name: deviceName || "User Device", + name, subnet: updatedSubnet, type: "olm", - userId: userId + userId: userId, + olmId: olmId // to lock this client to the olm even as the olm moves between clients in different orgs }) .returning();