Adjust register endpoint

This commit is contained in:
Owen
2026-03-24 18:26:10 -07:00
parent 6fe9494df4
commit 0b5b6ed5a3
5 changed files with 42 additions and 148 deletions

View File

@@ -105,12 +105,7 @@ authenticated.put(
site.createSite site.createSite
); );
authenticated.put(
"/org/:orgId/newt/provisioning-key",
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.createSite),
newt.createNewtProvisioningKey
);
authenticated.get( authenticated.get(
"/org/:orgId/sites", "/org/:orgId/sites",
verifyOrgAccess, verifyOrgAccess,

View File

@@ -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<typeof bodySchema>;
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<any> {
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<CreateNewtProvisioningKeyResponse>(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")
);
}
}

View File

@@ -10,4 +10,3 @@ export * from "./handleNewtPingMessage";
export * from "./handleNewtDisconnectingMessage"; export * from "./handleNewtDisconnectingMessage";
export * from "./handleConnectionLogMessage"; export * from "./handleConnectionLogMessage";
export * from "./registerNewt"; export * from "./registerNewt";
export * from "./createNewtProvisioningKey";

View File

@@ -2,7 +2,8 @@ import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { import {
newtProvisioningKeys, siteProvisioningKeys,
siteProvisioningKeyOrg,
newts, newts,
orgs, orgs,
roles, roles,
@@ -55,7 +56,7 @@ export async function registerNewt(
const { provisioningKey } = parsedBody.data; const { provisioningKey } = parsedBody.data;
// Keys are in the format "id.secret" // Keys are in the format "siteProvisioningKeyId.secret"
const dotIndex = provisioningKey.indexOf("."); const dotIndex = provisioningKey.indexOf(".");
if (dotIndex === -1) { if (dotIndex === -1) {
return next( return next(
@@ -69,46 +70,51 @@ export async function registerNewt(
const provisioningKeyId = provisioningKey.substring(0, dotIndex); const provisioningKeyId = provisioningKey.substring(0, dotIndex);
const provisioningKeySecret = provisioningKey.substring(dotIndex + 1); 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 const [keyRecord] = await db
.select() .select({
.from(newtProvisioningKeys) siteProvisioningKeyId:
siteProvisioningKeys.siteProvisioningKeyId,
siteProvisioningKeyHash:
siteProvisioningKeys.siteProvisioningKeyHash,
orgId: siteProvisioningKeyOrg.orgId
})
.from(siteProvisioningKeys)
.innerJoin(
siteProvisioningKeyOrg,
eq(
siteProvisioningKeys.siteProvisioningKeyId,
siteProvisioningKeyOrg.siteProvisioningKeyId
)
)
.where( .where(
eq(newtProvisioningKeys.provisioningKeyId, provisioningKeyId) eq(
siteProvisioningKeys.siteProvisioningKeyId,
provisioningKeyId
)
) )
.limit(1); .limit(1);
if (!keyRecord) { 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( return next(
createHttpError( createHttpError(
HttpCode.CONFLICT, HttpCode.UNAUTHORIZED,
"Provisioning key has already been used" "Invalid provisioning key"
) )
); );
} }
// Check expiry // Verify the secret portion against the stored hash
if (keyRecord.expiresAt !== null && keyRecord.expiresAt < Date.now()) { const validSecret = await verifyPassword(
provisioningKeySecret,
keyRecord.siteProvisioningKeyHash
);
if (!validSecret) {
return next( 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() dateCreated: moment().toISOString()
}); });
// Mark the provisioning key as used // Consume the provisioning key — cascade removes siteProvisioningKeyOrg
await trx await trx
.update(newtProvisioningKeys) .delete(siteProvisioningKeys)
.set({ siteId: newSite.siteId })
.where( .where(
eq( eq(
newtProvisioningKeys.provisioningKeyId, siteProvisioningKeys.siteProvisioningKeyId,
provisioningKeyId provisioningKeyId
) )
); );

View File

@@ -28,6 +28,7 @@ export type CreateSiteProvisioningKeyResponse = {
orgId: string; orgId: string;
name: string; name: string;
siteProvisioningKey: string; siteProvisioningKey: string;
provisioningKey: string; // combined "siteProvisioningKeyId.siteProvisioningKey" — put this in your newt config
lastChars: string; lastChars: string;
createdAt: string; createdAt: string;
}; };
@@ -65,6 +66,7 @@ export async function createSiteProvisioningKey(
const siteProvisioningKeyHash = await hashPassword(siteProvisioningKey); const siteProvisioningKeyHash = await hashPassword(siteProvisioningKey);
const lastChars = siteProvisioningKey.slice(-4); const lastChars = siteProvisioningKey.slice(-4);
const createdAt = moment().toISOString(); const createdAt = moment().toISOString();
const provisioningKey = `${siteProvisioningKeyId}.${siteProvisioningKey}`;
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
await trx.insert(siteProvisioningKeys).values({ await trx.insert(siteProvisioningKeys).values({
@@ -88,6 +90,7 @@ export async function createSiteProvisioningKey(
orgId, orgId,
name, name,
siteProvisioningKey, siteProvisioningKey,
provisioningKey,
lastChars, lastChars,
createdAt createdAt
}, },