spine-framework 0.3.69 → 0.3.71

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.
@@ -248,7 +248,9 @@ async function seedApp(appDir: string, appSlug: string) {
248
248
  is_active: true,
249
249
  route_prefix: manifest.route_prefix ?? ('/' + appSlug),
250
250
  renderer: manifest.renderer ?? 'custom',
251
- // min_role: first entry of required_roles array, or explicit min_role field
251
+ // required_roles: full array from manifest (e.g. ["support", "support_admin"])
252
+ required_roles: manifest.required_roles ?? (manifest.min_role ? [manifest.min_role] : []),
253
+ // min_role: kept as legacy fallback — first entry of required_roles
252
254
  min_role: manifest.required_roles?.[0] ?? manifest.min_role ?? null,
253
255
  },
254
256
  { onConflict: 'slug' }
@@ -0,0 +1,75 @@
1
+ -- Migration 017: Fix get_account_apps() to include globally-available apps
2
+ --
3
+ -- Problem: The RPC function excluded apps where owner_account_id IS NULL,
4
+ -- meaning installed apps (source='npm', owner_account_id=NULL) were invisible
5
+ -- to all users in the apps list.
6
+ --
7
+ -- Fix: Include apps where owner_account_id IS NULL (globally available apps).
8
+
9
+ CREATE OR REPLACE FUNCTION public.get_account_apps(
10
+ account_id uuid,
11
+ include_system boolean DEFAULT true,
12
+ include_inactive boolean DEFAULT false
13
+ )
14
+ RETURNS TABLE(
15
+ id uuid,
16
+ slug text,
17
+ name text,
18
+ description text,
19
+ icon text,
20
+ color text,
21
+ version text,
22
+ app_type text,
23
+ source text,
24
+ owner_account_id uuid,
25
+ is_active boolean,
26
+ is_system boolean,
27
+ min_role text,
28
+ config jsonb,
29
+ nav_items jsonb,
30
+ route_prefix text,
31
+ renderer text,
32
+ metadata jsonb,
33
+ integration_deps jsonb,
34
+ created_at timestamptz
35
+ )
36
+ LANGUAGE plpgsql
37
+ SECURITY DEFINER
38
+ AS $$
39
+ BEGIN
40
+ RETURN QUERY
41
+ SELECT
42
+ a.id,
43
+ a.slug,
44
+ a.name,
45
+ a.description,
46
+ a.icon,
47
+ a.color,
48
+ a.version,
49
+ a.app_type,
50
+ a.source,
51
+ a.owner_account_id,
52
+ a.is_active,
53
+ a.is_system,
54
+ a.min_role,
55
+ a.config,
56
+ a.nav_items,
57
+ a.route_prefix,
58
+ a.renderer,
59
+ a.metadata,
60
+ a.integration_deps,
61
+ a.created_at
62
+ FROM public.apps a
63
+ WHERE
64
+ (include_inactive OR a.is_active = true)
65
+ AND (
66
+ a.is_system = true -- always include system apps
67
+ OR a.owner_account_id = get_account_apps.account_id -- owned by this account
68
+ OR a.owner_account_id IS NULL -- globally available (npm-installed apps)
69
+ )
70
+ ORDER BY
71
+ a.is_system DESC,
72
+ a.app_type,
73
+ a.name;
74
+ END;
75
+ $$;
@@ -0,0 +1,17 @@
1
+ -- Migration 018: Add required_roles array to apps table
2
+ --
3
+ -- min_role (single string) only supports one role gate.
4
+ -- required_roles (jsonb array) supports gating by any of several roles.
5
+ -- e.g. ["support", "support_admin"] — user needs either role to access the app.
6
+ --
7
+ -- Both columns coexist. required_roles takes precedence at runtime;
8
+ -- min_role is kept as a legacy fallback and for display purposes.
9
+
10
+ ALTER TABLE public.apps
11
+ ADD COLUMN IF NOT EXISTS required_roles jsonb DEFAULT '[]'::jsonb;
12
+
13
+ -- Backfill: migrate existing min_role values into the array
14
+ UPDATE public.apps
15
+ SET required_roles = jsonb_build_array(min_role)
16
+ WHERE min_role IS NOT NULL
17
+ AND (required_roles = '[]'::jsonb OR required_roles IS NULL);
@@ -23,9 +23,14 @@ export function AppWrapper({ app, children }: AppWrapperProps) {
23
23
  return <Navigate to="/login" replace />
24
24
  }
25
25
 
26
- // Role gate
27
- if (app.min_role) {
28
- const hasRole = user.roles?.includes('system_admin') || user.roles?.includes(app.min_role)
26
+ // Role gate — check required_roles array (preferred) or min_role (fallback)
27
+ const requiredRoles: string[] = app.required_roles?.length
28
+ ? app.required_roles
29
+ : app.min_role ? [app.min_role] : []
30
+
31
+ if (requiredRoles.length > 0) {
32
+ const hasRole = user.roles?.includes('system_admin') ||
33
+ requiredRoles.some(r => user.roles?.includes(r))
29
34
  if (!hasRole) {
30
35
  return (
31
36
  <div className="min-h-screen flex items-center justify-center bg-slate-50">
@@ -39,7 +44,7 @@ export function AppWrapper({ app, children }: AppWrapperProps) {
39
44
  <p className="text-slate-600 mb-6">You don't have permission to access {app.name}.</p>
40
45
  <div className="bg-slate-100 rounded-lg p-4 text-left">
41
46
  <p className="text-sm text-slate-600 mb-2"><strong>Your roles:</strong> {user.roles?.join(', ') || 'None'}</p>
42
- <p className="text-sm text-slate-600"><strong>Required:</strong> {app.min_role}</p>
47
+ <p className="text-sm text-slate-600"><strong>Required (any):</strong> {requiredRoles.join(', ')}</p>
43
48
  </div>
44
49
  </div>
45
50
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-framework",
3
- "version": "0.3.69",
3
+ "version": "0.3.71",
4
4
  "description": "Multi-tenant, modular application platform for modern SaaS systems",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,6 +23,8 @@
23
23
  ".framework/migrations/005_auth_user_trigger.sql",
24
24
  ".framework/migrations/006_fix_current_actor_id.sql",
25
25
  ".framework/migrations/016_invites.sql",
26
+ ".framework/migrations/017_fix_get_account_apps.sql",
27
+ ".framework/migrations/018_apps_required_roles.sql",
26
28
  ".framework/public/",
27
29
  ".framework/scripts/",
28
30
  ".framework/seeds/",