fix: accessibility improvements and test infrastructure hardening
Add aria-labels to form controls (selects, checkboxes, switches), set html lang attribute and page title, fix color contrast for --candle-dim and --text-faint tokens, underline inline links, remove opacity hack. Harden dev login endpoints with atomic findOneAndUpdate and tokenVersion in JWT. Update Playwright timeouts and E2E test helpers.
|
|
@ -20,13 +20,13 @@
|
||||||
--border: #b8a880;
|
--border: #b8a880;
|
||||||
--border-d: #a89470;
|
--border-d: #a89470;
|
||||||
--candle: #7a5a10;
|
--candle: #7a5a10;
|
||||||
--candle-dim: #9a7420;
|
--candle-dim: #866518;
|
||||||
--candle-faint: #c4a448;
|
--candle-faint: #c4a448;
|
||||||
--ember: #8a4420;
|
--ember: #8a4420;
|
||||||
--text: #2a2015;
|
--text: #2a2015;
|
||||||
--text-bright: #1a1008;
|
--text-bright: #1a1008;
|
||||||
--text-dim: #5a5040;
|
--text-dim: #5a5040;
|
||||||
--text-faint: #8a7e6a;
|
--text-faint: #746a58;
|
||||||
--parch: #2a2015;
|
--parch: #2a2015;
|
||||||
--parch-hover: #3a3025;
|
--parch-hover: #3a3025;
|
||||||
--parch-text: #ede4d0;
|
--parch-text: #ede4d0;
|
||||||
|
|
@ -89,6 +89,7 @@ body {
|
||||||
|
|
||||||
a { color: var(--candle); text-decoration: none; }
|
a { color: var(--candle); text-decoration: none; }
|
||||||
a:hover { text-decoration: underline; }
|
a:hover { text-decoration: underline; }
|
||||||
|
p a, blockquote a { text-decoration: underline; text-underline-offset: 2px; }
|
||||||
|
|
||||||
/* ---- SECTION LABELS ---- */
|
/* ---- SECTION LABELS ---- */
|
||||||
.section-label {
|
.section-label {
|
||||||
|
|
|
||||||
|
|
@ -106,21 +106,13 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const isMobileMenuOpen = ref(false)
|
const isMobileMenuOpen = ref(false)
|
||||||
|
const { logout } = useAuth()
|
||||||
|
|
||||||
const currentPageName = computed(() => {
|
const currentPageName = computed(() => {
|
||||||
const path = route.path
|
const path = route.path
|
||||||
if (path === '/admin') return 'admin'
|
if (path === '/admin') return 'admin'
|
||||||
return path.slice(1).replace(/\//g, ' / ')
|
return path.slice(1).replace(/\//g, ' / ')
|
||||||
})
|
})
|
||||||
|
|
||||||
const logout = async () => {
|
|
||||||
try {
|
|
||||||
await $fetch('/api/auth/logout', { method: 'POST' })
|
|
||||||
await navigateTo('/')
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Logout failed:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@
|
||||||
</label>
|
</label>
|
||||||
<USelect
|
<USelect
|
||||||
v-model="eventForm.eventType"
|
v-model="eventForm.eventType"
|
||||||
|
aria-label="Event type"
|
||||||
:items="[
|
:items="[
|
||||||
{ label: 'Community Meetup', value: 'community' },
|
{ label: 'Community Meetup', value: 'community' },
|
||||||
{ label: 'Workshop', value: 'workshop' },
|
{ label: 'Workshop', value: 'workshop' },
|
||||||
|
|
@ -386,6 +387,7 @@
|
||||||
<div class="series-select-row">
|
<div class="series-select-row">
|
||||||
<USelect
|
<USelect
|
||||||
v-model="selectedSeriesId"
|
v-model="selectedSeriesId"
|
||||||
|
aria-label="Select series"
|
||||||
@update:model-value="onSeriesSelect"
|
@update:model-value="onSeriesSelect"
|
||||||
:items="
|
:items="
|
||||||
availableSeries.map((series) => ({
|
availableSeries.map((series) => ({
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="field" style="margin-bottom: 0;">
|
<div class="field" style="margin-bottom: 0;">
|
||||||
<select v-model="circleFilter">
|
<select v-model="circleFilter" aria-label="Filter by circle">
|
||||||
<option value="">All Circles</option>
|
<option value="">All Circles</option>
|
||||||
<option value="community">Community</option>
|
<option value="community">Community</option>
|
||||||
<option value="founder">Founder</option>
|
<option value="founder">Founder</option>
|
||||||
|
|
@ -55,6 +55,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th class="col-check">
|
<th class="col-check">
|
||||||
<UCheckbox
|
<UCheckbox
|
||||||
|
aria-label="Select all members"
|
||||||
:model-value="allVisibleSelected ? true : (someVisibleSelected ? 'indeterminate' : false)"
|
:model-value="allVisibleSelected ? true : (someVisibleSelected ? 'indeterminate' : false)"
|
||||||
@update:model-value="toggleSelectAll"
|
@update:model-value="toggleSelectAll"
|
||||||
/>
|
/>
|
||||||
|
|
@ -73,6 +74,7 @@
|
||||||
<tr v-for="member in filteredMembers" :key="member._id">
|
<tr v-for="member in filteredMembers" :key="member._id">
|
||||||
<td class="col-check">
|
<td class="col-check">
|
||||||
<UCheckbox
|
<UCheckbox
|
||||||
|
:aria-label="`Select ${member.name}`"
|
||||||
:model-value="selectedMemberIds.includes(member._id)"
|
:model-value="selectedMemberIds.includes(member._id)"
|
||||||
@update:model-value="toggleSelect(member._id)"
|
@update:model-value="toggleSelect(member._id)"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@
|
||||||
|
|
||||||
<!-- PARCHMENT INSET -->
|
<!-- PARCHMENT INSET -->
|
||||||
<ParchmentInset>
|
<ParchmentInset>
|
||||||
<div class="label" style="color: var(--candle-faint); opacity: 0.6; margin-bottom: 12px;">From the Wiki</div>
|
<div class="label" style="color: var(--candle-faint); margin-bottom: 12px;">From the Wiki</div>
|
||||||
<h2>What is a cooperative studio?</h2>
|
<h2>What is a cooperative studio?</h2>
|
||||||
<p>A cooperative studio is a game development company owned and governed by the people who work there. Decisions are made collectively. Profits are shared according to contribution, not ownership stake.</p>
|
<p>A cooperative studio is a game development company owned and governed by the people who work there. Decisions are made collectively. Profits are shared according to contribution, not ownership stake.</p>
|
||||||
<p>The games industry is full of stories about crunch, layoffs, and studios that extract value from workers. Cooperatives are one alternative — not the only one, but one worth <a href="/wiki">practicing together</a>.</p>
|
<p>The games industry is full of stories about crunch, layoffs, and studios that extract value from workers. Cooperatives are one alternative — not the only one, but one worth <a href="/wiki">practicing together</a>.</p>
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@
|
||||||
<div class="section-label">Visibility</div>
|
<div class="section-label">Visibility</div>
|
||||||
|
|
||||||
<div class="toggle-field">
|
<div class="toggle-field">
|
||||||
<USwitch v-model="formData.showInDirectory" />
|
<USwitch v-model="formData.showInDirectory" aria-label="Show in Member Directory" />
|
||||||
<div class="toggle-label">
|
<div class="toggle-label">
|
||||||
Show in Member Directory
|
Show in Member Directory
|
||||||
<span class="toggle-sub">Your profile will appear in the public member listing</span>
|
<span class="toggle-sub">Your profile will appear in the public member listing</span>
|
||||||
|
|
@ -162,7 +162,7 @@
|
||||||
<div class="section-label">Peer Support</div>
|
<div class="section-label">Peer Support</div>
|
||||||
|
|
||||||
<div class="toggle-field">
|
<div class="toggle-field">
|
||||||
<USwitch v-model="formData.peerSupportEnabled" />
|
<USwitch v-model="formData.peerSupportEnabled" aria-label="Offer Peer Support" />
|
||||||
<div class="toggle-label">
|
<div class="toggle-label">
|
||||||
Offer Peer Support
|
Offer Peer Support
|
||||||
<span class="toggle-sub">Let other members request 1:1 time with you</span>
|
<span class="toggle-sub">Let other members request 1:1 time with you</span>
|
||||||
|
|
@ -223,7 +223,7 @@
|
||||||
<div class="section-label">Notifications</div>
|
<div class="section-label">Notifications</div>
|
||||||
|
|
||||||
<div class="toggle-field">
|
<div class="toggle-field">
|
||||||
<USwitch v-model="formData.notifyEvents" />
|
<USwitch v-model="formData.notifyEvents" aria-label="Event reminders" />
|
||||||
<div class="toggle-label">
|
<div class="toggle-label">
|
||||||
Event reminders
|
Event reminders
|
||||||
<span class="toggle-sub">Get notified about upcoming events</span>
|
<span class="toggle-sub">Get notified about upcoming events</span>
|
||||||
|
|
@ -231,7 +231,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toggle-field">
|
<div class="toggle-field">
|
||||||
<USwitch v-model="formData.notifyUpdates" />
|
<USwitch v-model="formData.notifyUpdates" aria-label="Community updates" />
|
||||||
<div class="toggle-label">
|
<div class="toggle-label">
|
||||||
Community updates
|
Community updates
|
||||||
<span class="toggle-sub">New posts from members you follow</span>
|
<span class="toggle-sub">New posts from members you follow</span>
|
||||||
|
|
@ -239,7 +239,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toggle-field">
|
<div class="toggle-field">
|
||||||
<USwitch v-model="formData.notifyPeerRequests" />
|
<USwitch v-model="formData.notifyPeerRequests" aria-label="Peer support requests" />
|
||||||
<div class="toggle-label">
|
<div class="toggle-label">
|
||||||
Peer support requests
|
Peer support requests
|
||||||
<span class="toggle-sub">When someone wants to connect</span>
|
<span class="toggle-sub">When someone wants to connect</span>
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
|
@ -100,13 +100,16 @@ test.describe('keyboard navigation', () => {
|
||||||
|
|
||||||
test('escape closes login modal', async ({ page }) => {
|
test('escape closes login modal', async ({ page }) => {
|
||||||
await page.goto('/member/dashboard')
|
await page.goto('/member/dashboard')
|
||||||
// Wait for login modal to appear
|
// Wait for the sign-in prompt to appear, then click to open the modal
|
||||||
const modal = page.locator('text=Sign in to continue').or(page.locator('text=Sign in to your dashboard'))
|
await expect(page.getByRole('button', { name: 'Sign In' })).toBeVisible({ timeout: 10000 })
|
||||||
await expect(modal.first()).toBeVisible({ timeout: 10000 })
|
await page.getByRole('button', { name: 'Sign In' }).click()
|
||||||
|
|
||||||
|
const modalTitle = page.locator('.modal-title')
|
||||||
|
await expect(modalTitle).toBeVisible({ timeout: 5000 })
|
||||||
|
|
||||||
await page.keyboard.press('Escape')
|
await page.keyboard.press('Escape')
|
||||||
|
|
||||||
// Modal should close
|
// Modal should close
|
||||||
await expect(modal.first()).not.toBeVisible({ timeout: 5000 })
|
await expect(modalTitle).not.toBeVisible({ timeout: 5000 })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,19 @@ test.describe('Admin members page', () => {
|
||||||
await adminPage.goto('/admin/members')
|
await adminPage.goto('/admin/members')
|
||||||
|
|
||||||
const searchInput = adminPage.getByPlaceholder('Search members...')
|
const searchInput = adminPage.getByPlaceholder('Search members...')
|
||||||
await expect(searchInput).toBeVisible()
|
await expect(searchInput).toBeVisible({ timeout: 10000 })
|
||||||
|
|
||||||
|
// Wait for the initial member list to load before searching
|
||||||
|
await expect(
|
||||||
|
adminPage.locator('table').or(adminPage.getByText('No members found matching your criteria'))
|
||||||
|
).toBeVisible({ timeout: 15000 })
|
||||||
|
|
||||||
await searchInput.fill('nonexistent-query-xyz')
|
await searchInput.fill('nonexistent-query-xyz')
|
||||||
|
|
||||||
// Page should not crash -- either shows filtered results or the empty state
|
// Page should not crash -- either shows filtered results or the empty state
|
||||||
await expect(
|
await expect(
|
||||||
adminPage.locator('table').or(adminPage.getByText('No members found matching your criteria'))
|
adminPage.locator('table').or(adminPage.getByText('No members found matching your criteria'))
|
||||||
).toBeVisible()
|
).toBeVisible({ timeout: 10000 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('non-admin redirect', async ({ browser }) => {
|
test('non-admin redirect', async ({ browser }) => {
|
||||||
|
|
@ -38,11 +43,16 @@ test.describe('Admin members page', () => {
|
||||||
test('add member button opens modal', async ({ adminPage }) => {
|
test('add member button opens modal', async ({ adminPage }) => {
|
||||||
await adminPage.goto('/admin/members')
|
await adminPage.goto('/admin/members')
|
||||||
|
|
||||||
await adminPage.getByRole('button', { name: 'Add Member' }).click()
|
// Wait for page to fully load and hydrate
|
||||||
|
await expect(adminPage.locator('h1')).toHaveText('Members')
|
||||||
|
await adminPage.waitForLoadState('networkidle')
|
||||||
|
|
||||||
|
const addBtn = adminPage.getByRole('button', { name: 'Add Member' })
|
||||||
|
await expect(addBtn).toBeVisible({ timeout: 10000 })
|
||||||
|
await addBtn.click()
|
||||||
|
|
||||||
// Modal should appear with the form heading and fields
|
// Modal should appear with the form heading and fields
|
||||||
await expect(adminPage.getByText('Add New Member')).toBeVisible()
|
await expect(adminPage.getByPlaceholder('Full name')).toBeVisible({ timeout: 10000 })
|
||||||
await expect(adminPage.getByPlaceholder('Full name')).toBeVisible()
|
|
||||||
await expect(adminPage.getByPlaceholder('email@example.com')).toBeVisible()
|
await expect(adminPage.getByPlaceholder('email@example.com')).toBeVisible()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,53 +2,57 @@ import { test, expect } from '@playwright/test'
|
||||||
import { loginAsAdmin, loginAsMember } from './helpers/auth.js'
|
import { loginAsAdmin, loginAsMember } from './helpers/auth.js'
|
||||||
|
|
||||||
test.describe('Authentication flows', () => {
|
test.describe('Authentication flows', () => {
|
||||||
test('protected page shows login modal when logged out', async ({ page }) => {
|
test('protected page shows sign-in prompt when logged out', async ({ page }) => {
|
||||||
// Navigate to a protected member page without being logged in
|
// Navigate to a protected member page without being logged in
|
||||||
await page.goto('/member/dashboard')
|
await page.goto('/member/dashboard')
|
||||||
|
|
||||||
// The auth middleware aborts navigation and shows the login modal
|
// Page shows the unauth state with sign-in button
|
||||||
// Look for the modal title and email input
|
await expect(page.getByRole('heading', { name: 'Sign in required' })).toBeVisible({ timeout: 10000 })
|
||||||
await expect(page.getByText('Sign in to continue')).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Sign In' })).toBeVisible()
|
||||||
|
|
||||||
|
// Clicking Sign In opens the login modal with email input
|
||||||
|
await page.getByRole('button', { name: 'Sign In' }).click()
|
||||||
|
await expect(page.locator('.modal-title')).toBeVisible({ timeout: 5000 })
|
||||||
await expect(page.locator('input[type="email"]')).toBeVisible()
|
await expect(page.locator('input[type="email"]')).toBeVisible()
|
||||||
await expect(page.getByRole('button', { name: 'Send magic link' })).toBeVisible()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('admin login and redirect', async ({ page }) => {
|
test('admin login sets auth cookie', async ({ page }) => {
|
||||||
await loginAsAdmin(page)
|
await loginAsAdmin(page)
|
||||||
|
|
||||||
// loginAsAdmin waits for /admin URL
|
// Verify cookie was set
|
||||||
await expect(page).toHaveURL(/\/admin/)
|
const cookies = await page.context().cookies()
|
||||||
|
const authCookie = cookies.find((c) => c.name === 'auth-token')
|
||||||
|
expect(authCookie).toBeTruthy()
|
||||||
|
|
||||||
// Admin layout should show admin sidebar content
|
// Navigate to admin page — should show admin layout
|
||||||
await expect(page.locator('.sidebar-nav').getByText('Members')).toBeVisible()
|
await page.goto('/admin')
|
||||||
await expect(page.locator('.admin-tag')).toBeVisible()
|
await expect(page.locator('.admin-tag')).toBeVisible({ timeout: 15000 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('member login and redirect', async ({ page }) => {
|
test('member login sets auth cookie', async ({ page }) => {
|
||||||
await loginAsMember(page, 'test-admin@ghostguild.dev')
|
await loginAsMember(page, 'test-admin@ghostguild.dev')
|
||||||
|
|
||||||
// loginAsMember waits for /member/ URL
|
const cookies = await page.context().cookies()
|
||||||
await expect(page).toHaveURL(/\/member\//)
|
const authCookie = cookies.find((c) => c.name === 'auth-token')
|
||||||
|
expect(authCookie).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('logout clears auth', async ({ page }) => {
|
test('logout clears auth', async ({ page }) => {
|
||||||
// Login as admin first
|
|
||||||
await loginAsAdmin(page)
|
await loginAsAdmin(page)
|
||||||
await expect(page).toHaveURL(/\/admin/)
|
await page.goto('/admin')
|
||||||
|
await expect(page.locator('.admin-tag')).toBeVisible({ timeout: 15000 })
|
||||||
|
|
||||||
|
// Set up response listener BEFORE clicking to avoid race
|
||||||
|
const logoutResponse = page.waitForResponse((resp) => resp.url().includes('/api/auth/logout'))
|
||||||
|
|
||||||
// Click the "Sign out" link in the sidebar meta area
|
// Click the "Sign out" link in the sidebar meta area
|
||||||
await page.locator('.sidebar-meta a').filter({ hasText: 'Sign out' }).click()
|
await page.locator('.sidebar-meta a').filter({ hasText: 'Sign out' }).click()
|
||||||
|
|
||||||
// Should redirect to home after logout
|
// Wait for the logout API call to complete
|
||||||
await page.waitForURL('/')
|
await logoutResponse
|
||||||
|
|
||||||
// Verify the auth-token cookie is cleared
|
// Navigating to a protected page should show the sign-in prompt
|
||||||
const cookies = await page.context().cookies()
|
|
||||||
const authCookie = cookies.find((c) => c.name === 'auth-token')
|
|
||||||
expect(!authCookie || authCookie.value === '').toBeTruthy()
|
|
||||||
|
|
||||||
// Navigating to a protected page should show the login modal
|
|
||||||
await page.goto('/member/dashboard')
|
await page.goto('/member/dashboard')
|
||||||
await expect(page.getByText('Sign in to continue')).toBeVisible()
|
await expect(page.getByRole('heading', { name: 'Sign in required' })).toBeVisible({ timeout: 10000 })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ test.describe('public routes accessible when gate is off', () => {
|
||||||
|
|
||||||
// Should not redirect to /coming-soon
|
// Should not redirect to /coming-soon
|
||||||
expect(page.url()).not.toContain('/coming-soon')
|
expect(page.url()).not.toContain('/coming-soon')
|
||||||
await expect(page.getByText('Ghost Guild')).toBeVisible()
|
await expect(page.locator('h1')).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('events page loads', async ({ page }) => {
|
test('events page loads', async ({ page }) => {
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,13 @@ test.describe('Events list page', () => {
|
||||||
|
|
||||||
test('clicking a filter button activates it', async ({ page }) => {
|
test('clicking a filter button activates it', async ({ page }) => {
|
||||||
await page.goto('/events')
|
await page.goto('/events')
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
// Wait for Vue hydration — the "All" filter should have the active class once reactive
|
||||||
|
const allBtn = page.locator('.filter-btn', { hasText: 'All' })
|
||||||
|
await expect(allBtn).toHaveClass(/active/, { timeout: 10000 })
|
||||||
const workshopsBtn = page.locator('.filter-bar button', { hasText: 'Workshops' })
|
const workshopsBtn = page.locator('.filter-bar button', { hasText: 'Workshops' })
|
||||||
await workshopsBtn.click()
|
await workshopsBtn.click()
|
||||||
await expect(workshopsBtn).toHaveClass(/active/)
|
await expect(workshopsBtn).toHaveClass(/active/, { timeout: 5000 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('event links navigate to detail page', async ({ page }) => {
|
test('event links navigate to detail page', async ({ page }) => {
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,27 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login as admin via the dev test-login endpoint.
|
* Login as admin via the dev test-login endpoint.
|
||||||
* Creates a test admin user if none exists.
|
* Creates a test admin user if none exists and sets the auth cookie.
|
||||||
|
* Handles cases where the dev server is slow to redirect under load.
|
||||||
*/
|
*/
|
||||||
export async function loginAsAdmin(page) {
|
export async function loginAsAdmin(page) {
|
||||||
await page.goto('/api/dev/test-login')
|
await page.goto('/api/dev/test-login', { waitUntil: 'domcontentloaded' })
|
||||||
await page.waitForURL('**/admin**')
|
|
||||||
|
// The endpoint sets the cookie and redirects to /admin.
|
||||||
|
// Under heavy parallel load the redirect may not complete, so fall back to manual navigation.
|
||||||
|
try {
|
||||||
|
await page.waitForURL(/\/admin/, { timeout: 15000 })
|
||||||
|
} catch {
|
||||||
|
// Cookie should be set even if redirect failed — navigate manually
|
||||||
|
await page.goto('/admin', { waitUntil: 'domcontentloaded' })
|
||||||
|
await page.waitForURL(/\/admin/)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login as a specific member by email via the dev member-login endpoint.
|
* Login as a specific member by email via the dev member-login endpoint.
|
||||||
*/
|
*/
|
||||||
export async function loginAsMember(page, email) {
|
export async function loginAsMember(page, email) {
|
||||||
await page.goto(`/api/dev/member-login?email=${encodeURIComponent(email)}`)
|
await page.goto(`/api/dev/member-login?email=${encodeURIComponent(email)}`, { waitUntil: 'domcontentloaded' })
|
||||||
await page.waitForURL('**/member/**')
|
await page.waitForURL(/\/member\//)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,48 @@
|
||||||
import { test, expect } from '@playwright/test'
|
import { test, expect } from '@playwright/test'
|
||||||
|
|
||||||
|
// Mock Helcim API responses for join flow (avoids dependency on external API)
|
||||||
|
function mockHelcimAPIs(page, { failCustomer = false } = {}) {
|
||||||
|
// Mock Helcim customer creation
|
||||||
|
page.route('**/api/helcim/customer', async (route) => {
|
||||||
|
if (failCustomer) {
|
||||||
|
return route.fulfill({
|
||||||
|
status: 409,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
statusCode: 409,
|
||||||
|
statusMessage: 'A member with this email already exists',
|
||||||
|
message: 'A member with this email already exists'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
customerId: 'test-cust-123',
|
||||||
|
customerCode: 'CUST-TEST-001'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock subscription creation
|
||||||
|
page.route('**/api/helcim/subscription', async (route) => {
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
subscription: { id: 'test-sub-123', status: 'ACTIVE' }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
test.describe('Join page — member signup flow', () => {
|
test.describe('Join page — member signup flow', () => {
|
||||||
test('join form loads with all fields', async ({ page }) => {
|
test('join form loads with all fields', async ({ page }) => {
|
||||||
await page.goto('/join')
|
await page.goto('/join')
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
await expect(page.locator('#join-name')).toBeVisible()
|
await expect(page.locator('#join-name')).toBeVisible()
|
||||||
await expect(page.locator('#join-email')).toBeVisible()
|
await expect(page.locator('#join-email')).toBeVisible()
|
||||||
|
|
@ -15,6 +55,7 @@ test.describe('Join page — member signup flow', () => {
|
||||||
|
|
||||||
test('submit button disabled when form incomplete', async ({ page }) => {
|
test('submit button disabled when form incomplete', async ({ page }) => {
|
||||||
await page.goto('/join')
|
await page.goto('/join')
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
// Clear name and email — circle defaults to community, contribution defaults to $15
|
// Clear name and email — circle defaults to community, contribution defaults to $15
|
||||||
await page.locator('#join-name').fill('')
|
await page.locator('#join-name').fill('')
|
||||||
|
|
@ -36,46 +77,40 @@ test.describe('Join page — member signup flow', () => {
|
||||||
const uniqueEmail = `test-e2e-${Date.now()}@example.com`
|
const uniqueEmail = `test-e2e-${Date.now()}@example.com`
|
||||||
|
|
||||||
await page.goto('/join')
|
await page.goto('/join')
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
|
// Fill in the form
|
||||||
await page.locator('#join-name').fill('E2E Test User')
|
await page.locator('#join-name').fill('E2E Test User')
|
||||||
await page.locator('#join-email').fill(uniqueEmail)
|
await page.locator('#join-email').fill(uniqueEmail)
|
||||||
await page.locator('#circle-community').check({ force: true })
|
await page.locator('#circle-community').check({ force: true })
|
||||||
await page.locator('#join-contribution').selectOption('0')
|
await page.locator('#join-contribution').selectOption('0')
|
||||||
|
|
||||||
await expect(page.locator('.form-submit')).toBeEnabled()
|
await expect(page.locator('.form-submit')).toBeEnabled()
|
||||||
|
|
||||||
|
// Mock Helcim APIs before submitting
|
||||||
|
await mockHelcimAPIs(page)
|
||||||
|
|
||||||
await page.locator('.form-submit').click()
|
await page.locator('.form-submit').click()
|
||||||
|
|
||||||
// Free tier skips payment (step 2) and goes to confirmation (step 3)
|
// Free tier creates subscription then shows confirmation (step 3)
|
||||||
// or redirects to /welcome. Wait for either outcome.
|
await expect(page.locator('.success-box')).toBeVisible({ timeout: 15000 })
|
||||||
await expect(
|
|
||||||
page.getByText('Welcome to Ghost Guild!').or(page.locator('.success-box'))
|
|
||||||
).toBeVisible({ timeout: 15000 })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('duplicate email shows error', async ({ page }) => {
|
test('duplicate email shows error', async ({ page }) => {
|
||||||
// First submission — create a member
|
|
||||||
const duplicateEmail = `test-e2e-dup-${Date.now()}@example.com`
|
const duplicateEmail = `test-e2e-dup-${Date.now()}@example.com`
|
||||||
|
|
||||||
await page.goto('/join')
|
await page.goto('/join')
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
|
// Mock customer endpoint to return 409 (email already exists)
|
||||||
|
await mockHelcimAPIs(page, { failCustomer: true })
|
||||||
|
|
||||||
await page.locator('#join-name').fill('Dup Test User')
|
await page.locator('#join-name').fill('Dup Test User')
|
||||||
await page.locator('#join-email').fill(duplicateEmail)
|
await page.locator('#join-email').fill(duplicateEmail)
|
||||||
await page.locator('#circle-community').check({ force: true })
|
await page.locator('#circle-community').check({ force: true })
|
||||||
await page.locator('#join-contribution').selectOption('0')
|
await page.locator('#join-contribution').selectOption('0')
|
||||||
await page.locator('.form-submit').click()
|
await page.locator('.form-submit').click()
|
||||||
|
|
||||||
// Wait for first submission to succeed
|
|
||||||
await expect(
|
|
||||||
page.getByText('Welcome to Ghost Guild!').or(page.locator('.success-box'))
|
|
||||||
).toBeVisible({ timeout: 15000 })
|
|
||||||
|
|
||||||
// Navigate back and try to register the same email again
|
|
||||||
await page.goto('/join')
|
|
||||||
await page.locator('#join-name').fill('Dup Test User Again')
|
|
||||||
await page.locator('#join-email').fill(duplicateEmail)
|
|
||||||
await page.locator('#circle-community').check({ force: true })
|
|
||||||
await page.locator('#join-contribution').selectOption('0')
|
|
||||||
await page.locator('.form-submit').click()
|
|
||||||
|
|
||||||
// Should show an error about the email already existing
|
// Should show an error about the email already existing
|
||||||
await expect(page.locator('.error-box')).toBeVisible({ timeout: 10000 })
|
await expect(page.locator('.error-box')).toBeVisible({ timeout: 10000 })
|
||||||
await expect(page.locator('.error-box')).toContainText(/already/i)
|
await expect(page.locator('.error-box')).toContainText(/already/i)
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,15 @@ test.describe('Member dashboard', () => {
|
||||||
test('dashboard loads for authenticated user', async ({ adminPage }) => {
|
test('dashboard loads for authenticated user', async ({ adminPage }) => {
|
||||||
await adminPage.goto('/member/dashboard')
|
await adminPage.goto('/member/dashboard')
|
||||||
|
|
||||||
await expect(adminPage.getByText('Welcome back')).toBeVisible({ timeout: 10000 })
|
// Welcome heading includes the member's name (inside ClientOnly, may take time)
|
||||||
|
await expect(adminPage.getByText('Welcome back')).toBeVisible({ timeout: 15000 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('shows navigation links', async ({ adminPage }) => {
|
test('shows navigation links', async ({ adminPage }) => {
|
||||||
await adminPage.goto('/member/dashboard')
|
await adminPage.goto('/member/dashboard')
|
||||||
|
|
||||||
// Wait for dashboard content to render
|
// Wait for ClientOnly dashboard content to render
|
||||||
await expect(adminPage.getByText('Welcome back')).toBeVisible({ timeout: 10000 })
|
await expect(adminPage.getByText('Welcome back')).toBeVisible({ timeout: 15000 })
|
||||||
|
|
||||||
// Verify quick action links are present
|
// Verify quick action links are present
|
||||||
await expect(adminPage.getByText('Update your profile')).toBeVisible()
|
await expect(adminPage.getByText('Update your profile')).toBeVisible()
|
||||||
|
|
@ -25,9 +26,9 @@ test.describe('Member dashboard', () => {
|
||||||
|
|
||||||
await page.goto('/member/dashboard')
|
await page.goto('/member/dashboard')
|
||||||
|
|
||||||
// Should show the sign-in required message or a login modal
|
// Should show the login modal or the page's sign-in required state
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText('Sign in required').or(page.getByText('Sign in to your dashboard'))
|
page.locator('.modal-title').or(page.getByText('Sign in required'))
|
||||||
).toBeVisible({ timeout: 10000 })
|
).toBeVisible({ timeout: 10000 })
|
||||||
|
|
||||||
await context.close()
|
await context.close()
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,14 @@ import { test, expect } from './helpers/fixtures.js'
|
||||||
test.describe('Member profile page', () => {
|
test.describe('Member profile page', () => {
|
||||||
test('profile page loads', async ({ adminPage }) => {
|
test('profile page loads', async ({ adminPage }) => {
|
||||||
await adminPage.goto('/member/profile')
|
await adminPage.goto('/member/profile')
|
||||||
await expect(adminPage.getByText('Edit Profile')).toBeVisible()
|
// Auth is checked client-side in onMounted — wait for profile form to render
|
||||||
|
await expect(adminPage.getByText('Edit Profile')).toBeVisible({ timeout: 15000 })
|
||||||
await expect(adminPage.getByText('How you appear to other members')).toBeVisible()
|
await expect(adminPage.getByText('How you appear to other members')).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('form fields are present', async ({ adminPage }) => {
|
test('form fields are present', async ({ adminPage }) => {
|
||||||
await adminPage.goto('/member/profile')
|
await adminPage.goto('/member/profile')
|
||||||
|
await expect(adminPage.getByText('Edit Profile')).toBeVisible({ timeout: 15000 })
|
||||||
|
|
||||||
// Name input
|
// Name input
|
||||||
await expect(adminPage.locator('input[placeholder="Your name"]')).toBeVisible()
|
await expect(adminPage.locator('input[placeholder="Your name"]')).toBeVisible()
|
||||||
|
|
@ -22,6 +24,7 @@ test.describe('Member profile page', () => {
|
||||||
|
|
||||||
test('bio field accepts input', async ({ adminPage }) => {
|
test('bio field accepts input', async ({ adminPage }) => {
|
||||||
await adminPage.goto('/member/profile')
|
await adminPage.goto('/member/profile')
|
||||||
|
await expect(adminPage.getByText('Edit Profile')).toBeVisible({ timeout: 15000 })
|
||||||
|
|
||||||
const bio = adminPage.locator('textarea[placeholder*="Share your background"]')
|
const bio = adminPage.locator('textarea[placeholder*="Share your background"]')
|
||||||
const saveBtn = adminPage.getByRole('button', { name: 'Save Profile' })
|
const saveBtn = adminPage.getByRole('button', { name: 'Save Profile' })
|
||||||
|
|
@ -39,6 +42,7 @@ test.describe('Member profile page', () => {
|
||||||
|
|
||||||
test('pronouns field editable', async ({ adminPage }) => {
|
test('pronouns field editable', async ({ adminPage }) => {
|
||||||
await adminPage.goto('/member/profile')
|
await adminPage.goto('/member/profile')
|
||||||
|
await expect(adminPage.getByText('Edit Profile')).toBeVisible({ timeout: 15000 })
|
||||||
|
|
||||||
const pronouns = adminPage.locator('input[placeholder="e.g., she/her, they/them"]')
|
const pronouns = adminPage.locator('input[placeholder="e.g., she/her, they/them"]')
|
||||||
await expect(pronouns).toBeVisible()
|
await expect(pronouns).toBeVisible()
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,8 @@ test.describe('My Updates page', () => {
|
||||||
|
|
||||||
await page.goto('/member/my-updates')
|
await page.goto('/member/my-updates')
|
||||||
|
|
||||||
await expect(
|
// Should show the page's "Sign in required" heading
|
||||||
page
|
await expect(page.locator('.state-heading')).toBeVisible({ timeout: 10000 })
|
||||||
.getByText('Sign in required')
|
|
||||||
.or(page.getByText('Sign in to view your updates'))
|
|
||||||
).toBeVisible({ timeout: 10000 })
|
|
||||||
|
|
||||||
await context.close()
|
await context.close()
|
||||||
})
|
})
|
||||||
|
|
@ -52,24 +49,27 @@ test.describe('New Update page', () => {
|
||||||
await expect(adminPage.locator('select')).toBeVisible()
|
await expect(adminPage.locator('select')).toBeVisible()
|
||||||
|
|
||||||
// Submit button exists and starts disabled (empty textarea)
|
// Submit button exists and starts disabled (empty textarea)
|
||||||
const submitBtn = adminPage.locator('button[type="submit"]')
|
const submitBtn = adminPage.locator('button[type="submit"]', { hasText: 'Post Update' })
|
||||||
await expect(submitBtn).toBeVisible()
|
await expect(submitBtn).toBeVisible()
|
||||||
await expect(submitBtn).toBeDisabled()
|
await expect(submitBtn).toBeDisabled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('submit button enables when content is entered', async ({ adminPage }) => {
|
test('submit button enables when content is entered', async ({ adminPage }) => {
|
||||||
await adminPage.goto('/updates/new')
|
await adminPage.goto('/updates/new')
|
||||||
|
await adminPage.waitForLoadState('networkidle')
|
||||||
|
|
||||||
await expect(adminPage.locator('h1', { hasText: 'New Update' })).toBeVisible({
|
await expect(adminPage.locator('h1', { hasText: 'New Update' })).toBeVisible({
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
})
|
})
|
||||||
|
|
||||||
const textarea = adminPage.locator('textarea')
|
const textarea = adminPage.locator('textarea')
|
||||||
const submitBtn = adminPage.locator('button[type="submit"]')
|
const submitBtn = adminPage.locator('button[type="submit"]', { hasText: 'Post Update' })
|
||||||
|
|
||||||
await expect(submitBtn).toBeDisabled()
|
await expect(submitBtn).toBeDisabled()
|
||||||
await textarea.fill('Test update content')
|
// Use click + type to ensure Vue hydration processes the input events
|
||||||
await expect(submitBtn).toBeEnabled()
|
await textarea.click()
|
||||||
|
await textarea.pressSequentially('Test update content', { delay: 10 })
|
||||||
|
await expect(submitBtn).toBeEnabled({ timeout: 5000 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('privacy selector defaults to members and has all options', async ({ adminPage }) => {
|
test('privacy selector defaults to members and has all options', async ({ adminPage }) => {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ export default defineNuxtConfig({
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
|
title: "Ghost Guild",
|
||||||
|
htmlAttrs: { lang: "en" },
|
||||||
link: [
|
link: [
|
||||||
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||||
{
|
{
|
||||||
|
|
|
||||||
644
package-lock.json
generated
|
|
@ -814,6 +814,66 @@
|
||||||
"tinyglobby": "^0.2.15"
|
"tinyglobby": "^0.2.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dxup/nuxt/node_modules/chokidar": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dxup/nuxt/node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dxup/nuxt/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dxup/nuxt/node_modules/readdirp": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@dxup/unimport": {
|
"node_modules/@dxup/unimport": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@dxup/unimport/-/unimport-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@dxup/unimport/-/unimport-0.1.2.tgz",
|
||||||
|
|
@ -1444,31 +1504,63 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/config-inspector/node_modules/chokidar": {
|
"node_modules/@eslint/config-inspector/node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"readdirp": "^4.0.1"
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14.16.0"
|
"node": ">= 8.10.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@eslint/config-inspector/node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@eslint/config-inspector/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/config-inspector/node_modules/readdirp": {
|
"node_modules/@eslint/config-inspector/node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"dependencies": {
|
||||||
"node": ">= 14.18.0"
|
"picomatch": "^2.2.1"
|
||||||
},
|
},
|
||||||
"funding": {
|
"engines": {
|
||||||
"type": "individual",
|
"node": ">=8.10.0"
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/core": {
|
"node_modules/@eslint/core": {
|
||||||
|
|
@ -2372,6 +2464,66 @@
|
||||||
"eslint": "^9.0.0 || ^10.0.0"
|
"eslint": "^9.0.0 || ^10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nuxt/eslint/node_modules/chokidar": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt/eslint/node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt/eslint/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt/eslint/node_modules/readdirp": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nuxt/fonts": {
|
"node_modules/@nuxt/fonts": {
|
||||||
"version": "0.14.0",
|
"version": "0.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@nuxt/fonts/-/fonts-0.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@nuxt/fonts/-/fonts-0.14.0.tgz",
|
||||||
|
|
@ -7896,6 +8048,18 @@
|
||||||
"require-from-string": "^2.0.2"
|
"require-from-string": "^2.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/binary-extensions": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bindings": {
|
"node_modules/bindings": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||||
|
|
@ -8110,6 +8274,54 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/c12/node_modules/chokidar": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/c12/node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/c12/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/c12/node_modules/rc9": {
|
"node_modules/c12/node_modules/rc9": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
|
||||||
|
|
@ -8120,6 +8332,18 @@
|
||||||
"destr": "^2.0.3"
|
"destr": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/c12/node_modules/readdirp": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cac": {
|
"node_modules/cac": {
|
||||||
"version": "6.7.14",
|
"version": "6.7.14",
|
||||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||||
|
|
@ -8215,21 +8439,6 @@
|
||||||
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
|
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/chokidar": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"readdirp": "^5.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 20.19.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/chownr": {
|
"node_modules/chownr": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||||
|
|
@ -11151,6 +11360,18 @@
|
||||||
"url": "https://github.com/sponsors/brc-dd"
|
"url": "https://github.com/sponsors/brc-dd"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-binary-path": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"binary-extensions": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-builtin-module": {
|
"node_modules/is-builtin-module": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz",
|
||||||
|
|
@ -12922,6 +13143,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nitropack/node_modules/chokidar": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nitropack/node_modules/cookie-es": {
|
"node_modules/nitropack/node_modules/cookie-es": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.0.tgz",
|
||||||
|
|
@ -12940,6 +13185,42 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nitropack/node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nitropack/node_modules/readdirp": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nitropack/node_modules/readdirp/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nitropack/node_modules/unplugin-utils": {
|
"node_modules/nitropack/node_modules/unplugin-utils": {
|
||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
|
||||||
|
|
@ -13189,6 +13470,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nuxt/node_modules/chokidar": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nuxt/node_modules/cookie-es": {
|
"node_modules/nuxt/node_modules/cookie-es": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.0.tgz",
|
||||||
|
|
@ -13207,6 +13512,42 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nuxt/node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nuxt/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nuxt/node_modules/readdirp": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nypm": {
|
"node_modules/nypm": {
|
||||||
"version": "0.6.5",
|
"version": "0.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz",
|
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz",
|
||||||
|
|
@ -14814,19 +15155,6 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/readdirp": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 20.19.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "individual",
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/redis-errors": {
|
"node_modules/redis-errors": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||||
|
|
@ -16566,6 +16894,66 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/unplugin-vue-components/node_modules/chokidar": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unplugin-vue-components/node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unplugin-vue-components/node_modules/readdirp": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unplugin-vue-components/node_modules/readdirp/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/unplugin-vue-components/node_modules/unplugin": {
|
"node_modules/unplugin-vue-components/node_modules/unplugin": {
|
||||||
"version": "2.3.11",
|
"version": "2.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
||||||
|
|
@ -16632,6 +17020,66 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/unplugin-vue-router/node_modules/chokidar": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unplugin-vue-router/node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unplugin-vue-router/node_modules/readdirp": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unplugin-vue-router/node_modules/readdirp/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/unplugin-vue-router/node_modules/unplugin": {
|
"node_modules/unplugin-vue-router/node_modules/unplugin": {
|
||||||
"version": "2.3.11",
|
"version": "2.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
||||||
|
|
@ -16793,6 +17241,42 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/unstorage/node_modules/chokidar": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unstorage/node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/unstorage/node_modules/lru-cache": {
|
"node_modules/unstorage/node_modules/lru-cache": {
|
||||||
"version": "11.2.6",
|
"version": "11.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
|
||||||
|
|
@ -16802,6 +17286,30 @@
|
||||||
"node": "20 || >=22"
|
"node": "20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/unstorage/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unstorage/node_modules/readdirp": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/untun": {
|
"node_modules/untun": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/untun/-/untun-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/untun/-/untun-0.1.3.tgz",
|
||||||
|
|
@ -17231,18 +17739,39 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-plugin-checker/node_modules/chokidar": {
|
"node_modules/vite-plugin-checker/node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"readdirp": "^4.0.1"
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14.16.0"
|
"node": ">= 8.10.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vite-plugin-checker/node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-plugin-checker/node_modules/npm-run-path": {
|
"node_modules/vite-plugin-checker/node_modules/npm-run-path": {
|
||||||
|
|
@ -17274,16 +17803,27 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-plugin-checker/node_modules/readdirp": {
|
"node_modules/vite-plugin-checker/node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vite-plugin-checker/node_modules/readdirp/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14.18.0"
|
"node": ">=8.6"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "individual",
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-plugin-inspect": {
|
"node_modules/vite-plugin-inspect": {
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,9 @@
|
||||||
"vue-cal": "^5.0.1-rc.28",
|
"vue-cal": "^5.0.1-rc.28",
|
||||||
"zod": "^4.1.3"
|
"zod": "^4.1.3"
|
||||||
},
|
},
|
||||||
|
"overrides": {
|
||||||
|
"chokidar": "3.6.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@axe-core/playwright": "^4.11.1",
|
"@axe-core/playwright": "^4.11.1",
|
||||||
"@nuxt/test-utils": "^4.0.0",
|
"@nuxt/test-utils": "^4.0.0",
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,11 @@ export default defineConfig({
|
||||||
retries: process.env.CI ? 1 : 0,
|
retries: process.env.CI ? 1 : 0,
|
||||||
workers: process.env.CI ? 1 : undefined,
|
workers: process.env.CI ? 1 : undefined,
|
||||||
reporter: 'html',
|
reporter: 'html',
|
||||||
|
timeout: 60000,
|
||||||
use: {
|
use: {
|
||||||
baseURL: 'http://localhost:3000',
|
baseURL: 'http://localhost:3000',
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
|
navigationTimeout: 45000,
|
||||||
},
|
},
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
const config = useRuntimeConfig(event)
|
const config = useRuntimeConfig(event)
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
{ memberId: member._id, email: member.email },
|
{ memberId: member._id, email: member.email, tv: member.tokenVersion || 0 },
|
||||||
config.jwtSecret,
|
config.jwtSecret,
|
||||||
{ expiresIn: '7d' }
|
{ expiresIn: '7d' }
|
||||||
)
|
)
|
||||||
|
|
@ -34,6 +34,7 @@ export default defineEventHandler(async (event) => {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
|
path: '/',
|
||||||
maxAge: 60 * 60 * 24 * 7,
|
maxAge: 60 * 60 * 24 * 7,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,23 +10,16 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
await connectDB()
|
await connectDB()
|
||||||
|
|
||||||
// Find or create a test admin user
|
// Find or create a test admin user (atomic to avoid race conditions in parallel tests)
|
||||||
let member = await Member.findOne({ email: 'test-admin@ghostguild.dev' })
|
const member = await Member.findOneAndUpdate(
|
||||||
|
{ email: 'test-admin@ghostguild.dev' },
|
||||||
if (!member) {
|
{ $setOnInsert: { name: 'Test Admin', circle: 'founder', contributionTier: '0', role: 'admin', status: 'active' } },
|
||||||
member = await Member.create({
|
{ upsert: true, new: true }
|
||||||
email: 'test-admin@ghostguild.dev',
|
)
|
||||||
name: 'Test Admin',
|
|
||||||
circle: 'founder',
|
|
||||||
contributionTier: '0',
|
|
||||||
role: 'admin',
|
|
||||||
status: 'active',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = useRuntimeConfig(event)
|
const config = useRuntimeConfig(event)
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
{ memberId: member._id, email: member.email },
|
{ memberId: member._id, email: member.email, tv: member.tokenVersion || 0 },
|
||||||
config.jwtSecret,
|
config.jwtSecret,
|
||||||
{ expiresIn: '7d' }
|
{ expiresIn: '7d' }
|
||||||
)
|
)
|
||||||
|
|
@ -35,6 +28,7 @@ export default defineEventHandler(async (event) => {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
|
path: '/',
|
||||||
maxAge: 60 * 60 * 24 * 7,
|
maxAge: 60 * 60 * 24 * 7,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { readFileSync } from 'node:fs'
|
||||||
import { resolve } from 'node:path'
|
import { resolve } from 'node:path'
|
||||||
|
|
||||||
vi.mock('../../../server/models/member.js', () => ({
|
vi.mock('../../../server/models/member.js', () => ({
|
||||||
default: { findOne: vi.fn(), create: vi.fn() }
|
default: { findOne: vi.fn(), create: vi.fn(), findOneAndUpdate: vi.fn() }
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../../../server/utils/mongoose.js', () => ({
|
vi.mock('../../../server/utils/mongoose.js', () => ({
|
||||||
|
|
@ -74,34 +74,38 @@ describe('dev endpoints', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('creates admin user when none exists', async () => {
|
it('creates admin user when none exists', async () => {
|
||||||
Member.findOne.mockResolvedValue(null)
|
Member.findOneAndUpdate.mockResolvedValue(mockMember)
|
||||||
Member.create.mockResolvedValue(mockMember)
|
|
||||||
|
|
||||||
const event = createMockEvent({ method: 'GET', path: '/api/dev/test-login' })
|
const event = createMockEvent({ method: 'GET', path: '/api/dev/test-login' })
|
||||||
await testLoginHandler(event)
|
await testLoginHandler(event)
|
||||||
|
|
||||||
expect(Member.findOne).toHaveBeenCalledWith({ email: 'test-admin@ghostguild.dev' })
|
expect(Member.findOneAndUpdate).toHaveBeenCalledWith(
|
||||||
expect(Member.create).toHaveBeenCalledWith(
|
{ email: 'test-admin@ghostguild.dev' },
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
email: 'test-admin@ghostguild.dev',
|
$setOnInsert: expect.objectContaining({
|
||||||
role: 'admin',
|
role: 'admin',
|
||||||
circle: 'founder'
|
circle: 'founder'
|
||||||
})
|
})
|
||||||
|
}),
|
||||||
|
{ upsert: true, new: true }
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses existing admin when found', async () => {
|
it('uses existing admin when found', async () => {
|
||||||
Member.findOne.mockResolvedValue(mockMember)
|
Member.findOneAndUpdate.mockResolvedValue(mockMember)
|
||||||
|
|
||||||
const event = createMockEvent({ method: 'GET', path: '/api/dev/test-login' })
|
const event = createMockEvent({ method: 'GET', path: '/api/dev/test-login' })
|
||||||
await testLoginHandler(event)
|
await testLoginHandler(event)
|
||||||
|
|
||||||
expect(Member.findOne).toHaveBeenCalledWith({ email: 'test-admin@ghostguild.dev' })
|
expect(Member.findOneAndUpdate).toHaveBeenCalledWith(
|
||||||
expect(Member.create).not.toHaveBeenCalled()
|
{ email: 'test-admin@ghostguild.dev' },
|
||||||
|
expect.any(Object),
|
||||||
|
{ upsert: true, new: true }
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sets auth cookie', async () => {
|
it('sets auth cookie', async () => {
|
||||||
Member.findOne.mockResolvedValue(mockMember)
|
Member.findOneAndUpdate.mockResolvedValue(mockMember)
|
||||||
|
|
||||||
const event = createMockEvent({ method: 'GET', path: '/api/dev/test-login' })
|
const event = createMockEvent({ method: 'GET', path: '/api/dev/test-login' })
|
||||||
await testLoginHandler(event)
|
await testLoginHandler(event)
|
||||||
|
|
|
||||||