Skip to content

Commit

Permalink
feat: user access control for posts
Browse files Browse the repository at this point in the history
  • Loading branch information
Derlys committed Jan 21, 2024
1 parent d9be285 commit 1a363ec
Show file tree
Hide file tree
Showing 14 changed files with 262 additions and 8 deletions.
12 changes: 12 additions & 0 deletions libs/api/post/data-access/src/lib/api-user-post.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ export class ApiUserPostService {
}

async deletePost(userId: string, postId: string) {
const found = await this.core.data.post.findFirst({
where: { id: postId, authorId: userId },
})
if (!found) {
throw new Error('Post not found')
}
const deleted = await this.core.data.post.delete({ where: { id: postId } })
return !!deleted
}
Expand All @@ -35,6 +41,12 @@ export class ApiUserPostService {
}

async updatePost(userId: string, postId: string, input: UserUpdatePostInput) {
const found = await this.core.data.post.findFirst({
where: { id: postId, authorId: userId },
})
if (!found) {
throw new Error('Post not found')
}
return this.core.data.post.update({ where: { id: postId }, data: input })
}
}
6 changes: 2 additions & 4 deletions libs/web/dashboard/feature/src/lib/dashboard-feature.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
import { useAuth } from '@connectamind/web-auth-data-access'
import { UiContainer, UiDashboardGrid, UiDashboardItem, UiDebug } from '@pubkey-ui/core'
import { IconCurrencySolana, IconSettings, IconUser } from '@tabler/icons-react'
import { useUserFindManyPost } from '@connectamind/web-post-data-access'
import { IconBook, IconCurrencySolana, IconSettings, IconUser } from '@tabler/icons-react'

const links: UiDashboardItem[] = [
// User Dashboard Links
{ label: 'Profile', icon: IconUser, to: '/profile' },
{ label: 'Settings', icon: IconSettings, to: '/settings' },
{ label: 'Solana', icon: IconCurrencySolana, to: '/solana' },
{ label: 'Posts', icon: IconBook, to: '/posts' },
]

export default function DashboardFeature() {
const { items } = useUserFindManyPost()
const { user } = useAuth()

if (!user) return null

return (
<UiContainer>
<UiDashboardGrid links={links} />
<UiDebug data={items} open />
</UiContainer>
)
}
1 change: 1 addition & 0 deletions libs/web/post/feature/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { lazy } from 'react'

export const AdminPostFeature = lazy(() => import('./lib/admin-post-feature'))
export const UserPostFeature = lazy(() => import('./lib/user-post-feature'))
32 changes: 32 additions & 0 deletions libs/web/post/feature/src/lib/user-post-create.feature.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { UserCreatePostInput } from '@connectamind/sdk'
import { useUserFindManyPost } from '@connectamind/web-post-data-access'
import { UserPostUiCreateForm } from '@connectamind/web-post-ui'
import { toastError, UiBack, UiCard, UiPage } from '@pubkey-ui/core'
import { useNavigate } from 'react-router-dom'

export function UserPostCreateFeature() {
const navigate = useNavigate()
const { createPost } = useUserFindManyPost()

async function submit(input: UserCreatePostInput) {
return createPost(input)
.then((res) => {
if (res) {
navigate(`/posts/${res?.id}`)
}
})
.then(() => true)
.catch((err) => {
toastError(err.message)
return false
})
}

return (
<UiPage leftAction={<UiBack />} title="Create Post">
<UiCard>
<UserPostUiCreateForm submit={submit} />
</UiCard>
</UiPage>
)
}
32 changes: 32 additions & 0 deletions libs/web/post/feature/src/lib/user-post-detail.feature.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Group } from '@mantine/core'
import { UiBack, UiDebug, UiDebugModal, UiError, UiLoader, UiPage } from '@pubkey-ui/core'
import { useUserFindOnePost } from '@connectamind/web-post-data-access'
import { useParams } from 'react-router-dom'
import { UserPostUiUpdateForm } from '@connectamind/web-post-ui'

export function UserPostDetailFeature() {
const { postId } = useParams<{ postId: string }>() as { postId: string }
const { item, query, updatePost } = useUserFindOnePost({ postId })

if (query.isLoading) {
return <UiLoader />
}
if (!item) {
return <UiError message="Post not found." />
}

return (
<UiPage
title={<Group>{item.title}</Group>}
leftAction={<UiBack />}
rightAction={
<Group>
<UiDebugModal data={item} />
</Group>
}
>
<UiDebug data={item} open />
<UserPostUiUpdateForm submit={updatePost} post={item} />
</UiPage>
)
}
15 changes: 15 additions & 0 deletions libs/web/post/feature/src/lib/user-post-feature.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useRoutes } from 'react-router-dom'
import { UserPostDetailFeature } from './user-post-detail.feature'
import { UserPostCreateFeature } from './user-post-create.feature'
import { UserPostListFeature } from './user-post-list.feature'

export default function UserPostRoutes() {
return useRoutes([
{ path: '', element: <UserPostListFeature /> },
{
path: 'create',
element: <UserPostCreateFeature />,
},
{ path: ':postId/*', element: <UserPostDetailFeature /> },
])
}
48 changes: 48 additions & 0 deletions libs/web/post/feature/src/lib/user-post-list.feature.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Button, Group } from '@mantine/core'
import { UiPageLimit, UiSearchField } from '@connectamind/web-ui-core'
import { useUserFindManyPost } from '@connectamind/web-post-data-access'
import { UserPostUiTable } from '@connectamind/web-post-ui'
import { UiBack, UiDebugModal, UiInfo, UiLoader, UiPage } from '@pubkey-ui/core'
import { Link } from 'react-router-dom'

export function UserPostListFeature() {
const { deletePost, items, pagination, query, setSearch } = useUserFindManyPost()

return (
<UiPage
title="Posts"
leftAction={<UiBack />}
rightAction={
<Group>
<UiDebugModal data={items} />
<Button component={Link} to="create">
Create
</Button>
</Group>
}
>
<Group>
<UiSearchField placeholder="Search post" setSearch={setSearch} />
<UiPageLimit limit={pagination.limit} setLimit={pagination.setLimit} setPage={pagination.setPage} />
</Group>

{query.isLoading ? (
<UiLoader />
) : items?.length ? (
<UserPostUiTable
deletePost={(post) => {
if (!window.confirm('Are you sure?')) return
return deletePost(post.id)
}}
posts={items}
page={pagination.page}
totalRecords={pagination.total}
recordsPerPage={pagination.limit}
onPageChange={(page) => void pagination.setPage(page)}
/>
) : (
<UiInfo message="No posts found" />
)}
</UiPage>
)
}
3 changes: 3 additions & 0 deletions libs/web/post/ui/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export * from './lib/admin-post-ui-create-form'
export * from './lib/admin-post-ui-table'
export * from './lib/admin-post-ui-update-form'
export * from './lib/user-post-ui-create-form'
export * from './lib/user-post-ui-table'
export * from './lib/user-post-ui-update-form'
22 changes: 22 additions & 0 deletions libs/web/post/ui/src/lib/user-post-ui-create-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Button, Group } from '@mantine/core'
import { UserCreatePostInput } from '@connectamind/sdk'
import { formFieldText, formFieldTextarea, UiForm, UiFormField } from '@pubkey-ui/core'

export function UserPostUiCreateForm({ submit }: { submit: (res: UserCreatePostInput) => Promise<boolean> }) {
const model: UserCreatePostInput = {
title: '',
content: '',
}

const fields: UiFormField<UserCreatePostInput>[] = [
formFieldText('title', { label: 'Title', required: true }),
formFieldTextarea('content', { label: 'Content', required: true }),
]
return (
<UiForm model={model} fields={fields} submit={(res) => submit(res as UserCreatePostInput)}>
<Group justify="right">
<Button type="submit">Create</Button>
</Group>
</UiForm>
)
}
62 changes: 62 additions & 0 deletions libs/web/post/ui/src/lib/user-post-ui-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { ActionIcon, Anchor, Group, ScrollArea } from '@mantine/core'
import { Post } from '@connectamind/sdk'
import { IconPencil, IconTrash } from '@tabler/icons-react'
import { DataTable, DataTableProps } from 'mantine-datatable'
import { Link } from 'react-router-dom'

export function UserPostUiTable({
deletePost,
posts = [],
onPageChange,
page,
recordsPerPage,
totalRecords,
}: {
deletePost: (post: Post) => void
posts: Post[]
page: DataTableProps['page']
totalRecords: DataTableProps['totalRecords']
recordsPerPage: DataTableProps['recordsPerPage']
onPageChange: (page: number) => void
}) {
return (
<ScrollArea>
<DataTable
borderRadius="sm"
withTableBorder
shadow="xs"
onPageChange={onPageChange}
page={page ?? 1}
recordsPerPage={recordsPerPage ?? 10}
totalRecords={totalRecords ?? 1}
columns={[
{
accessor: 'title',
render: (item) => (
<Anchor component={Link} to={`/posts/${item.id}`} size="sm" fw={500}>
{item.title}
</Anchor>
),
},
{ accessor: 'author.username' },
{
accessor: 'actions',
title: 'Actions',
textAlign: 'right',
render: (item) => (
<Group gap="xs" justify="right">
<ActionIcon color="brand" variant="light" size="sm" component={Link} to={`/posts/${item.id}/settings`}>
<IconPencil size={16} />
</ActionIcon>
<ActionIcon color="red" variant="light" size="sm" onClick={() => deletePost(item)}>
<IconTrash size={16} />
</ActionIcon>
</Group>
),
},
]}
records={posts}
/>
</ScrollArea>
)
}
28 changes: 28 additions & 0 deletions libs/web/post/ui/src/lib/user-post-ui-update-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Button, Group } from '@mantine/core'
import { UserUpdatePostInput, Post } from '@connectamind/sdk'
import { formFieldText, formFieldTextarea, UiForm, UiFormField } from '@pubkey-ui/core'

export function UserPostUiUpdateForm({
submit,
post,
}: {
submit: (res: UserUpdatePostInput) => Promise<boolean>
post: Post
}) {
const model: UserUpdatePostInput = {
title: post.title ?? '',
content: post.content ?? '',
}

const fields: UiFormField<UserUpdatePostInput>[] = [
formFieldText('title', { label: 'Title' }),
formFieldTextarea('content', { label: 'Content' }),
]
return (
<UiForm model={model} fields={fields} submit={(res) => submit(res as UserUpdatePostInput)}>
<Group justify="right">
<Button type="submit">Save</Button>
</Group>
</UiForm>
)
}
6 changes: 2 additions & 4 deletions libs/web/shell/feature/src/lib/shell-admin-routes.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { DevAdminRoutes } from '@connectamind/web-dev-feature'
import { AdminUserFeature } from '@connectamind/web-user-feature'
import { UiContainer, UiDashboardGrid, UiDashboardItem, UiNotFound } from '@pubkey-ui/core'
import { IconUsers } from '@tabler/icons-react'
import { IconBook, IconUsers } from '@tabler/icons-react'
import { Navigate, RouteObject, useRoutes } from 'react-router-dom'
import { AdminPostFeature } from '@connectamind/web-post-feature'
import { AdminPriceFeature } from '@connectamind/web-price-feature'

const links: UiDashboardItem[] = [
// Admin Dashboard Links are added by the web-feature generator
{ label: 'Posts', icon: IconBook, to: '/admin/posts' },
{ label: 'Users', icon: IconUsers, to: '/admin/users' },
{ label: 'Posts', icon: IconUsers, to: '/admin/posts' },
{ label: 'Prices', icon: IconUsers, to: '/admin/prices' },
]

const routes: RouteObject[] = [
Expand Down
1 change: 1 addition & 0 deletions libs/web/shell/feature/src/lib/shell-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export function ShellLayout({ children }: { children: ReactNode }) {
toggle={toggle}
links={[
{ link: '/dashboard', label: 'Dashboard' },
{ link: '/posts', label: 'Posts' },
{ link: '/solana', label: 'Solana' },
]}
profile={
Expand Down
2 changes: 2 additions & 0 deletions libs/web/shell/feature/src/lib/shell-routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { UiNotFound } from '@pubkey-ui/core'
import { lazy } from 'react'
import { Navigate } from 'react-router-dom'
import { useGuardedRoutes } from './use-guarded-routes'
import { UserPostFeature } from '@connectamind/web-post-feature'

export const LazyAdminFeature = lazy(() => import('./shell-admin-routes'))

Expand All @@ -23,6 +24,7 @@ export function ShellRoutes() {
layout: [
// Here you can add routes that are part of the main layout
{ path: '/dashboard', element: <DashboardFeature /> },
{ path: '/posts/*', element: <UserPostFeature /> },
{ path: '/profile/*', element: <UserFeature /> },
{ path: '/settings/*', element: <SettingsFeature /> },
{ path: '/solana/*', element: <SolanaFeature /> },
Expand Down

0 comments on commit 1a363ec

Please sign in to comment.