@stevederico/skateboard-ui 1.2.17 → 1.2.19
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/CHANGELOG.md +12 -0
- package/Context.jsx +52 -0
- package/ProtectedRoute.jsx +7 -1
- package/README.md +90 -0
- package/Utilities.js +84 -1
- package/docs/AUTHENTICATION.md +533 -0
- package/package.json +1 -1
- package/deno.lock +0 -906
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
|
+
1.2.19
|
|
3
|
+
|
|
4
|
+
Skip noLogin backend validation
|
|
5
|
+
Export getConstants utility
|
|
6
|
+
|
|
7
|
+
1.2.18
|
|
8
|
+
|
|
9
|
+
Add authentication documentation
|
|
10
|
+
Add JSDoc comments
|
|
11
|
+
Update README configuration
|
|
12
|
+
Create docs folder
|
|
13
|
+
|
|
2
14
|
1.2.17
|
|
3
15
|
|
|
4
16
|
Fix SignIn layout positioning
|
package/Context.jsx
CHANGED
|
@@ -5,6 +5,22 @@ const context = createContext();
|
|
|
5
5
|
// Store dispatch reference for programmatic access outside components
|
|
6
6
|
let _dispatch = null;
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Get dispatch function for programmatic state updates outside components.
|
|
10
|
+
*
|
|
11
|
+
* Useful for updating state from non-React code (event handlers, utilities).
|
|
12
|
+
* Returns null if ContextProvider hasn't mounted yet.
|
|
13
|
+
*
|
|
14
|
+
* @returns {Function|null} Dispatch function or null
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* import { getDispatch } from '@stevederico/skateboard-ui/Context';
|
|
18
|
+
*
|
|
19
|
+
* const dispatch = getDispatch();
|
|
20
|
+
* if (dispatch) {
|
|
21
|
+
* dispatch({ type: 'CLEAR_USER' });
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
8
24
|
export function getDispatch() {
|
|
9
25
|
return _dispatch;
|
|
10
26
|
}
|
|
@@ -58,6 +74,24 @@ function safeLSRemoveItem(key) {
|
|
|
58
74
|
}
|
|
59
75
|
}
|
|
60
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Global state provider for skateboard-ui.
|
|
79
|
+
*
|
|
80
|
+
* Manages user authentication state and UI visibility (sidebar, tabbar).
|
|
81
|
+
* Persists user data to localStorage using app-specific keys.
|
|
82
|
+
*
|
|
83
|
+
* @param {Object} props
|
|
84
|
+
* @param {Object} props.constants - App configuration
|
|
85
|
+
* @param {string} props.constants.appName - Used for localStorage key namespacing
|
|
86
|
+
* @param {React.ReactNode} props.children - Child components
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* import { ContextProvider } from '@stevederico/skateboard-ui/Context';
|
|
90
|
+
*
|
|
91
|
+
* <ContextProvider constants={constants}>
|
|
92
|
+
* <App />
|
|
93
|
+
* </ContextProvider>
|
|
94
|
+
*/
|
|
61
95
|
export function ContextProvider({ children, constants }) {
|
|
62
96
|
const getStorageKey = () => {
|
|
63
97
|
const appName = constants.appName || 'skateboard';
|
|
@@ -133,6 +167,24 @@ export function ContextProvider({ children, constants }) {
|
|
|
133
167
|
);
|
|
134
168
|
}
|
|
135
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Hook to access skateboard-ui state.
|
|
172
|
+
*
|
|
173
|
+
* Returns { state, dispatch } where state contains:
|
|
174
|
+
* - user: Current user object or null
|
|
175
|
+
* - ui: { sidebarVisible, tabBarVisible }
|
|
176
|
+
*
|
|
177
|
+
* @returns {{ state: Object, dispatch: Function }}
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* import { getState } from '@stevederico/skateboard-ui/Context';
|
|
181
|
+
*
|
|
182
|
+
* function MyComponent() {
|
|
183
|
+
* const { state, dispatch } = getState();
|
|
184
|
+
* console.log(state.user);
|
|
185
|
+
* dispatch({ type: 'SET_USER', payload: userData });
|
|
186
|
+
* }
|
|
187
|
+
*/
|
|
136
188
|
export function getState() {
|
|
137
189
|
return useContext(context);
|
|
138
190
|
}
|
package/ProtectedRoute.jsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react';
|
|
2
2
|
import { Navigate, Outlet } from 'react-router-dom';
|
|
3
|
-
import { isAuthenticated, apiRequest, getAppKey } from './Utilities';
|
|
3
|
+
import { isAuthenticated, apiRequest, getAppKey, getConstants } from './Utilities';
|
|
4
4
|
|
|
5
5
|
const ProtectedRoute = () => {
|
|
6
6
|
const [status, setStatus] = useState('checking'); // 'checking' | 'valid' | 'invalid'
|
|
@@ -11,6 +11,12 @@ const ProtectedRoute = () => {
|
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// Skip backend validation for noLogin apps
|
|
15
|
+
if (getConstants().noLogin === true) {
|
|
16
|
+
setStatus('valid');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
// Validate session with backend
|
|
15
21
|
apiRequest('/me')
|
|
16
22
|
.then(() => setStatus('valid'))
|
package/README.md
CHANGED
|
@@ -25,6 +25,92 @@ createSkateboardApp({ constants, appRoutes });
|
|
|
25
25
|
|
|
26
26
|
That's it! You get routing, auth, layout, landing page, settings, and payments.
|
|
27
27
|
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
skateboard-ui requires a `constants` object that configures your application:
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
// constants.json or constants.js
|
|
34
|
+
const constants = {
|
|
35
|
+
// Required: Backend URLs (include /api prefix)
|
|
36
|
+
devBackendURL: "http://localhost:8000/api",
|
|
37
|
+
backendURL: "https://api.myapp.com/api",
|
|
38
|
+
|
|
39
|
+
// Required: App identity
|
|
40
|
+
appName: "MyApp",
|
|
41
|
+
appIcon: "🛹",
|
|
42
|
+
|
|
43
|
+
// Required: Landing page content
|
|
44
|
+
tagline: "Build apps faster with skateboard-ui",
|
|
45
|
+
cta: "Get Started",
|
|
46
|
+
|
|
47
|
+
// Required: Features section
|
|
48
|
+
features: {
|
|
49
|
+
title: "Everything you need",
|
|
50
|
+
items: [
|
|
51
|
+
{ icon: "Zap", title: "Fast", description: "Built for speed" },
|
|
52
|
+
{ icon: "Shield", title: "Secure", description: "Authentication included" }
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// Required: Company information
|
|
57
|
+
companyName: "Your Company",
|
|
58
|
+
companyWebsite: "https://yourcompany.com",
|
|
59
|
+
companyEmail: "hello@yourcompany.com",
|
|
60
|
+
|
|
61
|
+
// Optional: Authentication
|
|
62
|
+
noLogin: false, // Set true to disable authentication
|
|
63
|
+
|
|
64
|
+
// Optional: Payments (Stripe)
|
|
65
|
+
stripeProducts: [
|
|
66
|
+
{
|
|
67
|
+
name: "Pro Plan",
|
|
68
|
+
priceId: "price_123",
|
|
69
|
+
price: "$10/month",
|
|
70
|
+
lookup_key: "pro_plan"
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
|
|
74
|
+
// Optional: Legal documents
|
|
75
|
+
termsOfService: "Your terms of service...",
|
|
76
|
+
privacyPolicy: "Your privacy policy...",
|
|
77
|
+
|
|
78
|
+
// Optional: UI visibility
|
|
79
|
+
hideSidebar: false,
|
|
80
|
+
hideTabBar: false
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Backend URL Pattern
|
|
85
|
+
|
|
86
|
+
The `devBackendURL` and `backendURL` should include your full API base path (including the `/api` prefix):
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
const constants = {
|
|
90
|
+
devBackendURL: "http://localhost:8000/api", // Include /api prefix
|
|
91
|
+
backendURL: "https://api.myapp.com/api",
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Endpoints are relative to this base URL:
|
|
96
|
+
- `${getBackendURL()}/signup` → `http://localhost:8000/api/signup`
|
|
97
|
+
- `${getBackendURL()}/me` → `http://localhost:8000/api/me`
|
|
98
|
+
- `${getBackendURL()}/deals` → `http://localhost:8000/api/deals`
|
|
99
|
+
|
|
100
|
+
**Tip:** Include API versioning in the base URL (e.g., `/api/v2`) rather than in each endpoint path.
|
|
101
|
+
|
|
102
|
+
### Authentication Setup
|
|
103
|
+
|
|
104
|
+
skateboard-ui uses a hybrid cookie + localStorage authentication system. Your backend must implement specific endpoints and cookie handling.
|
|
105
|
+
|
|
106
|
+
**See [AUTHENTICATION.md](./docs/AUTHENTICATION.md) for complete backend setup requirements.**
|
|
107
|
+
|
|
108
|
+
Quick overview:
|
|
109
|
+
- Session token stored in HttpOnly cookie for security
|
|
110
|
+
- CSRF token in localStorage for request validation
|
|
111
|
+
- Backend validates cookies on protected endpoints
|
|
112
|
+
- Client-side `isAuthenticated()` checks localStorage for fast validation
|
|
113
|
+
|
|
28
114
|
## Components
|
|
29
115
|
|
|
30
116
|
### Core Components
|
|
@@ -253,6 +339,10 @@ import ProtectedRoute from '@stevederico/skateboard-ui/ProtectedRoute';
|
|
|
253
339
|
|
|
254
340
|
Used internally by createSkateboardApp. Redirects to /signin if not authenticated.
|
|
255
341
|
|
|
342
|
+
## Documentation
|
|
343
|
+
|
|
344
|
+
- **[Authentication Guide](./docs/AUTHENTICATION.md)** - Complete guide to the hybrid cookie + localStorage authentication system, including backend requirements, security considerations, and Express.js implementation examples
|
|
345
|
+
|
|
256
346
|
## Dependencies
|
|
257
347
|
|
|
258
348
|
- React 19.1+
|
package/Utilities.js
CHANGED
|
@@ -65,6 +65,27 @@ function safeRemoveItem(key) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Initialize skateboard-ui utilities with app constants.
|
|
70
|
+
*
|
|
71
|
+
* MUST be called before any utility functions are used, including
|
|
72
|
+
* before React components render. Stores constants in both module-level
|
|
73
|
+
* variable and window.__SKATEBOARD_CONSTANTS__ to handle Vite module
|
|
74
|
+
* duplication issues.
|
|
75
|
+
*
|
|
76
|
+
* @param {Object} constants - App configuration object
|
|
77
|
+
* @param {string} constants.appName - Application name
|
|
78
|
+
* @param {string} constants.devBackendURL - Development API base URL (include /api prefix)
|
|
79
|
+
* @param {string} constants.backendURL - Production API base URL (include /api prefix)
|
|
80
|
+
* @throws {Error} If constants is null/undefined
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* initializeUtilities({
|
|
84
|
+
* appName: "MyApp",
|
|
85
|
+
* devBackendURL: "http://localhost:8000/api",
|
|
86
|
+
* backendURL: "https://api.myapp.com/api"
|
|
87
|
+
* });
|
|
88
|
+
*/
|
|
68
89
|
export function initializeUtilities(constants) {
|
|
69
90
|
if (!constants) {
|
|
70
91
|
throw new Error('initializeUtilities called with null/undefined constants');
|
|
@@ -76,7 +97,7 @@ export function initializeUtilities(constants) {
|
|
|
76
97
|
}
|
|
77
98
|
}
|
|
78
99
|
|
|
79
|
-
function getConstants() {
|
|
100
|
+
export function getConstants() {
|
|
80
101
|
// Check window object first (handles module duplication)
|
|
81
102
|
if (typeof window !== 'undefined' && window.__SKATEBOARD_CONSTANTS__) {
|
|
82
103
|
return window.__SKATEBOARD_CONSTANTS__;
|
|
@@ -102,17 +123,66 @@ export function getCookie(name) {
|
|
|
102
123
|
return null;
|
|
103
124
|
}
|
|
104
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Get CSRF token from localStorage.
|
|
128
|
+
*
|
|
129
|
+
* Returns the CSRF token saved during signin/signup. This token
|
|
130
|
+
* should be sent in the X-CSRF-Token header for state-changing requests.
|
|
131
|
+
*
|
|
132
|
+
* @returns {string|null} CSRF token or null if not found
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* const csrfToken = getCSRFToken();
|
|
136
|
+
* fetch('/api/endpoint', {
|
|
137
|
+
* headers: { 'X-CSRF-Token': csrfToken }
|
|
138
|
+
* });
|
|
139
|
+
*/
|
|
105
140
|
export function getCSRFToken() {
|
|
106
141
|
const appName = getConstants().appName || 'skateboard';
|
|
107
142
|
const csrfKey = `${appName.toLowerCase().replace(/\s+/g, '-')}_csrf`;
|
|
108
143
|
return safeGetItem(csrfKey);
|
|
109
144
|
}
|
|
110
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Generate app-specific localStorage key.
|
|
148
|
+
*
|
|
149
|
+
* Creates namespaced keys using app name: `{appName}_{suffix}`
|
|
150
|
+
* App name is normalized (lowercase, hyphens replace spaces)
|
|
151
|
+
*
|
|
152
|
+
* @param {string} suffix - Key suffix (e.g., 'csrf', 'user', 'theme')
|
|
153
|
+
* @returns {string} Namespaced key
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* getAppKey('csrf') // "myapp_csrf"
|
|
157
|
+
* getAppKey('user') // "myapp_user"
|
|
158
|
+
* getAppKey('theme') // "myapp_theme"
|
|
159
|
+
*/
|
|
111
160
|
export function getAppKey(suffix) {
|
|
112
161
|
const appName = getConstants().appName || 'skateboard';
|
|
113
162
|
return `${appName.toLowerCase().replace(/\s+/g, '-')}_${suffix}`;
|
|
114
163
|
}
|
|
115
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Check if user is authenticated.
|
|
167
|
+
*
|
|
168
|
+
* Uses localStorage (NOT cookies) for fast client-side validation.
|
|
169
|
+
* Checks for both CSRF token and user data in localStorage.
|
|
170
|
+
* If constants.noLogin is true, always returns true.
|
|
171
|
+
*
|
|
172
|
+
* Note: This is a client-side check only. ProtectedRoute performs
|
|
173
|
+
* additional server-side validation via /me endpoint.
|
|
174
|
+
*
|
|
175
|
+
* @returns {boolean} True if authenticated or noLogin mode
|
|
176
|
+
*
|
|
177
|
+
* @see {@link ProtectedRoute} for server-side validation
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* if (isAuthenticated()) {
|
|
181
|
+
* // Show authenticated UI
|
|
182
|
+
* } else {
|
|
183
|
+
* // Redirect to signin
|
|
184
|
+
* }
|
|
185
|
+
*/
|
|
116
186
|
export function isAuthenticated() {
|
|
117
187
|
if (getConstants().noLogin === true) {
|
|
118
188
|
return true;
|
|
@@ -122,6 +192,19 @@ export function isAuthenticated() {
|
|
|
122
192
|
return Boolean(safeGetItem(csrfKey)) && Boolean(safeGetItem(userKey));
|
|
123
193
|
}
|
|
124
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Get the backend API base URL based on environment.
|
|
197
|
+
*
|
|
198
|
+
* Returns devBackendURL in development mode, backendURL in production.
|
|
199
|
+
* Endpoints should be concatenated: `${getBackendURL()}/endpoint`
|
|
200
|
+
*
|
|
201
|
+
* @returns {string} Base URL including /api prefix
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* const url = `${getBackendURL()}/signup`;
|
|
205
|
+
* // Dev: "http://localhost:8000/api/signup"
|
|
206
|
+
* // Prod: "https://api.myapp.com/api/signup"
|
|
207
|
+
*/
|
|
125
208
|
export function getBackendURL() {
|
|
126
209
|
let result = import.meta.env.DEV ? getConstants().devBackendURL : getConstants().backendURL;
|
|
127
210
|
return result
|