@orion-studios/cms 0.5.3 → 0.5.4

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.
@@ -389,7 +389,7 @@ var MIN_ROLE = {
389
389
  "pages.delete": "admin",
390
390
  "pages.restore": "editor",
391
391
  "globals.read": "content",
392
- "globals.write": "content",
392
+ "globals.write": "editor",
393
393
  "media.read": "content",
394
394
  "media.upload": "content",
395
395
  "media.update": "content",
@@ -663,6 +663,8 @@ var errors = {
663
663
  var isRecord2 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
664
664
  var SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
665
665
  var PATH_PATTERN = /^\/(?:[a-z0-9-]+(?:\/[a-z0-9-]+)*)?$/;
666
+ var USER_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
667
+ var canonicalizeUserId = (value) => USER_ID_PATTERN.test(value) ? value.toLowerCase() : null;
666
668
  var DEFAULT_MAX_UPLOAD_BYTES = 15 * 1024 * 1024;
667
669
  var ALLOWED_UPLOAD_TYPES = /* @__PURE__ */ new Set([
668
670
  "image/jpeg",
@@ -1706,23 +1708,30 @@ function createCmsRoutes(options) {
1706
1708
  const listUsers = async (request) => {
1707
1709
  const auth = await guard(request, "users.manage");
1708
1710
  if (auth instanceof Response) return auth;
1709
- const { data: authData, error: authError } = await db().auth.admin.listUsers({ perPage: 1e3 });
1710
- if (authError) return errors.badRequest(authError.message);
1711
1711
  const { data: profiles, error: profileError } = await db().from("cms_profiles").select("user_id, role, name");
1712
1712
  if (profileError) return errors.badRequest(profileError.message);
1713
- const roleById = new Map(
1714
- (profiles || []).map((profile) => [profile.user_id, profile])
1715
- );
1716
- const users = authData.users.map((user) => {
1717
- const profile = roleById.get(user.id);
1718
- return {
1713
+ const profileById = /* @__PURE__ */ new Map();
1714
+ for (const profile of profiles || []) {
1715
+ const userId = typeof profile.user_id === "string" ? canonicalizeUserId(profile.user_id) : null;
1716
+ if (!userId || !isCmsRole(profile.role)) continue;
1717
+ profileById.set(userId, {
1718
+ role: profile.role,
1719
+ name: typeof profile.name === "string" ? profile.name : ""
1720
+ });
1721
+ }
1722
+ const { data: authData, error: authError } = await db().auth.admin.listUsers({ perPage: 1e3 });
1723
+ if (authError) return errors.badRequest(authError.message);
1724
+ const users = authData.users.flatMap((user) => {
1725
+ const canonicalUserId = canonicalizeUserId(user.id);
1726
+ const profile = canonicalUserId ? profileById.get(canonicalUserId) : void 0;
1727
+ return profile ? [{
1719
1728
  id: user.id,
1720
1729
  email: user.email ?? null,
1721
- name: profile?.name || "",
1722
- role: profile?.role ?? null,
1730
+ name: profile.name,
1731
+ role: profile.role,
1723
1732
  created_at: user.created_at ?? null,
1724
1733
  last_sign_in_at: user.last_sign_in_at ?? null
1725
- };
1734
+ }] : [];
1726
1735
  });
1727
1736
  return json({ users, assignableRoles: assignableRoles(auth.user.role) });
1728
1737
  };
@@ -1757,20 +1766,31 @@ function createCmsRoutes(options) {
1757
1766
  await logActivity(auth.user, "user.create", email);
1758
1767
  return json({ user: { id: userId, email, name, role } }, 201);
1759
1768
  };
1760
- const targetRole = async (userId) => {
1761
- const { data } = await db().from("cms_profiles").select("role").eq("user_id", userId).maybeSingle();
1762
- return data?.role ?? null;
1769
+ const loadMembership = async (userId) => {
1770
+ const { data, error } = await db().from("cms_profiles").select("role").eq("user_id", userId).maybeSingle();
1771
+ if (error) return { status: "query-error", message: error.message };
1772
+ if (!data) return { status: "missing" };
1773
+ if (!isCmsRole(data.role)) return { status: "invalid-role" };
1774
+ return { status: "valid", role: data.role };
1775
+ };
1776
+ const membershipError = (membership) => {
1777
+ if (membership.status === "missing") return errors.notFound();
1778
+ if (membership.status === "invalid-role") return errors.badRequest("Invalid CMS profile role.");
1779
+ return errors.badRequest(membership.message);
1763
1780
  };
1764
1781
  const updateUser = async (request, userId) => {
1765
1782
  const auth = await guard(request, "users.manage");
1766
1783
  if (auth instanceof Response) return auth;
1784
+ const canonicalUserId = canonicalizeUserId(userId);
1785
+ if (!canonicalUserId) return errors.badRequest("Invalid user ID.");
1767
1786
  const body = await readJson(request);
1768
1787
  if (!body) return errors.badRequest("Invalid body.");
1769
- const current = await targetRole(userId);
1770
- if (current && !outranksOrEqual(auth.user.role, current)) return errors.forbidden();
1788
+ const membership = await loadMembership(canonicalUserId);
1789
+ if (membership.status !== "valid") return membershipError(membership);
1790
+ if (!outranksOrEqual(auth.user.role, membership.role)) return errors.forbidden();
1771
1791
  if (body.role !== void 0) {
1772
1792
  if (!isCmsRole(body.role)) return errors.badRequest("Invalid role.");
1773
- if (userId === auth.user.id && body.role !== auth.user.role) {
1793
+ if (canonicalUserId === auth.user.id && body.role !== auth.user.role) {
1774
1794
  return errors.badRequest("You can't change your own role.");
1775
1795
  }
1776
1796
  if (!outranksOrEqual(auth.user.role, body.role)) {
@@ -1779,32 +1799,37 @@ function createCmsRoutes(options) {
1779
1799
  }
1780
1800
  if (typeof body.password === "string") {
1781
1801
  if (body.password.length < 8) return errors.badRequest("Password must be at least 8 characters.");
1782
- const { error: passwordError } = await db().auth.admin.updateUserById(userId, {
1802
+ const { error: passwordError } = await db().auth.admin.updateUserById(canonicalUserId, {
1783
1803
  password: body.password
1784
1804
  });
1785
1805
  if (passwordError) return errors.badRequest(passwordError.message);
1786
1806
  }
1787
1807
  if (body.role !== void 0 || typeof body.name === "string") {
1788
- const patch = { user_id: userId };
1808
+ const patch = {};
1789
1809
  if (body.role !== void 0) patch.role = body.role;
1790
1810
  if (typeof body.name === "string") patch.name = body.name.trim();
1791
- const { error: profileError } = await db().from("cms_profiles").upsert(patch, { onConflict: "user_id" });
1811
+ const { error: profileError } = await db().from("cms_profiles").update(patch).eq("user_id", canonicalUserId);
1792
1812
  if (profileError) return errors.badRequest(profileError.message);
1793
1813
  }
1794
- await logActivity(auth.user, "user.update", userId);
1814
+ await logActivity(auth.user, "user.update", canonicalUserId);
1795
1815
  return json({ success: true });
1796
1816
  };
1797
1817
  const deleteUser = async (request, userId) => {
1798
1818
  const auth = await guard(request, "users.manage");
1799
1819
  if (auth instanceof Response) return auth;
1800
- if (userId === auth.user.id) return errors.badRequest("You can't remove your own account.");
1801
- const current = await targetRole(userId);
1802
- if (current && !outranksOrEqual(auth.user.role, current)) return errors.forbidden();
1803
- const { error: authError } = await db().auth.admin.deleteUser(userId);
1820
+ const canonicalUserId = canonicalizeUserId(userId);
1821
+ if (!canonicalUserId) return errors.badRequest("Invalid user ID.");
1822
+ const membership = await loadMembership(canonicalUserId);
1823
+ if (membership.status !== "valid") return membershipError(membership);
1824
+ if (canonicalUserId === auth.user.id) {
1825
+ return errors.badRequest("You can't remove your own account.");
1826
+ }
1827
+ if (!outranksOrEqual(auth.user.role, membership.role)) return errors.forbidden();
1828
+ const { error: authError } = await db().auth.admin.deleteUser(canonicalUserId);
1804
1829
  if (authError) return errors.badRequest(authError.message);
1805
- const { error: profileError } = await db().from("cms_profiles").delete().eq("user_id", userId);
1830
+ const { error: profileError } = await db().from("cms_profiles").delete().eq("user_id", canonicalUserId);
1806
1831
  if (profileError) return errors.badRequest(profileError.message);
1807
- await logActivity(auth.user, "user.delete", userId);
1832
+ await logActivity(auth.user, "user.delete", canonicalUserId);
1808
1833
  return json({ success: true });
1809
1834
  };
1810
1835
  const runSync = async (request) => {
@@ -149,7 +149,7 @@ var MIN_ROLE = {
149
149
  "pages.delete": "admin",
150
150
  "pages.restore": "editor",
151
151
  "globals.read": "content",
152
- "globals.write": "content",
152
+ "globals.write": "editor",
153
153
  "media.read": "content",
154
154
  "media.upload": "content",
155
155
  "media.update": "content",
@@ -179,13 +179,14 @@ var SECTION_ACTION = {
179
179
  forms: "forms.read",
180
180
  submissions: "submissions.read",
181
181
  analytics: "analytics.read",
182
- globals: "globals.read",
182
+ globals: "globals.write",
183
183
  redirects: "redirects.manage",
184
184
  users: "users.manage"
185
185
  };
186
186
  var userForRole = (role) => ({ id: "", email: null, name: "", role });
187
187
  var studioCan = (role, action) => can(userForRole(role), action);
188
188
  var canViewStudioSection = (role, section) => studioCan(role, SECTION_ACTION[section]);
189
+ var resolveStudioSection = (role, section) => role && canViewStudioSection(role, section) ? section : "dashboard";
189
190
  var studioAccess = (role) => ({
190
191
  changePageStructure: studioCan(role, "pages.changeStructure"),
191
192
  publishPages: studioCan(role, "pages.publish"),
@@ -3728,11 +3729,15 @@ function Studio({ registry, globals, siteName, logoUrl, supabaseUrl }) {
3728
3729
  useEffect11(() => {
3729
3730
  if (!session) {
3730
3731
  setRole(null);
3732
+ setSection("dashboard");
3733
+ setOpenPageId(null);
3734
+ setSubmissionsFormId(null);
3731
3735
  return;
3732
3736
  }
3733
3737
  api.me().then(
3734
3738
  ({ user }) => {
3735
3739
  setRole(user.role);
3740
+ setSection((currentSection) => resolveStudioSection(user.role, currentSection));
3736
3741
  refreshMedia();
3737
3742
  refreshUnread();
3738
3743
  refreshPages();
@@ -3784,6 +3789,7 @@ function Studio({ registry, globals, siteName, logoUrl, supabaseUrl }) {
3784
3789
  if (!role) return /* @__PURE__ */ jsx12("div", { className: "ost-root ost-loading", children: "Loading Studio\u2026" });
3785
3790
  const access = studioAccess(role);
3786
3791
  const nav = NAV.filter((item) => canViewStudioSection(role, item.key));
3792
+ const selectedSection = resolveStudioSection(role, section);
3787
3793
  const goTo = (key) => {
3788
3794
  setSection(key);
3789
3795
  setOpenPageId(null);
@@ -3806,7 +3812,7 @@ function Studio({ registry, globals, siteName, logoUrl, supabaseUrl }) {
3806
3812
  nav.map((item) => /* @__PURE__ */ jsxs12(
3807
3813
  "button",
3808
3814
  {
3809
- className: `ost-nav-item${section === item.key && !openPageId ? " is-active" : ""}`,
3815
+ className: `ost-nav-item${selectedSection === item.key && !openPageId ? " is-active" : ""}`,
3810
3816
  onClick: () => goTo(item.key),
3811
3817
  type: "button",
3812
3818
  children: [
@@ -3841,7 +3847,7 @@ function Studio({ registry, globals, siteName, logoUrl, supabaseUrl }) {
3841
3847
  role
3842
3848
  },
3843
3849
  openPageId
3844
- ) : section === "pages" ? /* @__PURE__ */ jsx12(PagesView, { api, canCreate: access.createPages, onOpen: openPage }) : section === "media" ? /* @__PURE__ */ jsx12(
3850
+ ) : selectedSection === "pages" ? /* @__PURE__ */ jsx12(PagesView, { api, canCreate: access.createPages, onOpen: openPage }) : selectedSection === "media" ? /* @__PURE__ */ jsx12(
3845
3851
  MediaView,
3846
3852
  {
3847
3853
  api,
@@ -3852,7 +3858,7 @@ function Studio({ registry, globals, siteName, logoUrl, supabaseUrl }) {
3852
3858
  onChanged: refreshMedia,
3853
3859
  onUpload: uploadMedia
3854
3860
  }
3855
- ) : section === "forms" ? /* @__PURE__ */ jsx12(
3861
+ ) : selectedSection === "forms" ? /* @__PURE__ */ jsx12(
3856
3862
  FormsView,
3857
3863
  {
3858
3864
  api,
@@ -3864,7 +3870,7 @@ function Studio({ registry, globals, siteName, logoUrl, supabaseUrl }) {
3864
3870
  setSection("submissions");
3865
3871
  }
3866
3872
  }
3867
- ) : section === "submissions" && access.readSubmissions ? /* @__PURE__ */ jsx12(
3873
+ ) : selectedSection === "submissions" && access.readSubmissions ? /* @__PURE__ */ jsx12(
3868
3874
  SubmissionsView,
3869
3875
  {
3870
3876
  api,
@@ -3873,7 +3879,7 @@ function Studio({ registry, globals, siteName, logoUrl, supabaseUrl }) {
3873
3879
  onUnreadChanged: refreshUnread
3874
3880
  },
3875
3881
  submissionsFormId || "all"
3876
- ) : section === "analytics" && access.readAnalytics ? /* @__PURE__ */ jsx12(AnalyticsView, { api }) : section === "globals" ? /* @__PURE__ */ jsx12(
3882
+ ) : selectedSection === "analytics" && access.readAnalytics ? /* @__PURE__ */ jsx12(AnalyticsView, { api }) : selectedSection === "globals" ? /* @__PURE__ */ jsx12(
3877
3883
  GlobalsView,
3878
3884
  {
3879
3885
  api,
@@ -3883,7 +3889,7 @@ function Studio({ registry, globals, siteName, logoUrl, supabaseUrl }) {
3883
3889
  onUploadMedia: uploadMedia,
3884
3890
  pages
3885
3891
  }
3886
- ) : section === "redirects" && access.manageRedirects ? /* @__PURE__ */ jsx12(RedirectsView, { api }) : section === "users" && access.manageUsers ? /* @__PURE__ */ jsx12(UsersView, { api, meId: session.user.id }) : /* @__PURE__ */ jsx12(
3892
+ ) : selectedSection === "redirects" && access.manageRedirects ? /* @__PURE__ */ jsx12(RedirectsView, { api }) : selectedSection === "users" && access.manageUsers ? /* @__PURE__ */ jsx12(UsersView, { api, meId: session.user.id }) : /* @__PURE__ */ jsx12(
3887
3893
  DashboardView,
3888
3894
  {
3889
3895
  api,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orion-studios/cms",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "Orion CMS v2 core engine \u2014 JSONB content model on Supabase primitives",
5
5
  "type": "module",
6
6
  "exports": {