Add hcaptcha
This commit is contained in:
parent
cfec56bcf9
commit
00d2b02fb2
8
src/app.d.ts
vendored
8
src/app.d.ts
vendored
@ -6,6 +6,10 @@ type SessionData = {
|
||||
user?: UserSession;
|
||||
};
|
||||
|
||||
type HCaptcha = {
|
||||
render(id: string, config: { sitekey: string; size: string; theme: string }): string;
|
||||
};
|
||||
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
@ -14,6 +18,10 @@ declare global {
|
||||
? { [P in keyof O]: O[P] }
|
||||
: never;
|
||||
|
||||
interface Window {
|
||||
hcaptcha: HCaptcha | null;
|
||||
}
|
||||
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
|
||||
|
@ -6,15 +6,11 @@
|
||||
|
||||
<style>
|
||||
.aside-wrapper {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.aside-wrapper,
|
||||
.aside-inner {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.aside-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
24
src/lib/components/form/HCaptcha.svelte
Normal file
24
src/lib/components/form/HCaptcha.svelte
Normal file
@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import { themeMode } from '$lib/theme-mode';
|
||||
import { waitIsTruthy } from '$lib/utils';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(async () => {
|
||||
if (!browser) return;
|
||||
await waitIsTruthy(() => !!window.hcaptcha);
|
||||
if (!window.hcaptcha) return;
|
||||
window.hcaptcha.render('hcaptcha-element', {
|
||||
sitekey: env.PUBLIC_HCAPTCHA_KEY,
|
||||
size: '',
|
||||
theme: $themeMode
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<script src="https://js.hcaptcha.com/1/api.js?render=explicit" async defer></script>
|
||||
</svelte:head>
|
||||
|
||||
<div class="h-captcha" id="hcaptcha-element"></div>
|
@ -66,7 +66,8 @@
|
||||
"otpFailed": "The code you entered was invalid. Please note that you will be given a new QR code for subsequent retries.",
|
||||
"required": "Please fill in the required fields",
|
||||
"existingRegistration": "The username or email address is already in use.",
|
||||
"activationFailed": "Your account could not be activated - the URL might have been used or expired already."
|
||||
"activationFailed": "Your account could not be activated - the URL might have been used or expired already.",
|
||||
"invalidCaptcha": "Please complete the captcha challenge."
|
||||
},
|
||||
"otp": {
|
||||
"title": "Two-factor authentication",
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"description": "{{siteName}} is a Single-Sign-On service used by other applications.",
|
||||
"metaDescription": "{{siteName}} - Single-Sign-On service",
|
||||
"cookieDisclaimer": "The website may use temporary cookies for storing your login session and ensuring your security. This web service is <a href=\"https://git.icynet.eu/IcyNetwork/sso-core\" target=\"_blank\">completely open source</a> and can be audited by anyone.",
|
||||
"submit": "Submit",
|
||||
"cancel": "Cancel",
|
||||
|
@ -13,3 +13,22 @@ export const hasPrivileges = (list: string[], privileges: RequiredPrivileges) =>
|
||||
}
|
||||
return list.includes(item);
|
||||
});
|
||||
|
||||
export const waitIsTruthy = (check: () => boolean, checkInterval = 500, checkTimeout = 5000) => {
|
||||
let time = 0;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const interval = setInterval(() => {
|
||||
if (check.call(null)) {
|
||||
clearInterval(interval);
|
||||
return resolve();
|
||||
}
|
||||
|
||||
if (time > checkTimeout) {
|
||||
clearInterval(interval);
|
||||
return reject();
|
||||
}
|
||||
|
||||
time += checkInterval;
|
||||
}, checkInterval);
|
||||
});
|
||||
};
|
||||
|
@ -1,8 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { env } from '$env/dynamic/public';
|
||||
import { t } from '$lib/i18n';
|
||||
import { forwardInitialTheme } from '$lib/theme-mode';
|
||||
import '../app.css';
|
||||
|
||||
forwardInitialTheme();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<meta name="title" content={env.PUBLIC_SITE_NAME} />
|
||||
<meta
|
||||
name="description"
|
||||
content={$t('common.metaDescription', { siteName: env.PUBLIC_SITE_NAME })}
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<slot></slot>
|
||||
|
@ -12,9 +12,16 @@ interface RegisterData {
|
||||
displayName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
'h-captcha-response': string;
|
||||
}
|
||||
|
||||
const fields: (keyof RegisterData)[] = ['username', 'displayName', 'email', 'password'];
|
||||
const fields: (keyof RegisterData)[] = [
|
||||
'username',
|
||||
'displayName',
|
||||
'email',
|
||||
'password',
|
||||
'h-captcha-response'
|
||||
];
|
||||
|
||||
const limiter = new RateLimiter({
|
||||
IP: [6, 'm']
|
||||
@ -32,7 +39,14 @@ export const actions = {
|
||||
|
||||
const body = await request.formData();
|
||||
const changes = Changesets.take<RegisterData>(fields, body);
|
||||
const { username, displayName, email, password } = changes;
|
||||
const {
|
||||
username,
|
||||
displayName,
|
||||
email,
|
||||
password,
|
||||
['h-captcha-response']: captchaToken
|
||||
} = changes;
|
||||
|
||||
// Each field must be present
|
||||
if (!username || !displayName || !email || !password) {
|
||||
return fail(400, {
|
||||
@ -87,6 +101,39 @@ export const actions = {
|
||||
});
|
||||
}
|
||||
|
||||
if (env.HCAPTCHA_SECRET) {
|
||||
if (!captchaToken) {
|
||||
return fail(400, {
|
||||
username,
|
||||
displayName,
|
||||
email,
|
||||
errors: ['invalidCaptcha'],
|
||||
fields: ['hcaptcha']
|
||||
});
|
||||
}
|
||||
|
||||
const requestBody = new FormData();
|
||||
requestBody.append('response', captchaToken);
|
||||
requestBody.append('secret', env.HCAPTCHA_SECRET);
|
||||
requestBody.append('remoteip', event.getClientAddress());
|
||||
|
||||
const testResultRequest = await fetch('https://api.hcaptcha.com/siteverify', {
|
||||
method: 'POST',
|
||||
body: requestBody
|
||||
});
|
||||
|
||||
const testResult = await testResultRequest.json();
|
||||
if (!testResult.success) {
|
||||
return fail(400, {
|
||||
username,
|
||||
displayName,
|
||||
email,
|
||||
errors: ['invalidCaptcha'],
|
||||
fields: ['hcaptcha']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!(await Users.checkRegistration(username, email))) {
|
||||
return fail(400, {
|
||||
username,
|
||||
|
@ -13,6 +13,7 @@
|
||||
import { enhance } from '$app/forms';
|
||||
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
||||
import ButtonRow from '$lib/components/container/ButtonRow.svelte';
|
||||
import HCaptcha from '$lib/components/form/HCaptcha.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
export let form: ActionData;
|
||||
@ -51,8 +52,10 @@
|
||||
<ColumnView>
|
||||
{#if !data.enabled}
|
||||
<Alert type="error">{$t('account.register.disabled')}</Alert>
|
||||
<div><a href="/login">{$t('account.login.title')}</a></div>
|
||||
{:else if form?.success}
|
||||
<Alert type="success">{$t(`account.register.${form.success}`)}</Alert>
|
||||
<div><a href="/login">{$t('account.login.title')}</a></div>
|
||||
{:else}
|
||||
<form action="" method="POST" use:enhance={enhanceFn}>
|
||||
<FormWrapper>
|
||||
@ -121,6 +124,12 @@
|
||||
</FormControl>
|
||||
</FormSection>
|
||||
|
||||
{#if env.PUBLIC_HCAPTCHA_KEY}
|
||||
<FormControl>
|
||||
<HCaptcha />
|
||||
</FormControl>
|
||||
{/if}
|
||||
|
||||
<ButtonRow>
|
||||
<Button type="submit" variant="primary" disabled={submitted}
|
||||
>{$t('account.register.submit')}</Button
|
||||
|
Loading…
Reference in New Issue
Block a user