a tool for shared writing and social publishing

re-architect access around permission tokens

Tokens give you permissions over a set of entities. When an entity is
created it defines its set. Permissions are, read, write,
change_entity_set, and create_token. The latter two are unused as of
now.

Tokens also define their "root entities" so they can be used directly in
links to give access. This may later prove to be a bad idea, but works
for now!

In the code we add the context of EntitySetProvider. For now there is
just on at the top level of a doc, but we will need to wrap other
sections of the tree if we implement scoped permissions.

+613 -139
+27 -14
app/[doc_id]/Doc.tsx
··· 1 - import { Fact, ReplicacheProvider } from "src/replicache"; 1 + import { Fact, PermissionToken, ReplicacheProvider } from "src/replicache"; 2 2 import { Database } from "../../supabase/database.types"; 3 3 import { Attributes } from "src/replicache/attributes"; 4 4 import { createServerClient } from "@supabase/ssr"; ··· 8 8 import { MobileFooter } from "components/MobileFooter"; 9 9 import { PopUpProvider } from "components/Toast"; 10 10 import { YJSFragmentToString } from "components/TextBlock/RenderYJSFragment"; 11 + import { 12 + EntitySetContext, 13 + EntitySetProvider, 14 + } from "components/EntitySetProvider"; 11 15 export function Doc(props: { 16 + token: PermissionToken; 12 17 initialFacts: Fact<keyof typeof Attributes>[]; 13 18 doc_id: string; 14 19 }) { 15 20 return ( 16 - <ReplicacheProvider name={props.doc_id} initialFacts={props.initialFacts}> 17 - <PopUpProvider> 18 - <ThemeProvider entityID={props.doc_id}> 19 - <SelectionManager /> 20 - <div 21 - className="pageContentWrapper w-full relative overflow-x-scroll snap-x snap-mandatory no-scrollbar grow items-stretch flex h-full" 22 - id="card-carousel" 23 - > 24 - <Cards rootCard={props.doc_id} /> 25 - </div> 26 - <MobileFooter entityID={props.doc_id} /> 27 - </ThemeProvider> 28 - </PopUpProvider> 21 + <ReplicacheProvider 22 + token={props.token} 23 + name={props.doc_id} 24 + initialFacts={props.initialFacts} 25 + > 26 + <EntitySetProvider 27 + set={props.token.permission_token_rights[0].entity_set} 28 + > 29 + <PopUpProvider> 30 + <ThemeProvider entityID={props.doc_id}> 31 + <SelectionManager /> 32 + <div 33 + className="pageContentWrapper w-full relative overflow-x-scroll snap-x snap-mandatory no-scrollbar grow items-stretch flex h-full" 34 + id="card-carousel" 35 + > 36 + <Cards rootCard={props.doc_id} /> 37 + </div> 38 + <MobileFooter entityID={props.doc_id} /> 39 + </ThemeProvider> 40 + </PopUpProvider> 41 + </EntitySetProvider> 29 42 </ReplicacheProvider> 30 43 ); 31 44 }
+26 -6
app/[doc_id]/page.tsx
··· 24 24 { cookies: {} }, 25 25 ); 26 26 type Props = { 27 + // this is now a token id not doc! Should probs rename 27 28 params: { doc_id: string }; 28 29 }; 29 30 export default async function DocumentPage(props: Props) { 30 - let { data } = await supabase.rpc("get_facts", { root: props.params.doc_id }); 31 + let res = await supabase 32 + .from("permission_tokens") 33 + .select("*, permission_token_rights(*)") 34 + .eq("id", props.params.doc_id) 35 + .single(); 36 + let rootEntity = res.data?.root_entity; 37 + if (!rootEntity || !res.data) 38 + return <div>404 no rootEntity found idk man</div>; 39 + let { data } = await supabase.rpc("get_facts", { 40 + root: rootEntity, 41 + }); 31 42 let initialFacts = (data as unknown as Fact<keyof typeof Attributes>[]) || []; 32 - return <Doc initialFacts={initialFacts} doc_id={props.params.doc_id} />; 43 + return ( 44 + <Doc initialFacts={initialFacts} doc_id={rootEntity} token={res.data} /> 45 + ); 33 46 } 34 47 35 48 export async function generateMetadata(props: Props): Promise<Metadata> { 36 - let { data } = await supabase.rpc("get_facts", { root: props.params.doc_id }); 49 + let res = await supabase 50 + .from("permission_tokens") 51 + .select("*, permission_token_rights(*)") 52 + .eq("id", props.params.doc_id) 53 + .single(); 54 + let rootEntity = res.data?.root_entity; 55 + if (!rootEntity || !res.data) return { title: "Doc not found" }; 56 + let { data } = await supabase.rpc("get_facts", { 57 + root: rootEntity, 58 + }); 37 59 let initialFacts = (data as unknown as Fact<keyof typeof Attributes>[]) || []; 38 60 let blocks = initialFacts 39 - .filter( 40 - (f) => f.attribute === "card/block" && f.entity === props.params.doc_id, 41 - ) 61 + .filter((f) => f.attribute === "card/block" && f.entity === rootEntity) 42 62 .map((_f) => { 43 63 let block = _f as Fact<"card/block">; 44 64 let type = initialFacts.find(
+45 -4
app/page.tsx
··· 1 1 import { drizzle } from "drizzle-orm/postgres-js"; 2 - import { entities } from "drizzle/schema"; 2 + import { 3 + entities, 4 + permission_tokens, 5 + permission_token_rights, 6 + entity_sets, 7 + } from "drizzle/schema"; 3 8 import { redirect } from "next/navigation"; 4 9 import postgres from "postgres"; 5 10 import { Doc } from "./[doc_id]/Doc"; ··· 12 17 export const fetchCache = "force-no-store"; 13 18 14 19 export default async function RootPage() { 15 - let rows = await db.insert(entities).values({}).returning(); 20 + // Creating a new document 21 + let { permissionToken, rights, entity, entity_set } = await db.transaction( 22 + async (tx) => { 23 + // Create a new entity set 24 + let [entity_set] = await tx.insert(entity_sets).values({}).returning(); 25 + // Create a root-entity 26 + let [entity] = await tx 27 + .insert(entities) 28 + // And add it to that permission set 29 + .values({ set: entity_set.id }) 30 + .returning(); 31 + //Create a new permission token 32 + let [permissionToken] = await tx 33 + .insert(permission_tokens) 34 + .values({ root_entity: entity.id }) 35 + .returning(); 36 + //and give it all the permission on that entity set 37 + let [rights] = await tx 38 + .insert(permission_token_rights) 39 + .values({ 40 + token: permissionToken.id, 41 + entity_set: entity_set.id, 42 + read: true, 43 + write: true, 44 + create_token: true, 45 + change_entity_set: true, 46 + }) 47 + .returning(); 48 + return { permissionToken, rights, entity, entity_set }; 49 + }, 50 + ); 51 + // Here i need to pass the permission token instead of the doc_id 52 + // In the replicache provider I guess I need to fetch the relevant stuff of the permission token? 16 53 return ( 17 54 <> 18 - <UpdateURL url={`/${rows[0].id}`} /> 19 - <Doc doc_id={rows[0].id} initialFacts={[]} /> 55 + <UpdateURL url={`/${permissionToken.id}`} /> 56 + <Doc 57 + doc_id={entity.id} 58 + token={{ id: permissionToken.id, permission_token_rights: [rights] }} 59 + initialFacts={[]} 60 + /> 20 61 </> 21 62 ); 22 63 }
+6
components/BlockOptions.tsx
··· 13 13 import { useState } from "react"; 14 14 import { Separator } from "./Layout"; 15 15 import { addLinkBlock } from "src/utils/addLinkBlock"; 16 + import { useEntitySetContext } from "./EntitySetProvider"; 16 17 17 18 type Props = { 18 19 parent: string; ··· 23 24 }; 24 25 export function BlockOptions(props: Props) { 25 26 let { rep } = useReplicache(); 27 + let entity_set = useEntitySetContext(); 26 28 27 29 let focusedElement = useUIState((s) => s.focusedBlock); 28 30 let focusedCardID = ··· 51 53 entity = crypto.randomUUID(); 52 54 await rep?.mutate.addBlock({ 53 55 parent: props.parent, 56 + permission_set: entity_set.set, 54 57 type: "text", 55 58 position: generateKeyBetween( 56 59 props.position, ··· 81 84 let entity = crypto.randomUUID(); 82 85 83 86 await rep?.mutate.addBlock({ 87 + permission_set: entity_set.set, 84 88 parent: props.parent, 85 89 type: "card", 86 90 position: generateKeyBetween( ··· 110 114 } 111 115 112 116 const BlockLinkButton = (props: Props) => { 117 + let entity_set = useEntitySetContext(); 113 118 let [linkOpen, setLinkOpen] = useState(false); 114 119 let [linkValue, setLinkValue] = useState(""); 115 120 let { rep } = useReplicache(); ··· 119 124 entity = crypto.randomUUID(); 120 125 121 126 await rep?.mutate.addBlock({ 127 + permission_set: entity_set.set, 122 128 parent: props.parent, 123 129 type: "card", 124 130 position: generateKeyBetween(props.position, props.nextPosition),
+9
components/Blocks.tsx
··· 13 13 import { BlockOptions } from "./BlockOptions"; 14 14 import { useBlocks } from "src/hooks/queries/useBlocks"; 15 15 import { setEditorState, useEditorStates } from "src/state/useEditorState"; 16 + import { useEntitySetContext } from "./EntitySetProvider"; 16 17 17 18 export type Block = { 18 19 parent: string; ··· 22 23 }; 23 24 export function Blocks(props: { entityID: string }) { 24 25 let rep = useReplicache(); 26 + let entity_set = useEntitySetContext(); 25 27 let blocks = useBlocks(props.entityID); 26 28 27 29 let lastBlock = blocks[blocks.length - 1]; ··· 38 40 let newEntityID = crypto.randomUUID(); 39 41 await rep.rep?.mutate.addBlock({ 40 42 parent: props.entityID, 43 + permission_set: entity_set.set, 41 44 type: "text", 42 45 position: generateKeyBetween(lastBlock.position || null, null), 43 46 newEntityID, ··· 77 80 focusBlock({ ...lastBlock, type: "text" }, { type: "end" }); 78 81 } else { 79 82 rep?.rep?.mutate.addBlock({ 83 + permission_set: entity_set.set, 80 84 parent: props.entityID, 81 85 type: "text", 82 86 position: generateKeyBetween(lastBlock?.position || null, null), ··· 97 101 98 102 function NewBlockButton(props: { lastBlock: Block | null; entityID: string }) { 99 103 let { rep } = useReplicache(); 104 + let entity_set = useEntitySetContext(); 100 105 let textContent = useEntity( 101 106 props.lastBlock?.type === "text" ? props.lastBlock.value : null, 102 107 "block/text", ··· 115 120 await rep?.mutate.addBlock({ 116 121 parent: props.entityID, 117 122 type: "text", 123 + permission_set: entity_set.set, 118 124 position: generateKeyBetween( 119 125 props.lastBlock?.position || null, 120 126 null, ··· 161 167 ); 162 168 let { rep } = useReplicache(); 163 169 170 + let entity_set = useEntitySetContext(); 164 171 useEffect(() => { 165 172 if (!selected || !rep) return; 166 173 let r = rep; ··· 198 205 if (e.key === "Enter") { 199 206 let newEntityID = crypto.randomUUID(); 200 207 r.mutate.addBlock({ 208 + permission_set: entity_set.set, 201 209 newEntityID, 202 210 parent: props.parent, 203 211 type: "text", ··· 215 223 window.addEventListener("keydown", listener); 216 224 return () => window.removeEventListener("keydown", listener); 217 225 }, [ 226 + entity_set, 218 227 selected, 219 228 props.entityID, 220 229 props.nextBlock,
+28
components/EntitySetProvider.tsx
··· 1 + "use client"; 2 + import { createContext, useContext } from "react"; 3 + import { useReplicache } from "src/replicache"; 4 + 5 + export const EntitySetContext = createContext({ 6 + set: "", 7 + permissions: { read: false, write: false }, 8 + }); 9 + export const useEntitySetContext = () => useContext(EntitySetContext); 10 + 11 + export function EntitySetProvider(props: { 12 + set: string; 13 + children: React.ReactNode; 14 + }) { 15 + let { permission_token } = useReplicache(); 16 + return ( 17 + <EntitySetContext.Provider 18 + value={{ 19 + set: props.set, 20 + permissions: permission_token.permission_token_rights.find( 21 + (r) => r.entity_set === props.set, 22 + ) || { read: false, write: false }, 23 + }} 24 + > 25 + {props.children} 26 + </EntitySetContext.Provider> 27 + ); 28 + }
+16 -8
components/TextBlock/index.tsx
··· 37 37 import { useIsMobile } from "src/hooks/isMobile"; 38 38 import { setMark } from "src/utils/prosemirror/setMark"; 39 39 import { rangeHasMark } from "src/utils/prosemirror/rangeHasMark"; 40 + import { useEntitySetContext } from "components/EntitySetProvider"; 40 41 41 42 export function TextBlock(props: BlockProps & { className: string }) { 42 43 let initialized = useInitialPageLoad(); 43 44 let first = props.previousBlock === null; 45 + let permission = useEntitySetContext().permissions.write; 44 46 return ( 45 47 <> 46 - {!initialized && ( 48 + {(!initialized || !permission) && ( 47 49 <RenderedTextBlock 48 50 entityID={props.entityID} 49 51 className={props.className} ··· 51 53 first={first} 52 54 /> 53 55 )} 54 - <div className={`relative group/text ${!initialized ? "hidden" : ""}`}> 55 - <IOSBS {...props} /> 56 - <BaseTextBlock {...props} /> 57 - </div> 56 + {permission && ( 57 + <div className={`relative group/text ${!initialized ? "hidden" : ""}`}> 58 + <IOSBS {...props} /> 59 + <BaseTextBlock {...props} /> 60 + </div> 61 + )} 58 62 </> 59 63 ); 60 64 } ··· 139 143 140 144 let [value, factID] = useYJSValue(props.entityID); 141 145 let repRef = useRef<null | Replicache<ReplicacheMutators>>(null); 142 - let propsRef = useRef(props); 146 + let entity_set = useEntitySetContext(); 147 + let propsRef = useRef({ ...props, entity_set }); 143 148 useEffect(() => { 144 - propsRef.current = props; 145 - }, [props]); 149 + propsRef.current = { ...props, entity_set }; 150 + }, [props, entity_set]); 146 151 let rep = useReplicache(); 147 152 useEffect(() => { 148 153 repRef.current = rep.rep; ··· 244 249 propsRef.current.nextPosition, 245 250 ); 246 251 repRef.current?.mutate.addBlock({ 252 + permission_set: entity_set.set, 247 253 newEntityID: entityID, 248 254 parent: propsRef.current.parent, 249 255 type: type, ··· 297 303 propsRef.current.nextPosition, 298 304 ); 299 305 repRef.current?.mutate.addBlock({ 306 + permission_set: entity_set.set, 300 307 newEntityID, 301 308 parent: propsRef.current.parent, 302 309 type: "text", ··· 332 339 } else { 333 340 entity = crypto.randomUUID(); 334 341 rep.rep.mutate.addBlock({ 342 + permission_set: entity_set.set, 335 343 type: "image", 336 344 newEntityID: entity, 337 345 parent: props.parent,
+3 -2
components/TextBlock/keymap.ts
··· 13 13 import { focusCard } from "components/Cards"; 14 14 15 15 export const TextBlockKeymap = ( 16 - propsRef: MutableRefObject<BlockProps>, 16 + propsRef: MutableRefObject<BlockProps & { entity_set: { set: string } }>, 17 17 repRef: MutableRefObject<Replicache<ReplicacheMutators> | null>, 18 18 ) => 19 19 keymap({ ··· 240 240 241 241 const enter = 242 242 ( 243 - propsRef: MutableRefObject<BlockProps>, 243 + propsRef: MutableRefObject<BlockProps & { entity_set: { set: string } }>, 244 244 repRef: MutableRefObject<Replicache<ReplicacheMutators> | null>, 245 245 ) => 246 246 (state: EditorState, dispatch?: (tr: Transaction) => void) => { ··· 255 255 ); 256 256 repRef.current?.mutate.addBlock({ 257 257 newEntityID, 258 + permission_set: propsRef.current.entity_set.set, 258 259 parent: propsRef.current.parent, 259 260 type: "text", 260 261 position,
+5 -3
components/UpdateURL.tsx
··· 1 1 "use client"; 2 + import { useRouter } from "next/navigation"; 2 3 import { useEffect } from "react"; 3 4 4 5 export function UpdateURL(props: { url: string }) { 6 + let router = useRouter(); 5 7 useEffect(() => { 6 - window.history.replaceState(null, "", props.url); 7 - }, [props.url]); 8 - return null; 8 + router.replace(props.url); 9 + }, [props.url, router]); 10 + return <></>; 9 11 }
+32 -3
drizzle/relations.ts
··· 1 1 import { relations } from "drizzle-orm/relations"; 2 - import { entities, facts } from "./schema"; 2 + import { entity_sets, entities, permission_tokens, facts, permission_token_rights } from "./schema"; 3 + 4 + export const entitiesRelations = relations(entities, ({one, many}) => ({ 5 + entity_set: one(entity_sets, { 6 + fields: [entities.set], 7 + references: [entity_sets.id] 8 + }), 9 + permission_tokens: many(permission_tokens), 10 + facts: many(facts), 11 + })); 12 + 13 + export const entity_setsRelations = relations(entity_sets, ({many}) => ({ 14 + entities: many(entities), 15 + permission_token_rights: many(permission_token_rights), 16 + })); 17 + 18 + export const permission_tokensRelations = relations(permission_tokens, ({one, many}) => ({ 19 + entity: one(entities, { 20 + fields: [permission_tokens.root_entity], 21 + references: [entities.id] 22 + }), 23 + permission_token_rights: many(permission_token_rights), 24 + })); 3 25 4 26 export const factsRelations = relations(facts, ({one}) => ({ 5 27 entity: one(entities, { ··· 8 30 }), 9 31 })); 10 32 11 - export const entitiesRelations = relations(entities, ({many}) => ({ 12 - facts: many(facts), 33 + export const permission_token_rightsRelations = relations(permission_token_rights, ({one}) => ({ 34 + entity_set: one(entity_sets, { 35 + fields: [permission_token_rights.entity_set], 36 + references: [entity_sets.id] 37 + }), 38 + permission_token: one(permission_tokens, { 39 + fields: [permission_token_rights.token], 40 + references: [permission_tokens.id] 41 + }), 13 42 }));
+54 -86
drizzle/schema.ts
··· 1 - import { 2 - pgTable, 3 - pgEnum, 4 - uuid, 5 - timestamp, 6 - text, 7 - bigint, 8 - foreignKey, 9 - jsonb, 10 - } from "drizzle-orm/pg-core"; 11 - import { Fact } from "src/replicache"; 12 - import { Attributes } from "src/replicache/attributes"; 1 + import { pgTable, pgEnum, text, bigint, foreignKey, uuid, timestamp, jsonb, primaryKey, boolean } from "drizzle-orm/pg-core" 2 + import { sql } from "drizzle-orm" 13 3 14 - export const aal_level = pgEnum("aal_level", ["aal1", "aal2", "aal3"]); 15 - export const code_challenge_method = pgEnum("code_challenge_method", [ 16 - "s256", 17 - "plain", 18 - ]); 19 - export const factor_status = pgEnum("factor_status", [ 20 - "unverified", 21 - "verified", 22 - ]); 23 - export const factor_type = pgEnum("factor_type", ["totp", "webauthn"]); 24 - export const request_status = pgEnum("request_status", [ 25 - "PENDING", 26 - "SUCCESS", 27 - "ERROR", 28 - ]); 29 - export const key_status = pgEnum("key_status", [ 30 - "default", 31 - "valid", 32 - "invalid", 33 - "expired", 34 - ]); 35 - export const key_type = pgEnum("key_type", [ 36 - "aead-ietf", 37 - "aead-det", 38 - "hmacsha512", 39 - "hmacsha256", 40 - "auth", 41 - "shorthash", 42 - "generichash", 43 - "kdf", 44 - "secretbox", 45 - "secretstream", 46 - "stream_xchacha20", 47 - ]); 48 - export const action = pgEnum("action", [ 49 - "INSERT", 50 - "UPDATE", 51 - "DELETE", 52 - "TRUNCATE", 53 - "ERROR", 54 - ]); 55 - export const equality_op = pgEnum("equality_op", [ 56 - "eq", 57 - "neq", 58 - "lt", 59 - "lte", 60 - "gt", 61 - "gte", 62 - "in", 63 - ]); 4 + export const aal_level = pgEnum("aal_level", ['aal1', 'aal2', 'aal3']) 5 + export const code_challenge_method = pgEnum("code_challenge_method", ['s256', 'plain']) 6 + export const factor_status = pgEnum("factor_status", ['unverified', 'verified']) 7 + export const factor_type = pgEnum("factor_type", ['totp', 'webauthn']) 8 + 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']) 9 + export const request_status = pgEnum("request_status", ['PENDING', 'SUCCESS', 'ERROR']) 10 + export const key_status = pgEnum("key_status", ['default', 'valid', 'invalid', 'expired']) 11 + export const key_type = pgEnum("key_type", ['aead-ietf', 'aead-det', 'hmacsha512', 'hmacsha256', 'auth', 'shorthash', 'generichash', 'kdf', 'secretbox', 'secretstream', 'stream_xchacha20']) 12 + export const action = pgEnum("action", ['INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'ERROR']) 13 + export const equality_op = pgEnum("equality_op", ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'in']) 14 + 15 + 16 + export const replicache_clients = pgTable("replicache_clients", { 17 + client_id: text("client_id").primaryKey().notNull(), 18 + client_group: text("client_group").notNull(), 19 + // You can use { mode: "bigint" } if numbers are exceeding js number limitations 20 + last_mutation: bigint("last_mutation", { mode: "number" }).notNull(), 21 + }); 64 22 65 23 export const entities = pgTable("entities", { 66 - id: uuid("id").defaultRandom().primaryKey().notNull(), 67 - created_at: timestamp("created_at", { withTimezone: true, mode: "string" }) 68 - .defaultNow() 69 - .notNull(), 24 + id: uuid("id").defaultRandom().primaryKey().notNull(), 25 + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 26 + set: uuid("set").notNull().references(() => entity_sets.id, { onDelete: "cascade", onUpdate: "cascade" } ), 27 + }); 28 + 29 + export const entity_sets = pgTable("entity_sets", { 30 + id: uuid("id").defaultRandom().primaryKey().notNull(), 31 + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 70 32 }); 71 33 72 - export const replicache_clients = pgTable("replicache_clients", { 73 - client_id: text("client_id").primaryKey().notNull(), 74 - client_group: text("client_group").notNull(), 75 - // You can use { mode: "bigint" } if numbers are exceeding js number limitations 76 - last_mutation: bigint("last_mutation", { mode: "number" }).notNull(), 34 + export const permission_tokens = pgTable("permission_tokens", { 35 + id: uuid("id").defaultRandom().primaryKey().notNull(), 36 + root_entity: uuid("root_entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "cascade" } ), 77 37 }); 78 38 79 39 export const facts = pgTable("facts", { 80 - id: uuid("id").defaultRandom().primaryKey().notNull(), 81 - entity: uuid("entity") 82 - .notNull() 83 - .references(() => entities.id, { 84 - onDelete: "cascade", 85 - onUpdate: "restrict", 86 - }), 87 - attribute: text("attribute").notNull().$type<keyof typeof Attributes>(), 88 - data: jsonb("data").notNull().$type<Fact<any>["data"]>(), 89 - created_at: timestamp("created_at", { mode: "string" }) 90 - .defaultNow() 91 - .notNull(), 92 - updated_at: timestamp("updated_at", { mode: "string" }), 93 - // You can use { mode: "bigint" } if numbers are exceeding js number limitations 94 - version: bigint("version", { mode: "number" }).default(0).notNull(), 40 + id: uuid("id").defaultRandom().primaryKey().notNull(), 41 + entity: uuid("entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "restrict" } ), 42 + attribute: text("attribute").notNull(), 43 + data: jsonb("data").notNull(), 44 + created_at: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(), 45 + updated_at: timestamp("updated_at", { mode: 'string' }), 46 + // You can use { mode: "bigint" } if numbers are exceeding js number limitations 47 + version: bigint("version", { mode: "number" }).default(0).notNull(), 95 48 }); 49 + 50 + export const permission_token_rights = pgTable("permission_token_rights", { 51 + token: uuid("token").notNull().references(() => permission_tokens.id, { onDelete: "cascade", onUpdate: "cascade" } ), 52 + entity_set: uuid("entity_set").notNull().references(() => entity_sets.id, { onDelete: "cascade", onUpdate: "cascade" } ), 53 + read: boolean("read").default(false).notNull(), 54 + write: boolean("write").default(false).notNull(), 55 + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 56 + create_token: boolean("create_token").default(false).notNull(), 57 + change_entity_set: boolean("change_entity_set").default(false).notNull(), 58 + }, 59 + (table) => { 60 + return { 61 + permission_token_rights_pkey: primaryKey({ columns: [table.token, table.entity_set], name: "permission_token_rights_pkey"}), 62 + } 63 + });
+2 -2
src/replicache/clientMutationContext.ts
··· 14 14 let supabase = supabaseBrowserClient(); 15 15 return cb({ supabase }); 16 16 }, 17 - async createEntity(_entityID) { 18 - tx.set(_entityID, true); 17 + async createEntity({ entityID }) { 18 + tx.set(entityID, true); 19 19 return true; 20 20 }, 21 21 scanIndex: {
+17 -2
src/replicache/index.tsx
··· 19 19 let ReplicacheContext = createContext({ 20 20 rep: null as null | Replicache<ReplicacheMutators>, 21 21 initialFacts: [] as Fact<keyof typeof Attributes>[], 22 + permission_token: {} as PermissionToken, 22 23 }); 23 24 export function useReplicache() { 24 25 return useContext(ReplicacheContext); ··· 28 29 tx: WriteTransaction, 29 30 args: Parameters<(typeof mutations)[k]>[0], 30 31 ) => Promise<void>; 32 + }; 33 + 34 + export type PermissionToken = { 35 + id: string; 36 + permission_token_rights: { 37 + entity_set: string; 38 + read: boolean; 39 + write: boolean; 40 + }[]; 31 41 }; 32 42 export function ReplicacheProvider(props: { 33 43 initialFacts: Fact<keyof typeof Attributes>[]; 44 + token: PermissionToken; 34 45 name: string; 35 46 children: React.ReactNode; 36 47 }) { ··· 55 66 licenseKey: "l381074b8d5224dabaef869802421225a", 56 67 pusher: async (pushRequest) => { 57 68 return { 58 - response: await Push(pushRequest, props.name), 69 + response: await Push(pushRequest, props.name, props.token), 59 70 httpRequestInfo: { errorMessage: "", httpStatusCode: 200 }, 60 71 }; 61 72 }, ··· 87 98 }, [props.name]); 88 99 return ( 89 100 <ReplicacheContext.Provider 90 - value={{ rep, initialFacts: props.initialFacts }} 101 + value={{ 102 + rep, 103 + initialFacts: props.initialFacts, 104 + permission_token: props.token, 105 + }} 91 106 > 92 107 {props.children} 93 108 </ReplicacheContext.Provider>
+9 -2
src/replicache/mutations.ts
··· 5 5 import { Database } from "supabase/database.types"; 6 6 7 7 export type MutationContext = { 8 - createEntity: (entityID: string) => Promise<boolean>; 8 + createEntity: (args: { 9 + entityID: string; 10 + permission_set: string; 11 + }) => Promise<boolean>; 9 12 scanIndex: { 10 13 eav: <A extends keyof typeof Attributes>( 11 14 entity: string, ··· 29 32 30 33 const addBlock: Mutation<{ 31 34 parent: string; 35 + permission_set: string; 32 36 type: Fact<"block/type">["data"]["value"]; 33 37 newEntityID: string; 34 38 position: string; 35 39 }> = async (args, ctx) => { 36 - await ctx.createEntity(args.newEntityID); 40 + await ctx.createEntity({ 41 + entityID: args.newEntityID, 42 + permission_set: args.permission_set, 43 + }); 37 44 await ctx.assertFact({ 38 45 entity: args.parent, 39 46 data: {
+11 -2
src/replicache/push.ts
··· 3 3 import { serverMutationContext } from "./serverMutationContext"; 4 4 import { mutations } from "./mutations"; 5 5 import { drizzle } from "drizzle-orm/postgres-js"; 6 + import { eq } from "drizzle-orm"; 6 7 import postgres from "postgres"; 7 - import { replicache_clients } from "drizzle/schema"; 8 + import { permission_token_rights, replicache_clients } from "drizzle/schema"; 8 9 import { getClientGroup } from "./utils"; 9 10 import { createClient } from "@supabase/supabase-js"; 10 11 import { Database } from "supabase/database.types"; ··· 18 19 export async function Push( 19 20 pushRequest: PushRequest, 20 21 rootEntity: string, 22 + token: { id: string }, 21 23 ): Promise<PushResponse | undefined> { 22 24 if (pushRequest.pushVersion !== 1) 23 25 return { error: "VersionNotSupported", versionType: "push" }; 24 26 let clientGroup = await getClientGroup(db, pushRequest.clientGroupID); 27 + let token_rights = await db 28 + .select() 29 + .from(permission_token_rights) 30 + .where(eq(permission_token_rights.token, token.id)); 25 31 for (let mutation of pushRequest.mutations) { 26 32 let lastMutationID = clientGroup[mutation.clientID] || 0; 27 33 if (mutation.id <= lastMutationID) continue; ··· 32 38 } 33 39 await db.transaction(async (tx) => { 34 40 try { 35 - await mutations[name](mutation.args as any, serverMutationContext(tx)); 41 + await mutations[name]( 42 + mutation.args as any, 43 + serverMutationContext(tx, token_rights), 44 + ); 36 45 } catch (e) { 37 46 console.log( 38 47 `Error occured while running mutation: ${name}`,
+45 -5
src/replicache/serverMutationContext.ts
··· 5 5 import { MutationContext } from "./mutations"; 6 6 import { entities, facts } from "drizzle/schema"; 7 7 import { Attributes, FilterAttributes } from "./attributes"; 8 - import { Fact } from "."; 8 + import { Fact, PermissionToken } from "."; 9 9 import { DeepReadonly } from "replicache"; 10 10 import { createClient } from "@supabase/supabase-js"; 11 11 import { Database } from "supabase/database.types"; 12 - export function serverMutationContext(tx: PgTransaction<any, any, any>) { 13 - let ctx: MutationContext = { 12 + export function serverMutationContext( 13 + tx: PgTransaction<any, any, any>, 14 + token_rights: PermissionToken["permission_token_rights"], 15 + ) { 16 + let ctx: MutationContext & { 17 + checkPermission: (entity: string) => Promise<boolean>; 18 + } = { 14 19 async runOnServer(cb) { 15 20 let supabase = createClient<Database>( 16 21 process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, ··· 18 23 ); 19 24 return cb({ supabase }); 20 25 }, 26 + async checkPermission(entity: string) { 27 + let [permission_set] = await tx 28 + .select({ entity_set: entities.set }) 29 + .from(entities) 30 + .where(driz.eq(entities.id, entity)); 31 + return ( 32 + !!permission_set && 33 + !!token_rights.find( 34 + (r) => r.entity_set === permission_set.entity_set && r.write == true, 35 + ) 36 + ); 37 + }, 21 38 async runOnClient(_cb) {}, 22 - async createEntity(entity) { 39 + async createEntity({ entityID, permission_set }) { 40 + if ( 41 + !token_rights.find( 42 + (r) => r.entity_set === permission_set && r.write === true, 43 + ) 44 + ) { 45 + console.log("NO RIGHT???"); 46 + console.log(token_rights); 47 + console.log(permission_set); 48 + return false; 49 + } 23 50 await tx.transaction( 24 51 async (tx2) => 25 52 await tx2 26 53 .insert(entities) 27 54 .values({ 28 - id: entity, 55 + set: permission_set, 56 + id: entityID, 29 57 }) 30 58 .catch(console.log), 31 59 ); ··· 54 82 if (!attribute) return; 55 83 let id = f.id || crypto.randomUUID(); 56 84 let data = { ...f.data }; 85 + let [permission_set] = await tx 86 + .select({ entity_set: entities.set }) 87 + .from(entities) 88 + .where(driz.eq(entities.id, f.entity)); 89 + if (!this.checkPermission(f.entity)) return; 57 90 if (attribute.cardinality === "one") { 58 91 let existingFact = await tx 59 92 .select({ id: facts.id, data: facts.data }) ··· 104 137 ); 105 138 }, 106 139 async retractFact(id) { 140 + let [f] = await tx 141 + .select() 142 + .from(facts) 143 + .rightJoin(entities, driz.eq(entities.id, facts.entity)) 144 + .where(driz.eq(facts.id, id)); 145 + if (!f || !this.checkPermission(f.entities.id)) return; 107 146 await tx.delete(facts).where(driz.eq(facts.id, id)); 108 147 }, 109 148 async deleteEntity(entity) { 149 + if (!this.checkPermission(entity)) return; 110 150 await Promise.all([ 111 151 tx.delete(entities).where(driz.eq(entities.id, entity)), 112 152 tx
+94
supabase/database.types.ts
··· 38 38 Row: { 39 39 created_at: string 40 40 id: string 41 + set: string 42 + } 43 + Insert: { 44 + created_at?: string 45 + id?: string 46 + set: string 47 + } 48 + Update: { 49 + created_at?: string 50 + id?: string 51 + set?: string 52 + } 53 + Relationships: [ 54 + { 55 + foreignKeyName: "entities_set_fkey" 56 + columns: ["set"] 57 + isOneToOne: false 58 + referencedRelation: "entity_sets" 59 + referencedColumns: ["id"] 60 + }, 61 + ] 62 + } 63 + entity_sets: { 64 + Row: { 65 + created_at: string 66 + id: string 41 67 } 42 68 Insert: { 43 69 created_at?: string ··· 81 107 { 82 108 foreignKeyName: "facts_entity_fkey" 83 109 columns: ["entity"] 110 + isOneToOne: false 111 + referencedRelation: "entities" 112 + referencedColumns: ["id"] 113 + }, 114 + ] 115 + } 116 + permission_token_rights: { 117 + Row: { 118 + change_entity_set: boolean 119 + create_token: boolean 120 + created_at: string 121 + entity_set: string 122 + read: boolean 123 + token: string 124 + write: boolean 125 + } 126 + Insert: { 127 + change_entity_set?: boolean 128 + create_token?: boolean 129 + created_at?: string 130 + entity_set: string 131 + read?: boolean 132 + token: string 133 + write?: boolean 134 + } 135 + Update: { 136 + change_entity_set?: boolean 137 + create_token?: boolean 138 + created_at?: string 139 + entity_set?: string 140 + read?: boolean 141 + token?: string 142 + write?: boolean 143 + } 144 + Relationships: [ 145 + { 146 + foreignKeyName: "permission_token_rights_entity_set_fkey" 147 + columns: ["entity_set"] 148 + isOneToOne: false 149 + referencedRelation: "entity_sets" 150 + referencedColumns: ["id"] 151 + }, 152 + { 153 + foreignKeyName: "permission_token_rights_token_fkey" 154 + columns: ["token"] 155 + isOneToOne: false 156 + referencedRelation: "permission_tokens" 157 + referencedColumns: ["id"] 158 + }, 159 + ] 160 + } 161 + permission_tokens: { 162 + Row: { 163 + id: string 164 + root_entity: string 165 + } 166 + Insert: { 167 + id?: string 168 + root_entity: string 169 + } 170 + Update: { 171 + id?: string 172 + root_entity?: string 173 + } 174 + Relationships: [ 175 + { 176 + foreignKeyName: "permission_tokens_root_entity_fkey" 177 + columns: ["root_entity"] 84 178 isOneToOne: false 85 179 referencedRelation: "entities" 86 180 referencedColumns: ["id"]
+184
supabase/migrations/20240703183954_add_permission_system.sql
··· 1 + create table "public"."entity_sets" ( 2 + "id" uuid not null default gen_random_uuid(), 3 + "created_at" timestamp with time zone not null default now() 4 + ); 5 + 6 + 7 + alter table "public"."entity_sets" enable row level security; 8 + 9 + create table "public"."permission_token_rights" ( 10 + "token" uuid not null, 11 + "entity_set" uuid not null, 12 + "read" boolean not null default false, 13 + "write" boolean not null default false, 14 + "created_at" timestamp with time zone not null default now(), 15 + "create_token" boolean not null default false, 16 + "change_entity_set" boolean not null default false 17 + ); 18 + 19 + 20 + alter table "public"."permission_token_rights" enable row level security; 21 + 22 + create table "public"."permission_tokens" ( 23 + "id" uuid not null default gen_random_uuid(), 24 + "root_entity" uuid not null 25 + ); 26 + 27 + 28 + alter table "public"."permission_tokens" enable row level security; 29 + 30 + alter table "public"."entities" add column "set" uuid not null; 31 + 32 + CREATE UNIQUE INDEX entity_sets_pkey ON public.entity_sets USING btree (id); 33 + 34 + CREATE UNIQUE INDEX permission_token_rights_pkey ON public.permission_token_rights USING btree (token, entity_set); 35 + 36 + CREATE UNIQUE INDEX permission_tokens_pkey ON public.permission_tokens USING btree (id); 37 + 38 + alter table "public"."entity_sets" add constraint "entity_sets_pkey" PRIMARY KEY using index "entity_sets_pkey"; 39 + 40 + alter table "public"."permission_token_rights" add constraint "permission_token_rights_pkey" PRIMARY KEY using index "permission_token_rights_pkey"; 41 + 42 + alter table "public"."permission_tokens" add constraint "permission_tokens_pkey" PRIMARY KEY using index "permission_tokens_pkey"; 43 + 44 + alter table "public"."entities" add constraint "entities_set_fkey" FOREIGN KEY (set) REFERENCES entity_sets(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; 45 + 46 + alter table "public"."entities" validate constraint "entities_set_fkey"; 47 + 48 + 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; 49 + 50 + alter table "public"."permission_token_rights" validate constraint "permission_token_rights_entity_set_fkey"; 51 + 52 + 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; 53 + 54 + alter table "public"."permission_token_rights" validate constraint "permission_token_rights_token_fkey"; 55 + 56 + 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; 57 + 58 + alter table "public"."permission_tokens" validate constraint "permission_tokens_root_entity_fkey"; 59 + 60 + grant delete on table "public"."entity_sets" to "anon"; 61 + 62 + grant insert on table "public"."entity_sets" to "anon"; 63 + 64 + grant references on table "public"."entity_sets" to "anon"; 65 + 66 + grant select on table "public"."entity_sets" to "anon"; 67 + 68 + grant trigger on table "public"."entity_sets" to "anon"; 69 + 70 + grant truncate on table "public"."entity_sets" to "anon"; 71 + 72 + grant update on table "public"."entity_sets" to "anon"; 73 + 74 + grant delete on table "public"."entity_sets" to "authenticated"; 75 + 76 + grant insert on table "public"."entity_sets" to "authenticated"; 77 + 78 + grant references on table "public"."entity_sets" to "authenticated"; 79 + 80 + grant select on table "public"."entity_sets" to "authenticated"; 81 + 82 + grant trigger on table "public"."entity_sets" to "authenticated"; 83 + 84 + grant truncate on table "public"."entity_sets" to "authenticated"; 85 + 86 + grant update on table "public"."entity_sets" to "authenticated"; 87 + 88 + grant delete on table "public"."entity_sets" to "service_role"; 89 + 90 + grant insert on table "public"."entity_sets" to "service_role"; 91 + 92 + grant references on table "public"."entity_sets" to "service_role"; 93 + 94 + grant select on table "public"."entity_sets" to "service_role"; 95 + 96 + grant trigger on table "public"."entity_sets" to "service_role"; 97 + 98 + grant truncate on table "public"."entity_sets" to "service_role"; 99 + 100 + grant update on table "public"."entity_sets" to "service_role"; 101 + 102 + grant delete on table "public"."permission_token_rights" to "anon"; 103 + 104 + grant insert on table "public"."permission_token_rights" to "anon"; 105 + 106 + grant references on table "public"."permission_token_rights" to "anon"; 107 + 108 + grant select on table "public"."permission_token_rights" to "anon"; 109 + 110 + grant trigger on table "public"."permission_token_rights" to "anon"; 111 + 112 + grant truncate on table "public"."permission_token_rights" to "anon"; 113 + 114 + grant update on table "public"."permission_token_rights" to "anon"; 115 + 116 + grant delete on table "public"."permission_token_rights" to "authenticated"; 117 + 118 + grant insert on table "public"."permission_token_rights" to "authenticated"; 119 + 120 + grant references on table "public"."permission_token_rights" to "authenticated"; 121 + 122 + grant select on table "public"."permission_token_rights" to "authenticated"; 123 + 124 + grant trigger on table "public"."permission_token_rights" to "authenticated"; 125 + 126 + grant truncate on table "public"."permission_token_rights" to "authenticated"; 127 + 128 + grant update on table "public"."permission_token_rights" to "authenticated"; 129 + 130 + grant delete on table "public"."permission_token_rights" to "service_role"; 131 + 132 + grant insert on table "public"."permission_token_rights" to "service_role"; 133 + 134 + grant references on table "public"."permission_token_rights" to "service_role"; 135 + 136 + grant select on table "public"."permission_token_rights" to "service_role"; 137 + 138 + grant trigger on table "public"."permission_token_rights" to "service_role"; 139 + 140 + grant truncate on table "public"."permission_token_rights" to "service_role"; 141 + 142 + grant update on table "public"."permission_token_rights" to "service_role"; 143 + 144 + grant delete on table "public"."permission_tokens" to "anon"; 145 + 146 + grant insert on table "public"."permission_tokens" to "anon"; 147 + 148 + grant references on table "public"."permission_tokens" to "anon"; 149 + 150 + grant select on table "public"."permission_tokens" to "anon"; 151 + 152 + grant trigger on table "public"."permission_tokens" to "anon"; 153 + 154 + grant truncate on table "public"."permission_tokens" to "anon"; 155 + 156 + grant update on table "public"."permission_tokens" to "anon"; 157 + 158 + grant delete on table "public"."permission_tokens" to "authenticated"; 159 + 160 + grant insert on table "public"."permission_tokens" to "authenticated"; 161 + 162 + grant references on table "public"."permission_tokens" to "authenticated"; 163 + 164 + grant select on table "public"."permission_tokens" to "authenticated"; 165 + 166 + grant trigger on table "public"."permission_tokens" to "authenticated"; 167 + 168 + grant truncate on table "public"."permission_tokens" to "authenticated"; 169 + 170 + grant update on table "public"."permission_tokens" to "authenticated"; 171 + 172 + grant delete on table "public"."permission_tokens" to "service_role"; 173 + 174 + grant insert on table "public"."permission_tokens" to "service_role"; 175 + 176 + grant references on table "public"."permission_tokens" to "service_role"; 177 + 178 + grant select on table "public"."permission_tokens" to "service_role"; 179 + 180 + grant trigger on table "public"."permission_tokens" to "service_role"; 181 + 182 + grant truncate on table "public"."permission_tokens" to "service_role"; 183 + 184 + grant update on table "public"."permission_tokens" to "service_role";