@commonpub/layer 0.18.2 → 0.18.3
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/composables/useAuth.ts +13 -4
- package/package.json +3 -3
- package/pages/learn/index.vue +43 -0
- package/pages/videos/[id].vue +14 -0
- package/pages/videos/index.vue +19 -0
- package/server/middleware/security.ts +34 -1
package/composables/useAuth.ts
CHANGED
|
@@ -25,7 +25,12 @@ interface AuthResponse {
|
|
|
25
25
|
session: ClientAuthSession | null;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
// `$fetch<T>(url, options)` instantiates Nuxt's NitroFetchRequest generic
|
|
29
|
+
// with an excessively deep type graph, which fails TS2589 on this
|
|
30
|
+
// specific call shape. The cast below narrows `$fetch` to a concrete
|
|
31
|
+
// signature the compiler can check without recursing. Verified session
|
|
32
|
+
// 133: removing the cast immediately reintroduces the error. Cleanup is
|
|
33
|
+
// upstream — wait for Nuxt to simplify the $fetch type.
|
|
29
34
|
async function authPost(url: string, body: Record<string, unknown>): Promise<AuthResponse | null> {
|
|
30
35
|
return ($fetch as (url: string, opts: Record<string, unknown>) => Promise<AuthResponse | null>)(url, {
|
|
31
36
|
method: 'POST',
|
|
@@ -35,6 +40,12 @@ async function authPost(url: string, body: Record<string, unknown>): Promise<Aut
|
|
|
35
40
|
});
|
|
36
41
|
}
|
|
37
42
|
|
|
43
|
+
async function authGet(url: string): Promise<AuthResponse | null> {
|
|
44
|
+
return ($fetch as (url: string, opts: Record<string, unknown>) => Promise<AuthResponse | null>)(url, {
|
|
45
|
+
credentials: 'include',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
export function useAuth() {
|
|
39
50
|
const user = useState<ClientAuthUser | null>('auth-user', () => null);
|
|
40
51
|
const session = useState<ClientAuthSession | null>('auth-session', () => null);
|
|
@@ -68,9 +79,7 @@ export function useAuth() {
|
|
|
68
79
|
async function refreshSession(): Promise<void> {
|
|
69
80
|
if (import.meta.server) return;
|
|
70
81
|
try {
|
|
71
|
-
const data = await (
|
|
72
|
-
'/api/me', { credentials: 'include' },
|
|
73
|
-
);
|
|
82
|
+
const data = await authGet('/api/me');
|
|
74
83
|
user.value = data?.user ?? null;
|
|
75
84
|
session.value = data?.session ?? null;
|
|
76
85
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
"vue-router": "^4.3.0",
|
|
55
55
|
"zod": "^4.3.6",
|
|
56
56
|
"@commonpub/auth": "0.5.1",
|
|
57
|
-
"@commonpub/docs": "0.6.2",
|
|
58
57
|
"@commonpub/config": "0.11.0",
|
|
59
|
-
"@commonpub/
|
|
58
|
+
"@commonpub/docs": "0.6.2",
|
|
60
59
|
"@commonpub/editor": "0.7.9",
|
|
60
|
+
"@commonpub/learning": "0.5.2",
|
|
61
61
|
"@commonpub/ui": "0.8.5",
|
|
62
62
|
"@commonpub/protocol": "0.9.9"
|
|
63
63
|
},
|
package/pages/learn/index.vue
CHANGED
|
@@ -357,4 +357,47 @@ const activeDifficultyFilter = ref('');
|
|
|
357
357
|
.cpub-empty-icon { font-size: 32px; color: var(--text-faint); margin-bottom: 12px; }
|
|
358
358
|
.cpub-empty-title { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
|
|
359
359
|
.cpub-empty-sub { font-size: 12px; color: var(--text-dim); }
|
|
360
|
+
|
|
361
|
+
/* MOBILE (≤ 768px) — stack sidebar, collapse multi-column grids,
|
|
362
|
+
shrink outer padding so content gets the full viewport. */
|
|
363
|
+
@media (max-width: 768px) {
|
|
364
|
+
.cpub-learn-hero { padding: 24px 16px 18px; }
|
|
365
|
+
.cpub-hero-title { font-size: 22px; }
|
|
366
|
+
.cpub-hero-sub { font-size: 13px; margin-bottom: 18px; }
|
|
367
|
+
.cpub-hero-stats { gap: 16px; margin-top: 18px; padding-top: 16px; }
|
|
368
|
+
|
|
369
|
+
.cpub-shell { flex-direction: column; min-height: auto; }
|
|
370
|
+
.cpub-page { padding: 20px 16px; }
|
|
371
|
+
.cpub-sidebar {
|
|
372
|
+
width: 100%;
|
|
373
|
+
border-left: none;
|
|
374
|
+
border-top: var(--border-width-default) solid var(--border);
|
|
375
|
+
padding: 16px 0;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/* In-progress cards: already scroll horizontally on mobile via cpub-ip-row */
|
|
379
|
+
|
|
380
|
+
/* Path cards: stack vertically so description + aside don't crush */
|
|
381
|
+
.cpub-path-card { flex-direction: column; gap: 14px; padding: 16px; }
|
|
382
|
+
.cpub-path-aside {
|
|
383
|
+
flex-direction: row;
|
|
384
|
+
align-items: center;
|
|
385
|
+
justify-content: flex-start;
|
|
386
|
+
width: 100%;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/* My-path rows: put status/meta under title instead of side-by-side */
|
|
390
|
+
.cpub-my-path-row {
|
|
391
|
+
flex-direction: column;
|
|
392
|
+
align-items: flex-start;
|
|
393
|
+
gap: 6px;
|
|
394
|
+
padding: 12px;
|
|
395
|
+
}
|
|
396
|
+
.cpub-my-path-meta { gap: 10px; flex-wrap: wrap; }
|
|
397
|
+
|
|
398
|
+
/* Course + explainer grids (not currently rendered on this page but
|
|
399
|
+
safe to include in case the template adds them back) */
|
|
400
|
+
.cpub-course-grid { grid-template-columns: 1fr; }
|
|
401
|
+
.cpub-explainer-grid { grid-template-columns: 1fr; }
|
|
402
|
+
}
|
|
360
403
|
</style>
|
package/pages/videos/[id].vue
CHANGED
|
@@ -227,4 +227,18 @@ const authorInitial = computed(() => {
|
|
|
227
227
|
.cpub-link:hover {
|
|
228
228
|
text-decoration: underline;
|
|
229
229
|
}
|
|
230
|
+
|
|
231
|
+
/* MOBILE (≤ 768px) — let meta items wrap instead of overflowing, shrink
|
|
232
|
+
title + info padding so content gets the full viewport width. */
|
|
233
|
+
@media (max-width: 768px) {
|
|
234
|
+
.cpub-video-player { margin-bottom: 16px; }
|
|
235
|
+
.cpub-video-info { padding: 16px; }
|
|
236
|
+
.cpub-video-title { font-size: 18px; }
|
|
237
|
+
.cpub-video-meta {
|
|
238
|
+
flex-wrap: wrap;
|
|
239
|
+
gap: 10px;
|
|
240
|
+
row-gap: 6px;
|
|
241
|
+
}
|
|
242
|
+
.cpub-video-desc { font-size: 13px; margin-bottom: 16px; }
|
|
243
|
+
}
|
|
230
244
|
</style>
|
package/pages/videos/index.vue
CHANGED
|
@@ -324,4 +324,23 @@ function formatDate(dateStr: string): string {
|
|
|
324
324
|
.cpub-empty-icon { font-size: 32px; color: var(--text-faint); margin-bottom: 12px; }
|
|
325
325
|
.cpub-empty-title { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
|
|
326
326
|
.cpub-empty-sub { font-size: 12px; color: var(--text-dim); }
|
|
327
|
+
|
|
328
|
+
/* MOBILE (≤ 768px) — collapse 2-track grid to single column, stack
|
|
329
|
+
sidebar under main content, shrink outer padding, and single-col
|
|
330
|
+
the thumbnail grid so cards get full viewport width. */
|
|
331
|
+
@media (max-width: 768px) {
|
|
332
|
+
.cpub-video-hero { padding: 24px 16px 18px; }
|
|
333
|
+
.cpub-hero-row { flex-wrap: wrap; gap: 10px; }
|
|
334
|
+
.cpub-hero-title { font-size: 22px; }
|
|
335
|
+
.cpub-hero-sub { font-size: 12px; margin-bottom: 14px; }
|
|
336
|
+
|
|
337
|
+
.cpub-filter-bar { padding: 0 16px; }
|
|
338
|
+
|
|
339
|
+
.cpub-page-wrap { padding: 20px 16px; }
|
|
340
|
+
.cpub-main-grid { grid-template-columns: 1fr; gap: 20px; }
|
|
341
|
+
|
|
342
|
+
.cpub-video-grid { grid-template-columns: 1fr; gap: 14px; }
|
|
343
|
+
|
|
344
|
+
.cpub-featured-title { font-size: 15px; }
|
|
345
|
+
}
|
|
327
346
|
</style>
|
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
// Security middleware — rate limiting + security headers + CSP
|
|
2
2
|
import { checkRateLimit, createRateLimitStore, createRedisFailOpenLogger, shouldSkipRateLimit, getSecurityHeaders, buildCspHeader, buildCspDirectives } from '@commonpub/server';
|
|
3
3
|
|
|
4
|
+
// Structured JSON sink for fail-open events. Emits one JSON line per event
|
|
5
|
+
// to stdout so Docker logs / Loki / Datadog / CloudWatch can parse without
|
|
6
|
+
// regex-scraping. Duplicated from packages/infra/structuredLogger.ts
|
|
7
|
+
// because layers/base doesn't depend on @commonpub/infra directly and the
|
|
8
|
+
// symbol isn't re-exported via @commonpub/server (which pins to the npm
|
|
9
|
+
// registry, not the workspace, in apps/reference). Keep this helper in
|
|
10
|
+
// sync with the one in infra if the event shape changes.
|
|
11
|
+
function jsonLog(component: string) {
|
|
12
|
+
return (message: string, meta?: Record<string, unknown>) => {
|
|
13
|
+
try {
|
|
14
|
+
const event: Record<string, unknown> = {
|
|
15
|
+
ts: new Date().toISOString(),
|
|
16
|
+
level: 'warn',
|
|
17
|
+
component,
|
|
18
|
+
message,
|
|
19
|
+
};
|
|
20
|
+
if (meta) {
|
|
21
|
+
for (const [k, v] of Object.entries(meta)) {
|
|
22
|
+
if (k === 'ts' || k === 'level' || k === 'component' || k === 'message') continue;
|
|
23
|
+
event[k] = v;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
process.stdout.write(JSON.stringify(event) + '\n');
|
|
27
|
+
} catch {
|
|
28
|
+
// Circular meta; fall through to plain console so the event isn't lost.
|
|
29
|
+
console.warn(`[${component}] ${message}`, meta);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
4
34
|
// Selects a Redis-backed store when NUXT_REDIS_URL is set, otherwise the
|
|
5
35
|
// in-process memory store. Unset env = byte-identical behavior to pre-0.6.
|
|
6
36
|
// `onRedisError` is rate-limited: first event logs immediately, subsequent
|
|
@@ -8,7 +38,10 @@ import { checkRateLimit, createRateLimitStore, createRedisFailOpenLogger, should
|
|
|
8
38
|
// flood the log at real traffic.
|
|
9
39
|
const store = createRateLimitStore({
|
|
10
40
|
redisUrl: process.env.NUXT_REDIS_URL,
|
|
11
|
-
onRedisError: createRedisFailOpenLogger({
|
|
41
|
+
onRedisError: createRedisFailOpenLogger({
|
|
42
|
+
scope: 'ratelimit:ip',
|
|
43
|
+
sink: jsonLog('ratelimit-ip'),
|
|
44
|
+
}),
|
|
12
45
|
});
|
|
13
46
|
const isDev = process.env.NODE_ENV !== 'production';
|
|
14
47
|
|