diff --git a/moon/apps/web/components/Setting/PersonalToken.tsx b/moon/apps/web/components/Setting/PersonalToken.tsx
new file mode 100644
index 000000000..a5003e529
--- /dev/null
+++ b/moon/apps/web/components/Setting/PersonalToken.tsx
@@ -0,0 +1,146 @@
+import React, {useState} from 'react'
+import {LoadingSpinner, LockIcon, Button, PlusIcon} from '@gitmono/ui'
+import {useGetTokenList} from '@/hooks/useGetTokenList'
+import {usePostTokenGenerate} from '@/hooks/usePostTokenGenerate'
+import {useDeleteTokenById} from '@/hooks/useDeleteTokenById'
+import {useQueryClient} from '@tanstack/react-query'
+import {legacyApiClient} from '@/utils/queryClient'
+import {ListToken} from '@gitmono/types'
+import toast from "react-hot-toast";
+import HandleTime from "@/components/MrView/components/HandleTime";
+
+const TokenItem = ({item}: { item: ListToken }) => {
+ const {mutate: deleteToken} = useDeleteTokenById()
+ const queryClient = useQueryClient()
+ const fetchTokenList = legacyApiClient.v1.getApiUserTokenList()
+
+ return (
+
+
+
+
+
Token #{item.id}
+
{item.token}
+
+
+
+
+
+
+
+ )
+}
+
+const PersonalToken = () => {
+ const {tokenList, isLoading} = useGetTokenList()
+ const {mutate: generateToken, isPending: isGenerating} = usePostTokenGenerate()
+ const queryClient = useQueryClient()
+ const fetchTokenList = legacyApiClient.v1.getApiUserTokenList()
+
+ const [generated, setGenerated] = useState(null)
+ const [copied, setCopied] = useState(false)
+
+ const handleGenerate = () => {
+ generateToken(undefined, {
+ onSuccess: (result) => {
+ setGenerated(result?.data ?? null)
+ queryClient.invalidateQueries({queryKey: fetchTokenList.requestKey()})
+ }
+ })
+ }
+
+ const handleCopy = async () => {
+ if (!generated) return
+ if (navigator.clipboard) {
+ await navigator.clipboard
+ .writeText(generated)
+ .then(() => toast.success("Copied to clipboard"))
+ .catch(() => toast.error("Copied failed"))
+ } else {
+ const textArea = document
+ .createElement('textarea')
+
+ textArea.value = generated
+ document.body.appendChild(textArea)
+ textArea.select()
+ try {
+ document.execCommand('copy')
+ toast.success('Copied to clipboard')
+ document.body.removeChild(textArea)
+ } catch {
+ toast.error("Copied failed")
+ }
+ }
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ }
+
+ return (
+
+
+ Personal tokens
+
+
+
+
+ This is a list of personal access tokens associated with your account. Remove any tokens that you do not
+ recognize.
+
+
+ {generated && (
+
+
Your new token has been generated.
+
+ {generated}
+
+
+
Make sure to copy your new token now. You won’t be able to see it
+ again.
+
+ )}
+
+
+ Tokens
+ {isLoading ? (
+
+
+
+ ) : (
+
+ {tokenList.map((item) => (
+
+ ))}
+
+ )}
+
+
+ )
+}
+
+export default PersonalToken
diff --git a/moon/apps/web/components/Setting/SSHKeys.tsx b/moon/apps/web/components/Setting/SSHKeys.tsx
new file mode 100644
index 000000000..48dbe649f
--- /dev/null
+++ b/moon/apps/web/components/Setting/SSHKeys.tsx
@@ -0,0 +1,184 @@
+import React, {useState} from 'react';
+import {LoadingSpinner, LockIcon, Button, TextField, PlusIcon} from '@gitmono/ui'
+import * as Dialog from '@gitmono/ui/src/Dialog'
+import {ListSSHKey} from "@gitmono/types";
+import {useGetSSHList} from '@/hooks/useGetSSHList'
+import {usePostSSHKey} from '@/hooks/usePostSSHKey'
+import {useDeleteSSHKeyById} from '@/hooks/useDeleteSSHKeyById'
+import {legacyApiClient} from "@/utils/queryClient";
+import {useQueryClient} from "@tanstack/react-query";
+import HandleTime from "@/components/MrView/components/HandleTime";
+
+const SshKeyItem = ({keyData}: { keyData: ListSSHKey }) => {
+ const {mutate: deleteSSHKey} = useDeleteSSHKeyById()
+ const fetchSSHList = legacyApiClient.v1.getApiUserSshList()
+ const queryClient = useQueryClient()
+
+ return (
+
+
+
+
+
{keyData.title}
+
{keyData.finger}
+
+
+
+
+
+
+
+ )
+}
+
+interface NewSSHKeyDialogProps {
+ open: boolean;
+ setOpen: (open: boolean) => void;
+}
+
+const NewSSHKeyDialog = ({open, setOpen}: NewSSHKeyDialogProps) => {
+ const {mutate: postSSHKey, isPending} = usePostSSHKey()
+ const [title, setTitle] = useState('')
+ const [sshKey, setSshKey] = useState('')
+ const [errors, setErrors] = useState<{ title?: string; sshKey?: string }>({})
+
+ const fetchSSHList = legacyApiClient.v1.getApiUserSshList()
+ const queryClient = useQueryClient()
+
+ const handleSubmit = (e?: React.FormEvent | React.MouseEvent) => {
+ if (e) e.preventDefault()
+ const nextErrors: { title?: string; sshKey?: string } = {}
+
+ if (!title.trim()) nextErrors.title = 'Title is required'
+ if (!sshKey.trim()) nextErrors.sshKey = 'SSH key is required'
+ setErrors(nextErrors)
+ if (Object.keys(nextErrors).length > 0) return
+ postSSHKey(
+ {data: {title: title.trim(), ssh_key: sshKey}},
+ {
+ onSuccess: () => {
+ setOpen(false)
+ setTitle('')
+ setSshKey('')
+ setErrors({})
+
+ queryClient.invalidateQueries({queryKey: fetchSSHList.requestKey()})
+ }
+ }
+ )
+ }
+
+ return (
+
+
+ Add SSH key
+
+
+
+
+ {errors.title && {errors.title}}
+
+
+
+
+ {errors.sshKey && {errors.sshKey}}
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+const SSHKeys = () => {
+ const {sshKeys, isLoading} = useGetSSHList()
+ const [open, setOpen] = useState(false)
+
+ return (
+ <>
+
+
+ SSH keys
+
+
+
+
+ This is a list of SSH keys associated with your account. Remove any keys that you do not recognize.
+
+
+
+
+ Authentication keys
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+ {sshKeys?.map((key) => (
+
+ ))}
+
+ )}
+
+
+
+ >
+ );
+};
+
+export default SSHKeys;
\ No newline at end of file
diff --git a/moon/apps/web/hooks/useDeleteSSHKeyById.ts b/moon/apps/web/hooks/useDeleteSSHKeyById.ts
new file mode 100644
index 000000000..c74ffa8ff
--- /dev/null
+++ b/moon/apps/web/hooks/useDeleteSSHKeyById.ts
@@ -0,0 +1,9 @@
+import { useMutation } from '@tanstack/react-query'
+import { DeleteApiUserSshByKeyIdData } from '@gitmono/types'
+import { legacyApiClient } from '@/utils/queryClient'
+
+export function useDeleteSSHKeyById() {
+ return useMutation({
+ mutationFn: ({ keyId }) => legacyApiClient.v1.deleteApiUserSshByKeyId().request(keyId)
+ })
+}
\ No newline at end of file
diff --git a/moon/apps/web/hooks/useDeleteTokenById.ts b/moon/apps/web/hooks/useDeleteTokenById.ts
new file mode 100644
index 000000000..4621ad81c
--- /dev/null
+++ b/moon/apps/web/hooks/useDeleteTokenById.ts
@@ -0,0 +1,9 @@
+import { useMutation } from '@tanstack/react-query'
+import { DeleteApiUserTokenByKeyIdData } from '@gitmono/types'
+import { legacyApiClient } from '@/utils/queryClient'
+
+export function useDeleteTokenById() {
+ return useMutation({
+ mutationFn: ({ keyId }) => legacyApiClient.v1.deleteApiUserTokenByKeyId().request(keyId)
+ })
+}
\ No newline at end of file
diff --git a/moon/apps/web/hooks/useGetSSHList.ts b/moon/apps/web/hooks/useGetSSHList.ts
new file mode 100644
index 000000000..575f0a609
--- /dev/null
+++ b/moon/apps/web/hooks/useGetSSHList.ts
@@ -0,0 +1,33 @@
+import {legacyApiClient} from "@/utils/queryClient";
+import {atomFamily} from "jotai/utils";
+import {atomWithWebStorage} from "@/utils/atomWithWebStorage";
+import {ListSSHKey} from "@gitmono/types";
+import {useAtom} from "jotai";
+import { useQuery } from '@tanstack/react-query'
+import {useEffect} from 'react'
+
+const fetchSSHList = legacyApiClient.v1.getApiUserSshList()
+const getSSHListAtom = atomFamily(() =>
+ atomWithWebStorage(`ssh-key`, [])
+)
+
+export const useGetSSHList = () => {
+ const [sshKeys, setSSHList] = useAtom(getSSHListAtom('ssh-key'))
+
+ const { data, isLoading, isPending, isFetching } = useQuery({
+ queryKey: fetchSSHList.requestKey(),
+ queryFn: async () => {
+ const result = await fetchSSHList.request()
+
+ return result.data
+ },
+ });
+
+ useEffect(() => {
+ if(data){
+ setSSHList(data)
+ }
+ }, [data, setSSHList]);
+
+ return { sshKeys, isLoading, isPending, isFetching }
+}
\ No newline at end of file
diff --git a/moon/apps/web/hooks/useGetTokenList.ts b/moon/apps/web/hooks/useGetTokenList.ts
new file mode 100644
index 000000000..d0f7be6b3
--- /dev/null
+++ b/moon/apps/web/hooks/useGetTokenList.ts
@@ -0,0 +1,38 @@
+import { atomFamily } from 'jotai/utils'
+import { atomWithWebStorage } from '@/utils/atomWithWebStorage'
+import { legacyApiClient } from '@/utils/queryClient'
+import { useQuery } from '@tanstack/react-query'
+import { useAtom } from 'jotai'
+import {useEffect} from 'react'
+import { ListToken } from '@gitmono/types'
+
+const fetchTokenList = legacyApiClient.v1.getApiUserTokenList()
+const getTokenListAtom = atomFamily(() =>
+ atomWithWebStorage(`token`, [])
+)
+
+export const useGetTokenList = () => {
+ const [tokenList, setTokenList] = useAtom(getTokenListAtom('token'))
+
+ const {data, isLoading, isPending, isFetching} = useQuery({
+ queryKey: fetchTokenList.requestKey(),
+ queryFn: async () => {
+ const result = await fetchTokenList.request()
+
+ return result.data
+ },
+ })
+
+ useEffect(() => {
+ if(data){
+ setTokenList(data)
+ }
+ }, [data, setTokenList]);
+
+ return {
+ tokenList,
+ isLoading,
+ isPending,
+ isFetching
+ }
+}
diff --git a/moon/apps/web/hooks/usePostSSHKey.ts b/moon/apps/web/hooks/usePostSSHKey.ts
new file mode 100644
index 000000000..7bd1461b3
--- /dev/null
+++ b/moon/apps/web/hooks/usePostSSHKey.ts
@@ -0,0 +1,9 @@
+import { useMutation } from '@tanstack/react-query';
+import { AddSSHKey, PostApiUserSshData, RequestParams } from '@gitmono/types'
+import { legacyApiClient } from '@/utils/queryClient'
+
+export function usePostSSHKey() {
+ return useMutation({
+ mutationFn: ({ data, params }) => legacyApiClient.v1.postApiUserSsh().request(data, params)
+ })
+}
\ No newline at end of file
diff --git a/moon/apps/web/hooks/usePostTokenGenerate.ts b/moon/apps/web/hooks/usePostTokenGenerate.ts
new file mode 100644
index 000000000..3511d1119
--- /dev/null
+++ b/moon/apps/web/hooks/usePostTokenGenerate.ts
@@ -0,0 +1,9 @@
+import { useMutation } from '@tanstack/react-query'
+import { PostApiUserTokenGenerateData } from '@gitmono/types'
+import { legacyApiClient } from '@/utils/queryClient'
+
+export function usePostTokenGenerate() {
+ return useMutation({
+ mutationFn: () => legacyApiClient.v1.postApiUserTokenGenerate().request()
+ })
+}
\ No newline at end of file
diff --git a/moon/apps/web/pages/me/settings/index.tsx b/moon/apps/web/pages/me/settings/index.tsx
index 9a1c1d30f..191e60f0d 100644
--- a/moon/apps/web/pages/me/settings/index.tsx
+++ b/moon/apps/web/pages/me/settings/index.tsx
@@ -1,25 +1,22 @@
-import { useEffect } from 'react'
-import { ProfileDisplay } from 'components/UserSettings/ProfileDisplay'
-import { ProfileSecurity } from 'components/UserSettings/ProfileSecurity'
-import { TwoFactorAuthentication } from 'components/UserSettings/TwoFactorAuthentication'
-import Head from 'next/head'
-
-import { CopyCurrentUrl } from '@/components/CopyCurrentUrl'
-import { DesktopAppUpsell } from '@/components/DesktopAppUpsell'
-import AuthAppProviders from '@/components/Providers/AuthAppProviders'
-import { PWAUpsell } from '@/components/PWAUpsell'
-import { ThemePicker } from '@/components/ThemePicker'
-import { Behaviors } from '@/components/UserSettings/Behaviors'
-import { CalDotComIntegration } from '@/components/UserSettings/CalDotComIntegration'
-import { FigmaIntegration } from '@/components/UserSettings/FigmaIntegration'
-import { NotificationSettings } from '@/components/UserSettings/Notifications/NotificationSettings'
-import { PushNotificationSettings } from '@/components/UserSettings/Notifications/PushNotificationSettings'
-import { NotificationSchedule } from '@/components/UserSettings/NotificationSchedule'
-import { UserSettingsPageWrapper } from '@/components/UserSettings/PageWrapper'
-import { PersonalCallLinks } from '@/components/UserSettings/PersonalCallLinks'
-import { SlackNotificationSettings } from '@/components/UserSettings/SlackNotificationSettings'
+import { useEffect } from 'react';
+import { ProfileDisplay } from 'components/UserSettings/ProfileDisplay';
+import { ProfileSecurity } from 'components/UserSettings/ProfileSecurity';
+import { TwoFactorAuthentication } from 'components/UserSettings/TwoFactorAuthentication';
+import Head from 'next/head';
+import { CopyCurrentUrl } from '@/components/CopyCurrentUrl';
+import AuthAppProviders from '@/components/Providers/AuthAppProviders';
+import SSHKeys from '@/components/Setting/SSHKeys';
+import PersonalToken from '@/components/Setting/PersonalToken';
+import { ThemePicker } from '@/components/ThemePicker';
+import { Behaviors } from '@/components/UserSettings/Behaviors';
+import { NotificationSettings } from '@/components/UserSettings/Notifications/NotificationSettings';
+import { PushNotificationSettings } from '@/components/UserSettings/Notifications/PushNotificationSettings';
+import { NotificationSchedule } from '@/components/UserSettings/NotificationSchedule';
+import { UserSettingsPageWrapper } from '@/components/UserSettings/PageWrapper';
+import { PersonalCallLinks } from '@/components/UserSettings/PersonalCallLinks';
+import { SlackNotificationSettings } from '@/components/UserSettings/SlackNotificationSettings';
import { Timezone } from '@/components/UserSettings/Timezone'
-import { PageWithProviders } from '@/utils/types'
+import { PageWithProviders } from '@/utils/types';
const UserSettingsPage: PageWithProviders = () => {
useEffect(() => {
@@ -45,10 +42,8 @@ const UserSettingsPage: PageWithProviders = () => {
-
-
-
-
+
+