@commonpub/layer 0.3.17 → 0.3.19
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/MessageThread.vue +45 -8
- package/components/views/ArticleView.vue +4 -4
- package/components/views/BlogView.vue +4 -4
- package/components/views/ExplainerView.vue +24 -15
- package/package.json +6 -6
- package/pages/auth/login.vue +190 -10
- package/pages/dashboard.vue +87 -6
- package/pages/explore.vue +134 -9
- package/pages/messages/[conversationId].vue +2 -3
- package/pages/messages/index.vue +176 -21
- package/pages/mirror/[id].vue +1 -1
- package/pages/tags/[slug].vue +32 -8
- package/server/api/admin/federation/hub-mirrors/[id]/backfill.post.ts +1 -1
- package/server/api/auth/federated/callback.get.ts +35 -13
- package/server/api/auth/federated/link.post.ts +82 -0
- package/server/api/messages/index.get.ts +9 -5
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { consumeOAuthState, exchangeCodeForToken, linkFederatedAccount } from '@commonpub/server';
|
|
1
|
+
import { consumeOAuthState, exchangeCodeForToken, linkFederatedAccount, findUserByFederatedAccount, createFederatedSession, storePendingLink } from '@commonpub/server';
|
|
2
|
+
import type { H3Event } from 'h3';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
|
|
4
5
|
const callbackSchema = z.object({
|
|
@@ -6,16 +7,30 @@ const callbackSchema = z.object({
|
|
|
6
7
|
state: z.string(),
|
|
7
8
|
});
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Set the Better Auth session cookie after federated login.
|
|
12
|
+
*/
|
|
13
|
+
function setSessionCookie(event: H3Event, token: string, expiresAt: Date): void {
|
|
14
|
+
setCookie(event, 'better-auth.session_token', token, {
|
|
15
|
+
httpOnly: true,
|
|
16
|
+
secure: process.env.NODE_ENV === 'production',
|
|
17
|
+
sameSite: 'lax',
|
|
18
|
+
path: '/',
|
|
19
|
+
expires: expiresAt,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
9
23
|
/**
|
|
10
24
|
* OAuth2 callback handler for federated login.
|
|
11
|
-
* Exchanges authorization code for token, links federated account
|
|
25
|
+
* Exchanges authorization code for token, links federated account,
|
|
26
|
+
* creates a session, and redirects.
|
|
12
27
|
*/
|
|
13
28
|
export default defineEventHandler(async (event) => {
|
|
14
29
|
requireFeature('federation');
|
|
15
30
|
const db = useDB();
|
|
16
31
|
const { code, state: stateToken } = parseQueryParams(event, callbackSchema);
|
|
17
32
|
|
|
18
|
-
// Retrieve and consume the stored OAuth state (single-use,
|
|
33
|
+
// Retrieve and consume the stored OAuth state (single-use, atomic)
|
|
19
34
|
const oauthState = await consumeOAuthState(db, stateToken);
|
|
20
35
|
if (!oauthState) {
|
|
21
36
|
throw createError({
|
|
@@ -33,14 +48,19 @@ export default defineEventHandler(async (event) => {
|
|
|
33
48
|
});
|
|
34
49
|
}
|
|
35
50
|
|
|
51
|
+
const ipAddress = getRequestHeader(event, 'x-forwarded-for')?.split(',')[0]?.trim()
|
|
52
|
+
?? getRequestHeader(event, 'x-real-ip')
|
|
53
|
+
?? undefined;
|
|
54
|
+
const userAgent = getRequestHeader(event, 'user-agent') ?? undefined;
|
|
55
|
+
|
|
36
56
|
// Check if a local user is already linked to this federated account
|
|
37
|
-
const { findUserByFederatedAccount } = await import('@commonpub/server');
|
|
38
57
|
const existingLink = await findUserByFederatedAccount(db, tokenResult.user.actorUri);
|
|
39
58
|
|
|
40
59
|
if (existingLink) {
|
|
41
|
-
// User already linked — redirect to dashboard
|
|
42
|
-
|
|
43
|
-
|
|
60
|
+
// User already linked — create session and redirect to dashboard
|
|
61
|
+
const session = await createFederatedSession(db, existingLink.userId, ipAddress, userAgent);
|
|
62
|
+
setSessionCookie(event, session.sessionToken, session.expiresAt);
|
|
63
|
+
return sendRedirect(event, '/dashboard', 302);
|
|
44
64
|
}
|
|
45
65
|
|
|
46
66
|
// Check if the current user is logged in — if so, link to their account
|
|
@@ -55,13 +75,15 @@ export default defineEventHandler(async (event) => {
|
|
|
55
75
|
return sendRedirect(event, '/settings/account?federated=linked', 302);
|
|
56
76
|
}
|
|
57
77
|
|
|
58
|
-
// Not logged in and no existing link —
|
|
59
|
-
//
|
|
60
|
-
const
|
|
61
|
-
federated: 'true',
|
|
78
|
+
// Not logged in and no existing link — store verified identity in a server-side token.
|
|
79
|
+
// Only the opaque token is passed to the client; the actorUri is never exposed in the URL.
|
|
80
|
+
const linkToken = await storePendingLink(db, {
|
|
62
81
|
actorUri: tokenResult.user.actorUri,
|
|
63
82
|
username: tokenResult.user.username,
|
|
64
|
-
|
|
83
|
+
instanceDomain: oauthState.instanceDomain,
|
|
84
|
+
displayName: tokenResult.user.displayName ?? undefined,
|
|
85
|
+
avatarUrl: tokenResult.user.avatarUrl ?? undefined,
|
|
65
86
|
});
|
|
66
|
-
|
|
87
|
+
|
|
88
|
+
return sendRedirect(event, `/auth/login?federated=true&linkToken=${linkToken}`, 302);
|
|
67
89
|
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { linkFederatedAccount, consumePendingLink } from '@commonpub/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const linkSchema = z.object({
|
|
5
|
+
/** Local credentials */
|
|
6
|
+
identity: z.string().min(1),
|
|
7
|
+
password: z.string().min(1),
|
|
8
|
+
/** Server-side link token from the OAuth callback — proves the federated identity was verified */
|
|
9
|
+
linkToken: z.string().min(1),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Link a verified federated identity to a local account.
|
|
14
|
+
* The linkToken is a server-side opaque token stored during the OAuth callback.
|
|
15
|
+
* It proves the federated identity was authenticated — the actorUri is never
|
|
16
|
+
* sent from the client, preventing identity hijacking.
|
|
17
|
+
*/
|
|
18
|
+
export default defineEventHandler(async (event) => {
|
|
19
|
+
requireFeature('federation');
|
|
20
|
+
const db = useDB();
|
|
21
|
+
const body = await parseBody(event, linkSchema);
|
|
22
|
+
|
|
23
|
+
// Step 1: Consume the pending link token (single-use, 10min TTL)
|
|
24
|
+
const pendingLink = await consumePendingLink(db, body.linkToken);
|
|
25
|
+
if (!pendingLink) {
|
|
26
|
+
throw createError({
|
|
27
|
+
statusCode: 400,
|
|
28
|
+
statusMessage: 'Link token is invalid or expired. Please start the federated login again.',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Step 2: Resolve identity to email
|
|
33
|
+
const { email } = await $fetch<{ email: string }>('/api/resolve-identity', {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
body: { identity: body.identity },
|
|
36
|
+
}).catch(() => {
|
|
37
|
+
throw createError({ statusCode: 401, statusMessage: 'Invalid credentials.' });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Step 3: Authenticate via Better Auth sign-in (also creates session + sets cookie)
|
|
41
|
+
const signInResponse = await $fetch<{ user?: { id: string }; session?: { token: string; expiresAt: string } }>(
|
|
42
|
+
'/api/auth/sign-in/email',
|
|
43
|
+
{
|
|
44
|
+
method: 'POST',
|
|
45
|
+
body: { email, password: body.password },
|
|
46
|
+
},
|
|
47
|
+
).catch(() => {
|
|
48
|
+
throw createError({ statusCode: 401, statusMessage: 'Invalid credentials.' });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!signInResponse?.user?.id || !signInResponse?.session?.token) {
|
|
52
|
+
throw createError({ statusCode: 401, statusMessage: 'Invalid credentials.' });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const userId = signInResponse.user.id;
|
|
56
|
+
|
|
57
|
+
// Step 4: Link the verified federated identity (from server-side token, NOT client input)
|
|
58
|
+
try {
|
|
59
|
+
await linkFederatedAccount(db, userId, pendingLink.actorUri, pendingLink.instanceDomain, {
|
|
60
|
+
preferredUsername: pendingLink.username,
|
|
61
|
+
displayName: pendingLink.displayName,
|
|
62
|
+
avatarUrl: pendingLink.avatarUrl,
|
|
63
|
+
});
|
|
64
|
+
} catch (err: unknown) {
|
|
65
|
+
const msg = err instanceof Error ? err.message : 'Failed to link account';
|
|
66
|
+
throw createError({ statusCode: 409, statusMessage: msg });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Step 5: Use the session Better Auth created — set cookie for the client
|
|
70
|
+
setCookie(event, 'better-auth.session_token', signInResponse.session.token, {
|
|
71
|
+
httpOnly: true,
|
|
72
|
+
secure: process.env.NODE_ENV === 'production',
|
|
73
|
+
sameSite: 'lax',
|
|
74
|
+
path: '/',
|
|
75
|
+
expires: new Date(signInResponse.session.expiresAt),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
success: true,
|
|
80
|
+
linked: true,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { listConversations } from '@commonpub/server';
|
|
1
|
+
import { listConversations, getConversationUnreadCounts } from '@commonpub/server';
|
|
2
2
|
import { users } from '@commonpub/schema';
|
|
3
3
|
import { inArray } from 'drizzle-orm';
|
|
4
4
|
|
|
@@ -6,11 +6,14 @@ export default defineEventHandler(async (event) => {
|
|
|
6
6
|
const db = useDB();
|
|
7
7
|
const user = requireAuth(event);
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const [conversationList, unreadCounts] = await Promise.all([
|
|
10
|
+
listConversations(db, user.id),
|
|
11
|
+
getConversationUnreadCounts(db, user.id),
|
|
12
|
+
]);
|
|
10
13
|
|
|
11
14
|
// Collect all unique participant IDs
|
|
12
15
|
const allIds = new Set<string>();
|
|
13
|
-
for (const conv of
|
|
16
|
+
for (const conv of conversationList) {
|
|
14
17
|
for (const id of (conv.participants ?? [])) {
|
|
15
18
|
allIds.add(id);
|
|
16
19
|
}
|
|
@@ -28,9 +31,10 @@ export default defineEventHandler(async (event) => {
|
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
// Replace participant IDs with resolved user objects
|
|
32
|
-
return
|
|
34
|
+
// Replace participant IDs with resolved user objects, include unread count
|
|
35
|
+
return conversationList.map((conv) => ({
|
|
33
36
|
...conv,
|
|
37
|
+
unreadCount: unreadCounts[conv.id] ?? 0,
|
|
34
38
|
participants: (conv.participants ?? []).map((id: string) => {
|
|
35
39
|
const u = userMap.get(id);
|
|
36
40
|
return u ? { username: u.username, displayName: u.displayName, avatarUrl: u.avatarUrl } : { username: id, displayName: null, avatarUrl: null };
|