Compare commits
3 commits
main
...
v0.37.3-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e11544207 | ||
|
|
f269751014 | ||
|
|
c752e2d52d |
20 changed files with 3213 additions and 91 deletions
|
|
@ -337,14 +337,7 @@ export type ListVolumesResponses = {
|
||||||
200: Array<{
|
200: Array<{
|
||||||
id: number;
|
id: number;
|
||||||
shortId: string;
|
shortId: string;
|
||||||
provisioningId: string | null;
|
|
||||||
name: string;
|
name: string;
|
||||||
type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp';
|
|
||||||
status: 'mounted' | 'unmounted' | 'error';
|
|
||||||
lastError: string | null;
|
|
||||||
createdAt: number;
|
|
||||||
updatedAt: number;
|
|
||||||
lastHealthCheck: number;
|
|
||||||
config: {
|
config: {
|
||||||
backend: 'nfs';
|
backend: 'nfs';
|
||||||
server: string;
|
server: string;
|
||||||
|
|
@ -359,6 +352,7 @@ export type ListVolumesResponses = {
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
guest?: boolean;
|
guest?: boolean;
|
||||||
|
mapToContainerUidGid?: boolean;
|
||||||
vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto';
|
vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto';
|
||||||
domain?: string;
|
domain?: string;
|
||||||
port?: string | number;
|
port?: string | number;
|
||||||
|
|
@ -392,7 +386,15 @@ export type ListVolumesResponses = {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
skipHostKeyCheck?: boolean;
|
skipHostKeyCheck?: boolean;
|
||||||
knownHosts?: string;
|
knownHosts?: string;
|
||||||
|
allowLegacySshRsa?: boolean;
|
||||||
};
|
};
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
lastHealthCheck: number;
|
||||||
|
type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp';
|
||||||
|
status: 'mounted' | 'unmounted' | 'error';
|
||||||
|
lastError: string | null;
|
||||||
|
provisioningId?: string | null;
|
||||||
autoRemount: boolean;
|
autoRemount: boolean;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
@ -416,6 +418,7 @@ export type CreateVolumeData = {
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
guest?: boolean;
|
guest?: boolean;
|
||||||
|
mapToContainerUidGid?: boolean;
|
||||||
vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto';
|
vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto';
|
||||||
domain?: string;
|
domain?: string;
|
||||||
port?: string | number;
|
port?: string | number;
|
||||||
|
|
@ -449,6 +452,7 @@ export type CreateVolumeData = {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
skipHostKeyCheck?: boolean;
|
skipHostKeyCheck?: boolean;
|
||||||
knownHosts?: string;
|
knownHosts?: string;
|
||||||
|
allowLegacySshRsa?: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
path?: never;
|
path?: never;
|
||||||
|
|
@ -463,14 +467,7 @@ export type CreateVolumeResponses = {
|
||||||
201: {
|
201: {
|
||||||
id: number;
|
id: number;
|
||||||
shortId: string;
|
shortId: string;
|
||||||
provisioningId: string | null;
|
|
||||||
name: string;
|
name: string;
|
||||||
type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp';
|
|
||||||
status: 'mounted' | 'unmounted' | 'error';
|
|
||||||
lastError: string | null;
|
|
||||||
createdAt: number;
|
|
||||||
updatedAt: number;
|
|
||||||
lastHealthCheck: number;
|
|
||||||
config: {
|
config: {
|
||||||
backend: 'nfs';
|
backend: 'nfs';
|
||||||
server: string;
|
server: string;
|
||||||
|
|
@ -485,6 +482,7 @@ export type CreateVolumeResponses = {
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
guest?: boolean;
|
guest?: boolean;
|
||||||
|
mapToContainerUidGid?: boolean;
|
||||||
vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto';
|
vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto';
|
||||||
domain?: string;
|
domain?: string;
|
||||||
port?: string | number;
|
port?: string | number;
|
||||||
|
|
@ -518,7 +516,15 @@ export type CreateVolumeResponses = {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
skipHostKeyCheck?: boolean;
|
skipHostKeyCheck?: boolean;
|
||||||
knownHosts?: string;
|
knownHosts?: string;
|
||||||
|
allowLegacySshRsa?: boolean;
|
||||||
};
|
};
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
lastHealthCheck: number;
|
||||||
|
type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp';
|
||||||
|
status: 'mounted' | 'unmounted' | 'error';
|
||||||
|
lastError: string | null;
|
||||||
|
provisioningId?: string | null;
|
||||||
autoRemount: boolean;
|
autoRemount: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -541,6 +547,7 @@ export type TestConnectionData = {
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
guest?: boolean;
|
guest?: boolean;
|
||||||
|
mapToContainerUidGid?: boolean;
|
||||||
vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto';
|
vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto';
|
||||||
domain?: string;
|
domain?: string;
|
||||||
port?: string | number;
|
port?: string | number;
|
||||||
|
|
@ -574,6 +581,7 @@ export type TestConnectionData = {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
skipHostKeyCheck?: boolean;
|
skipHostKeyCheck?: boolean;
|
||||||
knownHosts?: string;
|
knownHosts?: string;
|
||||||
|
allowLegacySshRsa?: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
path?: never;
|
path?: never;
|
||||||
|
|
@ -637,14 +645,7 @@ export type GetVolumeResponses = {
|
||||||
volume: {
|
volume: {
|
||||||
id: number;
|
id: number;
|
||||||
shortId: string;
|
shortId: string;
|
||||||
provisioningId: string | null;
|
|
||||||
name: string;
|
name: string;
|
||||||
type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp';
|
|
||||||
status: 'mounted' | 'unmounted' | 'error';
|
|
||||||
lastError: string | null;
|
|
||||||
createdAt: number;
|
|
||||||
updatedAt: number;
|
|
||||||
lastHealthCheck: number;
|
|
||||||
config: {
|
config: {
|
||||||
backend: 'nfs';
|
backend: 'nfs';
|
||||||
server: string;
|
server: string;
|
||||||
|
|
@ -659,6 +660,7 @@ export type GetVolumeResponses = {
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
guest?: boolean;
|
guest?: boolean;
|
||||||
|
mapToContainerUidGid?: boolean;
|
||||||
vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto';
|
vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto';
|
||||||
domain?: string;
|
domain?: string;
|
||||||
port?: string | number;
|
port?: string | number;
|
||||||
|
|
@ -692,13 +694,21 @@ export type GetVolumeResponses = {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
skipHostKeyCheck?: boolean;
|
skipHostKeyCheck?: boolean;
|
||||||
knownHosts?: string;
|
knownHosts?: string;
|
||||||
|
allowLegacySshRsa?: boolean;
|
||||||
};
|
};
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
lastHealthCheck: number;
|
||||||
|
type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp';
|
||||||
|
status: 'mounted' | 'unmounted' | 'error';
|
||||||
|
lastError: string | null;
|
||||||
|
provisioningId?: string | null;
|
||||||
autoRemount: boolean;
|
autoRemount: boolean;
|
||||||
};
|
};
|
||||||
statfs: {
|
statfs: {
|
||||||
total: number;
|
total?: number;
|
||||||
used: number;
|
used?: number;
|
||||||
free: number;
|
free?: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -723,6 +733,7 @@ export type UpdateVolumeData = {
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
guest?: boolean;
|
guest?: boolean;
|
||||||
|
mapToContainerUidGid?: boolean;
|
||||||
vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto';
|
vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto';
|
||||||
domain?: string;
|
domain?: string;
|
||||||
port?: string | number;
|
port?: string | number;
|
||||||
|
|
@ -756,6 +767,7 @@ export type UpdateVolumeData = {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
skipHostKeyCheck?: boolean;
|
skipHostKeyCheck?: boolean;
|
||||||
knownHosts?: string;
|
knownHosts?: string;
|
||||||
|
allowLegacySshRsa?: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
path: {
|
path: {
|
||||||
|
|
@ -779,14 +791,7 @@ export type UpdateVolumeResponses = {
|
||||||
200: {
|
200: {
|
||||||
id: number;
|
id: number;
|
||||||
shortId: string;
|
shortId: string;
|
||||||
provisioningId: string | null;
|
|
||||||
name: string;
|
name: string;
|
||||||
type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp';
|
|
||||||
status: 'mounted' | 'unmounted' | 'error';
|
|
||||||
lastError: string | null;
|
|
||||||
createdAt: number;
|
|
||||||
updatedAt: number;
|
|
||||||
lastHealthCheck: number;
|
|
||||||
config: {
|
config: {
|
||||||
backend: 'nfs';
|
backend: 'nfs';
|
||||||
server: string;
|
server: string;
|
||||||
|
|
@ -801,6 +806,7 @@ export type UpdateVolumeResponses = {
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
guest?: boolean;
|
guest?: boolean;
|
||||||
|
mapToContainerUidGid?: boolean;
|
||||||
vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto';
|
vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto';
|
||||||
domain?: string;
|
domain?: string;
|
||||||
port?: string | number;
|
port?: string | number;
|
||||||
|
|
@ -834,7 +840,15 @@ export type UpdateVolumeResponses = {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
skipHostKeyCheck?: boolean;
|
skipHostKeyCheck?: boolean;
|
||||||
knownHosts?: string;
|
knownHosts?: string;
|
||||||
|
allowLegacySshRsa?: boolean;
|
||||||
};
|
};
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
lastHealthCheck: number;
|
||||||
|
type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp';
|
||||||
|
status: 'mounted' | 'unmounted' | 'error';
|
||||||
|
lastError: string | null;
|
||||||
|
provisioningId?: string | null;
|
||||||
autoRemount: boolean;
|
autoRemount: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -855,8 +869,8 @@ export type MountVolumeResponses = {
|
||||||
* Volume mounted successfully
|
* Volume mounted successfully
|
||||||
*/
|
*/
|
||||||
200: {
|
200: {
|
||||||
error?: string;
|
|
||||||
status: 'mounted' | 'unmounted' | 'error';
|
status: 'mounted' | 'unmounted' | 'error';
|
||||||
|
error?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -876,8 +890,8 @@ export type UnmountVolumeResponses = {
|
||||||
* Volume unmounted successfully
|
* Volume unmounted successfully
|
||||||
*/
|
*/
|
||||||
200: {
|
200: {
|
||||||
error?: string;
|
|
||||||
status: 'mounted' | 'unmounted' | 'error';
|
status: 'mounted' | 'unmounted' | 'error';
|
||||||
|
error?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -904,8 +918,8 @@ export type HealthCheckVolumeResponses = {
|
||||||
* Volume health check result
|
* Volume health check result
|
||||||
*/
|
*/
|
||||||
200: {
|
200: {
|
||||||
error?: string;
|
|
||||||
status: 'mounted' | 'unmounted' | 'error';
|
status: 'mounted' | 'unmounted' | 'error';
|
||||||
|
error?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -932,7 +946,7 @@ export type ListFilesResponses = {
|
||||||
files: Array<{
|
files: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
type: 'file' | 'directory';
|
type: 'directory' | 'file';
|
||||||
size?: number;
|
size?: number;
|
||||||
modifiedAt?: number;
|
modifiedAt?: number;
|
||||||
}>;
|
}>;
|
||||||
|
|
@ -966,8 +980,8 @@ export type BrowseFilesystemResponses = {
|
||||||
directories: Array<{
|
directories: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
type: 'file' | 'directory';
|
type: 'directory';
|
||||||
size?: number;
|
size?: unknown;
|
||||||
modifiedAt?: number;
|
modifiedAt?: number;
|
||||||
}>;
|
}>;
|
||||||
path: string;
|
path: string;
|
||||||
|
|
@ -1136,6 +1150,7 @@ export type ListRepositoriesResponses = {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
skipHostKeyCheck?: boolean;
|
skipHostKeyCheck?: boolean;
|
||||||
knownHosts?: string;
|
knownHosts?: string;
|
||||||
|
allowLegacySshRsa?: boolean;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
cacert?: string;
|
cacert?: string;
|
||||||
|
|
@ -1319,6 +1334,7 @@ export type CreateRepositoryData = {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
skipHostKeyCheck?: boolean;
|
skipHostKeyCheck?: boolean;
|
||||||
knownHosts?: string;
|
knownHosts?: string;
|
||||||
|
allowLegacySshRsa?: boolean;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
cacert?: string;
|
cacert?: string;
|
||||||
|
|
@ -1557,6 +1573,7 @@ export type GetRepositoryResponses = {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
skipHostKeyCheck?: boolean;
|
skipHostKeyCheck?: boolean;
|
||||||
knownHosts?: string;
|
knownHosts?: string;
|
||||||
|
allowLegacySshRsa?: boolean;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
cacert?: string;
|
cacert?: string;
|
||||||
|
|
@ -1740,6 +1757,7 @@ export type UpdateRepositoryData = {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
skipHostKeyCheck?: boolean;
|
skipHostKeyCheck?: boolean;
|
||||||
knownHosts?: string;
|
knownHosts?: string;
|
||||||
|
allowLegacySshRsa?: boolean;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
cacert?: string;
|
cacert?: string;
|
||||||
|
|
@ -1931,6 +1949,7 @@ export type UpdateRepositoryResponses = {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
skipHostKeyCheck?: boolean;
|
skipHostKeyCheck?: boolean;
|
||||||
knownHosts?: string;
|
knownHosts?: string;
|
||||||
|
allowLegacySshRsa?: boolean;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
cacert?: string;
|
cacert?: string;
|
||||||
|
|
@ -2457,14 +2476,7 @@ export type ListBackupSchedulesResponses = {
|
||||||
volume: {
|
volume: {
|
||||||
id: number;
|
id: number;
|
||||||
shortId: string;
|
shortId: string;
|
||||||
provisioningId: string | null;
|
|
||||||
name: string;
|
name: string;
|
||||||
type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp';
|
|
||||||
status: 'mounted' | 'unmounted' | 'error';
|
|
||||||
lastError: string | null;
|
|
||||||
createdAt: number;
|
|
||||||
updatedAt: number;
|
|
||||||
lastHealthCheck: number;
|
|
||||||
config: {
|
config: {
|
||||||
backend: 'nfs';
|
backend: 'nfs';
|
||||||
server: string;
|
server: string;
|
||||||
|
|
@ -2479,6 +2491,7 @@ export type ListBackupSchedulesResponses = {
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
guest?: boolean;
|
guest?: boolean;
|
||||||
|
mapToContainerUidGid?: boolean;
|
||||||
vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto';
|
vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto';
|
||||||
domain?: string;
|
domain?: string;
|
||||||
port?: string | number;
|
port?: string | number;
|
||||||
|
|
@ -2512,7 +2525,15 @@ export type ListBackupSchedulesResponses = {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
skipHostKeyCheck?: boolean;
|
skipHostKeyCheck?: boolean;
|
||||||
knownHosts?: string;
|
knownHosts?: string;
|
||||||
|
allowLegacySshRsa?: boolean;
|
||||||
};
|
};
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
lastHealthCheck: number;
|
||||||
|
type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp';
|
||||||
|
status: 'mounted' | 'unmounted' | 'error';
|
||||||
|
lastError: string | null;
|
||||||
|
provisioningId?: string | null;
|
||||||
autoRemount: boolean;
|
autoRemount: boolean;
|
||||||
};
|
};
|
||||||
repository: {
|
repository: {
|
||||||
|
|
@ -2664,6 +2685,7 @@ export type ListBackupSchedulesResponses = {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
skipHostKeyCheck?: boolean;
|
skipHostKeyCheck?: boolean;
|
||||||
knownHosts?: string;
|
knownHosts?: string;
|
||||||
|
allowLegacySshRsa?: boolean;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
cacert?: string;
|
cacert?: string;
|
||||||
|
|
@ -2875,14 +2897,7 @@ export type GetBackupScheduleResponses = {
|
||||||
volume: {
|
volume: {
|
||||||
id: number;
|
id: number;
|
||||||
shortId: string;
|
shortId: string;
|
||||||
provisioningId: string | null;
|
|
||||||
name: string;
|
name: string;
|
||||||
type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp';
|
|
||||||
status: 'mounted' | 'unmounted' | 'error';
|
|
||||||
lastError: string | null;
|
|
||||||
createdAt: number;
|
|
||||||
updatedAt: number;
|
|
||||||
lastHealthCheck: number;
|
|
||||||
config: {
|
config: {
|
||||||
backend: 'nfs';
|
backend: 'nfs';
|
||||||
server: string;
|
server: string;
|
||||||
|
|
@ -2897,6 +2912,7 @@ export type GetBackupScheduleResponses = {
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
guest?: boolean;
|
guest?: boolean;
|
||||||
|
mapToContainerUidGid?: boolean;
|
||||||
vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto';
|
vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto';
|
||||||
domain?: string;
|
domain?: string;
|
||||||
port?: string | number;
|
port?: string | number;
|
||||||
|
|
@ -2930,7 +2946,15 @@ export type GetBackupScheduleResponses = {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
skipHostKeyCheck?: boolean;
|
skipHostKeyCheck?: boolean;
|
||||||
knownHosts?: string;
|
knownHosts?: string;
|
||||||
|
allowLegacySshRsa?: boolean;
|
||||||
};
|
};
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
lastHealthCheck: number;
|
||||||
|
type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp';
|
||||||
|
status: 'mounted' | 'unmounted' | 'error';
|
||||||
|
lastError: string | null;
|
||||||
|
provisioningId?: string | null;
|
||||||
autoRemount: boolean;
|
autoRemount: boolean;
|
||||||
};
|
};
|
||||||
repository: {
|
repository: {
|
||||||
|
|
@ -3082,6 +3106,7 @@ export type GetBackupScheduleResponses = {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
skipHostKeyCheck?: boolean;
|
skipHostKeyCheck?: boolean;
|
||||||
knownHosts?: string;
|
knownHosts?: string;
|
||||||
|
allowLegacySshRsa?: boolean;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
cacert?: string;
|
cacert?: string;
|
||||||
|
|
@ -3274,14 +3299,7 @@ export type GetBackupScheduleForVolumeResponses = {
|
||||||
volume: {
|
volume: {
|
||||||
id: number;
|
id: number;
|
||||||
shortId: string;
|
shortId: string;
|
||||||
provisioningId: string | null;
|
|
||||||
name: string;
|
name: string;
|
||||||
type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp';
|
|
||||||
status: 'mounted' | 'unmounted' | 'error';
|
|
||||||
lastError: string | null;
|
|
||||||
createdAt: number;
|
|
||||||
updatedAt: number;
|
|
||||||
lastHealthCheck: number;
|
|
||||||
config: {
|
config: {
|
||||||
backend: 'nfs';
|
backend: 'nfs';
|
||||||
server: string;
|
server: string;
|
||||||
|
|
@ -3296,6 +3314,7 @@ export type GetBackupScheduleForVolumeResponses = {
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
guest?: boolean;
|
guest?: boolean;
|
||||||
|
mapToContainerUidGid?: boolean;
|
||||||
vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto';
|
vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto';
|
||||||
domain?: string;
|
domain?: string;
|
||||||
port?: string | number;
|
port?: string | number;
|
||||||
|
|
@ -3329,7 +3348,15 @@ export type GetBackupScheduleForVolumeResponses = {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
skipHostKeyCheck?: boolean;
|
skipHostKeyCheck?: boolean;
|
||||||
knownHosts?: string;
|
knownHosts?: string;
|
||||||
|
allowLegacySshRsa?: boolean;
|
||||||
};
|
};
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
lastHealthCheck: number;
|
||||||
|
type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp';
|
||||||
|
status: 'mounted' | 'unmounted' | 'error';
|
||||||
|
lastError: string | null;
|
||||||
|
provisioningId?: string | null;
|
||||||
autoRemount: boolean;
|
autoRemount: boolean;
|
||||||
};
|
};
|
||||||
repository: {
|
repository: {
|
||||||
|
|
@ -3481,6 +3508,7 @@ export type GetBackupScheduleForVolumeResponses = {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
skipHostKeyCheck?: boolean;
|
skipHostKeyCheck?: boolean;
|
||||||
knownHosts?: string;
|
knownHosts?: string;
|
||||||
|
allowLegacySshRsa?: boolean;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
cacert?: string;
|
cacert?: string;
|
||||||
|
|
@ -3955,6 +3983,7 @@ export type GetScheduleMirrorsResponses = {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
skipHostKeyCheck?: boolean;
|
skipHostKeyCheck?: boolean;
|
||||||
knownHosts?: string;
|
knownHosts?: string;
|
||||||
|
allowLegacySshRsa?: boolean;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
cacert?: string;
|
cacert?: string;
|
||||||
|
|
@ -4167,6 +4196,7 @@ export type UpdateScheduleMirrorsResponses = {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
skipHostKeyCheck?: boolean;
|
skipHostKeyCheck?: boolean;
|
||||||
knownHosts?: string;
|
knownHosts?: string;
|
||||||
|
allowLegacySshRsa?: boolean;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
cacert?: string;
|
cacert?: string;
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,13 @@ const defaultValuesForType = (repoBase: string) => ({
|
||||||
azure: { backend: "azure" as const, compressionMode: "auto" as const },
|
azure: { backend: "azure" as const, compressionMode: "auto" as const },
|
||||||
rclone: { backend: "rclone" as const, compressionMode: "auto" as const },
|
rclone: { backend: "rclone" as const, compressionMode: "auto" as const },
|
||||||
rest: { backend: "rest" as const, compressionMode: "auto" as const },
|
rest: { backend: "rest" as const, compressionMode: "auto" as const },
|
||||||
sftp: { backend: "sftp" as const, compressionMode: "auto" as const, port: 22, skipHostKeyCheck: false },
|
sftp: {
|
||||||
|
backend: "sftp" as const,
|
||||||
|
compressionMode: "auto" as const,
|
||||||
|
port: 22,
|
||||||
|
skipHostKeyCheck: false,
|
||||||
|
allowLegacySshRsa: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const CreateRepositoryForm = ({
|
export const CreateRepositoryForm = ({
|
||||||
|
|
@ -251,7 +257,9 @@ export const CreateRepositoryForm = ({
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<FormLabel>Import existing repository</FormLabel>
|
<FormLabel>Import existing repository</FormLabel>
|
||||||
<FormDescription>Check this if the repository already exists at the specified location</FormDescription>
|
<FormDescription>
|
||||||
|
Check this if the repository already exists at the specified location
|
||||||
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|
@ -281,8 +289,8 @@ export const CreateRepositoryForm = ({
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Choose whether to use Zerobyte's recovery key (which you downloaded when creating your account) or enter
|
Choose whether to use Zerobyte's recovery key (which you downloaded when creating your
|
||||||
a custom password for the existing repository.
|
account) or enter a custom password for the existing repository.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export const AdvancedForm = ({ form }: Props) => {
|
||||||
const cacert = useWatch({ control: form.control, name: "cacert" });
|
const cacert = useWatch({ control: form.control, name: "cacert" });
|
||||||
const uploadLimitEnabled = useWatch({ control: form.control, name: "uploadLimit.enabled" });
|
const uploadLimitEnabled = useWatch({ control: form.control, name: "uploadLimit.enabled" });
|
||||||
const downloadLimitEnabled = useWatch({ control: form.control, name: "downloadLimit.enabled" });
|
const downloadLimitEnabled = useWatch({ control: form.control, name: "downloadLimit.enabled" });
|
||||||
|
const backend = useWatch({ control: form.control, name: "backend" });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible>
|
<Collapsible>
|
||||||
|
|
@ -44,7 +45,9 @@ export const AdvancedForm = ({ form }: Props) => {
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<FormLabel>Enable upload speed limit</FormLabel>
|
<FormLabel>Enable upload speed limit</FormLabel>
|
||||||
<FormDescription className="text-xs">Limit upload speed to the repository</FormDescription>
|
<FormDescription className="text-xs">
|
||||||
|
Limit upload speed to the repository
|
||||||
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|
@ -66,7 +69,9 @@ export const AdvancedForm = ({ form }: Props) => {
|
||||||
placeholder="10"
|
placeholder="10"
|
||||||
className="pr-12"
|
className="pr-12"
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => field.onChange(parseFloat(e.target.value) || 1)}
|
onChange={(e) =>
|
||||||
|
field.onChange(parseFloat(e.target.value) || 1)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
|
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
|
||||||
<div className="h-4 w-px bg-border" />
|
<div className="h-4 w-px bg-border" />
|
||||||
|
|
@ -120,7 +125,9 @@ export const AdvancedForm = ({ form }: Props) => {
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<FormLabel>Enable download speed limit</FormLabel>
|
<FormLabel>Enable download speed limit</FormLabel>
|
||||||
<FormDescription className="text-xs">Limit download speed from the repository</FormDescription>
|
<FormDescription className="text-xs">
|
||||||
|
Limit download speed from the repository
|
||||||
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|
@ -144,7 +151,9 @@ export const AdvancedForm = ({ form }: Props) => {
|
||||||
disabled={!downloadLimitEnabled}
|
disabled={!downloadLimitEnabled}
|
||||||
className="pr-12"
|
className="pr-12"
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => field.onChange(parseFloat(e.target.value) || 1)}
|
onChange={(e) =>
|
||||||
|
field.onChange(parseFloat(e.target.value) || 1)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
|
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
|
||||||
<div className="h-4 w-px bg-border" />
|
<div className="h-4 w-px bg-border" />
|
||||||
|
|
@ -208,8 +217,8 @@ export const AdvancedForm = ({ form }: Props) => {
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className={cn({ hidden: !cacert })}>
|
<TooltipContent className={cn({ hidden: !cacert })}>
|
||||||
<p className="max-w-xs">
|
<p className="max-w-xs">
|
||||||
This option is disabled because a CA certificate is provided. Remove the CA certificate to skip
|
This option is disabled because a CA certificate is provided. Remove the CA
|
||||||
TLS validation instead.
|
certificate to skip TLS validation instead.
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
@ -217,8 +226,8 @@ export const AdvancedForm = ({ form }: Props) => {
|
||||||
<div className="space-y-1 leading-none">
|
<div className="space-y-1 leading-none">
|
||||||
<FormLabel>Skip TLS certificate verification</FormLabel>
|
<FormLabel>Skip TLS certificate verification</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Disable TLS certificate verification for HTTPS connections with self-signed certificates. This is
|
Disable TLS certificate verification for HTTPS connections with self-signed
|
||||||
insecure and should only be used for testing.
|
certificates. This is insecure and should only be used for testing.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
@ -235,7 +244,9 @@ export const AdvancedForm = ({ form }: Props) => {
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div>
|
<div>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder={"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"}
|
placeholder={
|
||||||
|
"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||||
|
}
|
||||||
rows={6}
|
rows={6}
|
||||||
disabled={insecureTls}
|
disabled={insecureTls}
|
||||||
{...field}
|
{...field}
|
||||||
|
|
@ -244,8 +255,8 @@ export const AdvancedForm = ({ form }: Props) => {
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className={cn({ hidden: !insecureTls })}>
|
<TooltipContent className={cn({ hidden: !insecureTls })}>
|
||||||
<p className="max-w-xs">
|
<p className="max-w-xs">
|
||||||
CA certificate is disabled because TLS validation is being skipped. Uncheck "Skip TLS Certificate
|
CA certificate is disabled because TLS validation is being skipped. Uncheck
|
||||||
Verification" to provide a custom CA certificate.
|
"Skip TLS Certificate Verification" to provide a custom CA certificate.
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
@ -266,6 +277,26 @@ export const AdvancedForm = ({ form }: Props) => {
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{backend === "sftp" && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="allowLegacySshRsa"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Allow legacy SSH RSA/SHA1 algorithms</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Only enable this for legacy SFTP servers that offer <code>ssh-rsa</code> only.
|
||||||
|
It permits RSA/SHA1 signatures, which are weaker than modern SSH algorithms.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox checked={field.value ?? false} onCheckedChange={field.onChange} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ const defaultValuesForType = {
|
||||||
smb: { backend: "smb" as const, port: 445, vers: "3.0" as const, mapToContainerUidGid: false },
|
smb: { backend: "smb" as const, port: 445, vers: "3.0" as const, mapToContainerUidGid: false },
|
||||||
webdav: { backend: "webdav" as const, port: 80, ssl: false, path: "/webdav" },
|
webdav: { backend: "webdav" as const, port: 80, ssl: false, path: "/webdav" },
|
||||||
rclone: { backend: "rclone" as const, path: "/" },
|
rclone: { backend: "rclone" as const, path: "/" },
|
||||||
sftp: { backend: "sftp" as const, port: 22, path: "/", skipHostKeyCheck: false },
|
sftp: { backend: "sftp" as const, port: 22, path: "/", skipHostKeyCheck: false, allowLegacySshRsa: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, formId, loading, className }: Props) => {
|
export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, formId, loading, className }: Props) => {
|
||||||
|
|
@ -230,7 +230,10 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div>
|
<div>
|
||||||
<SelectItem disabled={!capabilities.rclone || !capabilities.sysAdmin} value="rclone">
|
<SelectItem
|
||||||
|
disabled={!capabilities.rclone || !capabilities.sysAdmin}
|
||||||
|
value="rclone"
|
||||||
|
>
|
||||||
rclone
|
rclone
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -238,7 +241,11 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
||||||
<TooltipContent className={cn({ hidden: capabilities.sysAdmin })}>
|
<TooltipContent className={cn({ hidden: capabilities.sysAdmin })}>
|
||||||
<p>Remote mounts require SYS_ADMIN capability</p>
|
<p>Remote mounts require SYS_ADMIN capability</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
<TooltipContent className={cn({ hidden: !capabilities.sysAdmin || capabilities.rclone })}>
|
<TooltipContent
|
||||||
|
className={cn({
|
||||||
|
hidden: !capabilities.sysAdmin || capabilities.rclone,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<p>Setup rclone to use this backend</p>
|
<p>Setup rclone to use this backend</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { Input } from "../../../../components/ui/input";
|
||||||
import { SecretInput } from "../../../../components/ui/secret-input";
|
import { SecretInput } from "../../../../components/ui/secret-input";
|
||||||
import { Textarea } from "../../../../components/ui/textarea";
|
import { Textarea } from "../../../../components/ui/textarea";
|
||||||
import { Switch } from "../../../../components/ui/switch";
|
import { Switch } from "../../../../components/ui/switch";
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../../components/ui/collapsible";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
form: UseFormReturn<FormValues>;
|
form: UseFormReturn<FormValues>;
|
||||||
|
|
@ -78,7 +79,9 @@ export const SFTPForm = ({ form }: Props) => {
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SecretInput placeholder="••••••••" value={field.value ?? ""} onChange={field.onChange} />
|
<SecretInput placeholder="••••••••" value={field.value ?? ""} onChange={field.onChange} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>Password for SFTP authentication (optional if using private key).</FormDescription>
|
<FormDescription>
|
||||||
|
Password for SFTP authentication (optional if using private key).
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|
@ -98,7 +101,9 @@ export const SFTPForm = ({ form }: Props) => {
|
||||||
value={field.value ?? ""}
|
value={field.value ?? ""}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>SSH private key for authentication (optional if using password).</FormDescription>
|
<FormDescription>
|
||||||
|
SSH private key for authentication (optional if using password).
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|
@ -158,6 +163,29 @@ export const SFTPForm = ({ form }: Props) => {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<Collapsible>
|
||||||
|
<CollapsibleTrigger>Advanced Settings</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent className="pb-4 pt-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="allowLegacySshRsa"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Allow legacy SSH RSA/SHA1 algorithms</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Only enable this for legacy SFTP servers that offer <code>ssh-rsa</code> only.
|
||||||
|
It permits RSA/SHA1 signatures, which are weaker than modern SSH algorithms.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch checked={field.value ?? false} onCheckedChange={field.onChange} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,9 @@ function ConfigRow({ icon, label, value, mono }: ConfigRowProps) {
|
||||||
<div className="flex items-center gap-3 py-3 first:pt-0 last:pb-0">
|
<div className="flex items-center gap-3 py-3 first:pt-0 last:pb-0">
|
||||||
<span className="text-muted-foreground shrink-0">{icon}</span>
|
<span className="text-muted-foreground shrink-0">{icon}</span>
|
||||||
<span className="text-sm text-muted-foreground w-40 shrink-0">{label}</span>
|
<span className="text-sm text-muted-foreground w-40 shrink-0">{label}</span>
|
||||||
<span className={cn("text-sm break-all", { "font-mono bg-muted/50 px-2 py-0.5 rounded": mono })}>{value}</span>
|
<span className={cn("text-sm break-all", { "font-mono bg-muted/50 px-2 py-0.5 rounded": mono })}>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -43,12 +45,19 @@ function BackendConfigRows({ volume }: { volume: Volume }) {
|
||||||
|
|
||||||
switch (config.backend) {
|
switch (config.backend) {
|
||||||
case "directory":
|
case "directory":
|
||||||
return <ConfigRow icon={<FolderOpen className="h-4 w-4" />} label="Directory Path" value={config.path} mono />;
|
return (
|
||||||
|
<ConfigRow icon={<FolderOpen className="h-4 w-4" />} label="Directory Path" value={config.path} mono />
|
||||||
|
);
|
||||||
case "nfs":
|
case "nfs":
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ConfigRow icon={<FolderOpen className="h-4 w-4" />} label="Server" value={config.server} mono />
|
<ConfigRow icon={<FolderOpen className="h-4 w-4" />} label="Server" value={config.server} mono />
|
||||||
<ConfigRow icon={<FolderOpen className="h-4 w-4" />} label="Export Path" value={config.exportPath} mono />
|
<ConfigRow
|
||||||
|
icon={<FolderOpen className="h-4 w-4" />}
|
||||||
|
label="Export Path"
|
||||||
|
value={config.exportPath}
|
||||||
|
mono
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
case "smb":
|
case "smb":
|
||||||
|
|
@ -84,6 +93,8 @@ function BackendConfigRows({ volume }: { volume: Volume }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function DonutChart({ statfs }: { statfs: StatFs }) {
|
function DonutChart({ statfs }: { statfs: StatFs }) {
|
||||||
|
const { used = 0, total = 0 } = statfs;
|
||||||
|
|
||||||
const chartData = useMemo(
|
const chartData = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ name: "Used", value: statfs.used, fill: "var(--strong-accent)" },
|
{ name: "Used", value: statfs.used, fill: "var(--strong-accent)" },
|
||||||
|
|
@ -93,8 +104,8 @@ function DonutChart({ statfs }: { statfs: StatFs }) {
|
||||||
);
|
);
|
||||||
|
|
||||||
const usagePercentage = useMemo(() => {
|
const usagePercentage = useMemo(() => {
|
||||||
return Math.round((statfs.used / statfs.total) * 100);
|
return Math.round((used / total) * 100);
|
||||||
}, [statfs]);
|
}, [used, total]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartContainer config={{}} className="mx-auto aspect-square max-h-[200px]">
|
<ChartContainer config={{}} className="mx-auto aspect-square max-h-[200px]">
|
||||||
|
|
@ -114,10 +125,18 @@ function DonutChart({ statfs }: { statfs: StatFs }) {
|
||||||
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
||||||
return (
|
return (
|
||||||
<text x={viewBox.cx} y={viewBox.cy} textAnchor="middle" dominantBaseline="middle">
|
<text x={viewBox.cx} y={viewBox.cy} textAnchor="middle" dominantBaseline="middle">
|
||||||
<tspan x={viewBox.cx} y={viewBox.cy} className="fill-foreground text-2xl font-bold">
|
<tspan
|
||||||
|
x={viewBox.cx}
|
||||||
|
y={viewBox.cy}
|
||||||
|
className="fill-foreground text-2xl font-bold"
|
||||||
|
>
|
||||||
{usagePercentage}%
|
{usagePercentage}%
|
||||||
</tspan>
|
</tspan>
|
||||||
<tspan x={viewBox.cx} y={(viewBox.cy || 0) + 20} className="fill-muted-foreground text-xs">
|
<tspan
|
||||||
|
x={viewBox.cx}
|
||||||
|
y={(viewBox.cy || 0) + 20}
|
||||||
|
className="fill-muted-foreground text-xs"
|
||||||
|
>
|
||||||
Used
|
Used
|
||||||
</tspan>
|
</tspan>
|
||||||
</text>
|
</text>
|
||||||
|
|
@ -132,7 +151,9 @@ function DonutChart({ statfs }: { statfs: StatFs }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
|
export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
|
||||||
const hasStorage = statfs.total > 0;
|
const { total = 0, used = 0, free = 0 } = statfs;
|
||||||
|
|
||||||
|
const hasStorage = total > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="px-6 py-6 @container/inner">
|
<Card className="px-6 py-6 @container/inner">
|
||||||
|
|
@ -144,7 +165,11 @@ export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div className="space-y-0 divide-y divide-border/50">
|
<div className="space-y-0 divide-y divide-border/50">
|
||||||
<ConfigRow icon={<HardDrive className="h-4 w-4" />} label="Name" value={volume.name} />
|
<ConfigRow icon={<HardDrive className="h-4 w-4" />} label="Name" value={volume.name} />
|
||||||
<ConfigRow icon={<HardDrive className="h-4 w-4" />} label="Backend" value={backendLabels[volume.type]} />
|
<ConfigRow
|
||||||
|
icon={<HardDrive className="h-4 w-4" />}
|
||||||
|
label="Backend"
|
||||||
|
value={backendLabels[volume.type]}
|
||||||
|
/>
|
||||||
<BackendConfigRows volume={volume} />
|
<BackendConfigRows volume={volume} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -162,21 +187,21 @@ export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
|
||||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||||
<span className="text-sm font-medium">Total</span>
|
<span className="text-sm font-medium">Total</span>
|
||||||
</div>
|
</div>
|
||||||
<ByteSize bytes={statfs.total} className="font-mono text-sm" />
|
<ByteSize bytes={total} className="font-mono text-sm" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/50">
|
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/50">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="h-3 w-3 rounded-full bg-strong-accent" />
|
<div className="h-3 w-3 rounded-full bg-strong-accent" />
|
||||||
<span className="text-sm font-medium">Used</span>
|
<span className="text-sm font-medium">Used</span>
|
||||||
</div>
|
</div>
|
||||||
<ByteSize bytes={statfs.used} className="font-mono text-sm" />
|
<ByteSize bytes={used} className="font-mono text-sm" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/50">
|
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/50">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="h-3 w-3 rounded-full bg-primary" />
|
<div className="h-3 w-3 rounded-full bg-primary" />
|
||||||
<span className="text-sm font-medium">Free</span>
|
<span className="text-sm font-medium">Free</span>
|
||||||
</div>
|
</div>
|
||||||
<ByteSize bytes={statfs.free} className="font-mono text-sm" />
|
<ByteSize bytes={free} className="font-mono text-sm" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
1
app/drizzle/20260522174225_organic_nehzno/migration.sql
Normal file
1
app/drizzle/20260522174225_organic_nehzno/migration.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE `two_factor` ADD `verified` integer DEFAULT true NOT NULL;
|
||||||
2821
app/drizzle/20260522174225_organic_nehzno/snapshot.json
Normal file
2821
app/drizzle/20260522174225_organic_nehzno/snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -505,6 +505,7 @@ export const twoFactor = sqliteTable(
|
||||||
userId: text("user_id")
|
userId: text("user_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => usersTable.id, { onDelete: "cascade" }),
|
.references(() => usersTable.id, { onDelete: "cascade" }),
|
||||||
|
verified: integer("verified", { mode: "boolean" }).notNull().default(true),
|
||||||
},
|
},
|
||||||
(table) => [index("twoFactor_secret_idx").on(table.secret), index("twoFactor_userId_idx").on(table.userId)],
|
(table) => [index("twoFactor_secret_idx").on(table.secret), index("twoFactor_userId_idx").on(table.userId)],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -279,6 +279,7 @@ describe("backup execution - validation failures", () => {
|
||||||
privateKey: "key",
|
privateKey: "key",
|
||||||
path: "/data",
|
path: "/data",
|
||||||
skipHostKeyCheck: false,
|
skipHostKeyCheck: false,
|
||||||
|
allowLegacySshRsa: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const repository = await createTestRepository();
|
const repository = await createTestRepository();
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ public_key="$(printf '%s' "$7" | base64 -d)"
|
||||||
repo_path="/srv/zerobyte-backend-integration/restic-repo"
|
repo_path="/srv/zerobyte-backend-integration/restic-repo"
|
||||||
repo_password_fingerprint_path="$repo_path/.zerobyte-password-sha256"
|
repo_password_fingerprint_path="$repo_path/.zerobyte-password-sha256"
|
||||||
repo_password_fingerprint="$(printf '%s' "$restic_password" | sha256sum | cut -d' ' -f1)"
|
repo_password_fingerprint="$(printf '%s' "$restic_password" | sha256sum | cut -d' ' -f1)"
|
||||||
|
legacy_sshd_dir="/etc/ssh/zerobyte-backend-integration-legacy"
|
||||||
|
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
|
@ -158,6 +159,11 @@ EOF
|
||||||
a2ensite zerobyte-backend-integration-dav >/dev/null
|
a2ensite zerobyte-backend-integration-dav >/dev/null
|
||||||
apache2ctl configtest
|
apache2ctl configtest
|
||||||
|
|
||||||
|
install -d -m 0700 "$legacy_sshd_dir"
|
||||||
|
if [[ ! -f "$legacy_sshd_dir/ssh_host_rsa_key" ]]; then
|
||||||
|
ssh-keygen -q -t rsa -b 2048 -N "" -f "$legacy_sshd_dir/ssh_host_rsa_key"
|
||||||
|
fi
|
||||||
|
|
||||||
install -d -m 0755 /etc/ssh/sshd_config.d
|
install -d -m 0755 /etc/ssh/sshd_config.d
|
||||||
write_file /etc/ssh/sshd_config.d/zerobyte-backend-integration.conf <<'EOF'
|
write_file /etc/ssh/sshd_config.d/zerobyte-backend-integration.conf <<'EOF'
|
||||||
Match User zerobyte-sftp
|
Match User zerobyte-sftp
|
||||||
|
|
@ -170,17 +176,68 @@ Match User zerobyte-sftp
|
||||||
EOF
|
EOF
|
||||||
sshd -t
|
sshd -t
|
||||||
|
|
||||||
|
write_file "$legacy_sshd_dir/sshd_config" <<EOF
|
||||||
|
Port 2222
|
||||||
|
ListenAddress 0.0.0.0
|
||||||
|
PidFile /run/zerobyte-backend-integration-legacy-sshd.pid
|
||||||
|
HostKey $legacy_sshd_dir/ssh_host_rsa_key
|
||||||
|
HostKeyAlgorithms ssh-rsa
|
||||||
|
PasswordAuthentication yes
|
||||||
|
PubkeyAuthentication no
|
||||||
|
KbdInteractiveAuthentication no
|
||||||
|
PermitRootLogin no
|
||||||
|
PermitTTY no
|
||||||
|
X11Forwarding no
|
||||||
|
AllowTcpForwarding no
|
||||||
|
Subsystem sftp internal-sftp
|
||||||
|
|
||||||
|
Match User zerobyte-sftp
|
||||||
|
ForceCommand internal-sftp
|
||||||
|
EOF
|
||||||
|
sshd -t -f "$legacy_sshd_dir/sshd_config"
|
||||||
|
|
||||||
|
write_file /etc/systemd/system/zerobyte-backend-integration-legacy-sshd.service <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Zerobyte Backend Integration Legacy SFTP
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/sbin/sshd -D -f $legacy_sshd_dir/sshd_config
|
||||||
|
ExecReload=/bin/kill -HUP \$MAINPID
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now zerobyte-backend-integration-legacy-sshd.service
|
||||||
|
|
||||||
systemctl restart apache2
|
systemctl restart apache2
|
||||||
systemctl restart smbd
|
systemctl restart smbd
|
||||||
systemctl restart ssh
|
systemctl restart ssh
|
||||||
|
systemctl restart zerobyte-backend-integration-legacy-sshd.service
|
||||||
|
systemctl is-active --quiet zerobyte-backend-integration-legacy-sshd.service
|
||||||
|
for _ in 1 2 3 4 5; do
|
||||||
|
ss -ltn | grep -q ':2222' && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
ss -ltn | grep -q ':2222'
|
||||||
REMOTE
|
REMOTE
|
||||||
|
|
||||||
ssh-keyscan "$TARGET_HOST" >"$KNOWN_HOSTS_PATH" 2>/dev/null
|
ssh-keyscan "$TARGET_HOST" >"$KNOWN_HOSTS_PATH" 2>/dev/null
|
||||||
|
if ! ssh-keyscan -T 5 -p 2222 "$TARGET_HOST" >>"$KNOWN_HOSTS_PATH" 2>/dev/null; then
|
||||||
|
echo "Failed to scan legacy SFTP host key from $TARGET_HOST:2222" >&2
|
||||||
|
echo "Check the target service with:" >&2
|
||||||
|
echo " ssh $TARGET systemctl status zerobyte-backend-integration-legacy-sshd.service" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
INTEGRATION_HOST="$TARGET_HOST" \
|
INTEGRATION_HOST="$TARGET_HOST" \
|
||||||
FIXTURE_UID="$FIXTURE_UID" \
|
FIXTURE_UID="$FIXTURE_UID" \
|
||||||
FIXTURE_GID="$FIXTURE_GID" \
|
FIXTURE_GID="$FIXTURE_GID" \
|
||||||
SMB_PASSWORD="$SMB_PASSWORD" \
|
SMB_PASSWORD="$SMB_PASSWORD" \
|
||||||
|
SFTP_PASSWORD="$SFTP_PASSWORD" \
|
||||||
WEBDAV_PASSWORD="$WEBDAV_PASSWORD" \
|
WEBDAV_PASSWORD="$WEBDAV_PASSWORD" \
|
||||||
RESTIC_PASSWORD="$RESTIC_PASSWORD" \
|
RESTIC_PASSWORD="$RESTIC_PASSWORD" \
|
||||||
SFTP_KEY_PATH="$KEY_PATH" \
|
SFTP_KEY_PATH="$KEY_PATH" \
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ const host = getRequiredEnv("INTEGRATION_HOST");
|
||||||
const fixtureUid = getRequiredNumberEnv("FIXTURE_UID");
|
const fixtureUid = getRequiredNumberEnv("FIXTURE_UID");
|
||||||
const fixtureGid = getRequiredNumberEnv("FIXTURE_GID");
|
const fixtureGid = getRequiredNumberEnv("FIXTURE_GID");
|
||||||
const smbPassword = getRequiredEnv("SMB_PASSWORD");
|
const smbPassword = getRequiredEnv("SMB_PASSWORD");
|
||||||
|
const sftpPassword = getRequiredEnv("SFTP_PASSWORD");
|
||||||
const webdavPassword = getRequiredEnv("WEBDAV_PASSWORD");
|
const webdavPassword = getRequiredEnv("WEBDAV_PASSWORD");
|
||||||
const resticPassword = getRequiredEnv("RESTIC_PASSWORD");
|
const resticPassword = getRequiredEnv("RESTIC_PASSWORD");
|
||||||
const privateKey = fs.readFileSync(getRequiredEnv("SFTP_KEY_PATH"), "utf8");
|
const privateKey = fs.readFileSync(getRequiredEnv("SFTP_KEY_PATH"), "utf8");
|
||||||
|
|
@ -29,7 +30,7 @@ const nfsEntries = [
|
||||||
|
|
||||||
const smbEntries = [
|
const smbEntries = [
|
||||||
{ path: "hello.txt", type: "file", uid: fixtureUid, gid: fixtureGid, mode: "0644", text: fileText },
|
{ path: "hello.txt", type: "file", uid: fixtureUid, gid: fixtureGid, mode: "0644", text: fileText },
|
||||||
{ path: "docs", type: "directory", uid: fixtureUid, gid: fixtureGid, mode: "0755" },
|
{ path: "docs", type: "directory", uid: fixtureUid, gid: fixtureGid, mode: "1755" },
|
||||||
{ path: "docs/readme.md", type: "file", uid: fixtureUid, gid: fixtureGid, mode: "0644", text: readmeText },
|
{ path: "docs/readme.md", type: "file", uid: fixtureUid, gid: fixtureGid, mode: "0644", text: readmeText },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -90,6 +91,41 @@ const config = {
|
||||||
fixtureRoot: "case-a",
|
fixtureRoot: "case-a",
|
||||||
expectedEntries: contentOnlyEntries,
|
expectedEntries: contentOnlyEntries,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "sftp-password-local-repo",
|
||||||
|
volume: {
|
||||||
|
backend: "sftp",
|
||||||
|
host,
|
||||||
|
port: 22,
|
||||||
|
username: "zerobyte-sftp",
|
||||||
|
password: sftpPassword,
|
||||||
|
path: "/srv/zerobyte-backend-integration/fixtures",
|
||||||
|
readOnly: true,
|
||||||
|
skipHostKeyCheck: false,
|
||||||
|
knownHosts,
|
||||||
|
},
|
||||||
|
repository: { backend: "local", path: "repo-sftp-password-volume" },
|
||||||
|
fixtureRoot: "case-a",
|
||||||
|
expectedEntries: contentOnlyEntries,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sftp-legacy-rsa-hostkey-local-repo",
|
||||||
|
volume: {
|
||||||
|
backend: "sftp",
|
||||||
|
host,
|
||||||
|
port: 2222,
|
||||||
|
username: "zerobyte-sftp",
|
||||||
|
password: sftpPassword,
|
||||||
|
path: "/srv/zerobyte-backend-integration/fixtures",
|
||||||
|
readOnly: true,
|
||||||
|
skipHostKeyCheck: false,
|
||||||
|
knownHosts,
|
||||||
|
allowLegacySshRsa: true,
|
||||||
|
},
|
||||||
|
repository: { backend: "local", path: "repo-sftp-legacy-rsa-hostkey-volume" },
|
||||||
|
fixtureRoot: "case-a",
|
||||||
|
expectedEntries: contentOnlyEntries,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "webdav-local-repo",
|
id: "webdav-local-repo",
|
||||||
volume: {
|
volume: {
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,8 @@ const mount = async (config: BackendConfig, mountPath: string) => {
|
||||||
`gid=${gid}`,
|
`gid=${gid}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (config.allowLegacySshRsa) options.push("HostKeyAlgorithms=+ssh-rsa");
|
||||||
|
|
||||||
if (config.skipHostKeyCheck) {
|
if (config.skipHostKeyCheck) {
|
||||||
options.push("StrictHostKeyChecking=no", "UserKnownHostsFile=/dev/null");
|
options.push("StrictHostKeyChecking=no", "UserKnownHostsFile=/dev/null");
|
||||||
} else if (config.knownHosts) {
|
} else if (config.knownHosts) {
|
||||||
|
|
|
||||||
1
bun.lock
1
bun.lock
|
|
@ -70,6 +70,7 @@
|
||||||
"zod": "^4.4.3",
|
"zod": "^4.4.3",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@better-auth/utils": "0.4.0",
|
||||||
"@effect/language-service": "^0.86.1",
|
"@effect/language-service": "^0.86.1",
|
||||||
"@faker-js/faker": "^10.4.0",
|
"@faker-js/faker": "^10.4.0",
|
||||||
"@hey-api/openapi-ts": "^0.97.1",
|
"@hey-api/openapi-ts": "^0.97.1",
|
||||||
|
|
|
||||||
51
e2e/0005-two-factor.spec.ts
Normal file
51
e2e/0005-two-factor.spec.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import type { Page } from "@playwright/test";
|
||||||
|
import { base32 } from "@better-auth/utils/base32";
|
||||||
|
import { createOTP } from "@better-auth/utils/otp";
|
||||||
|
import { expect, test } from "./test";
|
||||||
|
import { gotoAndWaitForAppReady } from "./helpers/page";
|
||||||
|
|
||||||
|
const workerPassword = "password123";
|
||||||
|
|
||||||
|
async function generateTotp(secret: string) {
|
||||||
|
const decodedSecret = new TextDecoder().decode(base32.decode(secret));
|
||||||
|
return createOTP(decodedSecret).totp();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fillOtp(page: Page, code: string) {
|
||||||
|
await page.locator('[data-slot="input-otp"]').click();
|
||||||
|
await page.keyboard.type(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
test("user can enable 2FA and sign in with a TOTP code", async ({ page }) => {
|
||||||
|
await gotoAndWaitForAppReady(page, "/settings");
|
||||||
|
|
||||||
|
const username = await page.locator("#username").inputValue();
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Enable 2FA" }).click();
|
||||||
|
await page.getByRole("textbox", { name: "Password" }).fill(workerPassword);
|
||||||
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole("heading", { name: "Scan QR Code" })).toBeVisible();
|
||||||
|
const secret = await page.locator("input[readonly]").inputValue();
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
|
await expect(page.getByRole("heading", { name: "Verify setup" })).toBeVisible();
|
||||||
|
|
||||||
|
await fillOtp(page, await generateTotp(secret));
|
||||||
|
|
||||||
|
await expect(page.getByText(/Status:\s*Enabled/)).toBeVisible();
|
||||||
|
|
||||||
|
await page.context().clearCookies();
|
||||||
|
await gotoAndWaitForAppReady(page, "/login");
|
||||||
|
|
||||||
|
await page.getByRole("textbox", { name: "Username" }).fill(username);
|
||||||
|
await page.getByRole("textbox", { name: "Password" }).fill(workerPassword);
|
||||||
|
await page.getByRole("button", { name: "Login" }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole("heading", { name: "Two-Factor Authentication" })).toBeVisible();
|
||||||
|
|
||||||
|
await fillOtp(page, await generateTotp(secret));
|
||||||
|
|
||||||
|
await expect(page).toHaveURL("/volumes");
|
||||||
|
await expect(page.getByRole("button", { name: "Create Volume" })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
@ -101,6 +101,7 @@
|
||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@better-auth/utils": "0.4.0",
|
||||||
"@effect/language-service": "^0.86.1",
|
"@effect/language-service": "^0.86.1",
|
||||||
"@faker-js/faker": "^10.4.0",
|
"@faker-js/faker": "^10.4.0",
|
||||||
"@hey-api/openapi-ts": "^0.97.1",
|
"@hey-api/openapi-ts": "^0.97.1",
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@ export const sftpConfigSchema = z.object({
|
||||||
readOnly: z.boolean().optional(),
|
readOnly: z.boolean().optional(),
|
||||||
skipHostKeyCheck: z.boolean().default(false),
|
skipHostKeyCheck: z.boolean().default(false),
|
||||||
knownHosts: z.string().optional(),
|
knownHosts: z.string().optional(),
|
||||||
|
allowLegacySshRsa: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const volumeConfigSchema = z.discriminatedUnion("backend", [
|
export const volumeConfigSchema = z.discriminatedUnion("backend", [
|
||||||
|
|
|
||||||
|
|
@ -318,6 +318,7 @@ describe("buildEnv", () => {
|
||||||
path: "/backups",
|
path: "/backups",
|
||||||
privateKey: PLAIN_PRIVATE_KEY,
|
privateKey: PLAIN_PRIVATE_KEY,
|
||||||
skipHostKeyCheck: true as const,
|
skipHostKeyCheck: true as const,
|
||||||
|
allowLegacySshRsa: false as const,
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws for passphrase-protected private keys", async () => {
|
test("throws for passphrase-protected private keys", async () => {
|
||||||
|
|
@ -349,6 +350,20 @@ describe("buildEnv", () => {
|
||||||
expect(env._SFTP_SSH_ARGS).toContain("UserKnownHostsFile=/dev/null");
|
expect(env._SFTP_SSH_ARGS).toContain("UserKnownHostsFile=/dev/null");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("does not allow legacy RSA/SHA1 SSH algorithms by default", async () => {
|
||||||
|
const env = await buildEnvForTest(baseSftpConfig, "org-1");
|
||||||
|
|
||||||
|
expect(env._SFTP_SSH_ARGS).not.toContain("HostKeyAlgorithms=+ssh-rsa");
|
||||||
|
expect(env._SFTP_SSH_ARGS).not.toContain("PubkeyAcceptedAlgorithms=+ssh-rsa");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("allows legacy RSA/SHA1 SSH algorithms after modern defaults when enabled", async () => {
|
||||||
|
const env = await buildEnvForTest({ ...baseSftpConfig, allowLegacySshRsa: true }, "org-1");
|
||||||
|
|
||||||
|
expect(env._SFTP_SSH_ARGS).toContain("HostKeyAlgorithms=+ssh-rsa");
|
||||||
|
expect(env._SFTP_SSH_ARGS).toContain("PubkeyAcceptedAlgorithms=+ssh-rsa");
|
||||||
|
});
|
||||||
|
|
||||||
test("uses StrictHostKeyChecking=yes when knownHosts is absent", async () => {
|
test("uses StrictHostKeyChecking=yes when knownHosts is absent", async () => {
|
||||||
const env = await buildEnvForTest(
|
const env = await buildEnvForTest(
|
||||||
{ ...baseSftpConfig, skipHostKeyCheck: false, knownHosts: undefined },
|
{ ...baseSftpConfig, skipHostKeyCheck: false, knownHosts: undefined },
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,10 @@ export const buildEnv = async (
|
||||||
keyPath,
|
keyPath,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (config.allowLegacySshRsa) {
|
||||||
|
sshArgs.push("-o", "HostKeyAlgorithms=+ssh-rsa", "-o", "PubkeyAcceptedAlgorithms=+ssh-rsa");
|
||||||
|
}
|
||||||
|
|
||||||
if (config.skipHostKeyCheck) {
|
if (config.skipHostKeyCheck) {
|
||||||
sshArgs.push("-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null");
|
sshArgs.push("-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null");
|
||||||
} else if (config.knownHosts) {
|
} else if (config.knownHosts) {
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,7 @@ export const sftpRepositoryConfigSchema = z
|
||||||
privateKey: z.string().min(1),
|
privateKey: z.string().min(1),
|
||||||
skipHostKeyCheck: z.boolean().default(false),
|
skipHostKeyCheck: z.boolean().default(false),
|
||||||
knownHosts: z.string().optional(),
|
knownHosts: z.string().optional(),
|
||||||
|
allowLegacySshRsa: z.boolean().default(false),
|
||||||
})
|
})
|
||||||
.extend(baseRepositoryConfigSchema.shape);
|
.extend(baseRepositoryConfigSchema.shape);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue