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;
|
user?: UserSession;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type HCaptcha = {
|
||||||
|
render(id: string, config: { sitekey: string; size: string; theme: string }): string;
|
||||||
|
};
|
||||||
|
|
||||||
// See https://kit.svelte.dev/docs/types#app
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
declare global {
|
declare global {
|
||||||
@ -14,6 +18,10 @@ declare global {
|
|||||||
? { [P in keyof O]: O[P] }
|
? { [P in keyof O]: O[P] }
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
hcaptcha: HCaptcha | null;
|
||||||
|
}
|
||||||
|
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
|
|
||||||
|
@ -6,15 +6,11 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.aside-wrapper {
|
.aside-wrapper {
|
||||||
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aside-wrapper,
|
|
||||||
.aside-inner {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aside-inner {
|
.aside-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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>
|
@ -1,93 +1,94 @@
|
|||||||
{
|
{
|
||||||
"title": "Manage your account",
|
"title": "Manage your account",
|
||||||
"altTitle": "Account management",
|
"altTitle": "Account management",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"usernameHint": "Only the English alphabet, numbers and _-. are allowed.",
|
"usernameHint": "Only the English alphabet, numbers and _-. are allowed.",
|
||||||
"displayName": "Display Name",
|
"displayName": "Display Name",
|
||||||
"displayNameHint": "Must be between 3 and 32 characters.",
|
"displayNameHint": "Must be between 3 and 32 characters.",
|
||||||
"changeEmail": "Change email address",
|
"changeEmail": "Change email address",
|
||||||
"currentEmail": "Current email address",
|
"currentEmail": "Current email address",
|
||||||
"email": "Email address",
|
"email": "Email address",
|
||||||
"newEmail": "New email address",
|
"newEmail": "New email address",
|
||||||
"emailHint": "Hint",
|
"emailHint": "Hint",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"repeatPassword": "Repeat password",
|
"repeatPassword": "Repeat password",
|
||||||
"changePassword": "Change password",
|
"changePassword": "Change password",
|
||||||
"currentPassword": "Current password",
|
"currentPassword": "Current password",
|
||||||
"newPassword": "New password",
|
"newPassword": "New password",
|
||||||
"repeatNewPassword": "Repeat new password",
|
"repeatNewPassword": "Repeat new password",
|
||||||
"forgotPassword": "Forgot your password?",
|
"forgotPassword": "Forgot your password?",
|
||||||
"resetPassword": "Reset your password",
|
"resetPassword": "Reset your password",
|
||||||
"setNewPassword": "Set a new password",
|
"setNewPassword": "Set a new password",
|
||||||
"passwordHint": "At least 8 characters, a capital letter and a number required.",
|
"passwordHint": "At least 8 characters, a capital letter and a number required.",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"changeSuccess": "Account settings changed successfully!",
|
"changeSuccess": "Account settings changed successfully!",
|
||||||
"activateSuccess": "Your account has been activated successfully! You may now log in.",
|
"activateSuccess": "Your account has been activated successfully! You may now log in.",
|
||||||
"passwordSetSuccess": "Your new password has been set successfully! You may now log in.",
|
"passwordSetSuccess": "Your new password has been set successfully! You may now log in.",
|
||||||
"passwordResetSucces": "If there is an account with that email address, we have sent a password reset email to it.",
|
"passwordResetSucces": "If there is an account with that email address, we have sent a password reset email to it.",
|
||||||
"logout": "Log out",
|
"logout": "Log out",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"avatar": {
|
"avatar": {
|
||||||
"title": "Profile avatar",
|
"title": "Profile avatar",
|
||||||
"change": "Change avatar",
|
"change": "Change avatar",
|
||||||
"remove": "Remove avatar",
|
"remove": "Remove avatar",
|
||||||
"done": "Done",
|
"done": "Done",
|
||||||
"restart": "Restart",
|
"restart": "Restart",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
"uploadLabel": "Upload image file",
|
"uploadLabel": "Upload image file",
|
||||||
"hint": "Allowed image formats: .png, .jpg, .jpeg. Image must be less than 10 MB in size."
|
"hint": "Allowed image formats: .png, .jpg, .jpeg. Image must be less than 10 MB in size."
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Log in",
|
"title": "Log in",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"submit": "Log in",
|
"submit": "Log in",
|
||||||
"otp": "Two-factor authentication is enabled",
|
"otp": "Two-factor authentication is enabled",
|
||||||
"otpCode": "Enter the code displayed on the authenticator app"
|
"otpCode": "Enter the code displayed on the authenticator app"
|
||||||
},
|
},
|
||||||
"register": {
|
"register": {
|
||||||
"title": "Create a new account",
|
"title": "Create a new account",
|
||||||
"disabled": "Registrations are currently disabled by the administrator! Sorry.",
|
"disabled": "Registrations are currently disabled by the administrator! Sorry.",
|
||||||
"userCreated": "User account has been created successfully! You may now log in.",
|
"userCreated": "User account has been created successfully! You may now log in.",
|
||||||
"emailSent": "User account has been created successfully, and an activation email has been sent to your email address.",
|
"emailSent": "User account has been created successfully, and an activation email has been sent to your email address.",
|
||||||
"submit": "Create account"
|
"submit": "Create account"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"invalidLogin": "Invalid email or password!",
|
"invalidLogin": "Invalid email or password!",
|
||||||
"invalidRequest": "Invalid request! Please try again.",
|
"invalidRequest": "Invalid request! Please try again.",
|
||||||
"emailRequired": "Email address is required.",
|
"emailRequired": "Email address is required.",
|
||||||
"invalidUsername": "The username entered is invalid or not allowed",
|
"invalidUsername": "The username entered is invalid or not allowed",
|
||||||
"invalidEmail": "The email address is invalid.",
|
"invalidEmail": "The email address is invalid.",
|
||||||
"passwordRequired": "The password is required.",
|
"passwordRequired": "The password is required.",
|
||||||
"passwordMismatch": "The passwords do not match!",
|
"passwordMismatch": "The passwords do not match!",
|
||||||
"passwordSame": "Your new password cannot be your current password.",
|
"passwordSame": "Your new password cannot be your current password.",
|
||||||
"invalidPassword": "The provided password is invalid.",
|
"invalidPassword": "The provided password is invalid.",
|
||||||
"invalidDisplayName": "The provided display name is invalid.",
|
"invalidDisplayName": "The provided display name is invalid.",
|
||||||
"otpFailed": "The code you entered was invalid. Please note that you will be given a new QR code for subsequent retries.",
|
"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",
|
"required": "Please fill in the required fields",
|
||||||
"existingRegistration": "The username or email address is already in use.",
|
"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",
|
"otp": {
|
||||||
"enabled": "Two-factor authentication is enabled.",
|
"title": "Two-factor authentication",
|
||||||
"disabled": "Your account does not have two-factor authentication enabled.",
|
"enabled": "Two-factor authentication is enabled.",
|
||||||
"unavailable": "Two-factor authentication is not set up.",
|
"disabled": "Your account does not have two-factor authentication enabled.",
|
||||||
"activated": "Two-factor authentication has been activated successfully!",
|
"unavailable": "Two-factor authentication is not set up.",
|
||||||
"deactivated": "Two-factor authentication has been deactivated successfully.",
|
"activated": "Two-factor authentication has been activated successfully!",
|
||||||
"scan": "Scan this QR code with the authenticator app of your choice",
|
"deactivated": "Two-factor authentication has been deactivated successfully.",
|
||||||
"code": "Enter the code displayed on the authenticator app",
|
"scan": "Scan this QR code with the authenticator app of your choice",
|
||||||
"return": "Return to account management",
|
"code": "Enter the code displayed on the authenticator app",
|
||||||
"retry": "Try again",
|
"return": "Return to account management",
|
||||||
"activate": "Set up two factor authentication",
|
"retry": "Try again",
|
||||||
"deactivate": "Deactivate"
|
"activate": "Set up two factor authentication",
|
||||||
},
|
"deactivate": "Deactivate"
|
||||||
"authorizations": {
|
},
|
||||||
"title": "Authorized applications",
|
"authorizations": {
|
||||||
"description": "These applications are authorized automatically when requested, provided you have already consented to the information they require. You should revoke any applications you do not recognize.",
|
"title": "Authorized applications",
|
||||||
"warning": "By revoking the authorization, you may be prompted to authorize the application again in the future. This does NOT ensure that your information is deleted by any third-party applications in question. Please contact each application's owner individually if you wish to remove your information from their servers.",
|
"description": "These applications are authorized automatically when requested, provided you have already consented to the information they require. You should revoke any applications you do not recognize.",
|
||||||
"website": "Visit website",
|
"warning": "By revoking the authorization, you may be prompted to authorize the application again in the future. This does NOT ensure that your information is deleted by any third-party applications in question. Please contact each application's owner individually if you wish to remove your information from their servers.",
|
||||||
"revoke": "Revoke",
|
"website": "Visit website",
|
||||||
"none": "You currently do not have any authorized applications."
|
"revoke": "Revoke",
|
||||||
}
|
"none": "You currently do not have any authorized applications."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,27 @@
|
|||||||
{
|
{
|
||||||
"description": "{{siteName}} is a Single-Sign-On service used by other applications.",
|
"description": "{{siteName}} is a Single-Sign-On service used by other applications.",
|
||||||
"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.",
|
"metaDescription": "{{siteName}} - Single-Sign-On service",
|
||||||
"submit": "Submit",
|
"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.",
|
||||||
"cancel": "Cancel",
|
"submit": "Submit",
|
||||||
"manage": "Manage",
|
"cancel": "Cancel",
|
||||||
"back": "Go back",
|
"manage": "Manage",
|
||||||
"home": "Home page",
|
"back": "Go back",
|
||||||
"required": "Required fields",
|
"home": "Home page",
|
||||||
"page": "Page",
|
"required": "Required fields",
|
||||||
"previous": "Previous",
|
"page": "Page",
|
||||||
"next": "Next",
|
"previous": "Previous",
|
||||||
"bool": {
|
"next": "Next",
|
||||||
"true": "Yes",
|
"bool": {
|
||||||
"false": "No"
|
"true": "Yes",
|
||||||
},
|
"false": "No"
|
||||||
"available": "Available",
|
},
|
||||||
"current": "Current",
|
"available": "Available",
|
||||||
"remove": "Remove",
|
"current": "Current",
|
||||||
"filter": "Filter",
|
"remove": "Remove",
|
||||||
"clear": "Clear",
|
"filter": "Filter",
|
||||||
"theme": {
|
"clear": "Clear",
|
||||||
"light": "Light mode",
|
"theme": {
|
||||||
"dark": "Dark mode"
|
"light": "Light mode",
|
||||||
}
|
"dark": "Dark mode"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,3 +13,22 @@ export const hasPrivileges = (list: string[], privileges: RequiredPrivileges) =>
|
|||||||
}
|
}
|
||||||
return list.includes(item);
|
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">
|
<script lang="ts">
|
||||||
|
import { env } from '$env/dynamic/public';
|
||||||
|
import { t } from '$lib/i18n';
|
||||||
import { forwardInitialTheme } from '$lib/theme-mode';
|
import { forwardInitialTheme } from '$lib/theme-mode';
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
|
|
||||||
forwardInitialTheme();
|
forwardInitialTheme();
|
||||||
</script>
|
</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>
|
<slot></slot>
|
||||||
|
@ -12,9 +12,16 @@ interface RegisterData {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: 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({
|
const limiter = new RateLimiter({
|
||||||
IP: [6, 'm']
|
IP: [6, 'm']
|
||||||
@ -32,7 +39,14 @@ export const actions = {
|
|||||||
|
|
||||||
const body = await request.formData();
|
const body = await request.formData();
|
||||||
const changes = Changesets.take<RegisterData>(fields, body);
|
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
|
// Each field must be present
|
||||||
if (!username || !displayName || !email || !password) {
|
if (!username || !displayName || !email || !password) {
|
||||||
return fail(400, {
|
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))) {
|
if (!(await Users.checkRegistration(username, email))) {
|
||||||
return fail(400, {
|
return fail(400, {
|
||||||
username,
|
username,
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
||||||
import ButtonRow from '$lib/components/container/ButtonRow.svelte';
|
import ButtonRow from '$lib/components/container/ButtonRow.svelte';
|
||||||
|
import HCaptcha from '$lib/components/form/HCaptcha.svelte';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
export let form: ActionData;
|
export let form: ActionData;
|
||||||
@ -51,8 +52,10 @@
|
|||||||
<ColumnView>
|
<ColumnView>
|
||||||
{#if !data.enabled}
|
{#if !data.enabled}
|
||||||
<Alert type="error">{$t('account.register.disabled')}</Alert>
|
<Alert type="error">{$t('account.register.disabled')}</Alert>
|
||||||
|
<div><a href="/login">{$t('account.login.title')}</a></div>
|
||||||
{:else if form?.success}
|
{:else if form?.success}
|
||||||
<Alert type="success">{$t(`account.register.${form.success}`)}</Alert>
|
<Alert type="success">{$t(`account.register.${form.success}`)}</Alert>
|
||||||
|
<div><a href="/login">{$t('account.login.title')}</a></div>
|
||||||
{:else}
|
{:else}
|
||||||
<form action="" method="POST" use:enhance={enhanceFn}>
|
<form action="" method="POST" use:enhance={enhanceFn}>
|
||||||
<FormWrapper>
|
<FormWrapper>
|
||||||
@ -121,6 +124,12 @@
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
|
{#if env.PUBLIC_HCAPTCHA_KEY}
|
||||||
|
<FormControl>
|
||||||
|
<HCaptcha />
|
||||||
|
</FormControl>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button type="submit" variant="primary" disabled={submitted}
|
<Button type="submit" variant="primary" disabled={submitted}
|
||||||
>{$t('account.register.submit')}</Button
|
>{$t('account.register.submit')}</Button
|
||||||
|
Loading…
Reference in New Issue
Block a user