mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
refactor: simplify dtos and improve type saftey in json returns
This commit is contained in:
@@ -6,7 +6,7 @@ import type {
|
||||
RepositoryStatus,
|
||||
} from "@ironmount/schemas/restic";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
import { int, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
|
||||
export const volumesTable = sqliteTable("volumes_table", {
|
||||
id: int().primaryKey({ autoIncrement: true }),
|
||||
@@ -14,9 +14,9 @@ export const volumesTable = sqliteTable("volumes_table", {
|
||||
type: text().$type<BackendType>().notNull(),
|
||||
status: text().$type<BackendStatus>().notNull().default("unmounted"),
|
||||
lastError: text("last_error"),
|
||||
lastHealthCheck: int("last_health_check", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
||||
createdAt: int("created_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
||||
updatedAt: int("updated_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
||||
lastHealthCheck: integer("last_health_check", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
||||
createdAt: integer("created_at", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
||||
updatedAt: integer("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
||||
config: text("config", { mode: "json" }).$type<typeof volumeConfigSchema.inferOut>().notNull(),
|
||||
autoRemount: int("auto_remount", { mode: "boolean" }).notNull().default(true),
|
||||
});
|
||||
@@ -27,8 +27,8 @@ export const usersTable = sqliteTable("users_table", {
|
||||
id: int().primaryKey({ autoIncrement: true }),
|
||||
username: text().notNull().unique(),
|
||||
passwordHash: text("password_hash").notNull(),
|
||||
createdAt: int("created_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
||||
updatedAt: int("updated_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
||||
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
||||
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
||||
});
|
||||
|
||||
export type User = typeof usersTable.$inferSelect;
|
||||
@@ -38,8 +38,8 @@ export const sessionsTable = sqliteTable("sessions_table", {
|
||||
userId: int("user_id")
|
||||
.notNull()
|
||||
.references(() => usersTable.id, { onDelete: "cascade" }),
|
||||
expiresAt: int("expires_at", { mode: "timestamp" }).notNull(),
|
||||
createdAt: int("created_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
||||
expiresAt: int("expires_at", { mode: "number" }).notNull(),
|
||||
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
||||
});
|
||||
|
||||
export type Session = typeof sessionsTable.$inferSelect;
|
||||
@@ -51,10 +51,10 @@ export const repositoriesTable = sqliteTable("repositories_table", {
|
||||
config: text("config", { mode: "json" }).$type<typeof repositoryConfigSchema.inferOut>().notNull(),
|
||||
compressionMode: text("compression_mode").$type<CompressionMode>().default("auto"),
|
||||
status: text().$type<RepositoryStatus>().default("unknown"),
|
||||
lastChecked: int("last_checked", { mode: "timestamp" }),
|
||||
lastChecked: int("last_checked", { mode: "number" }),
|
||||
lastError: text("last_error"),
|
||||
createdAt: int("created_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
||||
updatedAt: int("updated_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
||||
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
||||
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
||||
});
|
||||
|
||||
export type Repository = typeof repositoriesTable.$inferSelect;
|
||||
@@ -81,12 +81,12 @@ export const backupSchedulesTable = sqliteTable("backup_schedules_table", {
|
||||
}>(),
|
||||
excludePatterns: text("exclude_patterns", { mode: "json" }).$type<string[]>().default([]),
|
||||
includePatterns: text("include_patterns", { mode: "json" }).$type<string[]>().default([]),
|
||||
lastBackupAt: int("last_backup_at", { mode: "timestamp" }),
|
||||
lastBackupAt: int("last_backup_at", { mode: "number" }),
|
||||
lastBackupStatus: text("last_backup_status").$type<"success" | "error">(),
|
||||
lastBackupError: text("last_backup_error"),
|
||||
nextBackupAt: int("next_backup_at", { mode: "timestamp" }),
|
||||
createdAt: int("created_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
||||
updatedAt: int("updated_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
||||
nextBackupAt: int("next_backup_at", { mode: "number" }),
|
||||
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
||||
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
||||
});
|
||||
|
||||
export type BackupSchedule = typeof backupSchedulesTable.$inferSelect;
|
||||
|
||||
10
apps/server/src/jobs/cleanup-sessions.ts
Normal file
10
apps/server/src/jobs/cleanup-sessions.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Job } from "../core/scheduler";
|
||||
import { authService } from "../modules/auth/auth.service";
|
||||
|
||||
export class CleanupSessionsJob extends Job {
|
||||
async run() {
|
||||
authService.cleanupExpiredSessions();
|
||||
|
||||
return { done: true, timestamp: new Date() };
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,14 @@ import {
|
||||
logoutDto,
|
||||
registerBodySchema,
|
||||
registerDto,
|
||||
type GetMeDto,
|
||||
type GetStatusDto,
|
||||
type LoginDto,
|
||||
type LogoutDto,
|
||||
type RegisterDto,
|
||||
} from "./auth.dto";
|
||||
import { authService } from "./auth.service";
|
||||
import { toMessage } from "../../utils/errors";
|
||||
|
||||
const COOKIE_NAME = "session_id";
|
||||
const COOKIE_OPTIONS = {
|
||||
@@ -33,9 +39,12 @@ export const authController = new Hono()
|
||||
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
|
||||
});
|
||||
|
||||
return c.json({ message: "User registered successfully", user: { id: user.id, username: user.username } }, 201);
|
||||
return c.json<RegisterDto>(
|
||||
{ success: true, message: "User registered successfully", user: { id: user.id, username: user.username } },
|
||||
201,
|
||||
);
|
||||
} catch (error) {
|
||||
return c.json({ message: error instanceof Error ? error.message : "Registration failed" }, 400);
|
||||
return c.json<RegisterDto>({ success: false, message: toMessage(error) }, 400);
|
||||
}
|
||||
})
|
||||
.post("/login", loginDto, validator("json", loginBodySchema), async (c) => {
|
||||
@@ -46,15 +55,16 @@ export const authController = new Hono()
|
||||
|
||||
setCookie(c, COOKIE_NAME, sessionId, {
|
||||
...COOKIE_OPTIONS,
|
||||
expires: expiresAt,
|
||||
expires: new Date(expiresAt),
|
||||
});
|
||||
|
||||
return c.json({
|
||||
return c.json<LoginDto>({
|
||||
success: true,
|
||||
message: "Login successful",
|
||||
user: { id: user.id, username: user.username },
|
||||
});
|
||||
} catch (error) {
|
||||
return c.json({ message: error instanceof Error ? error.message : "Login failed" }, 401);
|
||||
return c.json<LoginDto>({ success: false, message: toMessage(error) }, 401);
|
||||
}
|
||||
})
|
||||
.post("/logout", logoutDto, async (c) => {
|
||||
@@ -65,13 +75,13 @@ export const authController = new Hono()
|
||||
deleteCookie(c, COOKIE_NAME, COOKIE_OPTIONS);
|
||||
}
|
||||
|
||||
return c.json({ message: "Logout successful" });
|
||||
return c.json<LogoutDto>({ success: true });
|
||||
})
|
||||
.get("/me", getMeDto, async (c) => {
|
||||
const sessionId = getCookie(c, COOKIE_NAME);
|
||||
|
||||
if (!sessionId) {
|
||||
return c.json({ message: "Not authenticated" }, 401);
|
||||
return c.json<GetMeDto>({ success: false, message: "Not authenticated" }, 401);
|
||||
}
|
||||
|
||||
const session = await authService.verifySession(sessionId);
|
||||
@@ -81,11 +91,13 @@ export const authController = new Hono()
|
||||
return c.json({ message: "Not authenticated" }, 401);
|
||||
}
|
||||
|
||||
return c.json({
|
||||
return c.json<GetMeDto>({
|
||||
success: true,
|
||||
user: session.user,
|
||||
message: "Authenticated",
|
||||
});
|
||||
})
|
||||
.get("/status", getStatusDto, async (c) => {
|
||||
const hasUsers = await authService.hasUsers();
|
||||
return c.json({ hasUsers });
|
||||
return c.json<GetStatusDto>({ hasUsers });
|
||||
});
|
||||
|
||||
@@ -14,10 +14,11 @@ export const registerBodySchema = type({
|
||||
|
||||
const loginResponseSchema = type({
|
||||
message: "string",
|
||||
success: "boolean",
|
||||
user: type({
|
||||
id: "string",
|
||||
id: "number",
|
||||
username: "string",
|
||||
}),
|
||||
}).optional(),
|
||||
});
|
||||
|
||||
export const loginDto = describeRoute({
|
||||
@@ -39,6 +40,8 @@ export const loginDto = describeRoute({
|
||||
},
|
||||
});
|
||||
|
||||
export type LoginDto = typeof loginResponseSchema.infer;
|
||||
|
||||
export const registerDto = describeRoute({
|
||||
description: "Register a new user",
|
||||
operationId: "register",
|
||||
@@ -58,6 +61,12 @@ export const registerDto = describeRoute({
|
||||
},
|
||||
});
|
||||
|
||||
export type RegisterDto = typeof loginResponseSchema.infer;
|
||||
|
||||
const logoutResponseSchema = type({
|
||||
success: "boolean",
|
||||
});
|
||||
|
||||
export const logoutDto = describeRoute({
|
||||
description: "Logout current user",
|
||||
operationId: "logout",
|
||||
@@ -67,13 +76,15 @@ export const logoutDto = describeRoute({
|
||||
description: "Logout successful",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(type({ message: "string" })),
|
||||
schema: resolver(logoutResponseSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export type LogoutDto = typeof logoutResponseSchema.infer;
|
||||
|
||||
export const getMeDto = describeRoute({
|
||||
description: "Get current authenticated user",
|
||||
operationId: "getMe",
|
||||
@@ -87,12 +98,11 @@ export const getMeDto = describeRoute({
|
||||
},
|
||||
},
|
||||
},
|
||||
401: {
|
||||
description: "Not authenticated",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export type GetMeDto = typeof loginResponseSchema.infer;
|
||||
|
||||
const statusResponseSchema = type({
|
||||
hasUsers: "boolean",
|
||||
});
|
||||
@@ -113,5 +123,7 @@ export const getStatusDto = describeRoute({
|
||||
},
|
||||
});
|
||||
|
||||
export type GetStatusDto = typeof statusResponseSchema.infer;
|
||||
|
||||
export type LoginBody = typeof loginBodySchema.infer;
|
||||
export type RegisterBody = typeof registerBodySchema.infer;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { eq, lt } from "drizzle-orm";
|
||||
import { db } from "../../db/db";
|
||||
import { sessionsTable, usersTable } from "../../db/schema";
|
||||
import { logger } from "../../utils/logger";
|
||||
@@ -30,7 +30,7 @@ export class AuthService {
|
||||
|
||||
logger.info(`User registered: ${username}`);
|
||||
const sessionId = crypto.randomUUID();
|
||||
const expiresAt = new Date(Date.now() + SESSION_DURATION);
|
||||
const expiresAt = new Date(Date.now() + SESSION_DURATION).getTime();
|
||||
|
||||
await db.insert(sessionsTable).values({
|
||||
id: sessionId,
|
||||
@@ -58,7 +58,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
const sessionId = crypto.randomUUID();
|
||||
const expiresAt = new Date(Date.now() + SESSION_DURATION);
|
||||
const expiresAt = new Date(Date.now() + SESSION_DURATION).getTime();
|
||||
|
||||
await db.insert(sessionsTable).values({
|
||||
id: sessionId,
|
||||
@@ -100,7 +100,7 @@ export class AuthService {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (session.session.expiresAt < new Date()) {
|
||||
if (session.session.expiresAt < Date.now()) {
|
||||
await db.delete(sessionsTable).where(eq(sessionsTable.id, sessionId));
|
||||
return null;
|
||||
}
|
||||
@@ -121,7 +121,7 @@ export class AuthService {
|
||||
* Clean up expired sessions
|
||||
*/
|
||||
async cleanupExpiredSessions() {
|
||||
const result = await db.delete(sessionsTable).where(eq(sessionsTable.expiresAt, new Date())).returning();
|
||||
const result = await db.delete(sessionsTable).where(lt(sessionsTable.expiresAt, Date.now())).returning();
|
||||
if (result.length > 0) {
|
||||
logger.info(`Cleaned up ${result.length} expired sessions`);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,13 @@ import {
|
||||
runBackupNowDto,
|
||||
updateBackupScheduleBody,
|
||||
updateBackupScheduleDto,
|
||||
type CreateBackupScheduleDto,
|
||||
type DeleteBackupScheduleDto,
|
||||
type GetBackupScheduleDto,
|
||||
type GetBackupScheduleForVolumeResponseDto,
|
||||
type ListBackupSchedulesResponseDto,
|
||||
type RunBackupNowDto,
|
||||
type UpdateBackupScheduleDto,
|
||||
} from "./backups.dto";
|
||||
import { backupsService } from "./backups.service";
|
||||
|
||||
@@ -17,27 +24,27 @@ export const backupScheduleController = new Hono()
|
||||
.get("/", listBackupSchedulesDto, async (c) => {
|
||||
const schedules = await backupsService.listSchedules();
|
||||
|
||||
return c.json({ schedules }, 200);
|
||||
return c.json<ListBackupSchedulesResponseDto>(schedules, 200);
|
||||
})
|
||||
.get("/:scheduleId", getBackupScheduleDto, async (c) => {
|
||||
const scheduleId = c.req.param("scheduleId");
|
||||
|
||||
const schedule = await backupsService.getSchedule(Number(scheduleId));
|
||||
|
||||
return c.json({ schedule }, 200);
|
||||
return c.json<GetBackupScheduleDto>(schedule, 200);
|
||||
})
|
||||
.get("/volume/:volumeId", getBackupScheduleForVolumeDto, async (c) => {
|
||||
const volumeId = c.req.param("volumeId");
|
||||
const schedule = await backupsService.getScheduleForVolume(Number(volumeId));
|
||||
|
||||
return c.json(schedule, 200);
|
||||
return c.json<GetBackupScheduleForVolumeResponseDto>(schedule, 200);
|
||||
})
|
||||
.post("/", createBackupScheduleDto, validator("json", createBackupScheduleBody), async (c) => {
|
||||
const body = c.req.valid("json");
|
||||
|
||||
const schedule = await backupsService.createSchedule(body);
|
||||
|
||||
return c.json({ message: "Backup schedule created successfully", schedule }, 201);
|
||||
return c.json<CreateBackupScheduleDto>(schedule, 201);
|
||||
})
|
||||
.patch("/:scheduleId", updateBackupScheduleDto, validator("json", updateBackupScheduleBody), async (c) => {
|
||||
const scheduleId = c.req.param("scheduleId");
|
||||
@@ -45,14 +52,14 @@ export const backupScheduleController = new Hono()
|
||||
|
||||
const schedule = await backupsService.updateSchedule(Number(scheduleId), body);
|
||||
|
||||
return c.json({ message: "Backup schedule updated successfully", schedule }, 200);
|
||||
return c.json<UpdateBackupScheduleDto>(schedule, 200);
|
||||
})
|
||||
.delete("/:scheduleId", deleteBackupScheduleDto, async (c) => {
|
||||
const scheduleId = c.req.param("scheduleId");
|
||||
|
||||
await backupsService.deleteSchedule(Number(scheduleId));
|
||||
|
||||
return c.json({ message: "Backup schedule deleted successfully" }, 200);
|
||||
return c.json<DeleteBackupScheduleDto>({ success: true }, 200);
|
||||
})
|
||||
.post("/:scheduleId/run", runBackupNowDto, async (c) => {
|
||||
const scheduleId = c.req.param("scheduleId");
|
||||
@@ -61,11 +68,5 @@ export const backupScheduleController = new Hono()
|
||||
console.error("Backup execution failed:", error);
|
||||
});
|
||||
|
||||
return c.json(
|
||||
{
|
||||
message: "Backup started",
|
||||
backupStarted: true,
|
||||
},
|
||||
200,
|
||||
);
|
||||
return c.json<RunBackupNowDto>({ success: true }, 200);
|
||||
});
|
||||
|
||||
@@ -35,9 +35,7 @@ export type BackupScheduleDto = typeof backupScheduleSchema.infer;
|
||||
/**
|
||||
* List all backup schedules
|
||||
*/
|
||||
export const listBackupSchedulesResponse = type({
|
||||
schedules: backupScheduleSchema.array(),
|
||||
});
|
||||
export const listBackupSchedulesResponse = backupScheduleSchema.array();
|
||||
|
||||
export type ListBackupSchedulesResponseDto = typeof listBackupSchedulesResponse.infer;
|
||||
|
||||
@@ -60,9 +58,7 @@ export const listBackupSchedulesDto = describeRoute({
|
||||
/**
|
||||
* Get a single backup schedule
|
||||
*/
|
||||
export const getBackupScheduleResponse = type({
|
||||
schedule: backupScheduleSchema,
|
||||
});
|
||||
export const getBackupScheduleResponse = backupScheduleSchema;
|
||||
|
||||
export type GetBackupScheduleDto = typeof getBackupScheduleResponse.infer;
|
||||
|
||||
@@ -118,10 +114,7 @@ export const createBackupScheduleBody = type({
|
||||
|
||||
export type CreateBackupScheduleBody = typeof createBackupScheduleBody.infer;
|
||||
|
||||
export const createBackupScheduleResponse = type({
|
||||
message: "string",
|
||||
schedule: backupScheduleSchema,
|
||||
});
|
||||
export const createBackupScheduleResponse = backupScheduleSchema;
|
||||
|
||||
export type CreateBackupScheduleDto = typeof createBackupScheduleResponse.infer;
|
||||
|
||||
@@ -156,10 +149,9 @@ export const updateBackupScheduleBody = type({
|
||||
|
||||
export type UpdateBackupScheduleBody = typeof updateBackupScheduleBody.infer;
|
||||
|
||||
export const updateBackupScheduleResponse = type({
|
||||
message: "string",
|
||||
schedule: backupScheduleSchema,
|
||||
});
|
||||
export const updateBackupScheduleResponse = backupScheduleSchema;
|
||||
|
||||
export type UpdateBackupScheduleDto = typeof updateBackupScheduleResponse.infer;
|
||||
|
||||
export const updateBackupScheduleDto = describeRoute({
|
||||
description: "Update a backup schedule",
|
||||
@@ -181,9 +173,11 @@ export const updateBackupScheduleDto = describeRoute({
|
||||
* Delete a backup schedule
|
||||
*/
|
||||
export const deleteBackupScheduleResponse = type({
|
||||
message: "string",
|
||||
success: "boolean",
|
||||
});
|
||||
|
||||
export type DeleteBackupScheduleDto = typeof deleteBackupScheduleResponse.infer;
|
||||
|
||||
export const deleteBackupScheduleDto = describeRoute({
|
||||
description: "Delete a backup schedule",
|
||||
operationId: "deleteBackupSchedule",
|
||||
@@ -204,10 +198,11 @@ export const deleteBackupScheduleDto = describeRoute({
|
||||
* Run a backup immediately
|
||||
*/
|
||||
export const runBackupNowResponse = type({
|
||||
message: "string",
|
||||
backupStarted: "boolean",
|
||||
success: "boolean",
|
||||
});
|
||||
|
||||
export type RunBackupNowDto = typeof runBackupNowResponse.infer;
|
||||
|
||||
export const runBackupNowDto = describeRoute({
|
||||
description: "Trigger a backup immediately for a schedule",
|
||||
operationId: "runBackupNow",
|
||||
|
||||
@@ -10,19 +10,19 @@ import { getVolumePath } from "../volumes/helpers";
|
||||
import type { CreateBackupScheduleBody, UpdateBackupScheduleBody } from "./backups.dto";
|
||||
import { toMessage } from "../../utils/errors";
|
||||
|
||||
const calculateNextRun = (cronExpression: string): Date => {
|
||||
const calculateNextRun = (cronExpression: string): number => {
|
||||
try {
|
||||
const interval = CronExpressionParser.parse(cronExpression, {
|
||||
currentDate: new Date(),
|
||||
tz: "UTC",
|
||||
});
|
||||
|
||||
return interval.next().toDate();
|
||||
return interval.next().getTime();
|
||||
} catch (error) {
|
||||
logger.error(`Failed to parse cron expression "${cronExpression}": ${error}`);
|
||||
const fallback = new Date();
|
||||
fallback.setMinutes(fallback.getMinutes() + 1);
|
||||
return fallback;
|
||||
return fallback.getTime();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -123,7 +123,7 @@ const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody
|
||||
|
||||
const [updated] = await db
|
||||
.update(backupSchedulesTable)
|
||||
.set({ ...data, nextBackupAt, updatedAt: new Date() })
|
||||
.set({ ...data, nextBackupAt, updatedAt: Date.now() })
|
||||
.where(eq(backupSchedulesTable.id, scheduleId))
|
||||
.returning();
|
||||
|
||||
@@ -204,11 +204,11 @@ const executeBackup = async (scheduleId: number) => {
|
||||
await db
|
||||
.update(backupSchedulesTable)
|
||||
.set({
|
||||
lastBackupAt: new Date(),
|
||||
lastBackupAt: Date.now(),
|
||||
lastBackupStatus: "success",
|
||||
lastBackupError: null,
|
||||
nextBackupAt: nextBackupAt,
|
||||
updatedAt: new Date(),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(backupSchedulesTable.id, scheduleId));
|
||||
|
||||
@@ -219,10 +219,10 @@ const executeBackup = async (scheduleId: number) => {
|
||||
await db
|
||||
.update(backupSchedulesTable)
|
||||
.set({
|
||||
lastBackupAt: new Date(),
|
||||
lastBackupAt: Date.now(),
|
||||
lastBackupStatus: "error",
|
||||
lastBackupError: toMessage(error),
|
||||
updatedAt: new Date(),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(backupSchedulesTable.id, scheduleId));
|
||||
|
||||
@@ -231,7 +231,7 @@ const executeBackup = async (scheduleId: number) => {
|
||||
};
|
||||
|
||||
const getSchedulesToExecute = async () => {
|
||||
const now = new Date();
|
||||
const now = Date.now();
|
||||
const schedules = await db.query.backupSchedulesTable.findMany({
|
||||
where: eq(backupSchedulesTable.enabled, true),
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import { volumeService } from "../volumes/volume.service";
|
||||
import { CleanupDanglingMountsJob } from "../../jobs/cleanup-dangling";
|
||||
import { VolumeHealthCheckJob } from "../../jobs/healthchecks";
|
||||
import { BackupExecutionJob } from "../../jobs/backup-execution";
|
||||
import { CleanupSessionsJob } from "../../jobs/cleanup-sessions";
|
||||
|
||||
export const startup = async () => {
|
||||
await Scheduler.start();
|
||||
@@ -30,6 +31,7 @@ export const startup = async () => {
|
||||
}
|
||||
|
||||
Scheduler.build(CleanupDanglingMountsJob).schedule("0 * * * *");
|
||||
Scheduler.build(VolumeHealthCheckJob).schedule("* * * * *");
|
||||
Scheduler.build(VolumeHealthCheckJob).schedule("*/5 * * * *");
|
||||
Scheduler.build(BackupExecutionJob).schedule("* * * * *");
|
||||
Scheduler.build(CleanupSessionsJob).schedule("0 0 * * *");
|
||||
};
|
||||
|
||||
@@ -5,11 +5,12 @@ import {
|
||||
createRepositoryDto,
|
||||
deleteRepositoryDto,
|
||||
getRepositoryDto,
|
||||
type GetRepositoryResponseDto,
|
||||
type ListRepositoriesResponseDto,
|
||||
listRepositoriesDto,
|
||||
listSnapshotsDto,
|
||||
type ListSnapshotsResponseDto,
|
||||
type DeleteRepositoryDto,
|
||||
type GetRepositoryDto,
|
||||
type ListRepositoriesDto,
|
||||
type ListSnapshotsDto,
|
||||
} from "./repositories.dto";
|
||||
import { repositoriesService } from "./repositories.service";
|
||||
|
||||
@@ -17,16 +18,7 @@ export const repositoriesController = new Hono()
|
||||
.get("/", listRepositoriesDto, async (c) => {
|
||||
const repositories = await repositoriesService.listRepositories();
|
||||
|
||||
const response = {
|
||||
repositories: repositories.map((repository) => ({
|
||||
...repository,
|
||||
updatedAt: repository.updatedAt.getTime(),
|
||||
createdAt: repository.createdAt.getTime(),
|
||||
lastChecked: repository.lastChecked?.getTime() ?? null,
|
||||
})),
|
||||
} satisfies ListRepositoriesResponseDto;
|
||||
|
||||
return c.json(response, 200);
|
||||
return c.json<ListRepositoriesDto>(repositories, 200);
|
||||
})
|
||||
.post("/", createRepositoryDto, validator("json", createRepositoryBody), async (c) => {
|
||||
const body = c.req.valid("json");
|
||||
@@ -38,22 +30,13 @@ export const repositoriesController = new Hono()
|
||||
const { name } = c.req.param();
|
||||
const res = await repositoriesService.getRepository(name);
|
||||
|
||||
const response = {
|
||||
repository: {
|
||||
...res.repository,
|
||||
createdAt: res.repository.createdAt.getTime(),
|
||||
updatedAt: res.repository.updatedAt.getTime(),
|
||||
lastChecked: res.repository.lastChecked?.getTime() ?? null,
|
||||
},
|
||||
} satisfies GetRepositoryResponseDto;
|
||||
|
||||
return c.json(response, 200);
|
||||
return c.json<GetRepositoryDto>(res.repository, 200);
|
||||
})
|
||||
.delete("/:name", deleteRepositoryDto, async (c) => {
|
||||
const { name } = c.req.param();
|
||||
await repositoriesService.deleteRepository(name);
|
||||
|
||||
return c.json({ message: "Repository deleted" }, 200);
|
||||
return c.json<DeleteRepositoryDto>({ message: "Repository deleted" }, 200);
|
||||
})
|
||||
.get("/:name/snapshots", listSnapshotsDto, async (c) => {
|
||||
const { name } = c.req.param();
|
||||
@@ -77,9 +60,9 @@ export const repositoriesController = new Hono()
|
||||
};
|
||||
});
|
||||
|
||||
const response = { snapshots } satisfies ListSnapshotsResponseDto;
|
||||
const response = { snapshots };
|
||||
|
||||
c.header("Cache-Control", "max-age=30, stale-while-revalidate=300");
|
||||
|
||||
return c.json(response, 200);
|
||||
return c.json<ListSnapshotsDto>(response, 200);
|
||||
});
|
||||
|
||||
@@ -25,10 +25,8 @@ export type RepositoryDto = typeof repositorySchema.infer;
|
||||
/**
|
||||
* List all repositories
|
||||
*/
|
||||
export const listRepositoriesResponse = type({
|
||||
repositories: repositorySchema.array(),
|
||||
});
|
||||
export type ListRepositoriesResponseDto = typeof listRepositoriesResponse.infer;
|
||||
export const listRepositoriesResponse = repositorySchema.array();
|
||||
export type ListRepositoriesDto = typeof listRepositoriesResponse.infer;
|
||||
|
||||
export const listRepositoriesDto = describeRoute({
|
||||
description: "List all repositories",
|
||||
@@ -65,6 +63,8 @@ export const createRepositoryResponse = type({
|
||||
}),
|
||||
});
|
||||
|
||||
export type CreateRepositoryDto = typeof createRepositoryResponse.infer;
|
||||
|
||||
export const createRepositoryDto = describeRoute({
|
||||
description: "Create a new restic repository",
|
||||
operationId: "createRepository",
|
||||
@@ -84,10 +84,8 @@ export const createRepositoryDto = describeRoute({
|
||||
/**
|
||||
* Get a single repository
|
||||
*/
|
||||
export const getRepositoryResponse = type({
|
||||
repository: repositorySchema,
|
||||
});
|
||||
export type GetRepositoryResponseDto = typeof getRepositoryResponse.infer;
|
||||
export const getRepositoryResponse = repositorySchema;
|
||||
export type GetRepositoryDto = typeof getRepositoryResponse.infer;
|
||||
|
||||
export const getRepositoryDto = describeRoute({
|
||||
description: "Get a single repository by name",
|
||||
@@ -112,6 +110,8 @@ export const deleteRepositoryResponse = type({
|
||||
message: "string",
|
||||
});
|
||||
|
||||
export type DeleteRepositoryDto = typeof deleteRepositoryResponse.infer;
|
||||
|
||||
export const deleteRepositoryDto = describeRoute({
|
||||
description: "Delete a repository",
|
||||
tags: ["Repositories"],
|
||||
@@ -143,7 +143,7 @@ const listSnapshotsResponse = type({
|
||||
snapshots: snapshotSchema.array(),
|
||||
});
|
||||
|
||||
export type ListSnapshotsResponseDto = typeof listSnapshotsResponse.infer;
|
||||
export type ListSnapshotsDto = typeof listSnapshotsResponse.infer;
|
||||
|
||||
export const listSnapshotsDto = describeRoute({
|
||||
description: "List all snapshots in a repository",
|
||||
|
||||
@@ -65,7 +65,7 @@ const createRepository = async (name: string, config: RepositoryConfig, compress
|
||||
.update(repositoriesTable)
|
||||
.set({
|
||||
status: "healthy",
|
||||
lastChecked: new Date(),
|
||||
lastChecked: Date.now(),
|
||||
lastError: null,
|
||||
})
|
||||
.where(eq(repositoriesTable.id, id));
|
||||
|
||||
@@ -4,22 +4,23 @@ import {
|
||||
createVolumeBody,
|
||||
createVolumeDto,
|
||||
deleteVolumeDto,
|
||||
type GetVolumeResponseDto,
|
||||
getContainersDto,
|
||||
getVolumeDto,
|
||||
healthCheckDto,
|
||||
type ListContainersResponseDto,
|
||||
type ListFilesResponseDto,
|
||||
type ListVolumesResponseDto,
|
||||
type ListVolumesDto,
|
||||
listFilesDto,
|
||||
listVolumesDto,
|
||||
mountVolumeDto,
|
||||
testConnectionBody,
|
||||
testConnectionDto,
|
||||
type UpdateVolumeResponseDto,
|
||||
unmountVolumeDto,
|
||||
updateVolumeBody,
|
||||
updateVolumeDto,
|
||||
type CreateVolumeDto,
|
||||
type GetVolumeDto,
|
||||
type ListContainersDto,
|
||||
type UpdateVolumeDto,
|
||||
type ListFilesDto,
|
||||
} from "./volume.dto";
|
||||
import { volumeService } from "./volume.service";
|
||||
import { getVolumePath } from "./helpers";
|
||||
@@ -32,19 +33,21 @@ export const volumeController = new Hono()
|
||||
volumes: volumes.map((volume) => ({
|
||||
path: getVolumePath(volume.name),
|
||||
...volume,
|
||||
updatedAt: volume.updatedAt.getTime(),
|
||||
createdAt: volume.createdAt.getTime(),
|
||||
lastHealthCheck: volume.lastHealthCheck.getTime(),
|
||||
})),
|
||||
} satisfies ListVolumesResponseDto;
|
||||
};
|
||||
|
||||
return c.json(response, 200);
|
||||
return c.json<ListVolumesDto>(response, 200);
|
||||
})
|
||||
.post("/", createVolumeDto, validator("json", createVolumeBody), async (c) => {
|
||||
const body = c.req.valid("json");
|
||||
const res = await volumeService.createVolume(body.name, body.config);
|
||||
|
||||
return c.json({ message: "Volume created", volume: res.volume }, 201);
|
||||
const response = {
|
||||
...res.volume,
|
||||
path: getVolumePath(res.volume.name),
|
||||
};
|
||||
|
||||
return c.json<CreateVolumeDto>(response, 201);
|
||||
})
|
||||
.post("/test-connection", testConnectionDto, validator("json", testConnectionBody), async (c) => {
|
||||
const body = c.req.valid("json");
|
||||
@@ -66,28 +69,21 @@ export const volumeController = new Hono()
|
||||
volume: {
|
||||
...res.volume,
|
||||
path: getVolumePath(res.volume.name),
|
||||
createdAt: res.volume.createdAt.getTime(),
|
||||
updatedAt: res.volume.updatedAt.getTime(),
|
||||
lastHealthCheck: res.volume.lastHealthCheck.getTime(),
|
||||
},
|
||||
statfs: {
|
||||
total: res.statfs.total ?? 0,
|
||||
used: res.statfs.used ?? 0,
|
||||
free: res.statfs.free ?? 0,
|
||||
},
|
||||
} satisfies GetVolumeResponseDto;
|
||||
};
|
||||
|
||||
return c.json(response, 200);
|
||||
return c.json<GetVolumeDto>(response, 200);
|
||||
})
|
||||
.get("/:name/containers", getContainersDto, async (c) => {
|
||||
const { name } = c.req.param();
|
||||
const { containers } = await volumeService.getContainersUsingVolume(name);
|
||||
|
||||
const response = {
|
||||
containers,
|
||||
} satisfies ListContainersResponseDto;
|
||||
|
||||
return c.json(response, 200);
|
||||
return c.json<ListContainersDto>(containers, 200);
|
||||
})
|
||||
.put("/:name", updateVolumeDto, validator("json", updateVolumeBody), async (c) => {
|
||||
const { name } = c.req.param();
|
||||
@@ -95,17 +91,11 @@ export const volumeController = new Hono()
|
||||
const res = await volumeService.updateVolume(name, body);
|
||||
|
||||
const response = {
|
||||
message: "Volume updated",
|
||||
volume: {
|
||||
...res.volume,
|
||||
path: getVolumePath(res.volume.name),
|
||||
createdAt: res.volume.createdAt.getTime(),
|
||||
updatedAt: res.volume.updatedAt.getTime(),
|
||||
lastHealthCheck: res.volume.lastHealthCheck.getTime(),
|
||||
},
|
||||
} satisfies UpdateVolumeResponseDto;
|
||||
...res.volume,
|
||||
path: getVolumePath(res.volume.name),
|
||||
};
|
||||
|
||||
return c.json(response, 200);
|
||||
return c.json<UpdateVolumeDto>(response, 200);
|
||||
})
|
||||
.post("/:name/mount", mountVolumeDto, async (c) => {
|
||||
const { name } = c.req.param();
|
||||
@@ -133,9 +123,9 @@ export const volumeController = new Hono()
|
||||
const response = {
|
||||
files: result.files,
|
||||
path: result.path,
|
||||
} satisfies ListFilesResponseDto;
|
||||
};
|
||||
|
||||
c.header("Cache-Control", "public, max-age=10, stale-while-revalidate=60");
|
||||
|
||||
return c.json(response, 200);
|
||||
return c.json<ListFilesDto>(response, 200);
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ export type VolumeDto = typeof volumeSchema.infer;
|
||||
export const listVolumesResponse = type({
|
||||
volumes: volumeSchema.array(),
|
||||
});
|
||||
export type ListVolumesResponseDto = typeof listVolumesResponse.infer;
|
||||
export type ListVolumesDto = typeof listVolumesResponse.infer;
|
||||
|
||||
export const listVolumesDto = describeRoute({
|
||||
description: "List all volumes",
|
||||
@@ -50,13 +50,8 @@ export const createVolumeBody = type({
|
||||
config: volumeConfigSchema,
|
||||
});
|
||||
|
||||
export const createVolumeResponse = type({
|
||||
message: "string",
|
||||
volume: type({
|
||||
name: "string",
|
||||
path: "string",
|
||||
}),
|
||||
});
|
||||
export const createVolumeResponse = volumeSchema;
|
||||
export type CreateVolumeDto = typeof createVolumeResponse.infer;
|
||||
|
||||
export const createVolumeDto = describeRoute({
|
||||
description: "Create a new volume",
|
||||
@@ -80,6 +75,7 @@ export const createVolumeDto = describeRoute({
|
||||
export const deleteVolumeResponse = type({
|
||||
message: "string",
|
||||
});
|
||||
export type DeleteVolumeDto = typeof deleteVolumeResponse.infer;
|
||||
|
||||
export const deleteVolumeDto = describeRoute({
|
||||
description: "Delete a volume",
|
||||
@@ -108,7 +104,7 @@ const getVolumeResponse = type({
|
||||
statfs: statfsSchema,
|
||||
});
|
||||
|
||||
export type GetVolumeResponseDto = typeof getVolumeResponse.infer;
|
||||
export type GetVolumeDto = typeof getVolumeResponse.infer;
|
||||
/**
|
||||
* Get a volume
|
||||
*/
|
||||
@@ -141,10 +137,8 @@ export const updateVolumeBody = type({
|
||||
|
||||
export type UpdateVolumeBody = typeof updateVolumeBody.infer;
|
||||
|
||||
export const updateVolumeResponse = type({
|
||||
message: "string",
|
||||
volume: volumeSchema,
|
||||
});
|
||||
export const updateVolumeResponse = volumeSchema;
|
||||
export type UpdateVolumeDto = typeof updateVolumeResponse.infer;
|
||||
|
||||
export const updateVolumeDto = describeRoute({
|
||||
description: "Update a volume's configuration",
|
||||
@@ -165,8 +159,6 @@ export const updateVolumeDto = describeRoute({
|
||||
},
|
||||
});
|
||||
|
||||
export type UpdateVolumeResponseDto = typeof updateVolumeResponse.infer;
|
||||
|
||||
/**
|
||||
* Test connection
|
||||
*/
|
||||
@@ -178,6 +170,7 @@ export const testConnectionResponse = type({
|
||||
success: "boolean",
|
||||
message: "string",
|
||||
});
|
||||
export type TestConnectionDto = typeof testConnectionResponse.infer;
|
||||
|
||||
export const testConnectionDto = describeRoute({
|
||||
description: "Test connection to backend",
|
||||
@@ -202,6 +195,7 @@ export const mountVolumeResponse = type({
|
||||
error: "string?",
|
||||
status: type.valueOf(BACKEND_STATUS),
|
||||
});
|
||||
export type MountVolumeDto = typeof mountVolumeResponse.infer;
|
||||
|
||||
export const mountVolumeDto = describeRoute({
|
||||
description: "Mount a volume",
|
||||
@@ -216,9 +210,6 @@ export const mountVolumeDto = describeRoute({
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Volume not found",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -229,6 +220,7 @@ export const unmountVolumeResponse = type({
|
||||
error: "string?",
|
||||
status: type.valueOf(BACKEND_STATUS),
|
||||
});
|
||||
export type UnmountVolumeDto = typeof unmountVolumeResponse.infer;
|
||||
|
||||
export const unmountVolumeDto = describeRoute({
|
||||
description: "Unmount a volume",
|
||||
@@ -243,9 +235,6 @@ export const unmountVolumeDto = describeRoute({
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Volume not found",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -253,6 +242,7 @@ export const healthCheckResponse = type({
|
||||
error: "string?",
|
||||
status: type.valueOf(BACKEND_STATUS),
|
||||
});
|
||||
export type HealthCheckDto = typeof healthCheckResponse.infer;
|
||||
|
||||
export const healthCheckDto = describeRoute({
|
||||
description: "Perform a health check on a volume",
|
||||
@@ -283,10 +273,8 @@ const containerSchema = type({
|
||||
image: "string",
|
||||
});
|
||||
|
||||
export const listContainersResponse = type({
|
||||
containers: containerSchema.array(),
|
||||
});
|
||||
export type ListContainersResponseDto = typeof listContainersResponse.infer;
|
||||
export const listContainersResponse = containerSchema.array();
|
||||
export type ListContainersDto = typeof listContainersResponse.infer;
|
||||
|
||||
export const getContainersDto = describeRoute({
|
||||
description: "Get containers using a volume by name",
|
||||
@@ -322,7 +310,7 @@ export const listFilesResponse = type({
|
||||
files: fileEntrySchema.array(),
|
||||
path: "string",
|
||||
});
|
||||
export type ListFilesResponseDto = typeof listFilesResponse.infer;
|
||||
export type ListFilesDto = typeof listFilesResponse.infer;
|
||||
|
||||
export const listFilesDto = describeRoute({
|
||||
description: "List files in a volume directory",
|
||||
@@ -348,8 +336,5 @@ export const listFilesDto = describeRoute({
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Volume not found",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ import Docker from "dockerode";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { ConflictError, InternalServerError, NotFoundError } from "http-errors-enhanced";
|
||||
import slugify from "slugify";
|
||||
import { VOLUME_MOUNT_BASE } from "../../core/constants";
|
||||
import { db } from "../../db/db";
|
||||
import { volumesTable } from "../../db/schema";
|
||||
import { toMessage } from "../../utils/errors";
|
||||
@@ -51,7 +50,7 @@ const createVolume = async (name: string, backendConfig: BackendConfig) => {
|
||||
|
||||
await db
|
||||
.update(volumesTable)
|
||||
.set({ status, lastError: error ?? null, lastHealthCheck: new Date() })
|
||||
.set({ status, lastError: error ?? null, lastHealthCheck: Date.now() })
|
||||
.where(eq(volumesTable.name, slug));
|
||||
|
||||
return { volume: created, status: 201 };
|
||||
@@ -85,7 +84,7 @@ const mountVolume = async (name: string) => {
|
||||
|
||||
await db
|
||||
.update(volumesTable)
|
||||
.set({ status, lastError: error ?? null, lastHealthCheck: new Date() })
|
||||
.set({ status, lastError: error ?? null, lastHealthCheck: Date.now() })
|
||||
.where(eq(volumesTable.name, name));
|
||||
|
||||
return { error, status };
|
||||
@@ -149,7 +148,7 @@ const updateVolume = async (name: string, volumeData: UpdateVolumeBody) => {
|
||||
config: volumeData.config,
|
||||
type: volumeData.config?.backend,
|
||||
autoRemount: volumeData.autoRemount,
|
||||
updatedAt: new Date(),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(volumesTable.name, name))
|
||||
.returning();
|
||||
@@ -163,7 +162,7 @@ const updateVolume = async (name: string, volumeData: UpdateVolumeBody) => {
|
||||
const { error, status } = await backend.mount();
|
||||
await db
|
||||
.update(volumesTable)
|
||||
.set({ status, lastError: error ?? null, lastHealthCheck: new Date() })
|
||||
.set({ status, lastError: error ?? null, lastHealthCheck: Date.now() })
|
||||
.where(eq(volumesTable.name, name));
|
||||
}
|
||||
|
||||
@@ -178,9 +177,9 @@ const testConnection = async (backendConfig: BackendConfig) => {
|
||||
name: "test-connection",
|
||||
path: tempDir,
|
||||
config: backendConfig,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
lastHealthCheck: new Date(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
lastHealthCheck: Date.now(),
|
||||
type: backendConfig.backend,
|
||||
status: "unmounted" as const,
|
||||
lastError: null,
|
||||
@@ -215,7 +214,7 @@ const checkHealth = async (name: string) => {
|
||||
|
||||
await db
|
||||
.update(volumesTable)
|
||||
.set({ lastHealthCheck: new Date(), status, lastError: error ?? null })
|
||||
.set({ lastHealthCheck: Date.now(), status, lastError: error ?? null })
|
||||
.where(eq(volumesTable.name, volume.name));
|
||||
|
||||
return { status, error };
|
||||
|
||||
Reference in New Issue
Block a user