@commonpub/layer 0.8.4 → 0.8.6
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/composables/useTheme.ts +8 -0
- package/package.json +5 -5
- package/pages/admin/federation.vue +24 -12
- package/pages/products/[slug].vue +5 -2
- package/server/api/auth/federated/login.post.ts +22 -2
- package/server/api/auth/sign-in-username.post.ts +17 -6
- package/server/api/profile/theme.put.ts +23 -0
package/composables/useTheme.ts
CHANGED
|
@@ -48,6 +48,14 @@ export function useTheme(): {
|
|
|
48
48
|
|
|
49
49
|
if (import.meta.client) {
|
|
50
50
|
applyThemeToElement(document.documentElement, newTheme);
|
|
51
|
+
|
|
52
|
+
// Persist to DB for cross-device sync (fire-and-forget, cookie is primary)
|
|
53
|
+
$fetch('/api/profile/theme', {
|
|
54
|
+
method: 'PUT',
|
|
55
|
+
body: { themeId: newTheme },
|
|
56
|
+
}).catch(() => {
|
|
57
|
+
// Not logged in or network error — cookie preference is sufficient
|
|
58
|
+
});
|
|
51
59
|
}
|
|
52
60
|
}
|
|
53
61
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -54,12 +54,12 @@
|
|
|
54
54
|
"vue-router": "^4.3.0",
|
|
55
55
|
"zod": "^4.3.6",
|
|
56
56
|
"@commonpub/auth": "0.5.1",
|
|
57
|
+
"@commonpub/docs": "0.6.2",
|
|
57
58
|
"@commonpub/config": "0.9.1",
|
|
58
|
-
"@commonpub/protocol": "0.9.9",
|
|
59
|
-
"@commonpub/ui": "0.8.5",
|
|
60
59
|
"@commonpub/editor": "0.7.9",
|
|
61
|
-
"@commonpub/
|
|
62
|
-
"@commonpub/
|
|
60
|
+
"@commonpub/learning": "0.5.0",
|
|
61
|
+
"@commonpub/protocol": "0.9.9",
|
|
62
|
+
"@commonpub/ui": "0.8.5"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -46,11 +46,15 @@ async function addTrusted(): Promise<void> {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
async function removeTrusted(domain: string): Promise<void> {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
try {
|
|
50
|
+
await $fetch('/api/admin/federation/trusted-instances', {
|
|
51
|
+
method: 'DELETE',
|
|
52
|
+
body: { domain },
|
|
53
|
+
});
|
|
54
|
+
await refreshTrusted();
|
|
55
|
+
} catch {
|
|
56
|
+
alert('Failed to remove trusted instance');
|
|
57
|
+
}
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
// Mirror creation
|
|
@@ -79,16 +83,24 @@ async function createMirror(): Promise<void> {
|
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
async function toggleMirror(id: string, currentStatus: string): Promise<void> {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
try {
|
|
87
|
+
await $fetch(`/api/admin/federation/mirrors/${id}`, {
|
|
88
|
+
method: 'PUT',
|
|
89
|
+
body: { action: currentStatus === 'active' ? 'pause' : 'resume' },
|
|
90
|
+
});
|
|
91
|
+
await refreshMirrors();
|
|
92
|
+
} catch {
|
|
93
|
+
alert('Failed to update mirror');
|
|
94
|
+
}
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
async function deleteMirror(id: string): Promise<void> {
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
try {
|
|
99
|
+
await $fetch(`/api/admin/federation/mirrors/${id}`, { method: 'DELETE' });
|
|
100
|
+
await refreshMirrors();
|
|
101
|
+
} catch {
|
|
102
|
+
alert('Failed to delete mirror');
|
|
103
|
+
}
|
|
92
104
|
}
|
|
93
105
|
|
|
94
106
|
// Backfill
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const route = useRoute();
|
|
3
3
|
const slug = route.params.slug as string;
|
|
4
4
|
|
|
5
|
-
const { data: product } = useLazyFetch(`/api/products/${slug}`) as { data: Ref<Record<string, any> | null> };
|
|
5
|
+
const { data: product, pending } = useLazyFetch(`/api/products/${slug}`) as { data: Ref<Record<string, any> | null>; pending: Ref<boolean> };
|
|
6
6
|
const { data: projectsUsing } = useLazyFetch(`/api/products/${slug}/content`) as { data: Ref<any[] | null> };
|
|
7
7
|
|
|
8
8
|
useSeoMeta({
|
|
@@ -12,7 +12,10 @@ useSeoMeta({
|
|
|
12
12
|
</script>
|
|
13
13
|
|
|
14
14
|
<template>
|
|
15
|
-
<div v-if="
|
|
15
|
+
<div v-if="pending" style="padding: 32px; text-align: center; color: var(--text-faint);">
|
|
16
|
+
<i class="fa-solid fa-circle-notch fa-spin"></i> Loading product...
|
|
17
|
+
</div>
|
|
18
|
+
<div v-else-if="product" class="product-detail">
|
|
16
19
|
<NuxtLink to="/products" class="cpub-back-link"><i class="fa-solid fa-arrow-left"></i> Products</NuxtLink>
|
|
17
20
|
|
|
18
21
|
<div class="product-layout">
|
|
@@ -36,8 +36,28 @@ export default defineEventHandler(async (event) => {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const redirectUri = `https://${config.instance.domain}/api/auth/federated/callback`;
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
let effectiveClientId = clientId ?? `cpub_${config.instance.domain}`;
|
|
40
|
+
let effectiveClientSecret = clientSecret ?? '';
|
|
41
|
+
|
|
42
|
+
// Auto-register as a dynamic client if no explicit credentials provided
|
|
43
|
+
if (!clientId) {
|
|
44
|
+
try {
|
|
45
|
+
const regUrl = endpoints.tokenEndpoint.replace('/token', '/register');
|
|
46
|
+
const regResult = await $fetch<{ client_id: string; client_secret: string }>(regUrl, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
body: {
|
|
49
|
+
client_name: config.instance.name || config.instance.domain,
|
|
50
|
+
redirect_uris: [redirectUri],
|
|
51
|
+
client_uri: `https://${config.instance.domain}`,
|
|
52
|
+
instance_domain: config.instance.domain,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
effectiveClientId = regResult.client_id;
|
|
56
|
+
effectiveClientSecret = regResult.client_secret;
|
|
57
|
+
} catch {
|
|
58
|
+
// Registration failed — proceed with default client ID (may fail at authorize step)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
41
61
|
|
|
42
62
|
// Store state for the callback handler
|
|
43
63
|
const stateToken = await storeOAuthState(db, {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { eq, and, isNull } from 'drizzle-orm';
|
|
2
|
+
import { users } from '@commonpub/schema';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
|
|
4
5
|
const signInSchema = z.object({
|
|
@@ -14,11 +15,21 @@ const signInSchema = z.object({
|
|
|
14
15
|
export default defineEventHandler(async (event) => {
|
|
15
16
|
const body = await parseBody(event, signInSchema);
|
|
16
17
|
|
|
17
|
-
let email
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
let email = body.identity;
|
|
19
|
+
|
|
20
|
+
// If identity doesn't look like an email, resolve username → email
|
|
21
|
+
if (!body.identity.includes('@')) {
|
|
22
|
+
const db = useDB();
|
|
23
|
+
const [user] = await db
|
|
24
|
+
.select({ email: users.email })
|
|
25
|
+
.from(users)
|
|
26
|
+
.where(and(eq(users.username, body.identity), isNull(users.deletedAt)))
|
|
27
|
+
.limit(1);
|
|
28
|
+
|
|
29
|
+
if (!user) {
|
|
30
|
+
throw createError({ statusCode: 401, statusMessage: 'Invalid credentials' });
|
|
31
|
+
}
|
|
32
|
+
email = user.email;
|
|
22
33
|
}
|
|
23
34
|
|
|
24
35
|
// Proxy to Better Auth's email sign-in (internal server-side call)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { setUserTheme } from '@commonpub/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const themeSchema = z.object({
|
|
5
|
+
themeId: z.string().min(1).max(32),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
|
|
9
|
+
const db = useDB();
|
|
10
|
+
const user = requireAuth(event);
|
|
11
|
+
const { themeId } = await parseBody(event, themeSchema);
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
await setUserTheme(db, user.id, themeId);
|
|
15
|
+
} catch (err) {
|
|
16
|
+
throw createError({
|
|
17
|
+
statusCode: 400,
|
|
18
|
+
statusMessage: err instanceof Error ? err.message : 'Invalid theme',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return { success: true };
|
|
23
|
+
});
|