@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.
- package/components/ContentCard.vue +1 -1
- package/components/ImageUpload.vue +1 -1
- package/components/ShareToHubModal.vue +1 -1
- package/components/blocks/BlockCodeView.vue +26 -25
- package/components/contest/ContestEntries.vue +112 -0
- package/components/contest/ContestHero.vue +204 -0
- package/components/contest/ContestJudges.vue +51 -0
- package/components/contest/ContestPrizes.vue +82 -0
- package/components/contest/ContestRules.vue +34 -0
- package/components/contest/ContestSidebar.vue +83 -0
- package/components/editors/ArticleEditor.vue +19 -1
- package/components/editors/BlogEditor.vue +1 -1
- package/components/editors/DocsPageTree.vue +10 -0
- package/components/hub/HubHero.vue +1 -1
- package/composables/useSanitize.ts +112 -9
- package/layouts/default.vue +7 -7
- package/middleware/feature-gate.global.ts +24 -0
- package/package.json +8 -8
- package/pages/[type]/index.vue +4 -3
- package/pages/admin/audit.vue +3 -2
- package/pages/admin/federation.vue +9 -1
- package/pages/admin/index.vue +7 -1
- package/pages/admin/reports.vue +152 -36
- package/pages/admin/settings.vue +17 -5
- package/pages/admin/theme.vue +5 -3
- package/pages/auth/forgot-password.vue +35 -35
- package/pages/auth/login.vue +6 -5
- package/pages/auth/reset-password.vue +44 -32
- package/pages/contests/[slug]/edit.vue +238 -56
- package/pages/contests/[slug]/index.vue +54 -450
- package/pages/contests/[slug]/judge.vue +141 -53
- package/pages/contests/[slug]/results.vue +182 -0
- package/pages/contests/create.vue +64 -64
- package/pages/contests/index.vue +2 -1
- package/pages/docs/[siteSlug]/[...pagePath].vue +6 -5
- package/pages/docs/[siteSlug]/edit.vue +58 -2
- package/pages/docs/[siteSlug]/index.vue +6 -5
- package/pages/federated-hubs/[id]/posts/[postId].vue +2 -2
- package/pages/hubs/index.vue +3 -2
- package/pages/index.vue +25 -7
- package/pages/learn/index.vue +1 -1
- package/pages/mirror/[id].vue +3 -3
- package/pages/notifications.vue +15 -1
- package/pages/settings/notifications.vue +7 -1
- package/pages/tags/[slug].vue +3 -2
- package/pages/tags/index.vue +3 -2
- package/pages/videos/[id].vue +18 -0
- package/server/api/admin/content/[id].patch.ts +1 -1
- package/server/api/admin/federation/mirrors/[id]/backfill.post.ts +1 -1
- package/server/api/admin/federation/refederate.post.ts +7 -3
- package/server/api/admin/federation/repair-types.post.ts +2 -45
- package/server/api/admin/federation/retry.post.ts +7 -4
- package/server/api/admin/reports.get.ts +1 -0
- package/server/api/auth/sign-in-username.post.ts +42 -0
- package/server/api/content/[id]/products-sync.post.ts +7 -6
- package/server/api/contests/[slug]/entries/[entryId].delete.ts +14 -0
- package/server/api/contests/[slug]/entries.get.ts +6 -1
- package/server/api/contests/[slug]/judge.post.ts +8 -2
- package/server/api/docs/[siteSlug]/nav.get.ts +1 -1
- package/server/api/docs/[siteSlug]/pages/[pageId]/duplicate.post.ts +16 -0
- package/server/api/docs/[siteSlug]/pages/reorder.post.ts +4 -1
- package/server/api/docs/migrate-content.post.ts +1 -7
- package/server/api/federation/hub-follow-status.get.ts +2 -18
- package/server/api/federation/hub-follow.post.ts +9 -27
- package/server/api/federation/hub-post-like.post.ts +9 -98
- package/server/api/federation/hub-post-likes.get.ts +3 -13
- package/server/api/notifications/read.post.ts +6 -1
- package/server/api/search/index.get.ts +2 -2
- package/server/api/search/trending.get.ts +3 -3
- package/server/api/users/index.get.ts +9 -2
- package/server/middleware/content-ap.ts +2 -2
- package/server/routes/.well-known/webfinger.ts +2 -2
- package/theme/base.css +23 -0
- package/components/EditorPropertiesPanel.vue +0 -393
- package/components/views/BlogView.vue +0 -735
- 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
|
|
20
|
-
const activityId = body
|
|
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
|
|
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,
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 {
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 {
|
|
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(
|
|
15
|
-
.orderBy(
|
|
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(
|
|
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,
|
|
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
|
-
?
|
|
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}/
|
|
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}/
|
|
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);
|