Compare commits

..

2 commits

Author SHA1 Message Date
1c3273cee2 Various pre-launch fixes.
Some checks failed
Test / vitest (push) Successful in 14m0s
Test / playwright (push) Failing after 20m2s
Test / Notify on failure (push) Successful in 3s
2026-05-22 18:53:07 +01:00
246f2023bc Add a sweet ghostie favicon 2026-05-22 14:58:57 +01:00
10 changed files with 30 additions and 39 deletions

View file

@ -88,19 +88,7 @@ defineEmits(['edit', 'delete', 'confirm-delete', 'cancel-delete'])
const { slackUrl } = useBoardChannels() const { slackUrl } = useBoardChannels()
const capitalizeAvatar = (str) => { const authorAvatar = computed(() => ghostieImagePath(props.post.author?.avatar))
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 || '') const slackHandle = computed(() => props.post.author?.board?.slackHandle || '')

View file

@ -33,8 +33,7 @@
</dl> </dl>
</DashedBox> </DashedBox>
<p class="signup-flow-body" style="margin-top: 16px"> <p class="signup-flow-body" style="margin-top: 16px">
Check {{ summary?.email }} for a sign-in link to finish setting up {{ successMessage || `Check ${summary?.email} for a sign-in link to finish setting up your account. The link expires in 15 minutes.` }}
your account. The link expires in 15 minutes.
</p> </p>
</template> </template>
@ -62,6 +61,7 @@ const props = defineProps({
summary: { type: Object, default: null }, summary: { type: Object, default: null },
errorMessage: { type: String, default: "" }, errorMessage: { type: String, default: "" },
dashboardHref: { type: String, default: "/welcome" }, dashboardHref: { type: String, default: "/welcome" },
successMessage: { type: String, default: "" },
}); });
defineEmits(["close"]); defineEmits(["close"]);

View file

@ -29,7 +29,7 @@
<NuxtLink to="/member/profile" class="member-link"> <NuxtLink to="/member/profile" class="member-link">
<img <img
v-if="memberData.avatar" v-if="memberData.avatar"
:src="`/ghosties/Ghost-${capitalize(memberData.avatar)}.png`" :src="ghostieImagePath(memberData.avatar)"
:alt="memberData.name" :alt="memberData.name"
class="member-avatar" class="member-avatar"
> >
@ -86,11 +86,6 @@ const handleLogout = async () => {
navigateTo("/"); navigateTo("/");
}; };
const capitalize = (str) => {
if (!str) return "";
return str.charAt(0).toUpperCase() + str.slice(1);
};
const breadcrumbs = computed(() => { const breadcrumbs = computed(() => {
if (!props.pagePath) return []; if (!props.pagePath) return [];
const segments = props.pagePath.split(" / "); const segments = props.pagePath.split(" / ");

View file

@ -229,6 +229,7 @@
:summary="flowSummary" :summary="flowSummary"
:error-message="errorMessage" :error-message="errorMessage"
dashboard-href="/member/dashboard?welcome=1" dashboard-href="/member/dashboard?welcome=1"
success-message="You're signed in. Taking you to your dashboard..."
@close="closeFlowOverlay" @close="closeFlowOverlay"
/> />
</div> </div>

View file

@ -23,7 +23,7 @@
<div class="profile-avatar"> <div class="profile-avatar">
<img <img
v-if="member.avatar" v-if="member.avatar"
:src="`/ghosties/Ghost-${member.avatar.charAt(0).toUpperCase() + member.avatar.slice(1)}.png`" :src="ghostieImagePath(member.avatar)"
:alt="member.name" :alt="member.name"
class="profile-avatar-img" class="profile-avatar-img"
/> />

View file

@ -89,7 +89,7 @@
<div class="mc-avatar"> <div class="mc-avatar">
<img <img
v-if="member.avatar" v-if="member.avatar"
:src="`/ghosties/Ghost-${capitalize(member.avatar)}.png`" :src="ghostieImagePath(member.avatar)"
:alt="member.name" :alt="member.name"
class="mc-avatar-img" class="mc-avatar-img"
> >
@ -185,14 +185,6 @@ const getInitials = (name) => {
.slice(0, 2) .slice(0, 2)
} }
const capitalize = (str) => {
if (!str) return ''
return str
.split('-')
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
.join('-')
}
// ---- Computed ---- // ---- Computed ----
const visibleTagOptions = computed(() => const visibleTagOptions = computed(() =>
showAllTags.value ? craftTagOptions.value : craftTagOptions.value.slice(0, 10) showAllTags.value ? craftTagOptions.value : craftTagOptions.value.slice(0, 10)

View file

@ -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`
}

View file

@ -23,6 +23,7 @@ export default defineNuxtConfig({
title: "Ghost Guild", title: "Ghost Guild",
titleTemplate: "%s · Ghost Guild", titleTemplate: "%s · Ghost Guild",
link: [ link: [
{ rel: "icon", type: "image/svg+xml", href: "/ghosties/Ghost-Sweet.svg" },
{ rel: "preconnect", href: "https://fonts.googleapis.com" }, { rel: "preconnect", href: "https://fonts.googleapis.com" },
{ {
rel: "preconnect", rel: "preconnect",

View file

@ -68,12 +68,14 @@ export default defineEventHandler(async (event) => {
await assignMemberNumber(member._id) 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, { await PreRegistration.findByIdAndUpdate(preReg._id, {
$set: { $set: {
status: 'accepted', status: 'accepted',
acceptedAt: new Date(), acceptedAt: new Date(),
memberId: member._id, memberId: member._id,
magicLinkJtiUsed: true,
} }
}) })

View file

@ -27,16 +27,13 @@ export default defineEventHandler(async (event) => {
throw createError({ statusCode: 400, statusMessage: 'This invitation has already been accepted' }) throw createError({ statusCode: 400, statusMessage: 'This invitation has already been accepted' })
} }
// Single-use enforcement // Match the jti so that re-invite (which rotates the jti) kills old links.
if (!decoded.jti || decoded.jti !== preReg.magicLinkJti || preReg.magicLinkJtiUsed) { // 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' }) throw createError({ statusCode: 401, statusMessage: 'Invalid or expired invitation link' })
} }
// Burn the token
await PreRegistration.findByIdAndUpdate(preReg._id, {
$set: { magicLinkJtiUsed: true }
})
return { return {
preRegistrationId: preReg._id, preRegistrationId: preReg._id,
name: preReg.name, name: preReg.name,