From 246f2023bc93528faafd3b5878b24f7b1a8af998 Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Fri, 22 May 2026 14:58:57 +0100 Subject: [PATCH 1/2] Add a sweet ghostie favicon --- nuxt.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/nuxt.config.ts b/nuxt.config.ts index ab9ad7d..349dc75 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -23,6 +23,7 @@ export default defineNuxtConfig({ title: "Ghost Guild", titleTemplate: "%s · Ghost Guild", link: [ + { rel: "icon", type: "image/svg+xml", href: "/ghosties/Ghost-Sweet.svg" }, { rel: "preconnect", href: "https://fonts.googleapis.com" }, { rel: "preconnect", From 1c3273cee2ea7808a2fc3e0f5a2ae126945f2246 Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Fri, 22 May 2026 18:53:07 +0100 Subject: [PATCH 2/2] Various pre-launch fixes. --- app/components/BoardPostCard.vue | 14 +------------- app/components/SignupFlowOverlay.vue | 4 ++-- app/components/TopStrip.vue | 7 +------ app/pages/accept-invite.vue | 1 + app/pages/members/[id].vue | 2 +- app/pages/members/index.vue | 10 +--------- app/utils/ghostieAvatar.js | 15 +++++++++++++++ server/api/invite/accept.post.js | 4 +++- server/api/invite/verify.post.js | 11 ++++------- 9 files changed, 29 insertions(+), 39 deletions(-) create mode 100644 app/utils/ghostieAvatar.js diff --git a/app/components/BoardPostCard.vue b/app/components/BoardPostCard.vue index a79a535..4e32218 100644 --- a/app/components/BoardPostCard.vue +++ b/app/components/BoardPostCard.vue @@ -88,19 +88,7 @@ defineEmits(['edit', 'delete', 'confirm-delete', 'cancel-delete']) const { slackUrl } = useBoardChannels() -const capitalizeAvatar = (str) => { - if (str.toLowerCase() === 'wtf') return 'WTF' - return str - .split('-') - .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()) - .join('-') -} - -const authorAvatar = computed(() => { - const a = props.post.author?.avatar - if (!a) return null - return `/ghosties/Ghost-${capitalizeAvatar(a)}.png` -}) +const authorAvatar = computed(() => ghostieImagePath(props.post.author?.avatar)) const slackHandle = computed(() => props.post.author?.board?.slackHandle || '') diff --git a/app/components/SignupFlowOverlay.vue b/app/components/SignupFlowOverlay.vue index 10fe663..73f8c09 100644 --- a/app/components/SignupFlowOverlay.vue +++ b/app/components/SignupFlowOverlay.vue @@ -33,8 +33,7 @@ @@ -62,6 +61,7 @@ const props = defineProps({ summary: { type: Object, default: null }, errorMessage: { type: String, default: "" }, dashboardHref: { type: String, default: "/welcome" }, + successMessage: { type: String, default: "" }, }); defineEmits(["close"]); diff --git a/app/components/TopStrip.vue b/app/components/TopStrip.vue index 37a8aec..b00915e 100644 --- a/app/components/TopStrip.vue +++ b/app/components/TopStrip.vue @@ -29,7 +29,7 @@ @@ -86,11 +86,6 @@ const handleLogout = async () => { navigateTo("/"); }; -const capitalize = (str) => { - if (!str) return ""; - return str.charAt(0).toUpperCase() + str.slice(1); -}; - const breadcrumbs = computed(() => { if (!props.pagePath) return []; const segments = props.pagePath.split(" / "); diff --git a/app/pages/accept-invite.vue b/app/pages/accept-invite.vue index d618b26..0a28074 100644 --- a/app/pages/accept-invite.vue +++ b/app/pages/accept-invite.vue @@ -229,6 +229,7 @@ :summary="flowSummary" :error-message="errorMessage" dashboard-href="/member/dashboard?welcome=1" + success-message="You're signed in. Taking you to your dashboard..." @close="closeFlowOverlay" /> diff --git a/app/pages/members/[id].vue b/app/pages/members/[id].vue index 5b66b1c..3d61a3e 100644 --- a/app/pages/members/[id].vue +++ b/app/pages/members/[id].vue @@ -23,7 +23,7 @@
diff --git a/app/pages/members/index.vue b/app/pages/members/index.vue index c387144..dab5837 100644 --- a/app/pages/members/index.vue +++ b/app/pages/members/index.vue @@ -89,7 +89,7 @@
@@ -185,14 +185,6 @@ const getInitials = (name) => { .slice(0, 2) } -const capitalize = (str) => { - if (!str) return '' - return str - .split('-') - .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()) - .join('-') -} - // ---- Computed ---- const visibleTagOptions = computed(() => showAllTags.value ? craftTagOptions.value : craftTagOptions.value.slice(0, 10) diff --git a/app/utils/ghostieAvatar.js b/app/utils/ghostieAvatar.js new file mode 100644 index 0000000..85de750 --- /dev/null +++ b/app/utils/ghostieAvatar.js @@ -0,0 +1,15 @@ +const AVATAR_FILE_NAMES = { + 'sweet': 'Sweet', + 'mild': 'Mild', + 'exasperated': 'Exasperated', + 'disbelieving': 'Disbelieving', + 'double-take': 'Double-Take', + 'wtf': 'WTF', +} + +export function ghostieImagePath(avatar) { + if (!avatar) return null + const name = AVATAR_FILE_NAMES[String(avatar).toLowerCase()] + if (!name) return null + return `/ghosties/Ghost-${name}.png` +} diff --git a/server/api/invite/accept.post.js b/server/api/invite/accept.post.js index 27e5109..2d6518e 100644 --- a/server/api/invite/accept.post.js +++ b/server/api/invite/accept.post.js @@ -68,12 +68,14 @@ export default defineEventHandler(async (event) => { await assignMemberNumber(member._id) - // Update pre-registration + // Update pre-registration. Burning the jti here (instead of in verify) keeps + // the verify endpoint idempotent so a page refresh doesn't lock the invitee out. await PreRegistration.findByIdAndUpdate(preReg._id, { $set: { status: 'accepted', acceptedAt: new Date(), memberId: member._id, + magicLinkJtiUsed: true, } }) diff --git a/server/api/invite/verify.post.js b/server/api/invite/verify.post.js index 8d75c72..0baaa9d 100644 --- a/server/api/invite/verify.post.js +++ b/server/api/invite/verify.post.js @@ -27,16 +27,13 @@ export default defineEventHandler(async (event) => { throw createError({ statusCode: 400, statusMessage: 'This invitation has already been accepted' }) } - // Single-use enforcement - if (!decoded.jti || decoded.jti !== preReg.magicLinkJti || preReg.magicLinkJtiUsed) { + // Match the jti so that re-invite (which rotates the jti) kills old links. + // The burn happens in accept.post.js once a Member is created — keeps verify + // idempotent so the form survives a refresh. + if (!decoded.jti || decoded.jti !== preReg.magicLinkJti) { throw createError({ statusCode: 401, statusMessage: 'Invalid or expired invitation link' }) } - // Burn the token - await PreRegistration.findByIdAndUpdate(preReg._id, { - $set: { magicLinkJtiUsed: true } - }) - return { preRegistrationId: preReg._id, name: preReg.name,