diff options
| author | Adam Soltys <[email protected]> | 2024-09-10 20:39:47 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2024-09-17 09:18:36 -0700 |
| commit | 1f63848ca9fb01336f04b41434de1cc06a5b3d93 (patch) | |
| tree | 2affa3041863be365f33d99e1f32bda60cf93a32 /src/routes/(static) | |
| parent | 6e48c584605bec5623c12c470d04d07303983801 (diff) | |
| download | coinos-ui-1f63848ca9fb01336f04b41434de1cc06a5b3d93.tar.gz coinos-ui-1f63848ca9fb01336f04b41434de1cc06a5b3d93.zip | |
move routes
Diffstat (limited to 'src/routes/(static)')
| -rw-r--r-- | src/routes/(static)/+layout.svelte | 2 | ||||
| -rw-r--r-- | src/routes/(static)/login/+page.server.ts | 29 | ||||
| -rw-r--r-- | src/routes/(static)/login/+page.svelte | 171 | ||||
| -rw-r--r-- | src/routes/(static)/register/+page.server.ts | 41 | ||||
| -rw-r--r-- | src/routes/(static)/register/+page.svelte | 292 | ||||
| -rw-r--r-- | src/routes/(static)/support/+page.svelte | 134 | ||||
| -rw-r--r-- | src/routes/(static)/support/+server.ts | 9 |
7 files changed, 677 insertions, 1 deletions
diff --git a/src/routes/(static)/+layout.svelte b/src/routes/(static)/+layout.svelte index 13073d5a..4b350bd0 100644 --- a/src/routes/(static)/+layout.svelte +++ b/src/routes/(static)/+layout.svelte @@ -9,7 +9,7 @@ <SvelteToast options={{ reversed: true, intro: { y: 192 } }} /> <LandingHeader {user} /> -<main class="container flex px-10 md:px-40 mx-auto min-h-[600px] pb-20"> +<main class="container flex px-4 md:px-40 mx-auto min-h-[600px] py-28"> <slot /> </main> <Footer /> diff --git a/src/routes/(static)/login/+page.server.ts b/src/routes/(static)/login/+page.server.ts new file mode 100644 index 00000000..fd7fec8c --- /dev/null +++ b/src/routes/(static)/login/+page.server.ts @@ -0,0 +1,29 @@ +import { fail, redirect } from "@sveltejs/kit"; +import { login, fd } from "$lib/utils"; + +export const load = async ({ parent }) => { + let { user } = await parent(); + if (user?.pubkey) redirect(307, `/${user.username}`); +}; + +export const actions = { + default: async ({ cookies, request }) => { + let form = await fd(request); + let { username, password, token, loginRedirect } = form; + let user = { username, password, token }; + + if (loginRedirect === "undefined") loginRedirect = undefined; + + try { + await login( + user, + cookies, + request.headers.get("cf-connecting-ip") + ); + } catch (e: any) { + return fail(400, { error: "Login failed", message: e.message, ...form }); + } + + redirect(307, loginRedirect || `/${user.username}`); + }, +}; diff --git a/src/routes/(static)/login/+page.svelte b/src/routes/(static)/login/+page.svelte new file mode 100644 index 00000000..84e1ea89 --- /dev/null +++ b/src/routes/(static)/login/+page.svelte @@ -0,0 +1,171 @@ +<script> + import { onMount } from "svelte"; + import { applyAction, deserialize } from "$app/forms"; + import { tick } from "svelte"; + import { fly } from "svelte/transition"; + import { enhance } from "$app/forms"; + import Pin from "$comp/Pin.svelte"; + import Icon from "$comp/Icon.svelte"; + import Spinner from "$comp/Spinner.svelte"; + import { focus, fail } from "$lib/utils"; + import { password, pin, loginRedirect } from "$lib/store"; + import { t } from "$lib/translations"; + import { page } from "$app/stores"; + import { generate } from "$lib/nostr"; + import { invalidateAll } from "$app/navigation"; + + onMount(() => { + $pin = undefined; + localStorage.clear(); + sessionStorage.clear(); + + let lang = $page.url.searchParams.get("lang"); + if (lang) document.cookie = `lang=${lang} ;`; + }); + + export let form; + + let token, formElement; + let code = []; + let redirect; + + $password = undefined; + + $: need2fa = form?.message === "2fa"; + let cancel = () => (need2fa = false); + $: if (form?.message === "2fa" && form.token === token) token = ""; + + let username, email, btn; + + $: update(form); + let update = (form) => form && ({ username, password: $password } = form); + + $: token && token?.length === 6 && tick().then(() => btn.click()); + + let revealPassword = false; + + let loading; + async function handleSubmit(e) { + loading = true; + + let data = new FormData(this); + let user = Object.fromEntries(data); + await generate(user); + + for (let k in user) { + data.set(k, user[k]); + } + + const response = await fetch("/login", { + method: "POST", + body: data, + }); + + const result = deserialize(await response.text()); + + if (result.type === "success") { + await invalidateAll(); + } + + applyAction(result); + loading = false; + } +</script> + +{#if need2fa} + <Pin bind:value={token} title="Enter 2FA Code" {cancel} notify={false} /> +{/if} + +<div class="mx-auto md:shadow-xl rounded-3xl max-w-xl w-full md:w-[480px] md:p-8"> + <h1 class="text-2xl font-bold text-center">{$t("login.signIn")}</h1> + + {#if form?.error} + <div class="text-red-600 text-center" in:fly> + {form.error} + </div> + {/if} + + <form class="space-y-5" on:submit|preventDefault={handleSubmit} method="POST"> + <input + type="hidden" + name="loginRedirect" + value={$loginRedirect || $page.url.searchParams.get("redirect")} + /> + <input type="hidden" name="token" value={token} /> + + <div> + <label for="username" class="font-semibold">{$t("login.username")}</label> + <input + name="username" + type="text" + required + class="bg-primary" + bind:value={username} + use:focus + autocapitalize="none" + /> + </div> + + <div class="relative"> + <label for="password" class="block font-semibold" + >{$t("login.password")}</label + > + {#if revealPassword} + <input + name="password" + type="text" + required + class="bg-primary" + bind:value={$password} + autocapitalize="none" + /> + {:else} + <input + name="password" + type="password" + required + class="bg-primary" + bind:value={$password} + autocapitalize="none" + /> + {/if} + <button + type="button" + on:click={() => (revealPassword = !revealPassword)} + class="absolute right-5 top-10" + > + <Icon icon={revealPassword ? "eye" : "eye-off"} /> + </button> + </div> + + <div class="flex justify-end items-center"> + <a href="/forgot" class="underline underline-offset-4 text-black" + >{$t("login.forgotUserOrPassword")}</a + > + </div> + + <button + type="submit" + class="bg-black text-white w-full rounded-2xl p-4 font-semibold hover:opacity-80" + disabled={loading} + bind:this={btn} + > + {#if loading} + <Spinner /> + {:else} + {$t("login.signIn")} + {/if} + </button> + + + <p class="text-secondary text-center font-medium"> + {$t("login.noAccount")} + <a + href={"/register"} + class="block md:inline text-black underline underline-offset-4 hover:opacity-80" + > + {$t("login.register")} + </a> + </p> + </form> +</div> diff --git a/src/routes/(static)/register/+page.server.ts b/src/routes/(static)/register/+page.server.ts new file mode 100644 index 00000000..731530c4 --- /dev/null +++ b/src/routes/(static)/register/+page.server.ts @@ -0,0 +1,41 @@ +import { fail, redirect } from "@sveltejs/kit"; +import { fd, post, login } from "$lib/utils"; + +export const load = async ({ parent }) => { + let { user } = await parent(); + if (user) redirect(307, `/${user.username}`); + + let index = Math.floor(Math.random() * 64) + 1; + + return { index }; +}; + +export const actions = { + default: async ({ cookies, request }) => { + let ip = request.headers.get("cf-connecting-ip"); + + let form = await fd(request); + let { profile, username, password, cipher, salt, pubkey, loginRedirect } = + form; + let user = { profile, username, password, cipher, salt, pubkey }; + let error; + + if (loginRedirect === "undefined") loginRedirect = undefined; + + try { + await post("/register", { user }, { "cf-connecting-ip": ip }); + } catch (e: any) { + error = e.message; + } + + try { + await login(user, cookies, ip); + error = null; + } catch (e: any) { + error ||= e.message; + } + + if (error) return fail(400, { error }); + redirect(307, loginRedirect || `/${user.username}`); + }, +}; diff --git a/src/routes/(static)/register/+page.svelte b/src/routes/(static)/register/+page.svelte new file mode 100644 index 00000000..b6a4c44e --- /dev/null +++ b/src/routes/(static)/register/+page.svelte @@ -0,0 +1,292 @@ +<script> + import punks from "$lib/punks"; + import { upload } from "$lib/upload"; + import { afterNavigate, invalidateAll } from "$app/navigation"; + import { applyAction, deserialize } from "$app/forms"; + import { tick } from "svelte"; + import { fly } from "svelte/transition"; + import { enhance } from "$app/forms"; + + import Pin from "$comp/Pin.svelte"; + import Icon from "$comp/Icon.svelte"; + import Spinner from "$comp/Spinner.svelte"; + + import { focus, fail } from "$lib/utils"; + import { avatar, password, loginRedirect } from "$lib/store"; + import { t } from "$lib/translations"; + import { page } from "$app/stores"; + import { generate } from "$lib/nostr"; + import { + NumberDictionary, + uniqueNamesGenerator, + colors, + adjectives, + animals, + } from "unique-names-generator"; + + export let form; + export let data; + + $: data && ($avatar = undefined); + let username; + let { index } = data; + + let cleared; + let clear = () => { + if (!cleared) { + cleared = true; + username = ""; + $password = ""; + revealPassword = false; + } + }; + + let refresh = async () => { + username = uniqueNamesGenerator({ + dictionaries: [animals, NumberDictionary.generate({ min: 10, max: 99 })], + length: 2, + separator: "", + }); + + $password = uniqueNamesGenerator({ + dictionaries: [colors, NumberDictionary.generate({ min: 100, max: 999 })], + length: 2, + separator: "", + }); + + revealPassword = true; + }; + + afterNavigate(async () => { + try { + await invalidateAll(); + } catch (e) { + console.log(e); + } + }); + + let token, formElement; + let code = []; + let redirect; + + $: need2fa = form?.message === "2fa"; + let cancel = () => (need2fa = false); + $: if (form?.message === "2fa" && form.token === token) token = ""; + + let email, btn; + + $: update(form); + let update = (form) => form && ({ username, password: $password } = form); + + $: token && token?.length === 6 && tick().then(() => btn.click()); + + let revealPassword; + + let loading; + async function handleSubmit(e) { + loading = true; + + let data = new FormData(this); + let user = Object.fromEntries(data); + await generate(user); + + for (let k in user) { + data.set(k, user[k]); + } + + data.set("profile", punks[index]); + if ($avatar) { + try { + let { hash } = JSON.parse( + await upload($avatar.file, $avatar.type, $avatar.progress, token) + ); + + data.set("profile", hash); + await fetch(`/api/public/${hash}.webp`, { + cache: "reload", + mode: "no-cors", + }); + } catch (e) { + console.log("problem uploading avatar", e); + } + } + + const response = await fetch("/register", { + method: "POST", + body: data, + }); + + const result = deserialize(await response.text()); + + if (result.type === "success") { + await invalidateAll(); + } + + applyAction(result); + loading = false; + } + + let avatarInput; + $: src = `/api/public/${punks[index]}.webp`; + let decr = () => (index = index <= 0 ? 63 : index - 1); + let incr = () => (index = index >= 63 ? 0 : index + 1); + let selectAvatar = () => avatarInput.click(); + + let progress; + let handleFile = async ({ target }) => { + let type = "profile"; + let file = target.files[0]; + if (!file) return; + + if (file.size > 10000000) form.error = "File too large"; + $avatar = { file, type, progress }; + + var reader = new FileReader(); + reader.onload = async (e) => { + src = e.target.result; + }; + + reader.readAsDataURL(file); + }; +</script> + +{#if need2fa} + <Pin bind:value={token} title="Enter 2FA Code" {cancel} notify={false} /> +{/if} + +<div + class="mx-auto md:shadow-xl rounded-3xl max-w-xl w-full md:w-[480px] md:p-8 space-y-5" +> + <h1 class="text-2xl font-bold text-center">{$t("login.createAccount")}</h1> + <input + type="file" + class="hidden" + bind:this={avatarInput} + on:change={(e) => handleFile(e, "profile")} + /> + + <div class="relative"> + <button + class="absolute w-8 h-12 left-12 bg-white rounded top-12" + on:click={decr} + > + <Icon icon="chevron-left" style="w-8 " /> + </button> + <button class="block relative w-32 mx-auto" on:click={selectAvatar}> + <button + class="w-32 h-32 rounded-full border-4 border-white overflow-hidden flex mx-auto relative" + > + <img + {src} + class="w-full h-full object-cover object-center overflow-hidden" + alt={username} + /> + </button> + <button + class="absolute bg-white rounded-full p-2 mx-auto right-0 bottom-0 z-10 bg-opacity-80" + > + <Icon icon="upload" style="w-8" /> + </button> + </button> + <button + class="absolute w-8 h-12 right-12 bg-white rounded top-12" + on:click={incr} + > + <Icon icon="chevron-left" style="w-8 rotate-180" /> + </button> + </div> + + {#if form?.error} + <div class="text-red-600 text-center" in:fly> + {form.error} + </div> + {/if} + + <form class="space-y-5" on:submit|preventDefault={handleSubmit} method="POST"> + <input + type="hidden" + name="loginRedirect" + value={$loginRedirect || $page.url.searchParams.get("redirect")} + /> + <input type="hidden" name="token" value={token} /> + + <div class="relative"> + <label for="username" class="font-semibold">{$t("login.username")}</label> + <input + use:focus + name="username" + type="text" + required + class="bg-primary" + bind:value={username} + on:focus={clear} + autocapitalize="none" + /> + <button + tabindex="-1" + type="button" + on:click={refresh} + class="absolute right-0 top-4" + > + <Icon icon="random" style="w-10 m-4" /> + </button> + </div> + + <div class="relative"> + <label for="password" class="block font-semibold" + >{$t("login.password")}</label + > + {#if revealPassword} + <input + name="password" + type="text" + required + class="bg-primary" + bind:value={$password} + autocapitalize="none" + /> + {:else} + <input + name="password" + type="password" + required + class="bg-primary" + bind:value={$password} + autocapitalize="none" + on:focus={clear} + /> + {/if} + <button + type="button" + tabindex="-1" + on:click={() => (revealPassword = !revealPassword)} + class="absolute right-0 top-4" + > + <Icon icon={revealPassword ? "eye" : "eye-off"} style="m-6" /> + </button> + </div> + + <button + type="submit" + class="bg-black text-white w-full rounded-2xl p-4 font-semibold hover:opacity-80" + disabled={loading} + bind:this={btn} + > + {#if loading} + <Spinner /> + {:else} + {$t("login.register")} + {/if} + </button> + + <p class="text-secondary text-center font-medium"> + {$t("login.haveAccount")} + <a + href={"/login"} + class="block md:inline text-black underline underline-offset-4 hover:opacity-80" + > + {$t("login.signIn")} + </a> + </p> + </form> +</div> diff --git a/src/routes/(static)/support/+page.svelte b/src/routes/(static)/support/+page.svelte new file mode 100644 index 00000000..20e41280 --- /dev/null +++ b/src/routes/(static)/support/+page.svelte @@ -0,0 +1,134 @@ +<script> + import { browser } from "$app/environment"; + import { onDestroy } from "svelte"; + import { fail, success, post } from "$lib/utils"; + import { scale } from "svelte/transition"; + import Icon from "$comp/Icon.svelte"; + import { t } from "$lib/translations"; + + export let data; + + let { user } = data; + $: if (user) { + data.subject = { ...user }; + } + + let email; + let message; + let sent; + let recaptchaSiteKey = "6LfCd8YkAAAAANmVJgzN3SQY3n3fv1RhiS5PgMYM"; + + let submit = (e) => { + e.preventDefault(); + grecaptcha.ready(() => { + grecaptcha.execute(recaptchaSiteKey, { action: "submit" }).then((token) => + post("/support", { username: user?.username, email, message, token }) + .then(() => (sent = true)) + .catch(() => fail("problem submitting")) + ); + }); + }; + + onDestroy(() => { + if (!browser) return; + const nodeBadge = document.querySelector(".grecaptcha-badge"); + if (nodeBadge) { + document.body.removeChild(nodeBadge.parentNode); + } + + const scriptSelector = + "script[src='https://www.google.com/recaptcha/api.js?render=" + + recaptchaSiteKey + + "']"; + const script = document.querySelector(scriptSelector); + if (script) { + script.remove(); + } + }); +</script> + +<svelte:head + ><script + src="https://www.google.com/recaptcha/api.js?render=6LfCd8YkAAAAANmVJgzN3SQY3n3fv1RhiS5PgMYM" + ></script></svelte:head +> + +<div + class="container max-w-lg mx-auto mt-20 space-y-8 shadow-xl rounded-3xl p-10" +> + {#if sent} + <h1 class="text-center text-3xl md:text-4xl font-semibold">Thank you!</h1> + <div class="text-center text-lg text-secondary"> + Someone will be in touch shortly. + </div> + + <div> + <a href={user ? `/${user.username}` : "/"}> + <button + class="rounded-full border py-2 px-5 font-bold hover:opacity-80 w-full bg-black text-white" + >Ok</button + > + </a> + </div> + {:else} + <h1 class="text-center text-3xl md:text-4xl font-semibold mb-8"> + {$t("user.support.header")} + </h1> + + <p class="text-secondary text-lg"> + Fill out this form or email us directly at <a + class="underline" + href="mailto:[email protected]">[email protected]</a + > and we'll do our best to get back to you in a timely manner. + </p> + + <form on:submit={submit}> + <div class="mb-4"> + <label for="account" class="font-semibold" + >{$t("user.support.accountName")}</label + > + <input + class="bg-primary" + type="text" + name="account" + required + value={user?.username || ""} + /> + </div> + + <div class="mb-4"> + <label for="email" class="font-semibold" + >{$t("user.support.email")}</label + > + <input + class="bg-primary" + type="email" + name="email" + bind:value={email} + required + /> + </div> + + <div class="mb-4"> + <label for="message" class="font-semibold" + >{$t("user.support.message")}</label + > + <textarea + rows={5} + class="bg-primary" + type="text" + name="message" + bind:value={message} + required + /> + </div> + + <button + type="submit" + disabled={!email || !message} + class="g-recaptcha bg-black text-white font-bold rounded-xl py-3 w-full mx-auto hover:opacity-80" + >{$t("user.support.send")}</button + > + </form> + {/if} +</div> diff --git a/src/routes/(static)/support/+server.ts b/src/routes/(static)/support/+server.ts new file mode 100644 index 00000000..e9a16cfc --- /dev/null +++ b/src/routes/(static)/support/+server.ts @@ -0,0 +1,9 @@ +import { json } from "@sveltejs/kit"; +import { auth, post } from "$lib/utils"; + +export const prerender = false; + +export async function POST({ cookies, request }) { + let result = await post("/email", await request.json(), auth(cookies)); + return json(result); +} |
