diff --git a/apps/tasks/package.json b/apps/tasks/package.json index aace08cf9..b342cafb0 100644 --- a/apps/tasks/package.json +++ b/apps/tasks/package.json @@ -21,6 +21,7 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/analytics": "workspace:^0.1.0", + "@homarr/auth": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", "@homarr/cron-job-api": "workspace:^0.1.0", "@homarr/cron-jobs": "workspace:^0.1.0", @@ -32,6 +33,7 @@ "@homarr/log": "workspace:^", "@homarr/ping": "workspace:^0.1.0", "@homarr/redis": "workspace:^0.1.0", + "@homarr/request-handler": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", "@homarr/widgets": "workspace:^0.1.0", diff --git a/apps/tasks/src/job-manager.ts b/apps/tasks/src/job-manager.ts index 475368d89..e19f2c315 100644 --- a/apps/tasks/src/job-manager.ts +++ b/apps/tasks/src/job-manager.ts @@ -26,7 +26,6 @@ export class JobManager implements IJobManager { logger.info(`Updating cron job interval name="${name}" expression="${cron}"`); const job = this.jobGroup.getJobRegistry().get(name); if (!job) throw new Error(`Job ${name} not found`); - if (job.cronExpression === "never") throw new Error(`Job ${name} cannot be updated as it is set to "never"`); if (!validateCron(cron)) { throw new Error(`Invalid cron expression: ${cron}`); } @@ -45,7 +44,6 @@ export class JobManager implements IJobManager { logger.info(`Disabling cron job name="${name}"`); const job = this.jobGroup.getJobRegistry().get(name); if (!job) throw new Error(`Job ${name} not found`); - if (job.cronExpression === "never") throw new Error(`Job ${name} cannot be disabled as it is set to "never"`); await this.updateConfigurationAsync(name, { isEnabled: false }); await this.jobGroup.stopAsync(name); diff --git a/apps/tasks/src/main.ts b/apps/tasks/src/main.ts index f996a1399..ad6181880 100644 --- a/apps/tasks/src/main.ts +++ b/apps/tasks/src/main.ts @@ -13,6 +13,7 @@ import { db } from "@homarr/db"; import { logger } from "@homarr/log"; import { JobManager } from "./job-manager"; +import { onStartAsync } from "./on-start"; const server = fastify({ maxParamLength: 5000, @@ -32,6 +33,7 @@ server.register(fastifyTRPCPlugin, { }); void (async () => { + await onStartAsync(); await jobGroup.initializeAsync(); await jobGroup.startAllAsync(); diff --git a/apps/tasks/src/on-start/index.ts b/apps/tasks/src/on-start/index.ts new file mode 100644 index 000000000..cb9564d1b --- /dev/null +++ b/apps/tasks/src/on-start/index.ts @@ -0,0 +1,7 @@ +import { invalidateUpdateCheckerCacheAsync } from "./invalidate-update-checker-cache"; +import { cleanupSessionsAsync } from "./session-cleanup"; + +export async function onStartAsync() { + await cleanupSessionsAsync(); + await invalidateUpdateCheckerCacheAsync(); +} diff --git a/apps/tasks/src/on-start/invalidate-update-checker-cache.ts b/apps/tasks/src/on-start/invalidate-update-checker-cache.ts new file mode 100644 index 000000000..7bbda3dec --- /dev/null +++ b/apps/tasks/src/on-start/invalidate-update-checker-cache.ts @@ -0,0 +1,18 @@ +import { logger } from "@homarr/log"; +import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker"; + +const localLogger = logger.child({ module: "invalidateUpdateCheckerCache" }); + +/** + * Invalidates the update checker cache on startup to ensure fresh data. + * It is important as we want to avoid showing pending updates after the update to latest version. + */ +export async function invalidateUpdateCheckerCacheAsync() { + try { + const handler = updateCheckerRequestHandler.handler({}); + await handler.invalidateAsync(); + localLogger.debug("Update checker cache invalidated"); + } catch (error) { + localLogger.error(new Error("Failed to invalidate update checker cache", { cause: error })); + } +} diff --git a/apps/tasks/src/on-start/session-cleanup.ts b/apps/tasks/src/on-start/session-cleanup.ts new file mode 100644 index 000000000..4dfe46341 --- /dev/null +++ b/apps/tasks/src/on-start/session-cleanup.ts @@ -0,0 +1,39 @@ +import { env } from "@homarr/auth/env"; +import { db, eq, inArray } from "@homarr/db"; +import { sessions, users } from "@homarr/db/schema"; +import { supportedAuthProviders } from "@homarr/definitions"; +import { logger } from "@homarr/log"; + +const localLogger = logger.child({ module: "sessionCleanup" }); + +/** + * Deletes sessions for users that have inactive auth providers. + * Sessions from other providers are deleted so they can no longer be used. + */ +export async function cleanupSessionsAsync() { + try { + const currentAuthProviders = env.AUTH_PROVIDERS; + + const inactiveAuthProviders = supportedAuthProviders.filter((provider) => !currentAuthProviders.includes(provider)); + const subQuery = db + .select({ id: users.id }) + .from(users) + .where(inArray(users.provider, inactiveAuthProviders)) + .as("sq"); + const sessionsWithInactiveProviders = await db + .select({ userId: sessions.userId }) + .from(sessions) + .rightJoin(subQuery, eq(sessions.userId, subQuery.id)); + + const userIds = sessionsWithInactiveProviders.map(({ userId }) => userId).filter((value) => value !== null); + await db.delete(sessions).where(inArray(sessions.userId, userIds)); + + if (sessionsWithInactiveProviders.length > 0) { + localLogger.info(`Deleted sessions for inactive providers count=${userIds.length}`); + } else { + localLogger.debug("No sessions to delete"); + } + } catch (error) { + localLogger.error(new Error("Failed to clean up sessions", { cause: error })); + } +} diff --git a/packages/cron-jobs-core/src/creator.ts b/packages/cron-jobs-core/src/creator.ts index 082f66f52..e21065d9e 100644 --- a/packages/cron-jobs-core/src/creator.ts +++ b/packages/cron-jobs-core/src/creator.ts @@ -72,8 +72,6 @@ const createCallback = eq(cronJobConfigurations.name, name), }); - if (defaultCronExpression === "never") return null; - const scheduledTask = createTask( configuration?.cronExpression ?? defaultCronExpression, () => void catchingCallbackAsync(), @@ -120,7 +118,7 @@ export const createCronJobCreator = ( options: CreateCronJobOptions = { runOnStart: false }, ) => { creatorOptions.logger.logDebug(`Validating cron expression '${defaultCronExpression}' for job: ${name}`); - if (defaultCronExpression !== "never" && !validate(defaultCronExpression)) { + if (!validate(defaultCronExpression)) { throw new Error(`Invalid cron expression '${defaultCronExpression}' for job '${name}'`); } creatorOptions.logger.logDebug(`Cron job expression '${defaultCronExpression}' for job ${name} is valid`); @@ -132,8 +130,6 @@ export const createCronJobCreator = ( // This is a type guard to check if the cron expression is valid and give the user a type hint return returnValue as unknown as ValidateCron extends true ? typeof returnValue - : TExpression extends "never" - ? typeof returnValue - : "Invalid cron expression"; + : "Invalid cron expression"; }; }; diff --git a/packages/cron-jobs-core/src/expressions.ts b/packages/cron-jobs-core/src/expressions.ts index 59c500b67..f71d01d21 100644 --- a/packages/cron-jobs-core/src/expressions.ts +++ b/packages/cron-jobs-core/src/expressions.ts @@ -8,4 +8,3 @@ export const EVERY_10_MINUTES = checkCron("*/10 * * * *") satisfies string; export const EVERY_HOUR = checkCron("0 * * * *") satisfies string; export const EVERY_DAY = checkCron("0 0 * * */1") satisfies string; export const EVERY_WEEK = checkCron("0 0 * * 1") satisfies string; -export const NEVER = "never"; diff --git a/packages/cron-jobs-core/src/group.ts b/packages/cron-jobs-core/src/group.ts index 684c50214..28ae714c8 100644 --- a/packages/cron-jobs-core/src/group.ts +++ b/packages/cron-jobs-core/src/group.ts @@ -45,7 +45,6 @@ export const createJobGroupCreator = ( } const scheduledTask = await job.createTaskAsync(); - if (!scheduledTask) continue; tasks.set(job.name, scheduledTask); } diff --git a/packages/cron-jobs/src/index.ts b/packages/cron-jobs/src/index.ts index edc542db8..c087637e4 100644 --- a/packages/cron-jobs/src/index.ts +++ b/packages/cron-jobs/src/index.ts @@ -21,7 +21,6 @@ import { refreshNotificationsJob } from "./jobs/integrations/notifications"; import { minecraftServerStatusJob } from "./jobs/minecraft-server-status"; import { pingJob } from "./jobs/ping"; import { rssFeedsJob } from "./jobs/rss-feeds"; -import { sessionCleanupJob } from "./jobs/session-cleanup"; import { updateCheckerJob } from "./jobs/update-checker"; import { createCronJobGroup } from "./lib"; @@ -39,7 +38,6 @@ export const jobGroup = createCronJobGroup({ rssFeeds: rssFeedsJob, indexerManager: indexerManagerJob, healthMonitoring: healthMonitoringJob, - sessionCleanup: sessionCleanupJob, updateChecker: updateCheckerJob, mediaTranscoding: mediaTranscodingJob, minecraftServerStatus: minecraftServerStatusJob, diff --git a/packages/cron-jobs/src/jobs/session-cleanup.ts b/packages/cron-jobs/src/jobs/session-cleanup.ts deleted file mode 100644 index 72f6c88fe..000000000 --- a/packages/cron-jobs/src/jobs/session-cleanup.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { env } from "@homarr/auth/env"; -import { NEVER } from "@homarr/cron-jobs-core/expressions"; -import { db, eq, inArray } from "@homarr/db"; -import { sessions, users } from "@homarr/db/schema"; -import { supportedAuthProviders } from "@homarr/definitions"; -import { logger } from "@homarr/log"; - -import { createCronJob } from "../lib"; - -/** - * Deletes sessions for users that have inactive auth providers. - * Sessions from other providers are deleted so they can no longer be used. - */ -export const sessionCleanupJob = createCronJob("sessionCleanup", NEVER, { - runOnStart: true, -}).withCallback(async () => { - const currentAuthProviders = env.AUTH_PROVIDERS; - - const inactiveAuthProviders = supportedAuthProviders.filter((provider) => !currentAuthProviders.includes(provider)); - const subQuery = db - .select({ id: users.id }) - .from(users) - .where(inArray(users.provider, inactiveAuthProviders)) - .as("sq"); - const sessionsWithInactiveProviders = await db - .select({ userId: sessions.userId }) - .from(sessions) - .rightJoin(subQuery, eq(sessions.userId, subQuery.id)); - - const userIds = sessionsWithInactiveProviders.map(({ userId }) => userId).filter((value) => value !== null); - await db.delete(sessions).where(inArray(sessions.userId, userIds)); - - if (sessionsWithInactiveProviders.length > 0) { - logger.info(`Deleted sessions for inactive providers count=${userIds.length}`); - } else { - logger.debug("No sessions to delete"); - } -}); diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index e6c904d8e..8b5e89369 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -3255,9 +3255,6 @@ "dnsHole": { "label": "DNS Hole Data" }, - "sessionCleanup": { - "label": "Session Cleanup" - }, "updateChecker": { "label": "Update checker" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07f87a0e9..f4d246ef5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -373,6 +373,9 @@ importers: '@homarr/analytics': specifier: workspace:^0.1.0 version: link:../../packages/analytics + '@homarr/auth': + specifier: workspace:^0.1.0 + version: link:../../packages/auth '@homarr/common': specifier: workspace:^0.1.0 version: link:../../packages/common @@ -406,6 +409,9 @@ importers: '@homarr/redis': specifier: workspace:^0.1.0 version: link:../../packages/redis + '@homarr/request-handler': + specifier: workspace:^0.1.0 + version: link:../../packages/request-handler '@homarr/server-settings': specifier: workspace:^0.1.0 version: link:../../packages/server-settings