Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashboard): api keys in dashboard #3483

Merged
merged 11 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions apps/dashboard/app/api-keys/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Box, Link, Typography } from "@mui/joy"

import ContentContainer from "@/components/content-container"
import ApiKeysList from "@/components/api-keys/list"
import ApiKeyCreate from "@/components/api-keys/create"

export default async function Home() {
return (
<ContentContainer>
<Box
sx={{
display: "flex",
flexDirection: "column",
rowGap: "1em",
alignItems: "flex-start",
maxWidth: "90em",
margin: "0 auto",
width: "100%",
}}
>
<Box
sx={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
maxWidth: "90em",
margin: "0 auto",
width: "100%",
}}
>
<Typography>
Your API Keys that can be used to access the{" "}
<Link href="https://dev.blink.sv/">Blink API</Link>
</Typography>
<ApiKeyCreate />
</Box>
<ApiKeysList />
</Box>
</ContentContainer>
)
}
88 changes: 88 additions & 0 deletions apps/dashboard/app/api-keys/server-actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"use server"
import { getServerSession } from "next-auth"
import { revalidatePath } from "next/cache"

import { authOptions } from "@/app/api/auth/[...nextauth]/route"
import { createApiKey, revokeApiKey } from "@/services/graphql/mutations/api-keys"

export const revokeApiKeyServerAction = async (id: string) => {
if (!id || typeof id !== "string") {
return {
error: true,
message: "API Key ID to revoke is not present",
data: null,
}
}

const session = await getServerSession(authOptions)
const token = session?.accessToken
if (!token || typeof token !== "string") {
return {
error: true,
message: "Token is not present",
data: null,
}
}

try {
await revokeApiKey(token, id)
} catch (err) {
console.log("error in revokeApiKey ", err)
return {
error: true,
message:
"Something went wrong Please try again and if error persist contact support",
data: null,
}
}

revalidatePath("/api-keys")

return {
error: false,
message: "API Key revoked successfully",
data: { ok: true },
}
}

export const createApiKeyServerAction = async (_prevState: unknown, form: FormData) => {
const apiKeyName = form.get("apiKeyName")
if (!apiKeyName || typeof apiKeyName !== "string") {
return {
error: true,
message: "API Key name to create is not present",
data: null,
}
}

const session = await getServerSession(authOptions)
const token = session?.accessToken
if (!token || typeof token !== "string") {
return {
error: true,
message: "Token is not present",
data: null,
}
}

let data
try {
data = await createApiKey(token, apiKeyName)
} catch (err) {
console.log("error in createApiKey ", err)
return {
error: true,
message:
"Something went wrong Please try again and if error persist contact support",
data: null,
}
}

revalidatePath("/api-keys")

return {
error: false,
message: "API Key created successfully",
data: { apiKeySecret: data?.apiKeyCreate.apiKeySecret },
}
}
4 changes: 2 additions & 2 deletions apps/dashboard/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import Sidebar from "@/components/side-bar"
import ThemeRegistry from "@/theme/theme-registry"

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Galoy Dashboard",
description: "Interact with your Galoy App",
}

export default async function RootLayout({ children }: { children: React.ReactNode }) {
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/app/security/email/add/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default function AddEmail() {
const [state, formAction] = useFormState(emailRegisterInitiateServerAction, {
error: null,
message: null,
responsePayload: {},
data: null,
})

return (
Expand Down
1 change: 0 additions & 1 deletion apps/dashboard/app/security/server-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ export const emailRegisterInitiateServerAction = async (
}

if (data?.userEmailRegistrationInitiate.errors.length) {
console.log()
return {
error: true,
message: data?.userEmailRegistrationInitiate.errors[0].message,
Expand Down
1 change: 1 addition & 0 deletions apps/dashboard/app/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export const URLS: Record<string, UrlInfo> = {
"/security": { title: "Security", protected: true },
"/security/email/add": { title: "Add Email", protected: true },
"/security/email/verify": { title: "Verify Email", protected: true },
"/api-keys": { title: "API Keys", protected: true },
}
3 changes: 2 additions & 1 deletion apps/dashboard/codegen.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
overwrite: true
schema: "https://api.staging.galoy.io/graphql"
schema: "http://localhost:4004/graphql"
# schema: "./supergraph.graphql"
documents:
- "app/**/*.ts"
Expand Down Expand Up @@ -66,3 +66,4 @@ generates:
EndpointId: "string"
EndpointUrl: "string"
NotificationCategory: "string"
DateTime: "string"
213 changes: 213 additions & 0 deletions apps/dashboard/components/api-keys/create.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
"use client"

import {
Box,
Button,
FormControl,
FormHelperText,
Input,
Modal,
ModalClose,
Sheet,
Typography,
Tooltip,
} from "@mui/joy"

import InfoOutlined from "@mui/icons-material/InfoOutlined"

import AddIcon from "@mui/icons-material/Add"
import CopyIcon from "@mui/icons-material/CopyAll"
import { useState } from "react"
import { useRouter } from "next/navigation"

import {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-next-line no-implicit-any error
experimental_useFormState as useFormState,
} from "react-dom"

import FormSubmitButton from "../form-submit-button"

import { createApiKeyServerAction } from "@/app/api-keys/server-actions"

const ApiKeyCreate = () => {
const router = useRouter()

const [open, setOpen] = useState(false)
const [copied, setCopied] = useState(false)
const [state, formAction] = useFormState(createApiKeyServerAction, {
error: null,
message: null,
data: null,
})

const handleModalClose = () => {
setOpen(false)
state.error = null
state.message = null
state.data = null
console.log("Modal has been closed")
router.refresh()
}

return (
<>
<Button onClick={() => setOpen(true)} variant="solid" color="primary">
{<AddIcon />}
</Button>
<Modal
open={open}
onClose={handleModalClose}
sx={{ display: "flex", justifyContent: "center", alignItems: "center" }}
>
<Sheet
variant="outlined"
sx={{
borderRadius: "md",
p: 3,
boxShadow: "lg",
display: "flex",
flexDirection: "column",
gap: 2,
alignItems: "flex-start",
}}
>
<ModalClose variant="plain" sx={{ alignSelf: "flex-end" }} />
<Typography
component="h2"
id="modal-title"
level="h4"
textColor="inherit"
fontWeight="lg"
>
Create API key
</Typography>
<Typography id="modal-desc" textColor="text.tertiary" textAlign="left">
Talk to the Blink Servers using this token.
</Typography>

{state?.data?.apiKeySecret ? (
<>
<Box
sx={{
maxWidth: 400,
display: "flex",
flexDirection: "row",
alignItems: "center",
width: "100%",
columnGap: 2,
backgroundColor: "neutral.solidDisabledBg",
padding: "0.6em",
borderRadius: "0.5em",
}}
>
<Typography
sx={{
overflow: "scroll",
}}
fontFamily="monospace"
>
{state?.data?.apiKeySecret}
</Typography>
<Tooltip
sx={{ cursor: "pointer" }}
open={copied}
title="Copied to Clipboard"
variant="plain"
onClick={() => {
setCopied(true)
setTimeout(() => {
setCopied(false)
}, 2000)
navigator.clipboard.writeText(state?.data?.apiKeySecret)
}}
>
<CopyIcon />
</Tooltip>
</Box>
<Typography
sx={{
p: "1em",
}}
variant="outlined"
color="success"
fontSize={14}
>
The API Key Secret will be shown only once here.
<br /> Please save it somewhere safely!
</Typography>
<Button
variant="outlined"
color="primary"
type="submit"
onClick={handleModalClose}
sx={{
width: "100%",
}}
>
Close
</Button>
</>
) : (
<>
<FormControl
sx={{
width: "100%",
}}
error={state.error}
>
<form
style={{
display: "flex",
flexDirection: "column",
gap: "1em",
}}
action={formAction}
>
<Input
name="apiKeyName"
id="apiKeyName"
sx={{
padding: "0.6em",
width: "100%",
}}
placeholder="API Key Name..."
/>
{state.error ? (
<FormHelperText>
<InfoOutlined />
{state.message}
</FormHelperText>
) : null}

<Box
sx={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
width: "100%",
alignItems: "center",
}}
>
<FormSubmitButton
variant="outlined"
color="primary"
type="submit"
sx={{
width: "100%",
}}
>
Create
</FormSubmitButton>
</Box>
</form>
</FormControl>
</>
)}
</Sheet>
</Modal>
</>
)
}

export default ApiKeyCreate
Loading
Loading