diff --git a/app/components/BoardPostCard.vue b/app/components/BoardPostCard.vue index 4e32218..a79a535 100644 --- a/app/components/BoardPostCard.vue +++ b/app/components/BoardPostCard.vue @@ -88,7 +88,19 @@ defineEmits(['edit', 'delete', 'confirm-delete', 'cancel-delete']) const { slackUrl } = useBoardChannels() -const authorAvatar = computed(() => ghostieImagePath(props.post.author?.avatar)) +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 slackHandle = computed(() => props.post.author?.board?.slackHandle || '') diff --git a/app/components/SignupFlowOverlay.vue b/app/components/SignupFlowOverlay.vue index 73f8c09..10fe663 100644 --- a/app/components/SignupFlowOverlay.vue +++ b/app/components/SignupFlowOverlay.vue @@ -33,7 +33,8 @@

- {{ successMessage || `Check ${summary?.email} for a sign-in link to finish setting up your account. The link expires in 15 minutes.` }} + Check {{ summary?.email }} for a sign-in link to finish setting up + your account. The link expires in 15 minutes.

@@ -61,7 +62,6 @@ 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 b00915e..37a8aec 100644 --- a/app/components/TopStrip.vue +++ b/app/components/TopStrip.vue @@ -29,7 +29,7 @@ @@ -86,6 +86,11 @@ 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 0a28074..d618b26 100644 --- a/app/pages/accept-invite.vue +++ b/app/pages/accept-invite.vue @@ -229,7 +229,6 @@ :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 3d61a3e..5b66b1c 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 dab5837..c387144 100644 --- a/app/pages/members/index.vue +++ b/app/pages/members/index.vue @@ -89,7 +89,7 @@
@@ -185,6 +185,14 @@ 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 deleted file mode 100644 index 85de750..0000000 --- a/app/utils/ghostieAvatar.js +++ /dev/null @@ -1,15 +0,0 @@ -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/nuxt.config.ts b/nuxt.config.ts index 349dc75..ab9ad7d 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -23,7 +23,6 @@ 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", diff --git a/server/api/invite/accept.post.js b/server/api/invite/accept.post.js index 2d6518e..27e5109 100644 --- a/server/api/invite/accept.post.js +++ b/server/api/invite/accept.post.js @@ -68,14 +68,12 @@ export default defineEventHandler(async (event) => { await assignMemberNumber(member._id) - // 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. + // Update pre-registration 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 0baaa9d..8d75c72 100644 --- a/server/api/invite/verify.post.js +++ b/server/api/invite/verify.post.js @@ -27,13 +27,16 @@ export default defineEventHandler(async (event) => { throw createError({ statusCode: 400, statusMessage: 'This invitation has already been accepted' }) } - // 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) { + // Single-use enforcement + if (!decoded.jti || decoded.jti !== preReg.magicLinkJti || preReg.magicLinkJtiUsed) { 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,