mirror of
https://github.com/PretendoNetwork/website.git
synced 2026-08-28 03:25:42 -05:00
feat: implement forgot password + improve captchas
This commit is contained in:
@@ -118,6 +118,12 @@ form.account.register {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
form.account.register .h-captcha {
|
||||
grid-column: 1 / span 2;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
form.account.register p,
|
||||
form.account.register div.email,
|
||||
form.account.register div.buttons {
|
||||
|
||||
131
src/assets/css/forgot-password.css
Normal file
131
src/assets/css/forgot-password.css
Normal file
@@ -0,0 +1,131 @@
|
||||
.account-form-wrapper {
|
||||
height: 80vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
flex-direction: column;
|
||||
margin: 15vh auto;
|
||||
width: fit-content;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
form.account {
|
||||
display: block;
|
||||
padding: 40px 48px;
|
||||
background-color: var(--bg-shade-2);
|
||||
color: var(--text-shade-1);
|
||||
border-radius: 12px;
|
||||
width: min(480px, 90vw);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
form.account h2 {
|
||||
margin: 0;
|
||||
color: var(--text-shade-3);
|
||||
}
|
||||
|
||||
form.account p {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
form.account div {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
form.account label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
form.account button {
|
||||
width: 100%;
|
||||
background: var(--accent-shade-0);
|
||||
}
|
||||
|
||||
form.account a {
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
color: var(--text-shade-1);
|
||||
text-align: right;
|
||||
margin: 6px 0;
|
||||
width: fit-content;
|
||||
}
|
||||
form.account a:hover {
|
||||
color: var(--text-shade-3);
|
||||
}
|
||||
|
||||
form.account a.pwdreset {
|
||||
margin-left: auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
form.account a.register {
|
||||
margin: auto;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
@keyframes banner-notice {
|
||||
0% {
|
||||
top: -150px;
|
||||
}
|
||||
20% {
|
||||
top: 35px;
|
||||
}
|
||||
80% {
|
||||
top: 35px;
|
||||
}
|
||||
100% {
|
||||
top: -150px;
|
||||
}
|
||||
}
|
||||
.banner-notice {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
top: -150px;
|
||||
width: 100%;
|
||||
animation: banner-notice 5s;
|
||||
}
|
||||
.banner-notice div {
|
||||
padding: 4px 36px;
|
||||
border-radius: 5px;
|
||||
z-index: 3;
|
||||
}
|
||||
.banner-notice.success div {
|
||||
background: var(--green-shade-0);
|
||||
}
|
||||
.banner-notice.error div {
|
||||
background: var(--red-shade-1);
|
||||
}
|
||||
|
||||
form.account.register {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
width: min(780px, 90vw);
|
||||
column-gap: 24px;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
form.account.forgot-password div.h-captcha {
|
||||
grid-column: 1 / span 2;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
form.account.register p,
|
||||
form.account.register div.email,
|
||||
form.account.register div.buttons {
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 720px) {
|
||||
form.account.register {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
form.account.register div.h-captcha,
|
||||
form.account.register p,
|
||||
form.account.register div.email,
|
||||
form.account.register div.buttons {
|
||||
grid-column: unset;
|
||||
}
|
||||
}
|
||||
46
src/components/Captcha.vue
Normal file
46
src/components/Captcha.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import VueHcaptcha from '@hcaptcha/vue3-hcaptcha';
|
||||
|
||||
const props = defineProps<{
|
||||
siteKey: string;
|
||||
theme?: string;
|
||||
}>();
|
||||
|
||||
const captchaEl = useTemplateRef('hcaptcha');
|
||||
const hcaptchaToken = ref<string | null>(null);
|
||||
|
||||
async function getToken(): Promise<string | null> {
|
||||
// Already has a non-expired token
|
||||
if (hcaptchaToken.value) {
|
||||
return hcaptchaToken.value;
|
||||
}
|
||||
|
||||
if (!captchaEl.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await captchaEl.value.executeAsync();
|
||||
return response.response;
|
||||
} catch (err) {
|
||||
console.error('Failed to execute captcha', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getToken
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VueHcaptcha
|
||||
ref="hcaptcha"
|
||||
:sitekey="props.siteKey"
|
||||
:theme="props.theme"
|
||||
@verify="(token: string) => (hcaptchaToken = token)"
|
||||
@expire="hcaptchaToken = null"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,36 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import VueHcaptcha from '@hcaptcha/vue3-hcaptcha';
|
||||
import type { ApiAuthForgotPasswordRequest } from '~~/shared/api-types';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const form = reactive({ emailOrUsername: '' });
|
||||
|
||||
const errorMessage = ref<string | null>();
|
||||
const hcaptcha = ref<VueHcaptcha | null>(null);
|
||||
const successMessage = ref<string | null>();
|
||||
const captchaRef = useTemplateRef('captcha');
|
||||
|
||||
// TODO style this entire page
|
||||
async function submit() {
|
||||
errorMessage.value = null;
|
||||
successMessage.value = null;
|
||||
try {
|
||||
const hCaptchaResponse = hcaptcha.value ? (await hcaptcha.value.executeAsync()).response : null;
|
||||
let captchaResponse: string | null = null;
|
||||
if (captchaRef.value) {
|
||||
captchaResponse = await captchaRef.value.getToken();
|
||||
if (!captchaResponse) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await $fetch('/api/auth/forgot-password', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
emailOrPassword: form.emailOrUsername,
|
||||
captchaResponse: hCaptchaResponse ?? undefined
|
||||
captchaResponse: captchaResponse ?? undefined
|
||||
} satisfies ApiAuthForgotPasswordRequest
|
||||
});
|
||||
|
||||
alert('Success - check your inbox');
|
||||
await navigateTo('/');
|
||||
// Success
|
||||
form.emailOrUsername = '';
|
||||
successMessage.value = 'An email has been sent.';
|
||||
setTimeout(() => {
|
||||
successMessage.value = null;
|
||||
}, 5000);
|
||||
} catch (error: unknown) {
|
||||
if (error === 'challenge-closed') {
|
||||
// Thrown if the captcha is closed, can be safely ignored
|
||||
return;
|
||||
}
|
||||
|
||||
const err = getApiError(error);
|
||||
errorMessage.value = err.code;
|
||||
setTimeout(() => { // TODO: replace this toast
|
||||
setTimeout(() => {
|
||||
errorMessage.value = null;
|
||||
}, 5000);
|
||||
}
|
||||
@@ -41,31 +49,42 @@ async function submit() {
|
||||
<div>
|
||||
<div class="account-form-wrapper">
|
||||
<form
|
||||
class="account register"
|
||||
class="account forgot-password"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<h2>Forgot password</h2>
|
||||
<h2>{{ t('account.forgotPassword.header') }}</h2>
|
||||
<p>{{ t('account.forgotPassword.sub') }}</p>
|
||||
<div>
|
||||
<label for="input">{{ t('account.forgotPassword.input') }}</label>
|
||||
<input
|
||||
id="input"
|
||||
v-model="form.emailOrUsername"
|
||||
type="text"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<vue-hcaptcha
|
||||
<Captcha
|
||||
v-if="$config.public.hcaptchaSiteKey"
|
||||
ref="hcaptcha"
|
||||
:sitekey="$config.public.hcaptchaSiteKey"
|
||||
ref="captcha"
|
||||
class="h-captcha"
|
||||
:site-key="$config.public.hcaptchaSiteKey"
|
||||
theme="dark"
|
||||
/>
|
||||
<div class="buttons">
|
||||
<button type="submit">
|
||||
Send
|
||||
{{ t('account.forgotPassword.submit') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
v-if="successMessage"
|
||||
class="banner-notice success"
|
||||
>
|
||||
<div>
|
||||
<p>{{ successMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="errorMessage"
|
||||
class="banner-notice error"
|
||||
@@ -78,5 +97,5 @@ async function submit() {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import "/assets/css/auth.css";
|
||||
@import "~/assets/css/forgot-password.css";
|
||||
</style>
|
||||
|
||||
@@ -97,5 +97,5 @@ async function loginSubmission() {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import "/assets/css/auth.css";
|
||||
@import "~/assets/css/auth.css";
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import VueHcaptcha from '@hcaptcha/vue3-hcaptcha';
|
||||
import type { ApiAuthRegisterRequest } from '~~/shared/api-types';
|
||||
|
||||
const route = useRoute();
|
||||
@@ -19,11 +18,17 @@ const loginURI = computed(() => {
|
||||
const registerForm = reactive({ email: '', username: '', mii_name: '', password: '', password_confirm: '' });
|
||||
|
||||
const errorMessage = ref<string | null>();
|
||||
const invisibleHcaptcha = ref<VueHcaptcha | null>(null);
|
||||
const captchaRef = useTemplateRef('captcha');
|
||||
|
||||
async function registerSubmission() {
|
||||
try {
|
||||
const hCaptchaResponse = invisibleHcaptcha.value ? (await invisibleHcaptcha.value.executeAsync()).response : null;
|
||||
let captchaResponse: string | null = null;
|
||||
if (captchaRef.value) {
|
||||
captchaResponse = await captchaRef.value.getToken();
|
||||
if (!captchaResponse) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const res = await $fetch('/api/auth/register', {
|
||||
method: 'POST',
|
||||
@@ -32,7 +37,7 @@ async function registerSubmission() {
|
||||
miiName: registerForm.mii_name,
|
||||
password: registerForm.password,
|
||||
username: registerForm.username,
|
||||
captchaResponse: hCaptchaResponse ?? undefined
|
||||
captchaResponse: captchaResponse ?? undefined
|
||||
} satisfies ApiAuthRegisterRequest
|
||||
});
|
||||
auth.set({
|
||||
@@ -41,11 +46,6 @@ async function registerSubmission() {
|
||||
});
|
||||
await authUtils.safelyRedirectAfterLogin(redirect.value);
|
||||
} catch (error: unknown) {
|
||||
if (error === 'challenge-closed') {
|
||||
// Thrown if the captcha is closed, can be safely ignored
|
||||
return;
|
||||
}
|
||||
|
||||
const err = getApiError(error);
|
||||
errorMessage.value = err.code;
|
||||
setTimeout(() => { // TODO: replace this toast
|
||||
@@ -119,11 +119,11 @@ async function registerSubmission() {
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<vue-hcaptcha
|
||||
<Captcha
|
||||
v-if="$config.public.hcaptchaSiteKey"
|
||||
ref="hcaptcha"
|
||||
:sitekey="$config.public.hcaptchaSiteKey"
|
||||
ref="captcha"
|
||||
class="h-captcha"
|
||||
:site-key="$config.public.hcaptchaSiteKey"
|
||||
theme="dark"
|
||||
/>
|
||||
<div class="buttons">
|
||||
|
||||
Reference in New Issue
Block a user