feat: add captcha when signing up (closes #53)
This commit is contained in:
parent
bb3d56f548
commit
6f7eb5eeee
23 changed files with 316 additions and 61 deletions
31
frontend/src/app.d.ts
vendored
31
frontend/src/app.d.ts
vendored
|
@ -1,12 +1,31 @@
|
|||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
declare module "svelte-hcaptcha" {
|
||||
import type { SvelteComponent } from "svelte";
|
||||
|
||||
export interface HCaptchaProps {
|
||||
sitekey?: string;
|
||||
apihost?: string;
|
||||
hl?: string;
|
||||
reCaptchaCompat?: boolean;
|
||||
theme?: CaptchaTheme;
|
||||
size?: string;
|
||||
}
|
||||
|
||||
declare class HCaptcha extends SvelteComponent {
|
||||
$$prop_def: HCaptchaProps;
|
||||
}
|
||||
|
||||
export default HCaptcha;
|
||||
}
|
||||
|
||||
export {};
|
||||
|
|
|
@ -150,6 +150,7 @@ export enum ErrorCode {
|
|||
AlreadyLinked = 1014,
|
||||
NotLinked = 1015,
|
||||
LastProvider = 1016,
|
||||
InvalidCaptcha = 1017,
|
||||
|
||||
UserNotFound = 2001,
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
import { fastFetch } from "$lib/api/fetch";
|
||||
import { usernameRegex } from "$lib/api/regex";
|
||||
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
|
||||
import { PUBLIC_HCAPTCHA_SITEKEY } from "$env/static/public";
|
||||
import HCaptcha from "svelte-hcaptcha";
|
||||
import { userStore } from "$lib/store";
|
||||
import { addToast } from "$lib/toast";
|
||||
import { DateTime } from "luxon";
|
||||
|
@ -23,6 +25,7 @@
|
|||
export let remoteName: string | undefined;
|
||||
export let error: APIError | undefined;
|
||||
export let requireInvite: boolean | undefined;
|
||||
export let requireCaptcha: boolean | undefined;
|
||||
export let isDeleted: boolean | undefined;
|
||||
export let ticket: string | undefined;
|
||||
export let token: string | undefined;
|
||||
|
@ -54,7 +57,31 @@
|
|||
let toggleForceDeleteModal = () => (forceDeleteModalOpen = !forceDeleteModalOpen);
|
||||
|
||||
export let linkAccount: () => Promise<void>;
|
||||
export let signupForm: (username: string, inviteCode: string) => Promise<void>;
|
||||
export let signupForm: (
|
||||
username: string,
|
||||
inviteCode: string,
|
||||
captchaToken: string,
|
||||
) => Promise<void>;
|
||||
|
||||
let captchaToken = "";
|
||||
let captcha: any;
|
||||
|
||||
const captchaSuccess = (token: any) => {
|
||||
captchaToken = token.detail.token;
|
||||
};
|
||||
|
||||
let canSubmit = false;
|
||||
$: canSubmit = usernameValid && (!!captchaToken || !requireCaptcha);
|
||||
|
||||
const captchaError = () => {
|
||||
addToast({
|
||||
header: "Captcha failed",
|
||||
body: "There was an error verifying the captcha, please try again.",
|
||||
});
|
||||
captcha.reset();
|
||||
};
|
||||
|
||||
export const resetCaptcha = (): void => captcha.reset();
|
||||
|
||||
const forceDeleteAccount = async () => {
|
||||
try {
|
||||
|
@ -116,7 +143,7 @@
|
|||
<Button color="secondary" href="/settings/auth">Cancel</Button>
|
||||
</div>
|
||||
{:else if ticket}
|
||||
<form on:submit|preventDefault={() => signupForm(username, inviteCode)}>
|
||||
<form on:submit|preventDefault={() => signupForm(username, inviteCode, captchaToken)}>
|
||||
<div>
|
||||
<FormGroup floating label="{authType} username">
|
||||
<Input readonly value={remoteName} />
|
||||
|
@ -144,12 +171,22 @@
|
|||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if requireCaptcha}
|
||||
<div class="mt-2 mx-2 mb-1">
|
||||
<HCaptcha
|
||||
bind:this={captcha}
|
||||
sitekey={PUBLIC_HCAPTCHA_SITEKEY}
|
||||
on:success={captchaSuccess}
|
||||
on:error={captchaError}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="text-muted my-1">
|
||||
By signing up, you agree to the <a href="/page/terms">terms of service</a> and the
|
||||
<a href="/page/privacy">privacy policy</a>.
|
||||
</div>
|
||||
<p>
|
||||
<Button type="submit" color="primary" disabled={!usernameValid}>Sign up</Button>
|
||||
<Button type="submit" color="primary" disabled={!canSubmit}>Sign up</Button>
|
||||
{#if !usernameValid && username.length > 0}
|
||||
<span class="text-danger-emphasis mb-2">That username is not valid.</span>
|
||||
{/if}
|
||||
|
|
|
@ -30,6 +30,7 @@ interface CallbackResponse {
|
|||
discord?: string;
|
||||
ticket?: string;
|
||||
require_invite: boolean;
|
||||
require_captcha: boolean;
|
||||
|
||||
is_deleted: boolean;
|
||||
deleted_at?: string;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import type { APIError, MeUser } from "$lib/api/entities";
|
||||
import { ErrorCode, type APIError, type MeUser } from "$lib/api/entities";
|
||||
import { apiFetch, apiFetchClient } from "$lib/api/fetch";
|
||||
import { userStore } from "$lib/store";
|
||||
import type { PageData } from "./$types";
|
||||
|
@ -10,7 +10,9 @@
|
|||
|
||||
export let data: PageData;
|
||||
|
||||
const signupForm = async (username: string, invite: string) => {
|
||||
let callbackPage: any;
|
||||
|
||||
const signupForm = async (username: string, invite: string, captchaToken: string) => {
|
||||
try {
|
||||
const resp = await apiFetch<SignupResponse>("/auth/discord/signup", {
|
||||
method: "POST",
|
||||
|
@ -18,6 +20,7 @@
|
|||
ticket: data.ticket,
|
||||
username: username,
|
||||
invite_code: invite,
|
||||
captcha_response: captchaToken,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -27,6 +30,10 @@
|
|||
addToast({ header: "Welcome!", body: "Signed up successfully!" });
|
||||
goto(`/@${resp.user.name}`);
|
||||
} catch (e) {
|
||||
if ((e as APIError).code === ErrorCode.InvalidCaptcha) {
|
||||
callbackPage.resetCaptcha();
|
||||
}
|
||||
|
||||
data.error = e as APIError;
|
||||
}
|
||||
};
|
||||
|
@ -48,10 +55,12 @@
|
|||
</script>
|
||||
|
||||
<CallbackPage
|
||||
bind:this={callbackPage}
|
||||
authType="Discord"
|
||||
remoteName={data.discord}
|
||||
error={data.error}
|
||||
requireInvite={data.require_invite}
|
||||
requireCaptcha={data.require_captcha}
|
||||
isDeleted={data.is_deleted}
|
||||
ticket={data.ticket}
|
||||
token={data.token}
|
||||
|
|
|
@ -30,6 +30,7 @@ interface CallbackResponse {
|
|||
google?: string;
|
||||
ticket?: string;
|
||||
require_invite: boolean;
|
||||
require_captcha: boolean;
|
||||
|
||||
is_deleted: boolean;
|
||||
deleted_at?: string;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import type { APIError, MeUser } from "$lib/api/entities";
|
||||
import { ErrorCode, type APIError, type MeUser } from "$lib/api/entities";
|
||||
import { apiFetch, apiFetchClient } from "$lib/api/fetch";
|
||||
import { userStore } from "$lib/store";
|
||||
import type { PageData } from "./$types";
|
||||
|
@ -10,7 +10,9 @@
|
|||
|
||||
export let data: PageData;
|
||||
|
||||
const signupForm = async (username: string, invite: string) => {
|
||||
let callbackPage: any;
|
||||
|
||||
const signupForm = async (username: string, invite: string, captchaToken: string) => {
|
||||
try {
|
||||
const resp = await apiFetch<SignupResponse>("/auth/google/signup", {
|
||||
method: "POST",
|
||||
|
@ -18,6 +20,7 @@
|
|||
ticket: data.ticket,
|
||||
username: username,
|
||||
invite_code: invite,
|
||||
captcha_response: captchaToken,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -27,6 +30,10 @@
|
|||
addToast({ header: "Welcome!", body: "Signed up successfully!" });
|
||||
goto(`/@${resp.user.name}`);
|
||||
} catch (e) {
|
||||
if ((e as APIError).code === ErrorCode.InvalidCaptcha) {
|
||||
callbackPage.resetCaptcha();
|
||||
}
|
||||
|
||||
data.error = e as APIError;
|
||||
}
|
||||
};
|
||||
|
@ -48,10 +55,12 @@
|
|||
</script>
|
||||
|
||||
<CallbackPage
|
||||
bind:this={callbackPage}
|
||||
authType="Google"
|
||||
remoteName={data.google}
|
||||
error={data.error}
|
||||
requireInvite={data.require_invite}
|
||||
requireCaptcha={data.require_captcha}
|
||||
isDeleted={data.is_deleted}
|
||||
ticket={data.ticket}
|
||||
token={data.token}
|
||||
|
|
|
@ -30,6 +30,7 @@ interface CallbackResponse {
|
|||
fediverse?: string;
|
||||
ticket?: string;
|
||||
require_invite: boolean;
|
||||
require_captcha: boolean;
|
||||
|
||||
is_deleted: boolean;
|
||||
deleted_at?: string;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import type { APIError, MeUser } from "$lib/api/entities";
|
||||
import { ErrorCode, type APIError, type MeUser } from "$lib/api/entities";
|
||||
import { apiFetch, apiFetchClient } from "$lib/api/fetch";
|
||||
import { userStore } from "$lib/store";
|
||||
import type { PageData } from "./$types";
|
||||
|
@ -10,7 +10,9 @@
|
|||
|
||||
export let data: PageData;
|
||||
|
||||
const signupForm = async (username: string, invite: string) => {
|
||||
let callbackPage: any;
|
||||
|
||||
const signupForm = async (username: string, invite: string, captchaToken: string) => {
|
||||
try {
|
||||
const resp = await apiFetch<SignupResponse>("/auth/mastodon/signup", {
|
||||
method: "POST",
|
||||
|
@ -19,6 +21,7 @@
|
|||
ticket: data.ticket,
|
||||
username: username,
|
||||
invite_code: invite,
|
||||
captcha_response: captchaToken,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -28,6 +31,10 @@
|
|||
addToast({ header: "Welcome!", body: "Signed up successfully!" });
|
||||
goto(`/@${resp.user.name}`);
|
||||
} catch (e) {
|
||||
if ((e as APIError).code === ErrorCode.InvalidCaptcha) {
|
||||
callbackPage.resetCaptcha();
|
||||
}
|
||||
|
||||
data.error = e as APIError;
|
||||
}
|
||||
};
|
||||
|
@ -50,10 +57,12 @@
|
|||
</script>
|
||||
|
||||
<CallbackPage
|
||||
bind:this={callbackPage}
|
||||
authType="Fediverse"
|
||||
remoteName="{data.fediverse}@{data.instance}"
|
||||
error={data.error}
|
||||
requireInvite={data.require_invite}
|
||||
requireCaptcha={data.require_captcha}
|
||||
isDeleted={data.is_deleted}
|
||||
ticket={data.ticket}
|
||||
token={data.token}
|
||||
|
|
|
@ -29,6 +29,7 @@ interface CallbackResponse {
|
|||
fediverse?: string;
|
||||
ticket?: string;
|
||||
require_invite: boolean;
|
||||
require_captcha: boolean;
|
||||
|
||||
is_deleted: boolean;
|
||||
deleted_at?: string;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import type { APIError, MeUser } from "$lib/api/entities";
|
||||
import { ErrorCode, type APIError, type MeUser } from "$lib/api/entities";
|
||||
import { apiFetch, apiFetchClient } from "$lib/api/fetch";
|
||||
import { userStore } from "$lib/store";
|
||||
import type { PageData } from "./$types";
|
||||
|
@ -10,7 +10,9 @@
|
|||
|
||||
export let data: PageData;
|
||||
|
||||
const signupForm = async (username: string, invite: string) => {
|
||||
let callbackPage: any;
|
||||
|
||||
const signupForm = async (username: string, invite: string, captchaToken: string) => {
|
||||
try {
|
||||
const resp = await apiFetch<SignupResponse>("/auth/misskey/signup", {
|
||||
method: "POST",
|
||||
|
@ -19,6 +21,7 @@
|
|||
ticket: data.ticket,
|
||||
username: username,
|
||||
invite_code: invite,
|
||||
captcha_response: captchaToken,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -28,6 +31,10 @@
|
|||
addToast({ header: "Welcome!", body: "Signed up successfully!" });
|
||||
goto(`/@${resp.user.name}`);
|
||||
} catch (e) {
|
||||
if ((e as APIError).code === ErrorCode.InvalidCaptcha) {
|
||||
callbackPage.resetCaptcha();
|
||||
}
|
||||
|
||||
data.error = e as APIError;
|
||||
}
|
||||
};
|
||||
|
@ -54,6 +61,7 @@
|
|||
remoteName="{data.fediverse}@{data.instance}"
|
||||
error={data.error}
|
||||
requireInvite={data.require_invite}
|
||||
requireCaptcha={data.require_captcha}
|
||||
isDeleted={data.is_deleted}
|
||||
ticket={data.ticket}
|
||||
token={data.token}
|
||||
|
|
|
@ -30,6 +30,7 @@ interface CallbackResponse {
|
|||
tumblr?: string;
|
||||
ticket?: string;
|
||||
require_invite: boolean;
|
||||
require_captcha: boolean;
|
||||
|
||||
is_deleted: boolean;
|
||||
deleted_at?: string;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import type { APIError, MeUser } from "$lib/api/entities";
|
||||
import { ErrorCode, type APIError, type MeUser } from "$lib/api/entities";
|
||||
import { apiFetch, apiFetchClient } from "$lib/api/fetch";
|
||||
import { userStore } from "$lib/store";
|
||||
import type { PageData } from "./$types";
|
||||
|
@ -10,7 +10,9 @@
|
|||
|
||||
export let data: PageData;
|
||||
|
||||
const signupForm = async (username: string, invite: string) => {
|
||||
let callbackPage: any;
|
||||
|
||||
const signupForm = async (username: string, invite: string, captchaToken: string) => {
|
||||
try {
|
||||
const resp = await apiFetch<SignupResponse>("/auth/tumblr/signup", {
|
||||
method: "POST",
|
||||
|
@ -18,6 +20,7 @@
|
|||
ticket: data.ticket,
|
||||
username: username,
|
||||
invite_code: invite,
|
||||
captcha_response: captchaToken,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -27,6 +30,10 @@
|
|||
addToast({ header: "Welcome!", body: "Signed up successfully!" });
|
||||
goto(`/@${resp.user.name}`);
|
||||
} catch (e) {
|
||||
if ((e as APIError).code === ErrorCode.InvalidCaptcha) {
|
||||
callbackPage.resetCaptcha();
|
||||
}
|
||||
|
||||
data.error = e as APIError;
|
||||
}
|
||||
};
|
||||
|
@ -48,10 +55,12 @@
|
|||
</script>
|
||||
|
||||
<CallbackPage
|
||||
bind:this={callbackPage}
|
||||
authType="Tumblr"
|
||||
remoteName={data.tumblr}
|
||||
error={data.error}
|
||||
requireInvite={data.require_invite}
|
||||
requireCaptcha={data.require_captcha}
|
||||
isDeleted={data.is_deleted}
|
||||
ticket={data.ticket}
|
||||
token={data.token}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue