@commonpub/layer 0.3.13 → 0.3.14
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/ContentAttachments.vue +16 -3
- package/package.json +4 -4
- package/pages/hubs/[slug]/posts/[postId].vue +5 -3
- package/pages/messages/[conversationId].vue +6 -6
- package/pages/mirror/[id].vue +2 -1
- package/server/api/federation/dm.post.ts +6 -2
- package/server/api/hubs/[slug]/posts/[postId]/like.post.ts +5 -1
- package/server/api/hubs/[slug]/posts/[postId].put.ts +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
defineProps<{
|
|
2
|
+
const { attachments } = defineProps<{
|
|
3
3
|
attachments: Array<{ type: string; url: string; name?: string }>;
|
|
4
4
|
}>();
|
|
5
5
|
|
|
@@ -10,6 +10,15 @@ function iconForType(type: string): string {
|
|
|
10
10
|
return 'fa-solid fa-file';
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function isSafeUrl(url: string): boolean {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = new URL(url);
|
|
16
|
+
return parsed.protocol === 'https:' || parsed.protocol === 'http:';
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
function fileName(att: { url: string; name?: string }): string {
|
|
14
23
|
if (att.name) return att.name;
|
|
15
24
|
try {
|
|
@@ -18,14 +27,18 @@ function fileName(att: { url: string; name?: string }): string {
|
|
|
18
27
|
return 'attachment';
|
|
19
28
|
}
|
|
20
29
|
}
|
|
30
|
+
|
|
31
|
+
const safeAttachments = computed(() =>
|
|
32
|
+
attachments.filter((att) => att.url && att.type && isSafeUrl(att.url)),
|
|
33
|
+
);
|
|
21
34
|
</script>
|
|
22
35
|
|
|
23
36
|
<template>
|
|
24
|
-
<div v-if="
|
|
37
|
+
<div v-if="safeAttachments.length > 0" class="cpub-attachments">
|
|
25
38
|
<div class="cpub-attachments-label">Attachments</div>
|
|
26
39
|
<div class="cpub-attachments-list">
|
|
27
40
|
<a
|
|
28
|
-
v-for="(att, i) in
|
|
41
|
+
v-for="(att, i) in safeAttachments"
|
|
29
42
|
:key="i"
|
|
30
43
|
:href="att.url"
|
|
31
44
|
target="_blank"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -44,14 +44,14 @@
|
|
|
44
44
|
"vue": "^3.4.0",
|
|
45
45
|
"vue-router": "^4.3.0",
|
|
46
46
|
"zod": "^4.3.6",
|
|
47
|
-
"@commonpub/auth": "0.5.0",
|
|
48
|
-
"@commonpub/config": "0.7.0",
|
|
49
47
|
"@commonpub/docs": "0.5.2",
|
|
50
48
|
"@commonpub/editor": "0.5.0",
|
|
51
49
|
"@commonpub/learning": "0.5.0",
|
|
52
50
|
"@commonpub/protocol": "0.9.4",
|
|
51
|
+
"@commonpub/server": "2.11.1",
|
|
53
52
|
"@commonpub/schema": "0.8.11",
|
|
54
|
-
"@commonpub/
|
|
53
|
+
"@commonpub/auth": "0.5.0",
|
|
54
|
+
"@commonpub/config": "0.7.0",
|
|
55
55
|
"@commonpub/ui": "0.7.1"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {}
|
|
@@ -62,20 +62,22 @@ async function toggleLike(): Promise<void> {
|
|
|
62
62
|
|
|
63
63
|
// Mod actions
|
|
64
64
|
async function togglePin(): Promise<void> {
|
|
65
|
+
const wasPinned = post.value?.isPinned;
|
|
65
66
|
try {
|
|
66
67
|
await $fetch(`/api/hubs/${slug.value}/posts/${postId.value}/pin`, { method: 'POST' });
|
|
67
68
|
await refreshPost();
|
|
68
|
-
toast.success(
|
|
69
|
+
toast.success(wasPinned ? 'Post unpinned' : 'Post pinned');
|
|
69
70
|
} catch {
|
|
70
71
|
toast.error('Failed to toggle pin');
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
async function toggleLock(): Promise<void> {
|
|
76
|
+
const wasLocked = post.value?.isLocked;
|
|
75
77
|
try {
|
|
76
78
|
await $fetch(`/api/hubs/${slug.value}/posts/${postId.value}/lock`, { method: 'POST' });
|
|
77
79
|
await refreshPost();
|
|
78
|
-
toast.success(
|
|
80
|
+
toast.success(wasLocked ? 'Post unlocked' : 'Post locked');
|
|
79
81
|
} catch {
|
|
80
82
|
toast.error('Failed to toggle lock');
|
|
81
83
|
}
|
|
@@ -352,7 +354,7 @@ useSeoMeta({
|
|
|
352
354
|
display: flex; gap: 6px;
|
|
353
355
|
}
|
|
354
356
|
|
|
355
|
-
.cpub-btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
357
|
+
.cpub-btn-primary { background: var(--accent); color: var(--accent-text, #fff); border-color: var(--accent); }
|
|
356
358
|
.cpub-btn-primary:hover:not(:disabled) { opacity: 0.9; }
|
|
357
359
|
.cpub-btn-primary:disabled { opacity: 0.5; cursor: default; }
|
|
358
360
|
.cpub-btn-danger { color: var(--red); border-color: var(--red); }
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
const route = useRoute();
|
|
3
3
|
const conversationId = route.params.conversationId as string;
|
|
4
4
|
|
|
5
|
-
useSeoMeta({ title: () => `Message — ${participantLabel.value}` });
|
|
6
5
|
definePageMeta({ middleware: 'auth' });
|
|
7
6
|
|
|
8
7
|
const { user } = useAuth();
|
|
@@ -57,17 +56,18 @@ const participantLabel = computed(() => {
|
|
|
57
56
|
return others.length > 0 ? others.join(', ') : 'Conversation';
|
|
58
57
|
});
|
|
59
58
|
|
|
59
|
+
useSeoMeta({ title: () => `Message — ${participantLabel.value}` });
|
|
60
|
+
|
|
60
61
|
async function handleSend(text: string): Promise<void> {
|
|
61
62
|
await $fetch(`/api/messages/${conversationId}` as string, {
|
|
62
63
|
method: 'POST',
|
|
63
64
|
body: { body: text },
|
|
64
65
|
});
|
|
65
66
|
// SSE will pick up the new message, but also do an immediate refresh for responsiveness
|
|
66
|
-
refresh()
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
});
|
|
67
|
+
await refresh();
|
|
68
|
+
if (initialMessages.value) {
|
|
69
|
+
messages.value = [...initialMessages.value];
|
|
70
|
+
}
|
|
71
71
|
}
|
|
72
72
|
</script>
|
|
73
73
|
|
package/pages/mirror/[id].vue
CHANGED
|
@@ -117,6 +117,7 @@ useSeoMeta({
|
|
|
117
117
|
<p v-if="transformedContent.author.bio" class="cpub-mirror-bio">{{ stripHtml(transformedContent.author.bio) }}</p>
|
|
118
118
|
</div>
|
|
119
119
|
</div>
|
|
120
|
+
<!-- Content is sanitized on ingest (inboxHandlers.ts → sanitizeHtml). Safe for v-html. -->
|
|
120
121
|
<div v-if="typeof transformedContent.content === 'string'" class="cpub-mirror-body prose" v-html="transformedContent.content" />
|
|
121
122
|
<ContentAttachments v-if="transformedContent.attachments?.length" :attachments="transformedContent.attachments" />
|
|
122
123
|
<div v-if="transformedContent.tags?.length" class="cpub-mirror-tags">
|
|
@@ -146,7 +147,7 @@ useSeoMeta({
|
|
|
146
147
|
display: flex; align-items: center; gap: 4px; font-size: 11px;
|
|
147
148
|
}
|
|
148
149
|
.cpub-fed-banner-follow {
|
|
149
|
-
margin-left: auto; background: var(--accent); color: #fff; border: none;
|
|
150
|
+
margin-left: auto; background: var(--accent); color: var(--accent-text, #fff); border: none;
|
|
150
151
|
font-size: 11px; font-weight: 600; padding: 3px 10px; cursor: pointer;
|
|
151
152
|
display: flex; align-items: center; gap: 4px; white-space: nowrap;
|
|
152
153
|
}
|
|
@@ -2,7 +2,7 @@ import { federateDirectMessage, resolveRemoteHandle } from '@commonpub/server';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
|
|
4
4
|
const dmSchema = z.object({
|
|
5
|
-
handle: z.string().min(3),
|
|
5
|
+
handle: z.string().min(3).regex(/^@?[\w.-]+@[\w.-]+\.\w+$/, 'Invalid federation handle'),
|
|
6
6
|
body: z.string().min(1).max(10000),
|
|
7
7
|
});
|
|
8
8
|
|
|
@@ -18,7 +18,11 @@ export default defineEventHandler(async (event) => {
|
|
|
18
18
|
throw createError({ statusCode: 404, statusMessage: 'Could not resolve remote user' });
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
try {
|
|
22
|
+
await federateDirectMessage(db, user.id, resolved.actorUri, input.body, config.instance.domain);
|
|
23
|
+
} catch {
|
|
24
|
+
throw createError({ statusCode: 502, statusMessage: 'Failed to deliver message to remote server' });
|
|
25
|
+
}
|
|
22
26
|
|
|
23
27
|
return {
|
|
24
28
|
sent: true,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { likePost, unlikePost, hasLikedPost, getHubBySlug, getPostById, federateHubPostLike } from '@commonpub/server';
|
|
1
|
+
import { likePost, unlikePost, hasLikedPost, getHubBySlug, getPostById, federateHubPostLike, checkBan } from '@commonpub/server';
|
|
2
2
|
|
|
3
3
|
export default defineEventHandler(async (event) => {
|
|
4
4
|
const user = requireAuth(event);
|
|
@@ -12,6 +12,10 @@ export default defineEventHandler(async (event) => {
|
|
|
12
12
|
const post = await getPostById(db, postId);
|
|
13
13
|
if (!post || post.hubId !== community.id) throw createError({ statusCode: 404, statusMessage: 'Post not found' });
|
|
14
14
|
|
|
15
|
+
// Banned users cannot like posts
|
|
16
|
+
const ban = await checkBan(db, community.id, user.id);
|
|
17
|
+
if (ban) throw createError({ statusCode: 403, statusMessage: 'You are banned from this hub' });
|
|
18
|
+
|
|
15
19
|
const alreadyLiked = await hasLikedPost(db, user.id, postId);
|
|
16
20
|
if (alreadyLiked) {
|
|
17
21
|
await unlikePost(db, user.id, postId);
|
|
@@ -14,7 +14,7 @@ export default defineEventHandler(async (event): Promise<HubPostItem> => {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const input = await parseBody(event, editPostSchema);
|
|
17
|
-
const updated = await editPost(db, postId, user.id, input);
|
|
17
|
+
const updated = await editPost(db, postId, user.id, community.id, input);
|
|
18
18
|
if (!updated) {
|
|
19
19
|
throw createError({ statusCode: 403, statusMessage: 'Not authorized to edit this post' });
|
|
20
20
|
}
|