diff --git a/server/routers/external.ts b/server/routers/external.ts index a658fa811..297cb2894 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -105,12 +105,7 @@ authenticated.put( site.createSite ); -authenticated.put( - "/org/:orgId/newt/provisioning-key", - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.createSite), - newt.createNewtProvisioningKey -); + authenticated.get( "/org/:orgId/sites", verifyOrgAccess, diff --git a/server/routers/newt/createNewtProvisioningKey.ts b/server/routers/newt/createNewtProvisioningKey.ts deleted file mode 100644 index 2af4d166d..000000000 --- a/server/routers/newt/createNewtProvisioningKey.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { newtProvisioningKeys, orgs } from "@server/db"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { eq } from "drizzle-orm"; -import { fromError } from "zod-validation-error"; -import { - generateId, - generateIdFromEntropySize -} from "@server/auth/sessions/app"; -import { hashPassword } from "@server/auth/password"; -import moment from "moment"; - -const paramsSchema = z.object({ - orgId: z.string().nonempty() -}); - -const bodySchema = z.object({ - expiresAt: z.number().int().positive().optional() // optional Unix timestamp (ms) -}); - -export type CreateNewtProvisioningKeyBody = z.infer; - -export type CreateNewtProvisioningKeyResponse = { - provisioningKeyId: string; - provisioningKey: string; // returned only once: "id.secret" - lastChars: string; - createdAt: string; - expiresAt: number | null; -}; - -export async function createNewtProvisioningKey( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = paramsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const parsedBody = bodySchema.safeParse(req.body); - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { orgId } = parsedParams.data; - const { expiresAt } = parsedBody.data; - - // Verify org exists - const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId)); - if (!org) { - return next( - createHttpError(HttpCode.NOT_FOUND, `Organization with ID ${orgId} not found`) - ); - } - - const provisioningKeyId = generateId(15); - const secret = generateIdFromEntropySize(25); - const keyHash = await hashPassword(secret); - const lastChars = secret.slice(-4); - const createdAt = moment().toISOString(); - const provisioningKey = `${provisioningKeyId}.${secret}`; - - await db.insert(newtProvisioningKeys).values({ - provisioningKeyId, - orgId, - keyHash, - lastChars, - createdAt, - expiresAt: expiresAt ?? null - }); - - return response(res, { - data: { - provisioningKeyId, - provisioningKey, - lastChars, - createdAt, - expiresAt: expiresAt ?? null - }, - success: true, - error: false, - message: "Provisioning key created successfully", - status: HttpCode.CREATED - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/newt/index.ts b/server/routers/newt/index.ts index 76207e8d4..33b5caf7c 100644 --- a/server/routers/newt/index.ts +++ b/server/routers/newt/index.ts @@ -10,4 +10,3 @@ export * from "./handleNewtPingMessage"; export * from "./handleNewtDisconnectingMessage"; export * from "./handleConnectionLogMessage"; export * from "./registerNewt"; -export * from "./createNewtProvisioningKey"; diff --git a/server/routers/newt/registerNewt.ts b/server/routers/newt/registerNewt.ts index f301b452a..b999eb35c 100644 --- a/server/routers/newt/registerNewt.ts +++ b/server/routers/newt/registerNewt.ts @@ -2,7 +2,8 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; import { - newtProvisioningKeys, + siteProvisioningKeys, + siteProvisioningKeyOrg, newts, orgs, roles, @@ -55,7 +56,7 @@ export async function registerNewt( const { provisioningKey } = parsedBody.data; - // Keys are in the format "id.secret" + // Keys are in the format "siteProvisioningKeyId.secret" const dotIndex = provisioningKey.indexOf("."); if (dotIndex === -1) { return next( @@ -69,46 +70,51 @@ export async function registerNewt( const provisioningKeyId = provisioningKey.substring(0, dotIndex); const provisioningKeySecret = provisioningKey.substring(dotIndex + 1); - // Look up the provisioning key by ID + // Look up the provisioning key by ID, joining to get the orgId const [keyRecord] = await db - .select() - .from(newtProvisioningKeys) + .select({ + siteProvisioningKeyId: + siteProvisioningKeys.siteProvisioningKeyId, + siteProvisioningKeyHash: + siteProvisioningKeys.siteProvisioningKeyHash, + orgId: siteProvisioningKeyOrg.orgId + }) + .from(siteProvisioningKeys) + .innerJoin( + siteProvisioningKeyOrg, + eq( + siteProvisioningKeys.siteProvisioningKeyId, + siteProvisioningKeyOrg.siteProvisioningKeyId + ) + ) .where( - eq(newtProvisioningKeys.provisioningKeyId, provisioningKeyId) + eq( + siteProvisioningKeys.siteProvisioningKeyId, + provisioningKeyId + ) ) .limit(1); if (!keyRecord) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Invalid provisioning key") - ); - } - - // Verify the secret - const validSecret = await verifyPassword( - provisioningKeySecret, - keyRecord.keyHash - ); - if (!validSecret) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Invalid provisioning key") - ); - } - - // Check if key has already been used - if (keyRecord.siteId !== null) { return next( createHttpError( - HttpCode.CONFLICT, - "Provisioning key has already been used" + HttpCode.UNAUTHORIZED, + "Invalid provisioning key" ) ); } - // Check expiry - if (keyRecord.expiresAt !== null && keyRecord.expiresAt < Date.now()) { + // Verify the secret portion against the stored hash + const validSecret = await verifyPassword( + provisioningKeySecret, + keyRecord.siteProvisioningKeyHash + ); + if (!validSecret) { return next( - createHttpError(HttpCode.GONE, "Provisioning key has expired") + createHttpError( + HttpCode.UNAUTHORIZED, + "Invalid provisioning key" + ) ); } @@ -200,13 +206,12 @@ export async function registerNewt( dateCreated: moment().toISOString() }); - // Mark the provisioning key as used + // Consume the provisioning key — cascade removes siteProvisioningKeyOrg await trx - .update(newtProvisioningKeys) - .set({ siteId: newSite.siteId }) + .delete(siteProvisioningKeys) .where( eq( - newtProvisioningKeys.provisioningKeyId, + siteProvisioningKeys.siteProvisioningKeyId, provisioningKeyId ) ); diff --git a/server/routers/siteProvisioning/createSiteProvisioningKey.ts b/server/routers/siteProvisioning/createSiteProvisioningKey.ts index 9bb298966..a10df65a6 100644 --- a/server/routers/siteProvisioning/createSiteProvisioningKey.ts +++ b/server/routers/siteProvisioning/createSiteProvisioningKey.ts @@ -28,6 +28,7 @@ export type CreateSiteProvisioningKeyResponse = { orgId: string; name: string; siteProvisioningKey: string; + provisioningKey: string; // combined "siteProvisioningKeyId.siteProvisioningKey" — put this in your newt config lastChars: string; createdAt: string; }; @@ -65,6 +66,7 @@ export async function createSiteProvisioningKey( const siteProvisioningKeyHash = await hashPassword(siteProvisioningKey); const lastChars = siteProvisioningKey.slice(-4); const createdAt = moment().toISOString(); + const provisioningKey = `${siteProvisioningKeyId}.${siteProvisioningKey}`; await db.transaction(async (trx) => { await trx.insert(siteProvisioningKeys).values({ @@ -88,6 +90,7 @@ export async function createSiteProvisioningKey( orgId, name, siteProvisioningKey, + provisioningKey, lastChars, createdAt },