add email login

This commit is contained in:
Emily
2024-10-10 15:49:55 +02:00
parent 0a7f2b58d0
commit 80e3b0caa9
14 changed files with 531 additions and 27 deletions

4
TODO
View File

@@ -1,4 +0,0 @@
- Email login (remove/fix github login)
- Remove github login

View File

@@ -35,13 +35,21 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
if (!to.name) return; if (!to.name) return;
const { user } = useLoggedUser(); const { user } = useLoggedUser();
await handleUserLogin(user.value); await handleUserLogin(user.value);
if (user.value?.logged) { if (user.value?.logged) {
if (to.path == '/login') return '/'; if (to.path == '/login' || to.path == '/register') return '/';
} else { } else {
if (to.path != '/login' && to.path != '/live_demo') return '/login'; if (
to.path != '/login' &&
to.path != '/register' &&
to.path != '/live_demo' &&
to.path != '/jwt_login'
) {
console.log('REDIRECT TO LOGIN', to.path);
return '/login';
}
} }
}) })

View File

@@ -7,13 +7,22 @@ const route = useRoute();
const { project, projectList, projectId } = useProject(); const { project, projectList, projectId } = useProject();
const justLogged = computed(() => route.query.just_logged); const justLogged = computed(() => route.query.just_logged);
const jwtLogin = computed(() => route.query.jwt_login as string);
onMounted(() => { const { token, setToken } = useAccessToken();
if (justLogged.value) {
setTimeout(() => { onMounted(async () => {
location.href = '/'
}, 500) if (jwtLogin.value) {
setToken(jwtLogin.value);
const user = await $fetch<any>('/api/user/me', { headers: { 'Authorization': 'Bearer ' + token.value } })
const loggedUser = useLoggedUser();
loggedUser.user = user;
// setTimeout(() => { location.reload(); }, 100);
} }
if (justLogged.value) { setTimeout(() => { location.href = '/' }, 500) }
}) })
const firstInteraction = useFetch<boolean>('/api/project/first_interaction', { const firstInteraction = useFetch<boolean>('/api/project/first_interaction', {

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
definePageMeta({ layout: 'none' });
const route = useRoute();
const jwtLogin = computed(() => route.query.jwt_login as string);
const { token, setToken } = useAccessToken();
onMounted(async () => {
if (jwtLogin.value) {
setToken(jwtLogin.value);
const user = await $fetch<any>('/api/user/me', { headers: { 'Authorization': 'Bearer ' + token.value } })
const loggedUser = useLoggedUser();
loggedUser.user = user;
setTimeout(() => { location.href = '/project_creation?just_logged=true' }, 100);
}
})
</script>
<template>
<div> You will be redirected soon </div>
</template>
<style scoped lang="scss"></style>

View File

@@ -20,9 +20,9 @@ async function loginWithoutAuth() {
const user = await $fetch<any>('/api/user/me', { headers: { 'Authorization': 'Bearer ' + token.value } }) const user = await $fetch<any>('/api/user/me', { headers: { 'Authorization': 'Bearer ' + token.value } })
const loggedUser = useLoggedUser(); const loggedUser = useLoggedUser();
loggedUser.value = user; loggedUser.user = user;
console.log('LOGIN DONE - USER', loggedUser.value); console.log('LOGIN DONE - USER', loggedUser.user);
const isFirstTime = await $fetch<boolean>('/api/user/is_first_time', { headers: { 'Authorization': 'Bearer ' + token.value } }) const isFirstTime = await $fetch<boolean>('/api/user/is_first_time', { headers: { 'Authorization': 'Bearer ' + token.value } })
@@ -58,9 +58,9 @@ async function handleOnSuccess(response: any) {
const user = await $fetch<any>('/api/user/me', { headers: { 'Authorization': 'Bearer ' + token.value } }) const user = await $fetch<any>('/api/user/me', { headers: { 'Authorization': 'Bearer ' + token.value } })
const loggedUser = useLoggedUser(); const loggedUser = useLoggedUser();
loggedUser.value = user; loggedUser.user = user;
console.log('LOGIN DONE - USER', loggedUser.value); console.log('LOGIN DONE - USER', loggedUser.user);
const isFirstTime = await $fetch<boolean>('/api/user/is_first_time', { headers: { 'Authorization': 'Bearer ' + token.value } }) const isFirstTime = await $fetch<boolean>('/api/user/is_first_time', { headers: { 'Authorization': 'Bearer ' + token.value } })
@@ -107,6 +107,48 @@ onMounted(() => {
} }
}) })
const isEmailLogin = ref<boolean>(false);
const email = ref<string>("");
const password = ref<string>("");
function goBackToEmailLogin() {
isEmailLogin.value = false;
email.value = '';
password.value = '';
}
async function signInWithCredentials() {
try {
const result = await $fetch<{error:true, message:string} | {error: false, access_token:string}>('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email.value, password: password.value })
})
if (result.error) return alert(result.message);
setToken(result.access_token);
const user = await $fetch<any>('/api/user/me', { headers: { 'Authorization': 'Bearer ' + token.value } })
const loggedUser = useLoggedUser();
loggedUser.user = user;
console.log('LOGIN DONE - USER', loggedUser.user);
const isFirstTime = await $fetch<boolean>('/api/user/is_first_time', { headers: { 'Authorization': 'Bearer ' + token.value } })
if (isFirstTime === true) {
router.push('/project_creation?just_logged=true');
} else {
router.push('/?just_logged=true');
}
} catch (ex) {
alert('Something went wrong.');
}
}
</script> </script>
@@ -126,7 +168,7 @@ onMounted(() => {
</div> </div>
<div class="text-text text-[2.2rem] font-bold poppins"> <div class="text-text text-[2.2rem] font-bold poppins">
Sign in with Sign in
</div> </div>
<div class="text-text/80 text-[1.2rem] text-center w-[70%] poppins mt-2"> <div class="text-text/80 text-[1.2rem] text-center w-[70%] poppins mt-2">
@@ -141,23 +183,52 @@ onMounted(() => {
<div class="mt-12"> <div class="mt-12">
<div v-if="!isNoAuth && isEmailLogin" class="flex flex-col gap-2">
<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>
<LyxUiInput class="px-3 py-2" placeholder="Password" v-model="password" type="password">
</LyxUiInput>
</div>
<div class="flex justify-center mt-4 z-[100]">
<LyxUiButton @click="signInWithCredentials()" class="text-center" type="primary">
Sign in
</LyxUiButton>
</div>
<div @click="goBackToEmailLogin()"
class="mt-4 text-center text-lyx-text-dark underline cursor-pointer z-[100]">
Go back
</div>
</div>
<div v-if="!isNoAuth && !isEmailLogin" class="flex flex-col gap-2">
<div v-if="!isNoAuth" class="flex flex-col gap-2">
<div @click="login" <div @click="login"
class="hover:bg-accent cursor-pointer flex text-[1.3rem] gap-4 items-center border-[1px] border-gray-400 rounded-lg px-8 py-3 relative z-[2]"> class="hover:bg-lyx-primary cursor-pointer flex text-[1.3rem] gap-4 items-center border-[1px] border-gray-400 rounded-lg px-8 py-3 relative z-[2]">
<div class="flex items-center"> <div class="flex items-center">
<i class="fab fa-google"></i> <i class="fab fa-google"></i>
</div> </div>
Continue with Google Continue with Google
</div> </div>
<div <div @click="isEmailLogin = true"
class=" opacity-35 cursor-not-allowed flex text-[1.3rem] gap-4 items-center border-[1px] border-gray-400 rounded-lg px-8 py-3 relative z-[2]"> class="hover:bg-[#262626] cursor-pointer flex text-[1.3rem] gap-4 items-center border-[1px] border-gray-400 rounded-lg px-8 py-3 relative z-[2]">
<div class="flex items-center"> <div class="flex items-center">
<i class="fab fa-github"></i> <i class="far fa-envelope"></i>
</div> </div>
Continue with GitHub Continue with Email
</div> </div>
<RouterLink tag="div" to="/register" class="mt-4 text-center text-lyx-text-dark underline cursor-pointer z-[100]">
You don't have an account ? Sign up
</RouterLink>
</div> </div>
<div v-if="isNoAuth" @click="loginWithoutAuth" <div v-if="isNoAuth" @click="loginWithoutAuth"
@@ -170,7 +241,7 @@ onMounted(() => {
</div> </div>
<div class="text-[.9rem] poppins mt-12 text-text-sub text-center relative z-[2]"> <div class="text-[.9rem] poppins mt-20 text-text-sub text-center relative z-[2]">
By continuing you are accepting By continuing you are accepting
<br> <br>
our our

View File

@@ -0,0 +1,170 @@
<script setup lang="ts">
definePageMeta({ layout: 'none' });
const emailSended = ref<boolean>(false);
const email = ref<string>("");
const password = ref<string>("");
const passwordConfirm = ref<string>("");
const canRegister = computed(() => {
if (email.value.length == 0) return false;
if (!email.value.includes('@')) return false;
if (!email.value.includes('.')) return false;
if (password.value !== passwordConfirm.value) return false;
if (password.value.length < 6) return false;
return true;
});
async function registerAccount() {
try {
const res = await $fetch<any>('/api/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: email.value, password: password.value })
});
if (res.error === true) return alert(res.message);
emailSended.value = true;
} catch (ex) {
alert('Something went wrong');
}
}
</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 lg: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">
Sign up
</div>
<div class="text-text/80 text-[1.2rem] text-center w-[70%] poppins mt-2">
Track web analytics and custom events
<br>
with extreme simplicity in under 30 sec.
<br>
<!-- <div class="font-bold poppins mt-4">
Start for Free now! Up to 3k visits/events monthly.
</div> -->
</div>
<div v-if="!emailSended" class="mt-12">
<div class="flex flex-col gap-2">
<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>
<LyxUiInput class="px-3 py-2" placeholder="Password" v-model="password" type="password">
</LyxUiInput>
<LyxUiInput class="px-3 py-2" placeholder="Confirm password" v-model="passwordConfirm"
type="password">
</LyxUiInput>
</div>
<div class="text-lyx-text-darker text-end text-[.8rem]">
Password must be at least 6 chars long
</div>
<div class="flex justify-center mt-4 z-[100]">
<LyxUiButton :disabled="!canRegister" @click="registerAccount()" class="text-center"
type="primary">
Sign up
</LyxUiButton>
</div>
<RouterLink to="/login"
class="mt-4 text-center text-lyx-text-dark underline cursor-pointer z-[100]">
Go back to login
</RouterLink>
</div>
</div>
<div v-if="emailSended" class="mt-12 flex flex-col text-center text-[1.1rem] z-[100]">
<div>
We sent you a confirm email.
</div>
<div>
Please check your inbox to confirm your account and complete your registration.
</div>
<RouterLink tag="div" to="/login"
class="mt-6 text-center text-lyx-text-dark underline cursor-pointer">
Go back
</RouterLink>
</div>
<div v-if="!emailSended" class="text-[.9rem] poppins mt-20 text-text-sub text-center relative z-[2]">
By continuing you are accepting
<br>
our
<a class="underline" href="https://litlyx.com/terms" target="_blank">Terms of Service</a> and
<a class="underline" href="https://litlyx.com/privacy" target="_blank">Privacy Policy</a>.
</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>
</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

@@ -3,8 +3,8 @@ import jwt from 'jsonwebtoken';
const { AUTH_JWT_SECRET } = useRuntimeConfig(); const { AUTH_JWT_SECRET } = useRuntimeConfig();
function createJwt(data: Object) { function createJwt(data: Object, expiresIn?: string) {
return jwt.sign(data, AUTH_JWT_SECRET, { expiresIn: '30d' }); return jwt.sign(data, AUTH_JWT_SECRET, { expiresIn: expiresIn ?? '30d' });
} }
function readJwt(data: string) { function readJwt(data: string) {
@@ -28,4 +28,13 @@ export function readUserJwt(raw: string) {
export function createUserJwt(data: TUserJwt) { export function createUserJwt(data: TUserJwt) {
return createJwt(data); return createJwt(data);
}
export function createRegisterJwt(email: string, hashedPassword: string) {
return createJwt({ email, password: hashedPassword }, '7d');
}
export function readRegisterJwt(raw: string) {
const data = readJwt(raw);
return data as { email: string, password: string } | undefined;
} }

View File

@@ -0,0 +1,24 @@
import { createUserJwt, readRegisterJwt } from '~/server/AuthManager';
import { UserModel } from '@schema/UserSchema';
import { PasswordModel } from '@schema/PasswordSchema';
import EmailService from '@services/EmailService';
export default defineEventHandler(async event => {
const { register_code } = getQuery(event);
const data = readRegisterJwt(register_code as string);
if (!data) return setResponseStatus(event, 400, 'Error decoding register_code');
try {
await PasswordModel.create({ email: data.email, password: data.password })
await UserModel.create({ email: data.email, given_name: '', name: 'EmailLogin', locale: '', picture: '', created_at: Date.now() });
setImmediate(() => { EmailService.sendWelcomeEmail(data.email); });
const jwt = createUserJwt({ email: data.email, name: 'EmailLogin' });
return sendRedirect(event,`https://dashboard.litlyx.com/jwt_login?jwt_login=${jwt}`);
} catch (ex) {
return setResponseStatus(event, 400, 'Error creating user');
}
});

View File

@@ -0,0 +1,24 @@
import { createUserJwt } from '~/server/AuthManager';
import { UserModel } from '@schema/UserSchema';
import crypto from 'crypto';
import { PasswordModel } from '@schema/PasswordSchema';
export default defineEventHandler(async event => {
const { email, password } = await readBody(event);
const user = await UserModel.findOne({ email });
if (!user) return { error: true, message: 'Email or Password wrong' }
const hash = crypto.createHash('sha256');
const hashedPassword = hash.update(password + '_litlyx').digest('hex');
const target = await PasswordModel.findOne({ email, password: hashedPassword });
if (!target) return { error: true, message: 'Email or Password wrong' }
return { error: false, access_token: createUserJwt({ email: target.email, name: user.name }) }
});

View File

@@ -0,0 +1,45 @@
import { createRegisterJwt, createUserJwt } from '~/server/AuthManager';
import { UserModel } from '@schema/UserSchema';
import { RegisterModel } from '@schema/RegisterSchema';
import EmailService from '@services/EmailService';
import crypto from 'crypto';
function canRegister(email: string, password: string) {
if (email.length == 0) return false;
if (!email.includes('@')) return false;
if (!email.includes('.')) return false;
if (password.length < 6) return false;
return true;
};
export default defineEventHandler(async event => {
const { email, password } = await readBody(event);
if (!canRegister(email, password)) return setResponseStatus(event, 400, 'Email or Password not match criteria');
const user = await UserModel.findOne({ email });
if (user) return {
error: true,
message: 'Email already registered'
}
const hash = crypto.createHash('sha256');
const hashedPassword = hash.update(password + '_litlyx').digest('hex');
const jwt = createRegisterJwt(email, hashedPassword);
await RegisterModel.create({ email, password: hashedPassword });
setImmediate(() => {
EmailService.sendConfirmEmail(email, `https://dashboard.litlyx.com/api/auth/confirm_email?register_code=${jwt}`);
});
return {
error: false,
message: 'OK'
}
});

View File

@@ -0,0 +1,14 @@
import { model, Schema, Types } from 'mongoose';
export type TPassword = {
email: string,
password: string,
}
const PasswordSchema = new Schema<TPassword>({
email: { type: String, index: true, unique: true },
password: { type: String },
});
export const PasswordModel = model<TPassword>('passwords', PasswordSchema);

View File

@@ -0,0 +1,16 @@
import { model, Schema, Types } from 'mongoose';
export type TRegister = {
email: string,
password: string,
created_at: Date
}
const RegisterSchema = new Schema<TRegister>({
email: { type: String },
password: { type: String },
created_at: { type: Date, default: () => Date.now() }
});
export const RegisterModel = model<TRegister>('registers', RegisterSchema);

View File

@@ -6,7 +6,7 @@ import { LIMIT_MAX_EMAIL } from './email_templates/LimitMaxEmail';
import { PURCHASE_EMAIL } from './email_templates/PurchaseEmail'; 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';
class EmailService { class EmailService {
@@ -150,6 +150,24 @@ class EmailService {
} }
} }
async sendConfirmEmail(target: string, link: string) {
try {
const sendSmtpEmail = new SendSmtpEmail();
sendSmtpEmail.subject = "Confirm your email";
sendSmtpEmail.sender = { "name": "Litlyx", "email": "no-reply@litlyx.com" };
sendSmtpEmail.to = [{ "email": target }];
sendSmtpEmail.htmlContent = CONFIRM_EMAIL
.replace(/\[CONFIRM_LINK\]/, link)
.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,69 @@
export const CONFIRM_EMAIL = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Confirmation</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 20px;
}
.container {
background-color: #ffffff;
padding: 20px;
max-width: 600px;
margin: 0 auto;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h1 {
color: #333333;
}
p {
color: #555555;
line-height: 1.5;
}
.button {
display: inline-block;
padding: 10px 20px;
background-color: #007bff;
color: #ffffff;
text-decoration: none;
border-radius: 5px;
}
.footer {
margin-top: 20px;
font-size: 12px;
color: #777777;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<h2>Confirm your email on Litlyx</h2>
<p>Hello,</p>
<p>Thank you so much for signing up on Litlyx! Please confirm your email address by clicking the button below:
</p>
<p><a href="[CONFIRM_LINK]" class="button">Confirm Email</a></p>
<p>If you didn't create an account with us, you can safely ignore this email.</p>
<p>We hope to hear from you soon!</p>
<div class="footer">
<p>&copy; 2024 Litlyx. All rights reserved.</p>
</div>
</div>
</body>
</html>`