feat: worked on email verification
This commit is contained in:
parent
9881f79534
commit
d1e93a3631
7 changed files with 213 additions and 31 deletions
|
@ -91,16 +91,31 @@ export class AuthController {
|
|||
|
||||
@Get('verify-email')
|
||||
@UseGuards(LoginGuard)
|
||||
@Render('auth/verify-email')
|
||||
@ApiExcludeEndpoint()
|
||||
public async getVerifyEmail(@Query('code') code?: string): Promise<any> {
|
||||
public async verifyEmail(@Res() response: Response, @Query('code') code: string): Promise<any> {
|
||||
if (!code) {
|
||||
//TODO: Write error page.
|
||||
return response.render('base/error', {
|
||||
error_header: 'Invalid Verification Code',
|
||||
error_message:
|
||||
'The verification code provided is invalid. Please try sending your verification email again.',
|
||||
button_name: 'Go Back to Login',
|
||||
button_link: '/auth/login',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
login: 'login',
|
||||
};
|
||||
try {
|
||||
await this.authService.markEmailVerified(code);
|
||||
} catch (e) {
|
||||
return response.render('base/error', {
|
||||
error_header: 'Invalid Verification Code',
|
||||
error_message:
|
||||
'The verification code provided is invalid. Please try sending your verification email again.',
|
||||
button_name: 'Go Back to Login',
|
||||
button_link: '/auth/login',
|
||||
});
|
||||
}
|
||||
|
||||
response.redirect('/auth/login');
|
||||
}
|
||||
|
||||
//TODO: Work on interaction view.
|
||||
|
|
|
@ -42,8 +42,11 @@ export class AuthService {
|
|||
});
|
||||
}
|
||||
|
||||
await this.userService.createUser(username, email, password);
|
||||
await this.mailService.sendVerificationEmail(email, '11111');
|
||||
const user = await this.userService.createUser(username, email, password);
|
||||
|
||||
const emailVerificationCode = await this.generateCode(PASSWORD_RESET_CACHE_KEY);
|
||||
await this.storeEmailVerifyCode(emailVerificationCode, user.id);
|
||||
await this.mailService.sendVerificationEmail(email, emailVerificationCode);
|
||||
return { error: false, message: 'User registered' };
|
||||
}
|
||||
|
||||
|
@ -180,7 +183,7 @@ export class AuthService {
|
|||
await this.cleanupOldEmailVerificationCode(userId);
|
||||
|
||||
await Promise.all([
|
||||
this.redisService.set(code, userId, PASSWORD_RESET_EXPIRATION),
|
||||
this.redisService.set(getEmailVerifyKey(code), userId, PASSWORD_RESET_EXPIRATION),
|
||||
this.redisService.set(getUserToVerifyKey(userId), code, PASSWORD_RESET_EXPIRATION),
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ export class User {
|
|||
@Column({ length: MAX_STRING_LENGTH, unique: true })
|
||||
email: string;
|
||||
|
||||
@Column({ name: 'pending_email', length: MAX_STRING_LENGTH, nullable: true })
|
||||
pendingEmail: string | null;
|
||||
@Column({ name: 'pending_email', type: String, length: MAX_STRING_LENGTH, nullable: true })
|
||||
pendingEmail?: string | null;
|
||||
|
||||
@Column({ name: 'email_verified', default: false })
|
||||
emailVerified: boolean;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
UnauthorizedException,
|
||||
|
@ -13,6 +14,7 @@ import { RedisService } from '../../redis/service/redis.service';
|
|||
import { Span } from 'nestjs-otel';
|
||||
import {
|
||||
DISABLED_USER_ERROR,
|
||||
EMAIL_NOT_VERIFIED_ERROR,
|
||||
INVALID_CREDENTIALS_ERROR,
|
||||
USER_NOT_FOUND_ERROR,
|
||||
userCacheKeyGenerate,
|
||||
|
@ -36,7 +38,7 @@ export class UserService {
|
|||
* @throws NotFoundException
|
||||
*/
|
||||
@Span()
|
||||
async getUserById(id: number, relations: string[] = []): Promise<User> {
|
||||
async getUserById(id: number | string, relations: string[] = []): Promise<User> {
|
||||
if (this.clsService.get('authType') === 'session') {
|
||||
if (this.clsService.get('user').id === id) {
|
||||
return this.clsService.get('user');
|
||||
|
@ -142,9 +144,9 @@ export class UserService {
|
|||
async createUser(username: string, email: string, password: string): Promise<User> {
|
||||
const hashedPassword = await hash(password);
|
||||
|
||||
const user = this.userRepository.create({ email, username, password: hashedPassword });
|
||||
const userObject = this.userRepository.create({ email, username, password: hashedPassword });
|
||||
|
||||
return await this.userRepository.save(user);
|
||||
return await this.userRepository.save(userObject);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -171,6 +173,10 @@ export class UserService {
|
|||
throw new UnauthorizedException(INVALID_CREDENTIALS_ERROR);
|
||||
}
|
||||
|
||||
if (!user.emailVerified) {
|
||||
throw new ForbiddenException(EMAIL_NOT_VERIFIED_ERROR);
|
||||
}
|
||||
|
||||
if (user.disabled) {
|
||||
throw new UnauthorizedException(DISABLED_USER_ERROR);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ export const USER_NOT_FOUND_ERROR = 'User not found';
|
|||
export const INVALID_CREDENTIALS_ERROR =
|
||||
'The email or password you entered is incorrect or the user was not found';
|
||||
export const DISABLED_USER_ERROR = 'User is disabled';
|
||||
export const EMAIL_NOT_VERIFIED_ERROR = 'Email requires verification';
|
||||
|
||||
// Caching Constants for Redis
|
||||
|
||||
|
|
|
@ -38,6 +38,17 @@
|
|||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.shake {
|
||||
animation: shake 0.5s;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
@keyframes shake {
|
||||
0% { transform: translateX(0); }
|
||||
25% { transform: translateX(-5px); }
|
||||
50% { transform: translateX(5px); }
|
||||
75% { transform: translateX(-5px); }
|
||||
100% { transform: translateX(0); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="relative flex items-center justify-start min-h-screen">
|
||||
|
@ -53,10 +64,11 @@
|
|||
<div class="relative bg-gray-800 p-8 rounded-lg shadow-lg w-full max-w-md ml-16 login-prompt">
|
||||
<h2 class="text-2xl font-bold mb-2 text-white text-center">Welcome back!</h2>
|
||||
<p class="text-gray-400 mb-6 text-center">We're so excited to see you again!</p>
|
||||
<form action="{{ login_url }}" method="POST" class="space-y-6">
|
||||
<form id="loginForm" action="{{ login_url }}" method="POST" class="space-y-6">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-400">Email or Username</label>
|
||||
<input type="text" placeholder="yip@yap.yop" name="username" required class="mt-1 block w-full px-3 py-2 bg-gray-700 text-white border border-gray-600 rounded-md shadow-sm placeholder-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<p id="username-error" class="text-xs text-red-600 hidden"></p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-400">Password</label>
|
||||
|
@ -74,17 +86,17 @@
|
|||
<div class="mt-4 text-sm text-center">
|
||||
<span class="text-gray-400">Need an account? </span><a href="{{ register }}" class="font-medium text-indigo-500 hover:text-indigo-400">Register</a>
|
||||
</div>
|
||||
<div class="mt-6 flex items-center justify-center text-gray-400 text-xs">
|
||||
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 784.69 187.35" width="40%" height="40%">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
stroke-width: 0px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="WatchingWaterwolf">
|
||||
<div class="mt-6 flex items-center justify-center text-gray-400 text-xs">
|
||||
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 784.69 187.35" width="40%" height="40%">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
stroke-width: 0px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="WatchingWaterwolf">
|
||||
<g>
|
||||
<g>
|
||||
<polygon class="cls-1" points="269.31 78.37 260.14 136.91 248.7 78.37 244.28 78.37 239.92 78.37 235.38 78.37 223.91 137.03 214.7 78.37 198.81 78.37 214.82 165.36 220.98 165.36 225.1 165.36 230.48 165.36 242.08 109.33 253.48 165.36 258.92 165.36 262.92 165.36 269.19 165.36 285.15 78.37 269.31 78.37"/>
|
||||
|
@ -110,9 +122,89 @@
|
|||
<path id="path0" class="cls-1" d="M80.35,1.12C23.73,10.38-12.01,65.53,3.72,119.36c2.66,9.11,2.57,9.09,4.65.98,1.01-3.96,2.15-7.97,2.51-8.91.46-1.16.46-3.35,0-6.88C1.7,33.58,80.91-15.25,140.59,24.61c62.01,41.42,41.22,138.57-32.4,151.41-9.96,1.74-9.44,1.91-17.14-5.73-7.48-7.42-7.33-7.49-4.35,2.04l1.5,4.82-4.37-.47c-6.64-.71-16.51-3.4-18.16-4.95-4.5-4.23-8.03-17.49-8.49-31.9-.16-5.13-.52-9.35-.8-9.37-3.57-.26-12.18,3.82-18.23,8.63-5.47,4.35-5.51,4.31-3.58-3.27,6.35-24.86,25.51-50.23,46.13-61.08l4.59-2.41-2.5-.47c-1.37-.26-5.19-.32-8.48-.13l-5.99.34,6.26-3.14c7.29-3.66,12.94-5.63,19.12-6.65,2.54-.42,4.42-1.06,4.42-1.5,0-2.75,17.52-24.58,19.72-24.58.32,0,.39,3,.17,6.71-.5,8.33-.65,8.12,9.92,13.97,9.29,5.14,10.94,6.49,10.67,8.77-.32,2.77-2.15,2.36-4.07-.92-1.99-3.39-3.89-4.57-8.77-5.46-4.07-.74-4.29-.51-3.37,3.57,1.14,5.08,4.17,7.32,12.24,9.03,9.23,1.96,24.42,8.39,23.96,10.15-.17.67.2,2,.83,2.95,1.1,1.67,1.09,1.81-.17,3.51-4.4,5.94-10.39,7.17-17.04,3.5-14.31-7.9-23.73-11.17-33.69-11.72-9.43-.51-17,1.63-9.42,2.67,3.57.49,7.31,2.31,9.46,4.63l1.59,1.7h-3.23c-21.98.09-38.75,19.17-33.92,38.6,1.68,6.75,6.31,15.09,12.02,21.63,2.26,2.59,2.38,2.46.96-.94-3.79-9.06-1.92-25.02,3.66-31.38,4.06-4.62,4.44-4.42,4.07,2.11-1.12,19.7,12.31,32.5,37.18,35.43,4.57.54,4.82.24,1.45-1.68-13.6-7.76-23.89-25.44-21.7-37.28,2.68-14.47,11.58-19.55,31.42-17.92l9.68.79,2.88-1.69c6.92-4.05,15.9-14.15,14.15-15.91-.53-.53-6.4-3.31-13.04-6.18l-12.07-5.22-.33-4.1c-.41-5.11-.38-5.07-6.27-9.04-4.45-3-4.87-3.48-4.87-5.5,0-5.65-3.19-17.27-4.52-16.45-.34.21-.82,3.62-1.08,7.58-.48,7.36-1.03,8.5-2.98,6.14-.62-.75-.88-3.38-.9-9.1-.02-8.59-.79-10.81-3.75-10.81-4.6,0-18.11,10.78-26.81,21.39-1.4,1.71-2.77,2.35-7.85,3.71-20.14,5.36-47.11,21.68-54.6,33.04-2.46,3.73-2.09,3.82,3.62.86,11.63-6.03,24.03-9.95,17.65-5.59-18.72,12.81-30.43,34.04-34.32,62.23-.94,6.84,20.19,26.42,37.06,34.32,69.91,32.76,147.48-28.19,131.88-103.62C175.69,26.85,127.77-6.64,80.35,1.12"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('loginForm').addEventListener('submit', async function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.target;
|
||||
const formData = new FormData(form);
|
||||
const loginData = {
|
||||
username: formData.get('username'),
|
||||
password: formData.get('password'),
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(form.action, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(loginData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessageDiv = document.getElementById('username-error');
|
||||
const usernameInput = form.querySelector('input[name="username"]');
|
||||
const passwordInput = form.querySelector('input[name="password"]');
|
||||
errorMessageDiv.classList.remove('hidden');
|
||||
usernameInput.classList.add('border-red-600', 'shake');
|
||||
passwordInput.classList.add('border-red-600', 'shake');
|
||||
|
||||
setTimeout(() => {
|
||||
usernameInput.classList.remove('shake');
|
||||
passwordInput.classList.remove('shake');
|
||||
}, 500);
|
||||
|
||||
if (response.status === 401) {
|
||||
errorMessageDiv.textContent = 'Incorrect Password/Username';
|
||||
} else if (response.status === 403) {
|
||||
const errorData = await response.json();
|
||||
if (errorData.message === 'Account is disabled') {
|
||||
errorMessageDiv.textContent = 'Account is disabled';
|
||||
} else if (errorData.message === 'Email requires verification') {
|
||||
errorMessageDiv.innerHTML = 'Email requires verification. <button id="resend-verification" class="text-indigo-500 hover:text-indigo-400">Resend verification email</button>';
|
||||
document.getElementById('resend-verification').addEventListener('click', async function() {
|
||||
try {
|
||||
const emailResponse = await fetch('/auth/email-verification', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email: loginData.username }),
|
||||
});
|
||||
|
||||
if (emailResponse.status === 429) {
|
||||
errorMessageDiv.textContent = 'Rate limit exceeded. Please wait.';
|
||||
} else if (emailResponse.ok) {
|
||||
alert('Verification email resent!');
|
||||
} else {
|
||||
errorMessageDiv.textContent = 'Failed to resend verification email. Please try again later.';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
errorMessageDiv.textContent = 'An error occurred. Please try again later.';
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
errorMessageDiv.textContent = 'An unknown error occurred';
|
||||
}
|
||||
} else {
|
||||
// Handle successful login
|
||||
alert('Login successful!');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
const errorMessageDiv = document.getElementById('username-error');
|
||||
errorMessageDiv.classList.remove('hidden');
|
||||
errorMessageDiv.textContent = 'An error occurred. Please try again later.';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
65
views/base/error.hbs
Normal file
65
views/base/error.hbs
Normal file
|
@ -0,0 +1,65 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Error Page</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.video-bg {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: -1;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.video-bg, .overlay {
|
||||
display: none;
|
||||
}
|
||||
.error-prompt {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
position: static;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="relative flex items-center justify-start min-h-screen">
|
||||
{{#if background_image}}
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-cover bg-center bg-no-repeat" style="background-image: url('{{background_image}}')"></div>
|
||||
{{else}}
|
||||
<video autoplay muted loop class="video-bg">
|
||||
<source src="/assets/login.webm" type="video/webm">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
{{/if}}
|
||||
<div class="overlay"></div>
|
||||
<div class="relative bg-gray-800 p-8 rounded-lg shadow-lg w-full max-w-md ml-16 error-prompt">
|
||||
<h2 class="text-2xl font-bold mb-2 text-white text-center">{{ error_header }}</h2>
|
||||
<p class="text-gray-400 mb-6 text-center">{{ error_message }}</p>
|
||||
{{#if button_name}}
|
||||
<div class="text-center">
|
||||
<button type="button" class="inline-block px-6 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-500 hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"><a href="{{ button_link }}">{{ button_name }}</a></button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue