feat: implement server-side logout by deleting token to invalidate user session

This commit is contained in:
Chneemann 2025-04-22 06:46:54 +02:00
parent d77aee2b9f
commit 8a2ed1a733
5 changed files with 55 additions and 15 deletions

View file

@ -1,7 +1,7 @@
from django.conf import settings from django.conf import settings
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status, authentication from rest_framework import status, authentication, permissions
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from rest_framework.authtoken.views import ObtainAuthToken from rest_framework.authtoken.views import ObtainAuthToken
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
@ -36,6 +36,17 @@ class LoginView(APIView):
token, created = Token.objects.get_or_create(user=user) token, created = Token.objects.get_or_create(user=user)
return Response({'token': token.key}, status=status.HTTP_200_OK) return Response({'token': token.key}, status=status.HTTP_200_OK)
class LogoutView(APIView):
authentication_classes = [authentication.TokenAuthentication]
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
try:
request.user.auth_token.delete()
except Token.DoesNotExist:
return Response({'detail': 'Token not found.'}, status=status.HTTP_400_BAD_REQUEST)
return Response({'detail': 'Successfully logged out.'}, status=status.HTTP_200_OK)
class RegisterView(APIView): class RegisterView(APIView):
def post(self, request): def post(self, request):
email = request.data.get('email') email = request.data.get('email')
@ -116,7 +127,7 @@ class ForgotPasswordView(APIView):
message = EmailMultiAlternatives( message = EmailMultiAlternatives(
subject='Reset your Password', subject='Reset your Password',
body=strip_tags(html_body), # Plain text fallback body=strip_tags(html_body),
from_email=settings.DEFAULT_FROM_EMAIL, from_email=settings.DEFAULT_FROM_EMAIL,
to=[user.email] to=[user.email]
) )
@ -142,6 +153,7 @@ class ChangePasswordView(APIView):
class AuthView(ObtainAuthToken): class AuthView(ObtainAuthToken):
authentication_classes = [authentication.TokenAuthentication] authentication_classes = [authentication.TokenAuthentication]
permission_classes = [permissions.IsAuthenticated]
def get(self, request): def get(self, request):
if request.user.is_authenticated: if request.user.is_authenticated:

View file

@ -21,6 +21,7 @@ from django.conf.urls.static import static
from user_app import views as user_views from user_app import views as user_views
from auth_app.views import ( from auth_app.views import (
LoginView, LoginView,
LogoutView,
RegisterView, RegisterView,
VerifyEmailView, VerifyEmailView,
AuthView, AuthView,
@ -49,6 +50,7 @@ urlpatterns = [
# Authentication URLs # Authentication URLs
path('auth/', AuthView.as_view(), name='auth_view'), path('auth/', AuthView.as_view(), name='auth_view'),
path('auth/login/', LoginView.as_view(), name='login'), path('auth/login/', LoginView.as_view(), name='login'),
path('auth/logout/', LogoutView.as_view(), name='logout'),
path('auth/register/', RegisterView.as_view(), name='register'), path('auth/register/', RegisterView.as_view(), name='register'),
path('auth/verify-email/', VerifyEmailView.as_view(), name='verify_email'), path('auth/verify-email/', VerifyEmailView.as_view(), name='verify_email'),
path('auth/forgot-password/', ForgotPasswordView.as_view(), name='forgot_password'), path('auth/forgot-password/', ForgotPasswordView.as_view(), name='forgot_password'),

View file

@ -40,6 +40,13 @@ export class AuthService {
this.storeAuthToken(data.token, storage); this.storeAuthToken(data.token, storage);
} }
async logout() {
const headers = this.getAuthHeaders();
await lastValueFrom(
this.http.post(`${environment.baseUrl}/auth/logout/`, null, { headers })
);
}
async verifyEmail(body: any) { async verifyEmail(body: any) {
await lastValueFrom( await lastValueFrom(
this.http.post(`${environment.baseUrl}/auth/verify-email/`, body) this.http.post(`${environment.baseUrl}/auth/verify-email/`, body)
@ -84,10 +91,9 @@ export class AuthService {
} }
private getAuthHeaders(): HttpHeaders { private getAuthHeaders(): HttpHeaders {
let authToken = localStorage.getItem('authToken'); let authToken =
if (!authToken) { localStorage.getItem('authToken') || sessionStorage.getItem('authToken');
authToken = sessionStorage.getItem('authToken');
}
return new HttpHeaders({ return new HttpHeaders({
Authorization: `Token ${authToken}`, Authorization: `Token ${authToken}`,
}); });

View file

@ -2,6 +2,7 @@ import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BehaviorSubject, lastValueFrom, Observable } from 'rxjs'; import { BehaviorSubject, lastValueFrom, Observable } from 'rxjs';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
import { Router } from '@angular/router';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -9,7 +10,7 @@ import { environment } from '../../environments/environment';
export class MovieService { export class MovieService {
private movieCache: { [key: number]: { [resolution: string]: boolean } } = {}; private movieCache: { [key: number]: { [resolution: string]: boolean } } = {};
constructor(private http: HttpClient) {} constructor(private http: HttpClient, private router: Router) {}
getAllMovies(): Promise<any> { getAllMovies(): Promise<any> {
const url = environment.baseUrl + '/video/'; const url = environment.baseUrl + '/video/';
@ -30,15 +31,24 @@ export class MovieService {
} }
private getAuthHeaders(): HttpHeaders { private getAuthHeaders(): HttpHeaders {
let authToken = localStorage.getItem('authToken'); let authToken =
localStorage.getItem('authToken') || sessionStorage.getItem('authToken');
if (!authToken) { if (!authToken) {
authToken = sessionStorage.getItem('authToken'); this.logout();
} }
return new HttpHeaders({ return new HttpHeaders({
Authorization: `Token ${authToken}`, Authorization: `Token ${authToken}`,
}); });
} }
private logout(): void {
localStorage.clear();
sessionStorage.clear();
this.router.navigate(['/']);
}
/** /**
* Checks if a movie is uploaded in multiple resolutions. * Checks if a movie is uploaded in multiple resolutions.
* *
@ -90,6 +100,9 @@ export class MovieService {
observer.complete(); observer.complete();
}, },
(error) => { (error) => {
if (error.status === 401) {
this.logout();
}
observer.next( observer.next(
this.movieCache[videoID] || { this.movieCache[videoID] || {
'360': false, '360': false,

View file

@ -1,6 +1,8 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { BtnLargeComponent } from '../buttons/btn-large/btn-large.component'; import { BtnLargeComponent } from '../buttons/btn-large/btn-large.component';
import { Router, RouterLink } from '@angular/router'; import { Router, RouterLink } from '@angular/router';
import { AuthService } from '../../../services/auth.service';
import { UserService } from '../../../services/user.service';
@Component({ @Component({
selector: 'app-header', selector: 'app-header',
@ -13,15 +15,20 @@ export class HeaderComponent {
@Input() showFullLogo: boolean = true; @Input() showFullLogo: boolean = true;
@Output() moviesChange = new EventEmitter<any[]>(); @Output() moviesChange = new EventEmitter<any[]>();
constructor(private router: Router) {} constructor(private router: Router, private authService: AuthService) {}
backToOverview(newMovies: any[]) { backToOverview(newMovies: any[]) {
this.moviesChange.emit(newMovies); this.moviesChange.emit(newMovies);
} }
logout() { async logout() {
localStorage.clear(); try {
sessionStorage.clear(); await this.authService.logout();
this.router.navigate(['/']); localStorage.clear();
sessionStorage.clear();
this.router.navigate(['/']);
} catch (error) {
console.log(error);
}
} }
} }