@commonpub/layer 0.8.3 → 0.8.5
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/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/composables/useTheme.ts +8 -0
- package/layouts/default.vue +7 -7
- package/middleware/feature-gate.global.ts +24 -0
- package/package.json +6 -6
- package/pages/[type]/index.vue +4 -3
- package/pages/admin/audit.vue +3 -2
- package/pages/admin/federation.vue +33 -13
- 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/products/[slug].vue +5 -2
- 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/federated/login.post.ts +22 -2
- 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/profile/theme.put.ts +23 -0
- 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
package/pages/admin/reports.vue
CHANGED
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
definePageMeta({ layout: 'admin', middleware: 'auth' });
|
|
3
3
|
useSeoMeta({ title: `Reports — Admin — ${useSiteName()}` });
|
|
4
4
|
|
|
5
|
-
const { data: reportsData, refresh } = await useFetch('/api/admin/reports');
|
|
6
5
|
const toast = useToast();
|
|
6
|
+
const statusFilter = ref<string>('pending');
|
|
7
|
+
|
|
8
|
+
const { data: reportsData, refresh } = await useFetch(() => {
|
|
9
|
+
const base = '/api/admin/reports';
|
|
10
|
+
return statusFilter.value ? `${base}?status=${statusFilter.value}` : base;
|
|
11
|
+
});
|
|
7
12
|
|
|
8
13
|
interface Report {
|
|
9
14
|
id: string;
|
|
@@ -14,6 +19,9 @@ interface Report {
|
|
|
14
19
|
targetType: string;
|
|
15
20
|
targetId: string;
|
|
16
21
|
createdAt: string;
|
|
22
|
+
reporter?: { id: string; username: string };
|
|
23
|
+
reviewer?: { id: string; username: string } | null;
|
|
24
|
+
resolution?: string | null;
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
const reports = computed<Report[]>(() => {
|
|
@@ -23,66 +31,174 @@ const reports = computed<Report[]>(() => {
|
|
|
23
31
|
return data.items ?? [];
|
|
24
32
|
});
|
|
25
33
|
|
|
26
|
-
|
|
34
|
+
// Bulk selection
|
|
35
|
+
const selectedIds = ref<Set<string>>(new Set());
|
|
36
|
+
const allSelected = computed(() => reports.value.length > 0 && reports.value.every(r => selectedIds.value.has(r.id)));
|
|
37
|
+
|
|
38
|
+
function toggleSelect(id: string): void {
|
|
39
|
+
const s = new Set(selectedIds.value);
|
|
40
|
+
if (s.has(id)) s.delete(id);
|
|
41
|
+
else s.add(id);
|
|
42
|
+
selectedIds.value = s;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toggleSelectAll(): void {
|
|
46
|
+
if (allSelected.value) {
|
|
47
|
+
selectedIds.value = new Set();
|
|
48
|
+
} else {
|
|
49
|
+
selectedIds.value = new Set(reports.value.map(r => r.id));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Single report actions
|
|
54
|
+
async function resolveReport(id: string, status: 'reviewed' | 'resolved' | 'dismissed', resolution?: string): Promise<void> {
|
|
55
|
+
const text = resolution ?? prompt(`Reason for ${status}:`);
|
|
56
|
+
if (!text) return;
|
|
27
57
|
try {
|
|
28
58
|
await $fetch(`/api/admin/reports/${id}/resolve` as string, {
|
|
29
59
|
method: 'POST',
|
|
30
|
-
body: { resolution },
|
|
60
|
+
body: { status, resolution: text },
|
|
31
61
|
});
|
|
32
|
-
toast.success(`Report ${
|
|
62
|
+
toast.success(`Report ${status}`);
|
|
63
|
+
selectedIds.value.delete(id);
|
|
33
64
|
await refresh();
|
|
34
65
|
} catch {
|
|
35
66
|
toast.error('Failed to update report');
|
|
36
67
|
}
|
|
37
68
|
}
|
|
69
|
+
|
|
70
|
+
// Bulk actions
|
|
71
|
+
async function bulkAction(status: 'reviewed' | 'resolved' | 'dismissed'): Promise<void> {
|
|
72
|
+
if (selectedIds.value.size === 0) return;
|
|
73
|
+
const text = prompt(`Reason for bulk ${status} (${selectedIds.value.size} reports):`);
|
|
74
|
+
if (!text) return;
|
|
75
|
+
let successCount = 0;
|
|
76
|
+
for (const id of selectedIds.value) {
|
|
77
|
+
try {
|
|
78
|
+
await $fetch(`/api/admin/reports/${id}/resolve` as string, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
body: { status, resolution: text },
|
|
81
|
+
});
|
|
82
|
+
successCount++;
|
|
83
|
+
} catch {
|
|
84
|
+
// continue with remaining
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
toast.success(`${successCount} report${successCount === 1 ? '' : 's'} ${status}`);
|
|
88
|
+
selectedIds.value = new Set();
|
|
89
|
+
await refresh();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
watch(statusFilter, () => {
|
|
93
|
+
selectedIds.value = new Set();
|
|
94
|
+
});
|
|
38
95
|
</script>
|
|
39
96
|
|
|
40
97
|
<template>
|
|
41
|
-
<div class="admin-reports">
|
|
42
|
-
<h1 class="admin-page-title">Reports</h1>
|
|
98
|
+
<div class="cpub-admin-reports">
|
|
99
|
+
<h1 class="cpub-admin-page-title">Reports</h1>
|
|
100
|
+
|
|
101
|
+
<!-- Filter bar -->
|
|
102
|
+
<div class="cpub-report-filters">
|
|
103
|
+
<button
|
|
104
|
+
v-for="s in ['pending', 'reviewed', 'resolved', 'dismissed', '']"
|
|
105
|
+
:key="s"
|
|
106
|
+
class="cpub-report-filter-btn"
|
|
107
|
+
:class="{ 'cpub-report-filter-active': statusFilter === s }"
|
|
108
|
+
@click="statusFilter = s"
|
|
109
|
+
>
|
|
110
|
+
{{ s || 'All' }}
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<!-- Bulk actions -->
|
|
115
|
+
<div v-if="selectedIds.size > 0" class="cpub-report-bulk">
|
|
116
|
+
<span class="cpub-report-bulk-count">{{ selectedIds.size }} selected</span>
|
|
117
|
+
<button v-if="statusFilter === 'pending'" class="cpub-btn cpub-btn-sm" @click="bulkAction('reviewed')">
|
|
118
|
+
<i class="fa-solid fa-eye" /> Mark Reviewed
|
|
119
|
+
</button>
|
|
120
|
+
<button class="cpub-btn cpub-btn-sm" style="color: var(--green); border-color: var(--green-border);" @click="bulkAction('resolved')">
|
|
121
|
+
<i class="fa-solid fa-check" /> Resolve
|
|
122
|
+
</button>
|
|
123
|
+
<button class="cpub-btn cpub-btn-sm" @click="bulkAction('dismissed')">
|
|
124
|
+
<i class="fa-solid fa-xmark" /> Dismiss
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
43
127
|
|
|
44
128
|
<template v-if="reports.length">
|
|
45
|
-
<div class="report-card" v-for="report in reports" :key="report.id">
|
|
46
|
-
<div class="report-header">
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
129
|
+
<div class="cpub-report-card" v-for="report in reports" :key="report.id">
|
|
130
|
+
<div class="cpub-report-header">
|
|
131
|
+
<label class="cpub-report-checkbox" @click.stop>
|
|
132
|
+
<input type="checkbox" :checked="selectedIds.has(report.id)" @change="toggleSelect(report.id)" />
|
|
133
|
+
</label>
|
|
134
|
+
<span class="cpub-report-status" :class="`cpub-status-${report.status}`">{{ report.status }}</span>
|
|
135
|
+
<span class="cpub-report-type">{{ report.targetType }}</span>
|
|
136
|
+
<time class="cpub-report-date">{{ new Date(report.createdAt).toLocaleDateString() }}</time>
|
|
50
137
|
</div>
|
|
51
|
-
<p class="report-reason"><strong>{{ report.reason }}</strong></p>
|
|
52
|
-
<p v-if="report.description" class="report-desc">{{ report.description }}</p>
|
|
53
|
-
<div class="report-meta">
|
|
54
|
-
<span class="report-meta-item">Reporter: <code>{{ report.reporterId }}</code></span>
|
|
55
|
-
<span class="report-meta-item">Target: <code>{{ report.targetId }}
|
|
138
|
+
<p class="cpub-report-reason"><strong>{{ report.reason }}</strong></p>
|
|
139
|
+
<p v-if="report.description" class="cpub-report-desc">{{ report.description }}</p>
|
|
140
|
+
<div class="cpub-report-meta">
|
|
141
|
+
<span class="cpub-report-meta-item">Reporter: <code>{{ report.reporter?.username ?? report.reporterId }}</code></span>
|
|
142
|
+
<span class="cpub-report-meta-item">Target: <code>{{ report.targetId.slice(0, 8) }}...</code></span>
|
|
143
|
+
<span v-if="report.reviewer" class="cpub-report-meta-item">Reviewed by: <code>{{ report.reviewer.username }}</code></span>
|
|
56
144
|
</div>
|
|
57
|
-
<
|
|
145
|
+
<p v-if="report.resolution" class="cpub-report-resolution">
|
|
146
|
+
<i class="fa-solid fa-comment-dots" /> {{ report.resolution }}
|
|
147
|
+
</p>
|
|
148
|
+
<div v-if="report.status === 'pending' || report.status === 'reviewed'" class="cpub-report-actions">
|
|
149
|
+
<button v-if="report.status === 'pending'" class="cpub-btn cpub-btn-sm" @click="resolveReport(report.id, 'reviewed')">
|
|
150
|
+
<i class="fa-solid fa-eye" /> Mark Reviewed
|
|
151
|
+
</button>
|
|
58
152
|
<button class="cpub-btn cpub-btn-sm" style="color: var(--green); border-color: var(--green-border);" @click="resolveReport(report.id, 'resolved')">
|
|
59
|
-
<i class="fa-solid fa-check"
|
|
153
|
+
<i class="fa-solid fa-check" /> Resolve
|
|
60
154
|
</button>
|
|
61
155
|
<button class="cpub-btn cpub-btn-sm" @click="resolveReport(report.id, 'dismissed')">
|
|
62
|
-
<i class="fa-solid fa-xmark"
|
|
156
|
+
<i class="fa-solid fa-xmark" /> Dismiss
|
|
63
157
|
</button>
|
|
64
158
|
</div>
|
|
65
159
|
</div>
|
|
160
|
+
|
|
161
|
+
<!-- Select all -->
|
|
162
|
+
<div class="cpub-report-select-all">
|
|
163
|
+
<label @click.stop>
|
|
164
|
+
<input type="checkbox" :checked="allSelected" @change="toggleSelectAll" />
|
|
165
|
+
<span>Select all</span>
|
|
166
|
+
</label>
|
|
167
|
+
</div>
|
|
66
168
|
</template>
|
|
67
|
-
<p class="admin-empty" v-else>No reports
|
|
169
|
+
<p class="cpub-admin-empty" v-else>No reports{{ statusFilter ? ` with status "${statusFilter}"` : '' }}.</p>
|
|
68
170
|
</div>
|
|
69
171
|
</template>
|
|
70
172
|
|
|
71
173
|
<style scoped>
|
|
72
|
-
.admin-page-title { font-size: var(--text-xl); font-weight: var(--font-weight-bold); margin-bottom: var(--space-6); }
|
|
73
|
-
.report-
|
|
74
|
-
.report-
|
|
75
|
-
.report-
|
|
76
|
-
.
|
|
77
|
-
.
|
|
78
|
-
.
|
|
79
|
-
.report-
|
|
80
|
-
.report-
|
|
81
|
-
.report-
|
|
82
|
-
.report-
|
|
83
|
-
.report-
|
|
84
|
-
.
|
|
85
|
-
.
|
|
86
|
-
.
|
|
87
|
-
.
|
|
174
|
+
.cpub-admin-page-title { font-size: var(--text-xl); font-weight: var(--font-weight-bold); margin-bottom: var(--space-6); }
|
|
175
|
+
.cpub-report-filters { display: flex; gap: 4px; margin-bottom: var(--space-4); flex-wrap: wrap; }
|
|
176
|
+
.cpub-report-filter-btn { padding: 4px 10px; font-size: 11px; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.04em; background: var(--surface); border: var(--border-width-default) solid var(--border); color: var(--text-dim); cursor: pointer; }
|
|
177
|
+
.cpub-report-filter-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
178
|
+
.cpub-report-filter-active { background: var(--accent-bg); border-color: var(--accent); color: var(--accent); }
|
|
179
|
+
.cpub-report-bulk { display: flex; align-items: center; gap: 8px; padding: 8px 12px; margin-bottom: var(--space-3); background: var(--accent-bg); border: var(--border-width-default) solid var(--accent-border); }
|
|
180
|
+
.cpub-report-bulk-count { font-size: 11px; font-family: var(--font-mono); color: var(--accent); font-weight: 600; }
|
|
181
|
+
.cpub-report-card { padding: 16px; border: var(--border-width-default) solid var(--border); background: var(--surface); margin-bottom: 12px; box-shadow: var(--shadow-md); }
|
|
182
|
+
.cpub-report-header { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
|
183
|
+
.cpub-report-checkbox { display: flex; align-items: center; cursor: pointer; }
|
|
184
|
+
.cpub-report-checkbox input { cursor: pointer; accent-color: var(--accent); }
|
|
185
|
+
.cpub-report-status { font-size: 10px; font-family: var(--font-mono); font-weight: 600; text-transform: uppercase; padding: 2px 8px; }
|
|
186
|
+
.cpub-status-pending { background: var(--yellow-bg); color: var(--yellow); border: var(--border-width-default) solid var(--yellow-border); }
|
|
187
|
+
.cpub-status-reviewed { background: var(--blue-bg, var(--accent-bg)); color: var(--blue, var(--accent)); border: var(--border-width-default) solid var(--blue-border, var(--accent-border)); }
|
|
188
|
+
.cpub-status-resolved { background: var(--green-bg); color: var(--green); border: var(--border-width-default) solid var(--green-border); }
|
|
189
|
+
.cpub-status-dismissed { background: var(--surface2); color: var(--text-faint); border: var(--border-width-default) solid var(--border2); }
|
|
190
|
+
.cpub-report-type { font-size: 10px; font-family: var(--font-mono); color: var(--accent); background: var(--accent-bg); padding: 2px 6px; border: var(--border-width-default) solid var(--accent-border); }
|
|
191
|
+
.cpub-report-date { font-size: 11px; color: var(--text-faint); margin-left: auto; font-family: var(--font-mono); }
|
|
192
|
+
.cpub-report-reason { font-size: 13px; margin-bottom: 4px; }
|
|
193
|
+
.cpub-report-desc { font-size: 12px; color: var(--text-dim); line-height: 1.5; margin-bottom: 8px; }
|
|
194
|
+
.cpub-report-meta { display: flex; gap: 16px; margin-bottom: 8px; flex-wrap: wrap; }
|
|
195
|
+
.cpub-report-meta-item { font-size: 10px; font-family: var(--font-mono); color: var(--text-faint); }
|
|
196
|
+
.cpub-report-meta-item code { background: var(--surface2); padding: 1px 4px; }
|
|
197
|
+
.cpub-report-resolution { font-size: 11px; color: var(--text-dim); font-style: italic; margin-bottom: 8px; }
|
|
198
|
+
.cpub-report-resolution i { color: var(--text-faint); margin-right: 4px; }
|
|
199
|
+
.cpub-report-actions { display: flex; gap: 6px; padding-top: 8px; border-top: var(--border-width-default) solid var(--border2); }
|
|
200
|
+
.cpub-report-select-all { display: flex; align-items: center; gap: 6px; padding: 8px 0; font-size: 11px; font-family: var(--font-mono); color: var(--text-faint); }
|
|
201
|
+
.cpub-report-select-all label { display: flex; align-items: center; gap: 6px; cursor: pointer; }
|
|
202
|
+
.cpub-report-select-all input { cursor: pointer; accent-color: var(--accent); }
|
|
203
|
+
.cpub-admin-empty { color: var(--text-faint); text-align: center; padding: var(--space-8) 0; }
|
|
88
204
|
</style>
|
package/pages/admin/settings.vue
CHANGED
|
@@ -3,7 +3,7 @@ definePageMeta({ layout: 'admin', middleware: 'auth' });
|
|
|
3
3
|
|
|
4
4
|
useSeoMeta({ title: `Settings — Admin — ${useSiteName()}` });
|
|
5
5
|
|
|
6
|
-
const { data: settings, refresh } = await useFetch<Record<string, string>>('/api/admin/settings');
|
|
6
|
+
const { data: settings, pending, refresh } = await useFetch<Record<string, string>>('/api/admin/settings');
|
|
7
7
|
|
|
8
8
|
const saving = ref(false);
|
|
9
9
|
const editKey = ref('');
|
|
@@ -58,7 +58,11 @@ async function addSetting(): Promise<void> {
|
|
|
58
58
|
<div class="admin-settings">
|
|
59
59
|
<h1 class="admin-page-title">Instance Settings</h1>
|
|
60
60
|
|
|
61
|
-
<div
|
|
61
|
+
<div v-if="pending" class="admin-loading">
|
|
62
|
+
<i class="fa-solid fa-circle-notch fa-spin"></i> Loading settings...
|
|
63
|
+
</div>
|
|
64
|
+
<template v-else-if="settings">
|
|
65
|
+
<div class="settings-list">
|
|
62
66
|
<div v-for="item in knownSettings" :key="item.key" class="settings-row">
|
|
63
67
|
<div class="settings-label">
|
|
64
68
|
<span class="settings-key">{{ item.label }}</span>
|
|
@@ -78,7 +82,7 @@ async function addSetting(): Promise<void> {
|
|
|
78
82
|
</div>
|
|
79
83
|
</div>
|
|
80
84
|
|
|
81
|
-
<div class="settings-custom"
|
|
85
|
+
<div class="settings-custom">
|
|
82
86
|
<h2 class="settings-section-title">Custom Settings</h2>
|
|
83
87
|
<div v-for="(value, key) in (settings as Record<string, string>)" :key="key" class="settings-row">
|
|
84
88
|
<template v-if="!knownSettings.some(k => k.key === key)">
|
|
@@ -105,8 +109,8 @@ async function addSetting(): Promise<void> {
|
|
|
105
109
|
<button class="cpub-btn cpub-btn-sm" :disabled="!newKey.trim()" @click="addSetting">Add</button>
|
|
106
110
|
</div>
|
|
107
111
|
</div>
|
|
108
|
-
|
|
109
|
-
<p class="admin-empty"
|
|
112
|
+
</template>
|
|
113
|
+
<p v-else class="admin-empty">No settings configured.</p>
|
|
110
114
|
</div>
|
|
111
115
|
</template>
|
|
112
116
|
|
|
@@ -154,6 +158,14 @@ async function addSetting(): Promise<void> {
|
|
|
154
158
|
background: var(--surface);
|
|
155
159
|
}
|
|
156
160
|
|
|
161
|
+
.admin-loading {
|
|
162
|
+
display: flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
gap: 8px;
|
|
165
|
+
padding: var(--space-8, 32px);
|
|
166
|
+
color: var(--text-faint);
|
|
167
|
+
}
|
|
168
|
+
|
|
157
169
|
.admin-empty {
|
|
158
170
|
color: var(--text-faint);
|
|
159
171
|
text-align: center;
|
package/pages/admin/theme.vue
CHANGED
|
@@ -4,7 +4,7 @@ import { BUILT_IN_THEMES } from '@commonpub/ui';
|
|
|
4
4
|
definePageMeta({ layout: 'admin', middleware: 'auth' });
|
|
5
5
|
useSeoMeta({ title: `Theme — Admin — ${useSiteName()}` });
|
|
6
6
|
|
|
7
|
-
const { data: settings, refresh } = await useFetch<Record<string, unknown>>('/api/admin/settings');
|
|
7
|
+
const { data: settings, pending, refresh } = await useFetch<Record<string, unknown>>('/api/admin/settings');
|
|
8
8
|
|
|
9
9
|
const saving = ref(false);
|
|
10
10
|
const saveSuccess = ref(false);
|
|
@@ -156,8 +156,10 @@ function removeTokenOverride(key: string): void {
|
|
|
156
156
|
<i class="fa-solid fa-check"></i> Saved
|
|
157
157
|
</div>
|
|
158
158
|
|
|
159
|
+
<p v-if="pending" class="admin-empty"><i class="fa-solid fa-circle-notch fa-spin"></i> Loading theme settings...</p>
|
|
160
|
+
|
|
159
161
|
<!-- Theme Families -->
|
|
160
|
-
<section class="admin-theme-families">
|
|
162
|
+
<section v-else class="admin-theme-families">
|
|
161
163
|
<div v-for="family in families" :key="family.id" class="admin-family-card" :class="{ active: activeFamily === family.id }" >
|
|
162
164
|
<button
|
|
163
165
|
class="admin-family-select"
|
|
@@ -285,7 +287,7 @@ function removeTokenOverride(key: string): void {
|
|
|
285
287
|
right: var(--space-4);
|
|
286
288
|
padding: var(--space-2) var(--space-4);
|
|
287
289
|
background: var(--green);
|
|
288
|
-
color:
|
|
290
|
+
color: var(--color-text-inverse);
|
|
289
291
|
font-size: var(--text-sm);
|
|
290
292
|
font-weight: var(--font-weight-semibold);
|
|
291
293
|
z-index: var(--z-toast);
|
|
@@ -16,9 +16,9 @@ async function handleSubmit(): Promise<void> {
|
|
|
16
16
|
loading.value = true;
|
|
17
17
|
|
|
18
18
|
try {
|
|
19
|
-
await $fetch('/api/auth/
|
|
19
|
+
await $fetch('/api/auth/request-password-reset', {
|
|
20
20
|
method: 'POST',
|
|
21
|
-
body: { email: email.value },
|
|
21
|
+
body: { email: email.value, redirectTo: '/auth/reset-password' },
|
|
22
22
|
});
|
|
23
23
|
success.value = true;
|
|
24
24
|
} catch (err: unknown) {
|
|
@@ -31,47 +31,47 @@ async function handleSubmit(): Promise<void> {
|
|
|
31
31
|
</script>
|
|
32
32
|
|
|
33
33
|
<template>
|
|
34
|
-
<div class="forgot-page">
|
|
35
|
-
<h1 class="forgot-title">Forgot Password</h1>
|
|
34
|
+
<div class="cpub-forgot-page">
|
|
35
|
+
<h1 class="cpub-forgot-title">Forgot Password</h1>
|
|
36
36
|
|
|
37
37
|
<template v-if="success">
|
|
38
|
-
<div class="forgot-success">
|
|
38
|
+
<div class="cpub-forgot-success">
|
|
39
39
|
<i class="fa-solid fa-envelope" style="font-size: 24px; color: var(--accent); margin-bottom: 12px;"></i>
|
|
40
|
-
<p class="forgot-success-text">
|
|
40
|
+
<p class="cpub-forgot-success-text">
|
|
41
41
|
If an account exists for <strong>{{ email }}</strong>, we've sent a password reset link.
|
|
42
42
|
Check your inbox and spam folder.
|
|
43
43
|
</p>
|
|
44
44
|
</div>
|
|
45
|
-
<NuxtLink to="/auth/login" class="back-link">
|
|
45
|
+
<NuxtLink to="/auth/login" class="cpub-back-link">
|
|
46
46
|
<i class="fa-solid fa-arrow-left"></i> Back to login
|
|
47
47
|
</NuxtLink>
|
|
48
48
|
</template>
|
|
49
49
|
|
|
50
50
|
<template v-else>
|
|
51
|
-
<p class="forgot-desc">Enter your email address and we'll send you a link to reset your password.</p>
|
|
51
|
+
<p class="cpub-forgot-desc">Enter your email address and we'll send you a link to reset your password.</p>
|
|
52
52
|
|
|
53
|
-
<form class="forgot-form" @submit.prevent="handleSubmit" aria-label="Forgot password form">
|
|
54
|
-
<div v-if="error" class="form-error" role="alert">{{ error }}</div>
|
|
53
|
+
<form class="cpub-forgot-form" @submit.prevent="handleSubmit" aria-label="Forgot password form">
|
|
54
|
+
<div v-if="error" class="cpub-form-error" role="alert">{{ error }}</div>
|
|
55
55
|
|
|
56
|
-
<div class="field">
|
|
57
|
-
<label for="email" class="field-label">Email</label>
|
|
56
|
+
<div class="cpub-field">
|
|
57
|
+
<label for="email" class="cpub-field-label">Email</label>
|
|
58
58
|
<input
|
|
59
59
|
id="email"
|
|
60
60
|
v-model="email"
|
|
61
61
|
type="email"
|
|
62
|
-
class="field-input"
|
|
62
|
+
class="cpub-field-input"
|
|
63
63
|
autocomplete="email"
|
|
64
64
|
required
|
|
65
65
|
placeholder="you@example.com"
|
|
66
66
|
/>
|
|
67
67
|
</div>
|
|
68
68
|
|
|
69
|
-
<button type="submit" class="submit-btn" :disabled="loading">
|
|
69
|
+
<button type="submit" class="cpub-submit-btn" :disabled="loading">
|
|
70
70
|
{{ loading ? 'Sending...' : 'Send Reset Link' }}
|
|
71
71
|
</button>
|
|
72
72
|
</form>
|
|
73
73
|
|
|
74
|
-
<p class="forgot-footer">
|
|
74
|
+
<p class="cpub-forgot-footer">
|
|
75
75
|
Remember your password?
|
|
76
76
|
<NuxtLink to="/auth/login">Log in</NuxtLink>
|
|
77
77
|
</p>
|
|
@@ -80,24 +80,24 @@ async function handleSubmit(): Promise<void> {
|
|
|
80
80
|
</template>
|
|
81
81
|
|
|
82
82
|
<style scoped>
|
|
83
|
-
.forgot-page { width: 100%; }
|
|
84
|
-
.forgot-title { font-size: 18px; font-weight: 600; margin-bottom: var(--space-3); }
|
|
85
|
-
.forgot-desc { font-size: 13px; color: var(--text-dim); margin-bottom: var(--space-5); line-height: 1.6; }
|
|
86
|
-
.forgot-form { display: flex; flex-direction: column; gap: var(--space-4); }
|
|
87
|
-
.forgot-success { text-align: center; padding: var(--space-5) 0; }
|
|
88
|
-
.forgot-success-text { font-size: 13px; color: var(--text-dim); line-height: 1.6; }
|
|
89
|
-
.back-link { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--accent); text-decoration: none; justify-content: center; margin-top: var(--space-4); }
|
|
90
|
-
.back-link:hover { text-decoration: underline; }
|
|
91
|
-
.form-error { padding: var(--space-3); background: var(--red-bg); color: var(--red); border: var(--border-width-default) solid var(--red); border-radius: var(--radius); font-size: 12px; }
|
|
92
|
-
.field { display: flex; flex-direction: column; gap: 4px; }
|
|
93
|
-
.field-label { font-size: 12px; font-weight: 500; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-dim); }
|
|
94
|
-
.field-input { padding: 8px 12px; border: var(--border-width-default) solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text); font-size: 13px; font-family: var(--font-sans); outline: none; width: 100%; transition: border-color 0.15s; }
|
|
95
|
-
.field-input::placeholder { color: var(--text-faint); }
|
|
96
|
-
.field-input:focus { border-color: var(--accent); }
|
|
97
|
-
.submit-btn { padding: 7px 14px; background: var(--accent); color: var(--color-text-inverse); border: var(--border-width-default) solid var(--accent); border-radius: var(--radius); font-size: 13px; font-weight: 500; cursor: pointer; box-shadow: var(--shadow-sm); transition: all 0.15s; }
|
|
98
|
-
.submit-btn:hover:not(:disabled) { box-shadow: var(--shadow-md); transform: translate(-1px, -1px); }
|
|
99
|
-
.submit-btn:disabled { opacity: 0.7; cursor: not-allowed; }
|
|
100
|
-
.forgot-footer { text-align: center; font-size: 12px; color: var(--text-dim); margin-top: var(--space-4); }
|
|
101
|
-
.forgot-footer a { color: var(--accent); text-decoration: none; }
|
|
102
|
-
.forgot-footer a:hover { text-decoration: underline; }
|
|
83
|
+
.cpub-forgot-page { width: 100%; }
|
|
84
|
+
.cpub-forgot-title { font-size: 18px; font-weight: 600; margin-bottom: var(--space-3); }
|
|
85
|
+
.cpub-forgot-desc { font-size: 13px; color: var(--text-dim); margin-bottom: var(--space-5); line-height: 1.6; }
|
|
86
|
+
.cpub-forgot-form { display: flex; flex-direction: column; gap: var(--space-4); }
|
|
87
|
+
.cpub-forgot-success { text-align: center; padding: var(--space-5) 0; }
|
|
88
|
+
.cpub-forgot-success-text { font-size: 13px; color: var(--text-dim); line-height: 1.6; }
|
|
89
|
+
.cpub-back-link { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--accent); text-decoration: none; justify-content: center; margin-top: var(--space-4); }
|
|
90
|
+
.cpub-back-link:hover { text-decoration: underline; }
|
|
91
|
+
.cpub-form-error { padding: var(--space-3); background: var(--red-bg); color: var(--red); border: var(--border-width-default) solid var(--red); border-radius: var(--radius); font-size: 12px; }
|
|
92
|
+
.cpub-field { display: flex; flex-direction: column; gap: 4px; }
|
|
93
|
+
.cpub-field-label { font-size: 12px; font-weight: 500; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-dim); }
|
|
94
|
+
.cpub-field-input { padding: 8px 12px; border: var(--border-width-default) solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text); font-size: 13px; font-family: var(--font-sans); outline: none; width: 100%; transition: border-color 0.15s; }
|
|
95
|
+
.cpub-field-input::placeholder { color: var(--text-faint); }
|
|
96
|
+
.cpub-field-input:focus { border-color: var(--accent); }
|
|
97
|
+
.cpub-submit-btn { padding: 7px 14px; background: var(--accent); color: var(--color-text-inverse); border: var(--border-width-default) solid var(--accent); border-radius: var(--radius); font-size: 13px; font-weight: 500; cursor: pointer; box-shadow: var(--shadow-sm); transition: all 0.15s; }
|
|
98
|
+
.cpub-submit-btn:hover:not(:disabled) { box-shadow: var(--shadow-md); transform: translate(-1px, -1px); }
|
|
99
|
+
.cpub-submit-btn:disabled { opacity: 0.7; cursor: not-allowed; }
|
|
100
|
+
.cpub-forgot-footer { text-align: center; font-size: 12px; color: var(--text-dim); margin-top: var(--space-4); }
|
|
101
|
+
.cpub-forgot-footer a { color: var(--accent); text-decoration: none; }
|
|
102
|
+
.cpub-forgot-footer a:hover { text-decoration: underline; }
|
|
103
103
|
</style>
|
package/pages/auth/login.vue
CHANGED
|
@@ -6,7 +6,7 @@ useSeoMeta({
|
|
|
6
6
|
description: 'Log in to your CommonPub account.',
|
|
7
7
|
});
|
|
8
8
|
|
|
9
|
-
const { signIn } = useAuth();
|
|
9
|
+
const { signIn, refreshSession } = useAuth();
|
|
10
10
|
const { federation } = useFeatures();
|
|
11
11
|
const route = useRoute();
|
|
12
12
|
|
|
@@ -51,12 +51,13 @@ async function handleSubmit(): Promise<void> {
|
|
|
51
51
|
});
|
|
52
52
|
await navigateTo('/dashboard');
|
|
53
53
|
} else {
|
|
54
|
-
// Normal login flow
|
|
55
|
-
|
|
54
|
+
// Normal login flow — username→email resolved server-side
|
|
55
|
+
await $fetch('/api/auth/sign-in-username', {
|
|
56
56
|
method: 'POST',
|
|
57
|
-
body: { identity: identity.value },
|
|
57
|
+
body: { identity: identity.value, password: password.value },
|
|
58
|
+
credentials: 'include',
|
|
58
59
|
});
|
|
59
|
-
await
|
|
60
|
+
await refreshSession();
|
|
60
61
|
await navigateTo(redirectTo.value);
|
|
61
62
|
}
|
|
62
63
|
} catch (err: unknown) {
|
|
@@ -8,6 +8,7 @@ useSeoMeta({
|
|
|
8
8
|
|
|
9
9
|
const route = useRoute();
|
|
10
10
|
const token = computed(() => (route.query.token as string) || '');
|
|
11
|
+
const tokenError = computed(() => (route.query.error as string) || '');
|
|
11
12
|
|
|
12
13
|
const password = ref('');
|
|
13
14
|
const confirmPassword = ref('');
|
|
@@ -49,32 +50,42 @@ async function handleSubmit(): Promise<void> {
|
|
|
49
50
|
</script>
|
|
50
51
|
|
|
51
52
|
<template>
|
|
52
|
-
<div class="reset-page">
|
|
53
|
-
<h1 class="reset-title">Reset Password</h1>
|
|
53
|
+
<div class="cpub-reset-page">
|
|
54
|
+
<h1 class="cpub-reset-title">Reset Password</h1>
|
|
54
55
|
|
|
55
56
|
<template v-if="success">
|
|
56
|
-
<div class="reset-success">
|
|
57
|
+
<div class="cpub-reset-success">
|
|
57
58
|
<i class="fa-solid fa-check-circle" style="font-size: 24px; color: var(--green); margin-bottom: 12px;"></i>
|
|
58
|
-
<p class="reset-success-text">Your password has been reset successfully.</p>
|
|
59
|
+
<p class="cpub-reset-success-text">Your password has been reset successfully.</p>
|
|
59
60
|
</div>
|
|
60
|
-
<NuxtLink to="/auth/login" class="back-link">
|
|
61
|
+
<NuxtLink to="/auth/login" class="cpub-back-link">
|
|
61
62
|
<i class="fa-solid fa-arrow-right"></i> Go to login
|
|
62
63
|
</NuxtLink>
|
|
63
64
|
</template>
|
|
64
65
|
|
|
66
|
+
<template v-else-if="tokenError">
|
|
67
|
+
<div class="cpub-reset-error-state">
|
|
68
|
+
<i class="fa-solid fa-circle-xmark" style="font-size: 24px; color: var(--red); margin-bottom: 12px;"></i>
|
|
69
|
+
<p class="cpub-reset-success-text">This reset link is invalid or has expired.</p>
|
|
70
|
+
</div>
|
|
71
|
+
<NuxtLink to="/auth/forgot-password" class="cpub-back-link">
|
|
72
|
+
<i class="fa-solid fa-arrow-left"></i> Request a new link
|
|
73
|
+
</NuxtLink>
|
|
74
|
+
</template>
|
|
75
|
+
|
|
65
76
|
<template v-else>
|
|
66
|
-
<p class="reset-desc">Enter your new password below.</p>
|
|
77
|
+
<p class="cpub-reset-desc">Enter your new password below.</p>
|
|
67
78
|
|
|
68
|
-
<form class="reset-form" @submit.prevent="handleSubmit" aria-label="Reset password form">
|
|
69
|
-
<div v-if="error" class="form-error" role="alert">{{ error }}</div>
|
|
79
|
+
<form class="cpub-reset-form" @submit.prevent="handleSubmit" aria-label="Reset password form">
|
|
80
|
+
<div v-if="error" class="cpub-form-error" role="alert">{{ error }}</div>
|
|
70
81
|
|
|
71
|
-
<div class="field">
|
|
72
|
-
<label for="password" class="field-label">New Password</label>
|
|
82
|
+
<div class="cpub-field">
|
|
83
|
+
<label for="password" class="cpub-field-label">New Password</label>
|
|
73
84
|
<input
|
|
74
85
|
id="password"
|
|
75
86
|
v-model="password"
|
|
76
87
|
type="password"
|
|
77
|
-
class="field-input"
|
|
88
|
+
class="cpub-field-input"
|
|
78
89
|
autocomplete="new-password"
|
|
79
90
|
required
|
|
80
91
|
placeholder="At least 8 characters"
|
|
@@ -82,20 +93,20 @@ async function handleSubmit(): Promise<void> {
|
|
|
82
93
|
/>
|
|
83
94
|
</div>
|
|
84
95
|
|
|
85
|
-
<div class="field">
|
|
86
|
-
<label for="confirm" class="field-label">Confirm Password</label>
|
|
96
|
+
<div class="cpub-field">
|
|
97
|
+
<label for="confirm" class="cpub-field-label">Confirm Password</label>
|
|
87
98
|
<input
|
|
88
99
|
id="confirm"
|
|
89
100
|
v-model="confirmPassword"
|
|
90
101
|
type="password"
|
|
91
|
-
class="field-input"
|
|
102
|
+
class="cpub-field-input"
|
|
92
103
|
autocomplete="new-password"
|
|
93
104
|
required
|
|
94
105
|
placeholder="Confirm your password"
|
|
95
106
|
/>
|
|
96
107
|
</div>
|
|
97
108
|
|
|
98
|
-
<button type="submit" class="submit-btn" :disabled="loading">
|
|
109
|
+
<button type="submit" class="cpub-submit-btn" :disabled="loading">
|
|
99
110
|
{{ loading ? 'Resetting...' : 'Reset Password' }}
|
|
100
111
|
</button>
|
|
101
112
|
</form>
|
|
@@ -104,21 +115,22 @@ async function handleSubmit(): Promise<void> {
|
|
|
104
115
|
</template>
|
|
105
116
|
|
|
106
117
|
<style scoped>
|
|
107
|
-
.reset-page { width: 100%; }
|
|
108
|
-
.reset-title { font-size: 18px; font-weight: 600; margin-bottom: var(--space-3); }
|
|
109
|
-
.reset-desc { font-size: 13px; color: var(--text-dim); margin-bottom: var(--space-5); line-height: 1.6; }
|
|
110
|
-
.reset-form { display: flex; flex-direction: column; gap: var(--space-4); }
|
|
111
|
-
.reset-success { text-align: center; padding: var(--space-5) 0; }
|
|
112
|
-
.reset-success-text { font-size: 13px; color: var(--text-dim); line-height: 1.6; }
|
|
113
|
-
.back-link { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--accent); text-decoration: none; justify-content: center; margin-top: var(--space-4); }
|
|
114
|
-
.back-link:hover { text-decoration: underline; }
|
|
115
|
-
.form-error { padding: var(--space-3); background: var(--red-bg); color: var(--red); border: var(--border-width-default) solid var(--red); border-radius: var(--radius); font-size: 12px; }
|
|
116
|
-
.field { display: flex; flex-direction: column; gap: 4px; }
|
|
117
|
-
.field-label { font-size: 12px; font-weight: 500; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-dim); }
|
|
118
|
-
.field-input { padding: 8px 12px; border: var(--border-width-default) solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text); font-size: 13px; font-family: var(--font-sans); outline: none; width: 100%; transition: border-color 0.15s; }
|
|
119
|
-
.field-input::placeholder { color: var(--text-faint); }
|
|
120
|
-
.field-input:focus { border-color: var(--accent); }
|
|
121
|
-
.submit-btn { padding: 7px 14px; background: var(--accent); color: var(--color-text-inverse); border: var(--border-width-default) solid var(--accent); border-radius: var(--radius); font-size: 13px; font-weight: 500; cursor: pointer; box-shadow: var(--shadow-sm); transition: all 0.15s; }
|
|
122
|
-
.submit-btn:hover:not(:disabled) { box-shadow: var(--shadow-md); transform: translate(-1px, -1px); }
|
|
123
|
-
.submit-btn:disabled { opacity: 0.7; cursor: not-allowed; }
|
|
118
|
+
.cpub-reset-page { width: 100%; }
|
|
119
|
+
.cpub-reset-title { font-size: 18px; font-weight: 600; margin-bottom: var(--space-3); }
|
|
120
|
+
.cpub-reset-desc { font-size: 13px; color: var(--text-dim); margin-bottom: var(--space-5); line-height: 1.6; }
|
|
121
|
+
.cpub-reset-form { display: flex; flex-direction: column; gap: var(--space-4); }
|
|
122
|
+
.cpub-reset-success { text-align: center; padding: var(--space-5) 0; }
|
|
123
|
+
.cpub-reset-success-text { font-size: 13px; color: var(--text-dim); line-height: 1.6; }
|
|
124
|
+
.cpub-back-link { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--accent); text-decoration: none; justify-content: center; margin-top: var(--space-4); }
|
|
125
|
+
.cpub-back-link:hover { text-decoration: underline; }
|
|
126
|
+
.cpub-form-error { padding: var(--space-3); background: var(--red-bg); color: var(--red); border: var(--border-width-default) solid var(--red); border-radius: var(--radius); font-size: 12px; }
|
|
127
|
+
.cpub-field { display: flex; flex-direction: column; gap: 4px; }
|
|
128
|
+
.cpub-field-label { font-size: 12px; font-weight: 500; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-dim); }
|
|
129
|
+
.cpub-field-input { padding: 8px 12px; border: var(--border-width-default) solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text); font-size: 13px; font-family: var(--font-sans); outline: none; width: 100%; transition: border-color 0.15s; }
|
|
130
|
+
.cpub-field-input::placeholder { color: var(--text-faint); }
|
|
131
|
+
.cpub-field-input:focus { border-color: var(--accent); }
|
|
132
|
+
.cpub-submit-btn { padding: 7px 14px; background: var(--accent); color: var(--color-text-inverse); border: var(--border-width-default) solid var(--accent); border-radius: var(--radius); font-size: 13px; font-weight: 500; cursor: pointer; box-shadow: var(--shadow-sm); transition: all 0.15s; }
|
|
133
|
+
.cpub-submit-btn:hover:not(:disabled) { box-shadow: var(--shadow-md); transform: translate(-1px, -1px); }
|
|
134
|
+
.cpub-submit-btn:disabled { opacity: 0.7; cursor: not-allowed; }
|
|
135
|
+
.cpub-reset-error-state { text-align: center; padding: var(--space-5) 0; }
|
|
124
136
|
</style>
|