@guard-rail/core 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -3
- package/dist/adapters/supabase/db.js +33 -0
- package/dist/adapters/supabase/db.js.map +1 -1
- package/dist/cli/doctor.js +37 -3
- package/dist/cli/doctor.js.map +1 -1
- package/dist/core/adapter-types.d.ts +35 -2
- package/dist/index.client.d.ts +2 -2
- package/dist/index.client.js +5 -1
- package/dist/index.client.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/react/GRail/client.js +68 -9
- package/dist/react/GRail/client.js.map +1 -1
- package/dist/react/Icons.d.ts +3 -1
- package/dist/react/Icons.js +5 -0
- package/dist/react/Icons.js.map +1 -1
- package/dist/server/actions.d.ts +19 -2
- package/dist/server/actions.js +35 -0
- package/dist/server/actions.js.map +1 -1
- package/package.json +1 -1
- package/supabase/admin/check-grants.sql +12 -3
- package/supabase/migrations/20261103000000_content_history.sql +211 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/actions.ts"],"sourcesContent":["\"use server\";\n\nimport { revalidatePath } from \"next/cache\";\nimport { cookies, headers } from \"next/headers\";\nimport {\n getAuth,\n getDB,\n getInviteHandler,\n getMembers,\n getPasswordResetHandler,\n getStorage,\n resolveSiteId,\n resolveSiteUrl,\n} from \"../core/config\";\nimport { getCachedViewer } from \"./session\";\nimport { requireRole } from \"./access\";\nimport { rateLimited } from \"./rateLimit\";\nimport { cleanEmail, isRole, validateOverrideValue, validPathKey } from \"../core/validate\";\nimport { rethrowIfBailout } from \"../core/bailout\";\nimport {\n GR_AUTH_ERROR_TTL_SECONDS,\n GR_COOKIES,\n GR_NEEDS_PASSWORD_TTL_SECONDS,\n} from \"../core/defs\";\nimport type {\n ActionState,\n GRAsset,\n GRMember,\n GRRole,\n OverrideRow,\n OverrideValue,\n SetPasswordState,\n} from \"../core/types\";\nimport type {\n AuthRedirectParams,\n GRStorageRow,\n GRUploadTarget,\n} from \"../core/adapter-types\";\n\n/** `notice` is for a success the admin should still read, e.g. \"no email was sent because...\". */\ntype Result = { success: boolean; error?: string; notice?: string };\n\nconst DENIED: Result = { success: false, error: \"Unauthorized.\" };\n\n// Every mutation below re-resolves the viewer server-side through\n// requireRole() (see access.ts) — the client's copy of the role decides what\n// to *show*, never what is *allowed*.\n\nexport async function saveOverride(row: OverrideRow): Promise<Result> {\n const viewer = await requireRole([\"admin\", \"editor\"]);\n if (!viewer) return DENIED;\n return writeOverride(row?.pathKey, row?.value);\n}\n\n/**\n * Writes one field's value by id, with no <GRail> involved — the setter that\n * pairs with getFieldValue(). It exists for fields with nothing on the page to\n * click: an SEO title or meta description, an og:image, a product's stock\n * note. Build a drawer or form of your own, and have it (or a Server Action of\n * yours) call this.\n *\n * A bare string is shorthand for a text field. Pass an OverrideValue for the\n * other types ({ type: \"image\", src, alt }, a link, a button).\n *\n * Does its own role check — an editor or admin — so it is safe to call\n * directly from a Client Component, the same as saveOverride. If your own\n * action does other work first, check isEditor() there too.\n *\n * Writes the same row a <GRail> with this id would, so the two stay\n * interchangeable: a field first written here can later be given a visible\n * <GRail>, and vice versa.\n */\nexport async function setFieldValue(\n id: string,\n value: string | OverrideValue,\n): Promise<Result> {\n const viewer = await requireRole([\"admin\", \"editor\"]);\n if (!viewer) return DENIED;\n\n const normalized: OverrideValue | string =\n typeof value === \"string\" ? { type: \"text\", text: value } : value;\n return writeOverride(id, normalized);\n}\n\n/** The write shared by saveOverride and setFieldValue; the caller has already authorized. */\nasync function writeOverride(\n rawPathKey: unknown,\n rawValue: unknown,\n): Promise<Result> {\n // Server Actions are public endpoints: the TypeScript types above describe\n // what the bundled editor sends, not what a request can contain.\n const pathKey = validPathKey(rawPathKey);\n if (!pathKey) return { success: false, error: \"A valid field id is required.\" };\n const checked = validateOverrideValue(rawValue);\n if (!checked.ok) return { success: false, error: checked.error };\n const value = checked.value;\n\n const rowToSend: GRStorageRow = {\n pathKey,\n siteId: await resolveSiteId(),\n type: value.type,\n value,\n };\n\n try {\n const db = getDB();\n const res = await db.setOverride(rowToSend);\n if (!res.success) return res;\n\n // TODO: too broad — purges every cached route on any single-field save.\n revalidatePath(\"/\", \"layout\");\n return { success: true };\n } catch (e) {\n rethrowIfBailout(e);\n console.error(\"Guardrail: save failed\", e);\n return { success: false, error: \"Database write failed.\" };\n }\n}\n\n/* --------------------------------------------------------------- media --- */\n\n// SVG is excluded on purpose: it can carry script, and next/image refuses it\n// by default anyway.\nconst IMAGE_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\", \"image/avif\"];\nconst MAX_UPLOAD_BYTES = 10 * 1024 * 1024;\n\n/**\n * Authorizes an upload and returns where the browser should send the bytes.\n * Only metadata comes through here — the file itself goes straight to storage.\n */\nexport async function createAssetUpload(file: {\n name: string;\n contentType: string;\n size: number;\n}): Promise<\n { success: true; target: GRUploadTarget } | { success: false; error: string }\n> {\n const viewer = await requireRole([\"admin\", \"editor\"]);\n if (!viewer) return { success: false, error: \"Unauthorized.\" };\n const slow = await rateLimited(\"upload\", viewer.userId);\n if (slow) return { success: false, error: slow };\n\n const storage = getStorage();\n if (!storage) {\n return { success: false, error: \"No storage adapter is configured.\" };\n }\n if (!IMAGE_TYPES.includes(file.contentType)) {\n return { success: false, error: \"Upload a PNG, JPEG, WebP, GIF, or AVIF image.\" };\n }\n if (!(file.size > 0) || file.size > MAX_UPLOAD_BYTES) {\n return { success: false, error: \"Images must be under 10 MB.\" };\n }\n\n try {\n const target = await storage.createUpload(await resolveSiteId(), file);\n return { success: true, target };\n } catch (e) {\n rethrowIfBailout(e);\n console.error(\"Guardrail: createUpload failed\", e);\n // Failures here are almost always setup (missing SQL, bucket, or policy),\n // so show the cause while developing. Production keeps it generic rather\n // than exposing backend detail to editors.\n const detail =\n process.env.NODE_ENV !== \"production\" && e instanceof Error\n ? ` ${e.message}`\n : \"\";\n return { success: false, error: `Could not start the upload.${detail}` };\n }\n}\n\nexport async function listAssets(): Promise<GRAsset[]> {\n const viewer = await requireRole([\"admin\", \"editor\"]);\n const storage = getStorage();\n if (!viewer || !storage) return [];\n return storage.listAssets(await resolveSiteId());\n}\n\n/* ---------------------------------------------------------------- team --- */\n\nexport async function listTeam(): Promise<GRMember[]> {\n // Any member may see who else is on the site; only admins may change it.\n const viewer = await requireRole([\"admin\", \"editor\"]);\n if (!viewer) return [];\n\n const members = getMembers();\n return members.listMembers(await resolveSiteId());\n}\n\nexport async function inviteMember(\n email: string,\n role: GRRole,\n): Promise<Result> {\n const viewer = await requireRole([\"admin\"]);\n if (!viewer) return DENIED;\n\n const normalized = cleanEmail(email);\n if (!normalized) return { success: false, error: \"Enter a valid email address.\" };\n if (!isRole(role)) return { success: false, error: \"Choose admin or editor.\" };\n\n const siteId = await resolveSiteId();\n const members = getMembers();\n\n const existing = await members.getMember(siteId, { email: normalized });\n if (existing?.status === \"active\") {\n return { success: false, error: \"That address is already on the team.\" };\n }\n\n // A pending invite is re-sent rather than rejected: provider email is\n // rate-limited and unreliable enough that resending has to be possible.\n if (!existing) {\n const res = await members.upsertMember(siteId, {\n email: normalized,\n role,\n userId: null,\n status: \"pending\",\n });\n if (!res.success) return res;\n }\n\n const auth = getAuth();\n const onInvite = getInviteHandler();\n const slow = await rateLimited(\"invite\", viewer.userId);\n if (slow) return { success: false, error: `Invite saved, but not sent. ${slow}` };\n\n const where = await linkOrigin();\n if (\"error\" in where) {\n revalidatePath(\"/\", \"layout\");\n return { success: false, error: `Invite saved, but ${where.error}` };\n }\n const origin = where.origin;\n\n // The membership is saved either way, so none of what follows can undo it;\n // each failure just says what to fix. Delivery has one shape: the adapter\n // creates the account and a link (if it can), the app's handler emails it.\n const made = auth.createInviteLink\n ? await auth.createInviteLink(normalized, { origin })\n : null;\n if (made && !made.success) {\n revalidatePath(\"/\", \"layout\");\n return {\n success: false,\n error: `Invite saved, but the sign-in link couldn't be created: ${made.error}`,\n };\n }\n if (!onInvite) {\n revalidatePath(\"/\", \"layout\");\n return {\n success: false,\n error:\n \"Invite saved, but this site has nothing set up to email it. Add resendEmails() (or your own onInvite) to withGuardrail() — see the README.\",\n };\n }\n\n try {\n await onInvite({\n email: normalized,\n role: existing?.role ?? role,\n siteId,\n invitedBy: viewer.email ?? viewer.userId,\n ...(made ? { link: made.link, isNewUser: made.isNewUser } : {}),\n });\n } catch (e) {\n console.error(\"Guardrail: onInvite handler threw\", e);\n revalidatePath(\"/\", \"layout\");\n return {\n success: false,\n error: `Invite saved, but the email failed: ${e instanceof Error ? e.message : \"unknown error\"}`,\n };\n }\n\n revalidatePath(\"/\", \"layout\");\n return made && !made.isNewUser\n ? { success: true, notice: ALREADY_HAS_ACCOUNT(normalized) }\n : { success: true };\n}\n\nconst ALREADY_HAS_ACCOUNT = (email: string) =>\n `${email} already has an account, so they were told they've been added rather than sent a link — they sign in with their existing password.`;\n\nexport async function updateMemberRole(\n email: string,\n role: GRRole,\n): Promise<Result> {\n const viewer = await requireRole([\"admin\"]);\n if (!viewer) return DENIED;\n\n const targetEmail = cleanEmail(email);\n if (!targetEmail) return { success: false, error: \"No such member.\" };\n if (!isRole(role)) return { success: false, error: \"Choose admin or editor.\" };\n\n const siteId = await resolveSiteId();\n const members = getMembers();\n\n const target = await members.getMember(siteId, { email: targetEmail });\n if (!target) return { success: false, error: \"No such member.\" };\n\n if (target.role === \"admin\" && role !== \"admin\") {\n const guard = await wouldStrandSite(siteId, target.email);\n if (guard) return guard;\n }\n\n const res = await members.upsertMember(siteId, {\n email: target.email,\n role,\n userId: target.userId,\n status: target.status,\n });\n if (res.success) revalidatePath(\"/\", \"layout\");\n return res;\n}\n\nexport async function removeMember(email: string): Promise<Result> {\n const viewer = await requireRole([\"admin\"]);\n if (!viewer) return DENIED;\n\n const normalized = cleanEmail(email);\n if (!normalized) return { success: false, error: \"No such member.\" };\n\n const siteId = await resolveSiteId();\n const members = getMembers();\n\n const guard = await wouldStrandSite(siteId, normalized);\n if (guard) return guard;\n\n const res = await members.removeMember(siteId, normalized);\n if (res.success) revalidatePath(\"/\", \"layout\");\n return res;\n}\n\n/* ------------------------------------------------------ password reset --- */\n\n/** One reset email per address per site per minute, so the form can't be used to spam someone. */\nconst RESET_COOLDOWN_MS = 60_000;\nconst lastResetAt = new Map<string, number>();\n\n/** Floor for a self-service request, so how long it takes doesn't reveal whether the address is a member. */\nconst RESET_MIN_DURATION_MS = 600;\n\n/**\n * Sends one password-reset email: the adapter mints a one-time link and the\n * app's onPasswordReset handler emails it. If either is missing, says which —\n * there is no quieter fallback to hide behind.\n */\nasync function dispatchPasswordReset(email: string, requestedBy: string): Promise<Result> {\n const auth = getAuth();\n const handler = getPasswordResetHandler();\n\n if (!auth.createPasswordResetLink) {\n return {\n success: false,\n error: \"this site's auth setup can't create reset links — give the Supabase adapter a serviceRoleKey.\",\n };\n }\n if (!handler) {\n return {\n success: false,\n error: \"this site has no onPasswordReset handler — add resendEmails() (or your own) to withGuardrail().\",\n };\n }\n\n const where = await linkOrigin();\n if (\"error\" in where) return { success: false, error: where.error };\n\n const made = await auth.createPasswordResetLink(email, { origin: where.origin });\n if (!made.success) return { success: false, error: made.error };\n try {\n await handler({ email, siteId: await resolveSiteId(), link: made.link, requestedBy });\n return { success: true };\n } catch (e) {\n console.error(\"Guardrail: onPasswordReset handler threw\", e);\n return { success: false, error: e instanceof Error ? e.message : \"unknown error\" };\n }\n}\n\n/**\n * \"Forgot password?\" from the sign-in bar. Called with no session, so it says\n * the same thing whether or not the address belongs to anyone: a member gets an\n * email, a stranger gets nothing, and neither can tell which happened from the\n * response or how long it took. Only a malformed address is rejected, since\n * that says nothing about anyone.\n */\nexport async function forgotPasswordAction(email: string): Promise<Result> {\n const started = Date.now();\n const normalized = cleanEmail(email);\n if (!normalized) return { success: false, error: \"Enter a valid email address.\" };\n\n // Over the limit reads as an error rather than a fake success: it counts\n // members and strangers alike, so it reveals nothing about the address.\n const slow = await rateLimited(\"forgot-password\", normalized);\n if (slow) return { success: false, error: slow };\n\n try {\n const siteId = await resolveSiteId();\n const key = `${siteId}:${normalized}`;\n const last = lastResetAt.get(key);\n if (!last || started - last >= RESET_COOLDOWN_MS) {\n lastResetAt.set(key, started);\n if (lastResetAt.size > 500) {\n for (const [k, t] of lastResetAt) if (started - t >= RESET_COOLDOWN_MS) lastResetAt.delete(k);\n }\n\n // With a privileged membership check, only members are sent anything.\n // Without one (an adapter that can mint reset links but not look members\n // up) any address that asks is — which is why the Supabase adapter offers\n // both together.\n const members = getMembers();\n const known = members.emailHasAccess ? await members.emailHasAccess(siteId, normalized) : true;\n if (known) {\n const res = await dispatchPasswordReset(normalized, \"self\");\n if (!res.success) console.error(\"Guardrail: password reset failed:\", res.error);\n }\n }\n } catch (e) {\n rethrowIfBailout(e);\n console.error(\"Guardrail: password reset failed\", e);\n }\n\n const wait = RESET_MIN_DURATION_MS - (Date.now() - started);\n if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));\n return { success: true };\n}\n\n/**\n * An admin sending a password reset to someone on the team, from the Team\n * drawer. Unlike the self-service request this reports what happened — an admin\n * is allowed to know, and needs to when it didn't work.\n */\nexport async function sendPasswordResetAction(email: string): Promise<Result> {\n const viewer = await requireRole([\"admin\"]);\n if (!viewer) return DENIED;\n\n const normalized = cleanEmail(email);\n if (!normalized) return { success: false, error: \"No such member.\" };\n const target = await getMembers().getMember(await resolveSiteId(), { email: normalized });\n if (!target) return { success: false, error: \"No such member.\" };\n\n const slow = await rateLimited(\"password-reset\", viewer.userId);\n if (slow) return { success: false, error: slow };\n\n const res = await dispatchPasswordReset(normalized, viewer.email ?? viewer.userId);\n return res.success\n ? { success: true, notice: `Password reset email sent to ${normalized}.` }\n : { success: false, error: `The reset email couldn't be sent: ${res.error}` };\n}\n\nconst SITE_URL_MISSING =\n \"siteUrl isn't set, so sign-in links can't be built safely — set GUARDRAIL_SITE_URL (or siteUrl in withGuardrail()) to this site's public address.\";\n\n/**\n * Where an emailed link points. Production takes it from configuration only: a\n * reset can be requested by anyone for any address, so an address read from the\n * request would let a forged Host/Origin header mail a valid token to someone\n * else's domain. Development (no configuration) uses the request's own address.\n */\nasync function linkOrigin(): Promise<{ origin: string } | { error: string }> {\n const configured = await resolveSiteUrl();\n if (configured) return { origin: configured };\n if (process.env.NODE_ENV === \"production\") return { error: SITE_URL_MISSING };\n const h = await headers();\n const host = h.get(\"host\") ?? \"localhost\";\n const proto = h.get(\"x-forwarded-proto\") ?? (host.startsWith(\"localhost\") ? \"http\" : \"https\");\n return { origin: `${proto}://${host}` };\n}\n\n/**\n * Refuses changes that would leave a site with no admin, which would lock\n * everyone out of team management with no in-app way to recover.\n */\nasync function wouldStrandSite(\n siteId: string,\n email: string,\n): Promise<Result | null> {\n const members = getMembers();\n const all = await members.listMembers(siteId);\n const admins = all.filter((m) => m.role === \"admin\");\n\n if (admins.length <= 1 && admins.some((m) => m.email === email)) {\n return {\n success: false,\n error: \"A site must keep at least one admin.\",\n };\n }\n return null;\n}\n\n/* ---------------------------------------------------------------- auth --- */\n\nexport async function loginAction(\n _prev: ActionState,\n formData: FormData,\n): Promise<ActionState> {\n const email = String(formData.get(\"email\") ?? \"\");\n const password = String(formData.get(\"password\") ?? \"\");\n\n const auth = getAuth();\n if (!auth.authenticate) {\n return {\n success: false,\n error: \"This site's auth provider does not accept passwords.\",\n email,\n };\n }\n\n // Bounded before it becomes a counter key: this runs before anything is\n // checked, for anyone, so the raw field could be arbitrarily large.\n const slow = await rateLimited(\"login\", cleanEmail(email) ?? email.trim().toLowerCase().slice(0, 254));\n if (slow) return { success: false, error: slow, email };\n\n const result = await auth.authenticate(email, password);\n if (!result.success) {\n return { success: false, error: result.error, email };\n }\n\n revalidatePath(\"/\", \"layout\");\n return { success: true, email };\n}\n\nexport async function logoutAction(): Promise<{ success: boolean }> {\n const auth = getAuth();\n await auth.logout();\n // Cleared here, not by the client afterward: a Server Action's own\n // revalidatePath() below triggers Next's router to refresh the current\n // route automatically, which can race a client-side navigation attempted\n // right after this resolves — confirmed live, where that automatic\n // refresh (for the pre-logout URL, ?edit=true) won the race and silently\n // undid a client-side `window.location.href` to the plain path. Deleting\n // the cookie here instead makes it part of the one server round-trip that\n // revalidatePath already accounts for, with nothing left to race.\n const cookieStore = await cookies();\n cookieStore.delete(GR_COOKIES.EDIT_MODE);\n // Signing out ends whatever was pending for that session too. A leftover\n // \"needs a password\" flag used to survive this and keep the sign-in bar\n // pinned on screen.\n cookieStore.delete(GR_COOKIES.NEEDS_PASSWORD);\n cookieStore.delete(GR_COOKIES.AUTH_ERROR);\n revalidatePath(\"/\", \"layout\");\n return { success: true };\n}\n\n/* ---------------------------------------------------------- redirects --- */\n\n/**\n * Completes a session that arrived via an invite or password-recovery link. Called by AuthRedirectSync (react/), the\n * generic client component that notices this kind of link and hands off\n * whatever it found in the URL — the actual token exchange is entirely the\n * auth adapter's job (see GRAuthAdapter.completeAuthRedirect); core only\n * orchestrates the result.\n *\n * A no-op {success:false}, not an error, when the adapter doesn't implement\n * this — most page loads carry no redirect data at all, and an adapter with\n * no such concept should not make every one of them fail.\n */\nexport async function completeAuthRedirectAction(\n params: AuthRedirectParams,\n): Promise<{ success: boolean; needsPassword?: boolean; error?: string }> {\n const auth = getAuth();\n if (!auth.completeAuthRedirect) return { success: false };\n if (await rateLimited(\"auth-link\")) return { success: false, error: \"Too many attempts.\" };\n\n const res = await auth.completeAuthRedirect(params);\n const cookieStore = await cookies();\n if (res.success) {\n cookieStore.delete(GR_COOKIES.AUTH_ERROR);\n if (res.needsPassword) {\n cookieStore.set(GR_COOKIES.NEEDS_PASSWORD, \"true\", {\n path: \"/\",\n maxAge: GR_NEEDS_PASSWORD_TTL_SECONDS,\n });\n }\n revalidatePath(\"/\", \"layout\");\n } else {\n // Say so, rather than leaving the person on a plain page with nothing\n // visibly different. The provider's own wording stays in the server log; the\n // cookie carries only a code, which the chrome turns into plain English.\n console.error(\"Guardrail: couldn't complete an emailed link:\", res.error);\n cookieStore.set(\n GR_COOKIES.AUTH_ERROR,\n /expired|invalid|already|used|not found/i.test(res.error ?? \"\") ? \"expired\" : \"failed\",\n { path: \"/\", maxAge: GR_AUTH_ERROR_TTL_SECONDS },\n );\n revalidatePath(\"/\", \"layout\");\n }\n return res;\n}\n\n/**\n * Sets the current session's password — the prompt completeAuthRedirectAction\n * triggers via needsPassword. Requires only a valid session, not a role: an\n * invited person's role is claimed as a side effect of getCachedViewer()\n * resolving them, which already happens independently of this.\n */\nexport async function setPasswordAction(\n _prev: SetPasswordState,\n formData: FormData,\n): Promise<SetPasswordState> {\n const viewer = await getCachedViewer();\n if (!viewer) return { success: false, error: \"Not signed in.\" };\n\n const auth = getAuth();\n if (!auth.setPassword) {\n return { success: false, error: \"This site's auth provider does not support setting a password.\" };\n }\n\n const password = String(formData.get(\"password\") ?? \"\");\n // `confirm` is absent from older custom forms, so only checked when sent.\n const confirm = formData.get(\"confirm\");\n if (password.length < 8) {\n return { success: false, error: \"Use at least 8 characters.\" };\n }\n if (confirm !== null && String(confirm) !== password) {\n return { success: false, error: \"The two passwords don't match.\" };\n }\n const res = await auth.setPassword(password);\n if (res.success) {\n const cookieStore = await cookies();\n cookieStore.delete(GR_COOKIES.NEEDS_PASSWORD);\n revalidatePath(\"/\", \"layout\");\n }\n return res;\n}\n\n/**\n * Dismisses the set-password step without setting a password.\n *\n * @deprecated Guardrail's own UI no longer offers this: passwords are the only\n * way back in, so the step can't be skipped. Kept so an app's own UI built on\n * it keeps working.\n */\nexport async function skipSetPasswordAction(): Promise<{ success: boolean }> {\n const cookieStore = await cookies();\n cookieStore.delete(GR_COOKIES.NEEDS_PASSWORD);\n revalidatePath(\"/\", \"layout\");\n return { success: true };\n}\n"],"mappings":";AAEA,SAAS,sBAAsB;AAC/B,SAAS,SAAS,eAAe;AACjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,uBAAuB;AAChC,SAAS,mBAAmB;AAC5B,SAAS,mBAAmB;AAC5B,SAAS,YAAY,QAAQ,uBAAuB,oBAAoB;AACxE,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAmBP,MAAM,SAAiB,EAAE,SAAS,OAAO,OAAO,gBAAgB;AAMhE,eAAsB,aAAa,KAAmC;AACpE,QAAM,SAAS,MAAM,YAAY,CAAC,SAAS,QAAQ,CAAC;AACpD,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,cAAc,KAAK,SAAS,KAAK,KAAK;AAC/C;AAoBA,eAAsB,cACpB,IACA,OACiB;AACjB,QAAM,SAAS,MAAM,YAAY,CAAC,SAAS,QAAQ,CAAC;AACpD,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,aACJ,OAAO,UAAU,WAAW,EAAE,MAAM,QAAQ,MAAM,MAAM,IAAI;AAC9D,SAAO,cAAc,IAAI,UAAU;AACrC;AAGA,eAAe,cACb,YACA,UACiB;AAGjB,QAAM,UAAU,aAAa,UAAU;AACvC,MAAI,CAAC,QAAS,QAAO,EAAE,SAAS,OAAO,OAAO,gCAAgC;AAC9E,QAAM,UAAU,sBAAsB,QAAQ;AAC9C,MAAI,CAAC,QAAQ,GAAI,QAAO,EAAE,SAAS,OAAO,OAAO,QAAQ,MAAM;AAC/D,QAAM,QAAQ,QAAQ;AAEtB,QAAM,YAA0B;AAAA,IAC9B;AAAA,IACA,QAAQ,MAAM,cAAc;AAAA,IAC5B,MAAM,MAAM;AAAA,IACZ;AAAA,EACF;AAEA,MAAI;AACF,UAAM,KAAK,MAAM;AACjB,UAAM,MAAM,MAAM,GAAG,YAAY,SAAS;AAC1C,QAAI,CAAC,IAAI,QAAS,QAAO;AAGzB,mBAAe,KAAK,QAAQ;AAC5B,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB,SAAS,GAAG;AACV,qBAAiB,CAAC;AAClB,YAAQ,MAAM,0BAA0B,CAAC;AACzC,WAAO,EAAE,SAAS,OAAO,OAAO,yBAAyB;AAAA,EAC3D;AACF;AAMA,MAAM,cAAc,CAAC,aAAa,cAAc,cAAc,aAAa,YAAY;AACvF,MAAM,mBAAmB,KAAK,OAAO;AAMrC,eAAsB,kBAAkB,MAMtC;AACA,QAAM,SAAS,MAAM,YAAY,CAAC,SAAS,QAAQ,CAAC;AACpD,MAAI,CAAC,OAAQ,QAAO,EAAE,SAAS,OAAO,OAAO,gBAAgB;AAC7D,QAAM,OAAO,MAAM,YAAY,UAAU,OAAO,MAAM;AACtD,MAAI,KAAM,QAAO,EAAE,SAAS,OAAO,OAAO,KAAK;AAE/C,QAAM,UAAU,WAAW;AAC3B,MAAI,CAAC,SAAS;AACZ,WAAO,EAAE,SAAS,OAAO,OAAO,oCAAoC;AAAA,EACtE;AACA,MAAI,CAAC,YAAY,SAAS,KAAK,WAAW,GAAG;AAC3C,WAAO,EAAE,SAAS,OAAO,OAAO,gDAAgD;AAAA,EAClF;AACA,MAAI,EAAE,KAAK,OAAO,MAAM,KAAK,OAAO,kBAAkB;AACpD,WAAO,EAAE,SAAS,OAAO,OAAO,8BAA8B;AAAA,EAChE;AAEA,MAAI;AACF,UAAM,SAAS,MAAM,QAAQ,aAAa,MAAM,cAAc,GAAG,IAAI;AACrE,WAAO,EAAE,SAAS,MAAM,OAAO;AAAA,EACjC,SAAS,GAAG;AACV,qBAAiB,CAAC;AAClB,YAAQ,MAAM,kCAAkC,CAAC;AAIjD,UAAM,SACJ,QAAQ,IAAI,aAAa,gBAAgB,aAAa,QAClD,IAAI,EAAE,OAAO,KACb;AACN,WAAO,EAAE,SAAS,OAAO,OAAO,8BAA8B,MAAM,GAAG;AAAA,EACzE;AACF;AAEA,eAAsB,aAAiC;AACrD,QAAM,SAAS,MAAM,YAAY,CAAC,SAAS,QAAQ,CAAC;AACpD,QAAM,UAAU,WAAW;AAC3B,MAAI,CAAC,UAAU,CAAC,QAAS,QAAO,CAAC;AACjC,SAAO,QAAQ,WAAW,MAAM,cAAc,CAAC;AACjD;AAIA,eAAsB,WAAgC;AAEpD,QAAM,SAAS,MAAM,YAAY,CAAC,SAAS,QAAQ,CAAC;AACpD,MAAI,CAAC,OAAQ,QAAO,CAAC;AAErB,QAAM,UAAU,WAAW;AAC3B,SAAO,QAAQ,YAAY,MAAM,cAAc,CAAC;AAClD;AAEA,eAAsB,aACpB,OACA,MACiB;AACjB,QAAM,SAAS,MAAM,YAAY,CAAC,OAAO,CAAC;AAC1C,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,aAAa,WAAW,KAAK;AACnC,MAAI,CAAC,WAAY,QAAO,EAAE,SAAS,OAAO,OAAO,+BAA+B;AAChF,MAAI,CAAC,OAAO,IAAI,EAAG,QAAO,EAAE,SAAS,OAAO,OAAO,0BAA0B;AAE7E,QAAM,SAAS,MAAM,cAAc;AACnC,QAAM,UAAU,WAAW;AAE3B,QAAM,WAAW,MAAM,QAAQ,UAAU,QAAQ,EAAE,OAAO,WAAW,CAAC;AACtE,MAAI,UAAU,WAAW,UAAU;AACjC,WAAO,EAAE,SAAS,OAAO,OAAO,uCAAuC;AAAA,EACzE;AAIA,MAAI,CAAC,UAAU;AACb,UAAM,MAAM,MAAM,QAAQ,aAAa,QAAQ;AAAA,MAC7C,OAAO;AAAA,MACP;AAAA,MACA,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV,CAAC;AACD,QAAI,CAAC,IAAI,QAAS,QAAO;AAAA,EAC3B;AAEA,QAAM,OAAO,QAAQ;AACrB,QAAM,WAAW,iBAAiB;AAClC,QAAM,OAAO,MAAM,YAAY,UAAU,OAAO,MAAM;AACtD,MAAI,KAAM,QAAO,EAAE,SAAS,OAAO,OAAO,+BAA+B,IAAI,GAAG;AAEhF,QAAM,QAAQ,MAAM,WAAW;AAC/B,MAAI,WAAW,OAAO;AACpB,mBAAe,KAAK,QAAQ;AAC5B,WAAO,EAAE,SAAS,OAAO,OAAO,qBAAqB,MAAM,KAAK,GAAG;AAAA,EACrE;AACA,QAAM,SAAS,MAAM;AAKrB,QAAM,OAAO,KAAK,mBACd,MAAM,KAAK,iBAAiB,YAAY,EAAE,OAAO,CAAC,IAClD;AACJ,MAAI,QAAQ,CAAC,KAAK,SAAS;AACzB,mBAAe,KAAK,QAAQ;AAC5B,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,2DAA2D,KAAK,KAAK;AAAA,IAC9E;AAAA,EACF;AACA,MAAI,CAAC,UAAU;AACb,mBAAe,KAAK,QAAQ;AAC5B,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OACE;AAAA,IACJ;AAAA,EACF;AAEA,MAAI;AACF,UAAM,SAAS;AAAA,MACb,OAAO;AAAA,MACP,MAAM,UAAU,QAAQ;AAAA,MACxB;AAAA,MACA,WAAW,OAAO,SAAS,OAAO;AAAA,MAClC,GAAI,OAAO,EAAE,MAAM,KAAK,MAAM,WAAW,KAAK,UAAU,IAAI,CAAC;AAAA,IAC/D,CAAC;AAAA,EACH,SAAS,GAAG;AACV,YAAQ,MAAM,qCAAqC,CAAC;AACpD,mBAAe,KAAK,QAAQ;AAC5B,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,uCAAuC,aAAa,QAAQ,EAAE,UAAU,eAAe;AAAA,IAChG;AAAA,EACF;AAEA,iBAAe,KAAK,QAAQ;AAC5B,SAAO,QAAQ,CAAC,KAAK,YACjB,EAAE,SAAS,MAAM,QAAQ,oBAAoB,UAAU,EAAE,IACzD,EAAE,SAAS,KAAK;AACtB;AAEA,MAAM,sBAAsB,CAAC,UAC3B,GAAG,KAAK;AAEV,eAAsB,iBACpB,OACA,MACiB;AACjB,QAAM,SAAS,MAAM,YAAY,CAAC,OAAO,CAAC;AAC1C,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,cAAc,WAAW,KAAK;AACpC,MAAI,CAAC,YAAa,QAAO,EAAE,SAAS,OAAO,OAAO,kBAAkB;AACpE,MAAI,CAAC,OAAO,IAAI,EAAG,QAAO,EAAE,SAAS,OAAO,OAAO,0BAA0B;AAE7E,QAAM,SAAS,MAAM,cAAc;AACnC,QAAM,UAAU,WAAW;AAE3B,QAAM,SAAS,MAAM,QAAQ,UAAU,QAAQ,EAAE,OAAO,YAAY,CAAC;AACrE,MAAI,CAAC,OAAQ,QAAO,EAAE,SAAS,OAAO,OAAO,kBAAkB;AAE/D,MAAI,OAAO,SAAS,WAAW,SAAS,SAAS;AAC/C,UAAM,QAAQ,MAAM,gBAAgB,QAAQ,OAAO,KAAK;AACxD,QAAI,MAAO,QAAO;AAAA,EACpB;AAEA,QAAM,MAAM,MAAM,QAAQ,aAAa,QAAQ;AAAA,IAC7C,OAAO,OAAO;AAAA,IACd;AAAA,IACA,QAAQ,OAAO;AAAA,IACf,QAAQ,OAAO;AAAA,EACjB,CAAC;AACD,MAAI,IAAI,QAAS,gBAAe,KAAK,QAAQ;AAC7C,SAAO;AACT;AAEA,eAAsB,aAAa,OAAgC;AACjE,QAAM,SAAS,MAAM,YAAY,CAAC,OAAO,CAAC;AAC1C,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,aAAa,WAAW,KAAK;AACnC,MAAI,CAAC,WAAY,QAAO,EAAE,SAAS,OAAO,OAAO,kBAAkB;AAEnE,QAAM,SAAS,MAAM,cAAc;AACnC,QAAM,UAAU,WAAW;AAE3B,QAAM,QAAQ,MAAM,gBAAgB,QAAQ,UAAU;AACtD,MAAI,MAAO,QAAO;AAElB,QAAM,MAAM,MAAM,QAAQ,aAAa,QAAQ,UAAU;AACzD,MAAI,IAAI,QAAS,gBAAe,KAAK,QAAQ;AAC7C,SAAO;AACT;AAKA,MAAM,oBAAoB;AAC1B,MAAM,cAAc,oBAAI,IAAoB;AAG5C,MAAM,wBAAwB;AAO9B,eAAe,sBAAsB,OAAe,aAAsC;AACxF,QAAM,OAAO,QAAQ;AACrB,QAAM,UAAU,wBAAwB;AAExC,MAAI,CAAC,KAAK,yBAAyB;AACjC,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,IACT;AAAA,EACF;AACA,MAAI,CAAC,SAAS;AACZ,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,QAAQ,MAAM,WAAW;AAC/B,MAAI,WAAW,MAAO,QAAO,EAAE,SAAS,OAAO,OAAO,MAAM,MAAM;AAElE,QAAM,OAAO,MAAM,KAAK,wBAAwB,OAAO,EAAE,QAAQ,MAAM,OAAO,CAAC;AAC/E,MAAI,CAAC,KAAK,QAAS,QAAO,EAAE,SAAS,OAAO,OAAO,KAAK,MAAM;AAC9D,MAAI;AACF,UAAM,QAAQ,EAAE,OAAO,QAAQ,MAAM,cAAc,GAAG,MAAM,KAAK,MAAM,YAAY,CAAC;AACpF,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB,SAAS,GAAG;AACV,YAAQ,MAAM,4CAA4C,CAAC;AAC3D,WAAO,EAAE,SAAS,OAAO,OAAO,aAAa,QAAQ,EAAE,UAAU,gBAAgB;AAAA,EACnF;AACF;AASA,eAAsB,qBAAqB,OAAgC;AACzE,QAAM,UAAU,KAAK,IAAI;AACzB,QAAM,aAAa,WAAW,KAAK;AACnC,MAAI,CAAC,WAAY,QAAO,EAAE,SAAS,OAAO,OAAO,+BAA+B;AAIhF,QAAM,OAAO,MAAM,YAAY,mBAAmB,UAAU;AAC5D,MAAI,KAAM,QAAO,EAAE,SAAS,OAAO,OAAO,KAAK;AAE/C,MAAI;AACF,UAAM,SAAS,MAAM,cAAc;AACnC,UAAM,MAAM,GAAG,MAAM,IAAI,UAAU;AACnC,UAAM,OAAO,YAAY,IAAI,GAAG;AAChC,QAAI,CAAC,QAAQ,UAAU,QAAQ,mBAAmB;AAChD,kBAAY,IAAI,KAAK,OAAO;AAC5B,UAAI,YAAY,OAAO,KAAK;AAC1B,mBAAW,CAAC,GAAG,CAAC,KAAK,YAAa,KAAI,UAAU,KAAK,kBAAmB,aAAY,OAAO,CAAC;AAAA,MAC9F;AAMA,YAAM,UAAU,WAAW;AAC3B,YAAM,QAAQ,QAAQ,iBAAiB,MAAM,QAAQ,eAAe,QAAQ,UAAU,IAAI;AAC1F,UAAI,OAAO;AACT,cAAM,MAAM,MAAM,sBAAsB,YAAY,MAAM;AAC1D,YAAI,CAAC,IAAI,QAAS,SAAQ,MAAM,qCAAqC,IAAI,KAAK;AAAA,MAChF;AAAA,IACF;AAAA,EACF,SAAS,GAAG;AACV,qBAAiB,CAAC;AAClB,YAAQ,MAAM,oCAAoC,CAAC;AAAA,EACrD;AAEA,QAAM,OAAO,yBAAyB,KAAK,IAAI,IAAI;AACnD,MAAI,OAAO,EAAG,OAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,IAAI,CAAC;AACtE,SAAO,EAAE,SAAS,KAAK;AACzB;AAOA,eAAsB,wBAAwB,OAAgC;AAC5E,QAAM,SAAS,MAAM,YAAY,CAAC,OAAO,CAAC;AAC1C,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,aAAa,WAAW,KAAK;AACnC,MAAI,CAAC,WAAY,QAAO,EAAE,SAAS,OAAO,OAAO,kBAAkB;AACnE,QAAM,SAAS,MAAM,WAAW,EAAE,UAAU,MAAM,cAAc,GAAG,EAAE,OAAO,WAAW,CAAC;AACxF,MAAI,CAAC,OAAQ,QAAO,EAAE,SAAS,OAAO,OAAO,kBAAkB;AAE/D,QAAM,OAAO,MAAM,YAAY,kBAAkB,OAAO,MAAM;AAC9D,MAAI,KAAM,QAAO,EAAE,SAAS,OAAO,OAAO,KAAK;AAE/C,QAAM,MAAM,MAAM,sBAAsB,YAAY,OAAO,SAAS,OAAO,MAAM;AACjF,SAAO,IAAI,UACP,EAAE,SAAS,MAAM,QAAQ,gCAAgC,UAAU,IAAI,IACvE,EAAE,SAAS,OAAO,OAAO,qCAAqC,IAAI,KAAK,GAAG;AAChF;AAEA,MAAM,mBACJ;AAQF,eAAe,aAA8D;AAC3E,QAAM,aAAa,MAAM,eAAe;AACxC,MAAI,WAAY,QAAO,EAAE,QAAQ,WAAW;AAC5C,MAAI,QAAQ,IAAI,aAAa,aAAc,QAAO,EAAE,OAAO,iBAAiB;AAC5E,QAAM,IAAI,MAAM,QAAQ;AACxB,QAAM,OAAO,EAAE,IAAI,MAAM,KAAK;AAC9B,QAAM,QAAQ,EAAE,IAAI,mBAAmB,MAAM,KAAK,WAAW,WAAW,IAAI,SAAS;AACrF,SAAO,EAAE,QAAQ,GAAG,KAAK,MAAM,IAAI,GAAG;AACxC;AAMA,eAAe,gBACb,QACA,OACwB;AACxB,QAAM,UAAU,WAAW;AAC3B,QAAM,MAAM,MAAM,QAAQ,YAAY,MAAM;AAC5C,QAAM,SAAS,IAAI,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO;AAEnD,MAAI,OAAO,UAAU,KAAK,OAAO,KAAK,CAAC,MAAM,EAAE,UAAU,KAAK,GAAG;AAC/D,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAIA,eAAsB,YACpB,OACA,UACsB;AACtB,QAAM,QAAQ,OAAO,SAAS,IAAI,OAAO,KAAK,EAAE;AAChD,QAAM,WAAW,OAAO,SAAS,IAAI,UAAU,KAAK,EAAE;AAEtD,QAAM,OAAO,QAAQ;AACrB,MAAI,CAAC,KAAK,cAAc;AACtB,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,MACP;AAAA,IACF;AAAA,EACF;AAIA,QAAM,OAAO,MAAM,YAAY,SAAS,WAAW,KAAK,KAAK,MAAM,KAAK,EAAE,YAAY,EAAE,MAAM,GAAG,GAAG,CAAC;AACrG,MAAI,KAAM,QAAO,EAAE,SAAS,OAAO,OAAO,MAAM,MAAM;AAEtD,QAAM,SAAS,MAAM,KAAK,aAAa,OAAO,QAAQ;AACtD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,EAAE,SAAS,OAAO,OAAO,OAAO,OAAO,MAAM;AAAA,EACtD;AAEA,iBAAe,KAAK,QAAQ;AAC5B,SAAO,EAAE,SAAS,MAAM,MAAM;AAChC;AAEA,eAAsB,eAA8C;AAClE,QAAM,OAAO,QAAQ;AACrB,QAAM,KAAK,OAAO;AASlB,QAAM,cAAc,MAAM,QAAQ;AAClC,cAAY,OAAO,WAAW,SAAS;AAIvC,cAAY,OAAO,WAAW,cAAc;AAC5C,cAAY,OAAO,WAAW,UAAU;AACxC,iBAAe,KAAK,QAAQ;AAC5B,SAAO,EAAE,SAAS,KAAK;AACzB;AAeA,eAAsB,2BACpB,QACwE;AACxE,QAAM,OAAO,QAAQ;AACrB,MAAI,CAAC,KAAK,qBAAsB,QAAO,EAAE,SAAS,MAAM;AACxD,MAAI,MAAM,YAAY,WAAW,EAAG,QAAO,EAAE,SAAS,OAAO,OAAO,qBAAqB;AAEzF,QAAM,MAAM,MAAM,KAAK,qBAAqB,MAAM;AAClD,QAAM,cAAc,MAAM,QAAQ;AAClC,MAAI,IAAI,SAAS;AACf,gBAAY,OAAO,WAAW,UAAU;AACxC,QAAI,IAAI,eAAe;AACrB,kBAAY,IAAI,WAAW,gBAAgB,QAAQ;AAAA,QACjD,MAAM;AAAA,QACN,QAAQ;AAAA,MACV,CAAC;AAAA,IACH;AACA,mBAAe,KAAK,QAAQ;AAAA,EAC9B,OAAO;AAIL,YAAQ,MAAM,iDAAiD,IAAI,KAAK;AACxE,gBAAY;AAAA,MACV,WAAW;AAAA,MACX,0CAA0C,KAAK,IAAI,SAAS,EAAE,IAAI,YAAY;AAAA,MAC9E,EAAE,MAAM,KAAK,QAAQ,0BAA0B;AAAA,IACjD;AACA,mBAAe,KAAK,QAAQ;AAAA,EAC9B;AACA,SAAO;AACT;AAQA,eAAsB,kBACpB,OACA,UAC2B;AAC3B,QAAM,SAAS,MAAM,gBAAgB;AACrC,MAAI,CAAC,OAAQ,QAAO,EAAE,SAAS,OAAO,OAAO,iBAAiB;AAE9D,QAAM,OAAO,QAAQ;AACrB,MAAI,CAAC,KAAK,aAAa;AACrB,WAAO,EAAE,SAAS,OAAO,OAAO,iEAAiE;AAAA,EACnG;AAEA,QAAM,WAAW,OAAO,SAAS,IAAI,UAAU,KAAK,EAAE;AAEtD,QAAM,UAAU,SAAS,IAAI,SAAS;AACtC,MAAI,SAAS,SAAS,GAAG;AACvB,WAAO,EAAE,SAAS,OAAO,OAAO,6BAA6B;AAAA,EAC/D;AACA,MAAI,YAAY,QAAQ,OAAO,OAAO,MAAM,UAAU;AACpD,WAAO,EAAE,SAAS,OAAO,OAAO,iCAAiC;AAAA,EACnE;AACA,QAAM,MAAM,MAAM,KAAK,YAAY,QAAQ;AAC3C,MAAI,IAAI,SAAS;AACf,UAAM,cAAc,MAAM,QAAQ;AAClC,gBAAY,OAAO,WAAW,cAAc;AAC5C,mBAAe,KAAK,QAAQ;AAAA,EAC9B;AACA,SAAO;AACT;AASA,eAAsB,wBAAuD;AAC3E,QAAM,cAAc,MAAM,QAAQ;AAClC,cAAY,OAAO,WAAW,cAAc;AAC5C,iBAAe,KAAK,QAAQ;AAC5B,SAAO,EAAE,SAAS,KAAK;AACzB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/actions.ts"],"sourcesContent":["\"use server\";\n\nimport { revalidatePath } from \"next/cache\";\nimport { cookies, headers } from \"next/headers\";\nimport {\n getAuth,\n getDB,\n getInviteHandler,\n getMembers,\n getPasswordResetHandler,\n getStorage,\n resolveSiteId,\n resolveSiteUrl,\n} from \"../core/config\";\nimport { getCachedViewer } from \"./session\";\nimport { requireRole } from \"./access\";\nimport { rateLimited } from \"./rateLimit\";\nimport { cleanEmail, isRole, validateOverrideValue, validPathKey } from \"../core/validate\";\nimport { rethrowIfBailout } from \"../core/bailout\";\nimport {\n GR_AUTH_ERROR_TTL_SECONDS,\n GR_COOKIES,\n GR_NEEDS_PASSWORD_TTL_SECONDS,\n} from \"../core/defs\";\nimport type {\n ActionState,\n GRAsset,\n GRMember,\n GRRole,\n OverrideRow,\n OverrideValue,\n SetPasswordState,\n} from \"../core/types\";\nimport type {\n AuthRedirectParams,\n GRRevertInfo,\n GRStorageRow,\n GRUploadTarget,\n} from \"../core/adapter-types\";\n\n/** `notice` is for a success the admin should still read, e.g. \"no email was sent because...\". */\ntype Result = { success: boolean; error?: string; notice?: string };\n\nconst DENIED: Result = { success: false, error: \"Unauthorized.\" };\n\n// Every mutation below re-resolves the viewer server-side through\n// requireRole() (see access.ts) — the client's copy of the role decides what\n// to *show*, never what is *allowed*.\n\nexport async function saveOverride(row: OverrideRow): Promise<Result> {\n const viewer = await requireRole([\"admin\", \"editor\"]);\n if (!viewer) return DENIED;\n return writeOverride(row?.pathKey, row?.value);\n}\n\n/**\n * Writes one field's value by id, with no <GRail> involved — the setter that\n * pairs with getFieldValue(). It exists for fields with nothing on the page to\n * click: an SEO title or meta description, an og:image, a product's stock\n * note. Build a drawer or form of your own, and have it (or a Server Action of\n * yours) call this.\n *\n * A bare string is shorthand for a text field. Pass an OverrideValue for the\n * other types ({ type: \"image\", src, alt }, a link, a button).\n *\n * Does its own role check — an editor or admin — so it is safe to call\n * directly from a Client Component, the same as saveOverride. If your own\n * action does other work first, check isEditor() there too.\n *\n * Writes the same row a <GRail> with this id would, so the two stay\n * interchangeable: a field first written here can later be given a visible\n * <GRail>, and vice versa.\n */\nexport async function setFieldValue(\n id: string,\n value: string | OverrideValue,\n): Promise<Result> {\n const viewer = await requireRole([\"admin\", \"editor\"]);\n if (!viewer) return DENIED;\n\n const normalized: OverrideValue | string =\n typeof value === \"string\" ? { type: \"text\", text: value } : value;\n return writeOverride(id, normalized);\n}\n\n/** The write shared by saveOverride and setFieldValue; the caller has already authorized. */\nasync function writeOverride(\n rawPathKey: unknown,\n rawValue: unknown,\n): Promise<Result> {\n // Server Actions are public endpoints: the TypeScript types above describe\n // what the bundled editor sends, not what a request can contain.\n const pathKey = validPathKey(rawPathKey);\n if (!pathKey) return { success: false, error: \"A valid field id is required.\" };\n const checked = validateOverrideValue(rawValue);\n if (!checked.ok) return { success: false, error: checked.error };\n const value = checked.value;\n\n const rowToSend: GRStorageRow = {\n pathKey,\n siteId: await resolveSiteId(),\n type: value.type,\n value,\n };\n\n try {\n const db = getDB();\n const res = await db.setOverride(rowToSend);\n if (!res.success) return res;\n\n // TODO: too broad — purges every cached route on any single-field save.\n revalidatePath(\"/\", \"layout\");\n return { success: true };\n } catch (e) {\n rethrowIfBailout(e);\n console.error(\"Guardrail: save failed\", e);\n return { success: false, error: \"Database write failed.\" };\n }\n}\n\n/* -------------------------------------------------------------- revert --- */\n\n/**\n * What reverting this field would do — \"previous\" (with the date of the version\n * it goes back to), \"default\" (back to what the page itself says), or \"none\".\n * The editor asks when it opens, so visitors never pay for it.\n */\nexport async function getRevertInfo(id: string): Promise<GRRevertInfo> {\n const viewer = await requireRole([\"admin\", \"editor\"]);\n const pathKey = validPathKey(id);\n if (!viewer || !pathKey) return { kind: \"none\" };\n\n const db = getDB();\n if (!db.getRevertInfo) return { kind: \"none\" };\n try {\n return await db.getRevertInfo(pathKey, await resolveSiteId());\n } catch (e) {\n rethrowIfBailout(e);\n return { kind: \"none\" };\n }\n}\n\n/**\n * One step back for a field. Restores its previous saved value and returns it,\n * or, with none left, removes the saved value (`action: \"reset\"`) so the field\n * shows the default written in the page. Same permission as saving.\n */\nexport async function revertField(\n id: string,\n): Promise<{ success: boolean; error?: string; action?: \"restored\" | \"reset\" | \"none\"; value?: OverrideValue }> {\n const viewer = await requireRole([\"admin\", \"editor\"]);\n if (!viewer) return DENIED;\n const pathKey = validPathKey(id);\n if (!pathKey) return { success: false, error: \"A valid field id is required.\" };\n\n const db = getDB();\n if (!db.revertOverride) {\n return { success: false, error: \"This site's database setup doesn't keep history, so there's nothing to revert to.\" };\n }\n try {\n const res = await db.revertOverride(pathKey, await resolveSiteId());\n if (!res.success) return { success: false, error: res.error };\n revalidatePath(\"/\", \"layout\");\n return res.action === \"restored\"\n ? { success: true, action: \"restored\", value: res.value }\n : { success: true, action: res.action };\n } catch (e) {\n rethrowIfBailout(e);\n console.error(\"Guardrail: revert failed\", e);\n return { success: false, error: \"Couldn't revert that.\" };\n }\n}\n\n/* --------------------------------------------------------------- media --- */\n\n// SVG is excluded on purpose: it can carry script, and next/image refuses it\n// by default anyway.\nconst IMAGE_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\", \"image/avif\"];\nconst MAX_UPLOAD_BYTES = 10 * 1024 * 1024;\n\n/**\n * Authorizes an upload and returns where the browser should send the bytes.\n * Only metadata comes through here — the file itself goes straight to storage.\n */\nexport async function createAssetUpload(file: {\n name: string;\n contentType: string;\n size: number;\n}): Promise<\n { success: true; target: GRUploadTarget } | { success: false; error: string }\n> {\n const viewer = await requireRole([\"admin\", \"editor\"]);\n if (!viewer) return { success: false, error: \"Unauthorized.\" };\n const slow = await rateLimited(\"upload\", viewer.userId);\n if (slow) return { success: false, error: slow };\n\n const storage = getStorage();\n if (!storage) {\n return { success: false, error: \"No storage adapter is configured.\" };\n }\n if (!IMAGE_TYPES.includes(file.contentType)) {\n return { success: false, error: \"Upload a PNG, JPEG, WebP, GIF, or AVIF image.\" };\n }\n if (!(file.size > 0) || file.size > MAX_UPLOAD_BYTES) {\n return { success: false, error: \"Images must be under 10 MB.\" };\n }\n\n try {\n const target = await storage.createUpload(await resolveSiteId(), file);\n return { success: true, target };\n } catch (e) {\n rethrowIfBailout(e);\n console.error(\"Guardrail: createUpload failed\", e);\n // Failures here are almost always setup (missing SQL, bucket, or policy),\n // so show the cause while developing. Production keeps it generic rather\n // than exposing backend detail to editors.\n const detail =\n process.env.NODE_ENV !== \"production\" && e instanceof Error\n ? ` ${e.message}`\n : \"\";\n return { success: false, error: `Could not start the upload.${detail}` };\n }\n}\n\nexport async function listAssets(): Promise<GRAsset[]> {\n const viewer = await requireRole([\"admin\", \"editor\"]);\n const storage = getStorage();\n if (!viewer || !storage) return [];\n return storage.listAssets(await resolveSiteId());\n}\n\n/* ---------------------------------------------------------------- team --- */\n\nexport async function listTeam(): Promise<GRMember[]> {\n // Any member may see who else is on the site; only admins may change it.\n const viewer = await requireRole([\"admin\", \"editor\"]);\n if (!viewer) return [];\n\n const members = getMembers();\n return members.listMembers(await resolveSiteId());\n}\n\nexport async function inviteMember(\n email: string,\n role: GRRole,\n): Promise<Result> {\n const viewer = await requireRole([\"admin\"]);\n if (!viewer) return DENIED;\n\n const normalized = cleanEmail(email);\n if (!normalized) return { success: false, error: \"Enter a valid email address.\" };\n if (!isRole(role)) return { success: false, error: \"Choose admin or editor.\" };\n\n const siteId = await resolveSiteId();\n const members = getMembers();\n\n const existing = await members.getMember(siteId, { email: normalized });\n if (existing?.status === \"active\") {\n return { success: false, error: \"That address is already on the team.\" };\n }\n\n // A pending invite is re-sent rather than rejected: provider email is\n // rate-limited and unreliable enough that resending has to be possible.\n if (!existing) {\n const res = await members.upsertMember(siteId, {\n email: normalized,\n role,\n userId: null,\n status: \"pending\",\n });\n if (!res.success) return res;\n }\n\n const auth = getAuth();\n const onInvite = getInviteHandler();\n const slow = await rateLimited(\"invite\", viewer.userId);\n if (slow) return { success: false, error: `Invite saved, but not sent. ${slow}` };\n\n const where = await linkOrigin();\n if (\"error\" in where) {\n revalidatePath(\"/\", \"layout\");\n return { success: false, error: `Invite saved, but ${where.error}` };\n }\n const origin = where.origin;\n\n // The membership is saved either way, so none of what follows can undo it;\n // each failure just says what to fix. Delivery has one shape: the adapter\n // creates the account and a link (if it can), the app's handler emails it.\n const made = auth.createInviteLink\n ? await auth.createInviteLink(normalized, { origin })\n : null;\n if (made && !made.success) {\n revalidatePath(\"/\", \"layout\");\n return {\n success: false,\n error: `Invite saved, but the sign-in link couldn't be created: ${made.error}`,\n };\n }\n if (!onInvite) {\n revalidatePath(\"/\", \"layout\");\n return {\n success: false,\n error:\n \"Invite saved, but this site has nothing set up to email it. Add resendEmails() (or your own onInvite) to withGuardrail() — see the README.\",\n };\n }\n\n try {\n await onInvite({\n email: normalized,\n role: existing?.role ?? role,\n siteId,\n invitedBy: viewer.email ?? viewer.userId,\n ...(made ? { link: made.link, isNewUser: made.isNewUser } : {}),\n });\n } catch (e) {\n console.error(\"Guardrail: onInvite handler threw\", e);\n revalidatePath(\"/\", \"layout\");\n return {\n success: false,\n error: `Invite saved, but the email failed: ${e instanceof Error ? e.message : \"unknown error\"}`,\n };\n }\n\n revalidatePath(\"/\", \"layout\");\n return made && !made.isNewUser\n ? { success: true, notice: ALREADY_HAS_ACCOUNT(normalized) }\n : { success: true };\n}\n\nconst ALREADY_HAS_ACCOUNT = (email: string) =>\n `${email} already has an account, so they were told they've been added rather than sent a link — they sign in with their existing password.`;\n\nexport async function updateMemberRole(\n email: string,\n role: GRRole,\n): Promise<Result> {\n const viewer = await requireRole([\"admin\"]);\n if (!viewer) return DENIED;\n\n const targetEmail = cleanEmail(email);\n if (!targetEmail) return { success: false, error: \"No such member.\" };\n if (!isRole(role)) return { success: false, error: \"Choose admin or editor.\" };\n\n const siteId = await resolveSiteId();\n const members = getMembers();\n\n const target = await members.getMember(siteId, { email: targetEmail });\n if (!target) return { success: false, error: \"No such member.\" };\n\n if (target.role === \"admin\" && role !== \"admin\") {\n const guard = await wouldStrandSite(siteId, target.email);\n if (guard) return guard;\n }\n\n const res = await members.upsertMember(siteId, {\n email: target.email,\n role,\n userId: target.userId,\n status: target.status,\n });\n if (res.success) revalidatePath(\"/\", \"layout\");\n return res;\n}\n\nexport async function removeMember(email: string): Promise<Result> {\n const viewer = await requireRole([\"admin\"]);\n if (!viewer) return DENIED;\n\n const normalized = cleanEmail(email);\n if (!normalized) return { success: false, error: \"No such member.\" };\n\n const siteId = await resolveSiteId();\n const members = getMembers();\n\n const guard = await wouldStrandSite(siteId, normalized);\n if (guard) return guard;\n\n const res = await members.removeMember(siteId, normalized);\n if (res.success) revalidatePath(\"/\", \"layout\");\n return res;\n}\n\n/* ------------------------------------------------------ password reset --- */\n\n/** One reset email per address per site per minute, so the form can't be used to spam someone. */\nconst RESET_COOLDOWN_MS = 60_000;\nconst lastResetAt = new Map<string, number>();\n\n/** Floor for a self-service request, so how long it takes doesn't reveal whether the address is a member. */\nconst RESET_MIN_DURATION_MS = 600;\n\n/**\n * Sends one password-reset email: the adapter mints a one-time link and the\n * app's onPasswordReset handler emails it. If either is missing, says which —\n * there is no quieter fallback to hide behind.\n */\nasync function dispatchPasswordReset(email: string, requestedBy: string): Promise<Result> {\n const auth = getAuth();\n const handler = getPasswordResetHandler();\n\n if (!auth.createPasswordResetLink) {\n return {\n success: false,\n error: \"this site's auth setup can't create reset links — give the Supabase adapter a serviceRoleKey.\",\n };\n }\n if (!handler) {\n return {\n success: false,\n error: \"this site has no onPasswordReset handler — add resendEmails() (or your own) to withGuardrail().\",\n };\n }\n\n const where = await linkOrigin();\n if (\"error\" in where) return { success: false, error: where.error };\n\n const made = await auth.createPasswordResetLink(email, { origin: where.origin });\n if (!made.success) return { success: false, error: made.error };\n try {\n await handler({ email, siteId: await resolveSiteId(), link: made.link, requestedBy });\n return { success: true };\n } catch (e) {\n console.error(\"Guardrail: onPasswordReset handler threw\", e);\n return { success: false, error: e instanceof Error ? e.message : \"unknown error\" };\n }\n}\n\n/**\n * \"Forgot password?\" from the sign-in bar. Called with no session, so it says\n * the same thing whether or not the address belongs to anyone: a member gets an\n * email, a stranger gets nothing, and neither can tell which happened from the\n * response or how long it took. Only a malformed address is rejected, since\n * that says nothing about anyone.\n */\nexport async function forgotPasswordAction(email: string): Promise<Result> {\n const started = Date.now();\n const normalized = cleanEmail(email);\n if (!normalized) return { success: false, error: \"Enter a valid email address.\" };\n\n // Over the limit reads as an error rather than a fake success: it counts\n // members and strangers alike, so it reveals nothing about the address.\n const slow = await rateLimited(\"forgot-password\", normalized);\n if (slow) return { success: false, error: slow };\n\n try {\n const siteId = await resolveSiteId();\n const key = `${siteId}:${normalized}`;\n const last = lastResetAt.get(key);\n if (!last || started - last >= RESET_COOLDOWN_MS) {\n lastResetAt.set(key, started);\n if (lastResetAt.size > 500) {\n for (const [k, t] of lastResetAt) if (started - t >= RESET_COOLDOWN_MS) lastResetAt.delete(k);\n }\n\n // With a privileged membership check, only members are sent anything.\n // Without one (an adapter that can mint reset links but not look members\n // up) any address that asks is — which is why the Supabase adapter offers\n // both together.\n const members = getMembers();\n const known = members.emailHasAccess ? await members.emailHasAccess(siteId, normalized) : true;\n if (known) {\n const res = await dispatchPasswordReset(normalized, \"self\");\n if (!res.success) console.error(\"Guardrail: password reset failed:\", res.error);\n }\n }\n } catch (e) {\n rethrowIfBailout(e);\n console.error(\"Guardrail: password reset failed\", e);\n }\n\n const wait = RESET_MIN_DURATION_MS - (Date.now() - started);\n if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));\n return { success: true };\n}\n\n/**\n * An admin sending a password reset to someone on the team, from the Team\n * drawer. Unlike the self-service request this reports what happened — an admin\n * is allowed to know, and needs to when it didn't work.\n */\nexport async function sendPasswordResetAction(email: string): Promise<Result> {\n const viewer = await requireRole([\"admin\"]);\n if (!viewer) return DENIED;\n\n const normalized = cleanEmail(email);\n if (!normalized) return { success: false, error: \"No such member.\" };\n const target = await getMembers().getMember(await resolveSiteId(), { email: normalized });\n if (!target) return { success: false, error: \"No such member.\" };\n\n const slow = await rateLimited(\"password-reset\", viewer.userId);\n if (slow) return { success: false, error: slow };\n\n const res = await dispatchPasswordReset(normalized, viewer.email ?? viewer.userId);\n return res.success\n ? { success: true, notice: `Password reset email sent to ${normalized}.` }\n : { success: false, error: `The reset email couldn't be sent: ${res.error}` };\n}\n\nconst SITE_URL_MISSING =\n \"siteUrl isn't set, so sign-in links can't be built safely — set GUARDRAIL_SITE_URL (or siteUrl in withGuardrail()) to this site's public address.\";\n\n/**\n * Where an emailed link points. Production takes it from configuration only: a\n * reset can be requested by anyone for any address, so an address read from the\n * request would let a forged Host/Origin header mail a valid token to someone\n * else's domain. Development (no configuration) uses the request's own address.\n */\nasync function linkOrigin(): Promise<{ origin: string } | { error: string }> {\n const configured = await resolveSiteUrl();\n if (configured) return { origin: configured };\n if (process.env.NODE_ENV === \"production\") return { error: SITE_URL_MISSING };\n const h = await headers();\n const host = h.get(\"host\") ?? \"localhost\";\n const proto = h.get(\"x-forwarded-proto\") ?? (host.startsWith(\"localhost\") ? \"http\" : \"https\");\n return { origin: `${proto}://${host}` };\n}\n\n/**\n * Refuses changes that would leave a site with no admin, which would lock\n * everyone out of team management with no in-app way to recover.\n */\nasync function wouldStrandSite(\n siteId: string,\n email: string,\n): Promise<Result | null> {\n const members = getMembers();\n const all = await members.listMembers(siteId);\n const admins = all.filter((m) => m.role === \"admin\");\n\n if (admins.length <= 1 && admins.some((m) => m.email === email)) {\n return {\n success: false,\n error: \"A site must keep at least one admin.\",\n };\n }\n return null;\n}\n\n/* ---------------------------------------------------------------- auth --- */\n\nexport async function loginAction(\n _prev: ActionState,\n formData: FormData,\n): Promise<ActionState> {\n const email = String(formData.get(\"email\") ?? \"\");\n const password = String(formData.get(\"password\") ?? \"\");\n\n const auth = getAuth();\n if (!auth.authenticate) {\n return {\n success: false,\n error: \"This site's auth provider does not accept passwords.\",\n email,\n };\n }\n\n // Bounded before it becomes a counter key: this runs before anything is\n // checked, for anyone, so the raw field could be arbitrarily large.\n const slow = await rateLimited(\"login\", cleanEmail(email) ?? email.trim().toLowerCase().slice(0, 254));\n if (slow) return { success: false, error: slow, email };\n\n const result = await auth.authenticate(email, password);\n if (!result.success) {\n return { success: false, error: result.error, email };\n }\n\n revalidatePath(\"/\", \"layout\");\n return { success: true, email };\n}\n\nexport async function logoutAction(): Promise<{ success: boolean }> {\n const auth = getAuth();\n await auth.logout();\n // Cleared here, not by the client afterward: a Server Action's own\n // revalidatePath() below triggers Next's router to refresh the current\n // route automatically, which can race a client-side navigation attempted\n // right after this resolves — confirmed live, where that automatic\n // refresh (for the pre-logout URL, ?edit=true) won the race and silently\n // undid a client-side `window.location.href` to the plain path. Deleting\n // the cookie here instead makes it part of the one server round-trip that\n // revalidatePath already accounts for, with nothing left to race.\n const cookieStore = await cookies();\n cookieStore.delete(GR_COOKIES.EDIT_MODE);\n // Signing out ends whatever was pending for that session too. A leftover\n // \"needs a password\" flag used to survive this and keep the sign-in bar\n // pinned on screen.\n cookieStore.delete(GR_COOKIES.NEEDS_PASSWORD);\n cookieStore.delete(GR_COOKIES.AUTH_ERROR);\n revalidatePath(\"/\", \"layout\");\n return { success: true };\n}\n\n/* ---------------------------------------------------------- redirects --- */\n\n/**\n * Completes a session that arrived via an invite or password-recovery link. Called by AuthRedirectSync (react/), the\n * generic client component that notices this kind of link and hands off\n * whatever it found in the URL — the actual token exchange is entirely the\n * auth adapter's job (see GRAuthAdapter.completeAuthRedirect); core only\n * orchestrates the result.\n *\n * A no-op {success:false}, not an error, when the adapter doesn't implement\n * this — most page loads carry no redirect data at all, and an adapter with\n * no such concept should not make every one of them fail.\n */\nexport async function completeAuthRedirectAction(\n params: AuthRedirectParams,\n): Promise<{ success: boolean; needsPassword?: boolean; error?: string }> {\n const auth = getAuth();\n if (!auth.completeAuthRedirect) return { success: false };\n if (await rateLimited(\"auth-link\")) return { success: false, error: \"Too many attempts.\" };\n\n const res = await auth.completeAuthRedirect(params);\n const cookieStore = await cookies();\n if (res.success) {\n cookieStore.delete(GR_COOKIES.AUTH_ERROR);\n if (res.needsPassword) {\n cookieStore.set(GR_COOKIES.NEEDS_PASSWORD, \"true\", {\n path: \"/\",\n maxAge: GR_NEEDS_PASSWORD_TTL_SECONDS,\n });\n }\n revalidatePath(\"/\", \"layout\");\n } else {\n // Say so, rather than leaving the person on a plain page with nothing\n // visibly different. The provider's own wording stays in the server log; the\n // cookie carries only a code, which the chrome turns into plain English.\n console.error(\"Guardrail: couldn't complete an emailed link:\", res.error);\n cookieStore.set(\n GR_COOKIES.AUTH_ERROR,\n /expired|invalid|already|used|not found/i.test(res.error ?? \"\") ? \"expired\" : \"failed\",\n { path: \"/\", maxAge: GR_AUTH_ERROR_TTL_SECONDS },\n );\n revalidatePath(\"/\", \"layout\");\n }\n return res;\n}\n\n/**\n * Sets the current session's password — the prompt completeAuthRedirectAction\n * triggers via needsPassword. Requires only a valid session, not a role: an\n * invited person's role is claimed as a side effect of getCachedViewer()\n * resolving them, which already happens independently of this.\n */\nexport async function setPasswordAction(\n _prev: SetPasswordState,\n formData: FormData,\n): Promise<SetPasswordState> {\n const viewer = await getCachedViewer();\n if (!viewer) return { success: false, error: \"Not signed in.\" };\n\n const auth = getAuth();\n if (!auth.setPassword) {\n return { success: false, error: \"This site's auth provider does not support setting a password.\" };\n }\n\n const password = String(formData.get(\"password\") ?? \"\");\n // `confirm` is absent from older custom forms, so only checked when sent.\n const confirm = formData.get(\"confirm\");\n if (password.length < 8) {\n return { success: false, error: \"Use at least 8 characters.\" };\n }\n if (confirm !== null && String(confirm) !== password) {\n return { success: false, error: \"The two passwords don't match.\" };\n }\n const res = await auth.setPassword(password);\n if (res.success) {\n const cookieStore = await cookies();\n cookieStore.delete(GR_COOKIES.NEEDS_PASSWORD);\n revalidatePath(\"/\", \"layout\");\n }\n return res;\n}\n\n/**\n * Dismisses the set-password step without setting a password.\n *\n * @deprecated Guardrail's own UI no longer offers this: passwords are the only\n * way back in, so the step can't be skipped. Kept so an app's own UI built on\n * it keeps working.\n */\nexport async function skipSetPasswordAction(): Promise<{ success: boolean }> {\n const cookieStore = await cookies();\n cookieStore.delete(GR_COOKIES.NEEDS_PASSWORD);\n revalidatePath(\"/\", \"layout\");\n return { success: true };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -14,13 +14,22 @@ with expected(role, object, priv) as (values
|
|
|
14
14
|
-- gr_members: never readable by anon
|
|
15
15
|
('authenticated','gr_members','SELECT'), ('authenticated','gr_members','INSERT'), ('authenticated','gr_members','UPDATE'), ('authenticated','gr_members','DELETE'),
|
|
16
16
|
('service_role','gr_members','SELECT'), ('service_role','gr_members','INSERT'), ('service_role','gr_members','UPDATE'), ('service_role','gr_members','DELETE'),
|
|
17
|
+
-- gr_content_history: editors read it (row-level security narrows who); only
|
|
18
|
+
-- the trigger and revert function write it, so signed-in users get no writes
|
|
19
|
+
('authenticated','gr_content_history','SELECT'),
|
|
20
|
+
('service_role','gr_content_history','SELECT'), ('service_role','gr_content_history','INSERT'), ('service_role','gr_content_history','UPDATE'), ('service_role','gr_content_history','DELETE'),
|
|
21
|
+
-- gr_rate_limits: server-side only
|
|
22
|
+
('service_role','gr_rate_limits','SELECT'), ('service_role','gr_rate_limits','INSERT'), ('service_role','gr_rate_limits','UPDATE'), ('service_role','gr_rate_limits','DELETE'),
|
|
17
23
|
-- functions
|
|
18
|
-
('authenticated','fn gr_has_role','EXECUTE'), ('authenticated','fn gr_claim_invite','EXECUTE'), ('authenticated','fn gr_ensure_bucket','EXECUTE')
|
|
24
|
+
('authenticated','fn gr_has_role','EXECUTE'), ('authenticated','fn gr_claim_invite','EXECUTE'), ('authenticated','fn gr_ensure_bucket','EXECUTE'),
|
|
25
|
+
('authenticated','fn gr_confirmed_email','EXECUTE'),
|
|
26
|
+
('authenticated','fn gr_revert_content','EXECUTE'), ('authenticated','fn gr_revert_info','EXECUTE'),
|
|
27
|
+
('service_role','fn gr_rate_hit','EXECUTE')
|
|
19
28
|
),
|
|
20
29
|
actual(role, object, priv) as (
|
|
21
30
|
select r.rolname, t.tbl, p.priv
|
|
22
31
|
from (values ('anon'),('authenticated'),('service_role')) r(rolname)
|
|
23
|
-
cross join (values ('gr_content'),('gr_members'),('gr_repeater_items')) t(tbl)
|
|
32
|
+
cross join (values ('gr_content'),('gr_members'),('gr_repeater_items'),('gr_content_history'),('gr_rate_limits')) t(tbl)
|
|
24
33
|
cross join (values ('SELECT'),('INSERT'),('UPDATE'),('DELETE')) p(priv)
|
|
25
34
|
where to_regclass('guardrail.'||t.tbl) is not null
|
|
26
35
|
and has_table_privilege(r.rolname, 'guardrail.'||t.tbl, p.priv)
|
|
@@ -31,7 +40,7 @@ actual(role, object, priv) as (
|
|
|
31
40
|
union all
|
|
32
41
|
select r.rolname, 'fn '||f.fn, 'EXECUTE'
|
|
33
42
|
from (values ('anon'),('authenticated'),('service_role')) r(rolname)
|
|
34
|
-
cross join (values ('gr_has_role(text,text[])','gr_has_role'),('gr_claim_invite(text)','gr_claim_invite'),('gr_ensure_bucket(text)','gr_ensure_bucket')) f(sig, fn)
|
|
43
|
+
cross join (values ('gr_has_role(text,text[])','gr_has_role'),('gr_claim_invite(text)','gr_claim_invite'),('gr_ensure_bucket(text)','gr_ensure_bucket'),('gr_confirmed_email()','gr_confirmed_email'),('gr_revert_content(text,text)','gr_revert_content'),('gr_revert_info(text,text)','gr_revert_info'),('gr_rate_hit(text,text,integer,integer)','gr_rate_hit')) f(sig, fn)
|
|
35
44
|
where to_regprocedure('guardrail.'||f.sig) is not null
|
|
36
45
|
and has_function_privilege(r.rolname, 'guardrail.'||f.sig, 'EXECUTE')
|
|
37
46
|
)
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
-- Content history and one-step revert.
|
|
2
|
+
--
|
|
3
|
+
-- gr_content keeps exactly one row per field: the current, published value, and
|
|
4
|
+
-- that is all the public (anon) key can read. This adds a separate append-only
|
|
5
|
+
-- table of the values each field used to have, so an editor can step back one
|
|
6
|
+
-- version — and, with nothing left to step back to, return to the default the
|
|
7
|
+
-- developer wrote in the page.
|
|
8
|
+
--
|
|
9
|
+
-- History is deliberately a different table, not extra rows in gr_content:
|
|
10
|
+
-- * it is readable by editors only, so a value an editor removed does not stay
|
|
11
|
+
-- visible to everyone for the retention window;
|
|
12
|
+
-- * the per-request content read and its row cap are untouched;
|
|
13
|
+
-- * gr_content's one-row-per-field shape (and the upsert that relies on it)
|
|
14
|
+
-- is unchanged, so nothing that already works needs to change.
|
|
15
|
+
--
|
|
16
|
+
-- Retention: the newest 20 prior versions of a field, and nothing older than 90
|
|
17
|
+
-- days. Both are enforced as part of saving, so no scheduled job is needed.
|
|
18
|
+
--
|
|
19
|
+
-- Safe to re-run; changes no existing data.
|
|
20
|
+
|
|
21
|
+
/* -------------------------------------------------------------- table --- */
|
|
22
|
+
|
|
23
|
+
create table if not exists guardrail.gr_content_history (
|
|
24
|
+
id bigint generated always as identity primary key,
|
|
25
|
+
site_id text not null,
|
|
26
|
+
path_key text not null,
|
|
27
|
+
type text not null,
|
|
28
|
+
value jsonb not null,
|
|
29
|
+
-- When this version was saved (the superseded row's updated_at), so the
|
|
30
|
+
-- editor can say which version a revert goes back to.
|
|
31
|
+
version_at timestamptz not null,
|
|
32
|
+
-- When it was replaced; drives the 90-day retention.
|
|
33
|
+
archived_at timestamptz not null default now()
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
create index if not exists gr_content_history_key_idx
|
|
37
|
+
on guardrail.gr_content_history (site_id, path_key, id desc);
|
|
38
|
+
create index if not exists gr_content_history_age_idx
|
|
39
|
+
on guardrail.gr_content_history (archived_at);
|
|
40
|
+
|
|
41
|
+
alter table guardrail.gr_content_history enable row level security;
|
|
42
|
+
|
|
43
|
+
-- Editors of a site may read its history. Nobody writes it directly: rows are
|
|
44
|
+
-- added by the trigger below and removed by gr_revert_content() and retention,
|
|
45
|
+
-- all of which run with the privileges of the function, not the caller.
|
|
46
|
+
revoke all on guardrail.gr_content_history from public, anon, authenticated;
|
|
47
|
+
grant select on guardrail.gr_content_history to authenticated;
|
|
48
|
+
grant select, insert, update, delete on guardrail.gr_content_history to service_role;
|
|
49
|
+
|
|
50
|
+
drop policy if exists "editors read content history" on guardrail.gr_content_history;
|
|
51
|
+
create policy "editors read content history"
|
|
52
|
+
on guardrail.gr_content_history for select
|
|
53
|
+
to authenticated
|
|
54
|
+
using (guardrail.gr_has_role(site_id, array['admin', 'editor']));
|
|
55
|
+
|
|
56
|
+
/* ------------------------------------------------------------ archive --- */
|
|
57
|
+
-- Every save that changes a field's value moves the old value into history.
|
|
58
|
+
-- Saving the same value again records nothing.
|
|
59
|
+
|
|
60
|
+
create or replace function guardrail.gr_archive_content()
|
|
61
|
+
returns trigger
|
|
62
|
+
language plpgsql
|
|
63
|
+
security definer
|
|
64
|
+
set search_path = guardrail, pg_temp
|
|
65
|
+
as $$
|
|
66
|
+
begin
|
|
67
|
+
-- gr_revert_content() sets this while it restores a version, so stepping back
|
|
68
|
+
-- doesn't push the version it is leaving onto the history it is reading.
|
|
69
|
+
if coalesce(current_setting('guardrail.skip_history', true), '') = 'on' then
|
|
70
|
+
return new;
|
|
71
|
+
end if;
|
|
72
|
+
|
|
73
|
+
if new.value is not distinct from old.value and new.type is not distinct from old.type then
|
|
74
|
+
return new;
|
|
75
|
+
end if;
|
|
76
|
+
|
|
77
|
+
insert into guardrail.gr_content_history (site_id, path_key, type, value, version_at)
|
|
78
|
+
values (old.site_id, old.path_key, old.type, old.value, old.updated_at);
|
|
79
|
+
|
|
80
|
+
-- Retention for this field: the newest 20 prior versions, none older than 90 days.
|
|
81
|
+
delete from guardrail.gr_content_history h
|
|
82
|
+
where h.site_id = old.site_id
|
|
83
|
+
and h.path_key = old.path_key
|
|
84
|
+
and (
|
|
85
|
+
h.archived_at < now() - interval '90 days'
|
|
86
|
+
or h.id not in (
|
|
87
|
+
select id from guardrail.gr_content_history
|
|
88
|
+
where site_id = old.site_id and path_key = old.path_key
|
|
89
|
+
order by id desc
|
|
90
|
+
limit 20
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
-- And, now and then, the same age limit across every field, so history for
|
|
95
|
+
-- fields nobody edits again still ages out.
|
|
96
|
+
if random() < 0.02 then
|
|
97
|
+
delete from guardrail.gr_content_history where archived_at < now() - interval '90 days';
|
|
98
|
+
end if;
|
|
99
|
+
|
|
100
|
+
return new;
|
|
101
|
+
end;
|
|
102
|
+
$$;
|
|
103
|
+
|
|
104
|
+
revoke all on function guardrail.gr_archive_content() from public;
|
|
105
|
+
|
|
106
|
+
drop trigger if exists gr_archive_content on guardrail.gr_content;
|
|
107
|
+
create trigger gr_archive_content
|
|
108
|
+
before update on guardrail.gr_content
|
|
109
|
+
for each row execute function guardrail.gr_archive_content();
|
|
110
|
+
|
|
111
|
+
-- A field that is deleted (a repeater item being removed, say) takes its history
|
|
112
|
+
-- with it: removed content shouldn't linger.
|
|
113
|
+
create or replace function guardrail.gr_drop_content_history()
|
|
114
|
+
returns trigger
|
|
115
|
+
language plpgsql
|
|
116
|
+
security definer
|
|
117
|
+
set search_path = guardrail, pg_temp
|
|
118
|
+
as $$
|
|
119
|
+
begin
|
|
120
|
+
delete from guardrail.gr_content_history
|
|
121
|
+
where site_id = old.site_id and path_key = old.path_key;
|
|
122
|
+
return old;
|
|
123
|
+
end;
|
|
124
|
+
$$;
|
|
125
|
+
|
|
126
|
+
revoke all on function guardrail.gr_drop_content_history() from public;
|
|
127
|
+
|
|
128
|
+
drop trigger if exists gr_drop_content_history on guardrail.gr_content;
|
|
129
|
+
create trigger gr_drop_content_history
|
|
130
|
+
after delete on guardrail.gr_content
|
|
131
|
+
for each row execute function guardrail.gr_drop_content_history();
|
|
132
|
+
|
|
133
|
+
/* ------------------------------------------------------------- revert --- */
|
|
134
|
+
|
|
135
|
+
-- What a revert would do for a field, so the button can say so:
|
|
136
|
+
-- previous — there is an earlier version (at = when it was saved)
|
|
137
|
+
-- default — no earlier version, but a saved value: reverting returns the field
|
|
138
|
+
-- to the default written in the page
|
|
139
|
+
-- none — nothing saved, nothing to revert
|
|
140
|
+
-- security invoker: the caller's own access applies (editors see history; anyone
|
|
141
|
+
-- else sees none of it).
|
|
142
|
+
create or replace function guardrail.gr_revert_info(p_site text, p_key text)
|
|
143
|
+
returns jsonb
|
|
144
|
+
language sql
|
|
145
|
+
stable
|
|
146
|
+
set search_path = guardrail, pg_temp
|
|
147
|
+
as $$
|
|
148
|
+
select coalesce(
|
|
149
|
+
(select jsonb_build_object('kind', 'previous', 'at', version_at)
|
|
150
|
+
from guardrail.gr_content_history
|
|
151
|
+
where site_id = p_site and path_key = p_key
|
|
152
|
+
order by id desc
|
|
153
|
+
limit 1),
|
|
154
|
+
(select jsonb_build_object('kind', 'default')
|
|
155
|
+
from guardrail.gr_content
|
|
156
|
+
where site_id = p_site and path_key = p_key),
|
|
157
|
+
jsonb_build_object('kind', 'none')
|
|
158
|
+
);
|
|
159
|
+
$$;
|
|
160
|
+
|
|
161
|
+
revoke all on function guardrail.gr_revert_info(text, text) from public, anon;
|
|
162
|
+
grant execute on function guardrail.gr_revert_info(text, text) to authenticated;
|
|
163
|
+
|
|
164
|
+
-- One step back: restore the newest earlier version (and drop it from history),
|
|
165
|
+
-- or, with none left, delete the saved value so the page's own default shows.
|
|
166
|
+
create or replace function guardrail.gr_revert_content(p_site text, p_key text)
|
|
167
|
+
returns jsonb
|
|
168
|
+
language plpgsql
|
|
169
|
+
security definer
|
|
170
|
+
set search_path = guardrail, pg_temp
|
|
171
|
+
as $$
|
|
172
|
+
declare
|
|
173
|
+
h guardrail.gr_content_history%rowtype;
|
|
174
|
+
begin
|
|
175
|
+
if not guardrail.gr_has_role(p_site, array['admin', 'editor']) then
|
|
176
|
+
raise exception 'Only Guardrail editors can revert content'
|
|
177
|
+
using errcode = '42501';
|
|
178
|
+
end if;
|
|
179
|
+
|
|
180
|
+
select * into h
|
|
181
|
+
from guardrail.gr_content_history
|
|
182
|
+
where site_id = p_site and path_key = p_key
|
|
183
|
+
order by id desc
|
|
184
|
+
limit 1
|
|
185
|
+
for update;
|
|
186
|
+
|
|
187
|
+
if found then
|
|
188
|
+
perform set_config('guardrail.skip_history', 'on', true);
|
|
189
|
+
update guardrail.gr_content
|
|
190
|
+
set type = h.type, value = h.value, updated_at = h.version_at
|
|
191
|
+
where site_id = p_site and path_key = p_key;
|
|
192
|
+
if not found then
|
|
193
|
+
insert into guardrail.gr_content (site_id, path_key, type, value, updated_at)
|
|
194
|
+
values (p_site, p_key, h.type, h.value, h.version_at);
|
|
195
|
+
end if;
|
|
196
|
+
perform set_config('guardrail.skip_history', 'off', true);
|
|
197
|
+
|
|
198
|
+
delete from guardrail.gr_content_history where id = h.id;
|
|
199
|
+
return jsonb_build_object('action', 'restored', 'type', h.type, 'value', h.value);
|
|
200
|
+
end if;
|
|
201
|
+
|
|
202
|
+
delete from guardrail.gr_content where site_id = p_site and path_key = p_key;
|
|
203
|
+
if found then
|
|
204
|
+
return jsonb_build_object('action', 'reset');
|
|
205
|
+
end if;
|
|
206
|
+
return jsonb_build_object('action', 'none');
|
|
207
|
+
end;
|
|
208
|
+
$$;
|
|
209
|
+
|
|
210
|
+
revoke all on function guardrail.gr_revert_content(text, text) from public, anon;
|
|
211
|
+
grant execute on function guardrail.gr_revert_content(text, text) to authenticated;
|