fix: harden v2 custom widgets and Workshop (#6480)
This commit is contained in:
1 parent
f77c47b94b
commit
400127b45c
138 files changed
+5134
-1977
No files matched your search
@@ -4,25 +4,23 @@ import { NextResponse } from "next/server";
|
||||
import {
|
||||
claimCustomWidgetConfigurationRequest,
|
||||
completeCustomWidgetConfigurationRequest,
|
||||
configureCustomWidgetSourceFromRequest,
|
||||
configurePreviewSessionSource,
|
||||
getCustomWidgetConfigurationRequest,
|
||||
parseStoredCustomWidgetDefinition,
|
||||
releaseCustomWidgetConfigurationRequest,
|
||||
serializeCustomWidgetDefinition,
|
||||
} from "@homarr/api/custom-widget-configuration";
|
||||
import { encryptSecret } from "@homarr/common/server";
|
||||
import { customWidgetSourceSchema } from "@homarr/custom-widgets/core";
|
||||
import { invalidateCustomWidgetResponseCache } from "@homarr/custom-widgets/server";
|
||||
import { customWidgetSourceSchema, hasSameCustomWidgetSourceAuthentication } from "@homarr/custom-widgets/core";
|
||||
import { and, db, eq, handleTransactionsAsync } from "@homarr/db";
|
||||
import { customWidgetDefinitions, customWidgetSecrets } from "@homarr/db/schema";
|
||||
import { db } from "@homarr/db";
|
||||
|
||||
import { adminRoute } from "../../admin";
|
||||
import { readConfigurationRequestBody } from "../body";
|
||||
|
||||
interface RouteContext {
|
||||
params: Promise<{ token: string }>;
|
||||
}
|
||||
|
||||
export async function GET(_request: NextRequest, context: RouteContext) {
|
||||
const getConfigurationRequest = async (_request: NextRequest, context: RouteContext): Promise<Response> => {
|
||||
const { token } = await context.params;
|
||||
const request = await getCustomWidgetConfigurationRequest(token);
|
||||
if (!request)
|
||||
@@ -38,9 +36,9 @@ export async function GET(_request: NextRequest, context: RouteContext) {
|
||||
},
|
||||
{ headers: { "Cache-Control": "no-store" } },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export async function POST(request: NextRequest, context: RouteContext) {
|
||||
const completeConfigurationRequest = async (request: NextRequest, context: RouteContext): Promise<Response> => {
|
||||
const { token } = await context.params;
|
||||
const pending = await getCustomWidgetConfigurationRequest(token);
|
||||
if (!pending || pending.status !== "pending") {
|
||||
@@ -94,19 +92,24 @@ export async function POST(request: NextRequest, context: RouteContext) {
|
||||
);
|
||||
invalidateCustomWidgetResponseCache([`custom-jsx:preview:${claimed.target.id}:`]);
|
||||
} else {
|
||||
const definition = await db.query.customWidgetDefinitions.findFirst({
|
||||
where: eq(customWidgetDefinitions.id, claimed.target.id),
|
||||
const result = await configureCustomWidgetSourceFromRequest(db, {
|
||||
definitionId: claimed.target.id,
|
||||
sourceId: claimed.sourceId,
|
||||
baseUrl: sourceResult.data.baseUrl,
|
||||
networkScope: sourceResult.data.networkScope,
|
||||
secrets,
|
||||
expectedSource: claimed.source,
|
||||
});
|
||||
if (!definition) return NextResponse.json({ error: "The custom widget no longer exists." }, { status: 404 });
|
||||
const parsed = parseStoredCustomWidgetDefinition(definition);
|
||||
const currentSource = parsed.sources[claimed.sourceId];
|
||||
if (!currentSource) {
|
||||
if (result.status === "definition-not-found") {
|
||||
return NextResponse.json({ error: "The custom widget no longer exists." }, { status: 404 });
|
||||
}
|
||||
if (result.status === "source-not-found") {
|
||||
return NextResponse.json(
|
||||
{ error: "This API source changed after the setup link was created. Request a new setup link." },
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
if (!hasSameCustomWidgetSourceAuthentication(currentSource, claimed.source)) {
|
||||
if (result.status === "binding-changed") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "This API source authentication changed after the setup link was created. Request a new setup link.",
|
||||
@@ -114,69 +117,6 @@ export async function POST(request: NextRequest, context: RouteContext) {
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
const configuredSource = customWidgetSourceSchema.parse({
|
||||
...currentSource,
|
||||
baseUrl: sourceResult.data.baseUrl,
|
||||
networkScope: sourceResult.data.networkScope,
|
||||
});
|
||||
const definitionChanges = {
|
||||
...serializeCustomWidgetDefinition({
|
||||
...parsed,
|
||||
sources: { ...parsed.sources, [claimed.sourceId]: configuredSource },
|
||||
}),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
const secretRows = secrets.map((secret) => ({
|
||||
definitionId: claimed.target.id,
|
||||
sourceId: secret.sourceId,
|
||||
kind: secret.kind,
|
||||
encryptedValue: encryptSecret(secret.value),
|
||||
updatedAt: new Date(),
|
||||
}));
|
||||
await handleTransactionsAsync(db, {
|
||||
async handleAsync(database, schema) {
|
||||
await database.transaction(async (transaction) => {
|
||||
await transaction
|
||||
.update(schema.customWidgetDefinitions)
|
||||
.set(definitionChanges)
|
||||
.where(eq(schema.customWidgetDefinitions.id, claimed.target.id));
|
||||
for (const secret of secretRows) {
|
||||
await transaction
|
||||
.delete(schema.customWidgetSecrets)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.customWidgetSecrets.definitionId, claimed.target.id),
|
||||
eq(schema.customWidgetSecrets.sourceId, secret.sourceId),
|
||||
eq(schema.customWidgetSecrets.kind, secret.kind),
|
||||
),
|
||||
);
|
||||
await transaction.insert(schema.customWidgetSecrets).values(secret);
|
||||
}
|
||||
});
|
||||
},
|
||||
handleSync(database) {
|
||||
database.transaction((transaction) => {
|
||||
transaction
|
||||
.update(customWidgetDefinitions)
|
||||
.set(definitionChanges)
|
||||
.where(eq(customWidgetDefinitions.id, claimed.target.id))
|
||||
.run();
|
||||
for (const secret of secretRows) {
|
||||
transaction
|
||||
.delete(customWidgetSecrets)
|
||||
.where(
|
||||
and(
|
||||
eq(customWidgetSecrets.definitionId, claimed.target.id),
|
||||
eq(customWidgetSecrets.sourceId, secret.sourceId),
|
||||
eq(customWidgetSecrets.kind, secret.kind),
|
||||
),
|
||||
)
|
||||
.run();
|
||||
transaction.insert(customWidgetSecrets).values(secret).run();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await completeCustomWidgetConfigurationRequest(token);
|
||||
@@ -184,4 +124,7 @@ export async function POST(request: NextRequest, context: RouteContext) {
|
||||
} finally {
|
||||
await releaseCustomWidgetConfigurationRequest(token);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const GET = adminRoute(getConfigurationRequest);
|
||||
export const POST = adminRoute(completeConfigurationRequest);
|
||||
Reference in new issue
Block a user