mirror of
https://github.com/SrIzan10/lofi.git
synced 2026-06-06 00:56:53 +00:00
feat: proxy chillhop requests and add turnstile to account creation
This commit is contained in:
@@ -9,3 +9,7 @@ ORIGIN=""
|
|||||||
# For production use 32 characters and generated with high entropy
|
# For production use 32 characters and generated with high entropy
|
||||||
# https://www.better-auth.com/docs/installation
|
# https://www.better-auth.com/docs/installation
|
||||||
BETTER_AUTH_SECRET=""
|
BETTER_AUTH_SECRET=""
|
||||||
|
|
||||||
|
# Cloudflare Turnstile
|
||||||
|
PUBLIC_TURNSTILE_SITE_KEY=""
|
||||||
|
TURNSTILE_SECRET_KEY=""
|
||||||
|
|||||||
5
bun.lock
5
bun.lock
@@ -25,6 +25,7 @@
|
|||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"svelte-sonner": "^0.3.28",
|
"svelte-sonner": "^0.3.28",
|
||||||
|
"svelte-turnstile": "^0.11.0",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tailwind-variants": "^1.0.0",
|
"tailwind-variants": "^1.0.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
@@ -810,6 +811,8 @@
|
|||||||
|
|
||||||
"svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="],
|
"svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="],
|
||||||
|
|
||||||
|
"svelte-turnstile": ["svelte-turnstile@0.11.0", "", { "dependencies": { "turnstile-types": "^1.2.3" }, "peerDependencies": { "svelte": "^3.58.0 || ^4.0.0 || ^5.0.0" } }, "sha512-2LFklx9JVsR3fJ7e3fGG1HEAWWEqRq1WfNaVrKgZJ+pzfY2NColiH+wH0kK2yX3DrcGLiJ9vBeTyiLFWotKpLA=="],
|
||||||
|
|
||||||
"tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
|
"tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
|
||||||
|
|
||||||
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
|
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
|
||||||
@@ -844,6 +847,8 @@
|
|||||||
|
|
||||||
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
||||||
|
|
||||||
|
"turnstile-types": ["turnstile-types@1.2.3", "", {}, "sha512-EDjhDB9TDwda2JRbhzO/kButPio3JgrC3gXMVAMotxldybTCJQVMvPNJ89rcAiN9vIrCb2i1E+VNBCqB8wue0A=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"undici": ["undici@7.24.4", "", {}, "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w=="],
|
"undici": ["undici@7.24.4", "", {}, "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w=="],
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"svelte-sonner": "^0.3.28",
|
"svelte-sonner": "^0.3.28",
|
||||||
|
"svelte-turnstile": "^0.11.0",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tailwind-variants": "^1.0.0",
|
"tailwind-variants": "^1.0.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ import type { auth } from '$lib/server/auth';
|
|||||||
const accountNumberClient = {
|
const accountNumberClient = {
|
||||||
id: 'account-number',
|
id: 'account-number',
|
||||||
getActions: ($fetch) => ({
|
getActions: ($fetch) => ({
|
||||||
createAccount: async (name?: string) =>
|
createAccount: async (name?: string, turnstileToken?: string) =>
|
||||||
$fetch('/create-account', {
|
$fetch('/create-account', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
name,
|
name,
|
||||||
|
turnstileToken,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
signInAccountNumber: async (accountNumber: string) =>
|
signInAccountNumber: async (accountNumber: string) =>
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
import Plus from '@lucide/svelte/icons/plus';
|
import Plus from '@lucide/svelte/icons/plus';
|
||||||
import Loader2 from '@lucide/svelte/icons/loader-2';
|
import Loader2 from '@lucide/svelte/icons/loader-2';
|
||||||
import type { Passkey } from '@better-auth/passkey';
|
import type { Passkey } from '@better-auth/passkey';
|
||||||
|
import { Turnstile } from 'svelte-turnstile';
|
||||||
|
import { dev } from '$app/environment';
|
||||||
|
import { env as publicEnv } from '$env/dynamic/public';
|
||||||
|
|
||||||
const session = authClient.useSession();
|
const session = authClient.useSession();
|
||||||
|
|
||||||
@@ -27,6 +30,11 @@
|
|||||||
let loadedPasskeysForUserId = $state<string | null>(null);
|
let loadedPasskeysForUserId = $state<string | null>(null);
|
||||||
let authScreen = $state<'login' | 'create'>('login');
|
let authScreen = $state<'login' | 'create'>('login');
|
||||||
let passkeyName = $state('');
|
let passkeyName = $state('');
|
||||||
|
let turnstileToken = $state('');
|
||||||
|
let resetTurnstile = $state<(() => void) | undefined>();
|
||||||
|
const turnstileSiteKey = $derived(
|
||||||
|
dev ? '1x00000000000000000000AA' : (publicEnv.PUBLIC_TURNSTILE_SITE_KEY ?? '')
|
||||||
|
);
|
||||||
|
|
||||||
const loadPasskeys = async () => {
|
const loadPasskeys = async () => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -84,18 +92,51 @@
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const createAccount = () =>
|
const clearTurnstile = () => {
|
||||||
runAuthAction(
|
turnstileToken = '';
|
||||||
|
resetTurnstile?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTurnstileCallback = (
|
||||||
|
event: CustomEvent<{ token: string; preClearanceObtained: boolean }>
|
||||||
|
) => {
|
||||||
|
turnstileToken = event.detail.token;
|
||||||
|
authMessage = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTurnstileError = () => {
|
||||||
|
clearTurnstile();
|
||||||
|
authMessage = 'Turnstile verification failed. Please try again.';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTurnstileExpired = () => {
|
||||||
|
clearTurnstile();
|
||||||
|
authMessage = 'Turnstile check expired. Please try again.';
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAccount = async () => {
|
||||||
|
if (!turnstileToken) {
|
||||||
|
authMessage = 'Please complete the Turnstile check before creating an account.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await runAuthAction(
|
||||||
'create-account',
|
'create-account',
|
||||||
() => authClient.createAccount(name),
|
() => authClient.createAccount(name, turnstileToken),
|
||||||
'Account creation failed',
|
'Account creation failed',
|
||||||
async () => {
|
async () => {
|
||||||
await session.get().refetch();
|
await session.get().refetch();
|
||||||
name = '';
|
name = '';
|
||||||
authScreen = 'login';
|
authScreen = 'login';
|
||||||
|
clearTurnstile();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (busyAction !== 'create-account') {
|
||||||
|
clearTurnstile();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const signInWithPasskey = () =>
|
const signInWithPasskey = () =>
|
||||||
runAuthAction(
|
runAuthAction(
|
||||||
'passkey-sign-in',
|
'passkey-sign-in',
|
||||||
@@ -382,10 +423,18 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Turnstile
|
||||||
|
siteKey={turnstileSiteKey}
|
||||||
|
bind:reset={resetTurnstile}
|
||||||
|
on:callback={handleTurnstileCallback}
|
||||||
|
on:error={handleTurnstileError}
|
||||||
|
on:expired={handleTurnstileExpired}
|
||||||
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={createAccount}
|
onclick={createAccount}
|
||||||
disabled={busyAction === 'create-account' || !name}
|
disabled={busyAction === 'create-account' || !name || !turnstileToken}
|
||||||
class="w-full disabled:opacity-50"
|
class="w-full disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{#if busyAction === 'create-account'}
|
{#if busyAction === 'create-account'}
|
||||||
|
|||||||
@@ -21,6 +21,58 @@ const getAnonymousDisplayName = (name?: string | null) => {
|
|||||||
return trimmedName ? trimmedName : 'Chillhop listener';
|
return trimmedName ? trimmedName : 'Chillhop listener';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TurnstileVerifyResult = {
|
||||||
|
success: boolean;
|
||||||
|
hostname?: string;
|
||||||
|
['error-codes']?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClientIpAddress = () => {
|
||||||
|
const headers = getRequestEvent().request.headers;
|
||||||
|
return headers.get('CF-Connecting-IP') ?? headers.get('X-Forwarded-For') ?? undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyTurnstileToken = async (token: string) => {
|
||||||
|
if (!env.TURNSTILE_SECRET_KEY) {
|
||||||
|
throw new APIError('INTERNAL_SERVER_ERROR', {
|
||||||
|
message: 'Turnstile secret key is not configured',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const verificationBody = new FormData();
|
||||||
|
verificationBody.set('secret', env.TURNSTILE_SECRET_KEY);
|
||||||
|
verificationBody.set('response', token);
|
||||||
|
|
||||||
|
const remoteIp = getClientIpAddress();
|
||||||
|
if (remoteIp) {
|
||||||
|
verificationBody.set('remoteip', remoteIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
verificationBody.set('idempotency_key', crypto.randomUUID());
|
||||||
|
|
||||||
|
const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
||||||
|
method: 'POST',
|
||||||
|
body: verificationBody,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new APIError('BAD_REQUEST', {
|
||||||
|
message: 'Turnstile verification failed. Please try again.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = (await response.json()) as TurnstileVerifyResult;
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new APIError('BAD_REQUEST', {
|
||||||
|
message:
|
||||||
|
result['error-codes']?.includes('timeout-or-duplicate')
|
||||||
|
? 'Turnstile check expired. Please try again.'
|
||||||
|
: 'Please complete the Turnstile check before creating an account.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const createAnonymousSession = async (ctx: any, name?: string | null) => {
|
const createAnonymousSession = async (ctx: any, name?: string | null) => {
|
||||||
const user = await ctx.context.internalAdapter.createUser({
|
const user = await ctx.context.internalAdapter.createUser({
|
||||||
email: generateOpaqueIdentifier(),
|
email: generateOpaqueIdentifier(),
|
||||||
@@ -65,9 +117,13 @@ const accountNumber = () =>
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim().max(100).optional(),
|
name: z.string().trim().max(100).optional(),
|
||||||
|
turnstileToken: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
async (ctx) => ctx.json(await createAnonymousSession(ctx, ctx.body.name)),
|
async (ctx) => {
|
||||||
|
await verifyTurnstileToken(ctx.body.turnstileToken);
|
||||||
|
return ctx.json(await createAnonymousSession(ctx, ctx.body.name));
|
||||||
|
},
|
||||||
),
|
),
|
||||||
signInAccountNumber: createAuthEndpoint(
|
signInAccountNumber: createAuthEndpoint(
|
||||||
'/sign-in/account-number',
|
'/sign-in/account-number',
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export async function getChillhopStation(id: number): Promise<Song[]> {
|
|||||||
const finalData = data.map(song => ({
|
const finalData = data.map(song => ({
|
||||||
artists: song.artists,
|
artists: song.artists,
|
||||||
title: song.title,
|
title: song.title,
|
||||||
endpoint: `https://stream.chillhop.com/mp3/${song.fileId}`,
|
endpoint: `/api/chstream/${song.fileId}`,
|
||||||
image: song.image,
|
image: song.image,
|
||||||
label: 'Chillhop Music',
|
label: 'Chillhop Music',
|
||||||
spotifyId: song.spotifyId,
|
spotifyId: song.spotifyId,
|
||||||
|
|||||||
58
src/routes/api/chstream/[fileId]/+server.ts
Normal file
58
src/routes/api/chstream/[fileId]/+server.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// overengineered proxy brought to you by codex.
|
||||||
|
|
||||||
|
import type { RequestHandler } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
const FILE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
||||||
|
|
||||||
|
function isValidFileId(fileId: string) {
|
||||||
|
return FILE_ID_PATTERN.test(fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async ({ params, request, fetch }) => {
|
||||||
|
const { fileId } = params;
|
||||||
|
|
||||||
|
if (!fileId || !isValidFileId(fileId)) {
|
||||||
|
return new Response('Invalid file ID', { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const upstreamHeaders = new Headers();
|
||||||
|
const range = request.headers.get('range');
|
||||||
|
|
||||||
|
if (range) {
|
||||||
|
upstreamHeaders.set('range', range);
|
||||||
|
}
|
||||||
|
|
||||||
|
const upstreamResponse = await fetch(`https://stream.chillhop.com/mp3/${fileId}`, {
|
||||||
|
headers: upstreamHeaders,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!upstreamResponse.ok && upstreamResponse.status !== 206) {
|
||||||
|
return new Response('File not found', { status: upstreamResponse.status === 404 ? 404 : 502 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseHeaders = new Headers();
|
||||||
|
const headersToForward = [
|
||||||
|
'content-type',
|
||||||
|
'content-length',
|
||||||
|
'content-range',
|
||||||
|
'accept-ranges',
|
||||||
|
'etag',
|
||||||
|
'last-modified',
|
||||||
|
'cache-control',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const header of headersToForward) {
|
||||||
|
const value = upstreamResponse.headers.get(header);
|
||||||
|
if (value) {
|
||||||
|
responseHeaders.set(header, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseHeaders.set('Content-Disposition', `inline; filename="${fileId}.mp3"`);
|
||||||
|
|
||||||
|
return new Response(upstreamResponse.body, {
|
||||||
|
status: upstreamResponse.status,
|
||||||
|
statusText: upstreamResponse.statusText,
|
||||||
|
headers: responseHeaders,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { resolve } from '$app/paths';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<a href={resolve('/demo/better-auth')}>better-auth</a>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { redirect } from '@sveltejs/kit';
|
|
||||||
import type { Actions } from './$types';
|
|
||||||
import type { PageServerLoad } from './$types';
|
|
||||||
|
|
||||||
export const load: PageServerLoad = (event) => {
|
|
||||||
if (!event.locals.user) {
|
|
||||||
return redirect(302, '/demo/better-auth/login');
|
|
||||||
}
|
|
||||||
return { user: event.locals.user };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const actions: Actions = {
|
|
||||||
signOut: async (event) => {
|
|
||||||
const { auth } = event.locals;
|
|
||||||
|
|
||||||
await auth.api.signOut({
|
|
||||||
headers: event.request.headers,
|
|
||||||
});
|
|
||||||
return redirect(302, '/demo/better-auth/login');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { authClient } from '$lib';
|
|
||||||
import { enhance } from '$app/forms';
|
|
||||||
import type { PageServerData } from './$types';
|
|
||||||
|
|
||||||
let { data }: { data: PageServerData } = $props();
|
|
||||||
let passkeyMessage = $state('');
|
|
||||||
let addingPasskey = $state(false);
|
|
||||||
|
|
||||||
const addPasskey = async () => {
|
|
||||||
addingPasskey = true;
|
|
||||||
passkeyMessage = '';
|
|
||||||
|
|
||||||
const result = await authClient.passkey.addPasskey({
|
|
||||||
name: 'Primary passkey',
|
|
||||||
authenticatorAttachment: 'platform',
|
|
||||||
});
|
|
||||||
|
|
||||||
addingPasskey = false;
|
|
||||||
passkeyMessage = result.error
|
|
||||||
? result.error.message || 'Failed to add passkey'
|
|
||||||
: 'Passkey added to your account.';
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<h1>Hi, {data.user.name}!</h1>
|
|
||||||
<p>Your user ID is {data.user.id}.</p>
|
|
||||||
<p>Your account number is {data.user.accountNumber}.</p>
|
|
||||||
<button onclick={addPasskey} disabled={addingPasskey}>
|
|
||||||
{addingPasskey ? 'Waiting for passkey...' : 'Add a passkey'}
|
|
||||||
</button>
|
|
||||||
<p>{passkeyMessage}</p>
|
|
||||||
<form method="post" action="?/signOut" use:enhance>
|
|
||||||
<button>Sign out</button>
|
|
||||||
</form>
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { fail, redirect } from '@sveltejs/kit';
|
|
||||||
import type { Actions, PageServerLoad } from './$types';
|
|
||||||
|
|
||||||
import { APIError } from 'better-auth';
|
|
||||||
|
|
||||||
const getErrorMessage = (error: unknown, fallback: string) => {
|
|
||||||
if (error instanceof APIError) return error.message || fallback;
|
|
||||||
if (error instanceof Error) return error.message || fallback;
|
|
||||||
if (typeof error === 'object' && error && 'message' in error && typeof error.message === 'string') {
|
|
||||||
return error.message || fallback;
|
|
||||||
}
|
|
||||||
return fallback;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const load: PageServerLoad = (event) => {
|
|
||||||
if (event.locals.user) {
|
|
||||||
return redirect(302, '/demo/better-auth');
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const actions: Actions = {
|
|
||||||
signInAccountNumber: async (event) => {
|
|
||||||
const { auth } = event.locals;
|
|
||||||
|
|
||||||
const formData = await event.request.formData();
|
|
||||||
const accountNumber = formData.get('accountNumber')?.toString().replace(/\D/g, '') ?? '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
await auth.api.signInAccountNumber({
|
|
||||||
body: {
|
|
||||||
accountNumber,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Demo Better Auth account number sign-in failed', { accountNumber, error });
|
|
||||||
return fail(error instanceof APIError ? 400 : 500, {
|
|
||||||
message: getErrorMessage(error, 'Account number sign-in failed'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect(302, '/demo/better-auth');
|
|
||||||
},
|
|
||||||
createAccount: async (event) => {
|
|
||||||
const { auth } = event.locals;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await auth.api.signInAnonymous();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Demo Better Auth account creation failed', { error });
|
|
||||||
return fail(error instanceof APIError ? 400 : 500, {
|
|
||||||
message: getErrorMessage(error, 'Account creation failed'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect(302, '/demo/better-auth');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import { enhance } from '$app/forms';
|
|
||||||
import { authClient } from '$lib';
|
|
||||||
import type { ActionData } from './$types';
|
|
||||||
|
|
||||||
let { form }: { form: ActionData } = $props();
|
|
||||||
let passkeyError = $state('');
|
|
||||||
let signingInWithPasskey = $state(false);
|
|
||||||
|
|
||||||
const signInWithPasskey = async () => {
|
|
||||||
signingInWithPasskey = true;
|
|
||||||
passkeyError = '';
|
|
||||||
|
|
||||||
const result = await authClient.signIn.passkey({
|
|
||||||
autoFill: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
signingInWithPasskey = false;
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
passkeyError = result.error.message || 'Passkey sign-in failed';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await goto('/demo/better-auth');
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<h1>Account Login</h1>
|
|
||||||
<form method="post" action="?/signInAccountNumber" use:enhance>
|
|
||||||
<label>
|
|
||||||
Account number
|
|
||||||
<input name="accountNumber" inputmode="numeric" maxlength="16" autocomplete="one-time-code" />
|
|
||||||
</label>
|
|
||||||
<button>Sign in with account number</button>
|
|
||||||
<button formaction="?/createAccount">Create account number</button>
|
|
||||||
</form>
|
|
||||||
<p style="color: red">{form?.message ?? ''}</p>
|
|
||||||
<button onclick={signInWithPasskey} disabled={signingInWithPasskey}>
|
|
||||||
{signingInWithPasskey ? 'Waiting for passkey...' : 'Sign in with passkey'}
|
|
||||||
</button>
|
|
||||||
<p style="color: red">{passkeyError}</p>
|
|
||||||
Reference in New Issue
Block a user