user is now unlocked by his email and token from the email link
This commit is contained in:
parent
3b4e1634ab
commit
133735e2f7
12 changed files with 156 additions and 14 deletions
|
|
@ -9,6 +9,7 @@ from django.utils.html import strip_tags
|
||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from users.models import CustomUser
|
from users.models import CustomUser
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
class LoginView(APIView):
|
class LoginView(APIView):
|
||||||
serializer_class = LoginSerializer
|
serializer_class = LoginSerializer
|
||||||
|
|
@ -55,7 +56,7 @@ class RegisterView(APIView):
|
||||||
merge_data = {
|
merge_data = {
|
||||||
'name': user.username,
|
'name': user.username,
|
||||||
'email': user.email,
|
'email': user.email,
|
||||||
'token': user.verify_email
|
'token': user.verify_email_token
|
||||||
}
|
}
|
||||||
html_body = render_to_string("mail.html", merge_data)
|
html_body = render_to_string("mail.html", merge_data)
|
||||||
|
|
||||||
|
|
@ -70,4 +71,19 @@ class RegisterView(APIView):
|
||||||
|
|
||||||
class VerifyEmailView(APIView):
|
class VerifyEmailView(APIView):
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
pass
|
email = request.data.get('email')
|
||||||
|
token = request.data.get('token')
|
||||||
|
|
||||||
|
if not email or not token:
|
||||||
|
return Response({"error": "Email and token are required."}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
user = CustomUser.objects.filter(Q(email=email) & Q(verify_email_token=token)).first()
|
||||||
|
if user:
|
||||||
|
if user.is_active:
|
||||||
|
return Response({"error": "User has already been activated"}, status=status.HTTP_409_CONFLICT)
|
||||||
|
|
||||||
|
user.is_active = True
|
||||||
|
user.save()
|
||||||
|
return Response(status=status.HTTP_200_OK)
|
||||||
|
else:
|
||||||
|
return Response({"error": "Invalid email or token."}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
Thank you for registering with Videoflix. To complete your registration
|
Thank you for registering with Videoflix. To complete your registration
|
||||||
and verify your email address, please click the link below:
|
and verify your email address, please click the link below:
|
||||||
</p>
|
</p>
|
||||||
<a class="btn" href="http://localhost:4200/verify-email?token={{ token }}&email={{ email }}">Activate account</a>
|
<a class="btn" href="http://localhost:4200/verify-email?email={{ email }}&token={{ token }}">Activate account</a>
|
||||||
<p>
|
<p>
|
||||||
If you did not create an account with us, please disregard this email.
|
If you did not create an account with us, please disregard this email.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class CustomUserAdmin(UserAdmin):
|
||||||
'Individual data',
|
'Individual data',
|
||||||
{
|
{
|
||||||
'fields': (
|
'fields': (
|
||||||
'verify_email',
|
'verify_email_token',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
import secrets
|
|
||||||
import string
|
import string
|
||||||
from django.db import models
|
import secrets
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
class CustomUser(AbstractUser):
|
class CustomUser(AbstractUser):
|
||||||
verify_email = models.CharField(max_length=15, blank=True)
|
verify_email_token = models.CharField(max_length=20, blank=True, null=True)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if not self.verify_email:
|
if not self.verify_email_token:
|
||||||
self.verify_email = self.generate_verification_token()
|
self.verify_email_token = self.generate_verification_token()
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_verification_token(length=15):
|
def generate_verification_token(length=20):
|
||||||
characters = string.ascii_letters + string.digits
|
characters = string.ascii_letters + string.digits
|
||||||
return ''.join(secrets.choice(characters) for _ in range(length))
|
return ''.join(secrets.choice(characters) for _ in range(length))
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ export const routes: Routes = [
|
||||||
{ path: 'login', component: HomeComponent },
|
{ path: 'login', component: HomeComponent },
|
||||||
{ path: 'register', component: HomeComponent },
|
{ path: 'register', component: HomeComponent },
|
||||||
{ path: 'forgot-password', component: HomeComponent },
|
{ path: 'forgot-password', component: HomeComponent },
|
||||||
|
{ path: 'verify-email', component: HomeComponent },
|
||||||
{ path: 'imprint', component: ImprintComponent },
|
{ path: 'imprint', component: ImprintComponent },
|
||||||
{ path: 'privacy-policy', component: PrivacyPolicyComponent },
|
{ path: 'privacy-policy', component: PrivacyPolicyComponent },
|
||||||
{ path: 'browse', component: BrowseComponent },
|
{ path: 'browse', component: BrowseComponent },
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<section>
|
||||||
|
<div class="content">
|
||||||
|
<div class="headline">Verify Email</div>
|
||||||
|
<div class="note">
|
||||||
|
@if (verified) {
|
||||||
|
<p>Your email has been successfully verified! You can now log in.</p>
|
||||||
|
<a routerLink="/login">To the login</a>
|
||||||
|
} @else {
|
||||||
|
<p>
|
||||||
|
There was an issue verifying your email.<br />Please try again or
|
||||||
|
contact support for assistance.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div *ngIf="errorService.error$ | async as errors">
|
||||||
|
<div *ngIf="errors?.['error']" class="error-msg">
|
||||||
|
<p>{{ errors["error"] }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
@import "./../../../../assets/style/colors.scss";
|
||||||
|
@import "./../../../../assets/style/form.scss";
|
||||||
|
@import "./../../../../assets/style/auth-layout.scss";
|
||||||
|
|
||||||
|
section {
|
||||||
|
background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)),
|
||||||
|
url(./../../../../assets/img/backgrounds/login.png);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { VerifyEmailComponent } from './verify-email.component';
|
||||||
|
|
||||||
|
describe('VerifyEmailComponent', () => {
|
||||||
|
let component: VerifyEmailComponent;
|
||||||
|
let fixture: ComponentFixture<VerifyEmailComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [VerifyEmailComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(VerifyEmailComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||||
|
import { AuthService } from '../../../services/auth.service';
|
||||||
|
import { ErrorService } from '../../../services/error.service';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-verify-email',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, RouterLink],
|
||||||
|
templateUrl: './verify-email.component.html',
|
||||||
|
styleUrl: './verify-email.component.scss',
|
||||||
|
})
|
||||||
|
export class VerifyEmailComponent {
|
||||||
|
verified: boolean = false;
|
||||||
|
|
||||||
|
authData = {
|
||||||
|
mail: '',
|
||||||
|
token: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private authService: AuthService,
|
||||||
|
public errorService: ErrorService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.route.queryParams.subscribe((params) => {
|
||||||
|
this.authData.mail = params['email'] || '';
|
||||||
|
this.authData.token = params['token'] || '';
|
||||||
|
this.verifyEmail();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyEmail() {
|
||||||
|
const body = {
|
||||||
|
email: this.authData.mail,
|
||||||
|
token: this.authData.token,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await this.authService.verifyEmail(body);
|
||||||
|
this.verified = true;
|
||||||
|
this.errorService.clearError();
|
||||||
|
} catch (error) {
|
||||||
|
this.verified = false;
|
||||||
|
this.errorMsg(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMsg(error: any) {
|
||||||
|
if (error instanceof HttpErrorResponse) {
|
||||||
|
const errorTypes = ['error'];
|
||||||
|
for (const type of errorTypes) {
|
||||||
|
if (error.error[type]) {
|
||||||
|
this.errorService.setError(type, error.error[type]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.errorService.clearError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,5 +6,6 @@
|
||||||
<app-forgot-password
|
<app-forgot-password
|
||||||
*ngIf="currentRoute === 'forgot-password'"
|
*ngIf="currentRoute === 'forgot-password'"
|
||||||
></app-forgot-password>
|
></app-forgot-password>
|
||||||
|
<app-verify-email *ngIf="currentRoute === 'verify-email'"></app-verify-email>
|
||||||
<app-footer></app-footer>
|
<app-footer></app-footer>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { CommonModule } from '@angular/common';
|
||||||
import { LoginComponent } from '../auth/login/login.component';
|
import { LoginComponent } from '../auth/login/login.component';
|
||||||
import { ForgotPasswordComponent } from '../auth/forgot-password/forgot-password.component';
|
import { ForgotPasswordComponent } from '../auth/forgot-password/forgot-password.component';
|
||||||
import { BrowseComponent } from './browse/browse.component';
|
import { BrowseComponent } from './browse/browse.component';
|
||||||
|
import { VerifyEmailComponent } from '../auth/verify-email/verify-email.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-home',
|
selector: 'app-home',
|
||||||
|
|
@ -20,6 +21,7 @@ import { BrowseComponent } from './browse/browse.component';
|
||||||
RegisterComponent,
|
RegisterComponent,
|
||||||
LoginComponent,
|
LoginComponent,
|
||||||
ForgotPasswordComponent,
|
ForgotPasswordComponent,
|
||||||
|
VerifyEmailComponent,
|
||||||
BrowseComponent,
|
BrowseComponent,
|
||||||
],
|
],
|
||||||
templateUrl: './home.component.html',
|
templateUrl: './home.component.html',
|
||||||
|
|
|
||||||
|
|
@ -11,22 +11,28 @@ export class AuthService {
|
||||||
|
|
||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
async login(body: any, storage: boolean): Promise<void> {
|
async login(body: any, storage: boolean) {
|
||||||
const data = await lastValueFrom(
|
const data = await lastValueFrom(
|
||||||
this.http.post<any>(`${environment.baseUrl}/auth/login/`, body)
|
this.http.post(`${environment.baseUrl}/auth/login/`, body)
|
||||||
);
|
);
|
||||||
this.storeAuthToken(data, storage);
|
this.storeAuthToken(data, storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async verifyEmail(body: any) {
|
||||||
|
await lastValueFrom(
|
||||||
|
this.http.post(`${environment.baseUrl}/auth/verify-email/`, body)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
storeAuthToken(data: any, storage: boolean) {
|
storeAuthToken(data: any, storage: boolean) {
|
||||||
storage
|
storage
|
||||||
? localStorage.setItem('authToken', data.toString())
|
? localStorage.setItem('authToken', data.toString())
|
||||||
: sessionStorage.setItem('authToken', data.toString());
|
: sessionStorage.setItem('authToken', data.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
async register(body: any): Promise<void> {
|
async register(body: any) {
|
||||||
await lastValueFrom(
|
await lastValueFrom(
|
||||||
this.http.post<any>(`${environment.baseUrl}/auth/register/`, body)
|
this.http.post(`${environment.baseUrl}/auth/register/`, body)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue