commit aa54287126a11a0bd74c96e86456d655ec878b35 Author: Raphael Rouiller Date: Mon Jul 8 14:06:52 2024 +0200 Base diff --git a/.env.EXAMPLE b/.env.EXAMPLE new file mode 100644 index 0000000..a9bd092 --- /dev/null +++ b/.env.EXAMPLE @@ -0,0 +1,46 @@ +#---IP_ADDRESS---# +IP_ADDRESS= + +#---DJANGO_SUPERUSER---# +DJANGO_SUPERUSER_USERNAME= +DJANGO_SUPERUSER_EMAIL= +DJANGO_SUPERUSER_PASSWORD= + +#---USER---# +USER_SERVICE_NAME= +USER_APP_NAME= + +#---STATS---# +STAT_SERVICE_NAME= +APP_SERVICE_NAME= + +#---STAT_SERVICE---# +STAT_SERVICE_NAME= +STAT_APP_NAME= + +#---CHAT---# +CHAT_SERVICE_NAME= +CHAT_APP_NAME= + +#---ONLINE---# +ONLINE_SERVICE_NAME= +ONLINE_APP_NAME= + +#---BOT---# +BOT_SERVICE_NAME= +BOT_APP_NAME= + +#---DATABASE---# +DB_ARCHIVE_NAME= +DB_ARCHIVE_USER= +DB_ARCHIVE_PASSWORD= +DB_ARCHIVE_PORT= +DB_ARCHIVE_HOST= + +#---JWTSERVICE---# +JWT_SERVICE_NAME= +JWT_SECRET_KEY='!7fz021!8_u3rvacoc6()cv%r((^oy81yya(p2ma$p^-c!1+w7' + +#---DJANGO_SECRET_KEY---# +SECRET_KEY_USER='tphie*yo87rgi0$$wkmke#b)u)&@kl-r2tmk=z*hrcj^grkl4_' +SECRET_KEY_BOT='+ab!*5)ra!e$76h((gw9r80%g_%h8#li9-5h%+_6@5meg3a!jh' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..726e084 --- /dev/null +++ b/.gitignore @@ -0,0 +1,144 @@ +### Django ### +*.log +*.pot +*.pyc +__pycache__/ +local_settings.py +db.sqlite3 +db.sqlite3-journal +media + +### Django.Python Stack ### +# Byte-compiled / optimized / DLL files +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo + +# Django stuff: + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pdm +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# Migration files +000*.py +[0-9]*.py + +# Databases & Profile Pictures +db_archive/db/ +db_archive/profile_pictures/ + +.DS_Store diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5d3f601 --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +compose_file = docker-compose.yml + +volume_dir = db_archive/db db_archive/profile_pictures + +service = django + +all: build up + +build: + mkdir -p $(volume_dir) + docker-compose -f $(compose_file) build + +up: + mkdir -p $(volume_dir) + docker-compose -f $(compose_file) up -d --build + +down: + docker-compose -f $(compose_file) down -v + +logs: + docker-compose -f $(compose_file) logs + +shell: + docker-compose -f $(compose_file) exec $(service) /bin/bash + +clean: + docker-compose -f $(compose_file) down --rmi all --volumes + docker system prune -af + find db_archive/db db_archive/profile_pictures -type f ! -name .gitkeep -delete + find db_archive/db db_archive/profile_pictures -type d -empty -delete + +redo: + docker-compose -f $(compose_file) down -v + docker system prune -af + docker-compose -f $(compose_file) up -d + +quick: + docker-compose -f $(compose_file) down -v + docker-compose -f $(compose_file) up -d + +shutdown: + docker-compose -f $(compose_file) down -v + docker system prune -af + +re: clean all + +.PHONY: all build up down clean flclean re \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..055013e --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# 🔗 Archive de Liens COVID-19 + +## 📚 À propos du projet + +Ce projet vise à créer une archive d'informations de haute qualité sur le sujet de la pandémie de COVID-19 et des vaccins associés. L'objectif est de rassembler, archiver et rendre accessibles des sources d'information fiables et vérifiées. + +## 🛠️ Fonctionnalités implémentées + +✅ Liste des sources archivées +✅ Catégorisation des sources +✅ Page de suggestions anonymes +✅ Fonction de recherche +✅ Téléchargement de l'archive complète + +## 🚀 Installation et Configuration + +### 📋 Prérequis +- Docker 🐳 installé sur votre machine + +### 🔧 Étapes d'installation + +1. **Préparation de l'environnement :** + ```bash + # Démarrer Docker 🐳 + # Créer les dossiers nécessaires 📁 + mkdir -p db_archive/db_archive + ``` + +2. **Configuration de l'environnement :** + - Copier `.env.EXAMPLE` vers `.env` 📝 + - Remplir les données manquantes dans `.env` 🖊️ + +3. **Lancement du projet :** + ```bash + # Construire et démarrer les conteneurs Docker 🏗️ + docker-compose up -d --build + ``` + +4. **Accès au site :** + - Ouvrez votre navigateur et allez sur `https://localhost` 🌐 + - Acceptez l'avertissement du certificat SSL ⚠️ + +## 📝 Notes + +Ce projet est basé sur une configuration Docker et Django. + +## 🔒 Sécurité et confidentialité + +- Aucune donnée personnelle n'est collectée lors de l'inscription ou de la suggestion de sources. +- Le système d'authentification est conçu pour être totalement anonyme. + +## 📥 Téléchargement de l'archive + +L'ensemble de l'archive est disponible au téléchargement pour assurer la pérennité des informations, même en cas d'indisponibilité du site. + +## 🤝 Contribution + +Les suggestions de nouvelles sources sont les bienvenues. Utilisez la page de suggestions sur le site pour proposer de nouvelles ressources à archiver. + +## 👤 Développeur + +Ce projet est développé et maintenu par Raphael. + +Profitez de cette ressource d'information sur la COVID-19 ! 🦠🔬 \ No newline at end of file diff --git a/chat/ChatApp/ChatApp/__init__.py b/chat/ChatApp/ChatApp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chat/ChatApp/ChatApp/asgi.py b/chat/ChatApp/ChatApp/asgi.py new file mode 100644 index 0000000..cd7c8b3 --- /dev/null +++ b/chat/ChatApp/ChatApp/asgi.py @@ -0,0 +1,33 @@ +""" +ASGI config for ChatApp project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ +""" + +import os +from channels.routing import ProtocolTypeRouter, URLRouter +from django.core.asgi import get_asgi_application +from channels.auth import AuthMiddlewareStack + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ChatApp.settings') + +asgi_app = get_asgi_application() + + +from chat import routing + +application = ProtocolTypeRouter( + { + "http" : asgi_app, + "websocket" : AuthMiddlewareStack( + URLRouter( + routing.websocket_urlpatterns + ) + ) + } +) + +ASGI_APPLICATION = 'ChatApp.asgi.application' \ No newline at end of file diff --git a/chat/ChatApp/ChatApp/settings.py b/chat/ChatApp/ChatApp/settings.py new file mode 100644 index 0000000..91c1ada --- /dev/null +++ b/chat/ChatApp/ChatApp/settings.py @@ -0,0 +1,176 @@ +""" +Django settings for ChatApp project. + +Generated by 'django-admin startproject' using Django 4.2.13. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +import sys +from pathlib import Path +import environ + +env = environ.Env() +sys.path.append('/home/archive/user_auth_system') + +SECRET_KEY_ENV = env('SECRET_KEY_USER') + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = SECRET_KEY_ENV + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = False +IP_ADDRESS= env('IP_ADDRESS') +DB_NAME = env('DB_ARCHIVE_NAME') +DB_USER = env('DB_ARCHIVE_USER') +DB_PASSWORD = env('DB_ARCHIVE_PASSWORD') +DB_HOST = env('DB_ARCHIVE_HOST') +DB_PORT = env('DB_ARCHIVE_PORT') + +ALLOWED_HOSTS = ['*'] + + +# Application definition + +INSTALLED_APPS = [ + 'chat.apps.ChatConfig', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'daphne', + 'django.contrib.staticfiles', + 'channels', + 'corsheaders', + 'user_management' +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'ChatApp.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'ChatApp.wsgi.application' +ASGI_APPLICATION = 'ChatApp.asgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': DB_NAME, + 'USER': DB_USER, + 'PASSWORD': DB_PASSWORD, + 'HOST': DB_HOST, + 'PORT': DB_PORT, + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels_redis.core.RedisChannelLayer", + 'CONFIG': { + "hosts": [('redis', 6379)], + }, + }, +} + +LOGIN_REDIRECT_URL = "room" + +SESSION_ENGINE = 'django.contrib.sessions.backends.db' +SESSION_COOKIE_NAME = 'sessionid' +SESSION_COOKIE_HTTPONLY = True +SESSION_COOKIE_SAMESITE = 'Lax' +SESSION_COOKIE_SECURE = True + +CSRF_COOKIE_NAME = 'csrftoken' +CSRF_COOKIE_HTTPONLY = True +CSRF_COOKIE_SAMESITE = 'None' +CSRF_COOKIE_SECURE = True + +CORS_ALLOW_CREDENTIALS = True +CORS_ALLOW_ALL_ORIGINS = True + +CSRF_TRUSTED_ORIGINS = ['https://localhost', 'https://' + IP_ADDRESS] + +# Connexion to CustomUser +AUTH_USER_MODEL = 'user_management.CustomUser' diff --git a/chat/ChatApp/ChatApp/urls.py b/chat/ChatApp/ChatApp/urls.py new file mode 100644 index 0000000..3ddd2da --- /dev/null +++ b/chat/ChatApp/ChatApp/urls.py @@ -0,0 +1,23 @@ +""" +URL configuration for ChatApp project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + path("", include("chat.urls")), +] diff --git a/chat/ChatApp/ChatApp/wsgi.py b/chat/ChatApp/ChatApp/wsgi.py new file mode 100644 index 0000000..5cd9565 --- /dev/null +++ b/chat/ChatApp/ChatApp/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for ChatApp project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ChatApp.settings') + +application = get_wsgi_application() diff --git a/chat/ChatApp/chat/__init__.py b/chat/ChatApp/chat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chat/ChatApp/chat/admin.py b/chat/ChatApp/chat/admin.py new file mode 100644 index 0000000..9530b4f --- /dev/null +++ b/chat/ChatApp/chat/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from .models import * + +# Register your models here. +# admin.site.register(User) #?? I don't need this user \ No newline at end of file diff --git a/chat/ChatApp/chat/apps.py b/chat/ChatApp/chat/apps.py new file mode 100644 index 0000000..2fe899a --- /dev/null +++ b/chat/ChatApp/chat/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ChatConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'chat' diff --git a/chat/ChatApp/chat/consumers.py b/chat/ChatApp/chat/consumers.py new file mode 100644 index 0000000..3c7227b --- /dev/null +++ b/chat/ChatApp/chat/consumers.py @@ -0,0 +1,49 @@ +import json +from channels.generic.websocket import AsyncWebsocketConsumer +from channels.db import database_sync_to_async +from .models import ChatMessage, ChatRoom + +class ChatConsumer(AsyncWebsocketConsumer): + @database_sync_to_async + def create_chat(self, room_name, message, username): + return ChatMessage.objects.create(room_name=room_name, message=message, username=username) + + async def connect(self): + self.user = self.scope['user'] + self.room_name = self.scope['url_route']['kwargs']['room_name'] + self.room_group_name = f"chat_{self.room_name}" + + await self.channel_layer.group_add( + self.room_group_name, + self.channel_name + ) + await self.accept() + + async def disconnect(self , close_code): + await self.channel_layer.group_discard( + self.room_group_name, + self.channel_name + ) + async def receive(self, text_data): + text_data_json = json.loads(text_data) + message = text_data_json["message"] + username = text_data_json["username"] + + room = await self.get_room_instance(self.room_name) + await self.create_chat(room, message, username) + await self.channel_layer.group_send( + self.room_group_name,{ + "type" : "sendMessage", + "message" : message, + "username" : username, + }) + + async def sendMessage(self, event) : + message = event["message"] + username = event["username"] + + await self.send(text_data = json.dumps({"message" : message ,"username" : username})) + + @database_sync_to_async + def get_room_instance(self, room_name): + return ChatRoom.objects.get(room_name=room_name) \ No newline at end of file diff --git a/chat/ChatApp/chat/forms.py b/chat/ChatApp/chat/forms.py new file mode 100644 index 0000000..91f79cf --- /dev/null +++ b/chat/ChatApp/chat/forms.py @@ -0,0 +1,7 @@ +from django.contrib.auth.forms import UserCreationForm +from django.contrib.auth.models import User + +class SignUpForm(UserCreationForm): + class Meta: + model = User + fields = ['username', 'password1', 'password2'] diff --git a/chat/ChatApp/chat/migrations/__init__.py b/chat/ChatApp/chat/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chat/ChatApp/chat/models.py b/chat/ChatApp/chat/models.py new file mode 100644 index 0000000..ac91dc7 --- /dev/null +++ b/chat/ChatApp/chat/models.py @@ -0,0 +1,24 @@ +from django.db import models +from django.conf import settings + +class ChatRoom(models.Model): + room_name = models.CharField(max_length=255, unique=True, default='default') + user1 = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='user1', on_delete=models.CASCADE) + user2 = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='user2', on_delete=models.CASCADE) + + def __str__(self): + return self.room_name + +class ChatMessage(models.Model): + room_name = models.ForeignKey(ChatRoom, default='99999', related_name='messages', on_delete=models.CASCADE) + message = models.TextField() + username = models.CharField(max_length=100) + timestamp = models.DateTimeField(auto_now_add=True) + + def as_dict(self): + return { + "room_name": self.room_name, + "message": self.message, + "username": self.username, + "timestamp": self.timestamp.isoformat(), + } diff --git a/chat/ChatApp/chat/routing.py b/chat/ChatApp/chat/routing.py new file mode 100644 index 0000000..b2b00cf --- /dev/null +++ b/chat/ChatApp/chat/routing.py @@ -0,0 +1,8 @@ +from django.urls import path , include +from chat.consumers import ChatConsumer + +# Here, "" is routing to the URL ChatConsumer which +# will handle the chat functionality. +websocket_urlpatterns = [ + path("chat//", ChatConsumer.as_asgi()), +] diff --git a/chat/ChatApp/chat/tests.py b/chat/ChatApp/chat/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/chat/ChatApp/chat/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/chat/ChatApp/chat/urls.py b/chat/ChatApp/chat/urls.py new file mode 100644 index 0000000..5b3aed9 --- /dev/null +++ b/chat/ChatApp/chat/urls.py @@ -0,0 +1,9 @@ +from django.urls import path, include +from chat import views as chat_views +from django.contrib.auth.views import LogoutView, LoginView + + +urlpatterns = [ + path("chat//", chat_views.chatRoom, name="chat_room"), + path('users_list/', chat_views.userList, name='user_list'), +] diff --git a/chat/ChatApp/chat/views.py b/chat/ChatApp/chat/views.py new file mode 100644 index 0000000..d4ec3af --- /dev/null +++ b/chat/ChatApp/chat/views.py @@ -0,0 +1,56 @@ +from .models import * +from .forms import * +from django.http import JsonResponse +from rest_framework import status +from django.middleware.csrf import get_token +from django.shortcuts import get_object_or_404 +from django.db.models import Q +from user_management.models import CustomUser +from django.core import serializers +import json + +def chatRoom(request, username): + print("-+--+--+-- chatRoom function in views.py - beginning ---+--+-") + if not request.user.is_authenticated: + return JsonResponse({"error": "User not authenticated"}, status=status.HTTP_401_UNAUTHORIZED) + other_user = get_object_or_404(CustomUser, username=username) + if other_user == request.user: + # Prevent users from chatting with themselves + return JsonResponse({"error": "User cannot chat with herself/himself"}, status=status.HTTP_403_FORBIDDEN) + + # Check if the other user is blocked by the authenticated user + if request.user.blocked_users.filter(id=other_user.id).exists(): + return JsonResponse({"error": "User is blocked"}, status=status.HTTP_403_FORBIDDEN) + + room_name = f"{min(request.user.id, other_user.id)}_{max(request.user.id, other_user.id)}" + + chat_room = ChatRoom.objects.filter(Q(user1=request.user, user2=other_user) | Q(user1=other_user, user2=request.user)).first() + if not chat_room: + chat_room = ChatRoom.objects.create(user1=request.user, user2=other_user, room_name=room_name) + + messages = ChatMessage.objects.filter(room_name=chat_room).order_by('timestamp') + serialized_messages = serializers.serialize('json', messages) + messages_list = json.loads(serialized_messages) + + print( "-- room_name: ", room_name) + print( "-- username: ", request.user.username) + print("-- messages: ", serialized_messages) + print("-- other_user: ", other_user.username) + + context = { + "room_name": room_name, + "username": request.user.username, + "messages": messages_list, + "other_user": other_user.username, + "status": status.HTTP_200_OK + } + return JsonResponse(context, status=status.HTTP_200_OK) + +def userList(request): + if not request.user.is_authenticated: + return JsonResponse({"error": "User not authenticated"}, status=status.HTTP_401_UNAUTHORIZED) + if request.method == 'POST': + users = CustomUser.objects.exclude(username=request.user.username) + user_data = [{'username': user.username} for user in users] + return JsonResponse({'users': user_data}, status=status.HTTP_200_OK) + return JsonResponse({"error": "Invalid request method"}, status=status.HTTP_405_METHOD_NOT_ALLOWED) \ No newline at end of file diff --git a/chat/ChatApp/manage.py b/chat/ChatApp/manage.py new file mode 100755 index 0000000..bc025dc --- /dev/null +++ b/chat/ChatApp/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ChatApp.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/chat/Dockerfile b/chat/Dockerfile new file mode 100644 index 0000000..faae4ae --- /dev/null +++ b/chat/Dockerfile @@ -0,0 +1,31 @@ +FROM python:3.11-slim + +# declaration of service variables environment +ENV CHAT_SERVICE_NAME=${CHAT_SERVICE_NAME} +ARG CHAT_SERVICE_NAME + +# declaration of environment variables for python +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +COPY ./ChatApp /home/archive/${CHAT_SERVICE_NAME} + +RUN apt-get update && apt-get install -y netcat-openbsd + +RUN mkdir -p /home/archive +WORKDIR /home/archive + +# installation of dependencies +COPY ./conf/requirements.txt . +RUN pip3 install --no-cache-dir -r requirements.txt \ + && mkdir depedencies && mv requirements.txt depedencies + +# copy and execute the initialization script +COPY ./tools/service_setup.sh /home/archive/service_setup.sh +RUN chmod +x /home/archive/service_setup.sh + +# set the final working directory +WORKDIR /home/archive/${CHAT_SERVICE_NAME} + +# Command for running the application +CMD ["bash", "/home/archive/service_setup.sh"] diff --git a/chat/conf/requirements.txt b/chat/conf/requirements.txt new file mode 100644 index 0000000..e51dd36 --- /dev/null +++ b/chat/conf/requirements.txt @@ -0,0 +1,10 @@ +Django>=4.2,<4.3 +daphne==4.0.0 +channels==4.0.0 +channels_redis==4.0.0 +djangorestframework==3.15.0 +psycopg[binary]==3.1.12 +django-cors-headers +django-environ +pillow +pyotp \ No newline at end of file diff --git a/chat/tools/service_setup.sh b/chat/tools/service_setup.sh new file mode 100644 index 0000000..ac65312 --- /dev/null +++ b/chat/tools/service_setup.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +echo "Waiting for postgres to get up and running..." +while ! nc -z db_archive 5434; do + echo "waiting for postgress to be listening..." + sleep 1 +done +#waiting for user to migrate +sleep 5 +echo "PostgreSQL started" +pip install -U 'Twisted[tls,http2]' +python3 manage.py makemigrations +python3 manage.py migrate +daphne -b 0.0.0.0 -p 8004 ChatApp.asgi:application \ No newline at end of file diff --git a/db_archive/docker/Dockerfile b/db_archive/docker/Dockerfile new file mode 100644 index 0000000..00c9239 --- /dev/null +++ b/db_archive/docker/Dockerfile @@ -0,0 +1,13 @@ +# Use the official PostgreSQL image from the Docker Hub +FROM postgres:16.2 + +# RUN apt-get update +# RUN apt-get install vim -y + +# Add your initialization script to the container +COPY ./tools/postgres_init.sh /docker-entrypoint-initdb.d/postgres_init.sh + +# COPY ./conf/pg_hba.conf /etc/postgresql/pg_hba.conf + +# Ensure the script is executable +RUN chmod +x /docker-entrypoint-initdb.d/postgres_init.sh \ No newline at end of file diff --git a/db_archive/docker/tools/postgres_init.sh b/db_archive/docker/tools/postgres_init.sh new file mode 100644 index 0000000..54b377f --- /dev/null +++ b/db_archive/docker/tools/postgres_init.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Specify the path to your file +file_path="/var/lib/postgresql/data/pg_hba.conf" + +# Use sed to replace "trust" with "m5a" in the file +sed -i 's/trust/md5/g' "$file_path" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..00a5e49 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,104 @@ +services: + user: + container_name: user + build: + context: ./user + dockerfile: Dockerfile + args: + USER_SERVICE_NAME: ${USER_SERVICE_NAME} + DJANGO_SUPERUSER_USERNAME: ${DJANGO_SUPERUSER_USERNAME} + DJANGO_SUPERUSER_EMAIL: ${DJANGO_SUPERUSER_EMAIL} + DJANGO_SUPERUSER_PASSWORD: ${DJANGO_SUPERUSER_PASSWORD} + env_file: + - .env + expose: + - "8003" + volumes: + - user:/home/archive/${USER_SERVICE_NAME} + - media_volume:/home/archive/user_auth_system/media + tty: true + networks: + - archive + init: true + depends_on: + - db_archive + restart: on-failure + + chat: + container_name: chat + build: + context: ./chat + dockerfile: Dockerfile + args: + CHAT_SERVICE_NAME: ${CHAT_SERVICE_NAME} + env_file: + - .env + expose: + - "8004" + volumes: + - user:/home/archive/${USER_SERVICE_NAME} + tty: true + networks: + - archive + init: true + depends_on: + - user + - db_archive + restart: on-failure + + nginx: + container_name: nginx + depends_on: + - user + - chat + build: + context: ./nginx + dockerfile: Dockerfile + ports: + - "443:443" + - "80:80" + volumes: + - media_volume:/home/archive/user_auth_system/media + networks: + - archive + env_file: .env + restart: on-failure + + db_archive: + container_name: db_archive + build: + context: ./db_archive/docker + dockerfile: Dockerfile + environment: + POSTGRES_DB: ${DB_ARCHIVE_NAME} + POSTGRES_USER: ${DB_ARCHIVE_USER} + POSTGRES_PASSWORD: ${DB_ARCHIVE_PASSWORD} + PGPORT: ${DB_ARCHIVE_PORT} + volumes: + - db_archive:/var/lib/postgresql/data/ + expose: + - "5434" + tty: true + networks: + - archive + init: true + restart: on-failure + +volumes: + user: + db_archive: + driver: local + driver_opts: + type: "none" + o: "bind" + device: "${PWD}/db_archive/db" + media_volume: + driver: local + driver_opts: + type: "none" + o: "bind" + device: "${PWD}/db_archive/profile_pictures" + +networks: + archive: + driver: bridge diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 0000000..b9dbb2b --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,17 @@ +FROM alpine:3.19 + +RUN apk update + +RUN apk add nginx +RUN mkdir -p /etc/nginx/ssl +RUN apk add openssl +RUN openssl req -newkey rsa:4096 -x509 -sha256 -days 365 -nodes -out /etc/nginx/ssl/ping.crt -keyout /etc/nginx/ssl/ping.key -subj "/C=CH/ST=Vaud/L=Renens/O=42/OU=42/CN=ft_archive" +RUN mkdir -p /var/run/nginx +RUN mkdir -p /usr/share/nginx/html +RUN mkdir -p /usr/share/nginx/static +COPY ./conf/default /etc/nginx/conf.d/default.conf +COPY ./conf/nginx.conf /etc/nginx/nginx.conf +COPY ./static /usr/share/nginx/static +COPY ./html /usr/share/nginx/html + +CMD [ "nginx", "-g", "daemon off;" ] \ No newline at end of file diff --git a/nginx/conf/default b/nginx/conf/default new file mode 100644 index 0000000..e41d141 --- /dev/null +++ b/nginx/conf/default @@ -0,0 +1,66 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_certificate /etc/nginx/ssl/ping.crt; + ssl_certificate_key /etc/nginx/ssl/ping.key; + + server_name archive; + + location /ws/status/ { + proxy_pass http://user:8003; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + + #disable buffering for websockets + proxy_buffering off; + } + + location /chat/ { + proxy_pass http://chat:8004; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } + + location /users_list/ { + proxy_pass http://chat:8004; + proxy_http_version 1.1; + proxy_set_header Host $host; + } + + location /auth/ { + proxy_pass http://user:8003; + proxy_http_version 1.1; + proxy_set_header Host $host; + } + + location /static/ { + alias /usr/share/nginx/static/; + } + + location /media/ { + alias /home/archive/user_auth_system/media/; + } + + location / { + alias /usr/share/nginx/html/; + try_files $uri /index.html; + } +} diff --git a/nginx/conf/nginx.conf b/nginx/conf/nginx.conf new file mode 100644 index 0000000..bc471b6 --- /dev/null +++ b/nginx/conf/nginx.conf @@ -0,0 +1,103 @@ +# /etc/nginx/nginx.conf + +user nginx; + +# Set number of worker processes automatically based on number of CPU cores. +worker_processes auto; + +# Enables the use of JIT for regular expressions to speed-up their processing. +pcre_jit on; + +# Configures default error logger. +error_log /var/log/nginx/error.log warn; + +# Includes files with directives to load dynamic modules. +include /etc/nginx/modules/*.conf; + +# Include files with config snippets into the root context.; + +events { + # The maximum number of simultaneous connections that can be opened by + # a worker process. + worker_connections 1024; +} + +http { + # Includes mapping of file name extensions to MIME types of responses + # and defines the default type. + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Name servers used to resolve names of upstream servers into addresses. + # It's also needed when using tcpsocket and udpsocket in Lua modules. + #resolver 1.1.1.1 1.0.0.1 2606:4700:4700::1111 2606:4700:4700::1001; + + # Don't tell nginx version to the clients. Default is 'on'. + server_tokens off; + + # Specifies the maximum accepted body size of a client request, as + # indicated by the request header Content-Length. If the stated content + # length is greater than this size, then the client receives the HTTP + # error code 413. Set to 0 to disable. Default is '1m'. + client_max_body_size 2m; + + # Sendfile copies data between one FD and other from within the kernel, + # which is more efficient than read() + write(). Default is off. + sendfile on; + + # Causes nginx to attempt to send its HTTP response head in one packet, + # instead of using partial frames. Default is 'off'. + tcp_nopush on; + + + # Enables the specified protocols. Default is TLSv1 TLSv1.1 TLSv1.2. + # TIP: If you're not obligated to support ancient clients, remove TLSv1.1. + ssl_protocols TLSv1.2 TLSv1.3; + + # Path of the file with Diffie-Hellman parameters for EDH ciphers. + # TIP: Generate with: `openssl dhparam -out /etc/ssl/nginx/dh2048.pem 2048` + #ssl_dhparam /etc/ssl/nginx/dh2048.pem; + + # Specifies that our cipher suits should be preferred over client ciphers. + # Default is 'off'. + ssl_prefer_server_ciphers on; + + # Enables a shared SSL cache with size that can hold around 8000 sessions. + # Default is 'none'. + ssl_session_cache shared:SSL:2m; + + # Specifies a time during which a client may reuse the session parameters. + # Default is '5m'. + ssl_session_timeout 1h; + + # Disable TLS session tickets (they are insecure). Default is 'on'. + ssl_session_tickets off; + + + # Enable gzipping of responses. + #gzip on; + + # Set the Vary HTTP header as defined in the RFC 2616. Default is 'off'. + gzip_vary on; + + + # Helper variable for proxying websockets. + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + + # Specifies the main log format. + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + # Sets the path, format, and configuration for a buffered log write. + access_log /var/log/nginx/access.log main; + + + # Includes virtual hosts configs. + # include /etc/nginx/http.d/*.conf; + include /etc/nginx/conf.d/*.conf; +} diff --git a/nginx/html/index.html b/nginx/html/index.html new file mode 100644 index 0000000..992221e --- /dev/null +++ b/nginx/html/index.html @@ -0,0 +1,37 @@ + + + + + + Archive de Liens + + + + + + + + +
+ +
+ +
+ +
+ + + + \ No newline at end of file diff --git a/nginx/html/templates/home.html b/nginx/html/templates/home.html new file mode 100644 index 0000000..bcc7f36 --- /dev/null +++ b/nginx/html/templates/home.html @@ -0,0 +1,2 @@ +

Bienvenue sur l'Archive de Liens

+

Explorez notre collection de sources archivées sur la pandémie de COVID-19 et les vaccins.

\ No newline at end of file diff --git a/nginx/html/templates/search.html b/nginx/html/templates/search.html new file mode 100644 index 0000000..5a0904b --- /dev/null +++ b/nginx/html/templates/search.html @@ -0,0 +1,6 @@ +

Rechercher des sources

+
+ + +
+
\ No newline at end of file diff --git a/nginx/html/templates/sources.html b/nginx/html/templates/sources.html new file mode 100644 index 0000000..ef0057e --- /dev/null +++ b/nginx/html/templates/sources.html @@ -0,0 +1,4 @@ +

Sources archivées

+
    + +
\ No newline at end of file diff --git a/nginx/html/templates/suggest.html b/nginx/html/templates/suggest.html new file mode 100644 index 0000000..920ade9 --- /dev/null +++ b/nginx/html/templates/suggest.html @@ -0,0 +1,15 @@ +

Suggérer une source

+
+ + + + + +
\ No newline at end of file diff --git a/nginx/static/404.png b/nginx/static/404.png new file mode 100644 index 0000000..dbe6f45 Binary files /dev/null and b/nginx/static/404.png differ diff --git a/nginx/static/favicon.ico b/nginx/static/favicon.ico new file mode 100644 index 0000000..de9315b Binary files /dev/null and b/nginx/static/favicon.ico differ diff --git a/nginx/static/scripts/main.js b/nginx/static/scripts/main.js new file mode 100644 index 0000000..bab3224 --- /dev/null +++ b/nginx/static/scripts/main.js @@ -0,0 +1,59 @@ +import { initRouter } from './router.js'; +import { initNavigation } from './navigation.js'; + +document.addEventListener('DOMContentLoaded', () => { + initRouter(); + initNavigation(); + + document.getElementById('download-archive').addEventListener('click', (e) => { + e.preventDefault(); + console.log('Téléchargement de l\'archive'); + }); +}); + +async function loadTemplate(templateName) { + const response = await fetch(`/templates/${templateName}.html`); + return await response.text(); +} + +export async function displayHome() { + const content = document.getElementById('content'); + content.innerHTML = await loadTemplate('home'); +} + +export async function displaySources(sources) { + const content = document.getElementById('content'); + content.innerHTML = await loadTemplate('sources'); + + const sourcesList = document.getElementById('sourcesList'); + sourcesList.innerHTML = sources.map(source => ` +
  • +

    ${source.title}

    +

    ${source.description}

    +

    Catégorie: ${source.category}

    + Lien archivé +
  • + `).join(''); +} + +export async function displaySuggestionForm() { + const content = document.getElementById('content'); + content.innerHTML = await loadTemplate('suggest'); + + document.getElementById('suggestionForm').addEventListener('submit', (e) => { + e.preventDefault(); + // Logique pour traiter la soumission du formulaire + console.log('Formulaire de suggestion soumis'); + }); +} + +export async function displaySearchForm() { + const content = document.getElementById('content'); + content.innerHTML = await loadTemplate('search'); + + document.getElementById('searchForm').addEventListener('submit', (e) => { + e.preventDefault(); + // Logique pour traiter la recherche + console.log('Recherche soumise'); + }); +} \ No newline at end of file diff --git a/nginx/static/scripts/navigation.js b/nginx/static/scripts/navigation.js new file mode 100644 index 0000000..4098fc0 --- /dev/null +++ b/nginx/static/scripts/navigation.js @@ -0,0 +1,17 @@ +export function initNavigation() { + document.getElementById('home-button').addEventListener('click', () => { + window.location.hash = '#/'; + }); + + document.getElementById('sources-button').addEventListener('click', () => { + window.location.hash = '#/sources'; + }); + + document.getElementById('suggest-button').addEventListener('click', () => { + window.location.hash = '#/suggest'; + }); + + document.getElementById('search-button').addEventListener('click', () => { + window.location.hash = '#/search'; + }); +} \ No newline at end of file diff --git a/nginx/static/scripts/router.js b/nginx/static/scripts/router.js new file mode 100644 index 0000000..52980f4 --- /dev/null +++ b/nginx/static/scripts/router.js @@ -0,0 +1,26 @@ +import { displayHome, displaySources, displaySuggestionForm, displaySearchForm } from './main.js'; + +const routes = { + '/': displayHome, + '/sources': () => { + // Simulons des données pour l'exemple + const sources = [ + { title: 'Source 1', description: 'Description 1', category: 'Médical', archiveUrl: '#' }, + { title: 'Source 2', description: 'Description 2', category: 'Scientifique', archiveUrl: '#' }, + ]; + displaySources(sources); + }, + '/suggest': displaySuggestionForm, + '/search': displaySearchForm +}; + +export function initRouter() { + async function router() { + const path = window.location.hash.slice(1) || '/'; + const route = routes[path] || routes['/']; + await route(); + } + + window.addEventListener('hashchange', router); + window.addEventListener('load', router); +} \ No newline at end of file diff --git a/nginx/static/styles/styles.css b/nginx/static/styles/styles.css new file mode 100644 index 0000000..603aa47 --- /dev/null +++ b/nginx/static/styles/styles.css @@ -0,0 +1,100 @@ +body { + font-family: Arial, sans-serif; + line-height: 1.6; + margin: 0; + padding: 0; + background-color: #f4f4f4; +} + +header { + background-color: #333; + color: #fff; + padding: 1rem; +} + +nav { + display: flex; + justify-content: space-between; + align-items: center; +} + +#tabs-list button { + background: none; + border: none; + color: #fff; + cursor: pointer; + margin-right: 1rem; +} + +main { + padding: 2rem; +} + +footer { + background-color: #333; + color: #fff; + text-align: center; + padding: 1rem; + position: fixed; + bottom: 0; + width: 100%; +} + +.source-list { + list-style-type: none; + padding: 0; +} + +.source-item { + background-color: #fff; + margin-bottom: 1rem; + padding: 1rem; + border-radius: 5px; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); +} + +.source-item h3 { + margin-top: 0; +} + +.source-item .category { + font-style: italic; + color: #666; +} + +.suggestion-form { + max-width: 600px; + margin: 0 auto; +} + +.suggestion-form input, +.suggestion-form textarea { + width: 100%; + padding: 0.5rem; + margin-bottom: 1rem; +} + +.suggestion-form button { + background-color: #333; + color: #fff; + border: none; + padding: 0.5rem 1rem; + cursor: pointer; +} + +.search-form { + margin-bottom: 2rem; +} + +.search-form input { + width: 70%; + padding: 0.5rem; +} + +.search-form button { + background-color: #333; + color: #fff; + border: none; + padding: 0.5rem 1rem; + cursor: pointer; +} \ No newline at end of file diff --git a/stat/Dockerfile b/stat/Dockerfile new file mode 100644 index 0000000..ea0b36f --- /dev/null +++ b/stat/Dockerfile @@ -0,0 +1,41 @@ +FROM python:3.11-slim + +# declaration of service variables environment +ENV STAT_SERVICE_NAME=${STAT_SERVICE_NAME} +ENV DJANGO_SUPERUSER_USERNAME=${DJANGO_SUPERUSER_USERNAME} +ENV DJANGO_SUPERUSER_EMAIL=${DJANGO_SUPERUSER_EMAIL} +ENV DJANGO_SUPERUSER_PASSWORD=${DJANGO_SUPERUSER_PASSWORD} +ARG STAT_SERVICE_NAME +ARG DJANGO_SUPERUSER_USERNAME +ARG DJANGO_SUPERUSER_EMAIL +ARG DJANGO_SUPERUSER_PASSWORD + +COPY ./user_statistics /home/archive/${STAT_SERVICE_NAME} + +# declaration of environment variables for python +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +RUN mkdir -p /home/archive +WORKDIR /home/archive + +# installation of dependencies +COPY ./conf/requirements.txt . +RUN pip3 install --no-cache-dir -r requirements.txt \ + && mkdir depedencies && mv requirements.txt depedencies + +# installation of postgresql-client +RUN apt-get update && apt-get install -y postgresql-client + +RUN apt-get update && apt-get install -y netcat-openbsd + +RUN mkdir -p logs + +# copy and execute the initialization script +COPY ./tools/init.sh /home/archive/init.sh + +# set the final working directory +WORKDIR /home/archive/${STAT_SERVICE_NAME} + +# Command for running the application +CMD ["/bin/bash", "/home/archive/init.sh"] \ No newline at end of file diff --git a/stat/conf/requirements.txt b/stat/conf/requirements.txt new file mode 100644 index 0000000..d43248e --- /dev/null +++ b/stat/conf/requirements.txt @@ -0,0 +1,8 @@ +Django>=4.2,<4.3 +daphne==4.0.0 +djangorestframework==3.15.0 +psycopg2-binary>=2.9,<3.0 +django-environ +django-cors-headers +pillow +pyotp \ No newline at end of file diff --git a/stat/tools/init.sh b/stat/tools/init.sh new file mode 100644 index 0000000..7e56739 --- /dev/null +++ b/stat/tools/init.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +LOGFILE="/home/archive/logs/setup.log" +echo "initialisation of Django" >> $LOGFILE +echo "backend name: $STAT_SERVICE_NAME" >> $LOGFILE + +echo "initialisation of the project $STAT_SERVICE_NAME" >> $LOGFILE + +echo "initialisation of Django done" >> $LOGFILE + +echo "Waiting for postgres to get up and running..." +while ! nc -z db_archive 5434; do + echo "waiting for postgress to be listening..." + sleep 1 +done +#waiting for user to migrate +sleep 5 +echo "PostgreSQL started" +pip install -U 'Twisted[tls,http2]' +python3 manage.py makemigrations +python3 manage.py migrate +daphne -b 0.0.0.0 -p 8005 user_statistics.asgi:application \ No newline at end of file diff --git a/stat/user_statistics/manage.py b/stat/user_statistics/manage.py new file mode 100755 index 0000000..c84293c --- /dev/null +++ b/stat/user_statistics/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'user_statistics.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/stat/user_statistics/stat_management/__init__.py b/stat/user_statistics/stat_management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stat/user_statistics/stat_management/admin.py b/stat/user_statistics/stat_management/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/stat/user_statistics/stat_management/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/stat/user_statistics/stat_management/apps.py b/stat/user_statistics/stat_management/apps.py new file mode 100644 index 0000000..47bbf31 --- /dev/null +++ b/stat/user_statistics/stat_management/apps.py @@ -0,0 +1,10 @@ +from django.apps import AppConfig +from user_statistics.settings import STAT_APP_NAME + + +class StatManagementConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = f'{STAT_APP_NAME}' + + def ready(self): + import stat_management.signals diff --git a/stat/user_statistics/stat_management/migrations/__init__.py b/stat/user_statistics/stat_management/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stat/user_statistics/stat_management/models.py b/stat/user_statistics/stat_management/models.py new file mode 100644 index 0000000..4758f16 --- /dev/null +++ b/stat/user_statistics/stat_management/models.py @@ -0,0 +1,55 @@ +from django.db import models +from django.contrib.auth.models import User +from django.contrib.auth import get_user_model + +User = get_user_model() + +class GameHistory(models.Model): + player_1_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='player_1') + player_2_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='player_2') + player_1_score = models.IntegerField(default=0) + player_2_score = models.IntegerField(default=0) + winner_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='winner') + date_played = models.DateTimeField(auto_now_add=True) + duration = models.FloatField(default=0.0) + + class Meta: + db_table = 'users_game_history' + + def __str__(self): + return (f'{self.player_1_id} vs {self.player_2_id} ' + f'on {self.date_played} ' + f'winner is {self.winner_id} ' + f'score is {self.player_1_score} - {self.player_2_score}') + + @property + def player_1(self): + return {'username': self.player_1_id.username, 'score': self.player_1_score} + + @property + def player_2(self): + return {'username': self.player_2_id.username, 'score': self.player_2_score} + + @property + def winner(self): + return {'username': self.winner_id.username} + +class Stats(models.Model): + player_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='player') + wins = models.IntegerField(default=0) + losses = models.IntegerField(default=0) + win_rate = models.FloatField(default=0.0) + total_games_played = models.IntegerField(default=0) + total_hours_played = models.FloatField(default=0.0) + goal_scored = models.IntegerField(default=0) + goal_conceded = models.IntegerField(default=0) + + class Meta: + db_table = 'users_stats' + + def __str__(self): + return (f'{self.player_id} wins: {self.wins}, losses: {self.losses}, win rate: {self.win_rate:.2f}, ' + f'total games: {self.total_games_played}, total hours played: {self.total_hours_played:.2f}, ' + f'goal scored: {self.goal_scored}, goal conceded: {self.goal_conceded}') + + diff --git a/stat/user_statistics/stat_management/serializers.py b/stat/user_statistics/stat_management/serializers.py new file mode 100644 index 0000000..b29cd25 --- /dev/null +++ b/stat/user_statistics/stat_management/serializers.py @@ -0,0 +1,29 @@ +from rest_framework import serializers +from .models import GameHistory, Stats + +class GameHistorySerializer(serializers.ModelSerializer): + player_1 = serializers.SerializerMethodField() + player_2 = serializers.SerializerMethodField() + winner = serializers.SerializerMethodField() + + class Meta: + model = GameHistory + fields = [ + 'id', 'player_1_id', 'player_1_score', 'player_2_score', 'player_2_id', + 'winner_id', 'player_1', 'player_2', 'winner', 'date_played', 'duration' + ] + + def get_player_1(self, obj): + return obj.player_1 + + def get_player_2(self, obj): + return obj.player_2 + + def get_winner(self, obj): + return obj.winner + + +class StatsSerializer(serializers.ModelSerializer): + class Meta: + model = Stats + fields = '__all__' diff --git a/stat/user_statistics/stat_management/signals.py b/stat/user_statistics/stat_management/signals.py new file mode 100644 index 0000000..9ff2bca --- /dev/null +++ b/stat/user_statistics/stat_management/signals.py @@ -0,0 +1,42 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from .models import GameHistory, Stats + +@receiver(post_save, sender=GameHistory) +def update_player_stats(sender, instance, created, **kwargs): + if created: + # get players + player_1_stats, _ = Stats.objects.get_or_create(player_id=instance.player_1_id) + player_2_stats, _ = Stats.objects.get_or_create(player_id=instance.player_2_id) + + # update wins and losses + if instance.winner_id == instance.player_1_id: + player_1_stats.wins += 1 + player_2_stats.losses += 1 + else: + player_1_stats.losses += 1 + player_2_stats.wins += 1 + + # update games played + player_1_stats.total_games_played += 1 + player_2_stats.total_games_played += 1 + + # update win rate + player_1_stats.win_rate = player_1_stats.wins / player_1_stats.total_games_played * 100 + player_2_stats.win_rate = player_2_stats.wins / player_2_stats.total_games_played * 100 + + # update hours played + player_1_stats.total_hours_played += instance.duration / 3600 + player_2_stats.total_hours_played += instance.duration / 3600 + + # update goal scored + player_1_stats.goal_scored += instance.player_1_score + player_2_stats.goal_scored += instance.player_2_score + + # update goal conceded + player_1_stats.goal_conceded += instance.player_2_score + player_2_stats.goal_conceded += instance.player_1_score + + # save stats + player_1_stats.save() + player_2_stats.save() \ No newline at end of file diff --git a/stat/user_statistics/stat_management/tests.py b/stat/user_statistics/stat_management/tests.py new file mode 100644 index 0000000..37a4e8c --- /dev/null +++ b/stat/user_statistics/stat_management/tests.py @@ -0,0 +1,75 @@ +from django.contrib.auth.models import User +from django.contrib.auth import get_user_model +from django.urls import reverse +from .models import GameHistory +from .serializers import GameHistorySerializer +from rest_framework.test import APITestCase +from rest_framework import status + +User = get_user_model() + +# Create your tests here. +class GameHistoryModelTest(APITestCase): + + def setUp(self): + self.user_1 = User.objects.create_user(username='user1', password='password') + self.user_2 = User.objects.create_user(username='user2', password='password') + self.user_1.save() + + def test_game_history_model(self): + + # loging user1 + self.client.login(username='user1', password='password') + + # create a game history + gameHistory = { + 'player_1_id': self.user_1.id, + 'player_2_id': self.user_2.id, + 'player_1_score': 5, + 'player_2_score': 3, + 'winner_id': self.user_1.id, + 'duration': '60' + } + + url = reverse('game_history') + + print("data send to : ", url) + print("data send : ", gameHistory) + # send a GameHistory + response = self.client.post(url, gameHistory, format='json') + print(response.data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # get stats + url = reverse('stats') + print("data send to : ", url) + response = self.client.get(url) + print(response.data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # create a game history + gameHistory = { + 'player_1_id': self.user_1.id, + 'player_2_id': self.user_2.id, + 'player_1_score': 4, + 'player_2_score': 13, + 'winner_id': self.user_2.id, + 'duration': '10806' + } + + url = reverse('game_history') + + print("data send to : ", url) + print("data send : ", gameHistory) + # send a GameHistory + response = self.client.post(url, gameHistory, format='json') + print(response.data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # get stats + url = reverse('stats') + print("data send to : ", url) + response = self.client.get(url) + print(response.data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + \ No newline at end of file diff --git a/stat/user_statistics/stat_management/urls.py b/stat/user_statistics/stat_management/urls.py new file mode 100644 index 0000000..1b462fd --- /dev/null +++ b/stat/user_statistics/stat_management/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from .views import GameHistoryView, AddGameHistoryView, StatsView + +urlpatterns = [ + path('game-history//', GameHistoryView.as_view(), name='game_history'), + path('game-history/', AddGameHistoryView.as_view(), name='add_game_history'), + path('stats//', StatsView.as_view(), name='stats'), +] \ No newline at end of file diff --git a/stat/user_statistics/stat_management/views.py b/stat/user_statistics/stat_management/views.py new file mode 100644 index 0000000..d800cd1 --- /dev/null +++ b/stat/user_statistics/stat_management/views.py @@ -0,0 +1,52 @@ +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from .models import GameHistory, Stats +from .serializers import GameHistorySerializer, StatsSerializer +from rest_framework.response import Response +from rest_framework import status +from django.db.models import Q +from django.shortcuts import get_object_or_404 + +class AddGameHistoryView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + serializer = GameHistorySerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + print("serilizer post: ",serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class GameHistoryView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, username): + try: + user = username + games = GameHistory.objects.filter(Q(player_1_id=user) | Q(player_2_id=user)).order_by('-date_played')[:10] + serializer = GameHistorySerializer(games, many=True) + print("serilizer get: ",serializer.data) + return Response(serializer.data, status=status.HTTP_200_OK) + except Exception as e: + return Response( + {'message': f"{type(e).__name__}: {str(e)}"}, + status=status.HTTP_404_NOT_FOUND + ) + +class StatsView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, username): + try: + user = username + stats = Stats.objects.filter(player_id=user).first() + if stats is None: + return Response([], status=status.HTTP_200_OK) + serializer = StatsSerializer(stats) + return Response(serializer.data, status=status.HTTP_200_OK) + except Exception as e: + return Response( + {'message': f"{type(e).__name__}: {str(e)}"}, + status=status.HTTP_404_NOT_FOUND + ) \ No newline at end of file diff --git a/stat/user_statistics/user_statistics/__init__.py b/stat/user_statistics/user_statistics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stat/user_statistics/user_statistics/asgi.py b/stat/user_statistics/user_statistics/asgi.py new file mode 100644 index 0000000..f22a386 --- /dev/null +++ b/stat/user_statistics/user_statistics/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for user_statistics project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'user_statistics.settings') + +application = get_asgi_application() diff --git a/stat/user_statistics/user_statistics/settings.py b/stat/user_statistics/user_statistics/settings.py new file mode 100644 index 0000000..3cfa5a6 --- /dev/null +++ b/stat/user_statistics/user_statistics/settings.py @@ -0,0 +1,175 @@ +""" +Django settings for user_statistics project. + +Generated by 'django-admin startproject' using Django 4.2.11. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +import sys +from pathlib import Path +import environ + +sys.path.append('/home/archive/user_auth_system') + +# Read environment variables from .env file +# And define CONSTANTS +env = environ.Env() +environ.Env.read_env(env_file='.env.django') + +USER_SERVICE_NAME = env('USER_SERVICE_NAME') +STAT_SERVICE_NAME = env('STAT_SERVICE_NAME') +AUTH_APP_NAME = env('USER_APP_NAME') +STAT_APP_NAME = env('STAT_APP_NAME') +DB_NAME = env('DB_ARCHIVE_NAME') +DB_USER = env('DB_ARCHIVE_USER') +DB_PASSWORD = env('DB_ARCHIVE_PASSWORD') +DB_HOST = env('DB_ARCHIVE_HOST') +DB_PORT = env('DB_ARCHIVE_PORT') +IP_ADDRESS= env('IP_ADDRESS') +SECRET_KEY_ENV = env('SECRET_KEY_USER') + + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = SECRET_KEY_ENV + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = False + +ALLOWED_HOSTS = ['*'] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'corsheaders', + f'{AUTH_APP_NAME}', + f'{STAT_APP_NAME}', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = f'{STAT_SERVICE_NAME}.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = f'{STAT_SERVICE_NAME}.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': DB_NAME, + 'USER': DB_USER, + 'PASSWORD': DB_PASSWORD, + 'HOST': DB_HOST, + 'PORT': DB_PORT, + } +} + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Definition of default user + +AUTH_USER_MODEL = f'{AUTH_APP_NAME}.CustomUser' + +# CORS settings + +SESSION_ENGINE = 'django.contrib.sessions.backends.db' +SESSION_COOKIE_NAME = 'sessionid' +SESSION_COOKIE_HTTPONLY = True +SESSION_COOKIE_SAMESITE = 'Lax' +SESSION_COOKIE_SECURE = True + +CSRF_COOKIE_NAME = 'csrftoken' +CSRF_COOKIE_HTTPONLY = True +CSRF_COOKIE_SAMESITE = 'Lax' +CSRF_COOKIE_SECURE = True + +CORS_ALLOW_CREDENTIALS = True +CORS_ALLOW_ALL_ORIGINS = True + +CSRF_TRUSTED_ORIGINS = ['https://localhost', 'https://' + IP_ADDRESS] diff --git a/stat/user_statistics/user_statistics/urls.py b/stat/user_statistics/user_statistics/urls.py new file mode 100644 index 0000000..0ac4769 --- /dev/null +++ b/stat/user_statistics/user_statistics/urls.py @@ -0,0 +1,24 @@ +""" +URL configuration for user_statistics project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include +from user_statistics.settings import STAT_APP_NAME + +urlpatterns = [ + path('admin/', admin.site.urls), + path('stat/', include(f'{STAT_APP_NAME}.urls')) +] diff --git a/stat/user_statistics/user_statistics/wsgi.py b/stat/user_statistics/user_statistics/wsgi.py new file mode 100644 index 0000000..1ef684a --- /dev/null +++ b/stat/user_statistics/user_statistics/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for user_statistics project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'user_statistics.settings') + +application = get_wsgi_application() diff --git a/user/Dockerfile b/user/Dockerfile new file mode 100644 index 0000000..1da8a01 --- /dev/null +++ b/user/Dockerfile @@ -0,0 +1,41 @@ +FROM python:3.11-slim + +# declaration of service variables environment +ENV USER_SERVICE_NAME=${USER_SERVICE_NAME} +ENV DJANGO_SUPERUSER_USERNAME=${DJANGO_SUPERUSER_USERNAME} +ENV DJANGO_SUPERUSER_EMAIL=${DJANGO_SUPERUSER_EMAIL} +ENV DJANGO_SUPERUSER_PASSWORD=${DJANGO_SUPERUSER_PASSWORD} +ARG USER_SERVICE_NAME +ARG DJANGO_SUPERUSER_USERNAME +ARG DJANGO_SUPERUSER_EMAIL +ARG DJANGO_SUPERUSER_PASSWORD + +COPY ./user_auth_system /home/archive/${USER_SERVICE_NAME} + +# declaration of environment variables for python +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +RUN apt-get clean \ + && apt-get update \ + && apt-get install -y netcat-openbsd \ + && mkdir -p /home/archive + +WORKDIR /home/archive + +# installation of dependencies +COPY ./conf/requirements.txt . +RUN pip3 install --no-cache-dir -r requirements.txt \ + && mkdir depedencies && mv requirements.txt depedencies + +RUN mkdir -p logs + +# copy and execute the initialization script +COPY ./tools/init.sh /home/archive/init.sh +RUN chmod +x /home/archive/init.sh + +# set the final working directory +WORKDIR /home/archive/${USER_SERVICE_NAME} + +# Command for running the application +CMD ["/bin/bash", "/home/archive/init.sh"] diff --git a/user/conf/requirements.txt b/user/conf/requirements.txt new file mode 100644 index 0000000..b0c3c96 --- /dev/null +++ b/user/conf/requirements.txt @@ -0,0 +1,9 @@ +Django>=4.2,<4.3 +djangorestframework==3.14.0 +psycopg2-binary>=2.9,<3.0 +django-environ==0.10.0 +django-cors-headers==4.0.0 +pillow==9.5.0 +djangorestframework-simplejwt==5.2.2 +pyotp==2.8.0 +qrcode==7.4.2 \ No newline at end of file diff --git a/user/tools/init.sh b/user/tools/init.sh new file mode 100644 index 0000000..e1c65fd --- /dev/null +++ b/user/tools/init.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +LOGFILE="/home/archive/logs/setup.log" +echo "initialisation of Django" >> $LOGFILE +echo "backend name: $USER_SERVICE_NAME" >> $LOGFILE + +echo "initialisation of the project $USER_SERVICE_NAME" >> $LOGFILE + +echo "initialisation of Django done" >> $LOGFILE +cd /home/archive/$USER_SERVICE_NAME + +echo "Waiting for postgres to get up and running..." +while ! nc -z db_archive 5434; do + echo "waiting for postgress to be listening..." + sleep 1 +done +echo "PostgreSQL started" +pip install -U 'Twisted[tls,http2]' +python3 manage.py makemigrations +python3 manage.py migrate +daphne -b 0.0.0.0 -p 8003 user_auth_system.asgi:application \ No newline at end of file diff --git a/user/user_auth_system/manage.py b/user/user_auth_system/manage.py new file mode 100755 index 0000000..4ff2b2a --- /dev/null +++ b/user/user_auth_system/manage.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys +from user_auth_system.settings import USER_SERVICE_NAME + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', f'{USER_SERVICE_NAME}.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/user/user_auth_system/user_auth_system/__init__.py b/user/user_auth_system/user_auth_system/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/user/user_auth_system/user_auth_system/settings.py b/user/user_auth_system/user_auth_system/settings.py new file mode 100644 index 0000000..26cc097 --- /dev/null +++ b/user/user_auth_system/user_auth_system/settings.py @@ -0,0 +1,130 @@ +from pathlib import Path +import os +import environ + +env = environ.Env() + +ARCHIVE_APP_NAME = env('ARCHIVE_APP_NAME') +DB_NAME = env('DB_ARCHIVE_NAME') +DB_USER = env('DB_ARCHIVE_USER') +DB_PASSWORD = env('DB_ARCHIVE_PASSWORD') +DB_HOST = env('DB_ARCHIVE_HOST') +DB_PORT = env.int('DB_ARCHIVE_PORT') +SECRET_KEY = env('SECRET_KEY_ARCHIVE') + +BASE_DIR = Path(__file__).resolve().parent.parent + +DEBUG = False + +ALLOWED_HOSTS = ['*'] + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'daphne', + 'rest_framework', + 'corsheaders', + f'{ARCHIVE_APP_NAME}.apps.ArchiveAppConfig', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = f'{ARCHIVE_APP_NAME}.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +DATABASES = { + 'default': { + "ENGINE": "django.db.backends.postgresql", + "NAME": DB_NAME, + "USER": DB_USER, + "PASSWORD": DB_PASSWORD, + "HOST": DB_HOST, + "PORT": DB_PORT, + } +} + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + 'OPTIONS': { + 'min_length': 8, + } + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, + { + 'NAME': 'your_app.validators.SpecialCharacterValidator', + 'OPTIONS': { + 'special_chars': '@$!%*?&', + } + }, + { + 'NAME': 'your_app.validators.UppercaseValidator', + }, + { + 'NAME': 'your_app.validators.LengthValidator', + 'OPTIONS': { + 'min_length': 8, + } + }, +] + +LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'UTC' +USE_I18N = True +USE_TZ = True + +STATIC_URL = 'static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +AUTH_USER_MODEL = f'{ARCHIVE_APP_NAME}.CustomUser' + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ], +} + +CORS_ALLOW_ALL_ORIGINS = True + +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True \ No newline at end of file diff --git a/user/user_auth_system/user_auth_system/urls.py b/user/user_auth_system/user_auth_system/urls.py new file mode 100644 index 0000000..d9a7f4e --- /dev/null +++ b/user/user_auth_system/user_auth_system/urls.py @@ -0,0 +1,30 @@ +""" +URL configuration for user_auth_system project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static +from user_auth_system.settings import AUTH_APP_NAME + + +urlpatterns = [ + path('admin/', admin.site.urls), + path('auth/', include(f'{AUTH_APP_NAME}.urls')), +] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/user/user_auth_system/user_management/__init__.py b/user/user_auth_system/user_management/__init__.py new file mode 100644 index 0000000..56d9ab7 --- /dev/null +++ b/user/user_auth_system/user_management/__init__.py @@ -0,0 +1,3 @@ +from user_auth_system.settings import AUTH_APP_NAME + +default_app_config = f'{AUTH_APP_NAME}.apps.AuthUserConfig' \ No newline at end of file diff --git a/user/user_auth_system/user_management/admin.py b/user/user_auth_system/user_management/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/user/user_auth_system/user_management/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/user/user_auth_system/user_management/apps.py b/user/user_auth_system/user_management/apps.py new file mode 100644 index 0000000..4648ea1 --- /dev/null +++ b/user/user_auth_system/user_management/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig +from user_auth_system.settings import AUTH_APP_NAME + +class AuthUserConfig(AppConfig): + name = AUTH_APP_NAME + verbose_name = 'Authentication and Authorization' diff --git a/user/user_auth_system/user_management/migrations/__init__.py b/user/user_auth_system/user_management/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/user/user_auth_system/user_management/models/__init__.py b/user/user_auth_system/user_management/models/__init__.py new file mode 100644 index 0000000..2ace7c7 --- /dev/null +++ b/user/user_auth_system/user_management/models/__init__.py @@ -0,0 +1,4 @@ +from .user import CustomUser +from .source import Source +from .tag import Tag +from .suggestion import Suggestion \ No newline at end of file diff --git a/user/user_auth_system/user_management/models/source.py b/user/user_auth_system/user_management/models/source.py new file mode 100644 index 0000000..2ff6c93 --- /dev/null +++ b/user/user_auth_system/user_management/models/source.py @@ -0,0 +1,17 @@ +from django.db import models +from .user import CustomUser +from .tag import Tag + +class Source(models.Model): + title = models.CharField(max_length=200) + url = models.URLField() + archived_url = models.URLField() + description = models.TextField() + category = models.CharField(max_length=50) + tags = models.ManyToManyField(Tag, related_name='sources') + added_by = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.title \ No newline at end of file diff --git a/user/user_auth_system/user_management/models/suggestion.py b/user/user_auth_system/user_management/models/suggestion.py new file mode 100644 index 0000000..6bdda20 --- /dev/null +++ b/user/user_auth_system/user_management/models/suggestion.py @@ -0,0 +1,12 @@ +from django.db import models +from .user import CustomUser + +class Suggestion(models.Model): + url = models.URLField() + description = models.TextField() + suggested_by = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, null=True) + created_at = models.DateTimeField(auto_now_add=True) + is_approved = models.BooleanField(default=False) + + def __str__(self): + return self.url \ No newline at end of file diff --git a/user/user_auth_system/user_management/models/tag.py b/user/user_auth_system/user_management/models/tag.py new file mode 100644 index 0000000..d195ff5 --- /dev/null +++ b/user/user_auth_system/user_management/models/tag.py @@ -0,0 +1,7 @@ +from django.db import models + +class Tag(models.Model): + name = models.CharField(max_length=50, unique=True) + + def __str__(self): + return self.name \ No newline at end of file diff --git a/user/user_auth_system/user_management/models/user.py b/user/user_auth_system/user_management/models/user.py new file mode 100644 index 0000000..0a89865 --- /dev/null +++ b/user/user_auth_system/user_management/models/user.py @@ -0,0 +1,11 @@ +from django.contrib.auth.models import AbstractUser +from django.db import models + +class CustomUser(AbstractUser): + profile_picture = models.ImageField(upload_to='profile_pics/', blank=True, null=True) + language = models.CharField(max_length=2, default='en') + is_2fa_enabled = models.BooleanField(default=False) + otp_secret = models.CharField(max_length=32, blank=True) + + def __str__(self): + return self.username \ No newline at end of file diff --git a/user/user_auth_system/user_management/serializers/__init__.py b/user/user_auth_system/user_management/serializers/__init__.py new file mode 100644 index 0000000..fa32f0d --- /dev/null +++ b/user/user_auth_system/user_management/serializers/__init__.py @@ -0,0 +1,4 @@ +from .user import UserSerializer +from .source import SourceSerializer +from .tag import TagSerializer +from .suggestion import SuggestionSerializer \ No newline at end of file diff --git a/user/user_auth_system/user_management/serializers/source.py b/user/user_auth_system/user_management/serializers/source.py new file mode 100644 index 0000000..2e56c5e --- /dev/null +++ b/user/user_auth_system/user_management/serializers/source.py @@ -0,0 +1,10 @@ +from rest_framework import serializers +from ..models import Source + +class SourceSerializer(serializers.ModelSerializer): + tags = serializers.StringRelatedField(many=True) + added_by = serializers.StringRelatedField() + + class Meta: + model = Source + fields = ['id', 'title', 'url', 'archived_url', 'description', 'category', 'tags', 'added_by', 'created_at', 'updated_at'] \ No newline at end of file diff --git a/user/user_auth_system/user_management/serializers/suggestion.py b/user/user_auth_system/user_management/serializers/suggestion.py new file mode 100644 index 0000000..6674360 --- /dev/null +++ b/user/user_auth_system/user_management/serializers/suggestion.py @@ -0,0 +1,10 @@ +from rest_framework import serializers +from ..models import Suggestion + +class SuggestionSerializer(serializers.ModelSerializer): + suggested_by = serializers.StringRelatedField() + + class Meta: + model = Suggestion + fields = ['id', 'url', 'description', 'suggested_by', 'created_at', 'is_approved'] + read_only_fields = ['suggested_by', 'is_approved'] \ No newline at end of file diff --git a/user/user_auth_system/user_management/serializers/tag.py b/user/user_auth_system/user_management/serializers/tag.py new file mode 100644 index 0000000..ea191bd --- /dev/null +++ b/user/user_auth_system/user_management/serializers/tag.py @@ -0,0 +1,7 @@ +from rest_framework import serializers +from ..models import Tag + +class TagSerializer(serializers.ModelSerializer): + class Meta: + model = Tag + fields = ['id', 'name'] \ No newline at end of file diff --git a/user/user_auth_system/user_management/serializers/user.py b/user/user_auth_system/user_management/serializers/user.py new file mode 100644 index 0000000..ee1ccd2 --- /dev/null +++ b/user/user_auth_system/user_management/serializers/user.py @@ -0,0 +1,12 @@ +from rest_framework import serializers +from ..models import CustomUser + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = CustomUser + fields = ['id', 'username', 'email', 'profile_picture', 'language', 'is_2fa_enabled'] + extra_kwargs = {'password': {'write_only': True}} + + def create(self, validated_data): + user = CustomUser.objects.create_user(**validated_data) + return user \ No newline at end of file diff --git a/user/user_auth_system/user_management/tests/__init__.py b/user/user_auth_system/user_management/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/user/user_auth_system/user_management/tests/test_search.py b/user/user_auth_system/user_management/tests/test_search.py new file mode 100644 index 0000000..9dcc713 --- /dev/null +++ b/user/user_auth_system/user_management/tests/test_search.py @@ -0,0 +1,27 @@ +from django.urls import reverse +from django.contrib.auth import get_user_model +from rest_framework import status +from rest_framework.test import APITestCase +from your_app.models import Source, Tag + +User = get_user_model() + +class SearchTests(APITestCase): + def setUp(self): + self.user = User.objects.create_user(username='testuser', password='testpassword123') + self.client.force_authenticate(user=self.user) + self.tag = Tag.objects.create(name='covid') + self.source = Source.objects.create( + title='COVID-19 Research', + url='https://example.com/covid', + description='Latest research on COVID-19', + added_by=self.user + ) + self.source.tags.add(self.tag) + + def test_search_source(self): + url = reverse('search') + response = self.client.get(url, {'q': 'COVID'}) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['title'], 'COVID-19 Research') \ No newline at end of file diff --git a/user/user_auth_system/user_management/tests/test_sources.py b/user/user_auth_system/user_management/tests/test_sources.py new file mode 100644 index 0000000..4ea86f3 --- /dev/null +++ b/user/user_auth_system/user_management/tests/test_sources.py @@ -0,0 +1,35 @@ +from django.urls import reverse +from django.contrib.auth import get_user_model +from rest_framework import status +from rest_framework.test import APITestCase +from your_app.models import Source, Tag + +User = get_user_model() + +class SourceTests(APITestCase): + def setUp(self): + self.user = User.objects.create_user(username='testuser', password='testpassword123') + self.client.force_authenticate(user=self.user) + self.tag = Tag.objects.create(name='test_tag') + + def test_create_source(self): + url = reverse('source-list-create') + data = { + 'title': 'Test Source', + 'url': 'https://example.com', + 'archived_url': 'https://archive.is/example.com', + 'description': 'This is a test source', + 'category': 'test', + 'tags': [self.tag.id] + } + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Source.objects.count(), 1) + self.assertEqual(Source.objects.get().title, 'Test Source') + + def test_list_sources(self): + Source.objects.create(title='Test Source', url='https://example.com', added_by=self.user) + url = reverse('source-list-create') + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) \ No newline at end of file diff --git a/user/user_auth_system/user_management/tests/test_suggestions.py b/user/user_auth_system/user_management/tests/test_suggestions.py new file mode 100644 index 0000000..359b549 --- /dev/null +++ b/user/user_auth_system/user_management/tests/test_suggestions.py @@ -0,0 +1,30 @@ +from django.urls import reverse +from django.contrib.auth import get_user_model +from rest_framework import status +from rest_framework.test import APITestCase +from your_app.models import Suggestion + +User = get_user_model() + +class SuggestionTests(APITestCase): + def setUp(self): + self.user = User.objects.create_user(username='testuser', password='testpassword123') + self.client.force_authenticate(user=self.user) + + def test_create_suggestion(self): + url = reverse('suggestion-list-create') + data = { + 'url': 'https://example.com', + 'description': 'This is a test suggestion' + } + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Suggestion.objects.count(), 1) + self.assertEqual(Suggestion.objects.get().url, 'https://example.com') + + def test_list_suggestions(self): + Suggestion.objects.create(url='https://example.com', suggested_by=self.user) + url = reverse('suggestion-list-create') + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) \ No newline at end of file diff --git a/user/user_auth_system/user_management/tests/test_user_auth.py b/user/user_auth_system/user_management/tests/test_user_auth.py new file mode 100644 index 0000000..a5b05af --- /dev/null +++ b/user/user_auth_system/user_management/tests/test_user_auth.py @@ -0,0 +1,41 @@ +from django.urls import reverse +from django.contrib.auth import get_user_model +from rest_framework import status +from rest_framework.test import APITestCase +from rest_framework_simplejwt.tokens import RefreshToken + +User = get_user_model() + +class UserAuthTests(APITestCase): + def setUp(self): + self.user = User.objects.create_user(username='testuser', password='testpassword123', email='test@example.com') + self.user.save() + + def test_register_user(self): + url = reverse('user-register') + data = {'username': 'newuser', 'password': 'newpassword123', 'email': 'newuser@example.com'} + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_login_user(self): + url = reverse('user-login') + data = {'username': 'testuser', 'password': 'testpassword123'} + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('access', response.data) + self.assertIn('refresh', response.data) + + def test_logout_user(self): + refresh = RefreshToken.for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + url = reverse('user-logout') + data = {'refresh': str(refresh)} + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_user_profile(self): + self.client.force_authenticate(user=self.user) + url = reverse('user-profile') + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['username'], 'testuser') \ No newline at end of file diff --git a/user/user_auth_system/user_management/urls.py b/user/user_auth_system/user_management/urls.py new file mode 100644 index 0000000..529c135 --- /dev/null +++ b/user/user_auth_system/user_management/urls.py @@ -0,0 +1,26 @@ +from django.urls import path +from .views import ( + SourceListCreateView, SourceRetrieveUpdateDestroyView, + TagListCreateView, SuggestionListCreateView, SuggestionApproveView, + UserRegistrationView, UserLoginView, UserLogoutView, + Enable2FAView, Verify2FAView, UserProfileView +) +from rest_framework_simplejwt.views import TokenRefreshView + +urlpatterns = [ + path('sources/', SourceListCreateView.as_view(), name='source-list-create'), + path('sources//', SourceRetrieveUpdateDestroyView.as_view(), name='source-detail'), + path('tags/', TagListCreateView.as_view(), name='tag-list-create'), + path('suggestions/', SuggestionListCreateView.as_view(), name='suggestion-list-create'), + path('suggestions//approve/', SuggestionApproveView.as_view(), name='suggestion-approve'), + + path('users/register/', UserRegistrationView.as_view(), name='user-register'), + path('users/login/', UserLoginView.as_view(), name='user-login'), + path('users/logout/', UserLogoutView.as_view(), name='user-logout'), + path('users/profile/', UserProfileView.as_view(), name='user-profile'), + + path('users/enable-2fa/', Enable2FAView.as_view(), name='enable-2fa'), + path('users/verify-2fa/', Verify2FAView.as_view(), name='verify-2fa'), + + path('token/refresh/', TokenRefreshView.as_view(), name='token-refresh'), +] \ No newline at end of file diff --git a/user/user_auth_system/user_management/validators.py b/user/user_auth_system/user_management/validators.py new file mode 100644 index 0000000..5a6f977 --- /dev/null +++ b/user/user_auth_system/user_management/validators.py @@ -0,0 +1,21 @@ +from django.core.exceptions import ValidationError + +class SpecialCharacterValidator: + def validate(self, password, user=None): + if not any(char in '@$!%*?&' for char in password): + raise ValidationError( + "The password must contain at least one special character (@$!%*?&)." + ) + + def get_help_text(self): + return "Your password must contain at least one special character (@$!%*?&)." + +class UppercaseValidator: + def validate(self, password, user=None): + if not any(char.isupper() for char in password): + raise ValidationError( + "The password must contain at least one uppercase letter." + ) + + def get_help_text(self): + return "Your password must contain at least one uppercase letter." \ No newline at end of file diff --git a/user/user_auth_system/user_management/validators/__init__.py b/user/user_auth_system/user_management/validators/__init__.py new file mode 100644 index 0000000..d01d7cf --- /dev/null +++ b/user/user_auth_system/user_management/validators/__init__.py @@ -0,0 +1,3 @@ +from .password_validators import SpecialCharacterValidator, UppercaseValidator, LengthValidator + +__all__ = ['SpecialCharacterValidator', 'UppercaseValidator', 'LengthValidator'] \ No newline at end of file diff --git a/user/user_auth_system/user_management/validators/password_validators.py b/user/user_auth_system/user_management/validators/password_validators.py new file mode 100644 index 0000000..81b4c41 --- /dev/null +++ b/user/user_auth_system/user_management/validators/password_validators.py @@ -0,0 +1,43 @@ +from django.core.exceptions import ValidationError +from django.utils.translation import gettext as _ + +class SpecialCharacterValidator: + def __init__(self, special_chars='@$!%*?&'): + self.special_chars = special_chars + + def validate(self, password, user=None): + if not any(char in self.special_chars for char in password): + raise ValidationError( + _("Le mot de passe doit contenir au moins un caractère spécial (%(special_chars)s)."), + code='password_no_symbol', + params={'special_chars': self.special_chars}, + ) + + def get_help_text(self): + return _(f"Votre mot de passe doit contenir au moins un caractère spécial ({self.special_chars}).") + +class UppercaseValidator: + def validate(self, password, user=None): + if not any(char.isupper() for char in password): + raise ValidationError( + _("Le mot de passe doit contenir au moins une lettre majuscule."), + code='password_no_upper', + ) + + def get_help_text(self): + return _("Votre mot de passe doit contenir au moins une lettre majuscule.") + +class LengthValidator: + def __init__(self, min_length=8): + self.min_length = min_length + + def validate(self, password, user=None): + if len(password) < self.min_length: + raise ValidationError( + _("Le mot de passe doit contenir au moins %(min_length)d caractères."), + code='password_too_short', + params={'min_length': self.min_length}, + ) + + def get_help_text(self): + return _(f"Votre mot de passe doit contenir au moins {self.min_length} caractères.") \ No newline at end of file diff --git a/user/user_auth_system/user_management/views/__init__.py b/user/user_auth_system/user_management/views/__init__.py new file mode 100644 index 0000000..b9e8416 --- /dev/null +++ b/user/user_auth_system/user_management/views/__init__.py @@ -0,0 +1,5 @@ +from .user import UserRegistrationView, UserLoginView, UserLogoutView, UserProfileView, Enable2FAView, Verify2FAView +from .source import SourceListCreateView, SourceRetrieveUpdateDestroyView +from .tag import TagListCreateView +from .suggestion import SuggestionListCreateView, SuggestionApproveView +from .search import SearchView \ No newline at end of file diff --git a/user/user_auth_system/user_management/views/search.py b/user/user_auth_system/user_management/views/search.py new file mode 100644 index 0000000..9c138b6 --- /dev/null +++ b/user/user_auth_system/user_management/views/search.py @@ -0,0 +1,13 @@ +from rest_framework import generics, permissions +from ..models import Source +from ..serializers import SourceSerializer + +class SearchView(generics.ListAPIView): + serializer_class = SourceSerializer + permission_classes = [permissions.AllowAny] + + def get_queryset(self): + query = self.request.query_params.get('q', '') + return Source.objects.filter(title__icontains=query) | \ + Source.objects.filter(description__icontains=query) | \ + Source.objects.filter(tags__name__icontains=query).distinct() \ No newline at end of file diff --git a/user/user_auth_system/user_management/views/source.py b/user/user_auth_system/user_management/views/source.py new file mode 100644 index 0000000..b711e1d --- /dev/null +++ b/user/user_auth_system/user_management/views/source.py @@ -0,0 +1,16 @@ +from rest_framework import generics, permissions +from ..models import Source +from ..serializers import SourceSerializer + +class SourceListCreateView(generics.ListCreateAPIView): + queryset = Source.objects.all() + serializer_class = SourceSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + + def perform_create(self, serializer): + serializer.save(added_by=self.request.user) + +class SourceRetrieveUpdateDestroyView(generics.RetrieveUpdateDestroyAPIView): + queryset = Source.objects.all() + serializer_class = SourceSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly] \ No newline at end of file diff --git a/user/user_auth_system/user_management/views/suggestion.py b/user/user_auth_system/user_management/views/suggestion.py new file mode 100644 index 0000000..ad03f0c --- /dev/null +++ b/user/user_auth_system/user_management/views/suggestion.py @@ -0,0 +1,23 @@ +from rest_framework import generics, permissions, status +from rest_framework.response import Response +from ..models import Suggestion +from ..serializers import SuggestionSerializer + +class SuggestionListCreateView(generics.ListCreateAPIView): + queryset = Suggestion.objects.all() + serializer_class = SuggestionSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + + def perform_create(self, serializer): + serializer.save(suggested_by=self.request.user) + +class SuggestionApproveView(generics.UpdateAPIView): + queryset = Suggestion.objects.all() + serializer_class = SuggestionSerializer + permission_classes = [permissions.IsAdminUser] + + def update(self, request, *args, **kwargs): + instance = self.get_object() + instance.is_approved = True + instance.save() + return Response({"message": "Suggestion approved"}) \ No newline at end of file diff --git a/user/user_auth_system/user_management/views/tag.py b/user/user_auth_system/user_management/views/tag.py new file mode 100644 index 0000000..67acdb4 --- /dev/null +++ b/user/user_auth_system/user_management/views/tag.py @@ -0,0 +1,8 @@ +from rest_framework import generics, permissions +from ..models import Tag +from ..serializers import TagSerializer + +class TagListCreateView(generics.ListCreateAPIView): + queryset = Tag.objects.all() + serializer_class = TagSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly] \ No newline at end of file diff --git a/user/user_auth_system/user_management/views/user.py b/user/user_auth_system/user_management/views/user.py new file mode 100644 index 0000000..d71a1ce --- /dev/null +++ b/user/user_auth_system/user_management/views/user.py @@ -0,0 +1,100 @@ +from rest_framework import generics, permissions, status +from rest_framework.response import Response +from rest_framework.views import APIView +from ..models import CustomUser +from ..serializers import UserSerializer +from django.contrib.auth import authenticate +from rest_framework_simplejwt.tokens import RefreshToken +import pyotp +import qrcode +import base64 +from io import BytesIO + +class UserRegistrationView(generics.CreateAPIView): + queryset = CustomUser.objects.all() + serializer_class = UserSerializer + permission_classes = [permissions.AllowAny] + +class UserLoginView(APIView): + permission_classes = [permissions.AllowAny] + + def post(self, request): + username = request.data.get('username') + password = request.data.get('password') + user = authenticate(username=username, password=password) + + if user: + if user.is_2fa_enabled: + return Response({"message": "2FA is enabled. Please provide OTP.", "require_2fa": True, "user_id": user.id}) + refresh = RefreshToken.for_user(user) + return Response({ + 'refresh': str(refresh), + 'access': str(refresh.access_token), + 'user': UserSerializer(user).data + }) + return Response({"error": "Invalid Credentials"}, status=status.HTTP_400_BAD_REQUEST) + +class UserLogoutView(APIView): + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + refresh_token = request.data["refresh_token"] + token = RefreshToken(refresh_token) + token.blacklist() + return Response({"message": "Successfully Logged out."}, status=status.HTTP_200_OK) + except Exception as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) + +class UserProfileView(generics.RetrieveUpdateAPIView): + serializer_class = UserSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_object(self): + return self.request.user + +class Enable2FAView(APIView): + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + user = request.user + secret_key = pyotp.random_base32() + user.otp_secret = secret_key + user.save() + + totp = pyotp.TOTP(secret_key) + uri = totp.provisioning_uri(name=user.email, issuer_name="ArchiveApp") + + qr = qrcode.QRCode(version=1, box_size=10, border=5) + qr.add_data(uri) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") + + buffered = BytesIO() + img.save(buffered, format="PNG") + img_str = base64.b64encode(buffered.getvalue()).decode() + + return Response({ + 'secret_key': secret_key, + 'qr_code': f"data:image/png;base64,{img_str}" + }) + +class Verify2FAView(APIView): + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + user = request.user + otp = request.data.get('otp') + totp = pyotp.TOTP(user.otp_secret) + + if totp.verify(otp): + user.is_2fa_enabled = True + user.save() + refresh = RefreshToken.for_user(user) + return Response({ + "message": "2FA verified successfully", + 'refresh': str(refresh), + 'access': str(refresh.access_token), + 'user': UserSerializer(user).data + }) + return Response({"error": "Invalid OTP"}, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file