Compare commits
No commits in common. "1c3273cee2ea7808a2fc3e0f5a2ae126945f2246" and "6a361b68579c653be017c70b579b87311e5bfbb3" have entirely different histories.
1c3273cee2
...
6a361b6857
10 changed files with 39 additions and 30 deletions
|
|
@ -88,7 +88,19 @@ defineEmits(['edit', 'delete', 'confirm-delete', 'cancel-delete'])
|
||||||
|
|
||||||
const { slackUrl } = useBoardChannels()
|
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 || '')
|
const slackHandle = computed(() => props.post.author?.board?.slackHandle || '')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,8 @@
|
||||||
</dl>
|
</dl>
|
||||||
</DashedBox>
|
</DashedBox>
|
||||||
<p class="signup-flow-body" style="margin-top: 16px">
|
<p class="signup-flow-body" style="margin-top: 16px">
|
||||||
{{ 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.
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -61,7 +62,6 @@ 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"]);
|
||||||
|
|
|
||||||
|
|
@ -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="ghostieImagePath(memberData.avatar)"
|
:src="`/ghosties/Ghost-${capitalize(memberData.avatar)}.png`"
|
||||||
:alt="memberData.name"
|
:alt="memberData.name"
|
||||||
class="member-avatar"
|
class="member-avatar"
|
||||||
>
|
>
|
||||||
|
|
@ -86,6 +86,11 @@ 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(" / ");
|
||||||
|
|
|
||||||
|
|
@ -229,7 +229,6 @@
|
||||||
: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>
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
<div class="profile-avatar">
|
<div class="profile-avatar">
|
||||||
<img
|
<img
|
||||||
v-if="member.avatar"
|
v-if="member.avatar"
|
||||||
:src="ghostieImagePath(member.avatar)"
|
:src="`/ghosties/Ghost-${member.avatar.charAt(0).toUpperCase() + member.avatar.slice(1)}.png`"
|
||||||
:alt="member.name"
|
:alt="member.name"
|
||||||
class="profile-avatar-img"
|
class="profile-avatar-img"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@
|
||||||
<div class="mc-avatar">
|
<div class="mc-avatar">
|
||||||
<img
|
<img
|
||||||
v-if="member.avatar"
|
v-if="member.avatar"
|
||||||
:src="ghostieImagePath(member.avatar)"
|
:src="`/ghosties/Ghost-${capitalize(member.avatar)}.png`"
|
||||||
:alt="member.name"
|
:alt="member.name"
|
||||||
class="mc-avatar-img"
|
class="mc-avatar-img"
|
||||||
>
|
>
|
||||||
|
|
@ -185,6 +185,14 @@ 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)
|
||||||
|
|
|
||||||
|
|
@ -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`
|
|
||||||
}
|
|
||||||
|
|
@ -23,7 +23,6 @@ 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",
|
||||||
|
|
|
||||||
|
|
@ -68,14 +68,12 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
await assignMemberNumber(member._id)
|
await assignMemberNumber(member._id)
|
||||||
|
|
||||||
// Update pre-registration. Burning the jti here (instead of in verify) keeps
|
// Update pre-registration
|
||||||
// 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,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,16 @@ 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' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match the jti so that re-invite (which rotates the jti) kills old links.
|
// Single-use enforcement
|
||||||
// The burn happens in accept.post.js once a Member is created — keeps verify
|
if (!decoded.jti || decoded.jti !== preReg.magicLinkJti || preReg.magicLinkJtiUsed) {
|
||||||
// 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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue