feat: access tokens (create, list, delete)
This commit is contained in:
parent
415bba96f0
commit
90232feb1e
58
apps/api/src/access-token/create.ts
Normal file
58
apps/api/src/access-token/create.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import crypto from 'node:crypto'
|
||||
import { access_token, db } from '@boring.tools/database'
|
||||
import { AccessTokenCreateInput, AccessTokenOutput } from '@boring.tools/schema'
|
||||
import { createRoute, type z } from '@hono/zod-openapi'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
import type { accessTokenApi } from '.'
|
||||
import { verifyAuthentication } from '../utils/authentication'
|
||||
|
||||
export const route = createRoute({
|
||||
method: 'post',
|
||||
path: '/',
|
||||
tags: ['access-token'],
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
'application/json': { schema: AccessTokenCreateInput },
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: {
|
||||
content: {
|
||||
'application/json': { schema: AccessTokenOutput },
|
||||
},
|
||||
description: 'Commits created',
|
||||
},
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const registerAccessTokenCreate = (api: typeof accessTokenApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = verifyAuthentication(c)
|
||||
|
||||
const data: z.infer<typeof AccessTokenCreateInput> = await c.req.json()
|
||||
const token = crypto.randomBytes(20).toString('hex')
|
||||
|
||||
const [result] = await db
|
||||
.insert(access_token)
|
||||
.values({
|
||||
...data,
|
||||
userId,
|
||||
token: `bt_${token}`,
|
||||
})
|
||||
.returning()
|
||||
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Not Found' })
|
||||
}
|
||||
|
||||
return c.json(result, 200)
|
||||
})
|
||||
}
|
45
apps/api/src/access-token/delete.ts
Normal file
45
apps/api/src/access-token/delete.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { access_token, db } from '@boring.tools/database'
|
||||
import { GeneralOutput } from '@boring.tools/schema'
|
||||
import { createRoute, type z } from '@hono/zod-openapi'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
import type { accessTokenApi } from '.'
|
||||
import { verifyAuthentication } from '../utils/authentication'
|
||||
|
||||
export const route = createRoute({
|
||||
method: 'delete',
|
||||
path: '/:id',
|
||||
tags: ['access-token'],
|
||||
responses: {
|
||||
201: {
|
||||
content: {
|
||||
'application/json': { schema: GeneralOutput },
|
||||
},
|
||||
description: 'Removes a access token by id',
|
||||
},
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const registerAccessTokenDelete = (api: typeof accessTokenApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const id = c.req.param('id')
|
||||
const userId = verifyAuthentication(c)
|
||||
|
||||
const result = await db
|
||||
.delete(access_token)
|
||||
.where(and(eq(access_token.userId, userId), eq(access_token.id, id)))
|
||||
.returning()
|
||||
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Not Found' })
|
||||
}
|
||||
|
||||
return c.json(result, 200)
|
||||
})
|
||||
}
|
16
apps/api/src/access-token/index.ts
Normal file
16
apps/api/src/access-token/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { OpenAPIHono } from '@hono/zod-openapi'
|
||||
import type { Variables } from '..'
|
||||
import type { ContextModule } from '../utils/sentry'
|
||||
import { registerAccessTokenCreate } from './create'
|
||||
import { registerAccessTokenDelete } from './delete'
|
||||
import { registerAccessTokenList } from './list'
|
||||
|
||||
export const accessTokenApi = new OpenAPIHono<{ Variables: Variables }>()
|
||||
|
||||
const module: ContextModule = {
|
||||
name: 'access-token',
|
||||
}
|
||||
|
||||
registerAccessTokenCreate(accessTokenApi)
|
||||
registerAccessTokenList(accessTokenApi)
|
||||
registerAccessTokenDelete(accessTokenApi)
|
49
apps/api/src/access-token/list.ts
Normal file
49
apps/api/src/access-token/list.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { changelog, db } from '@boring.tools/database'
|
||||
import { AccessTokenListOutput } from '@boring.tools/schema'
|
||||
import { createRoute } from '@hono/zod-openapi'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
import type { accessTokenApi } from '.'
|
||||
import { verifyAuthentication } from '../utils/authentication'
|
||||
|
||||
const route = createRoute({
|
||||
method: 'get',
|
||||
path: '/',
|
||||
tags: ['access-token'],
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: AccessTokenListOutput,
|
||||
},
|
||||
},
|
||||
description: 'Return version by id',
|
||||
},
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const registerAccessTokenList = (api: typeof accessTokenApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = verifyAuthentication(c)
|
||||
const result = await db.query.access_token.findMany({
|
||||
where: eq(changelog.userId, userId),
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Access Tokens not found' })
|
||||
}
|
||||
|
||||
const mappedData = result.map((at) => ({
|
||||
...at,
|
||||
token: at.token.substring(0, 10),
|
||||
}))
|
||||
|
||||
return c.json(mappedData, 200)
|
||||
})
|
||||
}
|
@ -5,10 +5,9 @@ import { apiReference } from '@scalar/hono-api-reference'
|
||||
import { cors } from 'hono/cors'
|
||||
|
||||
import changelog from './changelog'
|
||||
import { changelogCommitApi } from './changelog/commit'
|
||||
import version from './changelog/version'
|
||||
import user from './user'
|
||||
|
||||
import { accessTokenApi } from './access-token'
|
||||
import pageApi from './page'
|
||||
import { authentication } from './utils/authentication'
|
||||
import { handleError, handleZodError } from './utils/errors'
|
||||
@ -21,7 +20,7 @@ export type Variables = {
|
||||
}
|
||||
|
||||
export const app = new OpenAPIHono<{ Variables: Variables }>({
|
||||
// defaultHook: handleZodError,
|
||||
defaultHook: handleZodError,
|
||||
})
|
||||
|
||||
// app.use(
|
||||
@ -37,6 +36,7 @@ app.use('/v1/*', authentication)
|
||||
app.route('/v1/user', user)
|
||||
app.route('/v1/changelog', changelog)
|
||||
app.route('/v1/page', pageApi)
|
||||
app.route('/v1/access-token', accessTokenApi)
|
||||
|
||||
app.doc('/openapi.json', {
|
||||
openapi: '3.0.0',
|
||||
|
@ -20,12 +20,15 @@ const generatedToken = async (c: Context, next: Next) => {
|
||||
},
|
||||
})
|
||||
|
||||
console.log(accessTokenResult)
|
||||
|
||||
if (!accessTokenResult) {
|
||||
throw new HTTPException(401, { message: 'Unauthorized' })
|
||||
}
|
||||
|
||||
await db
|
||||
.update(access_token)
|
||||
.set({ lastUsedOn: new Date() })
|
||||
.where(eq(access_token.id, accessTokenResult.id))
|
||||
|
||||
c.set('user', accessTokenResult.user)
|
||||
|
||||
await next()
|
||||
|
@ -20,6 +20,7 @@
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tanstack/react-query": "^5.59.0",
|
||||
"@tanstack/react-router": "^1.58.15",
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"axios": "^1.7.7",
|
||||
"lucide-react": "^0.446.0",
|
||||
"react": "^18.3.1",
|
||||
|
72
apps/app/src/components/AccessToken/Delete.tsx
Normal file
72
apps/app/src/components/AccessToken/Delete.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
Button,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@boring.tools/ui'
|
||||
import { Trash2Icon } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useAccessTokenDelete } from '../../hooks/useAccessToken'
|
||||
|
||||
export const AccessTokenDelete = ({ id }: { id: string }) => {
|
||||
const accessTokenDelete = useAccessTokenDelete()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const removeChangelog = () => {
|
||||
accessTokenDelete.mutate(
|
||||
{ id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setIsOpen(false)
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Tooltip>
|
||||
<AlertDialog open={isOpen}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={'ghost-destructive'}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<Trash2Icon strokeWidth={1.5} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete your
|
||||
changelog and remove your data from our servers.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={() => setIsOpen(false)}>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction asChild>
|
||||
<Button onClick={removeChangelog} variant={'destructive'}>
|
||||
Delete
|
||||
</Button>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<TooltipContent>
|
||||
<p>Delete access token</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
35
apps/app/src/components/AccessToken/Table/Columns.tsx
Normal file
35
apps/app/src/components/AccessToken/Table/Columns.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import type { AccessTokenOutput } from '@boring.tools/schema'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import { format } from 'date-fns'
|
||||
import type { z } from 'zod'
|
||||
import { AccessTokenDelete } from '../Delete'
|
||||
|
||||
export const AccessTokenColumns: ColumnDef<
|
||||
z.infer<typeof AccessTokenOutput>
|
||||
>[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Name',
|
||||
},
|
||||
{
|
||||
accessorKey: 'lastUsedOn',
|
||||
header: 'Last seen',
|
||||
accessorFn: (row) => {
|
||||
if (!row.lastUsedOn) {
|
||||
return 'Never'
|
||||
}
|
||||
return format(new Date(row.lastUsedOn), 'HH:mm dd.MM.yyyy')
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'token',
|
||||
header: 'Token',
|
||||
accessorFn: (row) => `${row.token}...`,
|
||||
},
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: '',
|
||||
size: 10,
|
||||
cell: (props) => <AccessTokenDelete id={props.row.original.id} />,
|
||||
},
|
||||
]
|
83
apps/app/src/components/AccessToken/Table/DataTable.tsx
Normal file
83
apps/app/src/components/AccessToken/Table/DataTable.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
type ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table'
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@boring.tools/ui'
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
data: TData[]
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
style={{ width: header.getSize() }}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@boring.tools/ui'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { BellIcon } from 'lucide-react'
|
||||
import { NavigationRoutes } from '../utils/navigation-routes'
|
||||
|
||||
export const Navigation = () => {
|
||||
return (
|
||||
<div className="hidden border-r bg-muted/40 md:block">
|
||||
<div className="flex h-full max-h-screen flex-col gap-2">
|
||||
<div className="flex h-14 items-center border-b px-4 lg:h-[60px] lg:px-6">
|
||||
<Link to="/" className="flex items-center gap-2 font-semibold">
|
||||
<span className="">boring.tools</span>
|
||||
</Link>
|
||||
<Button variant="outline" size="icon" className="ml-auto h-8 w-8">
|
||||
<BellIcon className="h-4 w-4" />
|
||||
<span className="sr-only">Toggle notifications</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<nav className="grid items-start px-2 text-sm font-medium lg:px-4 gap-2">
|
||||
{NavigationRoutes.map((route) => {
|
||||
if (!route.childrens) {
|
||||
return (
|
||||
<Link
|
||||
key={route.name}
|
||||
to={route.to}
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary"
|
||||
activeProps={{ className: 'bg-muted text-primary' }}
|
||||
>
|
||||
<route.icon className="h-4 w-4" />
|
||||
{route.name}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
key={route.name}
|
||||
defaultValue="changelog"
|
||||
>
|
||||
<AccordionItem value="changelog">
|
||||
<AccordionTrigger>Changelog</AccordionTrigger>
|
||||
<AccordionContent className="gap-2 flex flex-col">
|
||||
{route.childrens.map((childRoute) => (
|
||||
<Link
|
||||
key={childRoute.name}
|
||||
to={childRoute.to}
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary"
|
||||
activeProps={{ className: 'bg-muted text-primary' }}
|
||||
activeOptions={{ exact: true }}
|
||||
>
|
||||
<childRoute.icon className="h-4 w-4" />
|
||||
{childRoute.name}
|
||||
</Link>
|
||||
))}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
<div className="mt-auto p-4">
|
||||
<Card>
|
||||
<CardHeader className="p-2 pt-0 md:p-4">
|
||||
<CardTitle>More Infos</CardTitle>
|
||||
<CardDescription>
|
||||
If you want more information about boring.tools, visit our
|
||||
documentation!
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-2 pt-0 md:p-4 md:pt-0">
|
||||
<a href="https://boring.tools">
|
||||
<Button size="sm" className="w-full">
|
||||
Documentation
|
||||
</Button>
|
||||
</a>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetTrigger,
|
||||
} from '@boring.tools/ui'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { MenuIcon } from 'lucide-react'
|
||||
import { NavigationRoutes } from '../utils/navigation-routes'
|
||||
|
||||
export const NavigationMobile = () => {
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="shrink-0 md:hidden">
|
||||
<MenuIcon className="h-5 w-5" />
|
||||
<span className="sr-only">Toggle navigation menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="flex flex-col">
|
||||
<nav className="grid gap-2 text-lg font-medium">
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center gap-2 text-lg font-semibold"
|
||||
>
|
||||
<span className="sr-only">boring.tools</span>
|
||||
</Link>
|
||||
{NavigationRoutes.map((route) => {
|
||||
if (!route.childrens) {
|
||||
return (
|
||||
<Link
|
||||
key={route.name}
|
||||
to={route.to}
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary"
|
||||
activeProps={{ className: 'bg-muted text-primary' }}
|
||||
activeOptions={{ exact: true }}
|
||||
>
|
||||
<route.icon className="h-4 w-4" />
|
||||
{route.name}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
key={route.name}
|
||||
defaultValue="changelog"
|
||||
>
|
||||
<AccordionItem value="changelog">
|
||||
<AccordionTrigger>Changelog</AccordionTrigger>
|
||||
<AccordionContent className="gap-2 flex flex-col">
|
||||
{route.childrens.map((childRoute) => (
|
||||
<Link
|
||||
key={childRoute.name}
|
||||
to={childRoute.to}
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary"
|
||||
activeProps={{ className: 'bg-muted text-primary' }}
|
||||
activeOptions={{ exact: true }}
|
||||
>
|
||||
<childRoute.icon className="h-4 w-4" />
|
||||
{childRoute.name}
|
||||
</Link>
|
||||
))}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
<div className="mt-auto">
|
||||
<Card>
|
||||
<CardHeader className="p-2 pt-0 md:p-4">
|
||||
<CardTitle>More Infos</CardTitle>
|
||||
<CardDescription>
|
||||
If you want more information about boring.tools, visit our
|
||||
documentation!
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-2 pt-0 md:p-4 md:pt-0">
|
||||
<a href="https://boring.tools">
|
||||
<Button size="sm" className="w-full">
|
||||
Documentation
|
||||
</Button>
|
||||
</a>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
@ -34,10 +34,8 @@ export function Sidebar() {
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarMenu>
|
||||
<SidebarChangelog />
|
||||
<SidebarPage />
|
||||
</SidebarMenu>
|
||||
<SidebarChangelog />
|
||||
<SidebarPage />
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
|
@ -2,6 +2,7 @@ import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
@ -28,60 +29,62 @@ export const SidebarChangelog = () => {
|
||||
}, [location, setValue])
|
||||
|
||||
return (
|
||||
<Collapsible asChild open={value} onOpenChange={() => setValue(!value)}>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild tooltip="Changelog">
|
||||
<Link
|
||||
to="/changelog"
|
||||
activeProps={{ className: 'bg-sidebar-accent' }}
|
||||
>
|
||||
<FileStackIcon />
|
||||
<span>Changelog</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenu>
|
||||
<Collapsible asChild open={value} onOpenChange={() => setValue(!value)}>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild tooltip="Changelog">
|
||||
<Link
|
||||
to="/changelog"
|
||||
activeProps={{ className: 'bg-sidebar-accent' }}
|
||||
>
|
||||
<FileStackIcon />
|
||||
<span>Changelog</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuAction className="data-[state=open]:rotate-90">
|
||||
<ChevronRightIcon />
|
||||
<span className="sr-only">Toggle</span>
|
||||
</SidebarMenuAction>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{!error &&
|
||||
data?.map((changelog) => (
|
||||
<SidebarMenuSubItem key={changelog.id}>
|
||||
<SidebarMenuSubButton asChild>
|
||||
<Link
|
||||
to={`/changelog/${changelog.id}`}
|
||||
activeProps={{
|
||||
className: 'bg-sidebar-primary',
|
||||
}}
|
||||
>
|
||||
<span>{changelog.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuAction className="data-[state=open]:rotate-90">
|
||||
<ChevronRightIcon />
|
||||
<span className="sr-only">Toggle</span>
|
||||
</SidebarMenuAction>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{!error &&
|
||||
data?.map((changelog) => (
|
||||
<SidebarMenuSubItem key={changelog.id}>
|
||||
<SidebarMenuSubButton asChild>
|
||||
<Link
|
||||
to={`/changelog/${changelog.id}`}
|
||||
activeProps={{
|
||||
className: 'bg-sidebar-primary',
|
||||
}}
|
||||
>
|
||||
<span>{changelog.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
|
||||
<SidebarMenuSubItem className="opacity-60">
|
||||
<SidebarMenuSubButton asChild>
|
||||
<Link
|
||||
to="/changelog/create"
|
||||
activeProps={{
|
||||
className: 'bg-sidebar-primary',
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
<PlusIcon className="w-3 h-3" />
|
||||
New changelog
|
||||
</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
<SidebarMenuSubItem className="opacity-60">
|
||||
<SidebarMenuSubButton asChild>
|
||||
<Link
|
||||
to="/changelog/create"
|
||||
activeProps={{
|
||||
className: 'bg-sidebar-primary',
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
<PlusIcon className="w-3 h-3" />
|
||||
New changelog
|
||||
</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
</SidebarMenu>
|
||||
)
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
@ -28,57 +29,59 @@ export const SidebarPage = () => {
|
||||
}, [location, setValue])
|
||||
|
||||
return (
|
||||
<Collapsible asChild open={value} onOpenChange={() => setValue(!value)}>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild tooltip="Page">
|
||||
<Link to="/page" activeProps={{ className: 'bg-sidebar-accent' }}>
|
||||
<NotebookTextIcon />
|
||||
<span>Page</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenu>
|
||||
<Collapsible asChild open={value} onOpenChange={() => setValue(!value)}>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild tooltip="Page">
|
||||
<Link to="/page" activeProps={{ className: 'bg-sidebar-accent' }}>
|
||||
<NotebookTextIcon />
|
||||
<span>Page</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuAction className="data-[state=open]:rotate-90">
|
||||
<ChevronRightIcon />
|
||||
<span className="sr-only">Toggle</span>
|
||||
</SidebarMenuAction>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{!error &&
|
||||
data?.map((page) => (
|
||||
<SidebarMenuSubItem key={page.id}>
|
||||
<SidebarMenuSubButton asChild>
|
||||
<Link
|
||||
to={`/page/${page?.id}`}
|
||||
activeProps={{
|
||||
className: 'bg-sidebar-primary',
|
||||
}}
|
||||
>
|
||||
<span>{page.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuAction className="data-[state=open]:rotate-90">
|
||||
<ChevronRightIcon />
|
||||
<span className="sr-only">Toggle</span>
|
||||
</SidebarMenuAction>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{!error &&
|
||||
data?.map((page) => (
|
||||
<SidebarMenuSubItem key={page.id}>
|
||||
<SidebarMenuSubButton asChild>
|
||||
<Link
|
||||
to={`/page/${page?.id}`}
|
||||
activeProps={{
|
||||
className: 'bg-sidebar-primary',
|
||||
}}
|
||||
>
|
||||
<span>{page.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
|
||||
<SidebarMenuSubItem className="opacity-60">
|
||||
<SidebarMenuSubButton asChild>
|
||||
<Link
|
||||
to="/page/create"
|
||||
activeProps={{
|
||||
className: 'bg-sidebar-primary',
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
<PlusIcon className="w-3 h-3" />
|
||||
New page
|
||||
</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
<SidebarMenuSubItem className="opacity-60">
|
||||
<SidebarMenuSubButton asChild>
|
||||
<Link
|
||||
to="/page/create"
|
||||
activeProps={{
|
||||
className: 'bg-sidebar-primary',
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
<PlusIcon className="w-3 h-3" />
|
||||
New page
|
||||
</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
</SidebarMenu>
|
||||
)
|
||||
}
|
||||
|
96
apps/app/src/components/SidebarSettings.tsx
Normal file
96
apps/app/src/components/SidebarSettings.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import { ChevronsUpDown } from 'lucide-react'
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from '@boring.tools/ui'
|
||||
import { SignOutButton, useUser } from '@clerk/clerk-react'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
|
||||
export function SidebarUser() {
|
||||
const { isMobile } = useSidebar()
|
||||
const { user } = useUser()
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage
|
||||
src={user?.imageUrl}
|
||||
alt={user?.fullName ?? 'Avatar'}
|
||||
/>
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">{user?.fullName}</span>
|
||||
<span className="truncate text-xs">
|
||||
{user?.primaryEmailAddress?.emailAddress}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||
side={isMobile ? 'bottom' : 'right'}
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage
|
||||
src={user?.imageUrl}
|
||||
alt={user?.fullName ?? 'Avatar'}
|
||||
/>
|
||||
<AvatarFallback className="rounded-lg">
|
||||
{user?.firstName?.substring(0, 1)}
|
||||
{user?.lastName?.substring(0, 1)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">
|
||||
{user?.fullName}
|
||||
</span>
|
||||
<span className="truncate text-xs">
|
||||
{user?.primaryEmailAddress?.emailAddress}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<Link to="/user">Profile</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<SignOutButton>
|
||||
<button type="button">Sign out</button>
|
||||
</SignOutButton>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { ChevronsUpDown } from 'lucide-react'
|
||||
import { ChevronsUpDown, KeyRoundIcon } from 'lucide-react'
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
@ -14,6 +14,7 @@ import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarSeparator,
|
||||
useSidebar,
|
||||
} from '@boring.tools/ui'
|
||||
import { SignOutButton, useUser } from '@clerk/clerk-react'
|
||||
@ -25,6 +26,18 @@ export function SidebarUser() {
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild tooltip="Access Tokens">
|
||||
<Link
|
||||
to="/access-tokens"
|
||||
activeProps={{ className: 'bg-sidebar-accent' }}
|
||||
>
|
||||
<KeyRoundIcon />
|
||||
<span>Access Tokens</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarSeparator />
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
67
apps/app/src/hooks/useAccessToken.ts
Normal file
67
apps/app/src/hooks/useAccessToken.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import type {
|
||||
AccessTokenCreateInput,
|
||||
AccessTokenListOutput,
|
||||
AccessTokenOutput,
|
||||
} from '@boring.tools/schema'
|
||||
import { useAuth } from '@clerk/clerk-react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import type { z } from 'zod'
|
||||
import { queryFetch } from '../utils/queryFetch'
|
||||
|
||||
type AccessToken = z.infer<typeof AccessTokenOutput>
|
||||
type AccessTokenList = z.infer<typeof AccessTokenListOutput>
|
||||
type AccessTokenCreate = z.infer<typeof AccessTokenCreateInput>
|
||||
|
||||
export const useAccessTokenList = () => {
|
||||
const { getToken } = useAuth()
|
||||
return useQuery({
|
||||
queryKey: ['accessTokenList'],
|
||||
queryFn: async (): Promise<AccessTokenList> =>
|
||||
await queryFetch({
|
||||
path: 'access-token',
|
||||
method: 'get',
|
||||
token: await getToken(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export const useAccessTokenCreate = () => {
|
||||
const { getToken } = useAuth()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (
|
||||
payload: AccessTokenCreate,
|
||||
): Promise<Readonly<AccessToken>> =>
|
||||
await queryFetch({
|
||||
path: 'access-token',
|
||||
data: payload,
|
||||
method: 'post',
|
||||
token: await getToken(),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['accessTokenList'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useAccessTokenDelete = () => {
|
||||
const { getToken } = useAuth()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
id,
|
||||
}: { id: string }): Promise<Readonly<AccessToken>> =>
|
||||
await queryFetch({
|
||||
path: `access-token/${id}`,
|
||||
method: 'delete',
|
||||
token: await getToken(),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['accessTokenList'],
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
@ -21,10 +21,12 @@ const IndexLazyImport = createFileRoute('/')()
|
||||
const UserIndexLazyImport = createFileRoute('/user/')()
|
||||
const PageIndexLazyImport = createFileRoute('/page/')()
|
||||
const ChangelogIndexLazyImport = createFileRoute('/changelog/')()
|
||||
const AccessTokensIndexLazyImport = createFileRoute('/access-tokens/')()
|
||||
const PageCreateLazyImport = createFileRoute('/page/create')()
|
||||
const PageIdLazyImport = createFileRoute('/page/$id')()
|
||||
const ChangelogCreateLazyImport = createFileRoute('/changelog/create')()
|
||||
const ChangelogIdLazyImport = createFileRoute('/changelog/$id')()
|
||||
const AccessTokensNewLazyImport = createFileRoute('/access-tokens/new')()
|
||||
const PageIdIndexLazyImport = createFileRoute('/page/$id/')()
|
||||
const ChangelogIdIndexLazyImport = createFileRoute('/changelog/$id/')()
|
||||
const PageIdEditLazyImport = createFileRoute('/page/$id/edit')()
|
||||
@ -57,6 +59,13 @@ const ChangelogIndexLazyRoute = ChangelogIndexLazyImport.update({
|
||||
import('./routes/changelog.index.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const AccessTokensIndexLazyRoute = AccessTokensIndexLazyImport.update({
|
||||
path: '/access-tokens/',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/access-tokens.index.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const PageCreateLazyRoute = PageCreateLazyImport.update({
|
||||
path: '/page/create',
|
||||
getParentRoute: () => rootRoute,
|
||||
@ -79,6 +88,13 @@ const ChangelogIdLazyRoute = ChangelogIdLazyImport.update({
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/changelog.$id.lazy').then((d) => d.Route))
|
||||
|
||||
const AccessTokensNewLazyRoute = AccessTokensNewLazyImport.update({
|
||||
path: '/access-tokens/new',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/access-tokens.new.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const PageIdIndexLazyRoute = PageIdIndexLazyImport.update({
|
||||
path: '/',
|
||||
getParentRoute: () => PageIdLazyRoute,
|
||||
@ -130,6 +146,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IndexLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/access-tokens/new': {
|
||||
id: '/access-tokens/new'
|
||||
path: '/access-tokens/new'
|
||||
fullPath: '/access-tokens/new'
|
||||
preLoaderRoute: typeof AccessTokensNewLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/changelog/$id': {
|
||||
id: '/changelog/$id'
|
||||
path: '/changelog/$id'
|
||||
@ -158,6 +181,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof PageCreateLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/access-tokens/': {
|
||||
id: '/access-tokens/'
|
||||
path: '/access-tokens'
|
||||
fullPath: '/access-tokens'
|
||||
preLoaderRoute: typeof AccessTokensIndexLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/changelog/': {
|
||||
id: '/changelog/'
|
||||
path: '/changelog'
|
||||
@ -260,10 +290,12 @@ const PageIdLazyRouteWithChildren = PageIdLazyRoute._addFileChildren(
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexLazyRoute
|
||||
'/access-tokens/new': typeof AccessTokensNewLazyRoute
|
||||
'/changelog/$id': typeof ChangelogIdLazyRouteWithChildren
|
||||
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||
'/page/$id': typeof PageIdLazyRouteWithChildren
|
||||
'/page/create': typeof PageCreateLazyRoute
|
||||
'/access-tokens': typeof AccessTokensIndexLazyRoute
|
||||
'/changelog': typeof ChangelogIndexLazyRoute
|
||||
'/page': typeof PageIndexLazyRoute
|
||||
'/user': typeof UserIndexLazyRoute
|
||||
@ -277,8 +309,10 @@ export interface FileRoutesByFullPath {
|
||||
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexLazyRoute
|
||||
'/access-tokens/new': typeof AccessTokensNewLazyRoute
|
||||
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||
'/page/create': typeof PageCreateLazyRoute
|
||||
'/access-tokens': typeof AccessTokensIndexLazyRoute
|
||||
'/changelog': typeof ChangelogIndexLazyRoute
|
||||
'/page': typeof PageIndexLazyRoute
|
||||
'/user': typeof UserIndexLazyRoute
|
||||
@ -293,10 +327,12 @@ export interface FileRoutesByTo {
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRoute
|
||||
'/': typeof IndexLazyRoute
|
||||
'/access-tokens/new': typeof AccessTokensNewLazyRoute
|
||||
'/changelog/$id': typeof ChangelogIdLazyRouteWithChildren
|
||||
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||
'/page/$id': typeof PageIdLazyRouteWithChildren
|
||||
'/page/create': typeof PageCreateLazyRoute
|
||||
'/access-tokens/': typeof AccessTokensIndexLazyRoute
|
||||
'/changelog/': typeof ChangelogIndexLazyRoute
|
||||
'/page/': typeof PageIndexLazyRoute
|
||||
'/user/': typeof UserIndexLazyRoute
|
||||
@ -312,10 +348,12 @@ export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/access-tokens/new'
|
||||
| '/changelog/$id'
|
||||
| '/changelog/create'
|
||||
| '/page/$id'
|
||||
| '/page/create'
|
||||
| '/access-tokens'
|
||||
| '/changelog'
|
||||
| '/page'
|
||||
| '/user'
|
||||
@ -328,8 +366,10 @@ export interface FileRouteTypes {
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
| '/access-tokens/new'
|
||||
| '/changelog/create'
|
||||
| '/page/create'
|
||||
| '/access-tokens'
|
||||
| '/changelog'
|
||||
| '/page'
|
||||
| '/user'
|
||||
@ -342,10 +382,12 @@ export interface FileRouteTypes {
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/access-tokens/new'
|
||||
| '/changelog/$id'
|
||||
| '/changelog/create'
|
||||
| '/page/$id'
|
||||
| '/page/create'
|
||||
| '/access-tokens/'
|
||||
| '/changelog/'
|
||||
| '/page/'
|
||||
| '/user/'
|
||||
@ -360,10 +402,12 @@ export interface FileRouteTypes {
|
||||
|
||||
export interface RootRouteChildren {
|
||||
IndexLazyRoute: typeof IndexLazyRoute
|
||||
AccessTokensNewLazyRoute: typeof AccessTokensNewLazyRoute
|
||||
ChangelogIdLazyRoute: typeof ChangelogIdLazyRouteWithChildren
|
||||
ChangelogCreateLazyRoute: typeof ChangelogCreateLazyRoute
|
||||
PageIdLazyRoute: typeof PageIdLazyRouteWithChildren
|
||||
PageCreateLazyRoute: typeof PageCreateLazyRoute
|
||||
AccessTokensIndexLazyRoute: typeof AccessTokensIndexLazyRoute
|
||||
ChangelogIndexLazyRoute: typeof ChangelogIndexLazyRoute
|
||||
PageIndexLazyRoute: typeof PageIndexLazyRoute
|
||||
UserIndexLazyRoute: typeof UserIndexLazyRoute
|
||||
@ -371,10 +415,12 @@ export interface RootRouteChildren {
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexLazyRoute: IndexLazyRoute,
|
||||
AccessTokensNewLazyRoute: AccessTokensNewLazyRoute,
|
||||
ChangelogIdLazyRoute: ChangelogIdLazyRouteWithChildren,
|
||||
ChangelogCreateLazyRoute: ChangelogCreateLazyRoute,
|
||||
PageIdLazyRoute: PageIdLazyRouteWithChildren,
|
||||
PageCreateLazyRoute: PageCreateLazyRoute,
|
||||
AccessTokensIndexLazyRoute: AccessTokensIndexLazyRoute,
|
||||
ChangelogIndexLazyRoute: ChangelogIndexLazyRoute,
|
||||
PageIndexLazyRoute: PageIndexLazyRoute,
|
||||
UserIndexLazyRoute: UserIndexLazyRoute,
|
||||
@ -393,10 +439,12 @@ export const routeTree = rootRoute
|
||||
"filePath": "__root.tsx",
|
||||
"children": [
|
||||
"/",
|
||||
"/access-tokens/new",
|
||||
"/changelog/$id",
|
||||
"/changelog/create",
|
||||
"/page/$id",
|
||||
"/page/create",
|
||||
"/access-tokens/",
|
||||
"/changelog/",
|
||||
"/page/",
|
||||
"/user/"
|
||||
@ -405,6 +453,9 @@ export const routeTree = rootRoute
|
||||
"/": {
|
||||
"filePath": "index.lazy.tsx"
|
||||
},
|
||||
"/access-tokens/new": {
|
||||
"filePath": "access-tokens.new.lazy.tsx"
|
||||
},
|
||||
"/changelog/$id": {
|
||||
"filePath": "changelog.$id.lazy.tsx",
|
||||
"children": [
|
||||
@ -427,6 +478,9 @@ export const routeTree = rootRoute
|
||||
"/page/create": {
|
||||
"filePath": "page.create.lazy.tsx"
|
||||
},
|
||||
"/access-tokens/": {
|
||||
"filePath": "access-tokens.index.lazy.tsx"
|
||||
},
|
||||
"/changelog/": {
|
||||
"filePath": "changelog.index.lazy.tsx"
|
||||
},
|
||||
|
36
apps/app/src/routes/access-tokens.index.lazy.tsx
Normal file
36
apps/app/src/routes/access-tokens.index.lazy.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { Button } from '@boring.tools/ui'
|
||||
import { Link, createLazyFileRoute } from '@tanstack/react-router'
|
||||
import { AccessTokenColumns } from '../components/AccessToken/Table/Columns'
|
||||
import { DataTable } from '../components/AccessToken/Table/DataTable'
|
||||
import { PageWrapper } from '../components/PageWrapper'
|
||||
import { useAccessTokenList } from '../hooks/useAccessToken'
|
||||
|
||||
const Component = () => {
|
||||
const { data, isPending } = useAccessTokenList()
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
breadcrumbs={[
|
||||
{
|
||||
name: 'Access tokens',
|
||||
to: '/access-tokens',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div className="flex w-full gap-5 justify-between items-center">
|
||||
<h1 className="text-3xl">Access Tokens</h1>
|
||||
|
||||
<Button asChild>
|
||||
<Link to="/access-tokens/new">Generate new token</Link>
|
||||
</Button>
|
||||
</div>
|
||||
{data && !isPending && (
|
||||
<DataTable data={data} columns={AccessTokenColumns} />
|
||||
)}
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export const Route = createLazyFileRoute('/access-tokens/')({
|
||||
component: Component,
|
||||
})
|
136
apps/app/src/routes/access-tokens.new.lazy.tsx
Normal file
136
apps/app/src/routes/access-tokens.new.lazy.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
import { AccessTokenCreateInput } from '@boring.tools/schema'
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
Button,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
} from '@boring.tools/ui'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { createLazyFileRoute, useRouter } from '@tanstack/react-router'
|
||||
import { AlertCircle, CopyIcon } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useCopyToClipboard } from 'usehooks-ts'
|
||||
import type { z } from 'zod'
|
||||
import { PageWrapper } from '../components/PageWrapper'
|
||||
import { useAccessTokenCreate } from '../hooks/useAccessToken'
|
||||
|
||||
const Component = () => {
|
||||
const router = useRouter()
|
||||
const [, copy] = useCopyToClipboard()
|
||||
const [token, setToken] = useState<null | string>(null)
|
||||
const accessTokenCreate = useAccessTokenCreate()
|
||||
const form = useForm<z.infer<typeof AccessTokenCreateInput>>({
|
||||
resolver: zodResolver(AccessTokenCreateInput),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = (values: z.infer<typeof AccessTokenCreateInput>) => {
|
||||
accessTokenCreate.mutate(values, {
|
||||
onSuccess(data) {
|
||||
if (data.token) {
|
||||
setToken(data.token)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
return (
|
||||
<PageWrapper
|
||||
breadcrumbs={[
|
||||
{
|
||||
name: 'Access tokens',
|
||||
to: '/access-tokens',
|
||||
},
|
||||
{
|
||||
name: 'New',
|
||||
to: '/access-tokens/new',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div className="flex w-full gap-5 justify-between items-center">
|
||||
<h1 className="text-3xl">New access token</h1>
|
||||
</div>
|
||||
|
||||
{token && (
|
||||
<div className="flex flex-col gap-3 w-full max-w-screen-md">
|
||||
<h2 className="text-xl">Your token</h2>
|
||||
<pre className="bg-muted text-xl p-3 rounded text-center flex justify-between items-center">
|
||||
{token}
|
||||
<Button
|
||||
onClick={() => copy(token)}
|
||||
size={'icon'}
|
||||
variant={'outline'}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</pre>
|
||||
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Reminder</AlertTitle>
|
||||
<AlertDescription>
|
||||
Your token is only visible this time. Please notify it securely.
|
||||
If you forget it, you have to create a new token.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="flex items-center justify-end">
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
type="button"
|
||||
onClick={() => router.history.back()}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!token && (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8 max-w-screen-md"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="CLI Token" {...field} autoFocus />
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="w-full flex items-center justify-end gap-5">
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
type="button"
|
||||
onClick={() => router.history.back()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">Create</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export const Route = createLazyFileRoute('/access-tokens/new')({
|
||||
component: Component,
|
||||
})
|
@ -26,47 +26,45 @@ const Component = () => {
|
||||
|
||||
return (
|
||||
<PageWrapper breadcrumbs={[{ name: 'Changelog', to: '/changelog' }]}>
|
||||
<>
|
||||
<div className="flex flex-col gap-5">
|
||||
<h1 className="text-3xl">Changelog</h1>
|
||||
<div className="flex flex-col gap-5">
|
||||
<h1 className="text-3xl">Changelog</h1>
|
||||
|
||||
<div className="flex gap-10 w-full">
|
||||
{!isPending &&
|
||||
data &&
|
||||
data.map((changelog) => {
|
||||
return (
|
||||
<Link
|
||||
to="/changelog/$id"
|
||||
params={{ id: changelog.id }}
|
||||
key={changelog.id}
|
||||
>
|
||||
<Card className="max-w-56 min-w-56 w-full h-36 hover:border-emerald-700 transition">
|
||||
<CardHeader className="flex items-center justify-center">
|
||||
<CardTitle>{changelog.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-center flex-col">
|
||||
<span>Versions: {changelog.computed.versionCount}</span>
|
||||
<div className="flex gap-10 w-full">
|
||||
{!isPending &&
|
||||
data &&
|
||||
data.map((changelog) => {
|
||||
return (
|
||||
<Link
|
||||
to="/changelog/$id"
|
||||
params={{ id: changelog.id }}
|
||||
key={changelog.id}
|
||||
>
|
||||
<Card className="max-w-56 min-w-56 w-full h-36 hover:border-emerald-700 transition">
|
||||
<CardHeader className="flex items-center justify-center">
|
||||
<CardTitle>{changelog.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-center flex-col">
|
||||
<span>Versions: {changelog.computed.versionCount}</span>
|
||||
|
||||
<span>Commits: {changelog.computed.commitCount}</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
<span>Commits: {changelog.computed.commitCount}</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
|
||||
<Link to="/changelog/create">
|
||||
<Card className="max-w-56 min-w-56 w-full h-36 hover:border-emerald-700 transition">
|
||||
<CardHeader className="flex items-center justify-center">
|
||||
<CardTitle>New Changelog</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-center">
|
||||
<PlusCircleIcon strokeWidth={1.5} className="w-10 h-10" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
<Link to="/changelog/create">
|
||||
<Card className="max-w-56 min-w-56 w-full h-36 hover:border-emerald-700 transition">
|
||||
<CardHeader className="flex items-center justify-center">
|
||||
<CardTitle>New Changelog</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-center">
|
||||
<PlusCircleIcon strokeWidth={1.5} className="w-10 h-10" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
{"root":["./src/main.tsx","./src/routeTree.gen.ts","./src/vite-env.d.ts","./src/components/Layout.tsx","./src/components/Navigation.tsx","./src/components/NavigationMobile.tsx","./src/components/PageWrapper.tsx","./src/components/Sidebar.tsx","./src/components/SidebarChangelog.tsx","./src/components/SidebarPage.tsx","./src/components/SidebarUser.tsx","./src/components/Changelog/Delete.tsx","./src/components/Changelog/VersionDelete.tsx","./src/components/Changelog/VersionStatus.tsx","./src/components/Page/Delete.tsx","./src/hooks/useChangelog.ts","./src/hooks/usePage.ts","./src/routes/__root.tsx","./src/routes/changelog.$id.edit.lazy.tsx","./src/routes/changelog.$id.index.lazy.tsx","./src/routes/changelog.$id.lazy.tsx","./src/routes/changelog.$id.version.$versionId.tsx","./src/routes/changelog.$id.versionCreate.lazy.tsx","./src/routes/changelog.create.lazy.tsx","./src/routes/changelog.index.lazy.tsx","./src/routes/index.lazy.tsx","./src/routes/page.$id.edit.lazy.tsx","./src/routes/page.$id.index.lazy.tsx","./src/routes/page.$id.lazy.tsx","./src/routes/page.create.lazy.tsx","./src/routes/page.index.lazy.tsx","./src/routes/user/index.lazy.tsx","./src/utils/navigation-routes.ts","./src/utils/queryFetch.ts"],"version":"5.6.2"}
|
||||
{"root":["./src/main.tsx","./src/routeTree.gen.ts","./src/vite-env.d.ts","./src/components/Layout.tsx","./src/components/Navigation.tsx","./src/components/NavigationMobile.tsx","./src/components/PageWrapper.tsx","./src/components/Sidebar.tsx","./src/components/SidebarChangelog.tsx","./src/components/SidebarPage.tsx","./src/components/SidebarUser.tsx","./src/components/Changelog/CommitList.tsx","./src/components/Changelog/Delete.tsx","./src/components/Changelog/VersionDelete.tsx","./src/components/Changelog/VersionList.tsx","./src/components/Changelog/VersionStatus.tsx","./src/components/Changelog/Version/Create/Step01.tsx","./src/components/Changelog/Version/Create/index.tsx","./src/components/Page/Delete.tsx","./src/hooks/useChangelog.ts","./src/hooks/usePage.ts","./src/routes/__root.tsx","./src/routes/changelog.$id.edit.lazy.tsx","./src/routes/changelog.$id.index.lazy.tsx","./src/routes/changelog.$id.lazy.tsx","./src/routes/changelog.$id.version.$versionId.tsx","./src/routes/changelog.$id.versionCreate.lazy.tsx","./src/routes/changelog.create.lazy.tsx","./src/routes/changelog.index.lazy.tsx","./src/routes/index.lazy.tsx","./src/routes/page.$id.edit.lazy.tsx","./src/routes/page.$id.index.lazy.tsx","./src/routes/page.$id.lazy.tsx","./src/routes/page.create.lazy.tsx","./src/routes/page.index.lazy.tsx","./src/routes/user/index.lazy.tsx","./src/utils/navigation-routes.ts","./src/utils/queryFetch.ts"],"version":"5.6.3"}
|
12
packages/schema/src/access-token/base.ts
Normal file
12
packages/schema/src/access-token/base.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { z } from '@hono/zod-openapi'
|
||||
|
||||
export const AccessTokenOutput = z
|
||||
.object({
|
||||
id: z.string().openapi({
|
||||
example: 'user_2metCkqOhUhHN1jEhLyh8wMODu7',
|
||||
}),
|
||||
token: z.string().optional(),
|
||||
name: z.string(),
|
||||
lastUsedOn: z.string().or(z.date()),
|
||||
})
|
||||
.openapi('Access Token')
|
7
packages/schema/src/access-token/create.ts
Normal file
7
packages/schema/src/access-token/create.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { z } from '@hono/zod-openapi'
|
||||
|
||||
export const AccessTokenCreateInput = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
})
|
||||
.openapi('Access Token')
|
3
packages/schema/src/access-token/index.ts
Normal file
3
packages/schema/src/access-token/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './base'
|
||||
export * from './create'
|
||||
export * from './list'
|
4
packages/schema/src/access-token/list.ts
Normal file
4
packages/schema/src/access-token/list.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { z } from '@hono/zod-openapi'
|
||||
import { AccessTokenOutput } from './base'
|
||||
|
||||
export const AccessTokenListOutput = z.array(AccessTokenOutput)
|
@ -4,3 +4,4 @@ export * from './changelog'
|
||||
export * from './version'
|
||||
export * from './page'
|
||||
export * from './commit'
|
||||
export * from './access-token'
|
||||
|
@ -29,3 +29,4 @@ export * from './breadcrumb'
|
||||
export * from './command'
|
||||
export * from './dialog'
|
||||
export * from './scroll-area'
|
||||
export * from './table'
|
||||
|
117
packages/ui/src/table.tsx
Normal file
117
packages/ui/src/table.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './lib/cn'
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn('w-full caption-bottom text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = 'Table'
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = 'TableHeader'
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn('[&_tr:last-child]:border-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = 'TableBody'
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = 'TableFooter'
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = 'TableRow'
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = 'TableHead'
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = 'TableCell'
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn('mt-4 text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = 'TableCaption'
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
Loading…
Reference in New Issue
Block a user