From 38fb9fa362da02ee3feddc0e4c03bd14a9bc0a66 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Sat, 10 Aug 2024 20:00:24 +0800 Subject: [PATCH] fix: fix some permission issue --- apps/backend/src/modules/auth/auth.ts | 10 +- apps/backend/src/modules/auth/oauth/github.ts | 2 +- apps/backend/src/modules/auth/oauth/google.ts | 2 +- .../openapi/{openapi.tsx => openapi.ts} | 14 +- .../backend/src/modules/space/space.module.ts | 44 ++++--- .../components/blocks/base/base-detail.svelte | 48 +++---- .../bulk-update-records-button.svelte | 31 +++-- .../create-field/create-field-button.svelte | 37 +++--- .../components/blocks/field/field-menu.svelte | 55 ++++---- .../filters-editor/filters-editor.svelte | 79 +++++++----- .../blocks/forms/create-form-button.svelte | 49 +++---- .../blocks/forms/empty-forms.svelte | 13 +- .../grid-view/grid-view-action-header.svelte | 3 +- .../blocks/grid-view/grid-view-empty.svelte | 7 +- .../blocks/grid-view/grid-view-header.svelte | 29 +++-- .../blocks/member/get-role-bg-color.ts | 2 + .../blocks/rls/create-rls-button.svelte | 11 +- .../components/blocks/rls/empty-rls.svelte | 12 +- .../components/blocks/role/role-picker.svelte | 5 +- .../blocks/share/share-button.svelte | 115 +++++++++-------- .../blocks/space/space-setting.svelte | 22 +++- .../blocks/table-header/table-header.svelte | 120 ++++++++++-------- .../blocks/tables-nav/tables-nav.svelte | 21 +-- .../view-color-editor.svelte | 8 +- .../blocks/view-fields/view-fields.svelte | 79 ++++++------ .../view-filter-editor.svelte | 8 +- .../blocks/view-sort/view-sort.svelte | 34 +++-- .../blocks/view/create-view-button.svelte | 49 +++---- .../blocks/webhook/empty-webhook.svelte | 12 +- .../src/lib/store/space-member.store.ts | 7 +- .../src/routes/(auth)/login/+page.svelte | 39 +++++- .../src/routes/(auth)/signup/+page.svelte | 33 ++++- drizzle.config.ts | 2 +- packages/authz/src/rbac/permission.ts | 21 ++- .../authz/src/space-member/space-action.ts | 5 + .../authz/src/space-member/space-member.ts | 4 +- .../src/space-member/space-permission.ts | 51 ++++++++ .../handlers/create-space.command-handler.ts | 2 +- packages/commands/src/create-space.command.ts | 4 +- packages/context/src/server.ts | 4 + packages/i18n/src/i18n/en/index.ts | 1 + packages/i18n/src/i18n/i18n-types.ts | 8 ++ .../src/table/table.filter-visitor.ts | 4 +- .../src/table/table.reference-visitor.ts | 2 + .../persistence/src/table/table.repository.ts | 8 +- packages/space/src/dto/create-space.dto.ts | 2 +- packages/space/src/space.factory.ts | 2 +- packages/space/src/space.service.ts | 18 ++- .../views/view/view-sort/view-sort.vo.ts | 4 + packages/trpc/src/authz.middleware.ts | 13 +- 50 files changed, 720 insertions(+), 435 deletions(-) rename apps/backend/src/modules/openapi/{openapi.tsx => openapi.ts} (98%) diff --git a/apps/backend/src/modules/auth/auth.ts b/apps/backend/src/modules/auth/auth.ts index 62a938f3d..cf63a8da5 100644 --- a/apps/backend/src/modules/auth/auth.ts +++ b/apps/backend/src/modules/auth/auth.ts @@ -140,10 +140,14 @@ export class Auth { emailVerified: user!.emailVerified, avatar: user!.avatar, }) + + const m = member ? { role: member.role, spaceId: member.spaceId } : null + setContextValue("member", m) + return { user, session, - member: member ? { role: member.role, spaceId: member.spaceId } : null, + member: m, } } } @@ -236,7 +240,7 @@ export class Auth { }) .execute() - const space = await this.spaceService.createPersonalSpace(username!) + const space = await this.spaceService.createSpace({ name: username! }) await this.spaceMemberService.createMember(userId, space.id.value, "owner") if (invitation.isSome()) { await this.spaceMemberService.createMember( @@ -309,7 +313,7 @@ export class Auth { let space = await this.spaceService.getSpace({ userId: user.id }) if (space.isSome()) { } else { - space = Some(await this.spaceService.createPersonalSpace(user.username)) + space = Some(await this.spaceService.createSpace({ name: user.username })) await this.spaceMemberService.createMember(user.id, space.unwrap().id.value, "owner") } const session = await this.lucia.createSession(user.id, { space_id: space.unwrap().id.value }) diff --git a/apps/backend/src/modules/auth/oauth/github.ts b/apps/backend/src/modules/auth/oauth/github.ts index a467af8b2..ae9e4e963 100644 --- a/apps/backend/src/modules/auth/oauth/github.ts +++ b/apps/backend/src/modules/auth/oauth/github.ts @@ -170,7 +170,7 @@ export class GithubOAuth { provider_user_id: githubUserResult.id.toString(), }) .execute() - const space = await this.spaceService.createPersonalSpace(githubUserResult.login) + const space = await this.spaceService.createSpace({ name: githubUserResult.login }) await this.spaceMemberService.createMember(userId, space.id.value, "owner") return space diff --git a/apps/backend/src/modules/auth/oauth/google.ts b/apps/backend/src/modules/auth/oauth/google.ts index c2152b048..da2181d16 100644 --- a/apps/backend/src/modules/auth/oauth/google.ts +++ b/apps/backend/src/modules/auth/oauth/google.ts @@ -160,7 +160,7 @@ export class GoogleOAuth { provider_user_id: googleUserResult.id.toString(), }) .execute() - const space = await this.spaceService.createPersonalSpace(googleUserResult.name) + const space = await this.spaceService.createSpace({ name: googleUserResult.name }) await this.spaceMemberService.createMember(userId, space.id.value, "owner") return space diff --git a/apps/backend/src/modules/openapi/openapi.tsx b/apps/backend/src/modules/openapi/openapi.ts similarity index 98% rename from apps/backend/src/modules/openapi/openapi.tsx rename to apps/backend/src/modules/openapi/openapi.ts index 881e40b13..cc5de05f0 100644 --- a/apps/backend/src/modules/openapi/openapi.tsx +++ b/apps/backend/src/modules/openapi/openapi.ts @@ -1,3 +1,5 @@ +import { injectSpaceMemberService, type ISpaceMemberService } from "@undb/authz" +import { type IBaseRepository, injectBaseRepository } from "@undb/base" import { BulkDeleteRecordsCommand, BulkDuplicateRecordsCommand, @@ -8,28 +10,26 @@ import { DuplicateRecordCommand, UpdateRecordCommand, } from "@undb/commands" -import { executionContext, getCurrentUser, getCurrentUserId, setContextValue } from "@undb/context/server" +import { executionContext, getCurrentUserId, setContextValue } from "@undb/context/server" import { CommandBus, QueryBus } from "@undb/cqrs" import { inject, singleton } from "@undb/di" -import { type ICommandBus, None, PaginatedDTO, type IQueryBus, Some } from "@undb/domain" +import { type ICommandBus, type IQueryBus, None, PaginatedDTO, Some } from "@undb/domain" import { createLogger } from "@undb/logger" import { API_TOKEN_HEADER_NAME, createOpenApiSpec, type IApiTokenService, injectApiTokenService } from "@undb/openapi" import { injectQueryBuilder, type IQueryBuilder } from "@undb/persistence" import { GetReadableRecordByIdQuery, GetReadableRecordsQuery } from "@undb/queries" +import { injectSpaceService, type ISpaceService } from "@undb/space" import { injectRecordRepository, injectTableRepository, - withUniqueTable, type IRecordReadableValueDTO, type IRecordRepository, type ITableRepository, + withUniqueTable, } from "@undb/table" +import { injectUserService, type IUserService } from "@undb/user" import Elysia, { t } from "elysia" import { withTransaction } from "../../db" -import { type IBaseRepository, injectBaseRepository } from "@undb/base" -import { injectUserService, type IUserService } from "@undb/user" -import { injectSpaceMemberService, type ISpaceMemberService } from "@undb/authz" -import { injectSpaceService, type ISpaceService } from "@undb/space" @singleton() export class OpenAPI { diff --git a/apps/backend/src/modules/space/space.module.ts b/apps/backend/src/modules/space/space.module.ts index fff676cae..75d062b40 100644 --- a/apps/backend/src/modules/space/space.module.ts +++ b/apps/backend/src/modules/space/space.module.ts @@ -1,5 +1,6 @@ +import { checkPermission } from "@undb/authz" import { DeleteSpaceCommand } from "@undb/commands" -import { getCurrentUserId } from "@undb/context/server" +import { getCurrentMember, getCurrentUserId } from "@undb/context/server" import { CommandBus } from "@undb/cqrs" import { inject, singleton } from "@undb/di" import { injectQueryBuilder, type IQueryBuilder } from "@undb/persistence" @@ -59,25 +60,34 @@ export class SpaceModule { }), }, ) - .delete("/api/space", async (ctx) => { - return withTransaction(this.qb)(async () => { - await this.commandBus.execute(new DeleteSpaceCommand({})) + .delete( + "/api/space", + async (ctx) => { + return withTransaction(this.qb)(async () => { + await this.commandBus.execute(new DeleteSpaceCommand({})) - const userId = getCurrentUserId() + const userId = getCurrentUserId() - await this.lucia.invalidateSession(userId) - const space = (await this.spaceService.getSpace({ userId })).expect("Space not found") + await this.lucia.invalidateSession(userId) + const space = (await this.spaceService.getSpace({ userId })).expect("Space not found") - const updatedSession = await this.lucia.createSession(userId, { space_id: space.id.value }) - const sessionCookie = this.lucia.createSessionCookie(updatedSession.id) - return new Response(null, { - status: 200, - headers: { - Location: "/", - "Set-Cookie": sessionCookie.serialize(), - }, + const updatedSession = await this.lucia.createSession(userId, { space_id: space.id.value }) + const sessionCookie = this.lucia.createSessionCookie(updatedSession.id) + return new Response(null, { + status: 200, + headers: { + Location: "/", + "Set-Cookie": sessionCookie.serialize(), + }, + }) }) - }) - }) + }, + { + beforeHandle(context) { + const role = getCurrentMember().role + checkPermission(role, ["space:delete"]) + }, + }, + ) } } diff --git a/apps/frontend/src/lib/components/blocks/base/base-detail.svelte b/apps/frontend/src/lib/components/blocks/base/base-detail.svelte index 19ad5a050..99fdea032 100644 --- a/apps/frontend/src/lib/components/blocks/base/base-detail.svelte +++ b/apps/frontend/src/lib/components/blocks/base/base-detail.svelte @@ -4,36 +4,38 @@ import { DatabaseIcon, ImportIcon, PlusCircleIcon, PlusIcon } from "lucide-svelte" import * as Table from "$lib/components/ui/table" import { goto } from "$app/navigation" - import { page } from "$app/stores" + import { hasPermission } from "$lib/store/space-member.store" export let base: GetBaseQuery$result["base"]
-
- - + -
+ Import Table + + + {/if}

Tables

diff --git a/apps/frontend/src/lib/components/blocks/bulk-update-records/bulk-update-records-button.svelte b/apps/frontend/src/lib/components/blocks/bulk-update-records/bulk-update-records-button.svelte index dfa807251..e7db0fb84 100644 --- a/apps/frontend/src/lib/components/blocks/bulk-update-records/bulk-update-records-button.svelte +++ b/apps/frontend/src/lib/components/blocks/bulk-update-records/bulk-update-records-button.svelte @@ -3,22 +3,25 @@ import * as Sheet from "$lib/components/ui/sheet" import { PencilIcon } from "lucide-svelte" import BulkUpdateRecords from "./bulk-update-records.svelte" + import { hasPermission } from "$lib/store/space-member.store" let open = false - - - - - - - Bulk Update Records - +{#if $hasPermission("record:update")} + + + + + + + Bulk Update Records + - (open = false)} /> - - + (open = false)} /> + + +{/if} diff --git a/apps/frontend/src/lib/components/blocks/create-field/create-field-button.svelte b/apps/frontend/src/lib/components/blocks/create-field/create-field-button.svelte index ae9c331b8..369e558ca 100644 --- a/apps/frontend/src/lib/components/blocks/create-field/create-field-button.svelte +++ b/apps/frontend/src/lib/components/blocks/create-field/create-field-button.svelte @@ -3,24 +3,27 @@ import { BetweenVerticalStartIcon } from "lucide-svelte" import * as Popover from "$lib/components/ui/popover" import CreateField from "./create-field.svelte" + import { hasPermission } from "$lib/store/space-member.store" let open = false - - - - - - { - open = false - }} - /> - - +{#if $hasPermission("field:create")} + + + + + + { + open = false + }} + /> + + +{/if} diff --git a/apps/frontend/src/lib/components/blocks/field/field-menu.svelte b/apps/frontend/src/lib/components/blocks/field/field-menu.svelte index 0e7e8e850..b5532bf95 100644 --- a/apps/frontend/src/lib/components/blocks/field/field-menu.svelte +++ b/apps/frontend/src/lib/components/blocks/field/field-menu.svelte @@ -23,6 +23,7 @@ import { GetForeignTableStore, GetRollupForeignTablesStore } from "$houdini" import * as Alert from "$lib/components/ui/alert" import { preferences } from "$lib/store/persisted.store" + import { hasPermission } from "$lib/store/space-member.store" export let field: Field const table = getTable() @@ -98,26 +99,30 @@ {/if} {/if} - + {#if $hasPermission("field:update")} + + {/if} {#if !field.isSystem} - + {#if $hasPermission("field:create")} + + {/if} @@ -155,14 +160,16 @@ - + {#if $hasPermission("field:delete")} + + {/if} diff --git a/apps/frontend/src/lib/components/blocks/filters-editor/filters-editor.svelte b/apps/frontend/src/lib/components/blocks/filters-editor/filters-editor.svelte index 4d9ef9d6e..230b304b4 100644 --- a/apps/frontend/src/lib/components/blocks/filters-editor/filters-editor.svelte +++ b/apps/frontend/src/lib/components/blocks/filters-editor/filters-editor.svelte @@ -19,6 +19,7 @@ import FieldFilterControl from "./field-filter-control.svelte" import autoAnimate from "@formkit/auto-animate" import { writable } from "svelte/store" + import { hasPermission } from "$lib/store/space-member.store" interface IField { id: string @@ -133,12 +134,14 @@
- - + {#if $hasPermission("table:update")} + + + {/if}
{:else if isMaybeGroup(child)} @@ -151,18 +154,20 @@ />
- - + {#if $hasPermission("table:update")} + + + {/if}
@@ -173,26 +178,30 @@ {/if}
- - {#if !disableGroup} - {#if level < 3} - + {#if $hasPermission("table:update")} + + {#if !disableGroup} + {#if level < 3} + + {/if} {/if} {/if}
- + {#if $hasPermission("table:update")} + + {/if}
diff --git a/apps/frontend/src/lib/components/blocks/forms/create-form-button.svelte b/apps/frontend/src/lib/components/blocks/forms/create-form-button.svelte index 74392fb25..065b26f16 100644 --- a/apps/frontend/src/lib/components/blocks/forms/create-form-button.svelte +++ b/apps/frontend/src/lib/components/blocks/forms/create-form-button.svelte @@ -13,6 +13,7 @@ import { zodClient } from "sveltekit-superforms/adapters" import { Input } from "$lib/components/ui/input" import { cn } from "$lib/utils" + import { hasPermission } from "$lib/store/space-member.store" let open = false @@ -49,26 +50,28 @@ const { enhance, form: formData } = form - - - - - -
- - - Name - - - - - - Create -
-
-
+{#if $hasPermission("table:update")} + + + + + +
+ + + Name + + + + + + Create +
+
+
+{/if} diff --git a/apps/frontend/src/lib/components/blocks/forms/empty-forms.svelte b/apps/frontend/src/lib/components/blocks/forms/empty-forms.svelte index 809208266..a940171a6 100644 --- a/apps/frontend/src/lib/components/blocks/forms/empty-forms.svelte +++ b/apps/frontend/src/lib/components/blocks/forms/empty-forms.svelte @@ -1,5 +1,9 @@
@@ -9,9 +13,12 @@ data-x-chunk-description="An empty state showing no products with a heading, description and a call to action to add a product." >
-

You have no forms

-

You can start selling as soon as you add a form.

- +

{$table.name.value} have no forms

+ + {#if $hasPermission("table:update")} +

You can start selling as soon as you add a form.

+ + {/if}
diff --git a/apps/frontend/src/lib/components/blocks/grid-view/grid-view-action-header.svelte b/apps/frontend/src/lib/components/blocks/grid-view/grid-view-action-header.svelte index 6a07a3395..0dbc36316 100644 --- a/apps/frontend/src/lib/components/blocks/grid-view/grid-view-action-header.svelte +++ b/apps/frontend/src/lib/components/blocks/grid-view/grid-view-action-header.svelte @@ -1,10 +1,11 @@ -{#if !readonly} +{#if !readonly && $hasPermission("field:create")} diff --git a/apps/frontend/src/lib/components/blocks/grid-view/grid-view-empty.svelte b/apps/frontend/src/lib/components/blocks/grid-view/grid-view-empty.svelte index 44b2a0789..a4cb2eb5d 100644 --- a/apps/frontend/src/lib/components/blocks/grid-view/grid-view-empty.svelte +++ b/apps/frontend/src/lib/components/blocks/grid-view/grid-view-empty.svelte @@ -1,13 +1,16 @@
-

You have no records

- {#if !readonly} +

{$table.name.value} have no records

+ {#if !readonly && $hasPermission("record:create")}

You can click button or use shortcut Ctrl + R and create your first record diff --git a/apps/frontend/src/lib/components/blocks/grid-view/grid-view-header.svelte b/apps/frontend/src/lib/components/blocks/grid-view/grid-view-header.svelte index b3311a253..25eff5236 100644 --- a/apps/frontend/src/lib/components/blocks/grid-view/grid-view-header.svelte +++ b/apps/frontend/src/lib/components/blocks/grid-view/grid-view-header.svelte @@ -4,6 +4,7 @@ import { ChevronDownIcon } from "lucide-svelte" import * as Popover from "$lib/components/ui/popover" import FieldMenu from "../field/field-menu.svelte" + import { hasPermission } from "$lib/store/space-member.store" export let field: Field @@ -19,18 +20,20 @@

- { - if (!open) { - update = false - } - }} - > - - - + {#if $hasPermission("field:update") || $hasPermission("field:delete") || $hasPermission("field:create")} + { + if (!open) { + update = false + } + }} + > + + + - - + + + {/if}
diff --git a/apps/frontend/src/lib/components/blocks/member/get-role-bg-color.ts b/apps/frontend/src/lib/components/blocks/member/get-role-bg-color.ts index f2601499c..dcea45209 100644 --- a/apps/frontend/src/lib/components/blocks/member/get-role-bg-color.ts +++ b/apps/frontend/src/lib/components/blocks/member/get-role-bg-color.ts @@ -6,6 +6,8 @@ export const getRoleBgColor = (role: ISpaceMemberRole) => { return "bg-blue-500" case "admin": return "bg-yellow-500" + case "editor": + return "bg-slate-500" case "viewer": return "bg-green-500" default: diff --git a/apps/frontend/src/lib/components/blocks/rls/create-rls-button.svelte b/apps/frontend/src/lib/components/blocks/rls/create-rls-button.svelte index f937d9aca..5bfe4002c 100644 --- a/apps/frontend/src/lib/components/blocks/rls/create-rls-button.svelte +++ b/apps/frontend/src/lib/components/blocks/rls/create-rls-button.svelte @@ -1,10 +1,13 @@ - +{#if $hasPermission("table:update")} + +{/if} diff --git a/apps/frontend/src/lib/components/blocks/rls/empty-rls.svelte b/apps/frontend/src/lib/components/blocks/rls/empty-rls.svelte index 9de129e11..14310f37e 100644 --- a/apps/frontend/src/lib/components/blocks/rls/empty-rls.svelte +++ b/apps/frontend/src/lib/components/blocks/rls/empty-rls.svelte @@ -1,5 +1,9 @@
@@ -9,9 +13,11 @@ data-x-chunk-description="An empty state showing no products with a heading, description and a call to action to add a product." >
-

You have no record level security

-

Click button to create your first record level security policy

- +

{$table.name.value} have no record level security

+ {#if $hasPermission("table:update")} +

Click button to create your first record level security policy

+ + {/if}
diff --git a/apps/frontend/src/lib/components/blocks/role/role-picker.svelte b/apps/frontend/src/lib/components/blocks/role/role-picker.svelte index 0dfa27023..8858ea283 100644 --- a/apps/frontend/src/lib/components/blocks/role/role-picker.svelte +++ b/apps/frontend/src/lib/components/blocks/role/role-picker.svelte @@ -4,7 +4,7 @@ import { LL } from "@undb/i18n/client" import Role from "../member/role.svelte" - export let role: ISpaceMemberWithoutOwner = "viewer" + export let role: ISpaceMemberWithoutOwner = "editor" $: selectedRole = role ? { @@ -35,6 +35,9 @@ + + + diff --git a/apps/frontend/src/lib/components/blocks/share/share-button.svelte b/apps/frontend/src/lib/components/blocks/share/share-button.svelte index 44e44d22f..eea1ceecc 100644 --- a/apps/frontend/src/lib/components/blocks/share/share-button.svelte +++ b/apps/frontend/src/lib/components/blocks/share/share-button.svelte @@ -14,6 +14,7 @@ import { copyToClipboard } from "@svelte-put/copy" import { toast } from "svelte-sonner" import { cn } from "$lib/utils" + import { hasPermission } from "$lib/store/space-member.store" export let type: IShareTarget["type"] export let id: IShareTarget["id"] @@ -67,64 +68,66 @@ } - - - - - -
-

- - +{#if $hasPermission("share:enable")} + + +

- -
+ + + +
+

+ - {#if enabled && share?.id} -
-
- { - copy() - e.target.select() + Share +

+
+ {enabled ? "enable" : "disable"} + - {/if} -
-
+ + {#if enabled && share?.id} +
+
+ { + copy() + e.target.select() + }} + /> + + + + +
+
+ {/if} + + +{/if} diff --git a/apps/frontend/src/lib/components/blocks/space/space-setting.svelte b/apps/frontend/src/lib/components/blocks/space/space-setting.svelte index 5cb1ef408..1d3f55d2a 100644 --- a/apps/frontend/src/lib/components/blocks/space/space-setting.svelte +++ b/apps/frontend/src/lib/components/blocks/space/space-setting.svelte @@ -11,6 +11,7 @@ import { toast } from "svelte-sonner" import { Button } from "$lib/components/ui/button" import * as AlertDialog from "$lib/components/ui/alert-dialog/index.js" + import { hasPermission } from "$lib/store/space-member.store" export let space: ISpaceDTO @@ -63,13 +64,18 @@ Space name - + Change space display name. - Update + Update {#if browser} {/if} @@ -86,7 +92,11 @@ - + @@ -106,7 +116,11 @@ + @@ -154,49 +168,53 @@ {view.name.value} - + {#if $hasPermission("table:update")} + + {/if} - - toggleModal(UPDATE_VIEW)}> - - Update View Name - - toggleModal(DUPLICATE_VIEW)}> - - Duplicate View - - - - - Download View - - - downloadView("excel")}> - - Download as Excel - - downloadView("csv")}> - - Download as CSV - - downloadView("json")}> - - Download as JSON - - - - {#if !view.isDefault} - toggleModal(DELETE_VIEW)} - > + {#if $hasPermission("table:update")} + + toggleModal(UPDATE_VIEW)}> + + Update View Name + + toggleModal(DUPLICATE_VIEW)}> - Delete View + Duplicate View - {/if} - + + + + Download View + + + downloadView("excel")}> + + Download as Excel + + downloadView("csv")}> + + Download as CSV + + downloadView("json")}> + + Download as JSON + + + + {#if !view.isDefault} + toggleModal(DELETE_VIEW)} + > + + Delete View + + {/if} + + {/if} - + {#if $hasPermission("table:create")} + + {/if} - + {#if $hasPermission("table:update")} + + {/if} - + {#if $hasPermission("field:create") || $hasPermission("field:update") || $hasPermission("field:delete")} + + {/if} @@ -165,45 +170,47 @@ -
- {#if hiddenCount > 0} - - {:else} + {#if $hasPermission("table:update")} +
+ {#if hiddenCount > 0} + + {:else} + + {/if} + - {/if} - - -
+
+ {/if}
diff --git a/apps/frontend/src/lib/components/blocks/view-filter-editor/view-filter-editor.svelte b/apps/frontend/src/lib/components/blocks/view-filter-editor/view-filter-editor.svelte index ad3db67d1..722d3de5f 100644 --- a/apps/frontend/src/lib/components/blocks/view-filter-editor/view-filter-editor.svelte +++ b/apps/frontend/src/lib/components/blocks/view-filter-editor/view-filter-editor.svelte @@ -16,6 +16,7 @@ type IViewFilterOptionSchema, type MaybeConditionGroup, } from "@undb/table" + import { hasPermission } from "$lib/store/space-member.store" const table = getTable() $: filter = $table.views.getViewById($viewId).filter.into(undefined) @@ -53,7 +54,12 @@ - - + {#if !$hasPermission("table:update")} + + + {/if} {/each} @@ -165,11 +173,13 @@ {/if}
- - + {#if !$hasPermission("table:update")} + + + {/if}
diff --git a/apps/frontend/src/lib/components/blocks/view/create-view-button.svelte b/apps/frontend/src/lib/components/blocks/view/create-view-button.svelte index 7607f79d5..221983385 100644 --- a/apps/frontend/src/lib/components/blocks/view/create-view-button.svelte +++ b/apps/frontend/src/lib/components/blocks/view/create-view-button.svelte @@ -12,6 +12,7 @@ import { zodClient } from "sveltekit-superforms/adapters" import { Input } from "$lib/components/ui/input" import { cn } from "$lib/utils" + import { hasPermission } from "$lib/store/space-member.store" let open = false @@ -58,27 +59,29 @@ const { enhance, form: formData } = form - - - - - -
- - - Name - - - - - +{#if $hasPermission("table:update")} + + + + + + + + + Name + + + + + - Create - - -
+ Create + +
+
+{/if} diff --git a/apps/frontend/src/lib/components/blocks/webhook/empty-webhook.svelte b/apps/frontend/src/lib/components/blocks/webhook/empty-webhook.svelte index d5b72f857..6f0da0008 100644 --- a/apps/frontend/src/lib/components/blocks/webhook/empty-webhook.svelte +++ b/apps/frontend/src/lib/components/blocks/webhook/empty-webhook.svelte @@ -1,5 +1,9 @@
@@ -9,9 +13,11 @@ data-x-chunk-description="An empty state showing no products with a heading, description and a call to action to add a product." >
-

You have no webhooks

-

Click button to create your first webhook

- +

{$table.name.value} have no webhooks

+ {#if $hasPermission("table:update")} +

Click button to create your first webhook

+ + {/if}
diff --git a/apps/frontend/src/lib/store/space-member.store.ts b/apps/frontend/src/lib/store/space-member.store.ts index 91bf2e71f..435f93822 100644 --- a/apps/frontend/src/lib/store/space-member.store.ts +++ b/apps/frontend/src/lib/store/space-member.store.ts @@ -3,7 +3,6 @@ import { derived, writable } from "svelte/store" export const role = writable(null) -export const hasPermission = derived( - role, - ($role) => (action: ISpaceAction) => !!$role && getHasPermission({ role: $role, action }), -) +export const hasPermission = derived(role, ($role) => (action: ISpaceAction) => { + return !!$role && getHasPermission({ role: $role, action }) +}) diff --git a/apps/frontend/src/routes/(auth)/login/+page.svelte b/apps/frontend/src/routes/(auth)/login/+page.svelte index 32a053bd8..e4073a692 100644 --- a/apps/frontend/src/routes/(auth)/login/+page.svelte +++ b/apps/frontend/src/routes/(auth)/login/+page.svelte @@ -11,10 +11,12 @@ import { defaults, superForm } from "sveltekit-superforms" import { zodClient } from "sveltekit-superforms/adapters" import * as Form from "$lib/components/ui/form" - import { toast } from "svelte-sonner" import { Button } from "$lib/components/ui/button" import { Separator } from "$lib/components/ui/separator" import PasswordInput from "$lib/components/ui/input/password-input.svelte" + import * as Alert from "$lib/components/ui/alert/index.js" + import autoAnimate from "@formkit/auto-animate" + import { LoaderCircleIcon } from "lucide-svelte" const schema = z.object({ email: z.string().email(), @@ -23,14 +25,28 @@ type LoginSchema = z.infer + let loginError = false + const loginMutation = createMutation({ - mutationFn: (input: LoginSchema) => fetch("/api/login", { method: "POST", body: JSON.stringify(input) }), + mutationFn: async (input: LoginSchema) => { + try { + const { ok } = await fetch("/api/login", { method: "POST", body: JSON.stringify(input) }) + if (!ok) { + throw new Error("Failed to login") + } + return + } catch (error) { + loginError = true + } + }, + onMutate(variables) { + loginError = false + }, async onSuccess(data, variables, context) { await goto("/") }, async onError(error, variables, context) { - toast.error(error.message) - await goto("/signup") + loginError = true }, }) @@ -103,7 +119,20 @@ - Login + + {#if $loginMutation.isPending} + + {/if} + Login + + +
+ {#if loginError} + + Error + Invalid email or password. + + {/if}
Don't have an account? diff --git a/apps/frontend/src/routes/(auth)/signup/+page.svelte b/apps/frontend/src/routes/(auth)/signup/+page.svelte index 63ea519cf..6ed5386db 100644 --- a/apps/frontend/src/routes/(auth)/signup/+page.svelte +++ b/apps/frontend/src/routes/(auth)/signup/+page.svelte @@ -16,6 +16,7 @@ import { toast } from "svelte-sonner" import { Separator } from "$lib/components/ui/separator" import PasswordInput from "$lib/components/ui/input/password-input.svelte" + import { LoaderCircleIcon } from "lucide-svelte" const schema = z.object({ email: z.string().email(), @@ -39,17 +40,30 @@ $: showBanner = !!invitationId + let signupError = false + const signupMutation = createMutation({ - mutationFn: (input: SignupSchema) => - fetch("/api/signup", { - method: "POST", - body: JSON.stringify({ ...input, invitationId }), - }), + mutationFn: async (input: SignupSchema) => { + try { + const { ok } = await fetch("/api/signup", { + method: "POST", + body: JSON.stringify({ ...input, invitationId }), + }) + if (!ok) { + throw new Error("Failed to signup") + } + } catch (error) { + signupError = true + } + }, + onMutate(variables) { + signupError = false + }, async onSuccess(data, variables, context) { await goto("/") }, onError(error, variables, context) { - toast.error(error.message) + signupError = true }, }) @@ -213,7 +227,12 @@
- +
Already have an account? diff --git a/drizzle.config.ts b/drizzle.config.ts index 5a6194cc9..d14213605 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -6,6 +6,6 @@ export default defineConfig({ dialect: "sqlite", tablesFilter: ["undb_*"], dbCredentials: { - url: "./apps/backend/.undb/undb.db", + url: "./apps/backend/undb.sqlite", }, }) diff --git a/packages/authz/src/rbac/permission.ts b/packages/authz/src/rbac/permission.ts index 87fb71e79..9f3d4c076 100644 --- a/packages/authz/src/rbac/permission.ts +++ b/packages/authz/src/rbac/permission.ts @@ -1,6 +1,6 @@ import { z } from "@undb/zod" -import { spaceMemberRole } from "../space-member" -import { spaceActions } from "../space-member/space-action" +import { spaceMemberRole, type ISpaceMemberRole } from "../space-member" +import { spaceActions, type ISpaceAction } from "../space-member/space-action" import { spacePermission } from "../space-member/space-permission" const checkPermissionInput = z.object({ @@ -13,3 +13,20 @@ type ICheckPermissionInput = z.infer export function getHasPermission(input: ICheckPermissionInput): boolean { return spacePermission[input.role][input.action] } + +/** + * @throws Error if permission denied + * @param role + * @param actions + */ +export function checkPermission(role: ISpaceMemberRole, actions: ISpaceAction[]) { + if (!role) { + throw new Error("Role not found") + } + for (const action of actions) { + const hasPermission = getHasPermission({ role, action }) + if (!hasPermission) { + throw new Error("Permission denied") + } + } +} diff --git a/packages/authz/src/space-member/space-action.ts b/packages/authz/src/space-member/space-action.ts index 487502fa7..c682e438a 100644 --- a/packages/authz/src/space-member/space-action.ts +++ b/packages/authz/src/space-member/space-action.ts @@ -18,11 +18,16 @@ export const spaceActions = z.enum([ "table:list", "table:delete", + "field:create", + "field:update", + "field:delete", + "record:create", "record:list", "record:delete", "record:read", "record:update", + "record:download", "share:enable", "share:disable", diff --git a/packages/authz/src/space-member/space-member.ts b/packages/authz/src/space-member/space-member.ts index f6c0ac915..fb88a4e25 100644 --- a/packages/authz/src/space-member/space-member.ts +++ b/packages/authz/src/space-member/space-member.ts @@ -3,8 +3,8 @@ import { spaceIdSchema } from "@undb/space" import { z } from "@undb/zod" import { memberId } from "../member/member-id.vo" -export const spaceMemberRole = z.enum(["owner", "admin", "viewer"]) -export const spaceMemberWithoutOwner = z.enum(["admin", "viewer"]) +export const spaceMemberRole = z.enum(["owner", "admin", "editor", "viewer"]) +export const spaceMemberWithoutOwner = z.enum(["admin", "editor", "viewer"]) export type ISpaceMemberRole = z.infer export type ISpaceMemberWithoutOwner = z.infer diff --git a/packages/authz/src/space-member/space-permission.ts b/packages/authz/src/space-member/space-permission.ts index cacfeb9f8..4dfd51189 100644 --- a/packages/authz/src/space-member/space-permission.ts +++ b/packages/authz/src/space-member/space-permission.ts @@ -20,11 +20,16 @@ export const spacePermission: Record { this.logger.debug(command) - const space = SpaceFactory.create({ ...command, isPersonal: false }) + const space = SpaceFactory.create(command) await this.repository.insert(space) const userId = getCurrentUserId() diff --git a/packages/commands/src/create-space.command.ts b/packages/commands/src/create-space.command.ts index 983c53deb..70233c1b2 100644 --- a/packages/commands/src/create-space.command.ts +++ b/packages/commands/src/create-space.command.ts @@ -2,15 +2,17 @@ import { Command, type CommandProps } from "@undb/domain" import { createSpaceDTO } from "@undb/space" import { z } from "@undb/zod" -export const createSpaceCommand = createSpaceDTO.omit({ id: true, isPersonal: true }) +export const createSpaceCommand = createSpaceDTO.omit({ id: true }) export type ICreateSpaceCommand = z.infer export class CreateSpaceCommand extends Command implements ICreateSpaceCommand { public readonly name: string + public readonly isPersonal?: boolean constructor(props: CommandProps) { super(props) this.name = props.name + this.isPersonal = props.isPersonal } } diff --git a/packages/context/src/server.ts b/packages/context/src/server.ts index af2ea44a1..2f58802d8 100644 --- a/packages/context/src/server.ts +++ b/packages/context/src/server.ts @@ -18,6 +18,10 @@ export const getCurrentUserId = () => { return executionContext.getStore()?.user?.userId! } +export const getCurrentMember = () => { + return executionContext.getStore()?.member! +} + export const getCurrentSpaceId = () => { return executionContext.getStore()?.spaceId } diff --git a/packages/i18n/src/i18n/en/index.ts b/packages/i18n/src/i18n/en/index.ts index e79101ebf..e1f2d9507 100644 --- a/packages/i18n/src/i18n/en/index.ts +++ b/packages/i18n/src/i18n/en/index.ts @@ -95,6 +95,7 @@ const aggregateFns: Record = { const workspaceRoles: Record = { owner: "Owner", admin: "Admin", + editor: "Editor", viewer: "Viewer" } diff --git a/packages/i18n/src/i18n/i18n-types.ts b/packages/i18n/src/i18n/i18n-types.ts index 966515d3d..f58fa6c3f 100644 --- a/packages/i18n/src/i18n/i18n-types.ts +++ b/packages/i18n/src/i18n/i18n-types.ts @@ -343,6 +343,10 @@ type RootTranslation = { * A​d​m​i​n */ admin: string + /** + * E​d​i​t​o​r + */ + editor: string /** * V​i​e​w​e​r */ @@ -682,6 +686,10 @@ export type TranslationFunctions = { * Admin */ admin: () => LocalizedString + /** + * Editor + */ + editor: () => LocalizedString /** * Viewer */ diff --git a/packages/persistence/src/table/table.filter-visitor.ts b/packages/persistence/src/table/table.filter-visitor.ts index d17a6f16f..123dddad2 100644 --- a/packages/persistence/src/table/table.filter-visitor.ts +++ b/packages/persistence/src/table/table.filter-visitor.ts @@ -50,10 +50,10 @@ export class TableFilterVisitor extends AbstractQBVisitor implements IT this.addCond(this.eb.eb("undb_table.space_id", "=", id.spaceId)) } withId(id: TableIdSpecification): void { - this.addCond(this.eb.eb("id", "=", id.id.value)) + this.addCond(this.eb.eb("undb_table.id", "=", id.id.value)) } withBaseId(id: TableBaseIdSpecification): void { - this.addCond(this.eb.eb("base_id", "=", id.baseId)) + this.addCond(this.eb.eb("undb_table.base_id", "=", id.baseId)) } idsIn(ids: TableIdsSpecification): void { if (!ids.ids.length) return diff --git a/packages/persistence/src/table/table.reference-visitor.ts b/packages/persistence/src/table/table.reference-visitor.ts index 418318942..1e7113b3d 100644 --- a/packages/persistence/src/table/table.reference-visitor.ts +++ b/packages/persistence/src/table/table.reference-visitor.ts @@ -8,6 +8,7 @@ import type { TableIdsSpecification, TableNameSpecification, TableSchemaSpecification, + TableSpaceIdSpecification, TableUniqueNameSpecification, TableViewsSpecification, WithDuplicatedFieldSpecification, @@ -43,6 +44,7 @@ export class TableReferenceVisitor implements ITableSpecVisitor { } withId(id: TableIdSpecification): void {} + withSpaceId(id: TableSpaceIdSpecification): void {} withBaseId(id: TableBaseIdSpecification): void {} idsIn(ids: TableIdsSpecification): void {} withName(name: TableNameSpecification): void {} diff --git a/packages/persistence/src/table/table.repository.ts b/packages/persistence/src/table/table.repository.ts index 2048332bf..9bf000dcc 100644 --- a/packages/persistence/src/table/table.repository.ts +++ b/packages/persistence/src/table/table.repository.ts @@ -115,7 +115,7 @@ export class TableRepository implements ITableRepository { async find(spec: Option): Promise { const tbs = await (getCurrentTransaction() ?? this.qb) .selectFrom("undb_table") - .selectAll() + .selectAll("undb_table") .$if(spec.isSome(), (qb) => new TableReferenceVisitor(qb).call(spec.unwrap())) .where((eb) => new TableDbQuerySpecHandler(this.qb, eb).handle(spec)) .execute() @@ -126,7 +126,7 @@ export class TableRepository implements ITableRepository { async findOne(spec: Option): Promise> { const tb = await (getCurrentTransaction() ?? this.qb) .selectFrom("undb_table") - .selectAll() + .selectAll("undb_table") .$if(spec.isSome(), (qb) => new TableReferenceVisitor(qb).call(spec.unwrap())) .where((eb) => new TableDbQuerySpecHandler(this.qb, eb).handle(spec)) .executeTakeFirst() @@ -142,7 +142,7 @@ export class TableRepository implements ITableRepository { const spec = Some(new TableIdSpecification(id)) const tb = await (getCurrentTransaction() ?? this.qb) .selectFrom("undb_table") - .selectAll() + .selectAll("undb_table") .$call((qb) => new TableReferenceVisitor(qb).call(spec.unwrap())) .where((eb) => new TableDbQuerySpecHandler(this.qb, eb).handle(spec)) .executeTakeFirst() @@ -154,7 +154,7 @@ export class TableRepository implements ITableRepository { const spec = Some(new TableIdsSpecification(ids)) const tbs = await (getCurrentTransaction() ?? this.qb) .selectFrom("undb_table") - .selectAll() + .selectAll("undb_table") .$call((qb) => new TableReferenceVisitor(qb).call(spec.unwrap())) .where((eb) => new TableDbQuerySpecHandler(this.qb, eb).handle(spec)) .execute() diff --git a/packages/space/src/dto/create-space.dto.ts b/packages/space/src/dto/create-space.dto.ts index dee16df3d..c6eabfb43 100644 --- a/packages/space/src/dto/create-space.dto.ts +++ b/packages/space/src/dto/create-space.dto.ts @@ -5,7 +5,7 @@ export const createSpaceDTO = z.object({ id: spaceIdSchema.optional(), avatar: spaceAvatarSchema.optional(), name: spaceNameSchema, - isPersonal: z.boolean(), + isPersonal: z.boolean().optional(), }) export type ICreateSpaceDTO = z.infer diff --git a/packages/space/src/space.factory.ts b/packages/space/src/space.factory.ts index d7c43576f..fbc5ab8cf 100644 --- a/packages/space/src/space.factory.ts +++ b/packages/space/src/space.factory.ts @@ -32,7 +32,7 @@ export class SpaceFactory { new WithSpaceId(SpaceId.fromOrCreate(input.id)), WithSpaceName.fromString(input.name), WithSpaceAvatar.fromString(input.avatar ?? undefined), - new WithSpaceIsPersonal(input.isPersonal), + new WithSpaceIsPersonal(input.isPersonal ?? false), ) // @ts-expect-error diff --git a/packages/space/src/space.service.ts b/packages/space/src/space.service.ts index 17ab304f1..d0ce4ac6f 100644 --- a/packages/space/src/space.service.ts +++ b/packages/space/src/space.service.ts @@ -1,7 +1,7 @@ import type { SetContextValue } from "@undb/context" import { inject, singleton } from "@undb/di" import { None, Option, Some } from "oxide.ts" -import type { ISpaceDTO } from "./dto" +import type { ICreateSpaceDTO, ISpaceDTO } from "./dto" import type { ISpaceSpecification } from "./interface" import type { Space } from "./space.do" import { SpaceFactory } from "./space.factory" @@ -24,6 +24,7 @@ interface IGetSpaceInput { } export interface ISpaceService { + createSpace(dto: ICreateSpaceDTO): Promise createPersonalSpace(username: string): Promise getSpace(input: IGetSpaceInput): Promise> getMemberSpaces(userId: string): Promise @@ -43,16 +44,21 @@ export class SpaceService implements ISpaceService { private readonly spaceQueryRepository: ISpaceQueryRepository, ) {} - async createPersonalSpace(username: string): Promise { - const space = SpaceFactory.create({ - name: username + "'s Personal Space", - isPersonal: true, - }) + async createSpace(dto: ICreateSpaceDTO): Promise { + const space = SpaceFactory.create(dto) await this.spaceRepository.insert(space) return space } + + async createPersonalSpace(username: string): Promise { + return this.createSpace({ + name: username + "'s Personal Space", + isPersonal: true, + }) + } + async getSpace(input: IGetSpaceInput): Promise> { let spec: Option = None diff --git a/packages/table/src/modules/views/view/view-sort/view-sort.vo.ts b/packages/table/src/modules/views/view/view-sort/view-sort.vo.ts index bcd194798..f7f2bc6fa 100644 --- a/packages/table/src/modules/views/view/view-sort/view-sort.vo.ts +++ b/packages/table/src/modules/views/view/view-sort/view-sort.vo.ts @@ -17,6 +17,10 @@ export class ViewSort extends ValueObject { super(props) } + public get isEmpty(): boolean { + return this.props?.length === 0 + } + public isEqual(sort: IViewSort): boolean { return isEqual(sort, this.props) } diff --git a/packages/trpc/src/authz.middleware.ts b/packages/trpc/src/authz.middleware.ts index 8389d0290..d7918b246 100644 --- a/packages/trpc/src/authz.middleware.ts +++ b/packages/trpc/src/authz.middleware.ts @@ -1,4 +1,4 @@ -import { getHasPermission, type ISpaceAction } from "@undb/authz" +import { checkPermission, type ISpaceAction } from "@undb/authz" import { executionContext } from "@undb/context/server" import { middleware } from "./trpc" @@ -6,14 +6,7 @@ export const authz = (...actions: ISpaceAction[]) => middleware(({ next }) => { const member = executionContext.getStore()?.member const role = member?.role - if (!role) { - throw new Error("Role not found") - } - for (const action of actions) { - const hasPermission = getHasPermission({ role, action }) - if (!hasPermission) { - throw new Error("Permission denied") - } - } + + checkPermission(role, actions) return next() })