diff --git a/app/[doc_id]/Doc.tsx b/app/[doc_id]/Doc.tsx index 60f3b033..b4798602 100644 --- a/app/[doc_id]/Doc.tsx +++ b/app/[doc_id]/Doc.tsx @@ -1,4 +1,4 @@ -import { Fact, ReplicacheProvider } from "src/replicache"; +import { Fact, PermissionToken, ReplicacheProvider } from "src/replicache"; import { Database } from "../../supabase/database.types"; import { Attributes } from "src/replicache/attributes"; import { createServerClient } from "@supabase/ssr"; @@ -8,24 +8,37 @@ import { ThemeProvider } from "components/ThemeManager/ThemeProvider"; import { MobileFooter } from "components/MobileFooter"; import { PopUpProvider } from "components/Toast"; import { YJSFragmentToString } from "components/TextBlock/RenderYJSFragment"; +import { + EntitySetContext, + EntitySetProvider, +} from "components/EntitySetProvider"; export function Doc(props: { + token: PermissionToken; initialFacts: Fact[]; doc_id: string; }) { return ( - - - - - - - - + + + + + + + + + + ); } diff --git a/app/[doc_id]/page.tsx b/app/[doc_id]/page.tsx index 5e6f3c5c..5277476f 100644 --- a/app/[doc_id]/page.tsx +++ b/app/[doc_id]/page.tsx @@ -24,21 +24,41 @@ let supabase = createServerClient( { cookies: {} }, ); type Props = { + // this is now a token id not doc! Should probs rename params: { doc_id: string }; }; export default async function DocumentPage(props: Props) { - let { data } = await supabase.rpc("get_facts", { root: props.params.doc_id }); + let res = await supabase + .from("permission_tokens") + .select("*, permission_token_rights(*)") + .eq("id", props.params.doc_id) + .single(); + let rootEntity = res.data?.root_entity; + if (!rootEntity || !res.data) + return
404 no rootEntity found idk man
; + let { data } = await supabase.rpc("get_facts", { + root: rootEntity, + }); let initialFacts = (data as unknown as Fact[]) || []; - return ; + return ( + + ); } export async function generateMetadata(props: Props): Promise { - let { data } = await supabase.rpc("get_facts", { root: props.params.doc_id }); + let res = await supabase + .from("permission_tokens") + .select("*, permission_token_rights(*)") + .eq("id", props.params.doc_id) + .single(); + let rootEntity = res.data?.root_entity; + if (!rootEntity || !res.data) return { title: "Doc not found" }; + let { data } = await supabase.rpc("get_facts", { + root: rootEntity, + }); let initialFacts = (data as unknown as Fact[]) || []; let blocks = initialFacts - .filter( - (f) => f.attribute === "card/block" && f.entity === props.params.doc_id, - ) + .filter((f) => f.attribute === "card/block" && f.entity === rootEntity) .map((_f) => { let block = _f as Fact<"card/block">; let type = initialFacts.find( diff --git a/app/page.tsx b/app/page.tsx index 4d326c36..bbd23bd2 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,10 @@ import { drizzle } from "drizzle-orm/postgres-js"; -import { entities } from "drizzle/schema"; +import { + entities, + permission_tokens, + permission_token_rights, + entity_sets, +} from "drizzle/schema"; import { redirect } from "next/navigation"; import postgres from "postgres"; import { Doc } from "./[doc_id]/Doc"; @@ -12,11 +17,47 @@ export const dynamic = "force-dynamic"; export const fetchCache = "force-no-store"; export default async function RootPage() { - let rows = await db.insert(entities).values({}).returning(); + // Creating a new document + let { permissionToken, rights, entity, entity_set } = await db.transaction( + async (tx) => { + // Create a new entity set + let [entity_set] = await tx.insert(entity_sets).values({}).returning(); + // Create a root-entity + let [entity] = await tx + .insert(entities) + // And add it to that permission set + .values({ set: entity_set.id }) + .returning(); + //Create a new permission token + let [permissionToken] = await tx + .insert(permission_tokens) + .values({ root_entity: entity.id }) + .returning(); + //and give it all the permission on that entity set + let [rights] = await tx + .insert(permission_token_rights) + .values({ + token: permissionToken.id, + entity_set: entity_set.id, + read: true, + write: true, + create_token: true, + change_entity_set: true, + }) + .returning(); + return { permissionToken, rights, entity, entity_set }; + }, + ); + // Here i need to pass the permission token instead of the doc_id + // In the replicache provider I guess I need to fetch the relevant stuff of the permission token? return ( <> - - + + ); } diff --git a/components/BlockOptions.tsx b/components/BlockOptions.tsx index e9d6bd62..6e36b57f 100644 --- a/components/BlockOptions.tsx +++ b/components/BlockOptions.tsx @@ -13,6 +13,7 @@ import { focusCard } from "./Cards"; import { useState } from "react"; import { Separator } from "./Layout"; import { addLinkBlock } from "src/utils/addLinkBlock"; +import { useEntitySetContext } from "./EntitySetProvider"; type Props = { parent: string; @@ -23,6 +24,7 @@ type Props = { }; export function BlockOptions(props: Props) { let { rep } = useReplicache(); + let entity_set = useEntitySetContext(); let focusedElement = useUIState((s) => s.focusedBlock); let focusedCardID = @@ -51,6 +53,7 @@ export function BlockOptions(props: Props) { entity = crypto.randomUUID(); await rep?.mutate.addBlock({ parent: props.parent, + permission_set: entity_set.set, type: "text", position: generateKeyBetween( props.position, @@ -81,6 +84,7 @@ export function BlockOptions(props: Props) { let entity = crypto.randomUUID(); await rep?.mutate.addBlock({ + permission_set: entity_set.set, parent: props.parent, type: "card", position: generateKeyBetween( @@ -110,6 +114,7 @@ export function BlockOptions(props: Props) { } const BlockLinkButton = (props: Props) => { + let entity_set = useEntitySetContext(); let [linkOpen, setLinkOpen] = useState(false); let [linkValue, setLinkValue] = useState(""); let { rep } = useReplicache(); @@ -119,6 +124,7 @@ const BlockLinkButton = (props: Props) => { entity = crypto.randomUUID(); await rep?.mutate.addBlock({ + permission_set: entity_set.set, parent: props.parent, type: "card", position: generateKeyBetween(props.position, props.nextPosition), diff --git a/components/Blocks.tsx b/components/Blocks.tsx index b53763ad..7de425a0 100644 --- a/components/Blocks.tsx +++ b/components/Blocks.tsx @@ -13,6 +13,7 @@ import { ExternalLinkBlock } from "./ExternalLinkBlock"; import { BlockOptions } from "./BlockOptions"; import { useBlocks } from "src/hooks/queries/useBlocks"; import { setEditorState, useEditorStates } from "src/state/useEditorState"; +import { useEntitySetContext } from "./EntitySetProvider"; export type Block = { parent: string; @@ -22,6 +23,7 @@ export type Block = { }; export function Blocks(props: { entityID: string }) { let rep = useReplicache(); + let entity_set = useEntitySetContext(); let blocks = useBlocks(props.entityID); let lastBlock = blocks[blocks.length - 1]; @@ -38,6 +40,7 @@ export function Blocks(props: { entityID: string }) { let newEntityID = crypto.randomUUID(); await rep.rep?.mutate.addBlock({ parent: props.entityID, + permission_set: entity_set.set, type: "text", position: generateKeyBetween(lastBlock.position || null, null), newEntityID, @@ -77,6 +80,7 @@ export function Blocks(props: { entityID: string }) { focusBlock({ ...lastBlock, type: "text" }, { type: "end" }); } else { rep?.rep?.mutate.addBlock({ + permission_set: entity_set.set, parent: props.entityID, type: "text", position: generateKeyBetween(lastBlock?.position || null, null), @@ -97,6 +101,7 @@ export function Blocks(props: { entityID: string }) { function NewBlockButton(props: { lastBlock: Block | null; entityID: string }) { let { rep } = useReplicache(); + let entity_set = useEntitySetContext(); let textContent = useEntity( props.lastBlock?.type === "text" ? props.lastBlock.value : null, "block/text", @@ -115,6 +120,7 @@ function NewBlockButton(props: { lastBlock: Block | null; entityID: string }) { await rep?.mutate.addBlock({ parent: props.entityID, type: "text", + permission_set: entity_set.set, position: generateKeyBetween( props.lastBlock?.position || null, null, @@ -161,6 +167,7 @@ function Block(props: BlockProps) { ); let { rep } = useReplicache(); + let entity_set = useEntitySetContext(); useEffect(() => { if (!selected || !rep) return; let r = rep; @@ -198,6 +205,7 @@ function Block(props: BlockProps) { if (e.key === "Enter") { let newEntityID = crypto.randomUUID(); r.mutate.addBlock({ + permission_set: entity_set.set, newEntityID, parent: props.parent, type: "text", @@ -215,6 +223,7 @@ function Block(props: BlockProps) { window.addEventListener("keydown", listener); return () => window.removeEventListener("keydown", listener); }, [ + entity_set, selected, props.entityID, props.nextBlock, diff --git a/components/EntitySetProvider.tsx b/components/EntitySetProvider.tsx new file mode 100644 index 00000000..555af619 --- /dev/null +++ b/components/EntitySetProvider.tsx @@ -0,0 +1,28 @@ +"use client"; +import { createContext, useContext } from "react"; +import { useReplicache } from "src/replicache"; + +export const EntitySetContext = createContext({ + set: "", + permissions: { read: false, write: false }, +}); +export const useEntitySetContext = () => useContext(EntitySetContext); + +export function EntitySetProvider(props: { + set: string; + children: React.ReactNode; +}) { + let { permission_token } = useReplicache(); + return ( + r.entity_set === props.set, + ) || { read: false, write: false }, + }} + > + {props.children} + + ); +} diff --git a/components/TextBlock/index.tsx b/components/TextBlock/index.tsx index f1098d51..d7d0c87b 100644 --- a/components/TextBlock/index.tsx +++ b/components/TextBlock/index.tsx @@ -37,13 +37,15 @@ import { isIOS } from "@react-aria/utils"; import { useIsMobile } from "src/hooks/isMobile"; import { setMark } from "src/utils/prosemirror/setMark"; import { rangeHasMark } from "src/utils/prosemirror/rangeHasMark"; +import { useEntitySetContext } from "components/EntitySetProvider"; export function TextBlock(props: BlockProps & { className: string }) { let initialized = useInitialPageLoad(); let first = props.previousBlock === null; + let permission = useEntitySetContext().permissions.write; return ( <> - {!initialized && ( + {(!initialized || !permission) && ( )} -
- - -
+ {permission && ( +
+ + +
+ )} ); } @@ -139,10 +143,11 @@ export function BaseTextBlock(props: BlockProps & { className: string }) { let [value, factID] = useYJSValue(props.entityID); let repRef = useRef>(null); - let propsRef = useRef(props); + let entity_set = useEntitySetContext(); + let propsRef = useRef({ ...props, entity_set }); useEffect(() => { - propsRef.current = props; - }, [props]); + propsRef.current = { ...props, entity_set }; + }, [props, entity_set]); let rep = useReplicache(); useEffect(() => { repRef.current = rep.rep; @@ -244,6 +249,7 @@ export function BaseTextBlock(props: BlockProps & { className: string }) { propsRef.current.nextPosition, ); repRef.current?.mutate.addBlock({ + permission_set: entity_set.set, newEntityID: entityID, parent: propsRef.current.parent, type: type, @@ -297,6 +303,7 @@ export function BaseTextBlock(props: BlockProps & { className: string }) { propsRef.current.nextPosition, ); repRef.current?.mutate.addBlock({ + permission_set: entity_set.set, newEntityID, parent: propsRef.current.parent, type: "text", @@ -332,6 +339,7 @@ export function BaseTextBlock(props: BlockProps & { className: string }) { } else { entity = crypto.randomUUID(); rep.rep.mutate.addBlock({ + permission_set: entity_set.set, type: "image", newEntityID: entity, parent: props.parent, diff --git a/components/TextBlock/keymap.ts b/components/TextBlock/keymap.ts index b9072304..dd1772a7 100644 --- a/components/TextBlock/keymap.ts +++ b/components/TextBlock/keymap.ts @@ -13,7 +13,7 @@ import { setEditorState, useEditorStates } from "src/state/useEditorState"; import { focusCard } from "components/Cards"; export const TextBlockKeymap = ( - propsRef: MutableRefObject, + propsRef: MutableRefObject, repRef: MutableRefObject | null>, ) => keymap({ @@ -240,7 +240,7 @@ const backspace = const enter = ( - propsRef: MutableRefObject, + propsRef: MutableRefObject, repRef: MutableRefObject | null>, ) => (state: EditorState, dispatch?: (tr: Transaction) => void) => { @@ -255,6 +255,7 @@ const enter = ); repRef.current?.mutate.addBlock({ newEntityID, + permission_set: propsRef.current.entity_set.set, parent: propsRef.current.parent, type: "text", position, diff --git a/components/UpdateURL.tsx b/components/UpdateURL.tsx index 64ef1295..8f35212a 100644 --- a/components/UpdateURL.tsx +++ b/components/UpdateURL.tsx @@ -1,9 +1,11 @@ "use client"; +import { useRouter } from "next/navigation"; import { useEffect } from "react"; export function UpdateURL(props: { url: string }) { + let router = useRouter(); useEffect(() => { - window.history.replaceState(null, "", props.url); - }, [props.url]); - return null; + router.replace(props.url); + }, [props.url, router]); + return <>; } diff --git a/drizzle/relations.ts b/drizzle/relations.ts index 3fde2f54..69d279a8 100644 --- a/drizzle/relations.ts +++ b/drizzle/relations.ts @@ -1,5 +1,27 @@ import { relations } from "drizzle-orm/relations"; -import { entities, facts } from "./schema"; +import { entity_sets, entities, permission_tokens, facts, permission_token_rights } from "./schema"; + +export const entitiesRelations = relations(entities, ({one, many}) => ({ + entity_set: one(entity_sets, { + fields: [entities.set], + references: [entity_sets.id] + }), + permission_tokens: many(permission_tokens), + facts: many(facts), +})); + +export const entity_setsRelations = relations(entity_sets, ({many}) => ({ + entities: many(entities), + permission_token_rights: many(permission_token_rights), +})); + +export const permission_tokensRelations = relations(permission_tokens, ({one, many}) => ({ + entity: one(entities, { + fields: [permission_tokens.root_entity], + references: [entities.id] + }), + permission_token_rights: many(permission_token_rights), +})); export const factsRelations = relations(facts, ({one}) => ({ entity: one(entities, { @@ -8,6 +30,13 @@ export const factsRelations = relations(facts, ({one}) => ({ }), })); -export const entitiesRelations = relations(entities, ({many}) => ({ - facts: many(facts), +export const permission_token_rightsRelations = relations(permission_token_rights, ({one}) => ({ + entity_set: one(entity_sets, { + fields: [permission_token_rights.entity_set], + references: [entity_sets.id] + }), + permission_token: one(permission_tokens, { + fields: [permission_token_rights.token], + references: [permission_tokens.id] + }), })); \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 3951d0d2..13b7c8ba 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -1,95 +1,63 @@ -import { - pgTable, - pgEnum, - uuid, - timestamp, - text, - bigint, - foreignKey, - jsonb, -} from "drizzle-orm/pg-core"; -import { Fact } from "src/replicache"; -import { Attributes } from "src/replicache/attributes"; +import { pgTable, pgEnum, text, bigint, foreignKey, uuid, timestamp, jsonb, primaryKey, boolean } from "drizzle-orm/pg-core" + import { sql } from "drizzle-orm" -export const aal_level = pgEnum("aal_level", ["aal1", "aal2", "aal3"]); -export const code_challenge_method = pgEnum("code_challenge_method", [ - "s256", - "plain", -]); -export const factor_status = pgEnum("factor_status", [ - "unverified", - "verified", -]); -export const factor_type = pgEnum("factor_type", ["totp", "webauthn"]); -export const request_status = pgEnum("request_status", [ - "PENDING", - "SUCCESS", - "ERROR", -]); -export const key_status = pgEnum("key_status", [ - "default", - "valid", - "invalid", - "expired", -]); -export const key_type = pgEnum("key_type", [ - "aead-ietf", - "aead-det", - "hmacsha512", - "hmacsha256", - "auth", - "shorthash", - "generichash", - "kdf", - "secretbox", - "secretstream", - "stream_xchacha20", -]); -export const action = pgEnum("action", [ - "INSERT", - "UPDATE", - "DELETE", - "TRUNCATE", - "ERROR", -]); -export const equality_op = pgEnum("equality_op", [ - "eq", - "neq", - "lt", - "lte", - "gt", - "gte", - "in", -]); +export const aal_level = pgEnum("aal_level", ['aal1', 'aal2', 'aal3']) +export const code_challenge_method = pgEnum("code_challenge_method", ['s256', 'plain']) +export const factor_status = pgEnum("factor_status", ['unverified', 'verified']) +export const factor_type = pgEnum("factor_type", ['totp', 'webauthn']) +export const one_time_token_type = pgEnum("one_time_token_type", ['confirmation_token', 'reauthentication_token', 'recovery_token', 'email_change_token_new', 'email_change_token_current', 'phone_change_token']) +export const request_status = pgEnum("request_status", ['PENDING', 'SUCCESS', 'ERROR']) +export const key_status = pgEnum("key_status", ['default', 'valid', 'invalid', 'expired']) +export const key_type = pgEnum("key_type", ['aead-ietf', 'aead-det', 'hmacsha512', 'hmacsha256', 'auth', 'shorthash', 'generichash', 'kdf', 'secretbox', 'secretstream', 'stream_xchacha20']) +export const action = pgEnum("action", ['INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'ERROR']) +export const equality_op = pgEnum("equality_op", ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'in']) + + +export const replicache_clients = pgTable("replicache_clients", { + client_id: text("client_id").primaryKey().notNull(), + client_group: text("client_group").notNull(), + // You can use { mode: "bigint" } if numbers are exceeding js number limitations + last_mutation: bigint("last_mutation", { mode: "number" }).notNull(), +}); export const entities = pgTable("entities", { - id: uuid("id").defaultRandom().primaryKey().notNull(), - created_at: timestamp("created_at", { withTimezone: true, mode: "string" }) - .defaultNow() - .notNull(), + id: uuid("id").defaultRandom().primaryKey().notNull(), + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + set: uuid("set").notNull().references(() => entity_sets.id, { onDelete: "cascade", onUpdate: "cascade" } ), }); -export const replicache_clients = pgTable("replicache_clients", { - client_id: text("client_id").primaryKey().notNull(), - client_group: text("client_group").notNull(), - // You can use { mode: "bigint" } if numbers are exceeding js number limitations - last_mutation: bigint("last_mutation", { mode: "number" }).notNull(), +export const entity_sets = pgTable("entity_sets", { + id: uuid("id").defaultRandom().primaryKey().notNull(), + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}); + +export const permission_tokens = pgTable("permission_tokens", { + id: uuid("id").defaultRandom().primaryKey().notNull(), + root_entity: uuid("root_entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "cascade" } ), }); export const facts = pgTable("facts", { - id: uuid("id").defaultRandom().primaryKey().notNull(), - entity: uuid("entity") - .notNull() - .references(() => entities.id, { - onDelete: "cascade", - onUpdate: "restrict", - }), - attribute: text("attribute").notNull().$type(), - data: jsonb("data").notNull().$type["data"]>(), - created_at: timestamp("created_at", { mode: "string" }) - .defaultNow() - .notNull(), - updated_at: timestamp("updated_at", { mode: "string" }), - // You can use { mode: "bigint" } if numbers are exceeding js number limitations - version: bigint("version", { mode: "number" }).default(0).notNull(), + id: uuid("id").defaultRandom().primaryKey().notNull(), + entity: uuid("entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "restrict" } ), + attribute: text("attribute").notNull(), + data: jsonb("data").notNull(), + created_at: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(), + updated_at: timestamp("updated_at", { mode: 'string' }), + // You can use { mode: "bigint" } if numbers are exceeding js number limitations + version: bigint("version", { mode: "number" }).default(0).notNull(), }); + +export const permission_token_rights = pgTable("permission_token_rights", { + token: uuid("token").notNull().references(() => permission_tokens.id, { onDelete: "cascade", onUpdate: "cascade" } ), + entity_set: uuid("entity_set").notNull().references(() => entity_sets.id, { onDelete: "cascade", onUpdate: "cascade" } ), + read: boolean("read").default(false).notNull(), + write: boolean("write").default(false).notNull(), + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + create_token: boolean("create_token").default(false).notNull(), + change_entity_set: boolean("change_entity_set").default(false).notNull(), +}, +(table) => { + return { + permission_token_rights_pkey: primaryKey({ columns: [table.token, table.entity_set], name: "permission_token_rights_pkey"}), + } +}); \ No newline at end of file diff --git a/src/replicache/clientMutationContext.ts b/src/replicache/clientMutationContext.ts index 57b5341f..afc275a1 100644 --- a/src/replicache/clientMutationContext.ts +++ b/src/replicache/clientMutationContext.ts @@ -14,8 +14,8 @@ export function clientMutationContext(tx: WriteTransaction) { let supabase = supabaseBrowserClient(); return cb({ supabase }); }, - async createEntity(_entityID) { - tx.set(_entityID, true); + async createEntity({ entityID }) { + tx.set(entityID, true); return true; }, scanIndex: { diff --git a/src/replicache/index.tsx b/src/replicache/index.tsx index b662facb..67328c30 100644 --- a/src/replicache/index.tsx +++ b/src/replicache/index.tsx @@ -19,6 +19,7 @@ export type Fact = { let ReplicacheContext = createContext({ rep: null as null | Replicache, initialFacts: [] as Fact[], + permission_token: {} as PermissionToken, }); export function useReplicache() { return useContext(ReplicacheContext); @@ -29,8 +30,18 @@ export type ReplicacheMutators = { args: Parameters<(typeof mutations)[k]>[0], ) => Promise; }; + +export type PermissionToken = { + id: string; + permission_token_rights: { + entity_set: string; + read: boolean; + write: boolean; + }[]; +}; export function ReplicacheProvider(props: { initialFacts: Fact[]; + token: PermissionToken; name: string; children: React.ReactNode; }) { @@ -55,7 +66,7 @@ export function ReplicacheProvider(props: { licenseKey: "l381074b8d5224dabaef869802421225a", pusher: async (pushRequest) => { return { - response: await Push(pushRequest, props.name), + response: await Push(pushRequest, props.name, props.token), httpRequestInfo: { errorMessage: "", httpStatusCode: 200 }, }; }, @@ -87,7 +98,11 @@ export function ReplicacheProvider(props: { }, [props.name]); return ( {props.children} diff --git a/src/replicache/mutations.ts b/src/replicache/mutations.ts index 72857705..804b3e69 100644 --- a/src/replicache/mutations.ts +++ b/src/replicache/mutations.ts @@ -5,7 +5,10 @@ import { SupabaseClient } from "@supabase/supabase-js"; import { Database } from "supabase/database.types"; export type MutationContext = { - createEntity: (entityID: string) => Promise; + createEntity: (args: { + entityID: string; + permission_set: string; + }) => Promise; scanIndex: { eav: ( entity: string, @@ -29,11 +32,15 @@ type Mutation = (args: T, ctx: MutationContext) => Promise; const addBlock: Mutation<{ parent: string; + permission_set: string; type: Fact<"block/type">["data"]["value"]; newEntityID: string; position: string; }> = async (args, ctx) => { - await ctx.createEntity(args.newEntityID); + await ctx.createEntity({ + entityID: args.newEntityID, + permission_set: args.permission_set, + }); await ctx.assertFact({ entity: args.parent, data: { diff --git a/src/replicache/push.ts b/src/replicache/push.ts index 53776aa1..b9a0ba6b 100644 --- a/src/replicache/push.ts +++ b/src/replicache/push.ts @@ -3,8 +3,9 @@ import { PushRequest, PushResponse } from "replicache"; import { serverMutationContext } from "./serverMutationContext"; import { mutations } from "./mutations"; import { drizzle } from "drizzle-orm/postgres-js"; +import { eq } from "drizzle-orm"; import postgres from "postgres"; -import { replicache_clients } from "drizzle/schema"; +import { permission_token_rights, replicache_clients } from "drizzle/schema"; import { getClientGroup } from "./utils"; import { createClient } from "@supabase/supabase-js"; import { Database } from "supabase/database.types"; @@ -18,10 +19,15 @@ const db = drizzle(client); export async function Push( pushRequest: PushRequest, rootEntity: string, + token: { id: string }, ): Promise { if (pushRequest.pushVersion !== 1) return { error: "VersionNotSupported", versionType: "push" }; let clientGroup = await getClientGroup(db, pushRequest.clientGroupID); + let token_rights = await db + .select() + .from(permission_token_rights) + .where(eq(permission_token_rights.token, token.id)); for (let mutation of pushRequest.mutations) { let lastMutationID = clientGroup[mutation.clientID] || 0; if (mutation.id <= lastMutationID) continue; @@ -32,7 +38,10 @@ export async function Push( } await db.transaction(async (tx) => { try { - await mutations[name](mutation.args as any, serverMutationContext(tx)); + await mutations[name]( + mutation.args as any, + serverMutationContext(tx, token_rights), + ); } catch (e) { console.log( `Error occured while running mutation: ${name}`, diff --git a/src/replicache/serverMutationContext.ts b/src/replicache/serverMutationContext.ts index 0eae62c3..6dcacc27 100644 --- a/src/replicache/serverMutationContext.ts +++ b/src/replicache/serverMutationContext.ts @@ -5,12 +5,17 @@ import * as Y from "yjs"; import { MutationContext } from "./mutations"; import { entities, facts } from "drizzle/schema"; import { Attributes, FilterAttributes } from "./attributes"; -import { Fact } from "."; +import { Fact, PermissionToken } from "."; import { DeepReadonly } from "replicache"; import { createClient } from "@supabase/supabase-js"; import { Database } from "supabase/database.types"; -export function serverMutationContext(tx: PgTransaction) { - let ctx: MutationContext = { +export function serverMutationContext( + tx: PgTransaction, + token_rights: PermissionToken["permission_token_rights"], +) { + let ctx: MutationContext & { + checkPermission: (entity: string) => Promise; + } = { async runOnServer(cb) { let supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, @@ -18,14 +23,37 @@ export function serverMutationContext(tx: PgTransaction) { ); return cb({ supabase }); }, + async checkPermission(entity: string) { + let [permission_set] = await tx + .select({ entity_set: entities.set }) + .from(entities) + .where(driz.eq(entities.id, entity)); + return ( + !!permission_set && + !!token_rights.find( + (r) => r.entity_set === permission_set.entity_set && r.write == true, + ) + ); + }, async runOnClient(_cb) {}, - async createEntity(entity) { + async createEntity({ entityID, permission_set }) { + if ( + !token_rights.find( + (r) => r.entity_set === permission_set && r.write === true, + ) + ) { + console.log("NO RIGHT???"); + console.log(token_rights); + console.log(permission_set); + return false; + } await tx.transaction( async (tx2) => await tx2 .insert(entities) .values({ - id: entity, + set: permission_set, + id: entityID, }) .catch(console.log), ); @@ -54,6 +82,11 @@ export function serverMutationContext(tx: PgTransaction) { if (!attribute) return; let id = f.id || crypto.randomUUID(); let data = { ...f.data }; + let [permission_set] = await tx + .select({ entity_set: entities.set }) + .from(entities) + .where(driz.eq(entities.id, f.entity)); + if (!this.checkPermission(f.entity)) return; if (attribute.cardinality === "one") { let existingFact = await tx .select({ id: facts.id, data: facts.data }) @@ -104,9 +137,16 @@ export function serverMutationContext(tx: PgTransaction) { ); }, async retractFact(id) { + let [f] = await tx + .select() + .from(facts) + .rightJoin(entities, driz.eq(entities.id, facts.entity)) + .where(driz.eq(facts.id, id)); + if (!f || !this.checkPermission(f.entities.id)) return; await tx.delete(facts).where(driz.eq(facts.id, id)); }, async deleteEntity(entity) { + if (!this.checkPermission(entity)) return; await Promise.all([ tx.delete(entities).where(driz.eq(entities.id, entity)), tx diff --git a/supabase/database.types.ts b/supabase/database.types.ts index 126af9c9..3b4bd810 100644 --- a/supabase/database.types.ts +++ b/supabase/database.types.ts @@ -35,6 +35,32 @@ export type Database = { public: { Tables: { entities: { + Row: { + created_at: string + id: string + set: string + } + Insert: { + created_at?: string + id?: string + set: string + } + Update: { + created_at?: string + id?: string + set?: string + } + Relationships: [ + { + foreignKeyName: "entities_set_fkey" + columns: ["set"] + isOneToOne: false + referencedRelation: "entity_sets" + referencedColumns: ["id"] + }, + ] + } + entity_sets: { Row: { created_at: string id: string @@ -87,6 +113,74 @@ export type Database = { }, ] } + permission_token_rights: { + Row: { + change_entity_set: boolean + create_token: boolean + created_at: string + entity_set: string + read: boolean + token: string + write: boolean + } + Insert: { + change_entity_set?: boolean + create_token?: boolean + created_at?: string + entity_set: string + read?: boolean + token: string + write?: boolean + } + Update: { + change_entity_set?: boolean + create_token?: boolean + created_at?: string + entity_set?: string + read?: boolean + token?: string + write?: boolean + } + Relationships: [ + { + foreignKeyName: "permission_token_rights_entity_set_fkey" + columns: ["entity_set"] + isOneToOne: false + referencedRelation: "entity_sets" + referencedColumns: ["id"] + }, + { + foreignKeyName: "permission_token_rights_token_fkey" + columns: ["token"] + isOneToOne: false + referencedRelation: "permission_tokens" + referencedColumns: ["id"] + }, + ] + } + permission_tokens: { + Row: { + id: string + root_entity: string + } + Insert: { + id?: string + root_entity: string + } + Update: { + id?: string + root_entity?: string + } + Relationships: [ + { + foreignKeyName: "permission_tokens_root_entity_fkey" + columns: ["root_entity"] + isOneToOne: false + referencedRelation: "entities" + referencedColumns: ["id"] + }, + ] + } replicache_clients: { Row: { client_group: string diff --git a/supabase/migrations/20240703183954_add_permission_system.sql b/supabase/migrations/20240703183954_add_permission_system.sql new file mode 100644 index 00000000..1231f391 --- /dev/null +++ b/supabase/migrations/20240703183954_add_permission_system.sql @@ -0,0 +1,184 @@ +create table "public"."entity_sets" ( + "id" uuid not null default gen_random_uuid(), + "created_at" timestamp with time zone not null default now() +); + + +alter table "public"."entity_sets" enable row level security; + +create table "public"."permission_token_rights" ( + "token" uuid not null, + "entity_set" uuid not null, + "read" boolean not null default false, + "write" boolean not null default false, + "created_at" timestamp with time zone not null default now(), + "create_token" boolean not null default false, + "change_entity_set" boolean not null default false +); + + +alter table "public"."permission_token_rights" enable row level security; + +create table "public"."permission_tokens" ( + "id" uuid not null default gen_random_uuid(), + "root_entity" uuid not null +); + + +alter table "public"."permission_tokens" enable row level security; + +alter table "public"."entities" add column "set" uuid not null; + +CREATE UNIQUE INDEX entity_sets_pkey ON public.entity_sets USING btree (id); + +CREATE UNIQUE INDEX permission_token_rights_pkey ON public.permission_token_rights USING btree (token, entity_set); + +CREATE UNIQUE INDEX permission_tokens_pkey ON public.permission_tokens USING btree (id); + +alter table "public"."entity_sets" add constraint "entity_sets_pkey" PRIMARY KEY using index "entity_sets_pkey"; + +alter table "public"."permission_token_rights" add constraint "permission_token_rights_pkey" PRIMARY KEY using index "permission_token_rights_pkey"; + +alter table "public"."permission_tokens" add constraint "permission_tokens_pkey" PRIMARY KEY using index "permission_tokens_pkey"; + +alter table "public"."entities" add constraint "entities_set_fkey" FOREIGN KEY (set) REFERENCES entity_sets(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; + +alter table "public"."entities" validate constraint "entities_set_fkey"; + +alter table "public"."permission_token_rights" add constraint "permission_token_rights_entity_set_fkey" FOREIGN KEY (entity_set) REFERENCES entity_sets(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; + +alter table "public"."permission_token_rights" validate constraint "permission_token_rights_entity_set_fkey"; + +alter table "public"."permission_token_rights" add constraint "permission_token_rights_token_fkey" FOREIGN KEY (token) REFERENCES permission_tokens(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; + +alter table "public"."permission_token_rights" validate constraint "permission_token_rights_token_fkey"; + +alter table "public"."permission_tokens" add constraint "permission_tokens_root_entity_fkey" FOREIGN KEY (root_entity) REFERENCES entities(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; + +alter table "public"."permission_tokens" validate constraint "permission_tokens_root_entity_fkey"; + +grant delete on table "public"."entity_sets" to "anon"; + +grant insert on table "public"."entity_sets" to "anon"; + +grant references on table "public"."entity_sets" to "anon"; + +grant select on table "public"."entity_sets" to "anon"; + +grant trigger on table "public"."entity_sets" to "anon"; + +grant truncate on table "public"."entity_sets" to "anon"; + +grant update on table "public"."entity_sets" to "anon"; + +grant delete on table "public"."entity_sets" to "authenticated"; + +grant insert on table "public"."entity_sets" to "authenticated"; + +grant references on table "public"."entity_sets" to "authenticated"; + +grant select on table "public"."entity_sets" to "authenticated"; + +grant trigger on table "public"."entity_sets" to "authenticated"; + +grant truncate on table "public"."entity_sets" to "authenticated"; + +grant update on table "public"."entity_sets" to "authenticated"; + +grant delete on table "public"."entity_sets" to "service_role"; + +grant insert on table "public"."entity_sets" to "service_role"; + +grant references on table "public"."entity_sets" to "service_role"; + +grant select on table "public"."entity_sets" to "service_role"; + +grant trigger on table "public"."entity_sets" to "service_role"; + +grant truncate on table "public"."entity_sets" to "service_role"; + +grant update on table "public"."entity_sets" to "service_role"; + +grant delete on table "public"."permission_token_rights" to "anon"; + +grant insert on table "public"."permission_token_rights" to "anon"; + +grant references on table "public"."permission_token_rights" to "anon"; + +grant select on table "public"."permission_token_rights" to "anon"; + +grant trigger on table "public"."permission_token_rights" to "anon"; + +grant truncate on table "public"."permission_token_rights" to "anon"; + +grant update on table "public"."permission_token_rights" to "anon"; + +grant delete on table "public"."permission_token_rights" to "authenticated"; + +grant insert on table "public"."permission_token_rights" to "authenticated"; + +grant references on table "public"."permission_token_rights" to "authenticated"; + +grant select on table "public"."permission_token_rights" to "authenticated"; + +grant trigger on table "public"."permission_token_rights" to "authenticated"; + +grant truncate on table "public"."permission_token_rights" to "authenticated"; + +grant update on table "public"."permission_token_rights" to "authenticated"; + +grant delete on table "public"."permission_token_rights" to "service_role"; + +grant insert on table "public"."permission_token_rights" to "service_role"; + +grant references on table "public"."permission_token_rights" to "service_role"; + +grant select on table "public"."permission_token_rights" to "service_role"; + +grant trigger on table "public"."permission_token_rights" to "service_role"; + +grant truncate on table "public"."permission_token_rights" to "service_role"; + +grant update on table "public"."permission_token_rights" to "service_role"; + +grant delete on table "public"."permission_tokens" to "anon"; + +grant insert on table "public"."permission_tokens" to "anon"; + +grant references on table "public"."permission_tokens" to "anon"; + +grant select on table "public"."permission_tokens" to "anon"; + +grant trigger on table "public"."permission_tokens" to "anon"; + +grant truncate on table "public"."permission_tokens" to "anon"; + +grant update on table "public"."permission_tokens" to "anon"; + +grant delete on table "public"."permission_tokens" to "authenticated"; + +grant insert on table "public"."permission_tokens" to "authenticated"; + +grant references on table "public"."permission_tokens" to "authenticated"; + +grant select on table "public"."permission_tokens" to "authenticated"; + +grant trigger on table "public"."permission_tokens" to "authenticated"; + +grant truncate on table "public"."permission_tokens" to "authenticated"; + +grant update on table "public"."permission_tokens" to "authenticated"; + +grant delete on table "public"."permission_tokens" to "service_role"; + +grant insert on table "public"."permission_tokens" to "service_role"; + +grant references on table "public"."permission_tokens" to "service_role"; + +grant select on table "public"."permission_tokens" to "service_role"; + +grant trigger on table "public"."permission_tokens" to "service_role"; + +grant truncate on table "public"."permission_tokens" to "service_role"; + +grant update on table "public"."permission_tokens" to "service_role";