Enhance user role management in Polycentricity3 with Cloudflare Turnstile for bot protection, SendGrid for email verification, and role-based access control. Update existing client-side routes/login/+page.svelte and routes/register/+page.svelte, add server-side logic, and create new components/routes. Align with Svelte 5.28.1 Runes, Gun.js 0.2020.1240, TypeScript, src/lib/types/index.ts, and GunSchema.md.
- Routes:
routes/login/+page.svelteandroutes/register/+page.svelteare client-side, lacking server logic. - Auth:
authService.tsis updated to store users atusers/<user_id>, usegunService.ts, supportmagic_keyandexpires_at, and handle verification. - Store:
userStore.tsusesUserSessiontype, supports$userStore. - Game Service:
gameService.tsincludesupdateUserRolefor role updates. - Types:
types/index.tsincludesexpires_atinUser.
- lib/services/authService.ts (Update) - Apply refined version with
userId-based verification, debouncedlast_login, and simplifiedinitializeAuth. - routes/register/+page.server.ts (New) - Verify Turnstile, call
registerUser, send SendGrid email. - routes/register/+page.svelte (Modify) - Add
TurnstileWidget, sendturnstileToken. - routes/login/+page.server.ts (New) - Verify Turnstile, call
loginUser. - routes/login/+page.svelte (Modify) - Add
TurnstileWidget, sendturnstileToken. - routes/verify/+page.server.ts (New) - Call
verifyUserwithuserIdandmagicKey. - routes/verify/+page.svelte (New) - Auto-verify via URL params, redirect to
/login. - lib/components/auth/TurnstileWidget.svelte (New) - Reusable Turnstile widget.
- lib/components/auth/RoleGuard.svelte (New) - Restrict UI/routes by
user.role.
- lib/services/authService.ts
- Update with
userId-basedverifyUser, debouncedlast_login(100ms), and simplifiedinitializeAuth.
- Update with
- routes/register/+page.server.ts
- Verify Turnstile token using
<CLOUDFLARE_TURNSTILE_SECRET>. - Call
registerUserfromauthService.ts. - Send SendGrid email (
<SENDGRID_API_KEY>) frompolycentricity@endogon.comwith linkhttps://localhost:5000/verify?userId=<user_id>&magicKey=<magic_key>.
- Verify Turnstile token using
- routes/register/+page.svelte
- Integrate
TurnstileWidgetto captureturnstileToken. - Send
name,email,password,turnstileTokento server. - Use Skeleton Labs classes (e.g.,
bg-surface-50-950/90). - Display “check email for verification” message post-registration.
- Integrate
- routes/login/+page.server.ts
- Verify Turnstile, call
loginUser.
- Verify Turnstile, call
- routes/login/+page.svelte
- Use
TurnstileWidget, sendturnstileToken. - Implement with
$state,onsubmit, defaults (bjorn@endogon.com,admin123).
- Use
- routes/verify/+page.server.ts
- Extract
userId,magicKeyfrom URL params, callverifyUser.
- Extract
- routes/verify/+page.svelte
- Auto-verify on mount with
$effect, redirect to/login. - Use Tailwind/Skeleton styling (e.g.,
text-primary-500-400).
- Auto-verify on mount with
- lib/components/auth/TurnstileWidget.svelte
- Load Turnstile script, dispatch
onverified/onerrorvia$props. - Use
$effectfor script loading.
- Load Turnstile script, dispatch
- lib/components/auth/RoleGuard.svelte
- Check
$userStore.user.rolewith$derived. - Redirect to
redirectToif unauthorized.
- Check
- User Store: Leverage
$userStore(typeUserSession) for reactive state. - Auth Service: Utilize refined
registerUser,loginUser,verifyUser,initializeAuth. - Game Service: Use
updateUserRolefor role updates post-verification. - Routes: Add
+page.server.tstologinandregister, update forms withTurnstileWidget.
- API Keys: Use placeholders
<CLOUDFLARE_TURNSTILE_SITEKEY>,<CLOUDFLARE_TURNSTILE_SECRET>,<SENDGRID_API_KEY>(to be provided). - Security:
- Use
generateIdformagic_key. - Enforce 24-hour expiration via
expires_at. - Use HTTPS for verification links.
- Use
- Performance:
- Debounce
last_loginwrites (100ms). - Use
get,putSignedfor efficient Gun.js operations.
- Debounce
- Testing: Verify admin login (
bjorn@endogon.com,admin123),GuesttoMemberupgrades,RoleGuardrestrictions.
- Role Guard:
<!-- src/routes/admin/+layout.svelte --> <RoleGuard allowedRoles={['Admin']} redirectTo="/login"> {#snippet children()} <slot /> {/snippet} </RoleGuard>
- Verification Flow:
- User registers, receives email with
https://localhost:5000/verify?userId=<user_id>&magicKey=<magic_key>. - Auto-verification sets
role: "Member", redirects to/login.
- User registers, receives email with
// src/routes/register/+page.server.ts
import type { RequestHandler } from '@sveltejs/kit';
import { json } from '@sveltejs/kit';
import { registerUser } from '$lib/services/authService';
import { sendVerificationEmail } from '$lib/services/emailService'; // Assume this exists or create it
export const POST: RequestHandler = async ({ request }) => {
try {
const { name, email, password, turnstileToken } = await request.json();
// Verify Turnstile token
const turnstileResponse = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
secret: '<CLOUDFLARE_TURNSTILE_SECRET>',
response: turnstileToken,
}),
});
const turnstileResult = await turnstileResponse.json();
if (!turnstileResult.success) {
return json({ error: 'Turnstile verification failed' }, { status: 400 });
}
// Register user
const user = await registerUser(name, email, password);
if (!user) {
return json({ error: 'Registration failed' }, { status: 500 });
}
// Send verification email
await sendVerificationEmail(email, user.user_id, user.magic_key!);
return json({ success: true, userId: user.user_id });
} catch (error) {
console.error('Registration error:', error);
return json({ error: 'Registration failed' }, { status: 500 });
}
};<!-- src/routes/register/+page.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import TurnstileWidget from '$lib/components/auth/TurnstileWidget.svelte';
import { registerUser } from '$lib/services/authService';
let name = $state('');
let email = $state('');
let password = $state('');
let turnstileToken = $state<string | null>(null);
let error = $state<string | null>(null);
let success = $state(false);
async function handleSubmit() {
if (!turnstileToken) {
error = 'Please complete the Turnstile verification';
return;
}
const user = await registerUser(name, email, password);
if (user) {
success = true;
setTimeout(() => goto('/login'), 3000);
} else {
error = 'Registration failed';
}
}
onMount(() => {
// Load Turnstile script
const script = document.createElement('script');
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
script.async = true;
document.body.appendChild(script);
return () => document.body.removeChild(script);
});
</script>
{#if success}
<p class="text-green-500">Registration successful! Check your email for verification.</p>
{:else}
<form onsubmit|preventDefault={handleSubmit} class="space-y-4">
<div>
<label for="name" class="block">Name</label>
<input id="name" type="text" bind:value={name} class="input" required />
</div>
<div>
<label for="email" class="block">Email</label>
<input id="email" type="email" bind:value={email} class="input" required />
</div>
<div>
<label for="password" class="block">Password</label>
<input id="password" type="password" bind:value={password} class="input" required />
</div>
<TurnstileWidget sitekey="<CLOUDFLARE_TURNSTILE_SITEKEY>" onverified={(e) => turnstileToken = e.detail} />
{#if error}
<p class="text-red-500">{error}</p>
{/if}
<button type="submit" class="btn variant-filled">Register</button>
</form>
{/if}// src/routes/login/+page.server.ts
import type { RequestHandler } from '@sveltejs/kit';
import { json } from '@sveltejs/kit';
import { loginUser } from '$lib/services/authService';
export const POST: RequestHandler = async ({ request }) => {
try {
const { email, password, turnstileToken } = await request.json();
// Verify Turnstile token
const turnstileResponse = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
secret: '<CLOUDFLARE_TURNSTILE_SECRET>',
response: turnstileToken,
}),
});
const turnstileResult = await turnstileResponse.json();
if (!turnstileResult.success) {
return json({ error: 'Turnstile verification failed' }, { status: 400 });
}
// Login user
const user = await loginUser(email, password);
if (!user) {
return json({ error: 'Login failed' }, { status: 401 });
}
return json({ success: true, userId: user.user_id });
} catch (error) {
console.error('Login error:', error);
return json({ error: 'Login failed' }, { status: 500 });
}
};<!-- src/routes/login/+page.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import TurnstileWidget from '$lib/components/auth/TurnstileWidget.svelte';
import { loginUser } from '$lib/services/authService';
let email = $state('bjorn@endogon.com');
let password = $state('admin123');
let turnstileToken = $state<string | null>(null);
let error = $state<string | null>(null);
let success = $state(false);
async function handleSubmit() {
if (!turnstileToken) {
error = 'Please complete the Turnstile verification';
return;
}
const user = await loginUser(email, password);
if (user) {
success = true;
goto('/dashboard');
} else {
error = 'Login failed';
}
}
onMount(() => {
// Load Turnstile script
const script = document.createElement('script');
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
script.async = true;
document.body.appendChild(script);
return () => document.body.removeChild(script);
});
</script>
{#if success}
<p class="text-green-500">Login successful! Redirecting...</p>
{:else}
<form onsubmit|preventDefault={handleSubmit} class="space-y-4">
<div>
<label for="email" class="block">Email</label>
<input id="email" type="email" bind:value={email} class="input" required />
</div>
<div>
<label for="password" class="block">Password</label>
<input id="password" type="password" bind:value={password} class="input" required />
</div>
<TurnstileWidget sitekey="<CLOUDFLARE_TURNSTILE_SITEKEY>" onverified={(e) => turnstileToken = e.detail} />
{#if error}
<p class="text-red-500">{error}</p>
{/if}
<button type="submit" class="btn variant-filled">Login</button>
</form>
{/if}// src/routes/verify/+page.server.ts
import type { RequestHandler } from '@sveltejs/kit';
import { json } from '@sveltejs/kit';
import { verifyUser } from '$lib/services/authService';
export const GET: RequestHandler = async ({ url }) => {
try {
const userId = url.searchParams.get('userId');
const magicKey = url.searchParams.get('magicKey');
if (!userId || !magicKey) {
return json({ error: 'Missing userId or magicKey' }, { status: 400 });
}
const success = await verifyUser(userId, magicKey);
if (!success) {
return json({ error: 'Verification failed' }, { status: 400 });
}
return json({ success: true });
} catch (error) {
console.error('Verification error:', error);
return json({ error: 'Verification failed' }, { status: 500 });
}
};<!-- src/routes/verify/+page.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
let error = $state<string | null>(null);
let success = $state(false);
onMount(async () => {
const userId = $page.url.searchParams.get('userId');
const magicKey = $page.url.searchParams.get('magicKey');
if (!userId || !magicKey) {
error = 'Invalid verification link';
return;
}
const response = await fetch(`/api/verify?userId=${userId}&magicKey=${magicKey}`);
const result = await response.json();
if (result.success) {
success = true;
setTimeout(() => goto('/login'), 2000);
} else {
error = result.error || 'Verification failed';
}
});
</script>
{#if success}
<p class="text-green-500">Verification successful! Redirecting to login...</p>
{:else if error}
<p class="text-red-500">{error}</p>
{:else}
<p>Verifying...</p>
{/if}<!-- src/lib/components/auth/TurnstileWidget.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
let { sitekey, onverified } = $props();
onMount(() => {
const script = document.createElement('script');
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
script.async = true;
document.body.appendChild(script);
script.onload = () => {
window.turnstile.render('#turnstile-widget', {
sitekey,
callback: (token) => onverified(token),
});
};
return () => {
document.body.removeChild(script);
};
});
</script>
<div id="turnstile-widget"></div><!-- src/lib/components/auth/RoleGuard.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { userStore } from '$lib/stores/userStore';
let { allowedRoles, redirectTo, children } = $props();
let hasAccess = $derived(
$userStore.isAuthenticated && allowedRoles.includes($userStore.user?.role),
);
onMount(() => {
if (!hasAccess) {
goto(redirectTo);
}
});
</script>
{#if hasAccess}
{@render children()}
{/if}