update error.service, added register send email for activation
This commit is contained in:
parent
1d1e6e58d9
commit
537519a3dd
9 changed files with 161 additions and 24 deletions
|
|
@ -4,6 +4,11 @@ from rest_framework import status
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
from .serializer import LoginSerializer
|
from .serializer import LoginSerializer
|
||||||
|
from users.serializer import UserSerializer
|
||||||
|
from django.utils.html import strip_tags
|
||||||
|
from django.core.mail import EmailMultiAlternatives
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
from users.models import CustomUser
|
||||||
|
|
||||||
class LoginView(APIView):
|
class LoginView(APIView):
|
||||||
serializer_class = LoginSerializer
|
serializer_class = LoginSerializer
|
||||||
|
|
@ -17,11 +22,46 @@ class LoginView(APIView):
|
||||||
user = authenticate(request, email=email, password=password)
|
user = authenticate(request, email=email, password=password)
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
|
if not user.is_active:
|
||||||
|
return Response({'password': 'Account is inactive, please check your mails'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
return self._create_token_response(user)
|
return self._create_token_response(user)
|
||||||
return Response({'detail': 'Unable to login with provided credentials.'}, status=status.HTTP_401_UNAUTHORIZED)
|
return Response({'detail': 'Unable to login with provided credentials.'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def _create_token_response(self, user):
|
def _create_token_response(self, user):
|
||||||
token, created = Token.objects.get_or_create(user=user)
|
token = Token.objects.get_or_create(user=user)
|
||||||
return Response({token.key}, status=status.HTTP_200_OK)
|
return Response({'token': token.key}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
class RegisterView(APIView):
|
||||||
|
def post(self, request):
|
||||||
|
email = request.data.get('email')
|
||||||
|
password = request.data.get('password')
|
||||||
|
if CustomUser.objects.filter(email=email).exists():
|
||||||
|
return Response({'detail': 'A user with this email already exists.'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
serialized = UserSerializer(data=request.data)
|
||||||
|
if serialized.is_valid():
|
||||||
|
user = serialized.save()
|
||||||
|
user.is_active = False
|
||||||
|
user.set_password(password)
|
||||||
|
user.save()
|
||||||
|
self.send_email(user)
|
||||||
|
return Response(serialized.data, status=status.HTTP_201_CREATED)
|
||||||
|
else:
|
||||||
|
return Response(serialized.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def send_email(self, user):
|
||||||
|
merge_data = {
|
||||||
|
'name': user.username
|
||||||
|
}
|
||||||
|
html_body = render_to_string("mail.html", merge_data)
|
||||||
|
|
||||||
|
message = EmailMultiAlternatives(
|
||||||
|
subject='Confirm your email',
|
||||||
|
body=strip_tags(html_body), # Plain text fallback
|
||||||
|
from_email='noreply@videoflix.com',
|
||||||
|
to=['andre.kempf.dev@gmail.com']
|
||||||
|
)
|
||||||
|
message.attach_alternative(html_body, "text/html")
|
||||||
|
message.send(fail_silently=False)
|
||||||
50
backend/videoflix/templates/mail.html
Normal file
50
backend/videoflix/templates/mail.html
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; }
|
||||||
|
.btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 9px 18px;
|
||||||
|
width: fit-content;
|
||||||
|
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 24px;
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
background-color: #2e3edf;
|
||||||
|
border: 1px solid #2e3edf;
|
||||||
|
transition: 300ms ease-in-out;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #2e3edf;
|
||||||
|
border: 1px solid #2e3edf;
|
||||||
|
transition: 300ms ease-in-out;
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 249px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<body>
|
||||||
|
<div class="center"><img src="https://andre-kempf.com/videoflix-logo.png" alt=""></div>
|
||||||
|
<p>Dear {{ name }},</p>
|
||||||
|
<p>
|
||||||
|
Thank you for registering with Videoflix. To complete your registration
|
||||||
|
and verify your email address, please click the link below:
|
||||||
|
</p>
|
||||||
|
<a class="btn" href="">Activate account</a>
|
||||||
|
<p>
|
||||||
|
If you did not create an account with us, please disregard this email.
|
||||||
|
</p>
|
||||||
|
<p>Best regards,</p>
|
||||||
|
<p>Your Videoflix Team.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -10,6 +10,7 @@ For the full list of settings and their values, see
|
||||||
https://docs.djangoproject.com/en/5.0/ref/settings/
|
https://docs.djangoproject.com/en/5.0/ref/settings/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
|
@ -25,6 +26,14 @@ SECRET_KEY = 'django-insecure-*o%q)*3l+9^fs!m0z_h=z9bc)su=%)fv$@ktb#pza-pvi^z*en
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|
||||||
|
# EMAIL
|
||||||
|
EMAIL_USE_TLS = True
|
||||||
|
EMAIL_HOST = 'smtp.gmail.com'
|
||||||
|
EMAIL_PORT = 587
|
||||||
|
EMAIL_HOST_USER = 'andre.kempf.dev@gmail.com'
|
||||||
|
EMAIL_HOST_PASSWORD = 'oyxwawshudwytgud'
|
||||||
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||||
|
|
||||||
ALLOWED_HOSTS = []
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
CORS_ALLOWED_ORIGINS = [
|
CORS_ALLOWED_ORIGINS = [
|
||||||
|
|
@ -61,7 +70,7 @@ ROOT_URLCONF = 'videoflix.urls'
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
'DIRS': [],
|
'DIRS': [os.path.join(BASE_DIR, 'templates')],
|
||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'context_processors': [
|
'context_processors': [
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,12 @@ Including another URLconf
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from users import views
|
from users import views
|
||||||
from auth.views import LoginView
|
from auth.views import LoginView, RegisterView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('users/', views.user_list),
|
path('users/', views.user_list),
|
||||||
path('users/<int:id>/', views.user_detail),
|
path('users/<int:id>/', views.user_detail),
|
||||||
path('auth/login/', LoginView.as_view()),
|
path('auth/login/', LoginView.as_view()),
|
||||||
|
path('auth/register/', RegisterView.as_view()),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#loginForm="ngForm"
|
#loginForm="ngForm"
|
||||||
(ngSubmit)="onSubmit(loginForm)"
|
(ngSubmit)="onSubmit(loginForm)"
|
||||||
onsubmit="return false"
|
onsubmit="return false"
|
||||||
|
(input)="this.errorService.clearError()"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="mail"
|
type="mail"
|
||||||
|
|
@ -28,9 +29,13 @@
|
||||||
} @else { @if (mail.touched && authData.mail.length > 0 &&
|
} @else { @if (mail.touched && authData.mail.length > 0 &&
|
||||||
!isUserEmailValid(authData.mail.toLowerCase())) {
|
!isUserEmailValid(authData.mail.toLowerCase())) {
|
||||||
<p>This is not a valid email format</p>
|
<p>This is not a valid email format</p>
|
||||||
}} @if ((errorService.error$ | async)) {
|
}}
|
||||||
<p>{{ errorService.error$ | async }}</p>
|
<!-- Backend error handling -->
|
||||||
}
|
<div *ngIf="errorService.error$ | async as errors">
|
||||||
|
<div *ngIf="errors?.['mail']">
|
||||||
|
<p>{{ errors["mail"] }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
|
|
@ -55,6 +60,12 @@
|
||||||
password.touched) {
|
password.touched) {
|
||||||
<p>Password is too short, min 6 characters</p>
|
<p>Password is too short, min 6 characters</p>
|
||||||
} }
|
} }
|
||||||
|
<!-- Backend error handling -->
|
||||||
|
<div *ngIf="errorService.error$ | async as errors">
|
||||||
|
<div *ngIf="errors?.['password']">
|
||||||
|
<p>{{ errors["password"] }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label class="container"
|
<label class="container"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component, ViewChild } from '@angular/core';
|
||||||
import { BtnLargeComponent } from '../../../shared/components/btn-large/btn-large.component';
|
import { BtnLargeComponent } from '../../../shared/components/btn-large/btn-large.component';
|
||||||
import { FormsModule, NgForm } from '@angular/forms';
|
import { FormsModule, NgForm } from '@angular/forms';
|
||||||
import { Router, RouterLink } from '@angular/router';
|
import { Router, RouterLink } from '@angular/router';
|
||||||
|
|
@ -45,12 +45,21 @@ export class LoginComponent {
|
||||||
this.router.navigate(['/browse/']);
|
this.router.navigate(['/browse/']);
|
||||||
this.errorService.clearError();
|
this.errorService.clearError();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
this.errorMsg(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMsg(error: any) {
|
||||||
if (error instanceof HttpErrorResponse) {
|
if (error instanceof HttpErrorResponse) {
|
||||||
this.errorService.setError(error.error.detail);
|
const errorTypes = ['mail', 'password'];
|
||||||
} else {
|
for (const type of errorTypes) {
|
||||||
this.errorService.setError('An unknown error has occurred.');
|
if (error.error[type]) {
|
||||||
}
|
this.errorService.setError(type, error.error[type]);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.errorService.clearError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
#registerForm="ngForm"
|
#registerForm="ngForm"
|
||||||
(ngSubmit)="onSubmit(registerForm)"
|
(ngSubmit)="onSubmit(registerForm)"
|
||||||
onsubmit="return false"
|
onsubmit="return false"
|
||||||
|
(input)="this.errorService.clearError()"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="mail"
|
type="mail"
|
||||||
|
|
|
||||||
|
|
@ -55,11 +55,11 @@ export class RegisterComponent {
|
||||||
this.registrationSuccess = true;
|
this.registrationSuccess = true;
|
||||||
this.errorService.clearError();
|
this.errorService.clearError();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpErrorResponse) {
|
// if (error instanceof HttpErrorResponse) {
|
||||||
this.errorService.setError(error.error.detail);
|
// this.errorService.setError(error.error.detail);
|
||||||
} else {
|
// } else {
|
||||||
this.errorService.setError('An unknown error has occurred.');
|
// this.errorService.setError('An unknown error has occurred.');
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,32 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { BehaviorSubject } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
export interface ErrorDetail {
|
||||||
|
type: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class ErrorService {
|
export class ErrorService {
|
||||||
private errorSubject = new BehaviorSubject<string | null>(null);
|
private errorSubject = new BehaviorSubject<ErrorDetail | null>(null);
|
||||||
error$ = this.errorSubject.asObservable();
|
|
||||||
|
|
||||||
setError(message: string | null) {
|
error$: Observable<{ [key: string]: string } | null> = this.errorSubject
|
||||||
this.errorSubject.next(message);
|
.asObservable()
|
||||||
|
.pipe(
|
||||||
|
map((errorDetail) => {
|
||||||
|
if (errorDetail) {
|
||||||
|
return { [errorDetail.type]: errorDetail.message };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setError(type: string, message: string) {
|
||||||
|
const errorDetail: ErrorDetail = { type, message };
|
||||||
|
this.errorSubject.next(errorDetail);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearError() {
|
clearError() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue