@stevederico/skateboard-ui 1.2.17 → 1.2.18

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 CHANGED
@@ -1,4 +1,11 @@
1
1
  # CHANGELOG
2
+ 1.2.18
3
+
4
+ Add authentication documentation
5
+ Add JSDoc comments
6
+ Update README configuration
7
+ Create docs folder
8
+
2
9
  1.2.17
3
10
 
4
11
  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/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');
@@ -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
@@ -0,0 +1,533 @@
1
+ # Authentication Guide
2
+
3
+ ## Architecture Overview
4
+
5
+ skateboard-ui uses a **hybrid cookie + localStorage authentication system** that combines security with performance:
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────────────────────────┐
9
+ │ Authentication Flow │
10
+ └─────────────────────────────────────────────────────────────────┘
11
+
12
+ Frontend Backend Storage
13
+ ──────── ─────── ───────
14
+
15
+ 1. POST /signin → Validate credentials
16
+ credentials
17
+ ← Set-Cookie: {appName}_token (HttpOnly)
18
+ ← Set-Cookie: csrf_token
19
+ ← Response: { csrfToken, ...user }
20
+
21
+ 2. Extract tokens → localStorage:
22
+ - CSRF from cookie {appName}_csrf
23
+ - User from response {appName}_user
24
+
25
+ 3. isAuthenticated() → Check localStorage
26
+ (client-side) (fast, no network)
27
+
28
+ 4. ProtectedRoute → GET /me
29
+ (server validation) Validate cookies
30
+ ← 200 OK or 401 Unauthorized
31
+
32
+ 5. API requests → Protected endpoints
33
+ + cookies (automatic) Validate {appName}_token
34
+ + X-CSRF-Token header Validate CSRF header
35
+ ```
36
+
37
+ ## How It Works
38
+
39
+ ### Cookie-Based Session Management
40
+ - **Session token** stored in `{appName}_token` cookie (HttpOnly, Secure, SameSite=Strict)
41
+ - Automatically sent with every request via browser
42
+ - Cannot be accessed by JavaScript (XSS protection)
43
+ - Backend validates cookie on each protected endpoint
44
+
45
+ ### localStorage for Client-Side Validation
46
+ - **CSRF token** and **user data** stored in localStorage
47
+ - Enables instant `isAuthenticated()` checks without network calls
48
+ - Used by client-side routing logic (ProtectedRoute initial check)
49
+ - Not used for actual authentication (cookies handle that)
50
+
51
+ ### CSRF Protection
52
+ - Dual-token system prevents CSRF attacks
53
+ - **CSRF token** sent in `X-CSRF-Token` header with state-changing requests
54
+ - Backend validates header matches stored session CSRF token
55
+ - Separate from session cookie to prevent cookie-based CSRF
56
+
57
+ ## Backend Requirements
58
+
59
+ ### Required Endpoints
60
+
61
+ #### POST /signup
62
+ Create new user account.
63
+
64
+ **Request:**
65
+ ```json
66
+ {
67
+ "email": "user@example.com",
68
+ "password": "securePassword123"
69
+ }
70
+ ```
71
+
72
+ **Response:**
73
+ - Status: 201 Created
74
+ - Headers:
75
+ - `Set-Cookie: {appName}_token={sessionToken}; HttpOnly; Secure; SameSite=Strict; Path=/`
76
+ - `Set-Cookie: csrf_token={csrfToken}; Secure; SameSite=Lax; Path=/`
77
+ - Body:
78
+ ```json
79
+ {
80
+ "csrfToken": "csrf_abc123...",
81
+ "user": {
82
+ "id": "user123",
83
+ "email": "user@example.com",
84
+ "name": "John Doe"
85
+ }
86
+ }
87
+ ```
88
+
89
+ #### POST /signin
90
+ Authenticate existing user.
91
+
92
+ **Request:**
93
+ ```json
94
+ {
95
+ "email": "user@example.com",
96
+ "password": "securePassword123"
97
+ }
98
+ ```
99
+
100
+ **Response:**
101
+ - Status: 200 OK
102
+ - Headers: Same as /signup
103
+ - Body: Same as /signup
104
+
105
+ #### GET /me
106
+ Validate current session and return user data.
107
+
108
+ **Request:**
109
+ - Headers: Cookies automatically sent by browser
110
+
111
+ **Response (authenticated):**
112
+ - Status: 200 OK
113
+ - Body:
114
+ ```json
115
+ {
116
+ "user": {
117
+ "id": "user123",
118
+ "email": "user@example.com",
119
+ "name": "John Doe"
120
+ }
121
+ }
122
+ ```
123
+
124
+ **Response (not authenticated):**
125
+ - Status: 401 Unauthorized
126
+
127
+ #### POST /signout
128
+ End current session.
129
+
130
+ **Request:**
131
+ - Headers:
132
+ - Cookies automatically sent
133
+ - `X-CSRF-Token: {csrfToken}`
134
+
135
+ **Response:**
136
+ - Status: 200 OK
137
+ - Headers:
138
+ - `Set-Cookie: {appName}_token=; Max-Age=0; Path=/` (clear cookie)
139
+ - `Set-Cookie: csrf_token=; Max-Age=0; Path=/` (clear cookie)
140
+
141
+ ### Cookie Configuration
142
+
143
+ **Session Token Cookie:**
144
+ ```javascript
145
+ {
146
+ name: '{appName}_token',
147
+ httpOnly: true, // Prevents JavaScript access (XSS protection)
148
+ secure: true, // HTTPS only (production)
149
+ sameSite: 'Strict', // Strongest CSRF protection
150
+ path: '/',
151
+ maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days (configurable)
152
+ }
153
+ ```
154
+
155
+ **CSRF Token Cookie:**
156
+ ```javascript
157
+ {
158
+ name: 'csrf_token',
159
+ httpOnly: false, // Must be readable by JavaScript
160
+ secure: true, // HTTPS only (production)
161
+ sameSite: 'Lax', // Allow top-level navigation
162
+ path: '/',
163
+ maxAge: 7 * 24 * 60 * 60 * 1000 // Match session token
164
+ }
165
+ ```
166
+
167
+ ### Protected Endpoints
168
+ All authenticated endpoints must:
169
+ 1. Validate `{appName}_token` cookie exists and is valid
170
+ 2. For state-changing operations (POST, PUT, DELETE), validate `X-CSRF-Token` header
171
+ 3. Return 401 if authentication fails
172
+ 4. Return 403 if CSRF validation fails
173
+
174
+ ## Frontend Flow
175
+
176
+ ### 1. User Signs In
177
+ ```javascript
178
+ // SignInView.jsx
179
+ const response = await fetch(`${getBackendURL()}/signin`, {
180
+ method: 'POST',
181
+ credentials: 'include', // Send and receive cookies
182
+ headers: { 'Content-Type': 'application/json' },
183
+ body: JSON.stringify({ email, password })
184
+ });
185
+
186
+ const data = await response.json();
187
+ // Backend has set cookies automatically
188
+ ```
189
+
190
+ ### 2. Store CSRF Token and User Data
191
+ ```javascript
192
+ // Extract CSRF token from cookie or response body
193
+ const csrfToken = getCookie('csrf_token') || data.csrfToken;
194
+
195
+ // Save to localStorage with app-specific keys
196
+ localStorage.setItem(`${appName}_csrf`, csrfToken);
197
+ localStorage.setItem(`${appName}_user`, JSON.stringify(data.user));
198
+ ```
199
+
200
+ ### 3. Client-Side Authentication Check
201
+ ```javascript
202
+ // Utilities.js:isAuthenticated()
203
+ export function isAuthenticated() {
204
+ const csrf = localStorage.getItem(getAppKey('csrf'));
205
+ const user = localStorage.getItem(getAppKey('user'));
206
+ return !!(csrf && user);
207
+ }
208
+
209
+ // Usage in components
210
+ if (isAuthenticated()) {
211
+ // Show authenticated UI
212
+ }
213
+ ```
214
+
215
+ ### 4. Server-Side Validation
216
+ ```javascript
217
+ // ProtectedRoute.jsx
218
+ useEffect(() => {
219
+ fetch(`${getBackendURL()}/me`, {
220
+ credentials: 'include' // Sends session cookie
221
+ })
222
+ .then(response => {
223
+ if (response.status === 401) {
224
+ // Not authenticated - redirect to signin
225
+ navigate('/signin');
226
+ }
227
+ });
228
+ }, []);
229
+ ```
230
+
231
+ ### 5. Making Authenticated Requests
232
+ ```javascript
233
+ // All API requests include cookies and CSRF token
234
+ const response = await fetch(`${getBackendURL()}/protected-endpoint`, {
235
+ method: 'POST',
236
+ credentials: 'include', // Automatically includes cookies
237
+ headers: {
238
+ 'Content-Type': 'application/json',
239
+ 'X-CSRF-Token': getCSRFToken() // From localStorage
240
+ },
241
+ body: JSON.stringify(data)
242
+ });
243
+ ```
244
+
245
+ ## Security Considerations
246
+
247
+ ### XSS Protection
248
+ - **Session token is HttpOnly** - JavaScript cannot access it
249
+ - Even if attacker injects malicious script, they cannot steal session token
250
+ - CSRF token in localStorage is less sensitive (validated server-side)
251
+
252
+ ### CSRF Protection
253
+ - **Dual-token pattern** prevents cookie-based CSRF attacks
254
+ - Attacker cannot forge `X-CSRF-Token` header from external site
255
+ - Cookie's SameSite attribute provides additional protection
256
+
257
+ ### SameSite Cookie Policy
258
+ - **Session token (Strict)** - Never sent on cross-site requests
259
+ - **CSRF token (Lax)** - Sent on top-level navigation (allows direct links)
260
+ - Provides strong CSRF protection at browser level
261
+
262
+ ### LocalStorage Trade-offs
263
+ - **Vulnerable to XSS** - If site has XSS vulnerability, localStorage can be read
264
+ - **Acceptable for CSRF token** - CSRF token alone cannot authenticate requests
265
+ - **Never store session token in localStorage** - Always use HttpOnly cookies
266
+
267
+ ### HTTPS Requirement
268
+ - All cookies marked `Secure` - only sent over HTTPS in production
269
+ - Development mode (HTTP) may need `Secure: false` based on environment
270
+
271
+ ## Example Backend Implementation (Express.js)
272
+
273
+ ```javascript
274
+ import express from 'express';
275
+ import cookieParser from 'cookie-parser';
276
+ import crypto from 'crypto';
277
+
278
+ const app = express();
279
+ app.use(express.json());
280
+ app.use(cookieParser());
281
+
282
+ // In-memory session store (use Redis in production)
283
+ const sessions = new Map();
284
+
285
+ // Generate secure random token
286
+ function generateToken() {
287
+ return crypto.randomBytes(32).toString('hex');
288
+ }
289
+
290
+ // Middleware: Validate session token
291
+ function requireAuth(req, res, next) {
292
+ const sessionToken = req.cookies.myapp_token;
293
+ const session = sessions.get(sessionToken);
294
+
295
+ if (!session) {
296
+ return res.status(401).json({ error: 'Not authenticated' });
297
+ }
298
+
299
+ req.session = session;
300
+ next();
301
+ }
302
+
303
+ // Middleware: Validate CSRF token
304
+ function requireCSRF(req, res, next) {
305
+ const csrfToken = req.headers['x-csrf-token'];
306
+
307
+ if (!req.session || req.session.csrfToken !== csrfToken) {
308
+ return res.status(403).json({ error: 'Invalid CSRF token' });
309
+ }
310
+
311
+ next();
312
+ }
313
+
314
+ // POST /api/signup
315
+ app.post('/api/signup', async (req, res) => {
316
+ const { email, password } = req.body;
317
+
318
+ // Validate input
319
+ if (!email || !password) {
320
+ return res.status(400).json({ error: 'Email and password required' });
321
+ }
322
+
323
+ // Create user (pseudo-code)
324
+ const user = await createUser(email, password);
325
+
326
+ // Create session
327
+ const sessionToken = generateToken();
328
+ const csrfToken = generateToken();
329
+
330
+ sessions.set(sessionToken, {
331
+ userId: user.id,
332
+ csrfToken,
333
+ createdAt: Date.now()
334
+ });
335
+
336
+ // Set cookies
337
+ res.cookie('myapp_token', sessionToken, {
338
+ httpOnly: true,
339
+ secure: process.env.NODE_ENV === 'production',
340
+ sameSite: 'strict',
341
+ maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
342
+ });
343
+
344
+ res.cookie('csrf_token', csrfToken, {
345
+ httpOnly: false, // Readable by JavaScript
346
+ secure: process.env.NODE_ENV === 'production',
347
+ sameSite: 'lax',
348
+ maxAge: 7 * 24 * 60 * 60 * 1000
349
+ });
350
+
351
+ // Return user data and CSRF token
352
+ res.status(201).json({
353
+ csrfToken,
354
+ user: {
355
+ id: user.id,
356
+ email: user.email,
357
+ name: user.name
358
+ }
359
+ });
360
+ });
361
+
362
+ // POST /api/signin
363
+ app.post('/api/signin', async (req, res) => {
364
+ const { email, password } = req.body;
365
+
366
+ // Validate credentials (pseudo-code)
367
+ const user = await validateCredentials(email, password);
368
+
369
+ if (!user) {
370
+ return res.status(401).json({ error: 'Invalid credentials' });
371
+ }
372
+
373
+ // Create session (same as signup)
374
+ const sessionToken = generateToken();
375
+ const csrfToken = generateToken();
376
+
377
+ sessions.set(sessionToken, {
378
+ userId: user.id,
379
+ csrfToken,
380
+ createdAt: Date.now()
381
+ });
382
+
383
+ // Set cookies (same as signup)
384
+ res.cookie('myapp_token', sessionToken, {
385
+ httpOnly: true,
386
+ secure: process.env.NODE_ENV === 'production',
387
+ sameSite: 'strict',
388
+ maxAge: 7 * 24 * 60 * 60 * 1000
389
+ });
390
+
391
+ res.cookie('csrf_token', csrfToken, {
392
+ httpOnly: false,
393
+ secure: process.env.NODE_ENV === 'production',
394
+ sameSite: 'lax',
395
+ maxAge: 7 * 24 * 60 * 60 * 1000
396
+ });
397
+
398
+ res.json({
399
+ csrfToken,
400
+ user: {
401
+ id: user.id,
402
+ email: user.email,
403
+ name: user.name
404
+ }
405
+ });
406
+ });
407
+
408
+ // GET /api/me
409
+ app.get('/api/me', requireAuth, async (req, res) => {
410
+ // Get user from session
411
+ const user = await getUserById(req.session.userId);
412
+
413
+ res.json({
414
+ user: {
415
+ id: user.id,
416
+ email: user.email,
417
+ name: user.name
418
+ }
419
+ });
420
+ });
421
+
422
+ // POST /api/signout
423
+ app.post('/api/signout', requireAuth, requireCSRF, (req, res) => {
424
+ const sessionToken = req.cookies.myapp_token;
425
+
426
+ // Delete session
427
+ sessions.delete(sessionToken);
428
+
429
+ // Clear cookies
430
+ res.clearCookie('myapp_token');
431
+ res.clearCookie('csrf_token');
432
+
433
+ res.json({ message: 'Signed out successfully' });
434
+ });
435
+
436
+ // Protected endpoint example
437
+ app.post('/api/protected-action', requireAuth, requireCSRF, (req, res) => {
438
+ // Handle protected action
439
+ res.json({ message: 'Action completed' });
440
+ });
441
+
442
+ app.listen(8000, () => {
443
+ console.log('Server running on http://localhost:8000');
444
+ });
445
+ ```
446
+
447
+ ## Troubleshooting
448
+
449
+ ### "Not authenticated" after signing in
450
+
451
+ **Symptoms:** User signs in successfully but immediately redirected to signin page.
452
+
453
+ **Causes:**
454
+ 1. Cookies not being sent with requests
455
+ 2. Cookie domain/path mismatch
456
+ 3. CORS blocking cookies
457
+
458
+ **Solutions:**
459
+ - Verify `credentials: 'include'` in all fetch calls
460
+ - Check cookie `domain` and `path` settings
461
+ - Ensure CORS allows credentials: `Access-Control-Allow-Credentials: true`
462
+ - Verify `Access-Control-Allow-Origin` is specific origin, not `*`
463
+
464
+ ### CSRF validation failing
465
+
466
+ **Symptoms:** Protected endpoints return 403 Forbidden.
467
+
468
+ **Causes:**
469
+ 1. CSRF token not in localStorage
470
+ 2. Header not being sent
471
+ 3. Token mismatch
472
+
473
+ **Solutions:**
474
+ - Check `localStorage.getItem('{appName}_csrf')` exists
475
+ - Verify `X-CSRF-Token` header is set
476
+ - Ensure CSRF cookie and localStorage token match
477
+ - Check CSRF token not expired (matches session lifetime)
478
+
479
+ ### Cookies not persisting across requests
480
+
481
+ **Symptoms:** User authenticated on signin but /me returns 401.
482
+
483
+ **Causes:**
484
+ 1. SameSite policy blocking cookies
485
+ 2. Secure flag on HTTP (development)
486
+ 3. Domain mismatch
487
+
488
+ **Solutions:**
489
+ - Development: Set `secure: false` when `NODE_ENV !== 'production'`
490
+ - Check frontend and backend on same domain (or use proxy)
491
+ - Verify SameSite policy allows your use case
492
+
493
+ ### LocalStorage cleared unexpectedly
494
+
495
+ **Symptoms:** `isAuthenticated()` returns false but session cookie exists.
496
+
497
+ **Causes:**
498
+ 1. User cleared browser data
499
+ 2. localStorage quota exceeded
500
+ 3. Private browsing mode
501
+
502
+ **Solutions:**
503
+ - Re-fetch user data from `/me` endpoint
504
+ - Implement session restoration from cookie
505
+ - Handle `isAuthenticated()` false as trigger to validate with backend
506
+
507
+ ### Session token stolen (security incident)
508
+
509
+ **Symptoms:** User receives "already logged in" errors or unauthorized actions.
510
+
511
+ **Response:**
512
+ 1. Revoke all sessions for affected user
513
+ 2. Force password reset
514
+ 3. Audit recent account activity
515
+ 4. Investigate XSS vulnerabilities if token was HttpOnly
516
+ 5. Check for MITM attack if token was Secure
517
+
518
+ ## No-Login Mode
519
+
520
+ For development or public-only apps, disable authentication:
521
+
522
+ ```javascript
523
+ const constants = {
524
+ noLogin: true,
525
+ // ... other constants
526
+ };
527
+ ```
528
+
529
+ Effects:
530
+ - `isAuthenticated()` always returns `true`
531
+ - ProtectedRoute allows all access
532
+ - Signin/signup views still render but aren't enforced
533
+ - No authentication state management
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stevederico/skateboard-ui",
3
3
  "private": false,
4
- "version": "1.2.17",
4
+ "version": "1.2.18",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  "./AppSidebar": {