@commonpub/layer 0.15.6 → 0.15.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonpub/layer",
3
- "version": "0.15.6",
3
+ "version": "0.15.8",
4
4
  "type": "module",
5
5
  "main": "./nuxt.config.ts",
6
6
  "files": [
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@aws-sdk/client-s3": "^3.1010.0",
31
- "@commonpub/explainer": "^0.7.11",
31
+ "@commonpub/explainer": "^0.7.12",
32
32
  "@commonpub/schema": "^0.13.0",
33
33
  "@commonpub/server": "^2.43.0",
34
34
  "@tiptap/core": "^2.11.0",
@@ -53,12 +53,12 @@
53
53
  "vue": "^3.4.0",
54
54
  "vue-router": "^4.3.0",
55
55
  "zod": "^4.3.6",
56
+ "@commonpub/config": "0.10.0",
56
57
  "@commonpub/auth": "0.5.1",
57
58
  "@commonpub/editor": "0.7.9",
58
- "@commonpub/config": "0.10.0",
59
- "@commonpub/docs": "0.6.2",
60
59
  "@commonpub/learning": "0.5.0",
61
60
  "@commonpub/protocol": "0.9.9",
61
+ "@commonpub/docs": "0.6.2",
62
62
  "@commonpub/ui": "0.8.5"
63
63
  },
64
64
  "devDependencies": {
@@ -1,10 +1,19 @@
1
1
  <script setup lang="ts">
2
2
  import type { Serialized, ContentListItem, PaginatedResponse } from '@commonpub/server';
3
+ import type { ContentType } from '../../composables/useContentTypes';
3
4
 
4
5
  const route = useRoute();
5
6
  const contentType = computed(() => route.params.type as string);
6
7
  const siteName = useSiteName();
7
8
  const { user } = useAuth();
9
+ const { isTypeEnabled } = useContentTypes();
10
+
11
+ // Hard 404 for any path that isn't an enabled content type — this catch-all
12
+ // route would otherwise match /foo, /@username, /wp-admin, /.env, etc. and
13
+ // render a broken empty listing with the URL segment as the "type".
14
+ if (!isTypeEnabled(contentType.value as ContentType)) {
15
+ throw createError({ statusCode: 404, statusMessage: 'Not Found' });
16
+ }
8
17
 
9
18
  useSeoMeta({
10
19
  title: () => `${contentType.value} — ${siteName}`,
@@ -2,6 +2,11 @@ import { listContent } from '@commonpub/server';
2
2
  import type { PaginatedResponse, ContentListItem } from '@commonpub/server';
3
3
  import { contentFiltersSchema } from '@commonpub/schema';
4
4
 
5
+ // Statuses a non-owner may request. Any other value (draft, scheduled, deleted,
6
+ // etc.) is coerced to 'published' — the old behavior passed the filter through
7
+ // verbatim, so /api/content?status=draft leaked every user's drafts.
8
+ const PUBLIC_STATUSES = new Set(['published', 'archived']);
9
+
5
10
  export default defineEventHandler(async (event): Promise<PaginatedResponse<ContentListItem>> => {
6
11
  const db = useDB();
7
12
  const user = getOptionalUser(event);
@@ -11,9 +16,13 @@ export default defineEventHandler(async (event): Promise<PaginatedResponse<Conte
11
16
 
12
17
  const config = useConfig();
13
18
 
19
+ const resolvedStatus = isOwnContent
20
+ ? filters.status
21
+ : (filters.status && PUBLIC_STATUSES.has(filters.status) ? filters.status : 'published');
22
+
14
23
  return listContent(db, {
15
24
  ...filters,
16
- status: isOwnContent ? filters.status : (filters.status ?? 'published'),
25
+ status: resolvedStatus,
17
26
  // Only show public content unless viewing own content
18
27
  visibility: isOwnContent ? filters.visibility : 'public',
19
28
  }, {
@@ -2,6 +2,10 @@ import { listPaths } from '@commonpub/server';
2
2
  import type { PaginatedResponse, LearningPathListItem } from '@commonpub/server';
3
3
  import { learningPathFiltersSchema } from '@commonpub/schema';
4
4
 
5
+ // Statuses a non-owner may request. The old behavior passed filters.status
6
+ // through verbatim, so /api/learn?status=draft leaked every author's drafts.
7
+ const PUBLIC_STATUSES = new Set(['published', 'archived']);
8
+
5
9
  export default defineEventHandler(async (event): Promise<PaginatedResponse<LearningPathListItem>> => {
6
10
  const db = useDB();
7
11
  const user = getOptionalUser(event);
@@ -10,8 +14,12 @@ export default defineEventHandler(async (event): Promise<PaginatedResponse<Learn
10
14
  // Allow author to see their own drafts (same pattern as content API)
11
15
  const isOwnContent = filters.authorId && user?.id === filters.authorId;
12
16
 
17
+ const resolvedStatus = isOwnContent
18
+ ? filters.status
19
+ : (filters.status && PUBLIC_STATUSES.has(filters.status) ? filters.status : 'published');
20
+
13
21
  return listPaths(db, {
14
22
  ...filters,
15
- status: isOwnContent ? filters.status : (filters.status ?? 'published'),
23
+ status: resolvedStatus,
16
24
  });
17
25
  });
@@ -0,0 +1,45 @@
1
+ import { users, hubs } from '@commonpub/schema';
2
+ import { eq, and, isNull } from 'drizzle-orm';
3
+
4
+ /**
5
+ * Redirect /@handle → canonical profile URL.
6
+ *
7
+ * WebFinger advertises `/@username` as the `profile-page` (Mastodon/Fediverse
8
+ * convention) — without this middleware, that URL falls through to the
9
+ * `[type]/index.vue` catchall and renders a broken content listing for
10
+ * "@usernames". Federated users clicking a CommonPub member's profile from
11
+ * Mastodon end up on a broken page.
12
+ *
13
+ * Matches /@{handle} exactly (no sub-paths). Looks up users first, then hubs
14
+ * (both are advertised via WebFinger). Misses 404.
15
+ */
16
+ export default defineEventHandler(async (event) => {
17
+ const path = getRequestURL(event).pathname;
18
+ const match = path.match(/^\/@([a-zA-Z0-9._-]+)$/);
19
+ if (!match) return;
20
+
21
+ const handle = match[1]!;
22
+ const db = useDB();
23
+
24
+ const [user] = await db
25
+ .select({ username: users.username })
26
+ .from(users)
27
+ .where(and(eq(users.username, handle), isNull(users.deletedAt)))
28
+ .limit(1);
29
+
30
+ if (user) {
31
+ return sendRedirect(event, `/u/${user.username}`, 301);
32
+ }
33
+
34
+ const [hub] = await db
35
+ .select({ slug: hubs.slug })
36
+ .from(hubs)
37
+ .where(and(eq(hubs.slug, handle), isNull(hubs.deletedAt)))
38
+ .limit(1);
39
+
40
+ if (hub) {
41
+ return sendRedirect(event, `/hubs/${hub.slug}`, 301);
42
+ }
43
+
44
+ throw createError({ statusCode: 404, statusMessage: 'Not Found' });
45
+ });