Skip to content

Commit

Permalink
feat(sftp-server): public key login (#211)
Browse files Browse the repository at this point in the history
* feat(sftp-server): public key login

* chore
  • Loading branch information
KirCute authored Dec 25, 2024
1 parent 9dae6a0 commit 2ea71ea
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 1 deletion.
11 changes: 10 additions & 1 deletion src/lang/en/users.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
4 changes: 4 additions & 0 deletions src/pages/manage/users/AddOrEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -159,6 +160,9 @@ const AddOrEdit = () => {
>
{t(`global.${id ? "save" : "add"}`)}
</Button>
<Show when={id && !UserMethods.is_guest(user)}>
<PublicKeys isMine={false} userId={parseInt(id)} />
</Show>
</VStack>
</MaybeLoading>
)
Expand Down
2 changes: 2 additions & 0 deletions src/pages/manage/users/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -311,6 +312,7 @@ const Profile = () => {
)}
</For>
</HStack>
<PublicKeys isMine={true} userId={me().id} />
</VStack>
)
}
Expand Down
97 changes: 97 additions & 0 deletions src/pages/manage/users/PublicKey.tsx
Original file line number Diff line number Diff line change
@@ -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<SSHPublicKey[]> => r.post(`/me/sshkey/delete?id=${props.id}`),
)
: useFetch(
(): PResp<SSHPublicKey[]> =>
r.post(
`/admin/user/sshkey/delete?uid=${props.userId}&id=${props.id}`,
),
)
const textEllipsisCss = {
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}
return (
<Show when={!deleted()}>
<HStack w="$full" p="$2">
<Heading
w={cols[0].w}
size="sm"
textAlign={cols[0].textAlign}
css={textEllipsisCss}
>
{props.title}
</Heading>
<Text
w={cols[1].w}
size="sm"
textAlign={cols[1].textAlign}
css={textEllipsisCss}
>
{props.fingerprint}
</Text>
<Text
w={cols[2].w}
size="sm"
textAlign={cols[2].textAlign}
css={textEllipsisCss}
>
{formatDate(new Date(props.last_used_time))}
</Text>
<Flex w={cols[3].w} gap="$1">
<Spacer />
<Button
size="sm"
colorScheme="danger"
loading={delLoading()}
onClick={async () => {
const resp = await del()
handleResp(resp, () => {
notify.success(t("global.delete_success"))
setDeleted(true)
})
}}
>
{t(`global.delete`)}
</Button>
</Flex>
</HStack>
</Show>
)
}
154 changes: 154 additions & 0 deletions src/pages/manage/users/PublicKeys.tsx
Original file line number Diff line number Diff line change
@@ -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<SSHPublicKey[]>([])
const [loading, get] = props.isMine
? useFetch((): PPageResp<SSHPublicKey> => r.get(`/me/sshkey/list`))
: useFetch(
(): PPageResp<SSHPublicKey> =>
r.get(`/admin/user/sshkey/list?uid=${props.userId}`),
)
const [addReq, setAddReq] = createStore<SSHKeyAddReq>({
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 (
<VStack w="$full" alignItems="start" spacing="$2">
<Flex w="$full">
<Heading>{t(`users.ssh_keys.heading`)}</Heading>
<Show when={props.isMine}>
<Spacer />
<Button loading={loading()} onClick={onOpen}>
{t(`global.add`)}
</Button>
<Modal opened={isOpen()} onClose={onClose} scrollBehavior="inside">
<ModalOverlay />
<ModalContent>
<ModalCloseButton />
<ModalHeader>{t(`users.ssh_keys.add_heading`)}</ModalHeader>
<ModalBody>
<FormControl mb="$4">
<FormLabel for="add_title">
{t(`users.ssh_keys.title`)}
</FormLabel>
<Input
id="add_title"
value={addReq.title}
onInput={(e) => setAddReq("title", e.currentTarget.value)}
/>
</FormControl>
<FormControl>
<FormLabel for="add_key">{t(`users.ssh_keys.key`)}</FormLabel>
<Textarea
id="add_key"
value={addReq.key}
onInput={(e) => setAddReq("key", e.currentTarget.value)}
/>
</FormControl>
</ModalBody>
<ModalFooter>
<Button
loading={addLoading()}
onClick={async () => {
const resp = await add()
handleResp(resp, () => {
setAddReq("title", "")
setAddReq("key", "")
refresh()
onClose()
})
}}
>
{t(`global.add`)}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</Show>
</Flex>
<VStack
w="$full"
overflowX="auto"
shadow="$md"
rounded="$lg"
spacing="$1"
p="$1"
>
<HStack class="title" w="$full" p="$2">
<Text w={cols[0].w} {...itemProps(cols[0])}>
{t(`users.ssh_keys.${cols[0].name}`)}
</Text>
<Text w={cols[1].w} {...itemProps(cols[1])}>
{t(`users.ssh_keys.${cols[1].name}`)}
</Text>
<Text w={cols[2].w} {...itemProps(cols[2])}>
{t(`users.ssh_keys.${cols[2].name}`)}
</Text>
<Text w={cols[3].w} {...itemProps(cols[3])}>
{t(`users.ssh_keys.${cols[3].name}`)}
</Text>
</HStack>
{keys().map((key) => (
<PublicKey {...props} {...key} />
))}
</VStack>
</VStack>
)
}
7 changes: 7 additions & 0 deletions src/types/sshkey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface SSHPublicKey {
id: string
title: string
fingerprint: string
added_time: string
last_used_time: string
}

0 comments on commit 2ea71ea

Please sign in to comment.