add password reset + password change

This commit is contained in:
Emily
2025-01-13 17:01:34 +01:00
parent ab95772dd4
commit 88ebfc188c
9 changed files with 416 additions and 5 deletions

View File

@@ -2,12 +2,18 @@
import type { SettingsTemplateEntry } from './Template.vue'; import type { SettingsTemplateEntry } from './Template.vue';
const entries: SettingsTemplateEntry[] = [ const entries: SettingsTemplateEntry[] = [
{ id: 'change_pass', title: 'Change password', text: 'Change your password' },
{ id: 'delete', title: 'Delete account', text: 'Delete your account' }, { id: 'delete', title: 'Delete account', text: 'Delete your account' },
] ]
const { user } = useLoggedUser();
const { setToken } = useAccessToken(); const { setToken } = useAccessToken();
const canChangePassword = useFetch('/api/user/password/can_change', {
headers: useComputedHeaders({ useSnapshotDates: false })
});
async function deleteAccount() { async function deleteAccount() {
const sure = confirm("Are you sure you want to delete this account ?"); const sure = confirm("Are you sure you want to delete this account ?");
if (!sure) return; if (!sure) return;
@@ -20,11 +26,57 @@ async function deleteAccount() {
location.href = "/login" location.href = "/login"
} }
const old_password = ref<string>("");
const new_password = ref<string>("");
const { createAlert } = useAlert()
async function changePassword() {
try {
const res = await $fetch("/api/user/password/change", {
...signHeaders({ 'Content-Type': 'application/json' }),
method: "POST",
body: JSON.stringify({ old_password: old_password.value, new_password: new_password.value })
})
if (!res) throw Error('No response');
if (res.error) return createAlert('Error', res.message, 'far fa-triangle-exclamation', 5000);
old_password.value = '';
new_password.value = '';
return createAlert('Success', 'Password changed successfully', 'far fa-circle-check', 5000);
} catch (ex) {
console.error(ex);
createAlert('Error', 'Internal error', 'far fa-triangle-exclamation', 5000);
}
}
</script> </script>
<template> <template>
<SettingsTemplate :entries="entries"> <SettingsTemplate :entries="entries">
<template #change_pass>
<div v-if="canChangePassword.data.value?.can_change">
<div class="flex flex-col gap-4">
<LyxUiInput type="password" class="py-1 px-2" v-model="old_password" placeholder="Current password"></LyxUiInput>
<LyxUiInput type="password" class="py-1 px-2" v-model="new_password" placeholder="New password"></LyxUiInput>
<LyxUiButton type="primary" @click="changePassword()"> Change password </LyxUiButton>
</div>
</div>
<div v-if="!canChangePassword.data.value?.can_change">
You cannot change the password for accounts created using social login options.
</div>
</template>
<template #delete> <template #delete>
<div <div
class="outline rounded-lg w-full px-8 py-4 flex flex-col gap-4 outline-[1px] outline-[#541c15] bg-[#1e1412]"> class="outline rounded-lg w-full px-8 py-4 flex flex-col gap-4 outline-[1px] outline-[#541c15] bg-[#1e1412]">

View File

@@ -45,7 +45,8 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
to.path != '/login' && to.path != '/login' &&
to.path != '/register' && to.path != '/register' &&
to.path != '/live_demo' && to.path != '/live_demo' &&
to.path != '/jwt_login' to.path != '/jwt_login' &&
to.path != '/forgot_password'
) { ) {
console.log('REDIRECT TO LOGIN', to.path); console.log('REDIRECT TO LOGIN', to.path);
return '/login'; return '/login';

View File

@@ -0,0 +1,150 @@
<script setup lang="ts">
definePageMeta({ layout: 'none' });
const email = ref<string>("");
const emailSended = ref<boolean>(false);
const { createAlert } = useAlert();
async function resetPassword() {
try {
const res = await $fetch('/api/user/password/reset', {
headers: { 'Content-Type': 'application/json' },
method: 'POST',
body: JSON.stringify({ email: email.value })
})
if (!res) throw Error('No response');
if (res.error) return createAlert('Error', res.message, 'far fa-triangle-exclamation', 5000);
emailSended.value = true;
return createAlert('Success', 'Email sent', 'far fa-circle-check', 5000);
} catch (ex) {
console.error(ex);
createAlert('Error', 'Internal error', 'far fa-triangle-exclamation', 5000);
}
}
</script>
<template>
<div class="home w-full h-full">
<div class="flex h-full">
<div class="flex-1 flex flex-col items-center pt-20 xl:pt-[22vh]">
<div class="rotating-thing absolute top-0"></div>
<div class="mb-8 bg-black rounded-xl">
<img class="w-[5rem]" :src="'logo.png'">
</div>
<div class="text-text text-[2.2rem] font-bold poppins">
Reset password
</div>
<div class="text-text/80 text-[1.2rem] font-light text-center w-[70%] poppins mt-2">
Enter your user account's verified email address and we will send you a temporary password.
</div>
<div class="mt-12">
<div v-if="!emailSended" class="flex flex-col gap-2 z-[110]">
<div class="flex flex-col gap-4 z-[100] w-[20vw] min-w-[20rem]">
<LyxUiInput class="px-3 py-2" placeholder="Email" v-model="email"></LyxUiInput>
</div>
<div class="flex justify-center mt-4">
<LyxUiButton @click="resetPassword()" class="text-center z-[110]" type="primary">
Reset password
</LyxUiButton>
</div>
<NuxtLink to="/login"
class="mt-4 text-center text-lyx-text-dark underline cursor-pointer z-[110]">
Go back
</NuxtLink>
</div>
<div v-if="emailSended" class="mt-12 flex flex-col text-center text-[1.1rem] z-[100]">
<div>
Check your email inbox.
</div>
<RouterLink tag="div" to="/login"
class="mt-6 text-center text-lyx-text-dark underline cursor-pointer z-[110]">
Go back
</RouterLink>
</div>
</div>
</div>
<div class="grow flex-1 items-center justify-center hidden lg:flex">
<!-- <GlobeSvg></GlobeSvg> -->
<img :src="'image-bg.png'" class="h-full py-6">
</div>
</div>
<!-- <div class="flex flex-col items-center justify-center mt-40 gap-20">
<div class="google-login text-gray-700" :class="{ disabled: !isReady }" @click="login">
<div class="icon">
<i class="fab fa-google"></i>
</div>
<div> Continua con Google </div>
</div>
</div> -->
</div>
</template>
<style scoped lang="scss">
.rotating-thing {
height: 100%;
aspect-ratio: 1 / 1;
opacity: 0.15;
background: radial-gradient(51.24% 31.29% at 50% 50%, rgb(51, 58, 232) 0%, rgba(51, 58, 232, 0) 100%);
animation: 12s linear 0s infinite normal none running spin;
}
.google-login {
cursor: pointer;
font-weight: bold;
background-color: #fcefed;
padding: 1rem 2rem;
border-radius: 1rem;
display: flex;
gap: 1rem;
align-items: center;
&.disabled {
filter: brightness(50%);
}
i {
font-size: 1.5rem;
}
}
</style>

View File

@@ -195,6 +195,13 @@ async function signInWithCredentials() {
</LyxUiInput> </LyxUiInput>
</div> </div>
<div class="flex justify-end">
<RouterLink tag="div" to="/forgot_password"
class="text-center text-lyx-text-dark underline cursor-pointer z-[110]">
Forgot password?
</RouterLink>
</div>
<div class="flex justify-center mt-4 z-[100]"> <div class="flex justify-center mt-4 z-[100]">
<LyxUiButton @click="signInWithCredentials()" class="text-center" type="primary"> <LyxUiButton @click="signInWithCredentials()" class="text-center" type="primary">
Sign in Sign in
@@ -228,10 +235,14 @@ async function signInWithCredentials() {
</div> </div>
<RouterLink tag="div" to="/register" <div class="flex flex-col gap-2 mt-4">
class="mt-4 text-center text-lyx-text-dark underline cursor-pointer z-[100]">
You don't have an account ? Sign up <RouterLink tag="div" to="/register"
</RouterLink> class="text-center text-lyx-text-dark underline cursor-pointer z-[100]">
You don't have an account ? Sign up
</RouterLink>
</div>
</div> </div>

View File

@@ -0,0 +1,10 @@
import { PasswordModel } from "@schema/PasswordSchema";
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return;
const hasPassword = await PasswordModel.exists({ email: userData.user.email });
if (hasPassword) return { can_change: true };
return { can_change: false }
});

View File

@@ -0,0 +1,33 @@
import crypto from 'crypto';
import { PasswordModel } from '@schema/PasswordSchema';
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return;
const { old_password, new_password } = await readBody(event);
if (new_password.length < 6) return { error: true, message: 'Password too short' }
const target = await PasswordModel.findOne({ email: userData.user.email });
if (!target) return { error: true, message: 'Internal error. User not found.' }
const hashOld = crypto.createHash('sha256');
const hashedPasswordOld = hashOld.update(old_password + '_litlyx').digest('hex');
if (target.password !== hashedPasswordOld) {
return { error: true, message: 'Old password not correct' }
}
const hashNew = crypto.createHash('sha256');
const hashedPasswordNew = hashNew.update(new_password + '_litlyx').digest('hex');
target.password = hashedPasswordNew;
await target.save();
return { error: false, message: 'Success' }
});

View File

@@ -0,0 +1,26 @@
import crypto from 'crypto';
import { PasswordModel } from '@schema/PasswordSchema';
import EmailService from '@services/EmailService'
export default defineEventHandler(async event => {
const { email } = await readBody(event);
const target = await PasswordModel.findOne({ email });
if (!target) return { error: true, message: 'Internal error. User not found.' }
const newPass = crypto.randomBytes(5).toString('hex');
const hash = crypto.createHash('sha256');
const hashedPassword = hash.update(newPass + '_litlyx').digest('hex');
target.password = hashedPassword;
await target.save();
await EmailService.sendResetPasswordEmail(email, newPass);
return { error: false, message: 'Password changed' }
});

View File

@@ -7,6 +7,7 @@ import { PURCHASE_EMAIL } from './email_templates/PurchaseEmail';
import { ANOMALY_VISITS_EVENTS_EMAIL } from './email_templates/AnomalyUsageEmail'; import { ANOMALY_VISITS_EVENTS_EMAIL } from './email_templates/AnomalyUsageEmail';
import { ANOMALY_DOMAIN_EMAIL } from './email_templates/AnomalyDomainEmail'; import { ANOMALY_DOMAIN_EMAIL } from './email_templates/AnomalyDomainEmail';
import { CONFIRM_EMAIL } from './email_templates/ConfirmEmail'; import { CONFIRM_EMAIL } from './email_templates/ConfirmEmail';
import { RESET_PASSWORD_EMAIL } from './email_templates/ResetPasswordEmail';
class EmailService { class EmailService {
@@ -168,6 +169,24 @@ class EmailService {
} }
} }
async sendResetPasswordEmail(target: string, newPassword: string) {
try {
const sendSmtpEmail = new SendSmtpEmail();
sendSmtpEmail.subject = "Password reset";
sendSmtpEmail.sender = { "name": "Litlyx", "email": "no-reply@litlyx.com" };
sendSmtpEmail.to = [{ "email": target }];
sendSmtpEmail.htmlContent = RESET_PASSWORD_EMAIL
.replace(/\[NEW_PASSWORD\]/, newPassword)
.toString();
await this.apiInstance.sendTransacEmail(sendSmtpEmail);
return true;
} catch (ex) {
console.error('ERROR SENDING EMAIL', ex);
return false;
}
}
} }
const instance = new EmailService(); const instance = new EmailService();

View File

@@ -0,0 +1,109 @@
export const RESET_PASSWORD_EMAIL = `<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" lang="en">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="x-apple-disable-message-reformatting" />
<!--$-->
</head>
<body
style='background-color:#fff;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif'>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="max-width:37.5em">
<tbody>
<tr style="width:100%">
<td>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:1px solid rgb(0,0,0, 0.1);border-radius:3px;overflow:hidden">
<tbody>
<tr>
<td>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0"
role="presentation">
<tbody style="width:100%">
<tr style="width:100%">
<img src="https://litlyx.com/images/locker2.png"
style="display:block;outline:none;border:none;text-decoration:none;max-width:100%"
width="620" />
</tr>
</tbody>
</table>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0"
role="presentation" style="padding:20px;padding-bottom:0">
<tbody style="width:100%">
<tr style="width:100%">
<td data-id="__react-email-column">
<h1 style="font-size:32px;font-weight:bold;text-align:center">
Dear user
</h1>
<h2 style="font-size:26px;font-weight:bold;text-align:center">
Below is the temporary password for logging in again to the
Litlyx dashboard.
</h2>
<p style="font-size:16px;line-height:24px;margin:16px 0">
<b>Temporary Password: </b> [NEW_PASSWORD]
<!-- September 7, 2022 at 10:58 AM -->
</p>
<p style="font-size:16px;line-height:24px;margin:16px 0">
Please ensure that you change your password as soon as possible.
To do so, go to <b>Settings > Account</b> in the dashboard. <br>
If you need further assistance, feel free to contact us at
<a href="mailto:help@litlyx.com">help@litlyx.com</a>.
</p>
</td>
</tr>
</tbody>
</table>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0"
role="presentation" style="padding:20px;padding-top:0">
<tbody style="width:100%">
<tr style="width:100%">
<td colspan="2" data-id="__react-email-column"
style="display:flex;justify-content:center;width:100%">
<a style="line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;background-color:#5680f8;border-radius:3px;color:#FFF;font-weight:bold;border:1px solid rgb(0,0,0, 0.1);cursor:pointer;padding:12px 30px 12px 30px"
target="_blank"
href="https://dashboard.litlyx.com"><span></span><span
style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px">
Go to Dashboard</span><span></span></a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="padding:45px 0 0 0">
<tbody>
<tr>
<td>
<img src="https://react-email-demo-lpdmf0ryo-resend.vercel.app/static/yelp-footer.png"
style="display:block;outline:none;border:none;text-decoration:none;max-width:100%"
width="620" />
</td>
</tr>
</tbody>
</table>
<p style="font-size:12px;line-height:24px;margin:16px 0;text-align:center;color:rgb(0,0,0, 0.7)">
2024 © Litlyx. All rights reserved.
<br>
Litlyx S.R.L. - Viale Tirreno, 187 - 00141 Rome - P.IVA: 17814721001- REA: RM-1743194
</p>
</td>
</tr>
</tbody>
</table>
</body>
</html>
`