Skip to content
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
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