@commonpub/layer 0.8.2 → 0.8.4

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.
Files changed (76) hide show
  1. package/components/ContentCard.vue +1 -1
  2. package/components/ImageUpload.vue +1 -1
  3. package/components/ShareToHubModal.vue +1 -1
  4. package/components/blocks/BlockCodeView.vue +26 -25
  5. package/components/contest/ContestEntries.vue +112 -0
  6. package/components/contest/ContestHero.vue +204 -0
  7. package/components/contest/ContestJudges.vue +51 -0
  8. package/components/contest/ContestPrizes.vue +82 -0
  9. package/components/contest/ContestRules.vue +34 -0
  10. package/components/contest/ContestSidebar.vue +83 -0
  11. package/components/editors/ArticleEditor.vue +19 -1
  12. package/components/editors/BlogEditor.vue +1 -1
  13. package/components/editors/DocsPageTree.vue +10 -0
  14. package/components/hub/HubHero.vue +1 -1
  15. package/composables/useSanitize.ts +112 -9
  16. package/layouts/default.vue +7 -7
  17. package/middleware/feature-gate.global.ts +24 -0
  18. package/package.json +8 -8
  19. package/pages/[type]/index.vue +4 -3
  20. package/pages/admin/audit.vue +3 -2
  21. package/pages/admin/federation.vue +9 -1
  22. package/pages/admin/index.vue +7 -1
  23. package/pages/admin/reports.vue +152 -36
  24. package/pages/admin/settings.vue +17 -5
  25. package/pages/admin/theme.vue +5 -3
  26. package/pages/auth/forgot-password.vue +35 -35
  27. package/pages/auth/login.vue +6 -5
  28. package/pages/auth/reset-password.vue +44 -32
  29. package/pages/contests/[slug]/edit.vue +238 -56
  30. package/pages/contests/[slug]/index.vue +54 -450
  31. package/pages/contests/[slug]/judge.vue +141 -53
  32. package/pages/contests/[slug]/results.vue +182 -0
  33. package/pages/contests/create.vue +64 -64
  34. package/pages/contests/index.vue +2 -1
  35. package/pages/docs/[siteSlug]/[...pagePath].vue +6 -5
  36. package/pages/docs/[siteSlug]/edit.vue +58 -2
  37. package/pages/docs/[siteSlug]/index.vue +6 -5
  38. package/pages/federated-hubs/[id]/posts/[postId].vue +2 -2
  39. package/pages/hubs/index.vue +3 -2
  40. package/pages/index.vue +25 -7
  41. package/pages/learn/index.vue +1 -1
  42. package/pages/mirror/[id].vue +3 -3
  43. package/pages/notifications.vue +15 -1
  44. package/pages/settings/notifications.vue +7 -1
  45. package/pages/tags/[slug].vue +3 -2
  46. package/pages/tags/index.vue +3 -2
  47. package/pages/videos/[id].vue +18 -0
  48. package/server/api/admin/content/[id].patch.ts +1 -1
  49. package/server/api/admin/federation/mirrors/[id]/backfill.post.ts +1 -1
  50. package/server/api/admin/federation/refederate.post.ts +7 -3
  51. package/server/api/admin/federation/repair-types.post.ts +2 -45
  52. package/server/api/admin/federation/retry.post.ts +7 -4
  53. package/server/api/admin/reports.get.ts +1 -0
  54. package/server/api/auth/sign-in-username.post.ts +42 -0
  55. package/server/api/content/[id]/products-sync.post.ts +7 -6
  56. package/server/api/contests/[slug]/entries/[entryId].delete.ts +14 -0
  57. package/server/api/contests/[slug]/entries.get.ts +6 -1
  58. package/server/api/contests/[slug]/judge.post.ts +8 -2
  59. package/server/api/docs/[siteSlug]/nav.get.ts +1 -1
  60. package/server/api/docs/[siteSlug]/pages/[pageId]/duplicate.post.ts +16 -0
  61. package/server/api/docs/[siteSlug]/pages/reorder.post.ts +4 -1
  62. package/server/api/docs/migrate-content.post.ts +1 -7
  63. package/server/api/federation/hub-follow-status.get.ts +2 -18
  64. package/server/api/federation/hub-follow.post.ts +9 -27
  65. package/server/api/federation/hub-post-like.post.ts +9 -98
  66. package/server/api/federation/hub-post-likes.get.ts +3 -13
  67. package/server/api/notifications/read.post.ts +6 -1
  68. package/server/api/search/index.get.ts +2 -2
  69. package/server/api/search/trending.get.ts +3 -3
  70. package/server/api/users/index.get.ts +9 -2
  71. package/server/middleware/content-ap.ts +2 -2
  72. package/server/routes/.well-known/webfinger.ts +2 -2
  73. package/theme/base.css +23 -0
  74. package/components/EditorPropertiesPanel.vue +0 -393
  75. package/components/views/BlogView.vue +0 -735
  76. package/server/api/resolve-identity.post.ts +0 -34
@@ -1,12 +1,15 @@
1
1
  import { activities } from '@commonpub/schema';
2
2
  import { eq, and } from 'drizzle-orm';
3
+ import { z } from 'zod';
4
+
5
+ const retrySchema = z.object({
6
+ activityId: z.string().uuid().optional(),
7
+ });
3
8
 
4
9
  /**
5
10
  * POST /api/admin/federation/retry
6
11
  * Reset failed activities to pending so the delivery worker retries them.
7
12
  * Optionally filter by activity ID.
8
- *
9
- * Body: { activityId?: string } — if omitted, retries ALL failed activities
10
13
  */
11
14
  export default defineEventHandler(async (event) => {
12
15
  requireAdmin(event);
@@ -16,8 +19,8 @@ export default defineEventHandler(async (event) => {
16
19
  throw createError({ statusCode: 404, statusMessage: 'Not Found' });
17
20
  }
18
21
 
19
- const body = await readBody(event);
20
- const activityId = body?.activityId as string | undefined;
22
+ const body = await parseBody(event, retrySchema);
23
+ const activityId = body.activityId;
21
24
 
22
25
  const db = useDB();
23
26
 
@@ -3,6 +3,7 @@ import type { PaginatedResponse, ReportListItem } from '@commonpub/server';
3
3
  import { z } from 'zod';
4
4
 
5
5
  const reportsQuerySchema = z.object({
6
+ status: z.enum(['pending', 'reviewed', 'resolved', 'dismissed']).optional(),
6
7
  limit: z.coerce.number().int().positive().max(100).optional(),
7
8
  offset: z.coerce.number().int().min(0).optional(),
8
9
  });
@@ -0,0 +1,42 @@
1
+ import { resolveIdentityToEmail } from '@commonpub/server';
2
+ import { z } from 'zod';
3
+
4
+ const signInSchema = z.object({
5
+ identity: z.string().min(1).max(255),
6
+ password: z.string().min(1).max(256),
7
+ });
8
+
9
+ /**
10
+ * Sign in with username or email + password.
11
+ * Resolves username → email server-side, then proxies to Better Auth's
12
+ * email sign-in endpoint. The email is never exposed to the client.
13
+ */
14
+ export default defineEventHandler(async (event) => {
15
+ const body = await parseBody(event, signInSchema);
16
+
17
+ let email: string;
18
+ try {
19
+ email = await resolveIdentityToEmail(useDB(), body.identity);
20
+ } catch {
21
+ throw createError({ statusCode: 401, statusMessage: 'Invalid credentials' });
22
+ }
23
+
24
+ // Proxy to Better Auth's email sign-in (internal server-side call)
25
+ const origin = getRequestURL(event).origin;
26
+ const response = await $fetch.raw(`${origin}/api/auth/sign-in/email`, {
27
+ method: 'POST',
28
+ body: { email, password: body.password },
29
+ headers: {
30
+ 'Content-Type': 'application/json',
31
+ Cookie: getRequestHeader(event, 'cookie') ?? '',
32
+ },
33
+ });
34
+
35
+ // Forward Set-Cookie headers from Better Auth's response
36
+ const setCookies = response.headers.getSetCookie?.() ?? [];
37
+ for (const cookie of setCookies) {
38
+ appendResponseHeader(event, 'Set-Cookie', cookie);
39
+ }
40
+
41
+ return response._data;
42
+ });
@@ -1,7 +1,12 @@
1
1
  import { syncContentProducts } from '@commonpub/server';
2
2
  import type { ContentProductItem } from '@commonpub/server';
3
3
  import { eq, and } from 'drizzle-orm';
4
- import { contentItems } from '@commonpub/schema';
4
+ import { contentItems, addContentProductSchema } from '@commonpub/schema';
5
+ import { z } from 'zod';
6
+
7
+ const productsSyncSchema = z.object({
8
+ items: z.array(addContentProductSchema),
9
+ });
5
10
 
6
11
  export default defineEventHandler(async (event): Promise<ContentProductItem[]> => {
7
12
  const db = useDB();
@@ -19,11 +24,7 @@ export default defineEventHandler(async (event): Promise<ContentProductItem[]> =
19
24
  throw createError({ statusCode: 403, statusMessage: 'Not authorized to modify this content' });
20
25
  }
21
26
 
22
- const body = await readBody(event);
23
-
24
- if (!Array.isArray(body?.items)) {
25
- throw createError({ statusCode: 400, statusMessage: 'items array is required' });
26
- }
27
+ const body = await parseBody(event, productsSyncSchema);
27
28
 
28
29
  return syncContentProducts(db, id, body.items);
29
30
  });
@@ -0,0 +1,14 @@
1
+ import { withdrawContestEntry } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event): Promise<{ withdrawn: boolean }> => {
4
+ requireFeature('contests');
5
+ const user = requireAuth(event);
6
+ const db = useDB();
7
+ const { entryId } = parseParams(event, { entryId: 'uuid' });
8
+
9
+ const result = await withdrawContestEntry(db, entryId, user.id);
10
+ if (!result.withdrawn) {
11
+ throw createError({ statusCode: 400, message: result.error ?? 'Cannot withdraw entry' });
12
+ }
13
+ return { withdrawn: true };
14
+ });
@@ -5,6 +5,7 @@ import { z } from 'zod';
5
5
  const entriesQuerySchema = z.object({
6
6
  limit: z.coerce.number().int().min(1).max(100).optional(),
7
7
  offset: z.coerce.number().int().min(0).optional(),
8
+ includeJudgeScores: z.coerce.boolean().optional(),
8
9
  });
9
10
 
10
11
  export default defineEventHandler(async (event): Promise<{ items: ContestEntryItem[]; total: number }> => {
@@ -14,5 +15,9 @@ export default defineEventHandler(async (event): Promise<{ items: ContestEntryIt
14
15
  const query = parseQueryParams(event, entriesQuerySchema);
15
16
  const contest = await getContestBySlug(db, slug);
16
17
  if (!contest) throw createError({ statusCode: 404, statusMessage: 'Contest not found' });
17
- return listContestEntries(db, contest.id, query);
18
+ return listContestEntries(db, contest.id, {
19
+ limit: query.limit,
20
+ offset: query.offset,
21
+ includeJudgeScores: query.includeJudgeScores,
22
+ });
18
23
  });
@@ -1,12 +1,18 @@
1
- import { judgeContestEntry } from '@commonpub/server';
1
+ import { judgeContestEntry, getContestBySlug } from '@commonpub/server';
2
2
  import { judgeEntrySchema } from '@commonpub/schema';
3
3
 
4
4
  export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
5
5
  requireFeature('contests');
6
6
  const user = requireAuth(event);
7
7
  const db = useDB();
8
+ const { slug } = parseParams(event, { slug: 'string' });
8
9
  const input = await parseBody(event, judgeEntrySchema);
9
10
 
10
- await judgeContestEntry(db, input.entryId, input.score, user.id);
11
+ const contest = await getContestBySlug(db, slug);
12
+ if (!contest) throw createError({ statusCode: 404, message: 'Contest not found' });
13
+ const judges = (contest.judges ?? []) as string[];
14
+ if (!judges.includes(user.id)) throw createError({ statusCode: 403, message: 'Not a judge for this contest' });
15
+
16
+ await judgeContestEntry(db, input.entryId, input.score, user.id, input.feedback);
11
17
  return { success: true };
12
18
  });
@@ -22,5 +22,5 @@ export default defineEventHandler(async (event) => {
22
22
 
23
23
  // Return pages directly as nav items
24
24
  const pages = await listDocsPages(db, version.id);
25
- return pages.map(p => ({ id: p.id, title: p.title, slug: p.slug, sortOrder: p.sortOrder, parentId: p.parentId }));
25
+ return pages.map(p => ({ id: p.id, title: p.title, sidebarLabel: p.sidebarLabel, slug: p.slug, sortOrder: p.sortOrder, parentId: p.parentId }));
26
26
  });
@@ -0,0 +1,16 @@
1
+ import { duplicateDocsPage } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ const user = requireAuth(event);
5
+ const db = useDB();
6
+ const { pageId } = parseParams(event, { pageId: 'uuid' });
7
+
8
+ try {
9
+ return await duplicateDocsPage(db, pageId, user.id);
10
+ } catch (err: unknown) {
11
+ if (err instanceof Error && err.message === 'Not authorized') {
12
+ throw createError({ statusCode: 404, statusMessage: 'Page not found or not owned by you' });
13
+ }
14
+ throw err;
15
+ }
16
+ });
@@ -3,6 +3,7 @@ import { z } from 'zod';
3
3
 
4
4
  const reorderSchema = z.object({
5
5
  pageIds: z.array(z.string().uuid()),
6
+ version: z.string().max(32).optional(),
6
7
  });
7
8
 
8
9
  export default defineEventHandler(async (event) => {
@@ -14,7 +15,9 @@ export default defineEventHandler(async (event) => {
14
15
  const site = await getDocsSiteBySlug(db, siteSlug);
15
16
  if (!site) throw createError({ statusCode: 404, statusMessage: 'Docs site not found' });
16
17
 
17
- const version = site.versions.find((v) => v.isDefault) ?? site.versions[0];
18
+ const version = body.version
19
+ ? site.versions.find((v) => v.version === body.version)
20
+ : site.versions.find((v) => v.isDefault) ?? site.versions[0];
18
21
  if (!version) throw createError({ statusCode: 404, statusMessage: 'No version found' });
19
22
 
20
23
  const result = await reorderDocsPages(db, version.id, user.id, body.pageIds);
@@ -16,13 +16,7 @@ import { docsPages } from '@commonpub/schema';
16
16
  import { eq } from 'drizzle-orm';
17
17
 
18
18
  export default defineEventHandler(async (event) => {
19
- const user = requireAuth(event);
20
-
21
- // Only allow admins to run migration
22
- if (!user.role || user.role !== 'admin') {
23
- throw createError({ statusCode: 403, statusMessage: 'Admin only' });
24
- }
25
-
19
+ requireAdmin(event);
26
20
  const db = useDB();
27
21
 
28
22
  // Fetch all pages
@@ -1,5 +1,4 @@
1
- import { userFederatedHubFollows } from '@commonpub/schema';
2
- import { eq, and } from 'drizzle-orm';
1
+ import { getFederatedHubFollowStatus } from '@commonpub/server';
3
2
  import { z } from 'zod';
4
3
 
5
4
  const querySchema = z.object({
@@ -18,20 +17,5 @@ export default defineEventHandler(async (event): Promise<{ joined: boolean; stat
18
17
  const db = useDB();
19
18
  const { federatedHubId } = parseQueryParams(event, querySchema);
20
19
 
21
- const [record] = await db
22
- .select({ status: userFederatedHubFollows.status })
23
- .from(userFederatedHubFollows)
24
- .where(
25
- and(
26
- eq(userFederatedHubFollows.userId, user.id),
27
- eq(userFederatedHubFollows.federatedHubId, federatedHubId),
28
- ),
29
- )
30
- .limit(1);
31
-
32
- if (!record) {
33
- return { joined: false, status: null };
34
- }
35
-
36
- return { joined: record.status === 'joined', status: record.status };
20
+ return getFederatedHubFollowStatus(db, federatedHubId, user.id);
37
21
  });
@@ -1,6 +1,4 @@
1
- import { sendHubFollow, getFederatedHub } from '@commonpub/server';
2
- import { userFederatedHubFollows } from '@commonpub/schema';
3
- import { eq, and } from 'drizzle-orm';
1
+ import { joinFederatedHub } from '@commonpub/server';
4
2
  import { z } from 'zod';
5
3
 
6
4
  const schema = z.object({
@@ -15,29 +13,13 @@ export default defineEventHandler(async (event): Promise<{ success: boolean; sta
15
13
  const config = useConfig();
16
14
  const { federatedHubId } = await parseBody(event, schema);
17
15
 
18
- const hub = await getFederatedHub(db, federatedHubId);
19
- if (!hub) {
20
- throw createError({ statusCode: 404, statusMessage: 'Federated hub not found' });
16
+ try {
17
+ const { status } = await joinFederatedHub(db, federatedHubId, user.id, config.instance.domain);
18
+ return { success: true, status };
19
+ } catch (err: unknown) {
20
+ if (err instanceof Error && err.message === 'Federated hub not found') {
21
+ throw createError({ statusCode: 404, statusMessage: 'Federated hub not found' });
22
+ }
23
+ throw err;
21
24
  }
22
-
23
- // Create per-user follow record (upsert)
24
- const userStatus = hub.followStatus === 'accepted' ? 'joined' : 'pending';
25
- await db
26
- .insert(userFederatedHubFollows)
27
- .values({
28
- userId: user.id,
29
- federatedHubId,
30
- status: userStatus,
31
- })
32
- .onConflictDoUpdate({
33
- target: [userFederatedHubFollows.userId, userFederatedHubFollows.federatedHubId],
34
- set: { status: userStatus, joinedAt: new Date() },
35
- });
36
-
37
- // Send instance-level Follow if not already accepted
38
- if (hub.followStatus !== 'accepted') {
39
- await sendHubFollow(db, hub.actorUri, config.instance.domain);
40
- }
41
-
42
- return { success: true, status: userStatus };
43
25
  });
@@ -1,7 +1,4 @@
1
- import { likeFederatedHubPost, unlikeFederatedHubPost } from '@commonpub/server';
2
- import { federatedHubPosts, federatedHubPostLikes, activities, remoteActors } from '@commonpub/schema';
3
- import { eq, and } from 'drizzle-orm';
4
- import { AP_CONTEXT, AP_PUBLIC } from '@commonpub/protocol';
1
+ import { toggleFederatedHubPostLike } from '@commonpub/server';
5
2
  import { z } from 'zod';
6
3
 
7
4
  const schema = z.object({
@@ -15,101 +12,15 @@ export default defineEventHandler(async (event): Promise<{ success: boolean; lik
15
12
  const config = useConfig();
16
13
  const { federatedHubPostId } = await parseBody(event, schema);
17
14
 
18
- // Get the post's objectUri (the remote Note's AP URI) and author
19
- const [post] = await db
20
- .select({
21
- objectUri: federatedHubPosts.objectUri,
22
- actorUri: federatedHubPosts.actorUri,
23
- })
24
- .from(federatedHubPosts)
25
- .where(eq(federatedHubPosts.id, federatedHubPostId))
26
- .limit(1);
27
-
28
- if (!post) {
29
- throw createError({ statusCode: 404, statusMessage: 'Post not found' });
30
- }
31
-
32
- // Check if user already liked this post
33
- const [existing] = await db
34
- .select({ id: federatedHubPostLikes.id })
35
- .from(federatedHubPostLikes)
36
- .where(and(
37
- eq(federatedHubPostLikes.postId, federatedHubPostId),
38
- eq(federatedHubPostLikes.userId, user.id),
39
- ))
40
- .limit(1);
41
-
42
15
  const localActorUri = `https://${config.instance.domain}/users/${user.username}`;
43
16
 
44
- if (existing) {
45
- // Unlike: remove like record, decrement counter, send Undo(Like)
46
- await db.delete(federatedHubPostLikes).where(eq(federatedHubPostLikes.id, existing.id));
47
- await unlikeFederatedHubPost(db, federatedHubPostId);
48
-
49
- // Find the original Like activity to reference in Undo
50
- const [likeAct] = await db
51
- .select({ id: activities.id, payload: activities.payload })
52
- .from(activities)
53
- .where(and(
54
- eq(activities.type, 'Like'),
55
- eq(activities.actorUri, localActorUri),
56
- eq(activities.objectUri, post.objectUri),
57
- eq(activities.direction, 'outbound'),
58
- ))
59
- .limit(1);
60
-
61
- const undoActivity = {
62
- '@context': AP_CONTEXT,
63
- type: 'Undo',
64
- id: `${localActorUri}/undo/${crypto.randomUUID()}`,
65
- actor: localActorUri,
66
- object: likeAct?.payload ?? {
67
- type: 'Like',
68
- actor: localActorUri,
69
- object: post.objectUri,
70
- },
71
- to: [post.actorUri],
72
- cc: [AP_PUBLIC],
73
- };
74
-
75
- await db.insert(activities).values({
76
- type: 'Undo',
77
- actorUri: localActorUri,
78
- objectUri: post.objectUri,
79
- payload: undoActivity,
80
- direction: 'outbound',
81
- status: 'pending',
82
- });
83
-
84
- return { success: true, liked: false };
17
+ try {
18
+ const { liked } = await toggleFederatedHubPostLike(db, federatedHubPostId, user.id, localActorUri);
19
+ return { success: true, liked };
20
+ } catch (err: unknown) {
21
+ if (err instanceof Error && err.message === 'Post not found') {
22
+ throw createError({ statusCode: 404, statusMessage: 'Post not found' });
23
+ }
24
+ throw err;
85
25
  }
86
-
87
- // Like: insert like record, increment counter, send Like
88
- await db.insert(federatedHubPostLikes).values({
89
- postId: federatedHubPostId,
90
- userId: user.id,
91
- }).onConflictDoNothing();
92
-
93
- await likeFederatedHubPost(db, federatedHubPostId);
94
-
95
- const likeActivity = {
96
- '@context': AP_CONTEXT,
97
- type: 'Like',
98
- id: `${localActorUri}/likes/${crypto.randomUUID()}`,
99
- actor: localActorUri,
100
- object: post.objectUri,
101
- to: [post.actorUri],
102
- cc: [AP_PUBLIC],
103
- };
104
-
105
- await db.insert(activities).values({
106
- type: 'Like',
107
- actorUri: localActorUri,
108
- objectUri: post.objectUri,
109
- payload: likeActivity,
110
- direction: 'outbound',
111
- status: 'pending',
112
- });
113
-
114
- return { success: true, liked: true };
115
26
  });
@@ -1,5 +1,4 @@
1
- import { federatedHubPostLikes } from '@commonpub/schema';
2
- import { eq, and, inArray } from 'drizzle-orm';
1
+ import { getLikedFederatedHubPostIds } from '@commonpub/server';
3
2
  import { z } from 'zod';
4
3
 
5
4
  export default defineEventHandler(async (event): Promise<{ likedPostIds: string[] }> => {
@@ -10,15 +9,6 @@ export default defineEventHandler(async (event): Promise<{ likedPostIds: string[
10
9
  const query = getQuery(event);
11
10
  const postIds = z.string().parse(query.postIds ?? '').split(',').filter(Boolean);
12
11
 
13
- if (postIds.length === 0) return { likedPostIds: [] };
14
-
15
- const liked = await db
16
- .select({ postId: federatedHubPostLikes.postId })
17
- .from(federatedHubPostLikes)
18
- .where(and(
19
- eq(federatedHubPostLikes.userId, user.id),
20
- inArray(federatedHubPostLikes.postId, postIds),
21
- ));
22
-
23
- return { likedPostIds: liked.map(l => l.postId) };
12
+ const likedPostIds = await getLikedFederatedHubPostIds(db, user.id, postIds);
13
+ return { likedPostIds };
24
14
  });
@@ -1,9 +1,14 @@
1
1
  import { markNotificationRead, markAllNotificationsRead } from '@commonpub/server';
2
+ import { z } from 'zod';
3
+
4
+ const markReadSchema = z.object({
5
+ notificationId: z.string().uuid().optional(),
6
+ });
2
7
 
3
8
  export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
4
9
  const user = requireAuth(event);
5
10
  const db = useDB();
6
- const body = await readBody(event);
11
+ const body = await parseBody(event, markReadSchema);
7
12
 
8
13
  if (body.notificationId) {
9
14
  await markNotificationRead(db, body.notificationId, user.id);
@@ -1,7 +1,7 @@
1
1
  import { searchContent, listHubs, escapeLike } from '@commonpub/server';
2
2
  import type { ContentSearchOptions, MeiliClient } from '@commonpub/server';
3
3
  import { users, follows, hubs } from '@commonpub/schema';
4
- import { sql, desc, ilike, or, and, isNull, eq } from 'drizzle-orm';
4
+ import { sql, desc, ilike, or, and, isNull, eq, inArray } from 'drizzle-orm';
5
5
  import { z } from 'zod';
6
6
 
7
7
  const searchQuerySchema = z.object({
@@ -82,7 +82,7 @@ export default defineEventHandler(async (event): Promise<{ items: unknown[]; tot
82
82
  const counts = await db
83
83
  .select({ followingId: follows.followingId, count: sql<number>`count(*)::int` })
84
84
  .from(follows)
85
- .where(sql`${follows.followingId} = ANY(ARRAY[${sql.join(userIds.map((id) => sql`${id}::uuid`), sql`, `)}])`)
85
+ .where(inArray(follows.followingId, userIds))
86
86
  .groupBy(follows.followingId);
87
87
  for (const c of counts) followerCounts[c.followingId] = c.count;
88
88
  }
@@ -1,4 +1,4 @@
1
- import { sql } from 'drizzle-orm';
1
+ import { eq, desc } from 'drizzle-orm';
2
2
  import { contentItems } from '@commonpub/schema';
3
3
 
4
4
  export default defineEventHandler(async (): Promise<Array<{ query: string; trend: number }>> => {
@@ -11,8 +11,8 @@ export default defineEventHandler(async (): Promise<Array<{ query: string; trend
11
11
  viewCount: contentItems.viewCount,
12
12
  })
13
13
  .from(contentItems)
14
- .where(sql`${contentItems.status} = 'published'`)
15
- .orderBy(sql`${contentItems.viewCount} DESC`)
14
+ .where(eq(contentItems.status, 'published'))
15
+ .orderBy(desc(contentItems.viewCount))
16
16
  .limit(8);
17
17
 
18
18
  return rows.map((r) => ({
@@ -1,11 +1,12 @@
1
1
  import { users, follows } from '@commonpub/schema';
2
- import { sql, desc, ilike, or, and, isNull } from 'drizzle-orm';
2
+ import { sql, desc, ilike, or, and, isNull, inArray } from 'drizzle-orm';
3
3
  import { z } from 'zod';
4
4
  import { escapeLike } from '@commonpub/server';
5
5
 
6
6
  const usersQuerySchema = z.object({
7
7
  q: z.string().max(200).optional(),
8
8
  search: z.string().max(200).optional(),
9
+ ids: z.string().max(2000).optional(),
9
10
  limit: z.coerce.number().int().positive().max(50).optional(),
10
11
  offset: z.coerce.number().int().min(0).optional(),
11
12
  });
@@ -19,6 +20,12 @@ export default defineEventHandler(async (event) => {
19
20
  const search = query.q || query.search;
20
21
 
21
22
  const conditions = [isNull(users.deletedAt)];
23
+ if (query.ids) {
24
+ const idList = query.ids.split(',').filter(Boolean).slice(0, 50);
25
+ if (idList.length > 0) {
26
+ conditions.push(inArray(users.id, idList));
27
+ }
28
+ }
22
29
  if (search) {
23
30
  const term = `%${escapeLike(search)}%`;
24
31
  conditions.push(or(ilike(users.username, term), ilike(users.displayName, term))!);
@@ -52,7 +59,7 @@ export default defineEventHandler(async (event) => {
52
59
  count: sql<number>`count(*)::int`,
53
60
  })
54
61
  .from(follows)
55
- .where(sql`${follows.followingId} = ANY(ARRAY[${sql.join(userIds.map((id) => sql`${id}::uuid`), sql`, `)}])`)
62
+ .where(inArray(follows.followingId, userIds))
56
63
  .groupBy(follows.followingId);
57
64
 
58
65
  for (const c of counts) {
@@ -1,6 +1,6 @@
1
1
  import { contentToArticle } from '@commonpub/protocol';
2
2
  import { contentItems, users } from '@commonpub/schema';
3
- import { eq, and, isNull, sql } from 'drizzle-orm';
3
+ import { eq, and, isNull, inArray } from 'drizzle-orm';
4
4
 
5
5
  /**
6
6
  * Middleware: serve ActivityPub Article JSON-LD for content URIs.
@@ -35,7 +35,7 @@ export default defineEventHandler(async (event) => {
35
35
 
36
36
  // For blog type, also match 'article' in DB (transition: pre-migration rows still have type='article')
37
37
  const typeFilter = type === 'blog'
38
- ? sql`${contentItems.type} IN ('blog', 'article')`
38
+ ? inArray(contentItems.type, ['blog', 'article'])
39
39
  : eq(contentItems.type, type as 'project' | 'explainer');
40
40
 
41
41
  const [row] = await db
@@ -44,7 +44,7 @@ export default defineEventHandler(async (event) => {
44
44
  username: instanceDomain,
45
45
  domain: instanceDomain,
46
46
  actorUri,
47
- oauthEndpoint: `https://${instanceDomain}/api/auth/oauth2/authorize`,
47
+ oauthEndpoint: `https://${instanceDomain}/auth/oauth/authorize`,
48
48
  });
49
49
  }
50
50
 
@@ -81,6 +81,6 @@ export default defineEventHandler(async (event) => {
81
81
  username: parsed.username,
82
82
  domain: instanceDomain,
83
83
  actorUri,
84
- oauthEndpoint: `https://${instanceDomain}/api/auth/oauth2/authorize`,
84
+ oauthEndpoint: `https://${instanceDomain}/auth/oauth/authorize`,
85
85
  });
86
86
  });
package/theme/base.css CHANGED
@@ -86,6 +86,29 @@
86
86
  --pink-bg: rgba(236, 72, 153, 0.08);
87
87
  --pink-border: rgba(236, 72, 153, 0.25);
88
88
 
89
+ /* Rank colors (contests) */
90
+ --gold: #fbbf24;
91
+ --silver: #94a3b8;
92
+ --bronze: #a0724a;
93
+
94
+ --color-text-inverse: #ffffff;
95
+
96
+ /* Code block tokens (GitHub Dark defaults — overridable by themes) */
97
+ --code-bg: #0d1117;
98
+ --code-text: #e6edf3;
99
+ --code-header-bg: #161b22;
100
+ --code-border: #30363d;
101
+ --code-muted: #8b949e;
102
+ --code-green: #7ee787;
103
+ --hljs-comment: #8b949e;
104
+ --hljs-keyword: #ff7b72;
105
+ --hljs-literal: #79c0ff;
106
+ --hljs-string: #a5d6ff;
107
+ --hljs-deletion: #ffa198;
108
+ --hljs-meta: #d2a8ff;
109
+ --hljs-name: #7ee787;
110
+ --hljs-variable: #ffa657;
111
+
89
112
  --color-success: var(--green);
90
113
  --color-warning: var(--yellow);
91
114
  --color-error: var(--red);