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`)}
+
+
+
+
+
+
+
+
+
+
+
+
+ {t(`users.ssh_keys.${cols[0].name}`)}
+
+
+ {t(`users.ssh_keys.${cols[1].name}`)}
+
+
+ {t(`users.ssh_keys.${cols[2].name}`)}
+
+
+ {t(`users.ssh_keys.${cols[3].name}`)}
+
+
+ {keys().map((key) => (
+
+ ))}
+
+
+ )
+}
diff --git a/src/types/sshkey.ts b/src/types/sshkey.ts
new file mode 100644
index 0000000000..cd27805888
--- /dev/null
+++ b/src/types/sshkey.ts
@@ -0,0 +1,7 @@
+export interface SSHPublicKey {
+ id: string
+ title: string
+ fingerprint: string
+ added_time: string
+ last_used_time: string
+}