Skip to content

Commit

Permalink
Merge pull request #97 from ZingerLittleBee/feat/token-status
Browse files Browse the repository at this point in the history
feat: 🎸 token add and delete
  • Loading branch information
ZingerLittleBee committed Apr 29, 2024
2 parents f596ca8 + 7efc423 commit 516061b
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 71 deletions.
8 changes: 4 additions & 4 deletions apps/hub/app/server/components/form/server-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const NoGroup = 'no-group'
export function ServerForm({ mode, id, server, onSubmit }: ServerFormProps) {
const router = useRouter()
const { data: groups } = api.group.list.useQuery()
const { mutateAsync } = api.server.create.useMutation()
const { mutateAsync: createServer } = api.server.create.useMutation()
const { mutateAsync: updateServer } = api.server.update.useMutation()
const setIsOpen = useBoundStore.use.setIsOpenServerForm()
const setTokenDialogProps = useBoundStore.use.setTokenDialogProps()
Expand Down Expand Up @@ -84,11 +84,11 @@ export function ServerForm({ mode, id, server, onSubmit }: ServerFormProps) {
}

if (mode === FormMode.Create) {
const token = await mutateAsync(params)
const token = await createServer(params)
setTokenDialogProps({
title: 'Server created!',
description: 'Copy the token for communication with the node',
tokens: [token],
serverId: token.serverId,
})
setIsOpen(false)
setIsOpenTokenDialog(true)
Expand Down Expand Up @@ -198,7 +198,7 @@ export function ServerForm({ mode, id, server, onSubmit }: ServerFormProps) {
<span className="hover:underline">
{group.name}
</span>
<span className="text-muted-foreground truncate text-sm">
<span className="truncate text-sm text-muted-foreground">
{group.description}
</span>
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/hub/app/server/components/group/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
AccordionTrigger,
} from '@/components/ui/accordion'
import { Badge } from '@/components/ui/badge'
import { Button, buttonVariants } from '@/components/ui/button'
import { buttonVariants } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
Expand Down
9 changes: 3 additions & 6 deletions apps/hub/app/server/components/table/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,15 @@ const Actions = ({ row }: { row: Row<Server> }) => {
const setConfirmDialogProps = useBoundStore.use.setConfirmDialogProps()
const setIsOpenServerForm = useBoundStore.use.setIsOpenServerForm()
const setServerFormProps = useBoundStore.use.setServerFormProps()
const tokens = api.server.getTokens.useQuery({
id: row.original.id,
})
const { mutateAsync: deleteServer } = api.server.delete.useMutation()
const server = row.original

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<Button variant="ghost" className="size-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
<MoreHorizontal className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
Expand All @@ -72,7 +69,7 @@ const Actions = ({ row }: { row: Row<Server> }) => {
onClick={() => {
setTokenDialogProps({
title: 'Token list',
tokens: tokens.data ?? [],
serverId: server.id,
})
setIsOpen(true)
}}
Expand Down
63 changes: 36 additions & 27 deletions apps/hub/app/server/components/token-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use client'

import { useState } from 'react'
import { useBoundStore } from '@/store'
import { CheckIcon, Copy } from 'lucide-react'
import { api } from '@/trpc/react'
import { X } from 'lucide-react'

import { Button } from '@/components/ui/button'
import {
Expand All @@ -16,18 +16,36 @@ import {
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import CopyButton from '@/components/copy-button'
import { STooltip } from '@/components/s-tooltip'

export type TokenDialogProps = {
title: string
description?: string
tokens: string[]
serverId: string
}

export function TokenDialog() {
const isOpen = useBoundStore.use.isOpenTokenDialog()
const setIsOpen = useBoundStore.use.setIsOpenTokenDialog()
const tokenDialogProps = useBoundStore.use.tokenDialogProps()
const [copySuccess, setCopySuccess] = useState(false)

const { data: tokens, refetch } = api.serverToken.list.useQuery({
id: tokenDialogProps.serverId,
})

const { mutateAsync: generateToken } = api.serverToken.create.useMutation()
const { mutateAsync: deleteToken } = api.serverToken.delete.useMutation()

const handleGenerateToken = async () => {
await generateToken({ serverId: tokenDialogProps.serverId })
await refetch()
}

const handleDeleteToken = async (token: string) => {
await deleteToken({ token })
await refetch()
}

return (
<Dialog open={isOpen} onOpenChange={setIsOpen} defaultOpen={isOpen}>
Expand All @@ -39,41 +57,32 @@ export function TokenDialog() {
'Copy the token to node and use it to connect to your server.'}
</DialogDescription>
</DialogHeader>
{tokenDialogProps.tokens.map((token, index) => (
{tokens?.map(({ token }, index) => (
<div key={index} className="flex items-center space-x-2">
<div className="grid flex-1 gap-2">
<Label htmlFor="link" className="sr-only">
Link
</Label>
<Input id="link" defaultValue={token} readOnly />
</div>
<Button
type="submit"
size="sm"
className="px-3"
onClick={async () => {
await navigator.clipboard.writeText(token)
setCopySuccess(true)
setTimeout(() => {
setCopySuccess(false)
}, 2000)
}}
>
<span className="sr-only">Copy</span>
{copySuccess ? (
<CheckIcon className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
<CopyButton content={token} />
<STooltip content="Delete Token">
<Button
variant="secondary"
size="sm"
onClick={() => handleDeleteToken(token)}
>
<span className="sr-only">Delete Token</span>
<X className="size-4" />
</Button>
</STooltip>
</div>
))}
<DialogFooter className="sm:justify-start">
<DialogClose asChild>
<Button type="button" variant="secondary">
Close
</Button>
<Button variant="secondary">Close</Button>
</DialogClose>
<Button onClick={handleGenerateToken}>Generate</Button>
</DialogFooter>
</DialogContent>
</Dialog>
Expand Down
2 changes: 1 addition & 1 deletion apps/hub/app/server/store/token-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const createTokenDialogSlice: StateCreator<
> = (set) => ({
tokenDialogProps: {
title: '',
tokens: [],
serverId: '',
},
isOpenTokenDialog: false,
setTokenDialogProps: (tokenDialogProps) =>
Expand Down
29 changes: 29 additions & 0 deletions apps/hub/components/copy-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useState } from 'react'
import { CheckIcon, Copy } from 'lucide-react'

import { Button } from '@/components/ui/button'

export default function CopyButton({ content }: { content: string }) {
const [copySuccess, setCopySuccess] = useState(false)

return (
<Button
size="sm"
variant="outline"
onClick={async () => {
await navigator.clipboard.writeText(content)
setCopySuccess(true)
setTimeout(() => {
setCopySuccess(false)
}, 2000)
}}
>
<span className="sr-only">Copy</span>
{copySuccess ? (
<CheckIcon className="size-4" />
) : (
<Copy className="size-4" />
)}
</Button>
)
}
2 changes: 2 additions & 0 deletions apps/hub/server/api/root.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { dashboardRouter } from '@/server/api/routers/dashboard'
import { groupRouter } from '@/server/api/routers/group'
import { serverRouter } from '@/server/api/routers/server'
import { serverTokenRouter } from '@/server/api/routers/serverToken'
import { userRouter } from '@/server/api/routers/user'
import { createTRPCRouter } from '@/server/api/trpc'

Expand All @@ -14,6 +15,7 @@ export const appRouter = createTRPCRouter({
dashboard: dashboardRouter,
group: groupRouter,
user: userRouter,
serverToken: serverTokenRouter,
})

// export type definition of API
Expand Down
40 changes: 10 additions & 30 deletions apps/hub/server/api/routers/server.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { env } from '@/env'
import { NotLoggedInError } from '@/server/api/error'
import {
createTRPCRouter,
getCaller,
protectedProcedure,
publicProcedure,
} from '@/server/api/trpc'
import { type Prisma } from '@serverbee/db'
import { sign } from 'jsonwebtoken'
import { z } from 'zod'

import { getLogger } from '@/lib/logging'
Expand Down Expand Up @@ -70,20 +69,17 @@ export const serverRouter = createTRPCRouter({
data: createData,
})

const payload = {
userId: ctx.session.user.id,
serverId: server.id,
}
const caller = await getCaller()

const token = sign(payload, env.SERVER_JWT_SECRET)

const serverToken = await ctx.db.serverToken.create({
data: {
token,
server: { connect: { id: server.id } },
},
const token: {
id: number
token: string
isExpires: boolean
serverId: string
} = await caller.serverToken.create({
serverId: server.id,
})
return serverToken.token
return token
}),
update: protectedProcedure
.input(
Expand All @@ -97,7 +93,6 @@ export const serverRouter = createTRPCRouter({
)
.mutation(async ({ ctx, input }) => {
if (!ctx.session.user) throw NotLoggedInError

try {
await ctx.db.server.update({
where: {
Expand All @@ -122,21 +117,6 @@ export const serverRouter = createTRPCRouter({
return false
}
}),
getTokens: protectedProcedure
.input(
z.object({
id: z.string(),
})
)
.query(async ({ input, ctx }) => {
if (!ctx.session.user) throw NotLoggedInError
const queryResult = await ctx.db.serverToken.findMany({
where: {
serverId: input.id,
},
})
return queryResult.map((item) => item.token)
}),
getOwnServerIds: protectedProcedure.query(async ({ ctx }) => {
if (!ctx.session.user) throw NotLoggedInError
const result = await ctx.db.server.findMany({
Expand Down
82 changes: 82 additions & 0 deletions apps/hub/server/api/routers/serverToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { env } from '@/env'
import { NotLoggedInError } from '@/server/api/error'
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc'
import { sign } from 'jsonwebtoken'
import { z } from 'zod'

import { getLogger } from '@/lib/logging'

const logger = getLogger('server-token.ts')

export const serverTokenRouter = createTRPCRouter({
list: protectedProcedure
.input(
z.object({
id: z.string(),
})
)
.query(async ({ input, ctx }) => {
if (!ctx.session.user) throw NotLoggedInError
const queryResult = await ctx.db.serverToken.findMany({
where: {
serverId: input.id,
},
})
return queryResult.map((item) => ({
...item,
}))
}),
create: protectedProcedure
.input(
z.object({
serverId: z.string(),
})
)
.mutation(async ({ ctx, input }) => {
const payload = {
serverId: input.serverId,
}

const token = sign(payload, env.SERVER_JWT_SECRET)

const serverToken = await ctx.db.serverToken.create({
data: {
token,
server: { connect: { id: input.serverId } },
},
})
return {
id: serverToken.id,
token: serverToken.token,
isExpires: false,
serverId: serverToken.serverId,
}
}),
delete: protectedProcedure
.input(
z.object({
token: z.string(),
})
)
.mutation(async ({ input, ctx }) => {
const token = await ctx.db.serverToken.findFirst({
where: {
token: input.token,
},
})
if (token) {
await ctx.db.serverToken.delete({
where: {
id: token.id,
},
})

await ctx.mongo
.db('serverbee')
.collection('invalid')
.insertOne({ token: input.token })

logger.info(`Token ${input.token} deleted`)
}
}),
})
Loading

0 comments on commit 516061b

Please sign in to comment.