diff --git a/src/lang/en/users.json b/src/lang/en/users.json index ce3a93b8d1..ed476692c7 100644 --- a/src/lang/en/users.json +++ b/src/lang/en/users.json @@ -44,5 +44,14 @@ "webauthn": "WebAuthn", "add_webauthn": "Add a Webauthn credential", "add_webauthn_success": "Webauthn credential successfully added!", - "webauthn_not_supported": "Webauthn is not supported in your browser or you are in an unsafe origin" + "webauthn_not_supported": "Webauthn is not supported in your browser or you are in an unsafe origin", + "ssh_keys": { + "heading": "SSH keys", + "add_heading": "Add new SSH key", + "title": "Title", + "key": "Key", + "fingerprint": "Fingerprint", + "last_used": "Last used time", + "operation": "Operation" + } } diff --git a/src/pages/manage/users/AddOrEdit.tsx b/src/pages/manage/users/AddOrEdit.tsx index 8af47bf60a..767e63fa25 100644 --- a/src/pages/manage/users/AddOrEdit.tsx +++ b/src/pages/manage/users/AddOrEdit.tsx @@ -15,6 +15,7 @@ import { PEmptyResp, PResp, User, UserMethods, UserPermissions } from "~/types" import { createStore } from "solid-js/store" import { For, Show } from "solid-js" import { me, setMe } from "~/store" +import { PublicKeys } from "./PublicKeys" const Permission = (props: { can: boolean @@ -159,6 +160,9 @@ const AddOrEdit = () => { > {t(`global.${id ? "save" : "add"}`)} + + + ) diff --git a/src/pages/manage/users/Profile.tsx b/src/pages/manage/users/Profile.tsx index c110f136c3..50a0150cde 100644 --- a/src/pages/manage/users/Profile.tsx +++ b/src/pages/manage/users/Profile.tsx @@ -29,6 +29,7 @@ import { supported, CredentialCreationOptionsJSON, } from "@github/webauthn-json/browser-ponyfill" +import { PublicKeys } from "./PublicKeys" const PermissionBadge = (props: { can: boolean; children: JSXElement }) => { return ( @@ -311,6 +312,7 @@ const Profile = () => { )} + ) } diff --git a/src/pages/manage/users/PublicKey.tsx b/src/pages/manage/users/PublicKey.tsx new file mode 100644 index 0000000000..8942336e80 --- /dev/null +++ b/src/pages/manage/users/PublicKey.tsx @@ -0,0 +1,97 @@ +import { PublicKeysProps } from "./PublicKeys" +import { SSHPublicKey } from "~/types/sshkey" +import { useFetch, useT } from "~/hooks" +import { createSignal, Show } from "solid-js" +import { Button, Flex, Heading, HStack, Spacer, Text } from "@hope-ui/solid" +import { PResp } from "~/types" +import { handleResp, notify, r } from "~/utils" + +const formatDate = (date: Date) => { + const year = date.getFullYear().toString() + const month = (date.getMonth() + 1).toString().padStart(2, "0") + const day = date.getDate().toString().padStart(2, "0") + const hours = date.getHours().toString().padStart(2, "0") + const minutes = date.getMinutes().toString().padStart(2, "0") + const seconds = date.getSeconds().toString().padStart(2, "0") + return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}` +} + +export interface PublicKeyCol { + name: "title" | "fingerprint" | "last_used" | "operation" + textAlign: "left" | "right" | "center" + w: any +} + +export const cols: PublicKeyCol[] = [ + { name: "title", textAlign: "left", w: "calc(35% - 110px)" }, + { name: "fingerprint", textAlign: "left", w: "calc(65% - 110px)" }, + { name: "last_used", textAlign: "right", w: "140px" }, + { name: "operation", textAlign: "right", w: "80px" }, +] + +export const PublicKey = (props: PublicKeysProps & SSHPublicKey) => { + const t = useT() + const [deleted, setDeleted] = createSignal(false) + const [delLoading, del] = props.isMine + ? useFetch( + (): PResp => r.post(`/me/sshkey/delete?id=${props.id}`), + ) + : useFetch( + (): PResp => + r.post( + `/admin/user/sshkey/delete?uid=${props.userId}&id=${props.id}`, + ), + ) + const textEllipsisCss = { + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + } + return ( + + + + {props.title} + + + {props.fingerprint} + + + {formatDate(new Date(props.last_used_time))} + + + + + + + + ) +} diff --git a/src/pages/manage/users/PublicKeys.tsx b/src/pages/manage/users/PublicKeys.tsx new file mode 100644 index 0000000000..c76175597a --- /dev/null +++ b/src/pages/manage/users/PublicKeys.tsx @@ -0,0 +1,154 @@ +import { + Button, + createDisclosure, + Flex, + FormControl, + FormLabel, + Heading, + HStack, + Input, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Spacer, + Text, + Textarea, + VStack, +} from "@hope-ui/solid" +import { createSignal, Show } from "solid-js" +import { useFetch, useT } from "~/hooks" +import { SSHPublicKey } from "~/types/sshkey" +import { PEmptyResp, PPageResp } from "~/types" +import { handleResp, r } from "~/utils" +import { cols, PublicKey, PublicKeyCol } from "./PublicKey" +import { createStore } from "solid-js/store" + +export interface PublicKeysProps { + isMine: boolean + userId: number +} + +export interface SSHKeyAddReq { + title: string + key: string +} + +export const PublicKeys = (props: PublicKeysProps) => { + const t = useT() + const [keys, setKeys] = createSignal([]) + const [loading, get] = props.isMine + ? useFetch((): PPageResp => r.get(`/me/sshkey/list`)) + : useFetch( + (): PPageResp => + r.get(`/admin/user/sshkey/list?uid=${props.userId}`), + ) + const [addReq, setAddReq] = createStore({ + title: "", + key: "", + }) + const [addLoading, add] = useFetch( + (): PEmptyResp => r.post(`/me/sshkey/add`, addReq), + ) + const { isOpen, onOpen, onClose } = createDisclosure() + const refresh = async () => { + const resp = await get() + handleResp(resp, (data) => { + setKeys(data.content) + }) + } + refresh() + const itemProps = (col: PublicKeyCol) => { + return { + fontWeight: "bold", + fontSize: "$sm", + color: "$neutral11", + textAlign: col.textAlign as any, + } + } + return ( + + + {t(`users.ssh_keys.heading`)} + + + + + + + + {t(`users.ssh_keys.add_heading`)} + + + + {t(`users.ssh_keys.title`)} + + setAddReq("title", e.currentTarget.value)} + /> + + + {t(`users.ssh_keys.key`)} +