Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5d15353fe |
No files matched your search
@@ -75,7 +75,6 @@ import {
|
|||||||
throwIfCustomWidgetBoardDuplicationForbidden,
|
throwIfCustomWidgetBoardDuplicationForbidden,
|
||||||
throwIfCustomWidgetPlacementChangeForbidden,
|
throwIfCustomWidgetPlacementChangeForbidden,
|
||||||
} from "./board/custom-widget-placement-access";
|
} from "./board/custom-widget-placement-access";
|
||||||
import { validateTimetableOptionsChangeAsync } from "./widgets/timetable";
|
|
||||||
|
|
||||||
interface BoardItemPlacementRectangle {
|
interface BoardItemPlacementRectangle {
|
||||||
xOffset: number;
|
xOffset: number;
|
||||||
@@ -1497,6 +1496,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
for (const item of input.items) {
|
for (const item of input.items) {
|
||||||
if (item.kind !== "timetable") continue;
|
if (item.kind !== "timetable") continue;
|
||||||
const previousItem = dbBoard.items.find((dbItem) => dbItem.id === item.id);
|
const previousItem = dbBoard.items.find((dbItem) => dbItem.id === item.id);
|
||||||
|
const { validateTimetableOptionsChangeAsync } = await import("./timetable-options-validation");
|
||||||
await validateTimetableOptionsChangeAsync(
|
await validateTimetableOptionsChangeAsync(
|
||||||
item.options,
|
item.options,
|
||||||
previousItem?.kind === "timetable" ? previousItem.options : undefined,
|
previousItem?.kind === "timetable" ? previousItem.options : undefined,
|
||||||
@@ -2073,6 +2073,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (input.kind === "timetable") {
|
if (input.kind === "timetable") {
|
||||||
|
const { validateTimetableOptionsChangeAsync } = await import("./timetable-options-validation");
|
||||||
await validateTimetableOptionsChangeAsync(input.options);
|
await validateTimetableOptionsChangeAsync(input.options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import { lookup } from "node:dns/promises";
|
||||||
|
import { BlockList, isIP } from "node:net";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { parse } from "superjson";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import type { TimetableResolvedAddress } from "@homarr/request-handler/timetable";
|
||||||
|
import { DEFAULT_TIMETABLE_BASE_URL, normalizeTimetableBaseUrl } from "@homarr/request-handler/timetable-url";
|
||||||
|
|
||||||
|
const timetableOptionsSchema = z.object({ baseUrl: z.string().optional() }).passthrough();
|
||||||
|
|
||||||
|
const blockedTimetableIpv4Addresses = new BlockList();
|
||||||
|
const blockedTimetableIpv4Subnets = [
|
||||||
|
["0.0.0.0", 8],
|
||||||
|
["10.0.0.0", 8],
|
||||||
|
["100.64.0.0", 10],
|
||||||
|
["127.0.0.0", 8],
|
||||||
|
["169.254.0.0", 16],
|
||||||
|
["172.16.0.0", 12],
|
||||||
|
["192.0.0.0", 24],
|
||||||
|
["192.0.2.0", 24],
|
||||||
|
["192.88.99.0", 24],
|
||||||
|
["192.168.0.0", 16],
|
||||||
|
["198.18.0.0", 15],
|
||||||
|
["198.51.100.0", 24],
|
||||||
|
["203.0.113.0", 24],
|
||||||
|
["224.0.0.0", 4],
|
||||||
|
["240.0.0.0", 4],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const blockedTimetableIpv6Addresses = new BlockList();
|
||||||
|
const blockedTimetableIpv6Subnets = [
|
||||||
|
["::", 96],
|
||||||
|
["::ffff:0:0", 96],
|
||||||
|
["::ffff:0:0:0", 96],
|
||||||
|
["64:ff9b::", 96],
|
||||||
|
["64:ff9b:1::", 48],
|
||||||
|
["100::", 64],
|
||||||
|
["2001::", 32],
|
||||||
|
["2001:2::", 48],
|
||||||
|
["2001:10::", 28],
|
||||||
|
["2001:db8::", 32],
|
||||||
|
["2002::", 16],
|
||||||
|
["fc00::", 7],
|
||||||
|
["fe80::", 10],
|
||||||
|
["fec0::", 10],
|
||||||
|
["ff00::", 8],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const [network, prefix] of blockedTimetableIpv4Subnets) {
|
||||||
|
blockedTimetableIpv4Addresses.addSubnet(network, prefix, "ipv4");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [network, prefix] of blockedTimetableIpv6Subnets) {
|
||||||
|
blockedTimetableIpv6Addresses.addSubnet(network, prefix, "ipv6");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const normalizeTimetableBaseUrlOrThrowBadRequest = (baseUrl: string) => {
|
||||||
|
try {
|
||||||
|
return normalizeTimetableBaseUrl(baseUrl);
|
||||||
|
} catch (cause) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: cause instanceof Error ? cause.message : "Invalid timetable base URL",
|
||||||
|
cause,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSavedTimetableBaseUrl = (serializedOptions: string) => {
|
||||||
|
try {
|
||||||
|
const options = timetableOptionsSchema.parse(parse<unknown>(serializedOptions));
|
||||||
|
return normalizeTimetableBaseUrl(options.baseUrl ?? DEFAULT_TIMETABLE_BASE_URL);
|
||||||
|
} catch (cause) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Timetable widget configuration is invalid",
|
||||||
|
cause,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeHostname = (hostname: string) => {
|
||||||
|
let withoutBrackets = hostname;
|
||||||
|
if (hostname.startsWith("[") && hostname.endsWith("]")) withoutBrackets = hostname.slice(1, -1);
|
||||||
|
return withoutBrackets.toLowerCase().replace(/\.$/, "");
|
||||||
|
};
|
||||||
|
|
||||||
|
const isBlockedTimetableAddress = (address: string, family: number) => {
|
||||||
|
if (family === 6) return blockedTimetableIpv6Addresses.check(address, "ipv6");
|
||||||
|
return blockedTimetableIpv4Addresses.check(address, "ipv4");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolvePublicTimetableAddressesAsync = async (baseUrl: string): Promise<TimetableResolvedAddress[]> => {
|
||||||
|
const hostname = normalizeHostname(new URL(baseUrl).hostname);
|
||||||
|
const hostnameFamily = isIP(hostname);
|
||||||
|
const isInternalHostname =
|
||||||
|
hostnameFamily === 0 &&
|
||||||
|
(!hostname.includes(".") ||
|
||||||
|
hostname === "localhost" ||
|
||||||
|
hostname.endsWith(".localhost") ||
|
||||||
|
hostname.endsWith(".local") ||
|
||||||
|
hostname.endsWith(".internal") ||
|
||||||
|
hostname.endsWith(".lan") ||
|
||||||
|
hostname.endsWith(".home.arpa"));
|
||||||
|
|
||||||
|
if (isInternalHostname) {
|
||||||
|
throw new TRPCError({ code: "BAD_REQUEST", message: "Timetable base URL must use a public host" });
|
||||||
|
}
|
||||||
|
|
||||||
|
let addresses: TimetableResolvedAddress[];
|
||||||
|
try {
|
||||||
|
let resolvedAddresses = [{ address: hostname, family: hostnameFamily }];
|
||||||
|
if (hostnameFamily === 0) resolvedAddresses = await lookup(hostname, { all: true, verbatim: true });
|
||||||
|
addresses = resolvedAddresses.filter(
|
||||||
|
(entry): entry is TimetableResolvedAddress => entry.family === 4 || entry.family === 6,
|
||||||
|
);
|
||||||
|
} catch (cause) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Timetable base URL host could not be resolved",
|
||||||
|
cause,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addresses.length === 0 || addresses.some(({ address, family }) => isBlockedTimetableAddress(address, family))) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Timetable base URL must not target a private or internal address",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return addresses.toSorted((left, right) => left.family - right.family || left.address.localeCompare(right.address));
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeTimetableOptionsBaseUrlOrThrowBadRequest = (options: Record<string, unknown>) => {
|
||||||
|
const parsedOptions = timetableOptionsSchema.safeParse(options);
|
||||||
|
if (!parsedOptions.success) {
|
||||||
|
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid timetable widget options" });
|
||||||
|
}
|
||||||
|
return normalizeTimetableBaseUrlOrThrowBadRequest(parsedOptions.data.baseUrl ?? DEFAULT_TIMETABLE_BASE_URL);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateTimetableOptionsChangeAsync = async (
|
||||||
|
nextOptions: Record<string, unknown>,
|
||||||
|
previousOptions?: Record<string, unknown>,
|
||||||
|
) => {
|
||||||
|
const nextBaseUrl = normalizeTimetableOptionsBaseUrlOrThrowBadRequest(nextOptions);
|
||||||
|
if (nextBaseUrl === DEFAULT_TIMETABLE_BASE_URL) return;
|
||||||
|
|
||||||
|
let previousBaseUrl: string | undefined;
|
||||||
|
if (previousOptions) {
|
||||||
|
try {
|
||||||
|
previousBaseUrl = normalizeTimetableOptionsBaseUrlOrThrowBadRequest(previousOptions);
|
||||||
|
} catch {
|
||||||
|
// Invalid legacy options must not prevent replacing them with a valid configuration.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nextBaseUrl === previousBaseUrl) return;
|
||||||
|
|
||||||
|
await resolvePublicTimetableAddressesAsync(nextBaseUrl);
|
||||||
|
};
|
||||||
@@ -9,7 +9,6 @@ import { boards, items } from "@homarr/db/schema";
|
|||||||
import type { WidgetOptionsSettings } from "../../../../widgets/src";
|
import type { WidgetOptionsSettings } from "../../../../widgets/src";
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
|
||||||
import { throwIfActionForbiddenAsync } from "../board/board-access";
|
import { throwIfActionForbiddenAsync } from "../board/board-access";
|
||||||
import { validateTimetableOptionsChangeAsync } from "./timetable";
|
|
||||||
import { throwIfCustomWidgetPlacementChangeForbidden } from "../board/custom-widget-placement-access";
|
import { throwIfCustomWidgetPlacementChangeForbidden } from "../board/custom-widget-placement-access";
|
||||||
|
|
||||||
export const optionsRouter = createTRPCRouter({
|
export const optionsRouter = createTRPCRouter({
|
||||||
@@ -50,6 +49,7 @@ export const optionsRouter = createTRPCRouter({
|
|||||||
const previousOptions = SuperJSON.parse<Record<string, unknown>>(item.options);
|
const previousOptions = SuperJSON.parse<Record<string, unknown>>(item.options);
|
||||||
const updatedOptions = { ...previousOptions, ...input.newOptions };
|
const updatedOptions = { ...previousOptions, ...input.newOptions };
|
||||||
if (item.kind === "timetable") {
|
if (item.kind === "timetable") {
|
||||||
|
const { validateTimetableOptionsChangeAsync } = await import("../timetable-options-validation");
|
||||||
await validateTimetableOptionsChangeAsync(updatedOptions, previousOptions);
|
await validateTimetableOptionsChangeAsync(updatedOptions, previousOptions);
|
||||||
}
|
}
|
||||||
throwIfCustomWidgetPlacementChangeForbidden({
|
throwIfCustomWidgetPlacementChangeForbidden({
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import { lookup } from "node:dns/promises";
|
|
||||||
import { BlockList, isIP } from "node:net";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { parse } from "superjson";
|
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
import type { Session } from "@homarr/auth";
|
import type { Session } from "@homarr/auth";
|
||||||
@@ -13,10 +10,15 @@ import {
|
|||||||
timetableGetTimetableRequestHandler,
|
timetableGetTimetableRequestHandler,
|
||||||
timetableSearchStationsRequestHandler,
|
timetableSearchStationsRequestHandler,
|
||||||
} from "@homarr/request-handler/timetable";
|
} from "@homarr/request-handler/timetable";
|
||||||
import { DEFAULT_TIMETABLE_BASE_URL, normalizeTimetableBaseUrl } from "@homarr/request-handler/timetable-url";
|
import { DEFAULT_TIMETABLE_BASE_URL } from "@homarr/request-handler/timetable-url";
|
||||||
|
|
||||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||||
import { throwIfActionForbiddenAsync } from "../board/board-access";
|
import { throwIfActionForbiddenAsync } from "../board/board-access";
|
||||||
|
import {
|
||||||
|
getSavedTimetableBaseUrl,
|
||||||
|
normalizeTimetableBaseUrlOrThrowBadRequest,
|
||||||
|
resolvePublicTimetableAddressesAsync,
|
||||||
|
} from "../timetable-options-validation";
|
||||||
|
|
||||||
const baseUrlSchema = z.string().url().max(2_048);
|
const baseUrlSchema = z.string().url().max(2_048);
|
||||||
const timetableSourceSchema = z.object({
|
const timetableSourceSchema = z.object({
|
||||||
@@ -24,172 +26,6 @@ const timetableSourceSchema = z.object({
|
|||||||
itemId: z.string().max(100).optional(),
|
itemId: z.string().max(100).optional(),
|
||||||
boardId: z.string().max(100).optional(),
|
boardId: z.string().max(100).optional(),
|
||||||
});
|
});
|
||||||
const timetableOptionsSchema = z.object({ baseUrl: z.string().optional() }).passthrough();
|
|
||||||
|
|
||||||
const blockedTimetableIpv4Addresses = new BlockList();
|
|
||||||
const blockedTimetableIpv6Addresses = new BlockList();
|
|
||||||
const blockedTimetableSubnets = [
|
|
||||||
["0.0.0.0", 8, "ipv4"],
|
|
||||||
["10.0.0.0", 8, "ipv4"],
|
|
||||||
["100.64.0.0", 10, "ipv4"],
|
|
||||||
["127.0.0.0", 8, "ipv4"],
|
|
||||||
["169.254.0.0", 16, "ipv4"],
|
|
||||||
["172.16.0.0", 12, "ipv4"],
|
|
||||||
["192.0.0.0", 24, "ipv4"],
|
|
||||||
["192.0.2.0", 24, "ipv4"],
|
|
||||||
["192.88.99.0", 24, "ipv4"],
|
|
||||||
["192.168.0.0", 16, "ipv4"],
|
|
||||||
["198.18.0.0", 15, "ipv4"],
|
|
||||||
["198.51.100.0", 24, "ipv4"],
|
|
||||||
["203.0.113.0", 24, "ipv4"],
|
|
||||||
["224.0.0.0", 4, "ipv4"],
|
|
||||||
["240.0.0.0", 4, "ipv4"],
|
|
||||||
["::", 96, "ipv6"],
|
|
||||||
["::ffff:0:0", 96, "ipv6"],
|
|
||||||
["::ffff:0:0:0", 96, "ipv6"],
|
|
||||||
["64:ff9b::", 96, "ipv6"],
|
|
||||||
["64:ff9b:1::", 48, "ipv6"],
|
|
||||||
["100::", 64, "ipv6"],
|
|
||||||
["2001::", 32, "ipv6"],
|
|
||||||
["2001:2::", 48, "ipv6"],
|
|
||||||
["2001:10::", 28, "ipv6"],
|
|
||||||
["2001:db8::", 32, "ipv6"],
|
|
||||||
["2002::", 16, "ipv6"],
|
|
||||||
["fc00::", 7, "ipv6"],
|
|
||||||
["fe80::", 10, "ipv6"],
|
|
||||||
["fec0::", 10, "ipv6"],
|
|
||||||
["ff00::", 8, "ipv6"],
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
for (const [network, prefix, family] of blockedTimetableSubnets) {
|
|
||||||
const blockList = family === "ipv4" ? blockedTimetableIpv4Addresses : blockedTimetableIpv6Addresses;
|
|
||||||
blockList.addSubnet(network, prefix, family);
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeBaseUrlOrThrowBadRequest = (baseUrl: string) => {
|
|
||||||
try {
|
|
||||||
return normalizeTimetableBaseUrl(baseUrl);
|
|
||||||
} catch (cause) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: cause instanceof Error ? cause.message : "Invalid timetable base URL",
|
|
||||||
cause,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const throwInvalidSavedTimetableConfiguration = (cause: unknown): never => {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
|
||||||
message: "Timetable widget configuration is invalid",
|
|
||||||
cause,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseSavedTimetableOptions = (serializedOptions: string) => {
|
|
||||||
try {
|
|
||||||
return timetableOptionsSchema.parse(parse<unknown>(serializedOptions));
|
|
||||||
} catch (cause) {
|
|
||||||
return throwInvalidSavedTimetableConfiguration(cause);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeSavedTimetableBaseUrl = (baseUrl: string) => {
|
|
||||||
try {
|
|
||||||
return normalizeTimetableBaseUrl(baseUrl);
|
|
||||||
} catch (cause) {
|
|
||||||
return throwInvalidSavedTimetableConfiguration(cause);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeHostname = (hostname: string) => {
|
|
||||||
const withoutBrackets = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
||||||
return withoutBrackets.toLowerCase().replace(/\.$/, "");
|
|
||||||
};
|
|
||||||
|
|
||||||
const isBlockedTimetableAddress = (address: string, family: number) => {
|
|
||||||
return family === 6
|
|
||||||
? blockedTimetableIpv6Addresses.check(address, "ipv6")
|
|
||||||
: blockedTimetableIpv4Addresses.check(address, "ipv4");
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolvePublicTimetableAddressesAsync = async (baseUrl: string): Promise<TimetableResolvedAddress[]> => {
|
|
||||||
const hostname = normalizeHostname(new URL(baseUrl).hostname);
|
|
||||||
const hostnameFamily = isIP(hostname);
|
|
||||||
const isInternalHostname =
|
|
||||||
hostnameFamily === 0 &&
|
|
||||||
(!hostname.includes(".") ||
|
|
||||||
hostname === "localhost" ||
|
|
||||||
hostname.endsWith(".localhost") ||
|
|
||||||
hostname.endsWith(".local") ||
|
|
||||||
hostname.endsWith(".internal") ||
|
|
||||||
hostname.endsWith(".lan") ||
|
|
||||||
hostname.endsWith(".home.arpa"));
|
|
||||||
|
|
||||||
if (isInternalHostname) {
|
|
||||||
throw new TRPCError({ code: "BAD_REQUEST", message: "Timetable base URL must use a public host" });
|
|
||||||
}
|
|
||||||
|
|
||||||
let addresses: TimetableResolvedAddress[];
|
|
||||||
try {
|
|
||||||
const resolvedAddresses =
|
|
||||||
hostnameFamily === 0
|
|
||||||
? await lookup(hostname, { all: true, verbatim: true })
|
|
||||||
: [{ address: hostname, family: hostnameFamily }];
|
|
||||||
addresses = resolvedAddresses.filter(
|
|
||||||
(entry): entry is TimetableResolvedAddress => entry.family === 4 || entry.family === 6,
|
|
||||||
);
|
|
||||||
} catch (cause) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Timetable base URL host could not be resolved",
|
|
||||||
cause,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addresses.length === 0 || addresses.some(({ address, family }) => isBlockedTimetableAddress(address, family))) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Timetable base URL must not target a private or internal address",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return addresses.toSorted((left, right) => left.family - right.family || left.address.localeCompare(right.address));
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeTimetableOptionsBaseUrlOrThrowBadRequest = (options: Record<string, unknown>) => {
|
|
||||||
const parsedOptions = timetableOptionsSchema.safeParse(options);
|
|
||||||
if (!parsedOptions.success) {
|
|
||||||
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid timetable widget options" });
|
|
||||||
}
|
|
||||||
return normalizeBaseUrlOrThrowBadRequest(parsedOptions.data.baseUrl ?? DEFAULT_TIMETABLE_BASE_URL);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const validateTimetableOptionsChangeAsync = async (
|
|
||||||
nextOptions: Record<string, unknown>,
|
|
||||||
previousOptions?: Record<string, unknown>,
|
|
||||||
) => {
|
|
||||||
const nextBaseUrl = normalizeTimetableOptionsBaseUrlOrThrowBadRequest(nextOptions);
|
|
||||||
if (nextBaseUrl === DEFAULT_TIMETABLE_BASE_URL) return;
|
|
||||||
|
|
||||||
let previousBaseUrl: string | undefined;
|
|
||||||
if (previousOptions) {
|
|
||||||
try {
|
|
||||||
previousBaseUrl = normalizeTimetableOptionsBaseUrlOrThrowBadRequest(previousOptions);
|
|
||||||
} catch {
|
|
||||||
// Invalid legacy options must not prevent replacing them with a valid configuration.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (nextBaseUrl === previousBaseUrl) return;
|
|
||||||
|
|
||||||
await resolvePublicTimetableAddressesAsync(nextBaseUrl);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSavedTimetableBaseUrl = (serializedOptions: string) => {
|
|
||||||
const options = parseSavedTimetableOptions(serializedOptions);
|
|
||||||
return normalizeSavedTimetableBaseUrl(options.baseUrl ?? DEFAULT_TIMETABLE_BASE_URL);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ResolvedTimetableSource {
|
interface ResolvedTimetableSource {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
pinnedAddresses?: TimetableResolvedAddress[];
|
pinnedAddresses?: TimetableResolvedAddress[];
|
||||||
@@ -200,7 +36,7 @@ const resolveTimetableBaseUrlAsync = async (
|
|||||||
input: z.infer<typeof timetableSourceSchema>,
|
input: z.infer<typeof timetableSourceSchema>,
|
||||||
allowBoardConfiguration = false,
|
allowBoardConfiguration = false,
|
||||||
): Promise<ResolvedTimetableSource> => {
|
): Promise<ResolvedTimetableSource> => {
|
||||||
const requestedBaseUrl = normalizeBaseUrlOrThrowBadRequest(input.baseUrl);
|
const requestedBaseUrl = normalizeTimetableBaseUrlOrThrowBadRequest(input.baseUrl);
|
||||||
if (allowBoardConfiguration) {
|
if (allowBoardConfiguration) {
|
||||||
if (input.boardId) {
|
if (input.boardId) {
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.boardId), "modify");
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.boardId), "modify");
|
||||||
|
|||||||
Reference in new issue
Block a user