Compare commits
2 commits
6a361b6857
...
1c3273cee2
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c3273cee2 | |||
| 246f2023bc |
10 changed files with 30 additions and 39 deletions
|
|
@ -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 || '')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"]);
|
||||||
|
|
|
||||||
|
|
@ -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(" / ");
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
15
app/utils/ghostieAvatar.js
Normal file
15
app/utils/ghostieAvatar.js
Normal 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`
|
||||||
|
}
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue