sign up page with captcha
This commit is contained in:
parent
2cf9d54d24
commit
2ddf2c7d31
14 changed files with 370 additions and 174 deletions
1
.env.example
Normal file
1
.env.example
Normal file
|
@ -0,0 +1 @@
|
|||
HCAPTCHA_KEY=ES_
|
30
package-lock.json
generated
30
package-lock.json
generated
|
@ -12,12 +12,16 @@
|
|||
"@sveltejs/enhanced-img": "^0.1.8",
|
||||
"better-sqlite3": "^9.4.3",
|
||||
"cookie": "^0.6.0",
|
||||
"email-validator": "^2.0.4",
|
||||
"hcaptcha": "^0.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"sequelize": "^6.37.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
"svelte-hcaptcha": "^0.1.1",
|
||||
"typeorm": "^0.3.20",
|
||||
"uuid": "^9.0.1",
|
||||
"verify-hcaptcha": "^1.0.0",
|
||||
"vite-plugin-vsharp": "^1.7.3",
|
||||
"ws": "^8.16.0"
|
||||
},
|
||||
|
@ -2938,6 +2942,14 @@
|
|||
"integrity": "sha512-f9iZD1t3CLy1AS6vzM5EKGa6p9pRcOeEFXRFbaG2Ta+Oe7MkfRQ3fsvPYidzHe1h4i0JvIvpcY55C+B6BZNGtQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/email-validator": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/email-validator/-/email-validator-2.0.4.tgz",
|
||||
"integrity": "sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==",
|
||||
"engines": {
|
||||
"node": ">4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
|
@ -3643,6 +3655,11 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hcaptcha": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/hcaptcha/-/hcaptcha-0.1.1.tgz",
|
||||
"integrity": "sha512-iMrDmH2VpIEKOrcKWidVjI89FdDKTEdZ7PfPWkP27sTazIIkob8YfdY2ezaufAnWBiUUcvzsn0qF+dyXtBH2Vw=="
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "10.7.3",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
||||
|
@ -6290,6 +6307,11 @@
|
|||
"svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-hcaptcha": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte-hcaptcha/-/svelte-hcaptcha-0.1.1.tgz",
|
||||
"integrity": "sha512-iFF3HwfrCRciJnDs4Y9/rpP/BM2U/5zt+vh+9d4tALPAHVkcANiJIKqYuS835pIaTm6gt+xOzjfFI3cgiRI29A=="
|
||||
},
|
||||
"node_modules/svelte-hmr": {
|
||||
"version": "0.15.3",
|
||||
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz",
|
||||
|
@ -7087,6 +7109,14 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/verify-hcaptcha": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/verify-hcaptcha/-/verify-hcaptcha-1.0.0.tgz",
|
||||
"integrity": "sha512-WRpRjUdybjvpxjciQ8+SQ1qXYIKlWghVA2sabGuX09s2jSvBHv6Dzz4Kzu8eBBCLvwiZ/6+ursx1aN4w7qRG9w==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.5.tgz",
|
||||
|
|
|
@ -48,12 +48,16 @@
|
|||
"@sveltejs/enhanced-img": "^0.1.8",
|
||||
"better-sqlite3": "^9.4.3",
|
||||
"cookie": "^0.6.0",
|
||||
"email-validator": "^2.0.4",
|
||||
"hcaptcha": "^0.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"sequelize": "^6.37.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
"svelte-hcaptcha": "^0.1.1",
|
||||
"typeorm": "^0.3.20",
|
||||
"uuid": "^9.0.1",
|
||||
"verify-hcaptcha": "^1.0.0",
|
||||
"vite-plugin-vsharp": "^1.7.3",
|
||||
"ws": "^8.16.0"
|
||||
}
|
||||
|
|
33
src/app.pcss
33
src/app.pcss
|
@ -23,6 +23,12 @@ body {
|
|||
font-family: "DM Sans", sans-serif;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
overflow-x: hidden;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
|
@ -41,6 +47,17 @@ h3 {
|
|||
@apply flex w-fit shrink-0 select-none flex-row items-center justify-center gap-4 rounded-xl py-3 pl-7 pr-5 font-bold transition;
|
||||
}
|
||||
|
||||
.button,
|
||||
.cta-button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
}
|
||||
|
||||
.button:disabled,
|
||||
.cta-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.button {
|
||||
border: 2px solid #46424d;
|
||||
@apply flex w-fit shrink-0 select-none flex-row items-center justify-center gap-4 rounded-xl py-3 pl-7 pr-5 font-bold transition;
|
||||
|
@ -62,3 +79,19 @@ h3 {
|
|||
background: #c3c3cd;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.input {
|
||||
border: 2px solid #46424d;
|
||||
/* @apply w-full rounded-xl px-4 py-3 text-sm font-bold transition; */
|
||||
width: 100%;
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.link {
|
||||
@apply text-blue-300 underline transition-all ease-out hover:text-blue-100;
|
||||
}
|
||||
|
|
111
src/components/HCaptcha.svelte
Normal file
111
src/components/HCaptcha.svelte
Normal file
|
@ -0,0 +1,111 @@
|
|||
<script lang="ts" context="module">
|
||||
declare global {
|
||||
interface Window {
|
||||
sitekey: string;
|
||||
hcaptchaOnLoad: Function | null;
|
||||
onSuccess: Function | null;
|
||||
onError: Function;
|
||||
onClose: Function;
|
||||
hcaptcha: any;
|
||||
}
|
||||
}
|
||||
|
||||
declare var hcaptcha: any;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onDestroy, createEventDispatcher, onMount } from "svelte";
|
||||
// @ts-ignore
|
||||
const browser = import.meta.env.SSR === undefined ? true : !import.meta.env.SSR;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let sitekey: string;
|
||||
export let apihost: string = "https://js.hcaptcha.com/1/api.js";
|
||||
export let hl: string = "";
|
||||
export let reCaptchaCompat: boolean = false;
|
||||
export let theme: "light" | "dark" = "light";
|
||||
export let size: "normal" | "compact" | "invisible" = "normal";
|
||||
|
||||
export const reset = () => {
|
||||
if (mounted && loaded && widgetID) hcaptcha.reset(widgetID);
|
||||
};
|
||||
|
||||
export const execute = (options) => {
|
||||
if (mounted && loaded && widgetID) return hcaptcha.execute(widgetID, options);
|
||||
};
|
||||
|
||||
// ensure that all captcha divs on a page are uniquely identifiable
|
||||
const id = Math.floor(Math.random() * 100);
|
||||
|
||||
let mounted = false;
|
||||
let loaded = false;
|
||||
let widgetID;
|
||||
|
||||
// construct the script tag for hCaptcha remote resources
|
||||
const query = new URLSearchParams({
|
||||
recaptchacompat: reCaptchaCompat ? "on" : "off",
|
||||
onload: "hcaptchaOnLoad",
|
||||
render: "explicit",
|
||||
});
|
||||
const scriptSrc = `${apihost}?${query.toString()}`;
|
||||
|
||||
onMount(() => {
|
||||
if (browser && !sitekey) sitekey = window.sitekey;
|
||||
|
||||
if (browser) {
|
||||
window.hcaptchaOnLoad = () => {
|
||||
// consumers can attach custom on:load handlers
|
||||
dispatch("load");
|
||||
loaded = true;
|
||||
};
|
||||
|
||||
window.onSuccess = (token) => {
|
||||
dispatch("success", {
|
||||
token: token,
|
||||
});
|
||||
};
|
||||
|
||||
window.onError = () => {
|
||||
dispatch("error");
|
||||
};
|
||||
|
||||
window.onClose = () => {
|
||||
dispatch("close");
|
||||
};
|
||||
}
|
||||
|
||||
dispatch("mount");
|
||||
mounted = true;
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (browser) {
|
||||
window.hcaptchaOnLoad = null;
|
||||
window.onSuccess = null;
|
||||
}
|
||||
// guard against script loading race conditions
|
||||
// i.e. if component is destroyed before hcaptcha reference is loaded
|
||||
if (loaded) hcaptcha = null;
|
||||
});
|
||||
|
||||
$: if (mounted && loaded) {
|
||||
widgetID = hcaptcha.render(`h-captcha-${id}`, {
|
||||
sitekey,
|
||||
hl, // force a specific localisation
|
||||
theme,
|
||||
callback: "onSuccess",
|
||||
"error-callback": "onError",
|
||||
"close-callback": "onClose",
|
||||
size,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{#if mounted && !window?.hcaptcha}
|
||||
<script src={scriptSrc} async defer></script>
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<div id="h-captcha-{id}" />
|
|
@ -2,25 +2,35 @@ import { db } from "$lib/server/db";
|
|||
import "reflect-metadata";
|
||||
import { building } from "$app/environment";
|
||||
import type { Handle } from "@sveltejs/kit";
|
||||
import {WebSocketServer} from "ws";
|
||||
import { WebSocketServer } from "ws";
|
||||
|
||||
let server: WebSocketServer;
|
||||
|
||||
function initServer() {
|
||||
server = new WebSocketServer({
|
||||
port: 21563,
|
||||
path: "/net"
|
||||
});
|
||||
server.on("connection", (socket) => {
|
||||
socket.on("message", (data) => {
|
||||
socket.send(data);
|
||||
})
|
||||
})
|
||||
try {
|
||||
server = new WebSocketServer({
|
||||
port: 21563,
|
||||
path: "/net",
|
||||
});
|
||||
server.on("error", (err) => {
|
||||
console.error("WebSocket server error:", err);
|
||||
});
|
||||
server.on("connection", (socket) => {
|
||||
socket.on("message", (data) => {
|
||||
socket.send(data);
|
||||
});
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const runAllTheInitFunctions = async () => {
|
||||
if (!db.isInitialized) await db.initialize();
|
||||
if (!server) initServer();
|
||||
if (!server)
|
||||
try {
|
||||
initServer();
|
||||
} catch {
|
||||
console.error("Could not initialize WebSocket server");
|
||||
}
|
||||
};
|
||||
|
||||
if (!building) {
|
||||
|
|
|
@ -22,4 +22,9 @@ export class SuyuUser extends BaseEntity {
|
|||
select: false,
|
||||
})
|
||||
apiKey: string;
|
||||
|
||||
@Column("text", {
|
||||
select: false,
|
||||
})
|
||||
email: string;
|
||||
}
|
||||
|
|
|
@ -3,14 +3,34 @@
|
|||
import { onMount, onDestroy } from "svelte";
|
||||
import Logo from "../components/LogoWithTextHorizontal.svelte";
|
||||
import { CodeBranchOutline, DiscordSolid, DownloadOutline } from "flowbite-svelte-icons";
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
let scrolled = false;
|
||||
|
||||
let cookies: {
|
||||
[key: string]: string;
|
||||
} = {};
|
||||
if (browser) {
|
||||
cookies = Object.fromEntries(
|
||||
document.cookie.split("; ").map((c) => {
|
||||
const [key, value] = c.split("=");
|
||||
return [key, value];
|
||||
}),
|
||||
);
|
||||
}
|
||||
onMount(() => {
|
||||
const handleScroll = () => {
|
||||
scrolled = window.scrollY > 0;
|
||||
};
|
||||
|
||||
handleScroll(); // we can't guarantee that the page starts at the top
|
||||
|
||||
cookies = Object.fromEntries(
|
||||
document.cookie.split("; ").map((c) => {
|
||||
const [key, value] = c.split("=");
|
||||
return [key, value];
|
||||
}),
|
||||
);
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
|
||||
return () => {
|
||||
|
@ -59,7 +79,9 @@
|
|||
>
|
||||
<DiscordSolid />
|
||||
</a>
|
||||
<a href="/account" class="button-sm">Sign in</a>
|
||||
<a href={cookies.token ? "/account" : "/signup"} class="button-sm"
|
||||
>{cookies.token ? "Account" : "Sign up"}</a
|
||||
>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
open-source, forever.
|
||||
</p>
|
||||
<div class="flex flex-row gap-4">
|
||||
<div class="cta-button">
|
||||
<button class="cta-button">
|
||||
Download <svg
|
||||
class=""
|
||||
style="--icon-color:#000"
|
||||
|
@ -46,7 +46,7 @@
|
|||
d="M5.46967 11.4697C5.17678 11.7626 5.17678 12.2374 5.46967 12.5303C5.76256 12.8232 6.23744 12.8232 6.53033 12.5303L10.5303 8.53033C10.8207 8.23999 10.8236 7.77014 10.5368 7.47624L6.63419 3.47624C6.34492 3.17976 5.87009 3.17391 5.57361 3.46318C5.27713 3.75244 5.27128 4.22728 5.56054 4.52376L8.94583 7.99351L5.46967 11.4697Z"
|
||||
></path></svg
|
||||
>
|
||||
</div>
|
||||
</button>
|
||||
<div class="button text-[#8A8F98]">
|
||||
Contribute <svg
|
||||
class=""
|
||||
|
|
|
@ -1,164 +1,42 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import HCaptcha from "$components/HCaptcha.svelte";
|
||||
import { PUBLIC_SITE_KEY } from "$env/static/public";
|
||||
import { SuyuAPI } from "$lib/client/api";
|
||||
import type { CreateAccountResponse } from "$types/api";
|
||||
import type { PageData } from "./$types";
|
||||
import Room from "$components/Room.svelte";
|
||||
|
||||
let usernameInput = "";
|
||||
let emailInput = "";
|
||||
let captchaToken = "";
|
||||
$: disabled = !usernameInput || !emailInput || !captchaToken;
|
||||
|
||||
export let data: PageData;
|
||||
let base64Token: string;
|
||||
$: base64Token = data?.token ? btoa(data.token) : "";
|
||||
|
||||
let usernameToCreate: string;
|
||||
let createBtn: HTMLButtonElement;
|
||||
|
||||
async function createAccount() {
|
||||
createBtn.disabled = true;
|
||||
const response = await SuyuAPI.users.createAccount({ username: usernameToCreate });
|
||||
if (response.success) {
|
||||
data = {
|
||||
...(data || {}),
|
||||
user: response.user,
|
||||
token: response.token,
|
||||
};
|
||||
// add token cookie
|
||||
document.cookie = `token=${response.token}; path=/`;
|
||||
} else {
|
||||
alert("Failed to create account: " + response.error);
|
||||
window.location.reload();
|
||||
}
|
||||
usernameToCreate = "";
|
||||
createBtn.disabled = false;
|
||||
$: {
|
||||
if (Object.keys(data.user).length === 0) goto("/signup");
|
||||
}
|
||||
|
||||
async function deleteAccount() {
|
||||
const response = await SuyuAPI.users.deleteAccount();
|
||||
if (response.success) {
|
||||
data = {
|
||||
...(data || {}),
|
||||
// @ts-expect-error since we're deleting the account, we can't expect the user to still exist
|
||||
user: undefined,
|
||||
token: undefined,
|
||||
};
|
||||
// remove token cookie
|
||||
document.cookie = "token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
} else {
|
||||
alert("Failed to delete account: " + response.error);
|
||||
}
|
||||
}
|
||||
|
||||
async function getWsMessage(event: MessageEvent): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (event.data instanceof Blob) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = () => {
|
||||
resolve((reader.result as string) || "");
|
||||
};
|
||||
|
||||
reader.readAsText(event.data);
|
||||
} else {
|
||||
resolve(event.data);
|
||||
}
|
||||
async function signUp() {
|
||||
const res = await SuyuAPI.users.createAccount({
|
||||
username: usernameInput,
|
||||
email: emailInput,
|
||||
captchaToken,
|
||||
});
|
||||
if (!res.success) {
|
||||
// TODO: modal
|
||||
alert(res.error);
|
||||
return;
|
||||
}
|
||||
// set "token" cookie
|
||||
document.cookie = `token=${res.token}; path=/; max-age=31536000; samesite=strict`;
|
||||
goto("/account");
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const ws = new WebSocket("wss://sjqr2hlh-21563.uks1.devtunnels.ms/net");
|
||||
ws.onmessage = async (event) => {
|
||||
const msg = await getWsMessage(event);
|
||||
console.log(msg);
|
||||
};
|
||||
ws.onopen = () => ws.send("hello, world");
|
||||
});
|
||||
async function captchaComplete(event: CustomEvent<any>) {
|
||||
captchaToken = event.detail.token;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="panel-blur main-panel">
|
||||
<h2>Online Services</h2>
|
||||
<p>
|
||||
{#if data?.token && data?.user && data.user.username}
|
||||
<p>Username: {data.user.username}</p>
|
||||
<p>Token: <code>{base64Token}</code></p>
|
||||
<button class="danger" on:click={deleteAccount}>Delete Account</button>
|
||||
{:else}
|
||||
<p>
|
||||
It appears you don't have an account; please register one to access suyu's online
|
||||
services.
|
||||
</p>
|
||||
<div class="create-account">
|
||||
<input bind:value={usernameToCreate} type="text" placeholder="Username" />
|
||||
<button bind:this={createBtn} on:click={createAccount}>Create Account</button>
|
||||
</div>
|
||||
{/if}
|
||||
</p>
|
||||
<h2>Rooms</h2>
|
||||
<div class="rooms">
|
||||
{#if data.rooms.length > 0}
|
||||
{#each data.rooms as room}
|
||||
<Room {room} />
|
||||
{/each}
|
||||
{:else}
|
||||
<p>No rooms are currently being hosted.</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class="align-center relative flex h-[calc(100vh-200px)] flex-col items-center justify-center gap-6 overflow-hidden"
|
||||
>
|
||||
a
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.main-panel {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
transform: translate(-50%);
|
||||
margin-top: 60px;
|
||||
width: calc(100% - 120px);
|
||||
height: calc(100% - 240px);
|
||||
max-height: 1000px;
|
||||
min-height: 400px;
|
||||
max-width: 1000px;
|
||||
padding: 28px 36px;
|
||||
padding-top: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-panel > h2 {
|
||||
margin-bottom: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.create-account {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.main-panel code {
|
||||
background-color: #222429;
|
||||
border: var(--border-primary);
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
user-select: all;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: pre;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.rooms {
|
||||
margin-top: -16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
height: 0px;
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
--mask-image: linear-gradient(transparent, black 8px, black calc(100% - 32px), transparent),
|
||||
linear-gradient(to left, black 8px, transparent 8px);
|
||||
padding-top: 8px;
|
||||
padding-bottom: 32px;
|
||||
mask-image: var(--mask-image);
|
||||
-webkit-mask-image: var(--mask-image);
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -12,6 +12,10 @@ import type {
|
|||
} from "$types/api";
|
||||
import crypto from "crypto";
|
||||
import { promisify } from "util";
|
||||
import { verify } from "hcaptcha";
|
||||
import { PUBLIC_SITE_KEY } from "$env/static/public";
|
||||
import { HCAPTCHA_KEY } from "$env/static/private";
|
||||
import validator from "validator";
|
||||
|
||||
const randomBytes = promisify(crypto.randomBytes);
|
||||
|
||||
|
@ -27,24 +31,42 @@ async function genKey(username: string) {
|
|||
return apiKey;
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
export async function POST({ request, getClientAddress }) {
|
||||
const body: CreateAccountRequest = await request.json();
|
||||
if (!body.username) {
|
||||
if (!body.username || !body.email || !body.captchaToken) {
|
||||
return json<CreateAccountResponse>({
|
||||
success: false,
|
||||
error: "username is required",
|
||||
error: "missing fields",
|
||||
});
|
||||
}
|
||||
if (!validator.isEmail(body.email)) {
|
||||
return json<CreateAccountResponse>({
|
||||
success: false,
|
||||
error: "invalid email",
|
||||
});
|
||||
}
|
||||
const res = await verify(HCAPTCHA_KEY, body.captchaToken, getClientAddress(), PUBLIC_SITE_KEY);
|
||||
if (!res.success) {
|
||||
return json<CreateAccountResponse>({
|
||||
success: false,
|
||||
error: "missing fields!",
|
||||
});
|
||||
}
|
||||
// check if user exists
|
||||
const user = await userRepo.findOne({
|
||||
where: {
|
||||
username: body.username,
|
||||
},
|
||||
where: [
|
||||
{
|
||||
username: body.username,
|
||||
},
|
||||
{
|
||||
email: body.email,
|
||||
},
|
||||
],
|
||||
});
|
||||
if (user) {
|
||||
return json<CreateAccountResponse>({
|
||||
success: false,
|
||||
error: "username already exists",
|
||||
error: "user already exists",
|
||||
});
|
||||
}
|
||||
// the api key can only be 80 characters total, including the username and colon
|
||||
|
@ -55,6 +77,7 @@ export async function POST({ request }) {
|
|||
displayName: body.username,
|
||||
roles: serializeRoles(["user"]),
|
||||
apiKey: key,
|
||||
email: body.email,
|
||||
});
|
||||
await userRepo.save(createdUser);
|
||||
return json<CreateAccountResponse>({
|
||||
|
|
13
src/routes/signup/+page.server.ts
Normal file
13
src/routes/signup/+page.server.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { RoomManager } from "$lib/server/class/Room.js";
|
||||
import { useAuth } from "$lib/util/api";
|
||||
|
||||
export async function load(opts) {
|
||||
const apiKey = opts.cookies.get("token");
|
||||
const user = await useAuth(apiKey || "unused");
|
||||
const rooms = RoomManager.getRooms().map((r) => r.toJSON());
|
||||
return {
|
||||
user: { ...user },
|
||||
rooms,
|
||||
token: apiKey,
|
||||
};
|
||||
}
|
64
src/routes/signup/+page.svelte
Normal file
64
src/routes/signup/+page.svelte
Normal file
|
@ -0,0 +1,64 @@
|
|||
<script lang="ts">
|
||||
import { browser } from "$app/environment";
|
||||
import { goto } from "$app/navigation";
|
||||
import HCaptcha from "$components/HCaptcha.svelte";
|
||||
import { PUBLIC_SITE_KEY } from "$env/static/public";
|
||||
import { SuyuAPI } from "$lib/client/api";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
let usernameInput = "";
|
||||
let emailInput = "";
|
||||
let captchaToken = "";
|
||||
$: disabled = !usernameInput || !emailInput || !captchaToken;
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
if (Object.keys(data.user).length !== 0 && browser) goto("/account");
|
||||
|
||||
async function signUp() {
|
||||
const res = await SuyuAPI.users.createAccount({
|
||||
username: usernameInput,
|
||||
email: emailInput,
|
||||
captchaToken,
|
||||
});
|
||||
if (!res.success) {
|
||||
// TODO: modal
|
||||
alert(res.error);
|
||||
return;
|
||||
}
|
||||
// set "token" cookie
|
||||
document.cookie = `token=${res.token}; path=/; max-age=31536000; samesite=strict`;
|
||||
goto("/account");
|
||||
}
|
||||
|
||||
async function captchaComplete(event: CustomEvent<any>) {
|
||||
captchaToken = event.detail.token;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="align-center relative flex h-[calc(100vh-200px)] flex-col items-center justify-center gap-6 overflow-hidden"
|
||||
>
|
||||
<div class="flex h-fit w-full max-w-[500px] flex-col rounded-[2.25rem] bg-black p-10">
|
||||
<h1 class="text-[60px] md:leading-[1.1]">Sign up</h1>
|
||||
<div class="mt-4 flex flex-col gap-4">
|
||||
<p>
|
||||
suyu believes in user privacy; as such, usernames are distributed on a first-come,
|
||||
first-serve basis, with no password required. Accounts are used for:
|
||||
</p>
|
||||
<ul class="[&>*]:before:mr-3 [&>*]:before:content-['•']">
|
||||
<li>Creating rooms</li>
|
||||
<li>Adding friends</li>
|
||||
</ul>
|
||||
<p>
|
||||
Lost your account? <a class="link" href="https://discord.gg/suyu" target="_blank"
|
||||
>Contact us</a
|
||||
>.
|
||||
</p>
|
||||
<input bind:value={emailInput} class="input" type="text" placeholder="Recovery Email" />
|
||||
<input bind:value={usernameInput} class="input" type="text" placeholder="Username" />
|
||||
<HCaptcha on:success={captchaComplete} theme="dark" sitekey={PUBLIC_SITE_KEY} />
|
||||
<button {disabled} on:click={signUp} class="cta-button mt-2">Sign up</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
2
src/types/api.d.ts
vendored
2
src/types/api.d.ts
vendored
|
@ -9,6 +9,8 @@ export interface GenericFailureResponse {
|
|||
|
||||
export interface CreateAccountRequest {
|
||||
username: string;
|
||||
email: string;
|
||||
captchaToken: string;
|
||||
}
|
||||
|
||||
export interface CreateAccountResponseSuccess {
|
||||
|
|
Loading…
Reference in a new issue