diff --git a/apps/api/src/access-token/create.ts b/apps/api/src/access-token/create.ts new file mode 100644 index 0000000..7fa9b3a --- /dev/null +++ b/apps/api/src/access-token/create.ts @@ -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 = 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) + }) +} diff --git a/apps/api/src/access-token/delete.ts b/apps/api/src/access-token/delete.ts new file mode 100644 index 0000000..0d84f01 --- /dev/null +++ b/apps/api/src/access-token/delete.ts @@ -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) + }) +} diff --git a/apps/api/src/access-token/index.ts b/apps/api/src/access-token/index.ts new file mode 100644 index 0000000..2330372 --- /dev/null +++ b/apps/api/src/access-token/index.ts @@ -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) diff --git a/apps/api/src/access-token/list.ts b/apps/api/src/access-token/list.ts new file mode 100644 index 0000000..6a6ce18 --- /dev/null +++ b/apps/api/src/access-token/list.ts @@ -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) + }) +} diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index e2d2eb4..188dac3 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -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', diff --git a/apps/api/src/utils/authentication.ts b/apps/api/src/utils/authentication.ts index 0b8bba0..9b8c9c6 100644 --- a/apps/api/src/utils/authentication.ts +++ b/apps/api/src/utils/authentication.ts @@ -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() diff --git a/apps/app/package.json b/apps/app/package.json index 3134f16..47bb5ca 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -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", diff --git a/apps/app/src/components/AccessToken/Delete.tsx b/apps/app/src/components/AccessToken/Delete.tsx new file mode 100644 index 0000000..cffa46a --- /dev/null +++ b/apps/app/src/components/AccessToken/Delete.tsx @@ -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 ( + + + + + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your + changelog and remove your data from our servers. + + + + setIsOpen(false)}> + Cancel + + + + + + + + +

Delete access token

+
+
+ ) +} diff --git a/apps/app/src/components/AccessToken/Table/Columns.tsx b/apps/app/src/components/AccessToken/Table/Columns.tsx new file mode 100644 index 0000000..f61303c --- /dev/null +++ b/apps/app/src/components/AccessToken/Table/Columns.tsx @@ -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 +>[] = [ + { + 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) => , + }, +] diff --git a/apps/app/src/components/AccessToken/Table/DataTable.tsx b/apps/app/src/components/AccessToken/Table/DataTable.tsx new file mode 100644 index 0000000..be1a44a --- /dev/null +++ b/apps/app/src/components/AccessToken/Table/DataTable.tsx @@ -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 { + columns: ColumnDef[] + data: TData[] +} + +export function DataTable({ + columns, + data, +}: DataTableProps) { + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+ ) +} diff --git a/apps/app/src/components/Navigation.tsx b/apps/app/src/components/Navigation.tsx deleted file mode 100644 index 33619e7..0000000 --- a/apps/app/src/components/Navigation.tsx +++ /dev/null @@ -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 ( -
-
-
- - boring.tools - - -
-
- -
-
- - - More Infos - - If you want more information about boring.tools, visit our - documentation! - - - - - - - - -
-
-
- ) -} diff --git a/apps/app/src/components/NavigationMobile.tsx b/apps/app/src/components/NavigationMobile.tsx deleted file mode 100644 index a4496dc..0000000 --- a/apps/app/src/components/NavigationMobile.tsx +++ /dev/null @@ -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 ( - - - - - - -
- - - More Infos - - If you want more information about boring.tools, visit our - documentation! - - - - - - - - -
-
-
- ) -} diff --git a/apps/app/src/components/Sidebar.tsx b/apps/app/src/components/Sidebar.tsx index fa580d0..920381c 100644 --- a/apps/app/src/components/Sidebar.tsx +++ b/apps/app/src/components/Sidebar.tsx @@ -34,10 +34,8 @@ export function Sidebar() { - - - - + + diff --git a/apps/app/src/components/SidebarChangelog.tsx b/apps/app/src/components/SidebarChangelog.tsx index f6824a9..b0b06d4 100644 --- a/apps/app/src/components/SidebarChangelog.tsx +++ b/apps/app/src/components/SidebarChangelog.tsx @@ -2,6 +2,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger, + SidebarMenu, SidebarMenuAction, SidebarMenuButton, SidebarMenuItem, @@ -28,60 +29,62 @@ export const SidebarChangelog = () => { }, [location, setValue]) return ( - setValue(!value)}> - - - - - Changelog - - + + setValue(!value)}> + + + + + Changelog + + - - - - Toggle - - - - - {!error && - data?.map((changelog) => ( - - - - {changelog.title} - - - - ))} + + + + Toggle + + + + + {!error && + data?.map((changelog) => ( + + + + {changelog.title} + + + + ))} - - - - - - New changelog - - - - - - - - + + + + + + New changelog + + + + + + + + + ) } diff --git a/apps/app/src/components/SidebarPage.tsx b/apps/app/src/components/SidebarPage.tsx index 3d0d714..92beb2f 100644 --- a/apps/app/src/components/SidebarPage.tsx +++ b/apps/app/src/components/SidebarPage.tsx @@ -2,6 +2,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger, + SidebarMenu, SidebarMenuAction, SidebarMenuButton, SidebarMenuItem, @@ -28,57 +29,59 @@ export const SidebarPage = () => { }, [location, setValue]) return ( - setValue(!value)}> - - - - - Page - - + + setValue(!value)}> + + + + + Page + + - - - - Toggle - - - - - {!error && - data?.map((page) => ( - - - - {page.title} - - - - ))} + + + + Toggle + + + + + {!error && + data?.map((page) => ( + + + + {page.title} + + + + ))} - - - - - - New page - - - - - - - - + + + + + + New page + + + + + + + + + ) } diff --git a/apps/app/src/components/SidebarSettings.tsx b/apps/app/src/components/SidebarSettings.tsx new file mode 100644 index 0000000..0e781dd --- /dev/null +++ b/apps/app/src/components/SidebarSettings.tsx @@ -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 ( + + + + + + + + CN + +
+ {user?.fullName} + + {user?.primaryEmailAddress?.emailAddress} + +
+ +
+
+ + +
+ + + + {user?.firstName?.substring(0, 1)} + {user?.lastName?.substring(0, 1)} + + +
+ + {user?.fullName} + + + {user?.primaryEmailAddress?.emailAddress} + +
+
+
+ + + + Profile + + + + + + + + +
+
+
+
+ ) +} diff --git a/apps/app/src/components/SidebarUser.tsx b/apps/app/src/components/SidebarUser.tsx index 0e781dd..49086a2 100644 --- a/apps/app/src/components/SidebarUser.tsx +++ b/apps/app/src/components/SidebarUser.tsx @@ -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 ( + + + + + Access Tokens + + + + diff --git a/apps/app/src/hooks/useAccessToken.ts b/apps/app/src/hooks/useAccessToken.ts new file mode 100644 index 0000000..d665c4d --- /dev/null +++ b/apps/app/src/hooks/useAccessToken.ts @@ -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 +type AccessTokenList = z.infer +type AccessTokenCreate = z.infer + +export const useAccessTokenList = () => { + const { getToken } = useAuth() + return useQuery({ + queryKey: ['accessTokenList'], + queryFn: async (): Promise => + 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> => + 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> => + await queryFetch({ + path: `access-token/${id}`, + method: 'delete', + token: await getToken(), + }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ['accessTokenList'], + }) + }, + }) +} diff --git a/apps/app/src/routeTree.gen.ts b/apps/app/src/routeTree.gen.ts index c0e14ce..f3f2237 100644 --- a/apps/app/src/routeTree.gen.ts +++ b/apps/app/src/routeTree.gen.ts @@ -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" }, diff --git a/apps/app/src/routes/access-tokens.index.lazy.tsx b/apps/app/src/routes/access-tokens.index.lazy.tsx new file mode 100644 index 0000000..574299d --- /dev/null +++ b/apps/app/src/routes/access-tokens.index.lazy.tsx @@ -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 ( + +
+

Access Tokens

+ + +
+ {data && !isPending && ( + + )} +
+ ) +} + +export const Route = createLazyFileRoute('/access-tokens/')({ + component: Component, +}) diff --git a/apps/app/src/routes/access-tokens.new.lazy.tsx b/apps/app/src/routes/access-tokens.new.lazy.tsx new file mode 100644 index 0000000..7eaa0bc --- /dev/null +++ b/apps/app/src/routes/access-tokens.new.lazy.tsx @@ -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) + const accessTokenCreate = useAccessTokenCreate() + const form = useForm>({ + resolver: zodResolver(AccessTokenCreateInput), + defaultValues: { + name: '', + }, + }) + + const onSubmit = (values: z.infer) => { + accessTokenCreate.mutate(values, { + onSuccess(data) { + if (data.token) { + setToken(data.token) + } + }, + }) + } + return ( + +
+

New access token

+
+ + {token && ( +
+

Your token

+
+            {token}
+            
+          
+ + + + Reminder + + Your token is only visible this time. Please notify it securely. + If you forget it, you have to create a new token. + + +
+ +
+
+ )} + + {!token && ( +
+ + ( + + Name + + + {' '} + + + )} + /> + +
+ + +
+ + + )} +
+ ) +} + +export const Route = createLazyFileRoute('/access-tokens/new')({ + component: Component, +}) diff --git a/apps/app/src/routes/changelog.index.lazy.tsx b/apps/app/src/routes/changelog.index.lazy.tsx index c4713e8..46566e7 100644 --- a/apps/app/src/routes/changelog.index.lazy.tsx +++ b/apps/app/src/routes/changelog.index.lazy.tsx @@ -26,47 +26,45 @@ const Component = () => { return ( - <> -
-

Changelog

+
+

Changelog

-
- {!isPending && - data && - data.map((changelog) => { - return ( - - - - {changelog.title} - - - Versions: {changelog.computed.versionCount} +
+ {!isPending && + data && + data.map((changelog) => { + return ( + + + + {changelog.title} + + + Versions: {changelog.computed.versionCount} - Commits: {changelog.computed.commitCount} - - - - ) - })} + Commits: {changelog.computed.commitCount} + + + + ) + })} - - - - New Changelog - - - - - - -
+ + + + New Changelog + + + + + +
- +
) } diff --git a/apps/app/tsconfig.tsbuildinfo b/apps/app/tsconfig.tsbuildinfo index 7f486b9..09a0137 100644 --- a/apps/app/tsconfig.tsbuildinfo +++ b/apps/app/tsconfig.tsbuildinfo @@ -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"} \ No newline at end of file +{"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"} \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 08eb98c..46b62d5 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/schema/src/access-token/base.ts b/packages/schema/src/access-token/base.ts new file mode 100644 index 0000000..4ae0358 --- /dev/null +++ b/packages/schema/src/access-token/base.ts @@ -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') diff --git a/packages/schema/src/access-token/create.ts b/packages/schema/src/access-token/create.ts new file mode 100644 index 0000000..7fc09a1 --- /dev/null +++ b/packages/schema/src/access-token/create.ts @@ -0,0 +1,7 @@ +import { z } from '@hono/zod-openapi' + +export const AccessTokenCreateInput = z + .object({ + name: z.string(), + }) + .openapi('Access Token') diff --git a/packages/schema/src/access-token/index.ts b/packages/schema/src/access-token/index.ts new file mode 100644 index 0000000..724f2c6 --- /dev/null +++ b/packages/schema/src/access-token/index.ts @@ -0,0 +1,3 @@ +export * from './base' +export * from './create' +export * from './list' diff --git a/packages/schema/src/access-token/list.ts b/packages/schema/src/access-token/list.ts new file mode 100644 index 0000000..bf778eb --- /dev/null +++ b/packages/schema/src/access-token/list.ts @@ -0,0 +1,4 @@ +import { z } from '@hono/zod-openapi' +import { AccessTokenOutput } from './base' + +export const AccessTokenListOutput = z.array(AccessTokenOutput) diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index dcd806b..f95f5ba 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -4,3 +4,4 @@ export * from './changelog' export * from './version' export * from './page' export * from './commit' +export * from './access-token' diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 6c573fa..b34cc3b 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -29,3 +29,4 @@ export * from './breadcrumb' export * from './command' export * from './dialog' export * from './scroll-area' +export * from './table' diff --git a/packages/ui/src/table.tsx b/packages/ui/src/table.tsx new file mode 100644 index 0000000..f6ec571 --- /dev/null +++ b/packages/ui/src/table.tsx @@ -0,0 +1,117 @@ +import * as React from 'react' + +import { cn } from './lib/cn' + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = 'Table' + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = 'TableHeader' + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = 'TableBody' + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0', + className, + )} + {...props} + /> +)) +TableFooter.displayName = 'TableFooter' + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = 'TableRow' + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableHead.displayName = 'TableHead' + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableCell.displayName = 'TableCell' + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = 'TableCaption' + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}