spine-framework 0.3.85 → 0.3.87
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/.framework/src/App.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { LoginPage } from './pages/auth/LoginPage'
|
|
|
6
6
|
import { RegisterPage } from './pages/auth/RegisterPage'
|
|
7
7
|
import { PendingApprovalPage } from './pages/auth/PendingApprovalPage'
|
|
8
8
|
import { NotFoundPage } from './pages/NotFoundPage'
|
|
9
|
-
import {
|
|
9
|
+
import { SetupPage } from './pages/SetupPage'
|
|
10
10
|
import { AppWrapper } from './components/AppWrapper'
|
|
11
11
|
import { AppsRegistryProvider, useAppsRegistry } from './contexts/AppContext'
|
|
12
12
|
import { CustomAppLoader } from './components/CustomAppLoader'
|
|
@@ -36,6 +36,8 @@ function App() {
|
|
|
36
36
|
<Routes>
|
|
37
37
|
<Route path="/login" element={<LoginPage />} />
|
|
38
38
|
<Route path="/register" element={<RegisterPage />} />
|
|
39
|
+
{/* / shows setup instructions when no user is logged in */}
|
|
40
|
+
<Route path="/" element={<SetupPage />} />
|
|
39
41
|
<Route path="*" element={<Navigate to="/login" replace />} />
|
|
40
42
|
</Routes>
|
|
41
43
|
) : user.status === 'pending' ? (
|
|
@@ -64,8 +66,9 @@ function AuthenticatedRouter() {
|
|
|
64
66
|
)
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
// Sort: longer explicit prefixes first (most specific), root (/) last
|
|
68
|
-
//
|
|
69
|
+
// Sort: longer explicit prefixes first (most specific), root (/) last.
|
|
70
|
+
// The /spine-framework namespace is registered statically before dynamic routes
|
|
71
|
+
// so it always wins — but also filter it from dynamic registration to be safe.
|
|
69
72
|
const sorted = [...apps]
|
|
70
73
|
.filter(app => app.slug !== 'spine-framework' && !app.route_prefix?.startsWith('/spine-framework'))
|
|
71
74
|
.sort((a, b) => {
|
|
@@ -74,33 +77,19 @@ function AuthenticatedRouter() {
|
|
|
74
77
|
return (b.route_prefix?.length || 0) - (a.route_prefix?.length || 0)
|
|
75
78
|
})
|
|
76
79
|
|
|
77
|
-
// Determine default redirect:
|
|
78
|
-
// - If an app owns root ('/'), redirect there directly
|
|
79
|
-
// - Otherwise use the first non-root app's prefix
|
|
80
|
-
// - Fall back to /dashboard if no apps installed
|
|
81
|
-
const rootApp = sorted.find(app => app.route_prefix === '/')
|
|
82
|
-
const defaultApp = sorted.find(app => app.route_prefix && app.route_prefix !== '/')
|
|
83
|
-
const defaultRedirect = rootApp ? '/' : (defaultApp?.route_prefix || '/dashboard')
|
|
84
|
-
|
|
85
80
|
return (
|
|
86
81
|
<Suspense fallback={<div className="min-h-screen flex items-center justify-center"><LoadingSpinner /></div>}>
|
|
87
82
|
<Routes>
|
|
88
|
-
{/*
|
|
89
|
-
{defaultRedirect !== '/' && (
|
|
90
|
-
<Route path="/" element={<Navigate to={defaultRedirect} replace />} />
|
|
91
|
-
)}
|
|
92
|
-
<Route path="/dashboard" element={<DashboardPage />} />
|
|
93
|
-
|
|
94
|
-
{/* Login/register routes for authenticated users (redirects to qualified app) */}
|
|
95
|
-
<Route path="/login" element={<LoginPage />} />
|
|
96
|
-
<Route path="/register" element={<RegisterPage />} />
|
|
97
|
-
|
|
98
|
-
{/* ── Spine Framework namespace (reserved — no custom app may use this path) ── */}
|
|
83
|
+
{/* ── Spine Framework namespace — registered first, always wins over /* ── */}
|
|
99
84
|
<Route path="/spine-framework/admin/*" element={<AdminApp />} />
|
|
100
85
|
<Route path="/spine-framework/api" element={<APIPage />} />
|
|
101
86
|
<Route path="/spine-framework/cli" element={<CLIPage />} />
|
|
102
87
|
|
|
103
|
-
{/*
|
|
88
|
+
{/* Auth routes for already-authenticated users (LoginPage handles redirect) */}
|
|
89
|
+
<Route path="/login" element={<LoginPage />} />
|
|
90
|
+
<Route path="/register" element={<RegisterPage />} />
|
|
91
|
+
|
|
92
|
+
{/* Dynamic app routes — no framework opinions about / or /dashboard */}
|
|
104
93
|
{sorted.map(app => {
|
|
105
94
|
const prefix = app.route_prefix!
|
|
106
95
|
|
|
@@ -135,6 +124,7 @@ function AuthenticatedRouter() {
|
|
|
135
124
|
return null
|
|
136
125
|
})}
|
|
137
126
|
|
|
127
|
+
{/* No apps claim this path */}
|
|
138
128
|
<Route path="*" element={<NotFoundPage />} />
|
|
139
129
|
</Routes>
|
|
140
130
|
</Suspense>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @stability stable
|
|
6
6
|
*
|
|
7
7
|
* Generic 404 page rendered by the catch-all route. Displays a large
|
|
8
|
-
* "404" heading and a
|
|
8
|
+
* "404" heading and a link back to the sign-in page.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import React from 'react'
|
|
@@ -25,11 +25,11 @@ export function NotFoundPage() {
|
|
|
25
25
|
|
|
26
26
|
<div className="mt-6">
|
|
27
27
|
<Link
|
|
28
|
-
to="/
|
|
28
|
+
to="/login"
|
|
29
29
|
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
30
30
|
>
|
|
31
31
|
<Home className="w-4 h-4 mr-2" />
|
|
32
|
-
Go
|
|
32
|
+
Go to sign in
|
|
33
33
|
</Link>
|
|
34
34
|
</div>
|
|
35
35
|
</div>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module src/pages/SetupPage
|
|
3
|
+
* @audience installer
|
|
4
|
+
* @layer frontend-page
|
|
5
|
+
* @stability stable
|
|
6
|
+
*
|
|
7
|
+
* Shown at the root path when no apps are installed or no users have been
|
|
8
|
+
* created yet. Guides the developer through the initial setup steps.
|
|
9
|
+
* Replaces the old /dashboard fallback — the framework has no opinion
|
|
10
|
+
* about what lives at / once apps are installed.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import React from 'react'
|
|
14
|
+
import { Link } from 'react-router-dom'
|
|
15
|
+
|
|
16
|
+
export function SetupPage() {
|
|
17
|
+
return (
|
|
18
|
+
<div className="min-h-screen flex items-center justify-center bg-slate-50 py-12 px-4 sm:px-6 lg:px-8">
|
|
19
|
+
<div className="max-w-lg w-full space-y-8">
|
|
20
|
+
<div className="text-center">
|
|
21
|
+
<div className="mx-auto h-12 w-12 flex items-center justify-center rounded-full bg-slate-900">
|
|
22
|
+
<span className="text-white font-bold text-xl">S</span>
|
|
23
|
+
</div>
|
|
24
|
+
<h2 className="mt-6 text-3xl font-extrabold text-slate-900">
|
|
25
|
+
Welcome to Spine
|
|
26
|
+
</h2>
|
|
27
|
+
<p className="mt-2 text-sm text-slate-600">
|
|
28
|
+
Your project is running but no apps are installed yet.
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div className="bg-white shadow rounded-lg p-6 space-y-4">
|
|
33
|
+
<h3 className="text-sm font-semibold text-slate-700 uppercase tracking-wide">
|
|
34
|
+
Getting started
|
|
35
|
+
</h3>
|
|
36
|
+
<ol className="space-y-3 text-sm text-slate-600 list-decimal list-inside">
|
|
37
|
+
<li>
|
|
38
|
+
Edit <code className="bg-slate-100 px-1 rounded">.env</code> with your Supabase credentials
|
|
39
|
+
</li>
|
|
40
|
+
<li>
|
|
41
|
+
Run migrations:{' '}
|
|
42
|
+
<code className="bg-slate-100 px-1 rounded">npx spine-framework migrate</code>
|
|
43
|
+
</li>
|
|
44
|
+
<li>
|
|
45
|
+
Install an app:{' '}
|
|
46
|
+
<code className="bg-slate-100 px-1 rounded">npx spine-framework install-app spine-framework-cortex@next</code>
|
|
47
|
+
</li>
|
|
48
|
+
<li>
|
|
49
|
+
Reassemble:{' '}
|
|
50
|
+
<code className="bg-slate-100 px-1 rounded">npm run assemble && netlify dev</code>
|
|
51
|
+
</li>
|
|
52
|
+
</ol>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div className="text-center">
|
|
56
|
+
<Link
|
|
57
|
+
to="/login"
|
|
58
|
+
className="text-sm font-medium text-slate-600 hover:text-slate-900"
|
|
59
|
+
>
|
|
60
|
+
Already set up? Sign in →
|
|
61
|
+
</Link>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -13,10 +13,9 @@
|
|
|
13
13
|
* @seeAlso src/contexts/AuthContext.tsx (login implementation)
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import React, { useState
|
|
17
|
-
import { Link
|
|
16
|
+
import React, { useState } from 'react'
|
|
17
|
+
import { Link } from 'react-router-dom'
|
|
18
18
|
import { useAuth } from '../../contexts/AuthContext'
|
|
19
|
-
import { useAppsRegistry } from '../../contexts/AppContext'
|
|
20
19
|
import { LoadingSpinner } from '../../components/ui/LoadingSpinner'
|
|
21
20
|
|
|
22
21
|
export function LoginPage() {
|
|
@@ -25,45 +24,11 @@ export function LoginPage() {
|
|
|
25
24
|
const [error, setError] = useState('')
|
|
26
25
|
const [isLoading, setIsLoading] = useState(false)
|
|
27
26
|
|
|
28
|
-
const {
|
|
29
|
-
const navigate = useNavigate()
|
|
30
|
-
const { routableApps: apps, loading } = useAppsRegistry()
|
|
27
|
+
const { login } = useAuth()
|
|
31
28
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// System admin gets special handling
|
|
36
|
-
if (user.roles?.includes('system_admin')) {
|
|
37
|
-
navigate('/spine-framework/admin', { replace: true })
|
|
38
|
-
return
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Find app matching user's role
|
|
42
|
-
const userRole = user.roles?.[0] // Single role per person
|
|
43
|
-
if (userRole) {
|
|
44
|
-
const matchingApp = apps.find(app => app.min_role === userRole)
|
|
45
|
-
if (matchingApp?.route_prefix) {
|
|
46
|
-
navigate(matchingApp.route_prefix, { replace: true })
|
|
47
|
-
return
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// No matching app - show 404
|
|
52
|
-
// The 404 will be handled by the catch-all route in App.tsx
|
|
53
|
-
}
|
|
54
|
-
}, [user, loading, apps, navigate])
|
|
55
|
-
|
|
56
|
-
// Show loading while checking auth state (but allow redirect logic to run)
|
|
57
|
-
if (loading) {
|
|
58
|
-
return (
|
|
59
|
-
<div className="min-h-screen flex items-center justify-center bg-slate-50">
|
|
60
|
-
<div className="text-center">
|
|
61
|
-
<LoadingSpinner className="w-8 h-8 mx-auto mb-4" />
|
|
62
|
-
<p className="text-slate-600">Checking authentication...</p>
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
65
|
-
)
|
|
66
|
-
}
|
|
29
|
+
// After login, App.tsx switches to AuthenticatedRouter which handles
|
|
30
|
+
// all routing decisions based on the user's role and installed apps.
|
|
31
|
+
// LoginPage has no redirect logic of its own.
|
|
67
32
|
|
|
68
33
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
69
34
|
e.preventDefault()
|
|
@@ -71,15 +36,11 @@ export function LoginPage() {
|
|
|
71
36
|
setIsLoading(true)
|
|
72
37
|
|
|
73
38
|
try {
|
|
74
|
-
console.log('Attempting login...')
|
|
75
39
|
await login(email, password)
|
|
76
|
-
//
|
|
77
|
-
console.log('Login successful, redirect will be handled by useEffect...')
|
|
40
|
+
// App.tsx switches to AuthenticatedRouter on success — no redirect needed here
|
|
78
41
|
} catch (err: any) {
|
|
79
|
-
console.error('Login error:', err)
|
|
80
42
|
setError(err.message || 'Login failed')
|
|
81
43
|
} finally {
|
|
82
|
-
console.log('Login process completed, setting loading to false')
|
|
83
44
|
setIsLoading(false)
|
|
84
45
|
}
|
|
85
46
|
}
|