mirror of
https://github.com/khairul169/vaulterm.git
synced 2025-04-28 16:49:39 +07:00
feat: add keychains
This commit is contained in:
parent
b50abccae0
commit
0a788b05e5
@ -16,6 +16,7 @@ export default function Layout() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Drawer.Screen name="hosts" options={{ title: "Hosts" }} />
|
<Drawer.Screen name="hosts" options={{ title: "Hosts" }} />
|
||||||
|
<Drawer.Screen name="keychains" options={{ title: "Keychains" }} />
|
||||||
<Drawer.Screen
|
<Drawer.Screen
|
||||||
name="terminal"
|
name="terminal"
|
||||||
options={{ title: "Terminal", headerShown: media.sm }}
|
options={{ title: "Terminal", headerShown: media.sm }}
|
||||||
|
3
frontend/app/(drawer)/keychains.tsx
Normal file
3
frontend/app/(drawer)/keychains.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import KeychainsPage from "@/pages/keychains/page";
|
||||||
|
|
||||||
|
export default KeychainsPage;
|
@ -26,10 +26,17 @@ export default function RootLayout() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Providers>
|
<Providers>
|
||||||
<Stack>
|
<Stack
|
||||||
|
screenOptions={{
|
||||||
|
headerShadowVisible: false,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="index"
|
name="index"
|
||||||
options={{ headerShown: false, title: "Loading..." }}
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
title: "Loading...",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen name="(drawer)" options={{ headerShown: false }} />
|
<Stack.Screen name="(drawer)" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="+not-found" options={{ title: "Not Found" }} />
|
<Stack.Screen name="+not-found" options={{ title: "Not Found" }} />
|
||||||
|
@ -36,20 +36,16 @@ const Providers = ({ children }: Props) => {
|
|||||||
}, [theme, colorScheme]);
|
}, [theme, colorScheme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AuthProvider />
|
<AuthProvider />
|
||||||
<ThemeProvider value={navTheme}>
|
<ThemeProvider value={navTheme}>
|
||||||
<TamaguiProvider config={tamaguiConfig} defaultTheme={colorScheme}>
|
<TamaguiProvider config={tamaguiConfig} defaultTheme={colorScheme}>
|
||||||
<Theme name="blue">
|
<Theme name="blue">
|
||||||
<PortalProvider shouldAddRootHost>
|
<PortalProvider shouldAddRootHost>{children}</PortalProvider>
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
{children}
|
|
||||||
</QueryClientProvider>
|
|
||||||
</PortalProvider>
|
|
||||||
</Theme>
|
</Theme>
|
||||||
</TamaguiProvider>
|
</TamaguiProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -122,7 +122,6 @@ const XTermJs = forwardRef<XTermRef, XTermJsProps>((props, ref) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onOpen() {
|
function onOpen() {
|
||||||
console.log("WS Open");
|
|
||||||
resizeTerminal();
|
resizeTerminal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
53
frontend/components/ui/grid-view.tsx
Normal file
53
frontend/components/ui/grid-view.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { GetProps, ScrollView, View, ViewStyle } from "tamagui";
|
||||||
|
|
||||||
|
type GridItem = { key: string };
|
||||||
|
|
||||||
|
type GridViewProps<T extends GridItem> = GetProps<typeof ScrollView> & {
|
||||||
|
data?: T[] | null;
|
||||||
|
renderItem: (item: T, index: number) => React.ReactNode;
|
||||||
|
columns: {
|
||||||
|
xs?: number;
|
||||||
|
sm?: number;
|
||||||
|
md?: number;
|
||||||
|
lg?: number;
|
||||||
|
xl?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const GridView = <T extends GridItem>({
|
||||||
|
data,
|
||||||
|
renderItem,
|
||||||
|
columns,
|
||||||
|
gap,
|
||||||
|
...props
|
||||||
|
}: GridViewProps<T>) => {
|
||||||
|
const basisProps = useMemo(() => {
|
||||||
|
const basis: ViewStyle = { flexBasis: "100%" };
|
||||||
|
if (columns.xs) basis.flexBasis = `${100 / columns.xs}%`;
|
||||||
|
if (columns.sm) basis.$gtXs = { flexBasis: `${100 / columns.sm}%` };
|
||||||
|
if (columns.md) basis.$gtSm = { flexBasis: `${100 / columns.md}%` };
|
||||||
|
if (columns.lg) basis.$gtMd = { flexBasis: `${100 / columns.lg}%` };
|
||||||
|
if (columns.xl) basis.$gtLg = { flexBasis: `${100 / columns.xl}%` };
|
||||||
|
return basis;
|
||||||
|
}, [columns]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
{...props}
|
||||||
|
contentContainerStyle={{
|
||||||
|
flexDirection: "row",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
...(props.contentContainerStyle as object),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{data?.map((item, idx) => (
|
||||||
|
<View key={item.key} p={gap} flexShrink={0} {...basisProps}>
|
||||||
|
{renderItem(item, idx)}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GridView;
|
@ -1,6 +1,6 @@
|
|||||||
import { Controller, FieldValues } from "react-hook-form";
|
import { Controller, FieldValues } from "react-hook-form";
|
||||||
import { FormFieldBaseProps } from "./utility";
|
import { FormFieldBaseProps } from "./utility";
|
||||||
import { Input, View } from "tamagui";
|
import { Input, TextArea } from "tamagui";
|
||||||
import { ComponentPropsWithoutRef } from "react";
|
import { ComponentPropsWithoutRef } from "react";
|
||||||
import { ErrorMessage } from "./form";
|
import { ErrorMessage } from "./form";
|
||||||
|
|
||||||
@ -24,4 +24,24 @@ export const InputField = <T extends FieldValues>({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
type TextAreaFieldProps<T extends FieldValues> = FormFieldBaseProps<T> &
|
||||||
|
ComponentPropsWithoutRef<typeof TextArea>;
|
||||||
|
|
||||||
|
export const TextAreaField = <T extends FieldValues>({
|
||||||
|
form,
|
||||||
|
name,
|
||||||
|
...props
|
||||||
|
}: TextAreaFieldProps<T>) => (
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name={name}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<>
|
||||||
|
<TextArea {...field} {...props} />
|
||||||
|
<ErrorMessage error={fieldState.error} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
export default Input;
|
export default Input;
|
||||||
|
@ -22,23 +22,24 @@ const Modal = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange} modal>
|
<Dialog open={open} onOpenChange={onOpenChange} modal>
|
||||||
<Adapt when="sm">
|
<Adapt when="sm" platform="touch">
|
||||||
<Sheet
|
<Sheet
|
||||||
animation="quick"
|
animation="quick"
|
||||||
zIndex={999}
|
zIndex={999}
|
||||||
modal
|
modal
|
||||||
dismissOnSnapToBottom
|
dismissOnSnapToBottom
|
||||||
disableDrag
|
// disableDrag
|
||||||
>
|
>
|
||||||
<Sheet.Frame>
|
|
||||||
<Adapt.Contents />
|
|
||||||
</Sheet.Frame>
|
|
||||||
<Sheet.Overlay
|
<Sheet.Overlay
|
||||||
animation="quicker"
|
opacity={0.1}
|
||||||
|
animation="quick"
|
||||||
enterStyle={{ opacity: 0 }}
|
enterStyle={{ opacity: 0 }}
|
||||||
exitStyle={{ opacity: 0 }}
|
exitStyle={{ opacity: 0 }}
|
||||||
zIndex={0}
|
zIndex={0}
|
||||||
/>
|
/>
|
||||||
|
<Sheet.Frame>
|
||||||
|
<Adapt.Contents />
|
||||||
|
</Sheet.Frame>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
</Adapt>
|
</Adapt>
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ const StyledPressable = styled(Button, {
|
|||||||
unstyled: true,
|
unstyled: true,
|
||||||
backgroundColor: "$colorTransparent",
|
backgroundColor: "$colorTransparent",
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
|
padding: 0,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { forwardRef } from "react";
|
import React, { forwardRef } from "react";
|
||||||
import { Controller, FieldValues } from "react-hook-form";
|
import { Controller, FieldValues } from "react-hook-form";
|
||||||
import { Select as BaseSelect } from "tamagui";
|
import { Adapt, Select as BaseSelect, Sheet, Text } from "tamagui";
|
||||||
import { FormFieldBaseProps } from "./utility";
|
import { FormFieldBaseProps } from "./utility";
|
||||||
import { ErrorMessage } from "./form";
|
import { ErrorMessage } from "./form";
|
||||||
import Icons from "./icons";
|
import Icons from "./icons";
|
||||||
@ -42,6 +42,24 @@ const Select = forwardRef<SelectRef, SelectProps>(
|
|||||||
<BaseSelect.Value placeholder={placeholder} />
|
<BaseSelect.Value placeholder={placeholder} />
|
||||||
</BaseSelect.Trigger>
|
</BaseSelect.Trigger>
|
||||||
|
|
||||||
|
<Adapt when="sm" platform="touch">
|
||||||
|
<Sheet native modal dismissOnSnapToBottom snapPoints={[40, 60, 80]}>
|
||||||
|
<Sheet.Overlay
|
||||||
|
opacity={0.1}
|
||||||
|
animation="quick"
|
||||||
|
enterStyle={{ opacity: 0 }}
|
||||||
|
exitStyle={{ opacity: 0 }}
|
||||||
|
zIndex={0}
|
||||||
|
/>
|
||||||
|
{/* <Sheet.Handle /> */}
|
||||||
|
<Sheet.Frame>
|
||||||
|
<Sheet.ScrollView contentContainerStyle={{ py: "$3" }}>
|
||||||
|
<Adapt.Contents />
|
||||||
|
</Sheet.ScrollView>
|
||||||
|
</Sheet.Frame>
|
||||||
|
</Sheet>
|
||||||
|
</Adapt>
|
||||||
|
|
||||||
<BaseSelect.Content>
|
<BaseSelect.Content>
|
||||||
<BaseSelect.ScrollUpButton />
|
<BaseSelect.ScrollUpButton />
|
||||||
<BaseSelect.Viewport>
|
<BaseSelect.Viewport>
|
||||||
|
@ -1,14 +1,26 @@
|
|||||||
import Icons from "@/components/ui/icons";
|
import Icons from "@/components/ui/icons";
|
||||||
|
import { keyFormModal } from "@/pages/keychains/components/form";
|
||||||
|
import { initialValues as keychainInitialValues } from "@/pages/keychains/schema/form";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button, Label, XStack } from "tamagui";
|
import { Button, Label, XStack } from "tamagui";
|
||||||
|
|
||||||
export default function CredentialsSection() {
|
type Props = {
|
||||||
|
type?: "user" | "rsa" | "pve" | "cert";
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CredentialsSection({ type = "user" }: Props) {
|
||||||
return (
|
return (
|
||||||
<XStack gap="$3">
|
<XStack gap="$3">
|
||||||
<Label flex={1} h="$3">
|
<Label flex={1} h="$3">
|
||||||
Credentials
|
Credentials
|
||||||
</Label>
|
</Label>
|
||||||
<Button size="$3" icon={<Icons size={16} name="plus" />}>
|
<Button
|
||||||
|
size="$3"
|
||||||
|
icon={<Icons size={16} name="plus" />}
|
||||||
|
onPress={() =>
|
||||||
|
keyFormModal.onOpen({ ...keychainInitialValues, type } as never)
|
||||||
|
}
|
||||||
|
>
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
</XStack>
|
</XStack>
|
||||||
|
59
frontend/pages/hosts/components/host-item.tsx
Normal file
59
frontend/pages/hosts/components/host-item.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { View, Text, Button, Card, XStack } from "tamagui";
|
||||||
|
import React from "react";
|
||||||
|
import { MultiTapPressable } from "@/components/ui/pressable";
|
||||||
|
import Icons from "@/components/ui/icons";
|
||||||
|
import OSIcons from "@/components/ui/os-icons";
|
||||||
|
|
||||||
|
type HostItemProps = {
|
||||||
|
host: any;
|
||||||
|
onMultiTap: () => void;
|
||||||
|
onTap: () => void;
|
||||||
|
onEdit?: (() => void) | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const HostItem = ({ host, onMultiTap, onTap, onEdit }: HostItemProps) => {
|
||||||
|
return (
|
||||||
|
<MultiTapPressable
|
||||||
|
cursor="pointer"
|
||||||
|
group
|
||||||
|
numberOfTaps={2}
|
||||||
|
onMultiTap={onMultiTap}
|
||||||
|
onTap={onTap}
|
||||||
|
>
|
||||||
|
<Card bordered p="$4">
|
||||||
|
<XStack>
|
||||||
|
<OSIcons
|
||||||
|
name={host.os}
|
||||||
|
size={18}
|
||||||
|
mr="$2"
|
||||||
|
fallback="desktop-classic"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View flex={1}>
|
||||||
|
<Text>{host.label}</Text>
|
||||||
|
<Text fontSize="$3" mt="$2">
|
||||||
|
{host.host}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{onEdit != null && (
|
||||||
|
<Button
|
||||||
|
circular
|
||||||
|
display="none"
|
||||||
|
$sm={{ display: "block" }}
|
||||||
|
$group-hover={{ display: "block" }}
|
||||||
|
onPress={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onEdit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icons name="pencil" size={16} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</XStack>
|
||||||
|
</Card>
|
||||||
|
</MultiTapPressable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HostItem;
|
@ -1,14 +1,13 @@
|
|||||||
import { View, Text, Button, ScrollView, Spinner, Card, XStack } from "tamagui";
|
import { View, Text, Spinner } from "tamagui";
|
||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import api from "@/lib/api";
|
import api from "@/lib/api";
|
||||||
import { useNavigation } from "expo-router";
|
import { useNavigation } from "expo-router";
|
||||||
import { MultiTapPressable } from "@/components/ui/pressable";
|
|
||||||
import Icons from "@/components/ui/icons";
|
|
||||||
import SearchInput from "@/components/ui/search-input";
|
import SearchInput from "@/components/ui/search-input";
|
||||||
import { useTermSession } from "@/stores/terminal-sessions";
|
import { useTermSession } from "@/stores/terminal-sessions";
|
||||||
import { hostFormModal } from "./form";
|
import { hostFormModal } from "./form";
|
||||||
import OSIcons from "@/components/ui/os-icons";
|
import GridView from "@/components/ui/grid-view";
|
||||||
|
import HostItem from "./host-item";
|
||||||
|
|
||||||
type HostsListProps = {
|
type HostsListProps = {
|
||||||
allowEdit?: boolean;
|
allowEdit?: boolean;
|
||||||
@ -38,10 +37,10 @@ const HostsList = ({ allowEdit = true }: HostsListProps) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items.map((i: any) => ({ ...i, key: i.id }));
|
||||||
}, [hosts.data, search]);
|
}, [hosts.data, search]);
|
||||||
|
|
||||||
const onOpen = (host: any) => {
|
const onEdit = (host: any) => {
|
||||||
if (!allowEdit) return;
|
if (!allowEdit) return;
|
||||||
hostFormModal.onOpen(host);
|
hostFormModal.onOpen(host);
|
||||||
};
|
};
|
||||||
@ -80,67 +79,23 @@ const HostsList = ({ allowEdit = true }: HostsListProps) => {
|
|||||||
<Text mt="$4">Loading...</Text>
|
<Text mt="$4">Loading...</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<ScrollView
|
<GridView
|
||||||
contentContainerStyle={{
|
data={hostsList}
|
||||||
padding: "$3",
|
columns={{ sm: 2, lg: 3, xl: 4 }}
|
||||||
paddingTop: 0,
|
contentContainerStyle={{ p: "$2", pt: 0 }}
|
||||||
flexDirection: "row",
|
gap="$2.5"
|
||||||
flexWrap: "wrap",
|
renderItem={(host: any) => (
|
||||||
}}
|
<HostItem
|
||||||
>
|
host={host}
|
||||||
{hostsList?.map((host: any) => (
|
onTap={() => {}}
|
||||||
<MultiTapPressable
|
|
||||||
key={host.id}
|
|
||||||
flexBasis="100%"
|
|
||||||
cursor="pointer"
|
|
||||||
$gtXs={{ flexBasis: "50%" }}
|
|
||||||
$gtMd={{ flexBasis: "33.3%" }}
|
|
||||||
$gtLg={{ flexBasis: "25%" }}
|
|
||||||
$gtXl={{ flexBasis: "20%" }}
|
|
||||||
p="$2"
|
|
||||||
group
|
|
||||||
numberOfTaps={2}
|
|
||||||
onMultiTap={() => onOpenTerminal(host)}
|
onMultiTap={() => onOpenTerminal(host)}
|
||||||
onTap={() => onOpen(host)}
|
onEdit={allowEdit ? () => onEdit(host) : null}
|
||||||
>
|
|
||||||
<Card bordered p="$4">
|
|
||||||
<XStack>
|
|
||||||
<OSIcons
|
|
||||||
name={host.os}
|
|
||||||
size={18}
|
|
||||||
mr="$2"
|
|
||||||
fallback="desktop-classic"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View flex={1}>
|
|
||||||
<Text>{host.label}</Text>
|
|
||||||
<Text fontSize="$3" mt="$2">
|
|
||||||
{host.host}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{allowEdit && (
|
|
||||||
<Button
|
|
||||||
circular
|
|
||||||
display="none"
|
|
||||||
$sm={{ display: "block" }}
|
|
||||||
$group-hover={{ display: "block" }}
|
|
||||||
onPress={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onOpen(host);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icons name="pencil" size={16} />
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</XStack>
|
/>
|
||||||
</Card>
|
|
||||||
</MultiTapPressable>
|
|
||||||
))}
|
|
||||||
</ScrollView>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HostsList;
|
export default React.memo(HostsList);
|
||||||
|
@ -42,7 +42,7 @@ export const IncusFormFields = ({ form }: MiscFormFieldProps) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CredentialsSection />
|
<CredentialsSection type="cert" />
|
||||||
|
|
||||||
<FormField label="Client Certificate">
|
<FormField label="Client Certificate">
|
||||||
<SelectField
|
<SelectField
|
||||||
|
@ -31,7 +31,7 @@ export const PVEFormFields = ({ form }: MiscFormFieldProps) => {
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<CredentialsSection />
|
<CredentialsSection type="pve" />
|
||||||
|
|
||||||
<FormField label="Account">
|
<FormField label="Account">
|
||||||
<SelectField
|
<SelectField
|
||||||
|
@ -2,18 +2,10 @@ import { useMutation, useQuery } from "@tanstack/react-query";
|
|||||||
import { FormSchema } from "../schema/form";
|
import { FormSchema } from "../schema/form";
|
||||||
import api, { queryClient } from "@/lib/api";
|
import api, { queryClient } from "@/lib/api";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { useKeychains } from "@/pages/keychains/hooks/query";
|
||||||
export const useKeychains = () => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["keychains"],
|
|
||||||
queryFn: () => api("/keychains"),
|
|
||||||
select: (i) => i.rows,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useKeychainsOptions = () => {
|
export const useKeychainsOptions = () => {
|
||||||
const keys = useKeychains();
|
const keys = useKeychains();
|
||||||
|
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
const items: any[] = keys.data || [];
|
const items: any[] = keys.data || [];
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import HostsList from "./components/hosts-list";
|
|||||||
import HostForm, { hostFormModal } from "./components/form";
|
import HostForm, { hostFormModal } from "./components/form";
|
||||||
import Icons from "@/components/ui/icons";
|
import Icons from "@/components/ui/icons";
|
||||||
import { initialValues } from "./schema/form";
|
import { initialValues } from "./schema/form";
|
||||||
|
import KeyForm from "../keychains/components/form";
|
||||||
|
|
||||||
export default function HostsPage() {
|
export default function HostsPage() {
|
||||||
return (
|
return (
|
||||||
@ -26,6 +27,7 @@ export default function HostsPage() {
|
|||||||
|
|
||||||
<HostsList />
|
<HostsList />
|
||||||
<HostForm />
|
<HostForm />
|
||||||
|
<KeyForm />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
87
frontend/pages/keychains/components/form.tsx
Normal file
87
frontend/pages/keychains/components/form.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import Icons from "@/components/ui/icons";
|
||||||
|
import Modal from "@/components/ui/modal";
|
||||||
|
import { SelectField } from "@/components/ui/select";
|
||||||
|
import { useZForm } from "@/hooks/useZForm";
|
||||||
|
import { createDisclosure } from "@/lib/utils";
|
||||||
|
import React from "react";
|
||||||
|
import { ScrollView, Sheet, XStack } from "tamagui";
|
||||||
|
import { FormSchema, formSchema, typeOptions } from "../schema/form";
|
||||||
|
import { InputField } from "@/components/ui/input";
|
||||||
|
import FormField from "@/components/ui/form";
|
||||||
|
import { useSaveKeychain } from "../hooks/query";
|
||||||
|
import { ErrorAlert } from "@/components/ui/alert";
|
||||||
|
import Button from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
UserTypeInputFields,
|
||||||
|
PVETypeInputFields,
|
||||||
|
RSATypeInputFields,
|
||||||
|
CertTypeInputFields,
|
||||||
|
} from "./input-fields";
|
||||||
|
|
||||||
|
export const keyFormModal = createDisclosure<FormSchema>();
|
||||||
|
|
||||||
|
const KeyForm = () => {
|
||||||
|
const { data } = keyFormModal.use();
|
||||||
|
const form = useZForm(formSchema, data);
|
||||||
|
const isEditing = data?.id != null;
|
||||||
|
const type = form.watch("type");
|
||||||
|
|
||||||
|
const saveMutation = useSaveKeychain();
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit((values) => {
|
||||||
|
saveMutation.mutate(values, {
|
||||||
|
onSuccess: () => {
|
||||||
|
keyFormModal.onClose();
|
||||||
|
form.reset();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
disclosure={keyFormModal}
|
||||||
|
title="Keychain"
|
||||||
|
description={`${isEditing ? "Edit" : "Add new"} key.`}
|
||||||
|
>
|
||||||
|
<ErrorAlert mx="$4" mb="$4" error={saveMutation.error} />
|
||||||
|
|
||||||
|
<Sheet.ScrollView
|
||||||
|
contentContainerStyle={{ padding: "$4", pt: 0, gap: "$4" }}
|
||||||
|
>
|
||||||
|
<FormField label="Label">
|
||||||
|
<InputField f={1} form={form} name="label" placeholder="Label..." />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Type">
|
||||||
|
<SelectField form={form} name="type" items={typeOptions} />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
{type === "user" ? (
|
||||||
|
<UserTypeInputFields form={form} />
|
||||||
|
) : type === "pve" ? (
|
||||||
|
<PVETypeInputFields form={form} />
|
||||||
|
) : type === "rsa" ? (
|
||||||
|
<RSATypeInputFields form={form} />
|
||||||
|
) : type === "cert" ? (
|
||||||
|
<CertTypeInputFields form={form} />
|
||||||
|
) : null}
|
||||||
|
</Sheet.ScrollView>
|
||||||
|
|
||||||
|
<XStack p="$4" gap="$4">
|
||||||
|
<Button flex={1} onPress={keyFormModal.onClose} bg="$colorTransparent">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
flex={1}
|
||||||
|
icon={<Icons name="content-save" size={18} />}
|
||||||
|
onPress={onSubmit}
|
||||||
|
isLoading={saveMutation.isPending}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</XStack>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KeyForm;
|
68
frontend/pages/keychains/components/input-fields.tsx
Normal file
68
frontend/pages/keychains/components/input-fields.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { UseFormReturn } from "react-hook-form";
|
||||||
|
import { FormSchema, pveRealms } from "../schema/form";
|
||||||
|
import FormField from "@/components/ui/form";
|
||||||
|
import { InputField, TextAreaField } from "@/components/ui/input";
|
||||||
|
import { SelectField } from "@/components/ui/select";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
form: UseFormReturn<FormSchema>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserTypeInputFields = ({ form }: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField label="Username">
|
||||||
|
<InputField f={1} form={form} name="data.username" />
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Password">
|
||||||
|
<InputField f={1} form={form} name="data.password" />
|
||||||
|
</FormField>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PVETypeInputFields = ({ form }: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField label="Username">
|
||||||
|
<InputField f={1} form={form} name="data.username" />
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Realm">
|
||||||
|
<SelectField form={form} name="data.realm" items={pveRealms} />
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Password">
|
||||||
|
<InputField f={1} form={form} name="data.password" />
|
||||||
|
</FormField>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RSATypeInputFields = ({ form }: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* <FormField label="Public Key">
|
||||||
|
<TextAreaField rows={7} f={1} form={form} name="data.public" />
|
||||||
|
</FormField> */}
|
||||||
|
<FormField label="Private Key">
|
||||||
|
<TextAreaField rows={7} f={1} form={form} name="data.private" />
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Passphrase">
|
||||||
|
<InputField f={1} form={form} name="data.passphrase" />
|
||||||
|
</FormField>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CertTypeInputFields = ({ form }: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField label="Client Certificate">
|
||||||
|
<TextAreaField rows={7} f={1} form={form} name="data.cert" />
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Client Key">
|
||||||
|
<TextAreaField rows={7} f={1} form={form} name="data.key" />
|
||||||
|
</FormField>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
56
frontend/pages/keychains/components/key-item.tsx
Normal file
56
frontend/pages/keychains/components/key-item.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { View, Text, Button, Card, XStack } from "tamagui";
|
||||||
|
import React from "react";
|
||||||
|
import Pressable from "@/components/ui/pressable";
|
||||||
|
import Icons from "@/components/ui/icons";
|
||||||
|
|
||||||
|
type KeyItemProps = {
|
||||||
|
data: any;
|
||||||
|
onPress?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const icons: Record<string, string> = {
|
||||||
|
user: "account",
|
||||||
|
pve: "account-key",
|
||||||
|
rsa: "key",
|
||||||
|
cert: "certificate",
|
||||||
|
};
|
||||||
|
|
||||||
|
const KeyItem = ({ data, onPress }: KeyItemProps) => {
|
||||||
|
return (
|
||||||
|
<Pressable group onPress={onPress}>
|
||||||
|
<Card bordered px="$4" py="$3">
|
||||||
|
<XStack alignItems="center">
|
||||||
|
<Icons
|
||||||
|
name={(icons[data.type] || "key") as never}
|
||||||
|
size={20}
|
||||||
|
mr="$3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View flex={1}>
|
||||||
|
<Text textAlign="left">{data.label}</Text>
|
||||||
|
<Text textAlign="left" fontSize="$3" mt="$1">
|
||||||
|
{data.type}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
circular
|
||||||
|
opacity={0}
|
||||||
|
$sm={{ opacity: 1 }}
|
||||||
|
animation="quickest"
|
||||||
|
animateOnly={["opacity"]}
|
||||||
|
$group-hover={{ opacity: 1 }}
|
||||||
|
onPress={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onPress?.();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icons name="pencil" size={16} />
|
||||||
|
</Button>
|
||||||
|
</XStack>
|
||||||
|
</Card>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KeyItem;
|
60
frontend/pages/keychains/components/key-list.tsx
Normal file
60
frontend/pages/keychains/components/key-list.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { View, Text, Spinner } from "tamagui";
|
||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
import SearchInput from "@/components/ui/search-input";
|
||||||
|
import GridView from "@/components/ui/grid-view";
|
||||||
|
import { useKeychains } from "../hooks/query";
|
||||||
|
import KeyItem from "./key-item";
|
||||||
|
import { keyFormModal } from "./form";
|
||||||
|
|
||||||
|
const KeyList = () => {
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const keys = useKeychains({ withData: true });
|
||||||
|
|
||||||
|
const keyList = useMemo(() => {
|
||||||
|
let items = keys.data || [];
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
items = items.filter((item: any) => {
|
||||||
|
const q = search.toLowerCase();
|
||||||
|
return item.label.toLowerCase().includes(q);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.map((i: any) => ({ ...i, key: i.id }));
|
||||||
|
}, [keys.data, search]);
|
||||||
|
|
||||||
|
const onEdit = (item: any) => {
|
||||||
|
keyFormModal.onOpen(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View p="$4" pb="$3">
|
||||||
|
<SearchInput
|
||||||
|
placeholder="Search key..."
|
||||||
|
value={search}
|
||||||
|
onChangeText={setSearch}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{keys.isLoading ? (
|
||||||
|
<View alignItems="center" justifyContent="center" flex={1}>
|
||||||
|
<Spinner size="large" />
|
||||||
|
<Text mt="$4">Loading...</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<GridView
|
||||||
|
data={keyList}
|
||||||
|
columns={{ sm: 2, lg: 3, xl: 4 }}
|
||||||
|
contentContainerStyle={{ p: "$2", pt: 0 }}
|
||||||
|
gap="$2.5"
|
||||||
|
renderItem={(item: any) => (
|
||||||
|
<KeyItem data={item} onPress={() => onEdit(item)} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(KeyList);
|
25
frontend/pages/keychains/hooks/query.ts
Normal file
25
frontend/pages/keychains/hooks/query.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import api, { queryClient } from "@/lib/api";
|
||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
import { FormSchema } from "../schema/form";
|
||||||
|
|
||||||
|
export const useKeychains = (query?: any) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["keychains", query],
|
||||||
|
queryFn: () => api("/keychains", { query }),
|
||||||
|
select: (i) => i.rows,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSaveKeychain = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (body: FormSchema) => {
|
||||||
|
return body.id
|
||||||
|
? api(`/keychains/${body.id}`, { method: "PUT", body })
|
||||||
|
: api(`/keychains`, { method: "POST", body });
|
||||||
|
},
|
||||||
|
onError: (e) => console.error(e),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["keychains"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
31
frontend/pages/keychains/page.tsx
Normal file
31
frontend/pages/keychains/page.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React from "react";
|
||||||
|
import KeyList from "./components/key-list";
|
||||||
|
import KeyForm, { keyFormModal } from "./components/form";
|
||||||
|
import Drawer from "expo-router/drawer";
|
||||||
|
import { Button } from "tamagui";
|
||||||
|
import Icons from "@/components/ui/icons";
|
||||||
|
import { initialValues } from "./schema/form";
|
||||||
|
|
||||||
|
export default function KeychainsPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Drawer.Screen
|
||||||
|
options={{
|
||||||
|
headerRight: () => (
|
||||||
|
<Button
|
||||||
|
bg="$colorTransparent"
|
||||||
|
icon={<Icons name="plus" size={24} />}
|
||||||
|
onPress={() => keyFormModal.onOpen(initialValues)}
|
||||||
|
$gtSm={{ mr: "$3" }}
|
||||||
|
>
|
||||||
|
New
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<KeyList />
|
||||||
|
<KeyForm />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
79
frontend/pages/keychains/schema/form.ts
Normal file
79
frontend/pages/keychains/schema/form.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { SelectItem } from "@/components/ui/select";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const baseSchema = z.object({
|
||||||
|
id: z.string().ulid().nullish(),
|
||||||
|
label: z.string().min(1, { message: "Label is required" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const userTypeSchema = baseSchema.merge(
|
||||||
|
z.object({
|
||||||
|
type: z.literal("user"),
|
||||||
|
data: z.object({
|
||||||
|
username: z.string().min(1, { message: "Username is required" }),
|
||||||
|
password: z.string().nullish(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const pveTypeSchema = baseSchema.merge(
|
||||||
|
z.object({
|
||||||
|
type: z.literal("pve"),
|
||||||
|
data: z.object({
|
||||||
|
username: z.string().min(1, { message: "Username is required" }),
|
||||||
|
realm: z.enum(["pam", "pve"]),
|
||||||
|
password: z.string().min(1, { message: "Password is required" }),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const rsaTypeSchema = baseSchema.merge(
|
||||||
|
z.object({
|
||||||
|
type: z.literal("rsa"),
|
||||||
|
data: z.object({
|
||||||
|
public: z.string().nullish(),
|
||||||
|
private: z.string().min(1, { message: "Private Key is required" }),
|
||||||
|
passphrase: z.string().nullish(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const certTypeSchema = baseSchema.merge(
|
||||||
|
z.object({
|
||||||
|
type: z.literal("cert"),
|
||||||
|
data: z.object({
|
||||||
|
cert: z.string().min(1, { message: "Certificate is required" }),
|
||||||
|
key: z.string().min(1, { message: "Key is required" }),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const formSchema = z.discriminatedUnion("type", [
|
||||||
|
userTypeSchema,
|
||||||
|
pveTypeSchema,
|
||||||
|
rsaTypeSchema,
|
||||||
|
certTypeSchema,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type FormSchema = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const initialValues: FormSchema = {
|
||||||
|
type: "user",
|
||||||
|
label: "",
|
||||||
|
data: {
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const typeOptions: SelectItem[] = [
|
||||||
|
{ label: "User Key", value: "user" },
|
||||||
|
{ label: "ProxmoxVE Key", value: "pve" },
|
||||||
|
{ label: "RSA Key", value: "rsa" },
|
||||||
|
{ label: "Client Certificate", value: "cert" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const pveRealms: SelectItem[] = [
|
||||||
|
{ label: "Linux PAM", value: "pam" },
|
||||||
|
{ label: "Proxmox VE", value: "pve" },
|
||||||
|
];
|
@ -49,6 +49,6 @@ func (r *Hosts) Create(item *models.Host) error {
|
|||||||
return r.db.Create(item).Error
|
return r.db.Create(item).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Hosts) Update(item *models.Host) error {
|
func (r *Hosts) Update(id string, item *models.Host) error {
|
||||||
return r.db.Save(item).Error
|
return r.db.Where("id = ?", id).Updates(item).Error
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,7 @@ func update(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
item.OS = osName
|
item.OS = osName
|
||||||
|
|
||||||
if err := repo.Update(item); err != nil {
|
if err := repo.Update(id, item); err != nil {
|
||||||
return utils.ResponseError(c, err, 500)
|
return utils.ResponseError(c, err, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,6 +32,12 @@ func (r *Keychains) Get(id string) (*models.Keychain, error) {
|
|||||||
return &keychain, nil
|
return &keychain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Keychains) Exists(id string) (bool, error) {
|
||||||
|
var count int64
|
||||||
|
ret := r.db.Model(&models.Keychain{}).Where("id = ?", id).Count(&count)
|
||||||
|
return count > 0, ret.Error
|
||||||
|
}
|
||||||
|
|
||||||
type KeychainDecrypted struct {
|
type KeychainDecrypted struct {
|
||||||
models.Keychain
|
models.Keychain
|
||||||
Data map[string]interface{}
|
Data map[string]interface{}
|
||||||
@ -50,3 +56,7 @@ func (r *Keychains) GetDecrypted(id string) (*KeychainDecrypted, error) {
|
|||||||
|
|
||||||
return &KeychainDecrypted{Keychain: *keychain, Data: data}, nil
|
return &KeychainDecrypted{Keychain: *keychain, Data: data}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Keychains) Update(id string, item *models.Keychain) error {
|
||||||
|
return r.db.Where("id = ?", id).Updates(item).Error
|
||||||
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package keychains
|
package keychains
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@ -13,18 +14,46 @@ func Router(app *fiber.App) {
|
|||||||
|
|
||||||
router.Get("/", getAll)
|
router.Get("/", getAll)
|
||||||
router.Post("/", create)
|
router.Post("/", create)
|
||||||
|
router.Put("/:id", update)
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetAllResult struct {
|
||||||
|
*models.Keychain
|
||||||
|
Data map[string]interface{} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAll(c *fiber.Ctx) error {
|
func getAll(c *fiber.Ctx) error {
|
||||||
|
withData := c.Query("withData")
|
||||||
|
|
||||||
repo := NewRepository()
|
repo := NewRepository()
|
||||||
rows, err := repo.GetAll()
|
rows, err := repo.GetAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return utils.ResponseError(c, err, 500)
|
return utils.ResponseError(c, err, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(fiber.Map{
|
if withData != "true" {
|
||||||
"rows": rows,
|
return c.JSON(fiber.Map{"rows": rows})
|
||||||
})
|
}
|
||||||
|
|
||||||
|
res := make([]*GetAllResult, len(rows))
|
||||||
|
doneCh := make(chan struct{})
|
||||||
|
|
||||||
|
// Decrypt data
|
||||||
|
for i, item := range rows {
|
||||||
|
go func(i int, item *models.Keychain) {
|
||||||
|
var data map[string]interface{}
|
||||||
|
item.DecryptData(&data)
|
||||||
|
|
||||||
|
res[i] = &GetAllResult{item, data}
|
||||||
|
doneCh <- struct{}{}
|
||||||
|
}(i, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
for range rows {
|
||||||
|
<-doneCh
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{"rows": res})
|
||||||
}
|
}
|
||||||
|
|
||||||
func create(c *fiber.Ctx) error {
|
func create(c *fiber.Ctx) error {
|
||||||
@ -50,3 +79,33 @@ func create(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
return c.Status(http.StatusCreated).JSON(item)
|
return c.Status(http.StatusCreated).JSON(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func update(c *fiber.Ctx) error {
|
||||||
|
var body CreateKeychainSchema
|
||||||
|
if err := c.BodyParser(&body); err != nil {
|
||||||
|
return utils.ResponseError(c, err, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := NewRepository()
|
||||||
|
id := c.Params("id")
|
||||||
|
|
||||||
|
exist, _ := repo.Exists(id)
|
||||||
|
if !exist {
|
||||||
|
return utils.ResponseError(c, fmt.Errorf("key %s not found", id), 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
item := &models.Keychain{
|
||||||
|
Type: body.Type,
|
||||||
|
Label: body.Label,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := item.EncryptData(body.Data); err != nil {
|
||||||
|
return utils.ResponseError(c, err, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := repo.Update(id, item); err != nil {
|
||||||
|
return utils.ResponseError(c, err, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(item)
|
||||||
|
}
|
||||||
|
@ -50,12 +50,14 @@ func sshHandler(c *websocket.Conn, data *models.HostDecrypted) {
|
|||||||
func pveHandler(c *websocket.Conn, data *models.HostDecrypted) {
|
func pveHandler(c *websocket.Conn, data *models.HostDecrypted) {
|
||||||
client := c.Query("client")
|
client := c.Query("client")
|
||||||
username, _ := data.Key["username"].(string)
|
username, _ := data.Key["username"].(string)
|
||||||
|
realm, _ := data.Key["realm"].(string)
|
||||||
password, _ := data.Key["password"].(string)
|
password, _ := data.Key["password"].(string)
|
||||||
|
|
||||||
pve := &lib.PVEServer{
|
pve := &lib.PVEServer{
|
||||||
HostName: data.Host.Host,
|
HostName: data.Host.Host,
|
||||||
Port: data.Port,
|
Port: data.Port,
|
||||||
Username: username,
|
Username: username,
|
||||||
|
Realm: realm,
|
||||||
Password: password,
|
Password: password,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ type PVEServer struct {
|
|||||||
HostName string
|
HostName string
|
||||||
Port int
|
Port int
|
||||||
Username string
|
Username string
|
||||||
|
Realm string
|
||||||
Password string
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,7 +73,7 @@ func (pve *PVEServer) GetAccessTicket() (*PVEAccessTicket, error) {
|
|||||||
|
|
||||||
// note for myself: don't forget the realm
|
// note for myself: don't forget the realm
|
||||||
body, err := fetch("POST", url, &PVERequestInit{Body: map[string]string{
|
body, err := fetch("POST", url, &PVERequestInit{Body: map[string]string{
|
||||||
"username": pve.Username,
|
"username": fmt.Sprintf("%s@%s", pve.Username, pve.Realm),
|
||||||
"password": pve.Password,
|
"password": pve.Password,
|
||||||
}})
|
}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user