mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-06 04:24:14 +00:00
Compare commits
8 Commits
dependabot
...
1.18.1-s.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53e096f7cb | ||
|
|
3dfd7e8a43 | ||
|
|
db6e60d0a3 | ||
|
|
54d2d689c1 | ||
|
|
68f5512732 | ||
|
|
416e124c02 | ||
|
|
d3e4d8cda8 | ||
|
|
81972dbb73 |
@@ -1,5 +1,5 @@
|
||||
import { CommandModule } from "yargs";
|
||||
import { db, idpOidcConfig, licenseKey } from "@server/db";
|
||||
import { db, idpOidcConfig, licenseKey, certificates, eventStreamingDestinations, alertWebhookActions } from "@server/db";
|
||||
import { encrypt, decrypt } from "@server/lib/crypto";
|
||||
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||
import { eq } from "drizzle-orm";
|
||||
@@ -129,9 +129,15 @@ export const rotateServerSecret: CommandModule<
|
||||
console.log("\nReading encrypted data from database...");
|
||||
const idpConfigs = await db.select().from(idpOidcConfig);
|
||||
const licenseKeys = await db.select().from(licenseKey);
|
||||
const certs = await db.select().from(certificates);
|
||||
const streamingDestinations = await db.select().from(eventStreamingDestinations);
|
||||
const webhookActions = await db.select().from(alertWebhookActions);
|
||||
|
||||
console.log(`Found ${idpConfigs.length} OIDC IdP configuration(s)`);
|
||||
console.log(`Found ${licenseKeys.length} license key(s)`);
|
||||
console.log(`Found ${certs.length} certificate(s)`);
|
||||
console.log(`Found ${streamingDestinations.length} event streaming destination(s)`);
|
||||
console.log(`Found ${webhookActions.length} alert webhook action(s)`);
|
||||
|
||||
// Prepare all decrypted and re-encrypted values
|
||||
console.log("\nDecrypting and re-encrypting values...");
|
||||
@@ -149,8 +155,27 @@ export const rotateServerSecret: CommandModule<
|
||||
encryptedInstanceId: string;
|
||||
};
|
||||
|
||||
type CertUpdate = {
|
||||
certId: number;
|
||||
encryptedCertFile: string | null;
|
||||
encryptedKeyFile: string | null;
|
||||
};
|
||||
|
||||
type StreamingDestinationUpdate = {
|
||||
destinationId: number;
|
||||
encryptedConfig: string;
|
||||
};
|
||||
|
||||
type WebhookActionUpdate = {
|
||||
webhookActionId: number;
|
||||
encryptedConfig: string;
|
||||
};
|
||||
|
||||
const idpUpdates: IdpUpdate[] = [];
|
||||
const licenseKeyUpdates: LicenseKeyUpdate[] = [];
|
||||
const certUpdates: CertUpdate[] = [];
|
||||
const streamingDestinationUpdates: StreamingDestinationUpdate[] = [];
|
||||
const webhookActionUpdates: WebhookActionUpdate[] = [];
|
||||
|
||||
// Process idpOidcConfig entries
|
||||
for (const idpConfig of idpConfigs) {
|
||||
@@ -217,6 +242,70 @@ export const rotateServerSecret: CommandModule<
|
||||
}
|
||||
}
|
||||
|
||||
// Process certificate entries
|
||||
for (const cert of certs) {
|
||||
try {
|
||||
const encryptedCertFile = cert.certFile
|
||||
? encrypt(decrypt(cert.certFile, oldSecret), newSecret)
|
||||
: null;
|
||||
const encryptedKeyFile = cert.keyFile
|
||||
? encrypt(decrypt(cert.keyFile, oldSecret), newSecret)
|
||||
: null;
|
||||
|
||||
certUpdates.push({
|
||||
certId: cert.certId,
|
||||
encryptedCertFile,
|
||||
encryptedKeyFile
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error processing certificate ${cert.certId} (${cert.domain}):`,
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Process eventStreamingDestinations entries
|
||||
for (const dest of streamingDestinations) {
|
||||
try {
|
||||
const decryptedConfig = decrypt(dest.config, oldSecret);
|
||||
const encryptedConfig = encrypt(decryptedConfig, newSecret);
|
||||
|
||||
streamingDestinationUpdates.push({
|
||||
destinationId: dest.destinationId,
|
||||
encryptedConfig
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error processing event streaming destination ${dest.destinationId}:`,
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Process alertWebhookActions entries
|
||||
for (const webhook of webhookActions) {
|
||||
try {
|
||||
if (webhook.config == null) continue;
|
||||
|
||||
const decryptedConfig = decrypt(webhook.config, oldSecret);
|
||||
const encryptedConfig = encrypt(decryptedConfig, newSecret);
|
||||
|
||||
webhookActionUpdates.push({
|
||||
webhookActionId: webhook.webhookActionId,
|
||||
encryptedConfig
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error processing alert webhook action ${webhook.webhookActionId}:`,
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Perform all database updates in a single transaction
|
||||
console.log("\nUpdating database in transaction...");
|
||||
await db.transaction(async (trx) => {
|
||||
@@ -250,10 +339,50 @@ export const rotateServerSecret: CommandModule<
|
||||
instanceId: update.encryptedInstanceId
|
||||
});
|
||||
}
|
||||
|
||||
// Update certificate entries
|
||||
for (const update of certUpdates) {
|
||||
await trx
|
||||
.update(certificates)
|
||||
.set({
|
||||
certFile: update.encryptedCertFile,
|
||||
keyFile: update.encryptedKeyFile
|
||||
})
|
||||
.where(eq(certificates.certId, update.certId));
|
||||
}
|
||||
|
||||
// Update event streaming destination entries
|
||||
for (const update of streamingDestinationUpdates) {
|
||||
await trx
|
||||
.update(eventStreamingDestinations)
|
||||
.set({ config: update.encryptedConfig })
|
||||
.where(
|
||||
eq(
|
||||
eventStreamingDestinations.destinationId,
|
||||
update.destinationId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Update alert webhook action entries
|
||||
for (const update of webhookActionUpdates) {
|
||||
await trx
|
||||
.update(alertWebhookActions)
|
||||
.set({ config: update.encryptedConfig })
|
||||
.where(
|
||||
eq(
|
||||
alertWebhookActions.webhookActionId,
|
||||
update.webhookActionId
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Rotated ${idpUpdates.length} OIDC IdP configuration(s)`);
|
||||
console.log(`Rotated ${licenseKeyUpdates.length} license key(s)`);
|
||||
console.log(`Rotated ${certUpdates.length} certificate(s)`);
|
||||
console.log(`Rotated ${streamingDestinationUpdates.length} event streaming destination(s)`);
|
||||
console.log(`Rotated ${webhookActionUpdates.length} alert webhook action(s)`);
|
||||
|
||||
// Update config file with new secret
|
||||
console.log("\nUpdating config file...");
|
||||
@@ -270,6 +399,9 @@ export const rotateServerSecret: CommandModule<
|
||||
console.log(`\nSummary:`);
|
||||
console.log(` - OIDC IdP configurations: ${idpUpdates.length}`);
|
||||
console.log(` - License keys: ${licenseKeyUpdates.length}`);
|
||||
console.log(` - Certificates: ${certUpdates.length}`);
|
||||
console.log(` - Event streaming destinations: ${streamingDestinationUpdates.length}`);
|
||||
console.log(` - Alert webhook actions: ${webhookActionUpdates.length}`);
|
||||
console.log(
|
||||
`\n IMPORTANT: Restart the server for the new secret to take effect.`
|
||||
);
|
||||
|
||||
@@ -64,7 +64,7 @@ export const NotifyTrialExpiring = ({
|
||||
|
||||
<EmailText>
|
||||
Some features and resources may now be
|
||||
restricted or disconnected. To restore full
|
||||
restricted. To restore full
|
||||
access and continue using all the features
|
||||
you had during your trial, please upgrade to
|
||||
a paid plan.
|
||||
@@ -85,7 +85,7 @@ export const NotifyTrialExpiring = ({
|
||||
<strong>{orgName}</strong> will end on{" "}
|
||||
<strong>{trialEndsAt}</strong>
|
||||
{isLastDay
|
||||
? " — that's tomorrow!"
|
||||
? " - that's tomorrow!"
|
||||
: `, in ${daysRemaining} days`}
|
||||
.
|
||||
</EmailText>
|
||||
@@ -93,8 +93,7 @@ export const NotifyTrialExpiring = ({
|
||||
<EmailText>
|
||||
After your trial ends, your account will be
|
||||
moved to the free plan and some
|
||||
functionality may be restricted or your
|
||||
sites may disconnect.
|
||||
functionality may be restricted.
|
||||
</EmailText>
|
||||
|
||||
<EmailText>
|
||||
|
||||
@@ -25,7 +25,7 @@ export const tier1LimitSet: LimitSet = {
|
||||
|
||||
export const tier2LimitSet: LimitSet = {
|
||||
[FeatureId.USERS]: {
|
||||
value: 100,
|
||||
value: 50,
|
||||
description: "Team limit"
|
||||
},
|
||||
[FeatureId.SITES]: {
|
||||
@@ -48,7 +48,7 @@ export const tier2LimitSet: LimitSet = {
|
||||
|
||||
export const tier3LimitSet: LimitSet = {
|
||||
[FeatureId.USERS]: {
|
||||
value: 500,
|
||||
value: 250,
|
||||
description: "Business limit"
|
||||
},
|
||||
[FeatureId.SITES]: {
|
||||
|
||||
@@ -131,41 +131,22 @@ export async function updateClientResources(
|
||||
: [];
|
||||
|
||||
const allSites: { siteId: number }[] = [];
|
||||
|
||||
if (resourceData.site) {
|
||||
let siteSingle;
|
||||
const resourceSiteId = resourceData.site;
|
||||
|
||||
if (resourceSiteId) {
|
||||
// Look up site by niceId
|
||||
[siteSingle] = await trx
|
||||
.select({ siteId: sites.siteId })
|
||||
.from(sites)
|
||||
.where(
|
||||
and(
|
||||
eq(sites.niceId, resourceSiteId),
|
||||
eq(sites.orgId, orgId)
|
||||
)
|
||||
// Look up site by niceId
|
||||
const [siteSingle] = await trx
|
||||
.select({ siteId: sites.siteId })
|
||||
.from(sites)
|
||||
.where(
|
||||
and(
|
||||
eq(sites.niceId, resourceData.site),
|
||||
eq(sites.orgId, orgId)
|
||||
)
|
||||
.limit(1);
|
||||
} else if (siteId) {
|
||||
// Use the provided siteId directly, but verify it belongs to the org
|
||||
[siteSingle] = await trx
|
||||
.select({ siteId: sites.siteId })
|
||||
.from(sites)
|
||||
.where(
|
||||
and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))
|
||||
)
|
||||
.limit(1);
|
||||
} else {
|
||||
throw new Error(`Target site is required`);
|
||||
)
|
||||
.limit(1);
|
||||
if (siteSingle) {
|
||||
allSites.push(siteSingle);
|
||||
}
|
||||
|
||||
if (!siteSingle) {
|
||||
throw new Error(
|
||||
`Site not found: ${resourceSiteId} in org ${orgId}`
|
||||
);
|
||||
}
|
||||
allSites.push(siteSingle);
|
||||
}
|
||||
|
||||
if (resourceData.sites) {
|
||||
@@ -180,15 +161,31 @@ export async function updateClientResources(
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
if (!site) {
|
||||
throw new Error(
|
||||
`Site not found: ${siteId} in org ${orgId}`
|
||||
);
|
||||
if (site) {
|
||||
allSites.push(site);
|
||||
}
|
||||
allSites.push(site);
|
||||
}
|
||||
}
|
||||
|
||||
if (siteId && allSites.length === 0) {
|
||||
// only add if there are not provided sites
|
||||
// Use the provided siteId directly, but verify it belongs to the org
|
||||
const [siteSingle] = await trx
|
||||
.select({ siteId: sites.siteId })
|
||||
.from(sites)
|
||||
.where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)))
|
||||
.limit(1);
|
||||
if (siteSingle) {
|
||||
allSites.push(siteSingle);
|
||||
}
|
||||
}
|
||||
|
||||
if (allSites.length === 0) {
|
||||
throw new Error(
|
||||
`No valid sites found for private private resource ${resourceNiceId} in org ${orgId}`
|
||||
);
|
||||
}
|
||||
|
||||
if (existingResource) {
|
||||
let domainInfo:
|
||||
| { subdomain: string | null; domainId: string }
|
||||
|
||||
@@ -19,12 +19,13 @@ import { eq, and, ne } from "drizzle-orm";
|
||||
|
||||
export async function getOrgTierData(
|
||||
orgId: string
|
||||
): Promise<{ tier: Tier | null; active: boolean }> {
|
||||
): Promise<{ tier: Tier | null; active: boolean; isTrial: boolean }> {
|
||||
let tier: Tier | null = null;
|
||||
let active = false;
|
||||
let isTrial = false;
|
||||
|
||||
if (build !== "saas") {
|
||||
return { tier, active };
|
||||
return { tier, active, isTrial };
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -35,7 +36,7 @@ export async function getOrgTierData(
|
||||
.limit(1);
|
||||
|
||||
if (!org) {
|
||||
return { tier, active };
|
||||
return { tier, active, isTrial };
|
||||
}
|
||||
|
||||
let orgIdToUse = org.orgId;
|
||||
@@ -44,7 +45,7 @@ export async function getOrgTierData(
|
||||
logger.warn(
|
||||
`Org ${orgId} is not a billing org and does not have a billingOrgId`
|
||||
);
|
||||
return { tier, active };
|
||||
return { tier, active, isTrial };
|
||||
}
|
||||
orgIdToUse = org.billingOrgId;
|
||||
}
|
||||
@@ -57,7 +58,7 @@ export async function getOrgTierData(
|
||||
.limit(1);
|
||||
|
||||
if (!customer) {
|
||||
return { tier, active };
|
||||
return { tier, active, isTrial };
|
||||
}
|
||||
|
||||
// Query for active subscriptions that are not license type
|
||||
@@ -84,11 +85,13 @@ export async function getOrgTierData(
|
||||
tier = subscription.type;
|
||||
active = true;
|
||||
}
|
||||
|
||||
isTrial = subscription.trial ?? false;
|
||||
}
|
||||
} catch (error) {
|
||||
// If org not found or error occurs, return null tier and inactive
|
||||
// This is acceptable behavior as per the function signature
|
||||
}
|
||||
|
||||
return { tier, active };
|
||||
return { tier, active, isTrial };
|
||||
}
|
||||
|
||||
@@ -104,8 +104,9 @@ export async function deleteMyAccount(
|
||||
(r) => r.isBillingOrg && r.isOwner
|
||||
)?.orgId;
|
||||
if (primaryOrgId) {
|
||||
const { tier, active } = await getOrgTierData(primaryOrgId);
|
||||
if (active && tier) {
|
||||
const { tier, active, isTrial } =
|
||||
await getOrgTierData(primaryOrgId);
|
||||
if (active && tier && !isTrial) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
|
||||
@@ -496,11 +496,6 @@ export async function createSiteResource(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(
|
||||
newSiteResource,
|
||||
trx
|
||||
); // we need to call this because we added to the admin role
|
||||
});
|
||||
|
||||
if (!newSiteResource) {
|
||||
@@ -526,6 +521,22 @@ export async function createSiteResource(
|
||||
await createCertificate(domainId, fullDomain, db);
|
||||
}
|
||||
|
||||
// Run in the background after the response is sent. Wrapped in its
|
||||
// own transaction so it always executes on the primary — avoiding any
|
||||
// replica-lag issues while still allowing the HTTP response to return
|
||||
// early.
|
||||
db.transaction(async (trx) => {
|
||||
await rebuildClientAssociationsFromSiteResource(
|
||||
newSiteResource!,
|
||||
trx
|
||||
);
|
||||
}).catch((err) => {
|
||||
logger.error(
|
||||
`Error rebuilding client associations for site resource ${newSiteResource!.siteResourceId}:`,
|
||||
err
|
||||
);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: newSiteResource,
|
||||
success: true,
|
||||
|
||||
@@ -63,17 +63,26 @@ export async function deleteSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
// Delete the site resource
|
||||
const [removedSiteResource] = await trx
|
||||
.delete(siteResources)
|
||||
.where(eq(siteResources.siteResourceId, siteResourceId))
|
||||
.returning();
|
||||
// Delete the site resource
|
||||
const [removedSiteResource] = await db
|
||||
.delete(siteResources)
|
||||
.where(eq(siteResources.siteResourceId, siteResourceId))
|
||||
.returning();
|
||||
|
||||
// Run in the background after the response is sent. Wrapped in its
|
||||
// own transaction so it always executes on the primary — avoiding any
|
||||
// replica-lag issues while still allowing the HTTP response to return
|
||||
// early.
|
||||
db.transaction(async (trx) => {
|
||||
await rebuildClientAssociationsFromSiteResource(
|
||||
removedSiteResource,
|
||||
trx
|
||||
);
|
||||
}).catch((err) => {
|
||||
logger.error(
|
||||
`Error rebuilding client associations for site resource ${removedSiteResource!.siteResourceId}:`,
|
||||
err
|
||||
);
|
||||
});
|
||||
|
||||
logger.info(`Deleted site resource ${siteResourceId}`);
|
||||
|
||||
@@ -431,9 +431,6 @@ export async function updateSiteResource(
|
||||
})
|
||||
.returning();
|
||||
|
||||
// wait some time to allow for messages to be handled
|
||||
await new Promise((resolve) => setTimeout(resolve, 750));
|
||||
|
||||
const sshPamSet =
|
||||
isLicensedSshPam &&
|
||||
(authDaemonPort !== undefined ||
|
||||
@@ -556,11 +553,6 @@ export async function updateSiteResource(
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(
|
||||
updatedSiteResource,
|
||||
trx
|
||||
);
|
||||
} else {
|
||||
// Update the site resource
|
||||
const sshPamSet =
|
||||
@@ -690,7 +682,24 @@ export async function updateSiteResource(
|
||||
}
|
||||
|
||||
logger.info(`Updated site resource ${siteResourceId}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Background: wait for removal messages to propagate, then rebuild
|
||||
// associations for the re-created resource. Own transaction ensures
|
||||
// execution on the primary against fully committed state.
|
||||
(async () => {
|
||||
await db.transaction(async (trx) => {
|
||||
if (!updatedSiteResource) {
|
||||
throw new Error("No updated resource found after update");
|
||||
}
|
||||
if (sitesChanged) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 750));
|
||||
await rebuildClientAssociationsFromSiteResource(
|
||||
updatedSiteResource,
|
||||
trx
|
||||
);
|
||||
}
|
||||
await handleMessagingForUpdatedSiteResource(
|
||||
existingSiteResource,
|
||||
updatedSiteResource,
|
||||
@@ -700,7 +709,12 @@ export async function updateSiteResource(
|
||||
})),
|
||||
trx
|
||||
);
|
||||
}
|
||||
});
|
||||
})().catch((err) => {
|
||||
logger.error(
|
||||
`Error rebuilding client associations for site resource ${updatedSiteResource?.siteResourceId}:`,
|
||||
err
|
||||
);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -16,6 +16,9 @@ export default async function migration() {
|
||||
thc."targetId",
|
||||
t."siteId",
|
||||
s."orgId",
|
||||
r."name" AS "resourceName",
|
||||
t."ip",
|
||||
t."port",
|
||||
thc."hcEnabled",
|
||||
thc."hcPath",
|
||||
thc."hcScheme",
|
||||
@@ -33,13 +36,17 @@ export default async function migration() {
|
||||
thc."hcTlsServerName"
|
||||
FROM "targetHealthCheck" thc
|
||||
JOIN "targets" t ON thc."targetId" = t."targetId"
|
||||
JOIN "sites" s ON t."siteId" = s."siteId"`
|
||||
JOIN "sites" s ON t."siteId" = s."siteId"
|
||||
JOIN "resources" r ON t."resourceId" = r."resourceId"`
|
||||
);
|
||||
const existingHealthChecks = healthChecksQuery.rows as {
|
||||
targetHealthCheckId: number;
|
||||
targetId: number;
|
||||
siteId: number;
|
||||
orgId: string;
|
||||
resourceName: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
hcEnabled: boolean;
|
||||
hcPath: string | null;
|
||||
hcScheme: string | null;
|
||||
@@ -385,6 +392,7 @@ export default async function migration() {
|
||||
"targetId",
|
||||
"orgId",
|
||||
"siteId",
|
||||
"name",
|
||||
"hcEnabled",
|
||||
"hcPath",
|
||||
"hcScheme",
|
||||
@@ -405,6 +413,7 @@ export default async function migration() {
|
||||
${hc.targetId},
|
||||
${hc.orgId},
|
||||
${hc.siteId},
|
||||
${`Resource ${hc.resourceName} - ${hc.ip}:${hc.port}`},
|
||||
${hc.hcEnabled},
|
||||
${hc.hcPath},
|
||||
${hc.hcScheme},
|
||||
|
||||
@@ -22,6 +22,9 @@ export default async function migration() {
|
||||
thc."targetId",
|
||||
t."siteId",
|
||||
s."orgId",
|
||||
r."name" AS "resourceName",
|
||||
t."ip",
|
||||
t."port",
|
||||
thc."hcEnabled",
|
||||
thc."hcPath",
|
||||
thc."hcScheme",
|
||||
@@ -39,13 +42,17 @@ export default async function migration() {
|
||||
thc."hcTlsServerName"
|
||||
FROM 'targetHealthCheck' thc
|
||||
JOIN 'targets' t ON thc."targetId" = t."targetId"
|
||||
JOIN 'sites' s ON t."siteId" = s."siteId"`
|
||||
JOIN 'sites' s ON t."siteId" = s."siteId"
|
||||
JOIN 'resources' r ON t."resourceId" = r."resourceId"`
|
||||
)
|
||||
.all() as {
|
||||
targetHealthCheckId: number;
|
||||
targetId: number;
|
||||
siteId: number;
|
||||
orgId: string;
|
||||
resourceName: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
hcEnabled: number;
|
||||
hcPath: string | null;
|
||||
hcScheme: string | null;
|
||||
@@ -392,6 +399,7 @@ export default async function migration() {
|
||||
"targetId",
|
||||
"orgId",
|
||||
"siteId",
|
||||
"name",
|
||||
"hcEnabled",
|
||||
"hcPath",
|
||||
"hcScheme",
|
||||
@@ -407,7 +415,7 @@ export default async function migration() {
|
||||
"hcStatus",
|
||||
"hcHealth",
|
||||
"hcTlsServerName"
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
);
|
||||
|
||||
const insertAll = db.transaction(() => {
|
||||
@@ -417,6 +425,7 @@ export default async function migration() {
|
||||
hc.targetId,
|
||||
hc.orgId,
|
||||
hc.siteId,
|
||||
`Resource ${hc.resourceName} - ${hc.ip}:${hc.port}`,
|
||||
hc.hcEnabled,
|
||||
hc.hcPath,
|
||||
hc.hcScheme,
|
||||
|
||||
Reference in New Issue
Block a user