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:
@@ -12,7 +12,6 @@ import type {
|
|||||||
LogoutResponses,
|
LogoutResponses,
|
||||||
GetMeData,
|
GetMeData,
|
||||||
GetMeResponses,
|
GetMeResponses,
|
||||||
GetMeErrors,
|
|
||||||
GetStatusData,
|
GetStatusData,
|
||||||
GetStatusResponses,
|
GetStatusResponses,
|
||||||
ListVolumesData,
|
ListVolumesData,
|
||||||
@@ -34,16 +33,13 @@ import type {
|
|||||||
GetContainersUsingVolumeErrors,
|
GetContainersUsingVolumeErrors,
|
||||||
MountVolumeData,
|
MountVolumeData,
|
||||||
MountVolumeResponses,
|
MountVolumeResponses,
|
||||||
MountVolumeErrors,
|
|
||||||
UnmountVolumeData,
|
UnmountVolumeData,
|
||||||
UnmountVolumeResponses,
|
UnmountVolumeResponses,
|
||||||
UnmountVolumeErrors,
|
|
||||||
HealthCheckVolumeData,
|
HealthCheckVolumeData,
|
||||||
HealthCheckVolumeResponses,
|
HealthCheckVolumeResponses,
|
||||||
HealthCheckVolumeErrors,
|
HealthCheckVolumeErrors,
|
||||||
ListFilesData,
|
ListFilesData,
|
||||||
ListFilesResponses,
|
ListFilesResponses,
|
||||||
ListFilesErrors,
|
|
||||||
ListRepositoriesData,
|
ListRepositoriesData,
|
||||||
ListRepositoriesResponses,
|
ListRepositoriesResponses,
|
||||||
CreateRepositoryData,
|
CreateRepositoryData,
|
||||||
@@ -130,7 +126,7 @@ export const logout = <ThrowOnError extends boolean = false>(options?: Options<L
|
|||||||
* Get current authenticated user
|
* Get current authenticated user
|
||||||
*/
|
*/
|
||||||
export const getMe = <ThrowOnError extends boolean = false>(options?: Options<GetMeData, ThrowOnError>) => {
|
export const getMe = <ThrowOnError extends boolean = false>(options?: Options<GetMeData, ThrowOnError>) => {
|
||||||
return (options?.client ?? _heyApiClient).get<GetMeResponses, GetMeErrors, ThrowOnError>({
|
return (options?.client ?? _heyApiClient).get<GetMeResponses, unknown, ThrowOnError>({
|
||||||
url: "/api/v1/auth/me",
|
url: "/api/v1/auth/me",
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
@@ -246,7 +242,7 @@ export const getContainersUsingVolume = <ThrowOnError extends boolean = false>(
|
|||||||
* Mount a volume
|
* Mount a volume
|
||||||
*/
|
*/
|
||||||
export const mountVolume = <ThrowOnError extends boolean = false>(options: Options<MountVolumeData, ThrowOnError>) => {
|
export const mountVolume = <ThrowOnError extends boolean = false>(options: Options<MountVolumeData, ThrowOnError>) => {
|
||||||
return (options.client ?? _heyApiClient).post<MountVolumeResponses, MountVolumeErrors, ThrowOnError>({
|
return (options.client ?? _heyApiClient).post<MountVolumeResponses, unknown, ThrowOnError>({
|
||||||
url: "/api/v1/volumes/{name}/mount",
|
url: "/api/v1/volumes/{name}/mount",
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
@@ -258,7 +254,7 @@ export const mountVolume = <ThrowOnError extends boolean = false>(options: Optio
|
|||||||
export const unmountVolume = <ThrowOnError extends boolean = false>(
|
export const unmountVolume = <ThrowOnError extends boolean = false>(
|
||||||
options: Options<UnmountVolumeData, ThrowOnError>,
|
options: Options<UnmountVolumeData, ThrowOnError>,
|
||||||
) => {
|
) => {
|
||||||
return (options.client ?? _heyApiClient).post<UnmountVolumeResponses, UnmountVolumeErrors, ThrowOnError>({
|
return (options.client ?? _heyApiClient).post<UnmountVolumeResponses, unknown, ThrowOnError>({
|
||||||
url: "/api/v1/volumes/{name}/unmount",
|
url: "/api/v1/volumes/{name}/unmount",
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
@@ -280,7 +276,7 @@ export const healthCheckVolume = <ThrowOnError extends boolean = false>(
|
|||||||
* List files in a volume directory
|
* List files in a volume directory
|
||||||
*/
|
*/
|
||||||
export const listFiles = <ThrowOnError extends boolean = false>(options: Options<ListFilesData, ThrowOnError>) => {
|
export const listFiles = <ThrowOnError extends boolean = false>(options: Options<ListFilesData, ThrowOnError>) => {
|
||||||
return (options.client ?? _heyApiClient).get<ListFilesResponses, ListFilesErrors, ThrowOnError>({
|
return (options.client ?? _heyApiClient).get<ListFilesResponses, unknown, ThrowOnError>({
|
||||||
url: "/api/v1/volumes/{name}/files",
|
url: "/api/v1/volumes/{name}/files",
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ export type RegisterResponses = {
|
|||||||
*/
|
*/
|
||||||
201: {
|
201: {
|
||||||
message: string;
|
message: string;
|
||||||
user: {
|
success: boolean;
|
||||||
id: string;
|
user?: {
|
||||||
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -55,8 +56,9 @@ export type LoginResponses = {
|
|||||||
*/
|
*/
|
||||||
200: {
|
200: {
|
||||||
message: string;
|
message: string;
|
||||||
user: {
|
success: boolean;
|
||||||
id: string;
|
user?: {
|
||||||
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -76,7 +78,7 @@ export type LogoutResponses = {
|
|||||||
* Logout successful
|
* Logout successful
|
||||||
*/
|
*/
|
||||||
200: {
|
200: {
|
||||||
message: string;
|
success: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,21 +91,15 @@ export type GetMeData = {
|
|||||||
url: "/api/v1/auth/me";
|
url: "/api/v1/auth/me";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetMeErrors = {
|
|
||||||
/**
|
|
||||||
* Not authenticated
|
|
||||||
*/
|
|
||||||
401: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetMeResponses = {
|
export type GetMeResponses = {
|
||||||
/**
|
/**
|
||||||
* Current user information
|
* Current user information
|
||||||
*/
|
*/
|
||||||
200: {
|
200: {
|
||||||
message: string;
|
message: string;
|
||||||
user: {
|
success: boolean;
|
||||||
id: string;
|
user?: {
|
||||||
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -232,11 +228,46 @@ export type CreateVolumeResponses = {
|
|||||||
* Volume created successfully
|
* Volume created successfully
|
||||||
*/
|
*/
|
||||||
201: {
|
201: {
|
||||||
message: string;
|
autoRemount: boolean;
|
||||||
volume: {
|
config:
|
||||||
|
| {
|
||||||
|
backend: "directory";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
backend: "nfs";
|
||||||
|
exportPath: string;
|
||||||
|
server: string;
|
||||||
|
version: "3" | "4" | "4.1";
|
||||||
|
port?: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
backend: "smb";
|
||||||
|
password: string;
|
||||||
|
server: string;
|
||||||
|
share: string;
|
||||||
|
username: string;
|
||||||
|
vers?: "1.0" | "2.0" | "2.1" | "3.0";
|
||||||
|
port?: number;
|
||||||
|
domain?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
backend: "webdav";
|
||||||
|
path: string;
|
||||||
|
server: string;
|
||||||
|
port?: number;
|
||||||
|
password?: string;
|
||||||
|
ssl?: boolean;
|
||||||
|
username?: string;
|
||||||
|
};
|
||||||
|
createdAt: number;
|
||||||
|
id: number;
|
||||||
|
lastError: string | null;
|
||||||
|
lastHealthCheck: number;
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
};
|
status: "error" | "mounted" | "unmounted";
|
||||||
|
type: "directory" | "nfs" | "smb" | "webdav";
|
||||||
|
updatedAt: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -438,8 +469,6 @@ export type UpdateVolumeResponses = {
|
|||||||
* Volume updated successfully
|
* Volume updated successfully
|
||||||
*/
|
*/
|
||||||
200: {
|
200: {
|
||||||
message: string;
|
|
||||||
volume: {
|
|
||||||
autoRemount: boolean;
|
autoRemount: boolean;
|
||||||
config:
|
config:
|
||||||
| {
|
| {
|
||||||
@@ -481,7 +510,6 @@ export type UpdateVolumeResponses = {
|
|||||||
type: "directory" | "nfs" | "smb" | "webdav";
|
type: "directory" | "nfs" | "smb" | "webdav";
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateVolumeResponse = UpdateVolumeResponses[keyof UpdateVolumeResponses];
|
export type UpdateVolumeResponse = UpdateVolumeResponses[keyof UpdateVolumeResponses];
|
||||||
@@ -506,14 +534,12 @@ export type GetContainersUsingVolumeResponses = {
|
|||||||
/**
|
/**
|
||||||
* List of containers using the volume
|
* List of containers using the volume
|
||||||
*/
|
*/
|
||||||
200: {
|
200: Array<{
|
||||||
containers: Array<{
|
|
||||||
id: string;
|
id: string;
|
||||||
image: string;
|
image: string;
|
||||||
name: string;
|
name: string;
|
||||||
state: string;
|
state: string;
|
||||||
}>;
|
}>;
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetContainersUsingVolumeResponse =
|
export type GetContainersUsingVolumeResponse =
|
||||||
@@ -528,13 +554,6 @@ export type MountVolumeData = {
|
|||||||
url: "/api/v1/volumes/{name}/mount";
|
url: "/api/v1/volumes/{name}/mount";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MountVolumeErrors = {
|
|
||||||
/**
|
|
||||||
* Volume not found
|
|
||||||
*/
|
|
||||||
404: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MountVolumeResponses = {
|
export type MountVolumeResponses = {
|
||||||
/**
|
/**
|
||||||
* Volume mounted successfully
|
* Volume mounted successfully
|
||||||
@@ -556,13 +575,6 @@ export type UnmountVolumeData = {
|
|||||||
url: "/api/v1/volumes/{name}/unmount";
|
url: "/api/v1/volumes/{name}/unmount";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UnmountVolumeErrors = {
|
|
||||||
/**
|
|
||||||
* Volume not found
|
|
||||||
*/
|
|
||||||
404: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UnmountVolumeResponses = {
|
export type UnmountVolumeResponses = {
|
||||||
/**
|
/**
|
||||||
* Volume unmounted successfully
|
* Volume unmounted successfully
|
||||||
@@ -617,13 +629,6 @@ export type ListFilesData = {
|
|||||||
url: "/api/v1/volumes/{name}/files";
|
url: "/api/v1/volumes/{name}/files";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ListFilesErrors = {
|
|
||||||
/**
|
|
||||||
* Volume not found
|
|
||||||
*/
|
|
||||||
404: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ListFilesResponses = {
|
export type ListFilesResponses = {
|
||||||
/**
|
/**
|
||||||
* List of files in the volume
|
* List of files in the volume
|
||||||
@@ -653,8 +658,7 @@ export type ListRepositoriesResponses = {
|
|||||||
/**
|
/**
|
||||||
* List of repositories
|
* List of repositories
|
||||||
*/
|
*/
|
||||||
200: {
|
200: Array<{
|
||||||
repositories: Array<{
|
|
||||||
compressionMode: "auto" | "better" | "fastest" | "max" | "off" | null;
|
compressionMode: "auto" | "better" | "fastest" | "max" | "off" | null;
|
||||||
config:
|
config:
|
||||||
| {
|
| {
|
||||||
@@ -677,7 +681,6 @@ export type ListRepositoriesResponses = {
|
|||||||
type: "local" | "s3";
|
type: "local" | "s3";
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
}>;
|
}>;
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ListRepositoriesResponse = ListRepositoriesResponses[keyof ListRepositoriesResponses];
|
export type ListRepositoriesResponse = ListRepositoriesResponses[keyof ListRepositoriesResponses];
|
||||||
@@ -753,7 +756,6 @@ export type GetRepositoryResponses = {
|
|||||||
* Repository details
|
* Repository details
|
||||||
*/
|
*/
|
||||||
200: {
|
200: {
|
||||||
repository: {
|
|
||||||
compressionMode: "auto" | "better" | "fastest" | "max" | "off" | null;
|
compressionMode: "auto" | "better" | "fastest" | "max" | "off" | null;
|
||||||
config:
|
config:
|
||||||
| {
|
| {
|
||||||
@@ -776,7 +778,6 @@ export type GetRepositoryResponses = {
|
|||||||
type: "local" | "s3";
|
type: "local" | "s3";
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetRepositoryResponse = GetRepositoryResponses[keyof GetRepositoryResponses];
|
export type GetRepositoryResponse = GetRepositoryResponses[keyof GetRepositoryResponses];
|
||||||
@@ -818,8 +819,7 @@ export type ListBackupSchedulesResponses = {
|
|||||||
/**
|
/**
|
||||||
* List of backup schedules
|
* List of backup schedules
|
||||||
*/
|
*/
|
||||||
200: {
|
200: Array<{
|
||||||
schedules: Array<{
|
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
cronExpression: string;
|
cronExpression: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -843,7 +843,6 @@ export type ListBackupSchedulesResponses = {
|
|||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
volumeId: number;
|
volumeId: number;
|
||||||
}>;
|
}>;
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ListBackupSchedulesResponse = ListBackupSchedulesResponses[keyof ListBackupSchedulesResponses];
|
export type ListBackupSchedulesResponse = ListBackupSchedulesResponses[keyof ListBackupSchedulesResponses];
|
||||||
@@ -877,8 +876,6 @@ export type CreateBackupScheduleResponses = {
|
|||||||
* Backup schedule created successfully
|
* Backup schedule created successfully
|
||||||
*/
|
*/
|
||||||
201: {
|
201: {
|
||||||
message: string;
|
|
||||||
schedule: {
|
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
cronExpression: string;
|
cronExpression: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -902,7 +899,6 @@ export type CreateBackupScheduleResponses = {
|
|||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
volumeId: number;
|
volumeId: number;
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateBackupScheduleResponse = CreateBackupScheduleResponses[keyof CreateBackupScheduleResponses];
|
export type CreateBackupScheduleResponse = CreateBackupScheduleResponses[keyof CreateBackupScheduleResponses];
|
||||||
@@ -921,7 +917,7 @@ export type DeleteBackupScheduleResponses = {
|
|||||||
* Backup schedule deleted successfully
|
* Backup schedule deleted successfully
|
||||||
*/
|
*/
|
||||||
200: {
|
200: {
|
||||||
message: string;
|
success: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -941,7 +937,6 @@ export type GetBackupScheduleResponses = {
|
|||||||
* Backup schedule details
|
* Backup schedule details
|
||||||
*/
|
*/
|
||||||
200: {
|
200: {
|
||||||
schedule: {
|
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
cronExpression: string;
|
cronExpression: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -965,7 +960,6 @@ export type GetBackupScheduleResponses = {
|
|||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
volumeId: number;
|
volumeId: number;
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetBackupScheduleResponse = GetBackupScheduleResponses[keyof GetBackupScheduleResponses];
|
export type GetBackupScheduleResponse = GetBackupScheduleResponses[keyof GetBackupScheduleResponses];
|
||||||
@@ -1000,8 +994,6 @@ export type UpdateBackupScheduleResponses = {
|
|||||||
* Backup schedule updated successfully
|
* Backup schedule updated successfully
|
||||||
*/
|
*/
|
||||||
200: {
|
200: {
|
||||||
message: string;
|
|
||||||
schedule: {
|
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
cronExpression: string;
|
cronExpression: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -1025,7 +1017,6 @@ export type UpdateBackupScheduleResponses = {
|
|||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
volumeId: number;
|
volumeId: number;
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateBackupScheduleResponse = UpdateBackupScheduleResponses[keyof UpdateBackupScheduleResponses];
|
export type UpdateBackupScheduleResponse = UpdateBackupScheduleResponses[keyof UpdateBackupScheduleResponses];
|
||||||
@@ -1086,8 +1077,7 @@ export type RunBackupNowResponses = {
|
|||||||
* Backup started successfully
|
* Backup started successfully
|
||||||
*/
|
*/
|
||||||
200: {
|
200: {
|
||||||
backupStarted: boolean;
|
success: boolean;
|
||||||
message: string;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,34 @@
|
|||||||
import * as React from "react"
|
// @ts-nocheck
|
||||||
import * as RechartsPrimitive from "recharts"
|
// biome-ignore-all lint: reason
|
||||||
|
import * as React from "react";
|
||||||
|
import * as RechartsPrimitive from "recharts";
|
||||||
|
|
||||||
import { cn } from "~/lib/utils"
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||||
const THEMES = { light: "", dark: ".dark" } as const
|
const THEMES = { light: "", dark: ".dark" } as const;
|
||||||
|
|
||||||
export type ChartConfig = {
|
export type ChartConfig = {
|
||||||
[k in string]: {
|
[k in string]: {
|
||||||
label?: React.ReactNode
|
label?: React.ReactNode;
|
||||||
icon?: React.ComponentType
|
icon?: React.ComponentType;
|
||||||
} & (
|
} & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> });
|
||||||
| { color?: string; theme?: never }
|
};
|
||||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChartContextProps = {
|
type ChartContextProps = {
|
||||||
config: ChartConfig
|
config: ChartConfig;
|
||||||
}
|
};
|
||||||
|
|
||||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||||
|
|
||||||
function useChart() {
|
function useChart() {
|
||||||
const context = React.useContext(ChartContext)
|
const context = React.useContext(ChartContext);
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("useChart must be used within a <ChartContainer />")
|
throw new Error("useChart must be used within a <ChartContainer />");
|
||||||
}
|
}
|
||||||
|
|
||||||
return context
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChartContainer({
|
function ChartContainer({
|
||||||
@@ -39,13 +38,11 @@ function ChartContainer({
|
|||||||
config,
|
config,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & {
|
}: React.ComponentProps<"div"> & {
|
||||||
config: ChartConfig
|
config: ChartConfig;
|
||||||
children: React.ComponentProps<
|
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
|
||||||
typeof RechartsPrimitive.ResponsiveContainer
|
|
||||||
>["children"]
|
|
||||||
}) {
|
}) {
|
||||||
const uniqueId = React.useId()
|
const uniqueId = React.useId();
|
||||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartContext.Provider value={{ config }}>
|
<ChartContext.Provider value={{ config }}>
|
||||||
@@ -54,26 +51,22 @@ function ChartContainer({
|
|||||||
data-chart={chartId}
|
data-chart={chartId}
|
||||||
className={cn(
|
className={cn(
|
||||||
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChartStyle id={chartId} config={config} />
|
<ChartStyle id={chartId} config={config} />
|
||||||
<RechartsPrimitive.ResponsiveContainer>
|
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
|
||||||
{children}
|
|
||||||
</RechartsPrimitive.ResponsiveContainer>
|
|
||||||
</div>
|
</div>
|
||||||
</ChartContext.Provider>
|
</ChartContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||||
const colorConfig = Object.entries(config).filter(
|
const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color);
|
||||||
([, config]) => config.theme || config.color
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!colorConfig.length) {
|
if (!colorConfig.length) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -85,22 +78,20 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
|||||||
${prefix} [data-chart=${id}] {
|
${prefix} [data-chart=${id}] {
|
||||||
${colorConfig
|
${colorConfig
|
||||||
.map(([key, itemConfig]) => {
|
.map(([key, itemConfig]) => {
|
||||||
const color =
|
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
|
||||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
return color ? ` --color-${key}: ${color};` : null;
|
||||||
itemConfig.color
|
|
||||||
return color ? ` --color-${key}: ${color};` : null
|
|
||||||
})
|
})
|
||||||
.join("\n")}
|
.join("\n")}
|
||||||
}
|
}
|
||||||
`
|
`,
|
||||||
)
|
)
|
||||||
.join("\n"),
|
.join("\n"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||||
|
|
||||||
function ChartTooltipContent({
|
function ChartTooltipContent({
|
||||||
active,
|
active,
|
||||||
@@ -118,61 +109,47 @@ function ChartTooltipContent({
|
|||||||
labelKey,
|
labelKey,
|
||||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||||
React.ComponentProps<"div"> & {
|
React.ComponentProps<"div"> & {
|
||||||
hideLabel?: boolean
|
hideLabel?: boolean;
|
||||||
hideIndicator?: boolean
|
hideIndicator?: boolean;
|
||||||
indicator?: "line" | "dot" | "dashed"
|
indicator?: "line" | "dot" | "dashed";
|
||||||
nameKey?: string
|
nameKey?: string;
|
||||||
labelKey?: string
|
labelKey?: string;
|
||||||
}) {
|
}) {
|
||||||
const { config } = useChart()
|
const { config } = useChart();
|
||||||
|
|
||||||
const tooltipLabel = React.useMemo(() => {
|
const tooltipLabel = React.useMemo(() => {
|
||||||
if (hideLabel || !payload?.length) {
|
if (hideLabel || !payload?.length) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [item] = payload
|
const [item] = payload;
|
||||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
|
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
const value =
|
const value =
|
||||||
!labelKey && typeof label === "string"
|
!labelKey && typeof label === "string" ? config[label as keyof typeof config]?.label || label : itemConfig?.label;
|
||||||
? config[label as keyof typeof config]?.label || label
|
|
||||||
: itemConfig?.label
|
|
||||||
|
|
||||||
if (labelFormatter) {
|
if (labelFormatter) {
|
||||||
return (
|
return <div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>;
|
||||||
<div className={cn("font-medium", labelClassName)}>
|
|
||||||
{labelFormatter(value, payload)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
||||||
}, [
|
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);
|
||||||
label,
|
|
||||||
labelFormatter,
|
|
||||||
payload,
|
|
||||||
hideLabel,
|
|
||||||
labelClassName,
|
|
||||||
config,
|
|
||||||
labelKey,
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!active || !payload?.length) {
|
if (!active || !payload?.length) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
const nestLabel = payload.length === 1 && indicator !== "dot";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!nestLabel ? tooltipLabel : null}
|
{!nestLabel ? tooltipLabel : null}
|
||||||
@@ -180,16 +157,16 @@ function ChartTooltipContent({
|
|||||||
{payload
|
{payload
|
||||||
.filter((item) => item.type !== "none")
|
.filter((item) => item.type !== "none")
|
||||||
.map((item, index) => {
|
.map((item, index) => {
|
||||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
const indicatorColor = color || item.payload.fill || item.color
|
const indicatorColor = color || item.payload.fill || item.color;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.dataKey}
|
key={item.dataKey}
|
||||||
className={cn(
|
className={cn(
|
||||||
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
||||||
indicator === "dot" && "items-center"
|
indicator === "dot" && "items-center",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{formatter && item?.value !== undefined && item.name ? (
|
{formatter && item?.value !== undefined && item.name ? (
|
||||||
@@ -201,16 +178,12 @@ function ChartTooltipContent({
|
|||||||
) : (
|
) : (
|
||||||
!hideIndicator && (
|
!hideIndicator && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn("shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)", {
|
||||||
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
|
||||||
{
|
|
||||||
"h-2.5 w-2.5": indicator === "dot",
|
"h-2.5 w-2.5": indicator === "dot",
|
||||||
"w-1": indicator === "line",
|
"w-1": indicator === "line",
|
||||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
"w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
|
||||||
indicator === "dashed",
|
|
||||||
"my-0.5": nestLabel && indicator === "dashed",
|
"my-0.5": nestLabel && indicator === "dashed",
|
||||||
}
|
})}
|
||||||
)}
|
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--color-bg": indicatorColor,
|
"--color-bg": indicatorColor,
|
||||||
@@ -223,14 +196,12 @@ function ChartTooltipContent({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-1 justify-between leading-none",
|
"flex flex-1 justify-between leading-none",
|
||||||
nestLabel ? "items-end" : "items-center"
|
nestLabel ? "items-end" : "items-center",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="grid gap-1.5">
|
<div className="grid gap-1.5">
|
||||||
{nestLabel ? tooltipLabel : null}
|
{nestLabel ? tooltipLabel : null}
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">{itemConfig?.label || item.name}</span>
|
||||||
{itemConfig?.label || item.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{item.value && (
|
{item.value && (
|
||||||
<span className="text-foreground font-mono font-medium tabular-nums">
|
<span className="text-foreground font-mono font-medium tabular-nums">
|
||||||
@@ -241,14 +212,14 @@ function ChartTooltipContent({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChartLegend = RechartsPrimitive.Legend
|
const ChartLegend = RechartsPrimitive.Legend;
|
||||||
|
|
||||||
function ChartLegendContent({
|
function ChartLegendContent({
|
||||||
className,
|
className,
|
||||||
@@ -258,35 +229,27 @@ function ChartLegendContent({
|
|||||||
nameKey,
|
nameKey,
|
||||||
}: React.ComponentProps<"div"> &
|
}: React.ComponentProps<"div"> &
|
||||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||||
hideIcon?: boolean
|
hideIcon?: boolean;
|
||||||
nameKey?: string
|
nameKey?: string;
|
||||||
}) {
|
}) {
|
||||||
const { config } = useChart()
|
const { config } = useChart();
|
||||||
|
|
||||||
if (!payload?.length) {
|
if (!payload?.length) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn("flex items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className)}>
|
||||||
className={cn(
|
|
||||||
"flex items-center justify-center gap-4",
|
|
||||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{payload
|
{payload
|
||||||
.filter((item) => item.type !== "none")
|
.filter((item) => item.type !== "none")
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
const key = `${nameKey || item.dataKey || "value"}`
|
const key = `${nameKey || item.dataKey || "value"}`;
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.value}
|
key={item.value}
|
||||||
className={cn(
|
className={cn("[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3")}
|
||||||
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{itemConfig?.icon && !hideIcon ? (
|
{itemConfig?.icon && !hideIcon ? (
|
||||||
<itemConfig.icon />
|
<itemConfig.icon />
|
||||||
@@ -300,56 +263,36 @@ function ChartLegendContent({
|
|||||||
)}
|
)}
|
||||||
{itemConfig?.label}
|
{itemConfig?.label}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to extract item config from a payload.
|
// Helper to extract item config from a payload.
|
||||||
function getPayloadConfigFromPayload(
|
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
|
||||||
config: ChartConfig,
|
|
||||||
payload: unknown,
|
|
||||||
key: string
|
|
||||||
) {
|
|
||||||
if (typeof payload !== "object" || payload === null) {
|
if (typeof payload !== "object" || payload === null) {
|
||||||
return undefined
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payloadPayload =
|
const payloadPayload =
|
||||||
"payload" in payload &&
|
"payload" in payload && typeof payload.payload === "object" && payload.payload !== null
|
||||||
typeof payload.payload === "object" &&
|
|
||||||
payload.payload !== null
|
|
||||||
? payload.payload
|
? payload.payload
|
||||||
: undefined
|
: undefined;
|
||||||
|
|
||||||
let configLabelKey: string = key
|
let configLabelKey: string = key;
|
||||||
|
|
||||||
if (
|
if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
|
||||||
key in payload &&
|
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||||
typeof payload[key as keyof typeof payload] === "string"
|
|
||||||
) {
|
|
||||||
configLabelKey = payload[key as keyof typeof payload] as string
|
|
||||||
} else if (
|
} else if (
|
||||||
payloadPayload &&
|
payloadPayload &&
|
||||||
key in payloadPayload &&
|
key in payloadPayload &&
|
||||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||||
) {
|
) {
|
||||||
configLabelKey = payloadPayload[
|
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
|
||||||
key as keyof typeof payloadPayload
|
|
||||||
] as string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return configLabelKey in config
|
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
|
||||||
? config[configLabelKey]
|
|
||||||
: config[key as keyof typeof config]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle };
|
||||||
ChartContainer,
|
|
||||||
ChartTooltip,
|
|
||||||
ChartTooltipContent,
|
|
||||||
ChartLegend,
|
|
||||||
ChartLegendContent,
|
|
||||||
ChartStyle,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ export type VolumeStatus = Volume["status"];
|
|||||||
|
|
||||||
export type User = GetMeResponse["user"];
|
export type User = GetMeResponse["user"];
|
||||||
|
|
||||||
export type Repository = GetRepositoryResponse["repository"];
|
export type Repository = GetRepositoryResponse;
|
||||||
|
|
||||||
export type BackupSchedule = GetBackupScheduleResponse["schedule"];
|
export type BackupSchedule = GetBackupScheduleResponse;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const authMiddleware: MiddlewareFunction = async ({ context, request }) =
|
|||||||
|
|
||||||
const isAuthRoute = ["/login", "/onboarding"].includes(new URL(request.url).pathname);
|
const isAuthRoute = ["/login", "/onboarding"].includes(new URL(request.url).pathname);
|
||||||
|
|
||||||
if (!session.data?.user.id && !isAuthRoute) {
|
if (!session.data?.user?.id && !isAuthRoute) {
|
||||||
const status = await getStatus();
|
const status = await getStatus();
|
||||||
if (!status.data?.hasUsers) {
|
if (!status.data?.hasUsers) {
|
||||||
throw redirect("/onboarding");
|
throw redirect("/onboarding");
|
||||||
@@ -16,7 +16,7 @@ export const authMiddleware: MiddlewareFunction = async ({ context, request }) =
|
|||||||
throw redirect("/login");
|
throw redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.data?.user.id) {
|
if (session.data?.user?.id) {
|
||||||
context.set(appContext, { user: session.data.user, hasUsers: true });
|
context.set(appContext, { user: session.data.user, hasUsers: true });
|
||||||
|
|
||||||
if (isAuthRoute) {
|
if (isAuthRoute) {
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export const CreateScheduleForm = ({ initialValues, onSubmit, volume, loading, s
|
|||||||
<SelectValue placeholder="Select a repository" />
|
<SelectValue placeholder="Select a repository" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{repositoriesData?.repositories.map((repo) => (
|
{repositoriesData?.map((repo) => (
|
||||||
<SelectItem key={repo.id} value={repo.id}>
|
<SelectItem key={repo.id} value={repo.id}>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<RepositoryIcon backend={repo.type} />
|
<RepositoryIcon backend={repo.type} />
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const HealthchecksCard = ({ volume }: Props) => {
|
|||||||
...updateVolumeMutation(),
|
...updateVolumeMutation(),
|
||||||
onSuccess: (d) => {
|
onSuccess: (d) => {
|
||||||
toast.success("Volume updated", {
|
toast.success("Volume updated", {
|
||||||
description: `Auto remount is now ${d.volume.autoRemount ? "enabled" : "paused"}.`,
|
description: `Auto remount is now ${d.autoRemount ? "enabled" : "paused"}.`,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const VolumeBackupsTabContent = ({ volume }: Props) => {
|
|||||||
|
|
||||||
const [isEnabled, setIsEnabled] = useState(existingSchedule?.enabled ?? true);
|
const [isEnabled, setIsEnabled] = useState(existingSchedule?.enabled ?? true);
|
||||||
|
|
||||||
const repositories = repositoriesData?.repositories || [];
|
const repositories = repositoriesData || [];
|
||||||
const selectedRepository = repositories.find((r) => r.id === (existingSchedule?.repositoryId ?? ""));
|
const selectedRepository = repositories.find((r) => r.id === (existingSchedule?.repositoryId ?? ""));
|
||||||
|
|
||||||
const summary = useMemo(() => {
|
const summary = useMemo(() => {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const DockerTabContent = ({ volume }: Props) => {
|
|||||||
refetchOnWindowFocus: true,
|
refetchOnWindowFocus: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const containers = containersData?.containers || [];
|
const containers = containersData || [];
|
||||||
|
|
||||||
const getStateClass = (state: string) => {
|
const getStateClass = (state: string) => {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ export function meta(_: Route.MetaArgs) {
|
|||||||
|
|
||||||
export const clientLoader = async () => {
|
export const clientLoader = async () => {
|
||||||
const repositories = await listRepositories();
|
const repositories = await listRepositories();
|
||||||
if (repositories.data) return { repositories: repositories.data.repositories };
|
if (repositories.data) return repositories.data;
|
||||||
return { repositories: [] };
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Repositories({ loaderData }: Route.ComponentProps) {
|
export default function Repositories({ loaderData }: Route.ComponentProps) {
|
||||||
@@ -52,14 +52,14 @@ export default function Repositories({ loaderData }: Route.ComponentProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const filteredRepositories =
|
const filteredRepositories =
|
||||||
data?.repositories.filter((repository) => {
|
data?.filter((repository) => {
|
||||||
const matchesSearch = repository.name.toLowerCase().includes(searchQuery.toLowerCase());
|
const matchesSearch = repository.name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
const matchesStatus = !statusFilter || repository.status === statusFilter;
|
const matchesStatus = !statusFilter || repository.status === statusFilter;
|
||||||
const matchesBackend = !backendFilter || repository.type === backendFilter;
|
const matchesBackend = !backendFilter || repository.type === backendFilter;
|
||||||
return matchesSearch && matchesStatus && matchesBackend;
|
return matchesSearch && matchesStatus && matchesBackend;
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
const hasNoRepositories = data?.repositories.length === 0;
|
const hasNoRepositories = data?.length === 0;
|
||||||
const hasNoFilteredRepositories = filteredRepositories.length === 0 && !hasNoRepositories;
|
const hasNoFilteredRepositories = filteredRepositories.length === 0 && !hasNoRepositories;
|
||||||
|
|
||||||
if (hasNoRepositories) {
|
if (hasNoRepositories) {
|
||||||
|
|||||||
@@ -83,8 +83,6 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
|
|||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { repository } = data;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
@@ -94,14 +92,14 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
|
|||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center gap-2 px-2 py-1 rounded-md text-xs bg-gray-500/10 text-gray-500",
|
"inline-flex items-center gap-2 px-2 py-1 rounded-md text-xs bg-gray-500/10 text-gray-500",
|
||||||
{
|
{
|
||||||
"bg-green-500/10 text-green-500": repository.status === "healthy",
|
"bg-green-500/10 text-green-500": data.status === "healthy",
|
||||||
"bg-red-500/10 text-red-500": repository.status === "error",
|
"bg-red-500/10 text-red-500": data.status === "error",
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{repository.status || "unknown"}
|
{data.status || "unknown"}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs bg-primary/10 rounded-md px-2 py-1">{repository.type}</span>
|
<span className="text-xs bg-primary/10 rounded-md px-2 py-1">{data.type}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
@@ -117,10 +115,10 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
|
|||||||
<TabsTrigger value="snapshots">Snapshots</TabsTrigger>
|
<TabsTrigger value="snapshots">Snapshots</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="info">
|
<TabsContent value="info">
|
||||||
<RepositoryInfoTabContent repository={data.repository} />
|
<RepositoryInfoTabContent repository={data} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="snapshots">
|
<TabsContent value="snapshots">
|
||||||
<RepositorySnapshotsTabContent repository={data.repository} />
|
<RepositorySnapshotsTabContent repository={data} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
RepositoryStatus,
|
RepositoryStatus,
|
||||||
} from "@ironmount/schemas/restic";
|
} from "@ironmount/schemas/restic";
|
||||||
import { sql } from "drizzle-orm";
|
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", {
|
export const volumesTable = sqliteTable("volumes_table", {
|
||||||
id: int().primaryKey({ autoIncrement: true }),
|
id: int().primaryKey({ autoIncrement: true }),
|
||||||
@@ -14,9 +14,9 @@ export const volumesTable = sqliteTable("volumes_table", {
|
|||||||
type: text().$type<BackendType>().notNull(),
|
type: text().$type<BackendType>().notNull(),
|
||||||
status: text().$type<BackendStatus>().notNull().default("unmounted"),
|
status: text().$type<BackendStatus>().notNull().default("unmounted"),
|
||||||
lastError: text("last_error"),
|
lastError: text("last_error"),
|
||||||
lastHealthCheck: int("last_health_check", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
lastHealthCheck: integer("last_health_check", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
||||||
createdAt: int("created_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
createdAt: integer("created_at", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
||||||
updatedAt: int("updated_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
updatedAt: integer("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
||||||
config: text("config", { mode: "json" }).$type<typeof volumeConfigSchema.inferOut>().notNull(),
|
config: text("config", { mode: "json" }).$type<typeof volumeConfigSchema.inferOut>().notNull(),
|
||||||
autoRemount: int("auto_remount", { mode: "boolean" }).notNull().default(true),
|
autoRemount: int("auto_remount", { mode: "boolean" }).notNull().default(true),
|
||||||
});
|
});
|
||||||
@@ -27,8 +27,8 @@ export const usersTable = sqliteTable("users_table", {
|
|||||||
id: int().primaryKey({ autoIncrement: true }),
|
id: int().primaryKey({ autoIncrement: true }),
|
||||||
username: text().notNull().unique(),
|
username: text().notNull().unique(),
|
||||||
passwordHash: text("password_hash").notNull(),
|
passwordHash: text("password_hash").notNull(),
|
||||||
createdAt: int("created_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
||||||
updatedAt: int("updated_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type User = typeof usersTable.$inferSelect;
|
export type User = typeof usersTable.$inferSelect;
|
||||||
@@ -38,8 +38,8 @@ export const sessionsTable = sqliteTable("sessions_table", {
|
|||||||
userId: int("user_id")
|
userId: int("user_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => usersTable.id, { onDelete: "cascade" }),
|
.references(() => usersTable.id, { onDelete: "cascade" }),
|
||||||
expiresAt: int("expires_at", { mode: "timestamp" }).notNull(),
|
expiresAt: int("expires_at", { mode: "number" }).notNull(),
|
||||||
createdAt: int("created_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Session = typeof sessionsTable.$inferSelect;
|
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(),
|
config: text("config", { mode: "json" }).$type<typeof repositoryConfigSchema.inferOut>().notNull(),
|
||||||
compressionMode: text("compression_mode").$type<CompressionMode>().default("auto"),
|
compressionMode: text("compression_mode").$type<CompressionMode>().default("auto"),
|
||||||
status: text().$type<RepositoryStatus>().default("unknown"),
|
status: text().$type<RepositoryStatus>().default("unknown"),
|
||||||
lastChecked: int("last_checked", { mode: "timestamp" }),
|
lastChecked: int("last_checked", { mode: "number" }),
|
||||||
lastError: text("last_error"),
|
lastError: text("last_error"),
|
||||||
createdAt: int("created_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
||||||
updatedAt: int("updated_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Repository = typeof repositoriesTable.$inferSelect;
|
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([]),
|
excludePatterns: text("exclude_patterns", { mode: "json" }).$type<string[]>().default([]),
|
||||||
includePatterns: text("include_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">(),
|
lastBackupStatus: text("last_backup_status").$type<"success" | "error">(),
|
||||||
lastBackupError: text("last_backup_error"),
|
lastBackupError: text("last_backup_error"),
|
||||||
nextBackupAt: int("next_backup_at", { mode: "timestamp" }),
|
nextBackupAt: int("next_backup_at", { mode: "number" }),
|
||||||
createdAt: int("created_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
||||||
updatedAt: int("updated_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type BackupSchedule = typeof backupSchedulesTable.$inferSelect;
|
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,
|
logoutDto,
|
||||||
registerBodySchema,
|
registerBodySchema,
|
||||||
registerDto,
|
registerDto,
|
||||||
|
type GetMeDto,
|
||||||
|
type GetStatusDto,
|
||||||
|
type LoginDto,
|
||||||
|
type LogoutDto,
|
||||||
|
type RegisterDto,
|
||||||
} from "./auth.dto";
|
} from "./auth.dto";
|
||||||
import { authService } from "./auth.service";
|
import { authService } from "./auth.service";
|
||||||
|
import { toMessage } from "../../utils/errors";
|
||||||
|
|
||||||
const COOKIE_NAME = "session_id";
|
const COOKIE_NAME = "session_id";
|
||||||
const COOKIE_OPTIONS = {
|
const COOKIE_OPTIONS = {
|
||||||
@@ -33,9 +39,12 @@ export const authController = new Hono()
|
|||||||
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
|
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) {
|
} 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) => {
|
.post("/login", loginDto, validator("json", loginBodySchema), async (c) => {
|
||||||
@@ -46,15 +55,16 @@ export const authController = new Hono()
|
|||||||
|
|
||||||
setCookie(c, COOKIE_NAME, sessionId, {
|
setCookie(c, COOKIE_NAME, sessionId, {
|
||||||
...COOKIE_OPTIONS,
|
...COOKIE_OPTIONS,
|
||||||
expires: expiresAt,
|
expires: new Date(expiresAt),
|
||||||
});
|
});
|
||||||
|
|
||||||
return c.json({
|
return c.json<LoginDto>({
|
||||||
|
success: true,
|
||||||
message: "Login successful",
|
message: "Login successful",
|
||||||
user: { id: user.id, username: user.username },
|
user: { id: user.id, username: user.username },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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) => {
|
.post("/logout", logoutDto, async (c) => {
|
||||||
@@ -65,13 +75,13 @@ export const authController = new Hono()
|
|||||||
deleteCookie(c, COOKIE_NAME, COOKIE_OPTIONS);
|
deleteCookie(c, COOKIE_NAME, COOKIE_OPTIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json({ message: "Logout successful" });
|
return c.json<LogoutDto>({ success: true });
|
||||||
})
|
})
|
||||||
.get("/me", getMeDto, async (c) => {
|
.get("/me", getMeDto, async (c) => {
|
||||||
const sessionId = getCookie(c, COOKIE_NAME);
|
const sessionId = getCookie(c, COOKIE_NAME);
|
||||||
|
|
||||||
if (!sessionId) {
|
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);
|
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({ message: "Not authenticated" }, 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json({
|
return c.json<GetMeDto>({
|
||||||
|
success: true,
|
||||||
user: session.user,
|
user: session.user,
|
||||||
|
message: "Authenticated",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.get("/status", getStatusDto, async (c) => {
|
.get("/status", getStatusDto, async (c) => {
|
||||||
const hasUsers = await authService.hasUsers();
|
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({
|
const loginResponseSchema = type({
|
||||||
message: "string",
|
message: "string",
|
||||||
|
success: "boolean",
|
||||||
user: type({
|
user: type({
|
||||||
id: "string",
|
id: "number",
|
||||||
username: "string",
|
username: "string",
|
||||||
}),
|
}).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const loginDto = describeRoute({
|
export const loginDto = describeRoute({
|
||||||
@@ -39,6 +40,8 @@ export const loginDto = describeRoute({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type LoginDto = typeof loginResponseSchema.infer;
|
||||||
|
|
||||||
export const registerDto = describeRoute({
|
export const registerDto = describeRoute({
|
||||||
description: "Register a new user",
|
description: "Register a new user",
|
||||||
operationId: "register",
|
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({
|
export const logoutDto = describeRoute({
|
||||||
description: "Logout current user",
|
description: "Logout current user",
|
||||||
operationId: "logout",
|
operationId: "logout",
|
||||||
@@ -67,13 +76,15 @@ export const logoutDto = describeRoute({
|
|||||||
description: "Logout successful",
|
description: "Logout successful",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: resolver(type({ message: "string" })),
|
schema: resolver(logoutResponseSchema),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type LogoutDto = typeof logoutResponseSchema.infer;
|
||||||
|
|
||||||
export const getMeDto = describeRoute({
|
export const getMeDto = describeRoute({
|
||||||
description: "Get current authenticated user",
|
description: "Get current authenticated user",
|
||||||
operationId: "getMe",
|
operationId: "getMe",
|
||||||
@@ -87,12 +98,11 @@ export const getMeDto = describeRoute({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
401: {
|
|
||||||
description: "Not authenticated",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type GetMeDto = typeof loginResponseSchema.infer;
|
||||||
|
|
||||||
const statusResponseSchema = type({
|
const statusResponseSchema = type({
|
||||||
hasUsers: "boolean",
|
hasUsers: "boolean",
|
||||||
});
|
});
|
||||||
@@ -113,5 +123,7 @@ export const getStatusDto = describeRoute({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type GetStatusDto = typeof statusResponseSchema.infer;
|
||||||
|
|
||||||
export type LoginBody = typeof loginBodySchema.infer;
|
export type LoginBody = typeof loginBodySchema.infer;
|
||||||
export type RegisterBody = typeof registerBodySchema.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 { db } from "../../db/db";
|
||||||
import { sessionsTable, usersTable } from "../../db/schema";
|
import { sessionsTable, usersTable } from "../../db/schema";
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from "../../utils/logger";
|
||||||
@@ -30,7 +30,7 @@ export class AuthService {
|
|||||||
|
|
||||||
logger.info(`User registered: ${username}`);
|
logger.info(`User registered: ${username}`);
|
||||||
const sessionId = crypto.randomUUID();
|
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({
|
await db.insert(sessionsTable).values({
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
@@ -58,7 +58,7 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sessionId = crypto.randomUUID();
|
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({
|
await db.insert(sessionsTable).values({
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
@@ -100,7 +100,7 @@ export class AuthService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.session.expiresAt < new Date()) {
|
if (session.session.expiresAt < Date.now()) {
|
||||||
await db.delete(sessionsTable).where(eq(sessionsTable.id, sessionId));
|
await db.delete(sessionsTable).where(eq(sessionsTable.id, sessionId));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,7 @@ export class AuthService {
|
|||||||
* Clean up expired sessions
|
* Clean up expired sessions
|
||||||
*/
|
*/
|
||||||
async cleanupExpiredSessions() {
|
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) {
|
if (result.length > 0) {
|
||||||
logger.info(`Cleaned up ${result.length} expired sessions`);
|
logger.info(`Cleaned up ${result.length} expired sessions`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ import {
|
|||||||
runBackupNowDto,
|
runBackupNowDto,
|
||||||
updateBackupScheduleBody,
|
updateBackupScheduleBody,
|
||||||
updateBackupScheduleDto,
|
updateBackupScheduleDto,
|
||||||
|
type CreateBackupScheduleDto,
|
||||||
|
type DeleteBackupScheduleDto,
|
||||||
|
type GetBackupScheduleDto,
|
||||||
|
type GetBackupScheduleForVolumeResponseDto,
|
||||||
|
type ListBackupSchedulesResponseDto,
|
||||||
|
type RunBackupNowDto,
|
||||||
|
type UpdateBackupScheduleDto,
|
||||||
} from "./backups.dto";
|
} from "./backups.dto";
|
||||||
import { backupsService } from "./backups.service";
|
import { backupsService } from "./backups.service";
|
||||||
|
|
||||||
@@ -17,27 +24,27 @@ export const backupScheduleController = new Hono()
|
|||||||
.get("/", listBackupSchedulesDto, async (c) => {
|
.get("/", listBackupSchedulesDto, async (c) => {
|
||||||
const schedules = await backupsService.listSchedules();
|
const schedules = await backupsService.listSchedules();
|
||||||
|
|
||||||
return c.json({ schedules }, 200);
|
return c.json<ListBackupSchedulesResponseDto>(schedules, 200);
|
||||||
})
|
})
|
||||||
.get("/:scheduleId", getBackupScheduleDto, async (c) => {
|
.get("/:scheduleId", getBackupScheduleDto, async (c) => {
|
||||||
const scheduleId = c.req.param("scheduleId");
|
const scheduleId = c.req.param("scheduleId");
|
||||||
|
|
||||||
const schedule = await backupsService.getSchedule(Number(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) => {
|
.get("/volume/:volumeId", getBackupScheduleForVolumeDto, async (c) => {
|
||||||
const volumeId = c.req.param("volumeId");
|
const volumeId = c.req.param("volumeId");
|
||||||
const schedule = await backupsService.getScheduleForVolume(Number(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) => {
|
.post("/", createBackupScheduleDto, validator("json", createBackupScheduleBody), async (c) => {
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
|
|
||||||
const schedule = await backupsService.createSchedule(body);
|
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) => {
|
.patch("/:scheduleId", updateBackupScheduleDto, validator("json", updateBackupScheduleBody), async (c) => {
|
||||||
const scheduleId = c.req.param("scheduleId");
|
const scheduleId = c.req.param("scheduleId");
|
||||||
@@ -45,14 +52,14 @@ export const backupScheduleController = new Hono()
|
|||||||
|
|
||||||
const schedule = await backupsService.updateSchedule(Number(scheduleId), body);
|
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) => {
|
.delete("/:scheduleId", deleteBackupScheduleDto, async (c) => {
|
||||||
const scheduleId = c.req.param("scheduleId");
|
const scheduleId = c.req.param("scheduleId");
|
||||||
|
|
||||||
await backupsService.deleteSchedule(Number(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) => {
|
.post("/:scheduleId/run", runBackupNowDto, async (c) => {
|
||||||
const scheduleId = c.req.param("scheduleId");
|
const scheduleId = c.req.param("scheduleId");
|
||||||
@@ -61,11 +68,5 @@ export const backupScheduleController = new Hono()
|
|||||||
console.error("Backup execution failed:", error);
|
console.error("Backup execution failed:", error);
|
||||||
});
|
});
|
||||||
|
|
||||||
return c.json(
|
return c.json<RunBackupNowDto>({ success: true }, 200);
|
||||||
{
|
|
||||||
message: "Backup started",
|
|
||||||
backupStarted: true,
|
|
||||||
},
|
|
||||||
200,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,9 +35,7 @@ export type BackupScheduleDto = typeof backupScheduleSchema.infer;
|
|||||||
/**
|
/**
|
||||||
* List all backup schedules
|
* List all backup schedules
|
||||||
*/
|
*/
|
||||||
export const listBackupSchedulesResponse = type({
|
export const listBackupSchedulesResponse = backupScheduleSchema.array();
|
||||||
schedules: backupScheduleSchema.array(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type ListBackupSchedulesResponseDto = typeof listBackupSchedulesResponse.infer;
|
export type ListBackupSchedulesResponseDto = typeof listBackupSchedulesResponse.infer;
|
||||||
|
|
||||||
@@ -60,9 +58,7 @@ export const listBackupSchedulesDto = describeRoute({
|
|||||||
/**
|
/**
|
||||||
* Get a single backup schedule
|
* Get a single backup schedule
|
||||||
*/
|
*/
|
||||||
export const getBackupScheduleResponse = type({
|
export const getBackupScheduleResponse = backupScheduleSchema;
|
||||||
schedule: backupScheduleSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type GetBackupScheduleDto = typeof getBackupScheduleResponse.infer;
|
export type GetBackupScheduleDto = typeof getBackupScheduleResponse.infer;
|
||||||
|
|
||||||
@@ -118,10 +114,7 @@ export const createBackupScheduleBody = type({
|
|||||||
|
|
||||||
export type CreateBackupScheduleBody = typeof createBackupScheduleBody.infer;
|
export type CreateBackupScheduleBody = typeof createBackupScheduleBody.infer;
|
||||||
|
|
||||||
export const createBackupScheduleResponse = type({
|
export const createBackupScheduleResponse = backupScheduleSchema;
|
||||||
message: "string",
|
|
||||||
schedule: backupScheduleSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type CreateBackupScheduleDto = typeof createBackupScheduleResponse.infer;
|
export type CreateBackupScheduleDto = typeof createBackupScheduleResponse.infer;
|
||||||
|
|
||||||
@@ -156,10 +149,9 @@ export const updateBackupScheduleBody = type({
|
|||||||
|
|
||||||
export type UpdateBackupScheduleBody = typeof updateBackupScheduleBody.infer;
|
export type UpdateBackupScheduleBody = typeof updateBackupScheduleBody.infer;
|
||||||
|
|
||||||
export const updateBackupScheduleResponse = type({
|
export const updateBackupScheduleResponse = backupScheduleSchema;
|
||||||
message: "string",
|
|
||||||
schedule: backupScheduleSchema,
|
export type UpdateBackupScheduleDto = typeof updateBackupScheduleResponse.infer;
|
||||||
});
|
|
||||||
|
|
||||||
export const updateBackupScheduleDto = describeRoute({
|
export const updateBackupScheduleDto = describeRoute({
|
||||||
description: "Update a backup schedule",
|
description: "Update a backup schedule",
|
||||||
@@ -181,9 +173,11 @@ export const updateBackupScheduleDto = describeRoute({
|
|||||||
* Delete a backup schedule
|
* Delete a backup schedule
|
||||||
*/
|
*/
|
||||||
export const deleteBackupScheduleResponse = type({
|
export const deleteBackupScheduleResponse = type({
|
||||||
message: "string",
|
success: "boolean",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type DeleteBackupScheduleDto = typeof deleteBackupScheduleResponse.infer;
|
||||||
|
|
||||||
export const deleteBackupScheduleDto = describeRoute({
|
export const deleteBackupScheduleDto = describeRoute({
|
||||||
description: "Delete a backup schedule",
|
description: "Delete a backup schedule",
|
||||||
operationId: "deleteBackupSchedule",
|
operationId: "deleteBackupSchedule",
|
||||||
@@ -204,10 +198,11 @@ export const deleteBackupScheduleDto = describeRoute({
|
|||||||
* Run a backup immediately
|
* Run a backup immediately
|
||||||
*/
|
*/
|
||||||
export const runBackupNowResponse = type({
|
export const runBackupNowResponse = type({
|
||||||
message: "string",
|
success: "boolean",
|
||||||
backupStarted: "boolean",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type RunBackupNowDto = typeof runBackupNowResponse.infer;
|
||||||
|
|
||||||
export const runBackupNowDto = describeRoute({
|
export const runBackupNowDto = describeRoute({
|
||||||
description: "Trigger a backup immediately for a schedule",
|
description: "Trigger a backup immediately for a schedule",
|
||||||
operationId: "runBackupNow",
|
operationId: "runBackupNow",
|
||||||
|
|||||||
@@ -10,19 +10,19 @@ import { getVolumePath } from "../volumes/helpers";
|
|||||||
import type { CreateBackupScheduleBody, UpdateBackupScheduleBody } from "./backups.dto";
|
import type { CreateBackupScheduleBody, UpdateBackupScheduleBody } from "./backups.dto";
|
||||||
import { toMessage } from "../../utils/errors";
|
import { toMessage } from "../../utils/errors";
|
||||||
|
|
||||||
const calculateNextRun = (cronExpression: string): Date => {
|
const calculateNextRun = (cronExpression: string): number => {
|
||||||
try {
|
try {
|
||||||
const interval = CronExpressionParser.parse(cronExpression, {
|
const interval = CronExpressionParser.parse(cronExpression, {
|
||||||
currentDate: new Date(),
|
currentDate: new Date(),
|
||||||
tz: "UTC",
|
tz: "UTC",
|
||||||
});
|
});
|
||||||
|
|
||||||
return interval.next().toDate();
|
return interval.next().getTime();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to parse cron expression "${cronExpression}": ${error}`);
|
logger.error(`Failed to parse cron expression "${cronExpression}": ${error}`);
|
||||||
const fallback = new Date();
|
const fallback = new Date();
|
||||||
fallback.setMinutes(fallback.getMinutes() + 1);
|
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
|
const [updated] = await db
|
||||||
.update(backupSchedulesTable)
|
.update(backupSchedulesTable)
|
||||||
.set({ ...data, nextBackupAt, updatedAt: new Date() })
|
.set({ ...data, nextBackupAt, updatedAt: Date.now() })
|
||||||
.where(eq(backupSchedulesTable.id, scheduleId))
|
.where(eq(backupSchedulesTable.id, scheduleId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -204,11 +204,11 @@ const executeBackup = async (scheduleId: number) => {
|
|||||||
await db
|
await db
|
||||||
.update(backupSchedulesTable)
|
.update(backupSchedulesTable)
|
||||||
.set({
|
.set({
|
||||||
lastBackupAt: new Date(),
|
lastBackupAt: Date.now(),
|
||||||
lastBackupStatus: "success",
|
lastBackupStatus: "success",
|
||||||
lastBackupError: null,
|
lastBackupError: null,
|
||||||
nextBackupAt: nextBackupAt,
|
nextBackupAt: nextBackupAt,
|
||||||
updatedAt: new Date(),
|
updatedAt: Date.now(),
|
||||||
})
|
})
|
||||||
.where(eq(backupSchedulesTable.id, scheduleId));
|
.where(eq(backupSchedulesTable.id, scheduleId));
|
||||||
|
|
||||||
@@ -219,10 +219,10 @@ const executeBackup = async (scheduleId: number) => {
|
|||||||
await db
|
await db
|
||||||
.update(backupSchedulesTable)
|
.update(backupSchedulesTable)
|
||||||
.set({
|
.set({
|
||||||
lastBackupAt: new Date(),
|
lastBackupAt: Date.now(),
|
||||||
lastBackupStatus: "error",
|
lastBackupStatus: "error",
|
||||||
lastBackupError: toMessage(error),
|
lastBackupError: toMessage(error),
|
||||||
updatedAt: new Date(),
|
updatedAt: Date.now(),
|
||||||
})
|
})
|
||||||
.where(eq(backupSchedulesTable.id, scheduleId));
|
.where(eq(backupSchedulesTable.id, scheduleId));
|
||||||
|
|
||||||
@@ -231,7 +231,7 @@ const executeBackup = async (scheduleId: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getSchedulesToExecute = async () => {
|
const getSchedulesToExecute = async () => {
|
||||||
const now = new Date();
|
const now = Date.now();
|
||||||
const schedules = await db.query.backupSchedulesTable.findMany({
|
const schedules = await db.query.backupSchedulesTable.findMany({
|
||||||
where: eq(backupSchedulesTable.enabled, true),
|
where: eq(backupSchedulesTable.enabled, true),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { volumeService } from "../volumes/volume.service";
|
|||||||
import { CleanupDanglingMountsJob } from "../../jobs/cleanup-dangling";
|
import { CleanupDanglingMountsJob } from "../../jobs/cleanup-dangling";
|
||||||
import { VolumeHealthCheckJob } from "../../jobs/healthchecks";
|
import { VolumeHealthCheckJob } from "../../jobs/healthchecks";
|
||||||
import { BackupExecutionJob } from "../../jobs/backup-execution";
|
import { BackupExecutionJob } from "../../jobs/backup-execution";
|
||||||
|
import { CleanupSessionsJob } from "../../jobs/cleanup-sessions";
|
||||||
|
|
||||||
export const startup = async () => {
|
export const startup = async () => {
|
||||||
await Scheduler.start();
|
await Scheduler.start();
|
||||||
@@ -30,6 +31,7 @@ export const startup = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Scheduler.build(CleanupDanglingMountsJob).schedule("0 * * * *");
|
Scheduler.build(CleanupDanglingMountsJob).schedule("0 * * * *");
|
||||||
Scheduler.build(VolumeHealthCheckJob).schedule("* * * * *");
|
Scheduler.build(VolumeHealthCheckJob).schedule("*/5 * * * *");
|
||||||
Scheduler.build(BackupExecutionJob).schedule("* * * * *");
|
Scheduler.build(BackupExecutionJob).schedule("* * * * *");
|
||||||
|
Scheduler.build(CleanupSessionsJob).schedule("0 0 * * *");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import {
|
|||||||
createRepositoryDto,
|
createRepositoryDto,
|
||||||
deleteRepositoryDto,
|
deleteRepositoryDto,
|
||||||
getRepositoryDto,
|
getRepositoryDto,
|
||||||
type GetRepositoryResponseDto,
|
|
||||||
type ListRepositoriesResponseDto,
|
|
||||||
listRepositoriesDto,
|
listRepositoriesDto,
|
||||||
listSnapshotsDto,
|
listSnapshotsDto,
|
||||||
type ListSnapshotsResponseDto,
|
type DeleteRepositoryDto,
|
||||||
|
type GetRepositoryDto,
|
||||||
|
type ListRepositoriesDto,
|
||||||
|
type ListSnapshotsDto,
|
||||||
} from "./repositories.dto";
|
} from "./repositories.dto";
|
||||||
import { repositoriesService } from "./repositories.service";
|
import { repositoriesService } from "./repositories.service";
|
||||||
|
|
||||||
@@ -17,16 +18,7 @@ export const repositoriesController = new Hono()
|
|||||||
.get("/", listRepositoriesDto, async (c) => {
|
.get("/", listRepositoriesDto, async (c) => {
|
||||||
const repositories = await repositoriesService.listRepositories();
|
const repositories = await repositoriesService.listRepositories();
|
||||||
|
|
||||||
const response = {
|
return c.json<ListRepositoriesDto>(repositories, 200);
|
||||||
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);
|
|
||||||
})
|
})
|
||||||
.post("/", createRepositoryDto, validator("json", createRepositoryBody), async (c) => {
|
.post("/", createRepositoryDto, validator("json", createRepositoryBody), async (c) => {
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
@@ -38,22 +30,13 @@ export const repositoriesController = new Hono()
|
|||||||
const { name } = c.req.param();
|
const { name } = c.req.param();
|
||||||
const res = await repositoriesService.getRepository(name);
|
const res = await repositoriesService.getRepository(name);
|
||||||
|
|
||||||
const response = {
|
return c.json<GetRepositoryDto>(res.repository, 200);
|
||||||
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);
|
|
||||||
})
|
})
|
||||||
.delete("/:name", deleteRepositoryDto, async (c) => {
|
.delete("/:name", deleteRepositoryDto, async (c) => {
|
||||||
const { name } = c.req.param();
|
const { name } = c.req.param();
|
||||||
await repositoriesService.deleteRepository(name);
|
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) => {
|
.get("/:name/snapshots", listSnapshotsDto, async (c) => {
|
||||||
const { name } = c.req.param();
|
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");
|
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
|
* List all repositories
|
||||||
*/
|
*/
|
||||||
export const listRepositoriesResponse = type({
|
export const listRepositoriesResponse = repositorySchema.array();
|
||||||
repositories: repositorySchema.array(),
|
export type ListRepositoriesDto = typeof listRepositoriesResponse.infer;
|
||||||
});
|
|
||||||
export type ListRepositoriesResponseDto = typeof listRepositoriesResponse.infer;
|
|
||||||
|
|
||||||
export const listRepositoriesDto = describeRoute({
|
export const listRepositoriesDto = describeRoute({
|
||||||
description: "List all repositories",
|
description: "List all repositories",
|
||||||
@@ -65,6 +63,8 @@ export const createRepositoryResponse = type({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type CreateRepositoryDto = typeof createRepositoryResponse.infer;
|
||||||
|
|
||||||
export const createRepositoryDto = describeRoute({
|
export const createRepositoryDto = describeRoute({
|
||||||
description: "Create a new restic repository",
|
description: "Create a new restic repository",
|
||||||
operationId: "createRepository",
|
operationId: "createRepository",
|
||||||
@@ -84,10 +84,8 @@ export const createRepositoryDto = describeRoute({
|
|||||||
/**
|
/**
|
||||||
* Get a single repository
|
* Get a single repository
|
||||||
*/
|
*/
|
||||||
export const getRepositoryResponse = type({
|
export const getRepositoryResponse = repositorySchema;
|
||||||
repository: repositorySchema,
|
export type GetRepositoryDto = typeof getRepositoryResponse.infer;
|
||||||
});
|
|
||||||
export type GetRepositoryResponseDto = typeof getRepositoryResponse.infer;
|
|
||||||
|
|
||||||
export const getRepositoryDto = describeRoute({
|
export const getRepositoryDto = describeRoute({
|
||||||
description: "Get a single repository by name",
|
description: "Get a single repository by name",
|
||||||
@@ -112,6 +110,8 @@ export const deleteRepositoryResponse = type({
|
|||||||
message: "string",
|
message: "string",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type DeleteRepositoryDto = typeof deleteRepositoryResponse.infer;
|
||||||
|
|
||||||
export const deleteRepositoryDto = describeRoute({
|
export const deleteRepositoryDto = describeRoute({
|
||||||
description: "Delete a repository",
|
description: "Delete a repository",
|
||||||
tags: ["Repositories"],
|
tags: ["Repositories"],
|
||||||
@@ -143,7 +143,7 @@ const listSnapshotsResponse = type({
|
|||||||
snapshots: snapshotSchema.array(),
|
snapshots: snapshotSchema.array(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ListSnapshotsResponseDto = typeof listSnapshotsResponse.infer;
|
export type ListSnapshotsDto = typeof listSnapshotsResponse.infer;
|
||||||
|
|
||||||
export const listSnapshotsDto = describeRoute({
|
export const listSnapshotsDto = describeRoute({
|
||||||
description: "List all snapshots in a repository",
|
description: "List all snapshots in a repository",
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ const createRepository = async (name: string, config: RepositoryConfig, compress
|
|||||||
.update(repositoriesTable)
|
.update(repositoriesTable)
|
||||||
.set({
|
.set({
|
||||||
status: "healthy",
|
status: "healthy",
|
||||||
lastChecked: new Date(),
|
lastChecked: Date.now(),
|
||||||
lastError: null,
|
lastError: null,
|
||||||
})
|
})
|
||||||
.where(eq(repositoriesTable.id, id));
|
.where(eq(repositoriesTable.id, id));
|
||||||
|
|||||||
@@ -4,22 +4,23 @@ import {
|
|||||||
createVolumeBody,
|
createVolumeBody,
|
||||||
createVolumeDto,
|
createVolumeDto,
|
||||||
deleteVolumeDto,
|
deleteVolumeDto,
|
||||||
type GetVolumeResponseDto,
|
|
||||||
getContainersDto,
|
getContainersDto,
|
||||||
getVolumeDto,
|
getVolumeDto,
|
||||||
healthCheckDto,
|
healthCheckDto,
|
||||||
type ListContainersResponseDto,
|
type ListVolumesDto,
|
||||||
type ListFilesResponseDto,
|
|
||||||
type ListVolumesResponseDto,
|
|
||||||
listFilesDto,
|
listFilesDto,
|
||||||
listVolumesDto,
|
listVolumesDto,
|
||||||
mountVolumeDto,
|
mountVolumeDto,
|
||||||
testConnectionBody,
|
testConnectionBody,
|
||||||
testConnectionDto,
|
testConnectionDto,
|
||||||
type UpdateVolumeResponseDto,
|
|
||||||
unmountVolumeDto,
|
unmountVolumeDto,
|
||||||
updateVolumeBody,
|
updateVolumeBody,
|
||||||
updateVolumeDto,
|
updateVolumeDto,
|
||||||
|
type CreateVolumeDto,
|
||||||
|
type GetVolumeDto,
|
||||||
|
type ListContainersDto,
|
||||||
|
type UpdateVolumeDto,
|
||||||
|
type ListFilesDto,
|
||||||
} from "./volume.dto";
|
} from "./volume.dto";
|
||||||
import { volumeService } from "./volume.service";
|
import { volumeService } from "./volume.service";
|
||||||
import { getVolumePath } from "./helpers";
|
import { getVolumePath } from "./helpers";
|
||||||
@@ -32,19 +33,21 @@ export const volumeController = new Hono()
|
|||||||
volumes: volumes.map((volume) => ({
|
volumes: volumes.map((volume) => ({
|
||||||
path: getVolumePath(volume.name),
|
path: getVolumePath(volume.name),
|
||||||
...volume,
|
...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) => {
|
.post("/", createVolumeDto, validator("json", createVolumeBody), async (c) => {
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
const res = await volumeService.createVolume(body.name, body.config);
|
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) => {
|
.post("/test-connection", testConnectionDto, validator("json", testConnectionBody), async (c) => {
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
@@ -66,28 +69,21 @@ export const volumeController = new Hono()
|
|||||||
volume: {
|
volume: {
|
||||||
...res.volume,
|
...res.volume,
|
||||||
path: getVolumePath(res.volume.name),
|
path: getVolumePath(res.volume.name),
|
||||||
createdAt: res.volume.createdAt.getTime(),
|
|
||||||
updatedAt: res.volume.updatedAt.getTime(),
|
|
||||||
lastHealthCheck: res.volume.lastHealthCheck.getTime(),
|
|
||||||
},
|
},
|
||||||
statfs: {
|
statfs: {
|
||||||
total: res.statfs.total ?? 0,
|
total: res.statfs.total ?? 0,
|
||||||
used: res.statfs.used ?? 0,
|
used: res.statfs.used ?? 0,
|
||||||
free: res.statfs.free ?? 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) => {
|
.get("/:name/containers", getContainersDto, async (c) => {
|
||||||
const { name } = c.req.param();
|
const { name } = c.req.param();
|
||||||
const { containers } = await volumeService.getContainersUsingVolume(name);
|
const { containers } = await volumeService.getContainersUsingVolume(name);
|
||||||
|
|
||||||
const response = {
|
return c.json<ListContainersDto>(containers, 200);
|
||||||
containers,
|
|
||||||
} satisfies ListContainersResponseDto;
|
|
||||||
|
|
||||||
return c.json(response, 200);
|
|
||||||
})
|
})
|
||||||
.put("/:name", updateVolumeDto, validator("json", updateVolumeBody), async (c) => {
|
.put("/:name", updateVolumeDto, validator("json", updateVolumeBody), async (c) => {
|
||||||
const { name } = c.req.param();
|
const { name } = c.req.param();
|
||||||
@@ -95,17 +91,11 @@ export const volumeController = new Hono()
|
|||||||
const res = await volumeService.updateVolume(name, body);
|
const res = await volumeService.updateVolume(name, body);
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
message: "Volume updated",
|
|
||||||
volume: {
|
|
||||||
...res.volume,
|
...res.volume,
|
||||||
path: getVolumePath(res.volume.name),
|
path: getVolumePath(res.volume.name),
|
||||||
createdAt: res.volume.createdAt.getTime(),
|
};
|
||||||
updatedAt: res.volume.updatedAt.getTime(),
|
|
||||||
lastHealthCheck: res.volume.lastHealthCheck.getTime(),
|
|
||||||
},
|
|
||||||
} satisfies UpdateVolumeResponseDto;
|
|
||||||
|
|
||||||
return c.json(response, 200);
|
return c.json<UpdateVolumeDto>(response, 200);
|
||||||
})
|
})
|
||||||
.post("/:name/mount", mountVolumeDto, async (c) => {
|
.post("/:name/mount", mountVolumeDto, async (c) => {
|
||||||
const { name } = c.req.param();
|
const { name } = c.req.param();
|
||||||
@@ -133,9 +123,9 @@ export const volumeController = new Hono()
|
|||||||
const response = {
|
const response = {
|
||||||
files: result.files,
|
files: result.files,
|
||||||
path: result.path,
|
path: result.path,
|
||||||
} satisfies ListFilesResponseDto;
|
};
|
||||||
|
|
||||||
c.header("Cache-Control", "public, max-age=10, stale-while-revalidate=60");
|
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({
|
export const listVolumesResponse = type({
|
||||||
volumes: volumeSchema.array(),
|
volumes: volumeSchema.array(),
|
||||||
});
|
});
|
||||||
export type ListVolumesResponseDto = typeof listVolumesResponse.infer;
|
export type ListVolumesDto = typeof listVolumesResponse.infer;
|
||||||
|
|
||||||
export const listVolumesDto = describeRoute({
|
export const listVolumesDto = describeRoute({
|
||||||
description: "List all volumes",
|
description: "List all volumes",
|
||||||
@@ -50,13 +50,8 @@ export const createVolumeBody = type({
|
|||||||
config: volumeConfigSchema,
|
config: volumeConfigSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createVolumeResponse = type({
|
export const createVolumeResponse = volumeSchema;
|
||||||
message: "string",
|
export type CreateVolumeDto = typeof createVolumeResponse.infer;
|
||||||
volume: type({
|
|
||||||
name: "string",
|
|
||||||
path: "string",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createVolumeDto = describeRoute({
|
export const createVolumeDto = describeRoute({
|
||||||
description: "Create a new volume",
|
description: "Create a new volume",
|
||||||
@@ -80,6 +75,7 @@ export const createVolumeDto = describeRoute({
|
|||||||
export const deleteVolumeResponse = type({
|
export const deleteVolumeResponse = type({
|
||||||
message: "string",
|
message: "string",
|
||||||
});
|
});
|
||||||
|
export type DeleteVolumeDto = typeof deleteVolumeResponse.infer;
|
||||||
|
|
||||||
export const deleteVolumeDto = describeRoute({
|
export const deleteVolumeDto = describeRoute({
|
||||||
description: "Delete a volume",
|
description: "Delete a volume",
|
||||||
@@ -108,7 +104,7 @@ const getVolumeResponse = type({
|
|||||||
statfs: statfsSchema,
|
statfs: statfsSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type GetVolumeResponseDto = typeof getVolumeResponse.infer;
|
export type GetVolumeDto = typeof getVolumeResponse.infer;
|
||||||
/**
|
/**
|
||||||
* Get a volume
|
* Get a volume
|
||||||
*/
|
*/
|
||||||
@@ -141,10 +137,8 @@ export const updateVolumeBody = type({
|
|||||||
|
|
||||||
export type UpdateVolumeBody = typeof updateVolumeBody.infer;
|
export type UpdateVolumeBody = typeof updateVolumeBody.infer;
|
||||||
|
|
||||||
export const updateVolumeResponse = type({
|
export const updateVolumeResponse = volumeSchema;
|
||||||
message: "string",
|
export type UpdateVolumeDto = typeof updateVolumeResponse.infer;
|
||||||
volume: volumeSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateVolumeDto = describeRoute({
|
export const updateVolumeDto = describeRoute({
|
||||||
description: "Update a volume's configuration",
|
description: "Update a volume's configuration",
|
||||||
@@ -165,8 +159,6 @@ export const updateVolumeDto = describeRoute({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export type UpdateVolumeResponseDto = typeof updateVolumeResponse.infer;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test connection
|
* Test connection
|
||||||
*/
|
*/
|
||||||
@@ -178,6 +170,7 @@ export const testConnectionResponse = type({
|
|||||||
success: "boolean",
|
success: "boolean",
|
||||||
message: "string",
|
message: "string",
|
||||||
});
|
});
|
||||||
|
export type TestConnectionDto = typeof testConnectionResponse.infer;
|
||||||
|
|
||||||
export const testConnectionDto = describeRoute({
|
export const testConnectionDto = describeRoute({
|
||||||
description: "Test connection to backend",
|
description: "Test connection to backend",
|
||||||
@@ -202,6 +195,7 @@ export const mountVolumeResponse = type({
|
|||||||
error: "string?",
|
error: "string?",
|
||||||
status: type.valueOf(BACKEND_STATUS),
|
status: type.valueOf(BACKEND_STATUS),
|
||||||
});
|
});
|
||||||
|
export type MountVolumeDto = typeof mountVolumeResponse.infer;
|
||||||
|
|
||||||
export const mountVolumeDto = describeRoute({
|
export const mountVolumeDto = describeRoute({
|
||||||
description: "Mount a volume",
|
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?",
|
error: "string?",
|
||||||
status: type.valueOf(BACKEND_STATUS),
|
status: type.valueOf(BACKEND_STATUS),
|
||||||
});
|
});
|
||||||
|
export type UnmountVolumeDto = typeof unmountVolumeResponse.infer;
|
||||||
|
|
||||||
export const unmountVolumeDto = describeRoute({
|
export const unmountVolumeDto = describeRoute({
|
||||||
description: "Unmount a volume",
|
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?",
|
error: "string?",
|
||||||
status: type.valueOf(BACKEND_STATUS),
|
status: type.valueOf(BACKEND_STATUS),
|
||||||
});
|
});
|
||||||
|
export type HealthCheckDto = typeof healthCheckResponse.infer;
|
||||||
|
|
||||||
export const healthCheckDto = describeRoute({
|
export const healthCheckDto = describeRoute({
|
||||||
description: "Perform a health check on a volume",
|
description: "Perform a health check on a volume",
|
||||||
@@ -283,10 +273,8 @@ const containerSchema = type({
|
|||||||
image: "string",
|
image: "string",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const listContainersResponse = type({
|
export const listContainersResponse = containerSchema.array();
|
||||||
containers: containerSchema.array(),
|
export type ListContainersDto = typeof listContainersResponse.infer;
|
||||||
});
|
|
||||||
export type ListContainersResponseDto = typeof listContainersResponse.infer;
|
|
||||||
|
|
||||||
export const getContainersDto = describeRoute({
|
export const getContainersDto = describeRoute({
|
||||||
description: "Get containers using a volume by name",
|
description: "Get containers using a volume by name",
|
||||||
@@ -322,7 +310,7 @@ export const listFilesResponse = type({
|
|||||||
files: fileEntrySchema.array(),
|
files: fileEntrySchema.array(),
|
||||||
path: "string",
|
path: "string",
|
||||||
});
|
});
|
||||||
export type ListFilesResponseDto = typeof listFilesResponse.infer;
|
export type ListFilesDto = typeof listFilesResponse.infer;
|
||||||
|
|
||||||
export const listFilesDto = describeRoute({
|
export const listFilesDto = describeRoute({
|
||||||
description: "List files in a volume directory",
|
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 { eq } from "drizzle-orm";
|
||||||
import { ConflictError, InternalServerError, NotFoundError } from "http-errors-enhanced";
|
import { ConflictError, InternalServerError, NotFoundError } from "http-errors-enhanced";
|
||||||
import slugify from "slugify";
|
import slugify from "slugify";
|
||||||
import { VOLUME_MOUNT_BASE } from "../../core/constants";
|
|
||||||
import { db } from "../../db/db";
|
import { db } from "../../db/db";
|
||||||
import { volumesTable } from "../../db/schema";
|
import { volumesTable } from "../../db/schema";
|
||||||
import { toMessage } from "../../utils/errors";
|
import { toMessage } from "../../utils/errors";
|
||||||
@@ -51,7 +50,7 @@ const createVolume = async (name: string, backendConfig: BackendConfig) => {
|
|||||||
|
|
||||||
await db
|
await db
|
||||||
.update(volumesTable)
|
.update(volumesTable)
|
||||||
.set({ status, lastError: error ?? null, lastHealthCheck: new Date() })
|
.set({ status, lastError: error ?? null, lastHealthCheck: Date.now() })
|
||||||
.where(eq(volumesTable.name, slug));
|
.where(eq(volumesTable.name, slug));
|
||||||
|
|
||||||
return { volume: created, status: 201 };
|
return { volume: created, status: 201 };
|
||||||
@@ -85,7 +84,7 @@ const mountVolume = async (name: string) => {
|
|||||||
|
|
||||||
await db
|
await db
|
||||||
.update(volumesTable)
|
.update(volumesTable)
|
||||||
.set({ status, lastError: error ?? null, lastHealthCheck: new Date() })
|
.set({ status, lastError: error ?? null, lastHealthCheck: Date.now() })
|
||||||
.where(eq(volumesTable.name, name));
|
.where(eq(volumesTable.name, name));
|
||||||
|
|
||||||
return { error, status };
|
return { error, status };
|
||||||
@@ -149,7 +148,7 @@ const updateVolume = async (name: string, volumeData: UpdateVolumeBody) => {
|
|||||||
config: volumeData.config,
|
config: volumeData.config,
|
||||||
type: volumeData.config?.backend,
|
type: volumeData.config?.backend,
|
||||||
autoRemount: volumeData.autoRemount,
|
autoRemount: volumeData.autoRemount,
|
||||||
updatedAt: new Date(),
|
updatedAt: Date.now(),
|
||||||
})
|
})
|
||||||
.where(eq(volumesTable.name, name))
|
.where(eq(volumesTable.name, name))
|
||||||
.returning();
|
.returning();
|
||||||
@@ -163,7 +162,7 @@ const updateVolume = async (name: string, volumeData: UpdateVolumeBody) => {
|
|||||||
const { error, status } = await backend.mount();
|
const { error, status } = await backend.mount();
|
||||||
await db
|
await db
|
||||||
.update(volumesTable)
|
.update(volumesTable)
|
||||||
.set({ status, lastError: error ?? null, lastHealthCheck: new Date() })
|
.set({ status, lastError: error ?? null, lastHealthCheck: Date.now() })
|
||||||
.where(eq(volumesTable.name, name));
|
.where(eq(volumesTable.name, name));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,9 +177,9 @@ const testConnection = async (backendConfig: BackendConfig) => {
|
|||||||
name: "test-connection",
|
name: "test-connection",
|
||||||
path: tempDir,
|
path: tempDir,
|
||||||
config: backendConfig,
|
config: backendConfig,
|
||||||
createdAt: new Date(),
|
createdAt: Date.now(),
|
||||||
updatedAt: new Date(),
|
updatedAt: Date.now(),
|
||||||
lastHealthCheck: new Date(),
|
lastHealthCheck: Date.now(),
|
||||||
type: backendConfig.backend,
|
type: backendConfig.backend,
|
||||||
status: "unmounted" as const,
|
status: "unmounted" as const,
|
||||||
lastError: null,
|
lastError: null,
|
||||||
@@ -215,7 +214,7 @@ const checkHealth = async (name: string) => {
|
|||||||
|
|
||||||
await db
|
await db
|
||||||
.update(volumesTable)
|
.update(volumesTable)
|
||||||
.set({ lastHealthCheck: new Date(), status, lastError: error ?? null })
|
.set({ lastHealthCheck: Date.now(), status, lastError: error ?? null })
|
||||||
.where(eq(volumesTable.name, volume.name));
|
.where(eq(volumesTable.name, volume.name));
|
||||||
|
|
||||||
return { status, error };
|
return { status, error };
|
||||||
|
|||||||
Reference in New Issue
Block a user