From 80e3b0caa9965f0cef4260266102e1c00fd8d7d0 Mon Sep 17 00:00:00 2001 From: Emily Date: Thu, 10 Oct 2024 15:49:55 +0200 Subject: [PATCH] add email login --- TODO | 4 - dashboard/middleware/01.client_auth.global.ts | 14 +- dashboard/pages/index.vue | 19 +- dashboard/pages/jwt_login.vue | 31 ++++ dashboard/pages/login.vue | 95 ++++++++-- dashboard/pages/register.vue | 170 ++++++++++++++++++ dashboard/server/AuthManager.ts | 13 +- dashboard/server/api/auth/confirm_email.ts | 24 +++ dashboard/server/api/auth/login.post.ts | 24 +++ dashboard/server/api/auth/register.post.ts | 45 +++++ shared/schema/PasswordSchema.ts | 14 ++ shared/schema/RegisterSchema.ts | 16 ++ shared/services/EmailService.ts | 20 ++- .../services/email_templates/ConfirmEmail.ts | 69 +++++++ 14 files changed, 531 insertions(+), 27 deletions(-) delete mode 100644 TODO create mode 100644 dashboard/pages/jwt_login.vue create mode 100644 dashboard/pages/register.vue create mode 100644 dashboard/server/api/auth/confirm_email.ts create mode 100644 dashboard/server/api/auth/login.post.ts create mode 100644 dashboard/server/api/auth/register.post.ts create mode 100644 shared/schema/PasswordSchema.ts create mode 100644 shared/schema/RegisterSchema.ts create mode 100644 shared/services/email_templates/ConfirmEmail.ts diff --git a/TODO b/TODO deleted file mode 100644 index f1b6b75..0000000 --- a/TODO +++ /dev/null @@ -1,4 +0,0 @@ -- Email login (remove/fix github login) - -- Remove github login - diff --git a/dashboard/middleware/01.client_auth.global.ts b/dashboard/middleware/01.client_auth.global.ts index c2ae583..19f3934 100644 --- a/dashboard/middleware/01.client_auth.global.ts +++ b/dashboard/middleware/01.client_auth.global.ts @@ -35,13 +35,21 @@ export default defineNuxtRouteMiddleware(async (to, from) => { if (!to.name) return; const { user } = useLoggedUser(); - + await handleUserLogin(user.value); if (user.value?.logged) { - if (to.path == '/login') return '/'; + if (to.path == '/login' || to.path == '/register') return '/'; } 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'; + } } }) diff --git a/dashboard/pages/index.vue b/dashboard/pages/index.vue index fb51276..27eb784 100644 --- a/dashboard/pages/index.vue +++ b/dashboard/pages/index.vue @@ -7,13 +7,22 @@ const route = useRoute(); const { project, projectList, projectId } = useProject(); const justLogged = computed(() => route.query.just_logged); +const jwtLogin = computed(() => route.query.jwt_login as string); -onMounted(() => { - if (justLogged.value) { - setTimeout(() => { - location.href = '/' - }, 500) +const { token, setToken } = useAccessToken(); + +onMounted(async () => { + + if (jwtLogin.value) { + setToken(jwtLogin.value); + const user = await $fetch('/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('/api/project/first_interaction', { diff --git a/dashboard/pages/jwt_login.vue b/dashboard/pages/jwt_login.vue new file mode 100644 index 0000000..ec1b57c --- /dev/null +++ b/dashboard/pages/jwt_login.vue @@ -0,0 +1,31 @@ + + + + + + + + \ No newline at end of file diff --git a/dashboard/pages/login.vue b/dashboard/pages/login.vue index 63e25be..794fb12 100644 --- a/dashboard/pages/login.vue +++ b/dashboard/pages/login.vue @@ -20,9 +20,9 @@ async function loginWithoutAuth() { const user = await $fetch('/api/user/me', { headers: { 'Authorization': 'Bearer ' + token.value } }) 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('/api/user/is_first_time', { headers: { 'Authorization': 'Bearer ' + token.value } }) @@ -58,9 +58,9 @@ async function handleOnSuccess(response: any) { const user = await $fetch('/api/user/me', { headers: { 'Authorization': 'Bearer ' + token.value } }) 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('/api/user/is_first_time', { headers: { 'Authorization': 'Bearer ' + token.value } }) @@ -107,6 +107,48 @@ onMounted(() => { } }) +const isEmailLogin = ref(false); +const email = ref(""); +const password = ref(""); + +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('/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('/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.'); + } +} @@ -126,7 +168,7 @@ onMounted(() => {
- Sign in with + Sign in
@@ -141,23 +183,52 @@ onMounted(() => {
+
+ + +
+ + + +
+ +
+ + Sign in + +
+ +
+ Go back +
+ + +
+ +
-
+ 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]">
Continue with Google
-
+
- +
- Continue with GitHub + Continue with Email
+ + + + You don't have an account ? Sign up + +
{
-
+
By continuing you are accepting
our diff --git a/dashboard/pages/register.vue b/dashboard/pages/register.vue new file mode 100644 index 0000000..93183b6 --- /dev/null +++ b/dashboard/pages/register.vue @@ -0,0 +1,170 @@ + + + + + + + + \ No newline at end of file diff --git a/dashboard/server/AuthManager.ts b/dashboard/server/AuthManager.ts index 07b227c..1124e8c 100644 --- a/dashboard/server/AuthManager.ts +++ b/dashboard/server/AuthManager.ts @@ -3,8 +3,8 @@ import jwt from 'jsonwebtoken'; const { AUTH_JWT_SECRET } = useRuntimeConfig(); -function createJwt(data: Object) { - return jwt.sign(data, AUTH_JWT_SECRET, { expiresIn: '30d' }); +function createJwt(data: Object, expiresIn?: string) { + return jwt.sign(data, AUTH_JWT_SECRET, { expiresIn: expiresIn ?? '30d' }); } function readJwt(data: string) { @@ -28,4 +28,13 @@ export function readUserJwt(raw: string) { export function createUserJwt(data: TUserJwt) { 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; } \ No newline at end of file diff --git a/dashboard/server/api/auth/confirm_email.ts b/dashboard/server/api/auth/confirm_email.ts new file mode 100644 index 0000000..01dd14f --- /dev/null +++ b/dashboard/server/api/auth/confirm_email.ts @@ -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'); + } + +}); \ No newline at end of file diff --git a/dashboard/server/api/auth/login.post.ts b/dashboard/server/api/auth/login.post.ts new file mode 100644 index 0000000..6f012ad --- /dev/null +++ b/dashboard/server/api/auth/login.post.ts @@ -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 }) } + +}); \ No newline at end of file diff --git a/dashboard/server/api/auth/register.post.ts b/dashboard/server/api/auth/register.post.ts new file mode 100644 index 0000000..fef023f --- /dev/null +++ b/dashboard/server/api/auth/register.post.ts @@ -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' + } + +}); \ No newline at end of file diff --git a/shared/schema/PasswordSchema.ts b/shared/schema/PasswordSchema.ts new file mode 100644 index 0000000..a15a2e0 --- /dev/null +++ b/shared/schema/PasswordSchema.ts @@ -0,0 +1,14 @@ +import { model, Schema, Types } from 'mongoose'; + +export type TPassword = { + email: string, + password: string, +} + +const PasswordSchema = new Schema({ + email: { type: String, index: true, unique: true }, + password: { type: String }, +}); + +export const PasswordModel = model('passwords', PasswordSchema); + diff --git a/shared/schema/RegisterSchema.ts b/shared/schema/RegisterSchema.ts new file mode 100644 index 0000000..2834d48 --- /dev/null +++ b/shared/schema/RegisterSchema.ts @@ -0,0 +1,16 @@ +import { model, Schema, Types } from 'mongoose'; + +export type TRegister = { + email: string, + password: string, + created_at: Date +} + +const RegisterSchema = new Schema({ + email: { type: String }, + password: { type: String }, + created_at: { type: Date, default: () => Date.now() } +}); + +export const RegisterModel = model('registers', RegisterSchema); + diff --git a/shared/services/EmailService.ts b/shared/services/EmailService.ts index 74367a2..06112d0 100644 --- a/shared/services/EmailService.ts +++ b/shared/services/EmailService.ts @@ -6,7 +6,7 @@ import { LIMIT_MAX_EMAIL } from './email_templates/LimitMaxEmail'; import { PURCHASE_EMAIL } from './email_templates/PurchaseEmail'; import { ANOMALY_VISITS_EVENTS_EMAIL } from './email_templates/AnomalyUsageEmail'; import { ANOMALY_DOMAIN_EMAIL } from './email_templates/AnomalyDomainEmail'; - +import { CONFIRM_EMAIL } from './email_templates/ConfirmEmail'; 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(); diff --git a/shared/services/email_templates/ConfirmEmail.ts b/shared/services/email_templates/ConfirmEmail.ts new file mode 100644 index 0000000..05f9a3b --- /dev/null +++ b/shared/services/email_templates/ConfirmEmail.ts @@ -0,0 +1,69 @@ + +export const CONFIRM_EMAIL = ` + + + + + + Email Confirmation + + + + +
+

Confirm your email on Litlyx

+

Hello,

+

Thank you so much for signing up on Litlyx! Please confirm your email address by clicking the button below: +

+

Confirm Email

+

If you didn't create an account with us, you can safely ignore this email.

+

We hope to hear from you soon!

+ + +
+ + +` \ No newline at end of file