user is now unlocked by his email and token from the email link

This commit is contained in:
Chneemann 2024-08-05 18:02:51 +02:00
parent 3b4e1634ab
commit 133735e2f7
12 changed files with 156 additions and 14 deletions

View file

@ -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)

View file

@ -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>

View file

@ -13,7 +13,7 @@ class CustomUserAdmin(UserAdmin):
'Individual data', 'Individual data',
{ {
'fields': ( 'fields': (
'verify_email', 'verify_email_token',
) )
} }
) )

View file

@ -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))

View file

@ -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 },

View file

@ -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>

View file

@ -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);
}

View file

@ -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();
});
});

View file

@ -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();
}
}
}

View file

@ -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>

View file

@ -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',

View file

@ -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)
); );
} }
} }