From e7eae26f2f2d216bf97bf1603c72ff4ae867a653 Mon Sep 17 00:00:00 2001 From: Nima Dekhli Date: Tue, 9 Apr 2024 18:33:28 +0200 Subject: [PATCH] 42 show username on post with more empasis (#56) * remove user mail and requirement for post title * show logged in user in navbar --- .../migrations/0012_remove_user_email.py | 17 +++++ api/neuronaApp/models/user_models.py | 1 - .../serializers/authentication_serializer.py | 23 +++++- .../serializers/posts_serializer.py | 2 - .../serializers/users_serializer.py | 14 +++- api/neuronaApp/urls.py | 1 + api/neuronaApp/views/authentication_view.py | 26 ++++--- api/neuronaApp/views/posts_view.py | 1 + api/neuronaApp/views/user_profile_view.py | 10 ++- .../migrations/0003_remove_userlogs_email.py | 17 +++++ api/neuronaLogs/models/logs_managing.py | 4 - api/neuronaLogs/models/logs_models.py | 1 - frontend/src/Authentication/Passkey.js | 22 +++--- frontend/src/api/routes.js | 3 + frontend/src/components/Navbar.vue | 27 +++++-- frontend/src/components/Post.vue | 74 ++++++++++--------- frontend/src/components/PostWriting.vue | 30 +++----- frontend/src/components/Register.vue | 50 +++++++------ frontend/src/components/Timeline.vue | 3 +- 19 files changed, 207 insertions(+), 119 deletions(-) create mode 100644 api/neuronaApp/migrations/0012_remove_user_email.py create mode 100644 api/neuronaLogs/migrations/0003_remove_userlogs_email.py diff --git a/api/neuronaApp/migrations/0012_remove_user_email.py b/api/neuronaApp/migrations/0012_remove_user_email.py new file mode 100644 index 0000000..728b29f --- /dev/null +++ b/api/neuronaApp/migrations/0012_remove_user_email.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.2 on 2024-04-09 15:02 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('neuronaApp', '0011_alter_posts_space_alter_posts_tag'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='email', + ), + ] diff --git a/api/neuronaApp/models/user_models.py b/api/neuronaApp/models/user_models.py index a396499..09474a9 100644 --- a/api/neuronaApp/models/user_models.py +++ b/api/neuronaApp/models/user_models.py @@ -35,7 +35,6 @@ def generate_and_get(self, challenge): class User(models.Model): username = models.CharField(max_length=100, unique=True) - email = models.EmailField(max_length=100, unique=True) display_name = models.CharField(max_length=100, blank=True) about = models.TextField(max_length=2000, blank=True) image_url = models.URLField(max_length=200, null=True) diff --git a/api/neuronaApp/serializers/authentication_serializer.py b/api/neuronaApp/serializers/authentication_serializer.py index 3142e8d..63aeb7f 100644 --- a/api/neuronaApp/serializers/authentication_serializer.py +++ b/api/neuronaApp/serializers/authentication_serializer.py @@ -5,10 +5,13 @@ USERNAME_MIN_LENGTH = 3 USERNAME_MAX_LENGTH = 15 +DISPLAY_NAME_MIN_LENGTH = 2 +DISPLAY_NAME_MAX_LENGTH = 50 USERNAME_REGEX = r"^[a-zA-Z][a-zA-Z0-9_]{" + str(USERNAME_MIN_LENGTH) + r"," + str(USERNAME_MAX_LENGTH) + r"}$" EMAIL_REGEX = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" + class ChallengeSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Challenges @@ -40,6 +43,7 @@ def validate_username(self, value): def get_error_message(self): return self.errors["username"][0] + class EmailSerializer(serializers.Serializer): email = serializers.EmailField(max_length=100) @@ -56,6 +60,23 @@ def get_error_message(self): return self.errors["email"][0] +class DisplayNameSerializer(serializers.Serializer): + display_name = serializers.CharField(max_length=50) + + def validate_display_name(self, value): + if len(value) > DISPLAY_NAME_MAX_LENGTH: + raise serializers.ValidationError(f"Display name must be at most {DISPLAY_NAME_MAX_LENGTH} characters long") + + if len(value) < DISPLAY_NAME_MIN_LENGTH: + raise serializers.ValidationError( + f"Display name must be at least {DISPLAY_NAME_MIN_LENGTH} characters long") + + return value + + def get_error_message(self): + return self.errors["display_name"][0] + + class UsernameOrEmailSerializer(serializers.Serializer): username_or_email = serializers.CharField(max_length=100) @@ -100,4 +121,4 @@ class Meta: fields = [ "key", "expires_at" - ] \ No newline at end of file + ] diff --git a/api/neuronaApp/serializers/posts_serializer.py b/api/neuronaApp/serializers/posts_serializer.py index 2799f6d..9c57c6f 100644 --- a/api/neuronaApp/serializers/posts_serializer.py +++ b/api/neuronaApp/serializers/posts_serializer.py @@ -8,7 +8,6 @@ class Meta: model = Posts fields = [ "user", - "title", "content", "space", "tag", @@ -24,7 +23,6 @@ class Meta: "id", "user", "votes_and_comments", - "title", "created_at", "content", "space", diff --git a/api/neuronaApp/serializers/users_serializer.py b/api/neuronaApp/serializers/users_serializer.py index 9332b92..8a46ace 100644 --- a/api/neuronaApp/serializers/users_serializer.py +++ b/api/neuronaApp/serializers/users_serializer.py @@ -1,7 +1,10 @@ +import urllib.parse + from rest_framework import serializers from neuronaApp.models import User +DEFAULT_AVATAR_BASE_URL = "https://avatar.iran.liara.run/username?username=" class UserSerializer(serializers.ModelSerializer): class Meta: @@ -10,6 +13,13 @@ class Meta: "id", "username", "display_name", - "email", "image_url", - ] \ No newline at end of file + ] + + def to_representation(self, instance): + data = super().to_representation(instance) + + if data["image_url"] is None: + display_name_encoded = urllib.parse.quote_plus(data["display_name"]) + data["image_url"] = DEFAULT_AVATAR_BASE_URL + display_name_encoded + return data \ No newline at end of file diff --git a/api/neuronaApp/urls.py b/api/neuronaApp/urls.py index d4028a8..feafe7b 100644 --- a/api/neuronaApp/urls.py +++ b/api/neuronaApp/urls.py @@ -9,6 +9,7 @@ router.register(r'passkey-options', views.PasskeyChallengeView, basename='passkey-options') router.register(r'posts', PostsViewSet) router.register(r'comments', CommentsViewSet) +router.register(r'profile', views.Profile, basename='profile') urlpatterns = [ path("register/", views.RegisterView.as_view(), name="register"), diff --git a/api/neuronaApp/views/authentication_view.py b/api/neuronaApp/views/authentication_view.py index 7509bbd..daa749b 100644 --- a/api/neuronaApp/views/authentication_view.py +++ b/api/neuronaApp/views/authentication_view.py @@ -9,7 +9,7 @@ from neuronaApp.models import Challenges, User, PublicKeys, ApiKeys from neuronaApp.serializers.authentication_serializer import ChallengeSerializer, ChallengeIdSerializer, \ UsernameSerializer, \ - EmailSerializer, UsernameOrEmailSerializer, ApiKeySerializer + EmailSerializer, UsernameOrEmailSerializer, ApiKeySerializer, DisplayNameSerializer import webauthn import logging @@ -28,7 +28,7 @@ class PasskeyChallengeView(viewsets.ViewSet): @action(detail=False, methods=["post"]) def register(self, request, **kwargs): username_serializer = UsernameSerializer(data=request.data) - email_serializer = EmailSerializer(data=request.data) + display_name_serializer = DisplayNameSerializer(data=request.data) if not username_serializer.is_valid(): return Response({ @@ -36,16 +36,16 @@ def register(self, request, **kwargs): "message": username_serializer.get_error_message() }, status=400) - if not email_serializer.is_valid(): + if not display_name_serializer.is_valid(): return Response({ - "error": email_serializer.errors, - "message": email_serializer.get_error_message() + "error": display_name_serializer.errors, + "message": display_name_serializer.get_error_message() }, status=400) options = webauthn.generate_registration_options( rp_name="Neurona", rp_id=getattr(settings, "PASSKEY_RP_ID", "localhost"), - user_name=email_serializer.validated_data["email"], + user_name=username_serializer.validated_data["username"], ) challenge = Challenges().generate_and_get(options.challenge) @@ -97,8 +97,8 @@ def post(self, request, **kwargs): registration_data = request.data.get("data", {}) username_serializer = UsernameSerializer(data=registration_data) - email_serializer = EmailSerializer(data=registration_data) challenge_id_serializer = ChallengeIdSerializer(data=registration_data) + display_name_serializer = DisplayNameSerializer(data=registration_data) # if the data is not valid, return the errors if not username_serializer.is_valid(): @@ -107,10 +107,10 @@ def post(self, request, **kwargs): "message": username_serializer.get_error_message() }, status=400) - if not email_serializer.is_valid(): + if not display_name_serializer.is_valid(): return Response({ - "error": email_serializer.errors, - "message": email_serializer.get_error_message() + "error": display_name_serializer.errors, + "message": display_name_serializer.get_error_message() }, status=400) if not challenge_id_serializer.is_valid(): @@ -141,9 +141,9 @@ def post(self, request, **kwargs): # if the registration response is valid, save the user and the public key in the database username = username_serializer.validated_data["username"] - email = email_serializer.validated_data["email"] + display_name = display_name_serializer.validated_data["display_name"] - User(username=username, email=email).save() + User(username=username, display_name=display_name).save() user = User.objects.get(username=username) PublicKeys( @@ -177,6 +177,7 @@ def post(self, request, **kwargs): }, status=400) user = user_serializer.get_user() + logger.info(f"found user = {user.display_name} @ {user.username}") if not challenge_id_serializer.is_valid(): return Response({ @@ -212,6 +213,7 @@ def post(self, request, **kwargs): logger.error(f"authentication_verification: {e}") return Response({"message": "Login failed. Please try again."}, status=400) + class LogoutView(APIView): """ This view is responsible for logging out the user. diff --git a/api/neuronaApp/views/posts_view.py b/api/neuronaApp/views/posts_view.py index ec34880..3557842 100644 --- a/api/neuronaApp/views/posts_view.py +++ b/api/neuronaApp/views/posts_view.py @@ -17,6 +17,7 @@ class PostsViewSet(viewsets.ViewSet): def list(self, request, *args, **kwargs): queryset = Posts.objects.all() serializer = PostsComplexSerializer(queryset, context=request.user, many=True) + return Response(serializer.data) def create(self, request, *args, **kwargs): diff --git a/api/neuronaApp/views/user_profile_view.py b/api/neuronaApp/views/user_profile_view.py index ca85bcc..d6ac42c 100644 --- a/api/neuronaApp/views/user_profile_view.py +++ b/api/neuronaApp/views/user_profile_view.py @@ -2,7 +2,8 @@ from rest_framework.decorators import action from rest_framework.response import Response -from neuronaApp.serializers import UsernameSerializer, EmailSerializer +from neuronaApp.serializers import UsernameSerializer, EmailSerializer, UserSerializer +from neuronaApp.token_authentication import TokenAuthentication class Validity(viewsets.ViewSet): @@ -25,3 +26,10 @@ def email(self, request, *args, **kwargs): return Response({"message": serializer.get_error_message()}, status=400) return Response(status=200) + +class Profile(viewsets.ViewSet): + authentication_classes = (TokenAuthentication,) + + def list(self, request, *args, **kwargs): + user = request.user + return Response(UserSerializer(user).data) diff --git a/api/neuronaLogs/migrations/0003_remove_userlogs_email.py b/api/neuronaLogs/migrations/0003_remove_userlogs_email.py new file mode 100644 index 0000000..c723839 --- /dev/null +++ b/api/neuronaLogs/migrations/0003_remove_userlogs_email.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.2 on 2024-04-09 15:02 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('neuronaLogs', '0002_alter_commentlogs_comment_id_alter_postlogs_admin_id_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='userlogs', + name='email', + ), + ] diff --git a/api/neuronaLogs/models/logs_managing.py b/api/neuronaLogs/models/logs_managing.py index 08994c1..67b045c 100644 --- a/api/neuronaLogs/models/logs_managing.py +++ b/api/neuronaLogs/models/logs_managing.py @@ -10,7 +10,6 @@ def save_user_log(instance, action): UserLogs( user_id=instance.pk, - email=instance.email, display_name=instance.display_name, username=instance.username, about=instance.about, @@ -28,9 +27,6 @@ def user_changes(sender, instance, **kwargs): if instance.pk: original = sender.objects.get(pk=instance.pk) - if original.email != instance.email: - save_user_log(instance, UserAction.CHANGED_EMAIL) - if original.display_name != instance.display_name: save_user_log(instance, UserAction.CHANGED_DISPLAY_NAME) diff --git a/api/neuronaLogs/models/logs_models.py b/api/neuronaLogs/models/logs_models.py index 0501485..0bbc611 100644 --- a/api/neuronaLogs/models/logs_models.py +++ b/api/neuronaLogs/models/logs_models.py @@ -21,7 +21,6 @@ class UserAction(Enum): class UserLogs(models.Model): user_id = models.IntegerField(null=True) - email = models.EmailField(max_length=100) display_name = models.CharField(max_length=100) username = models.CharField(max_length=100) about = models.TextField(max_length=2000, blank=True) diff --git a/frontend/src/Authentication/Passkey.js b/frontend/src/Authentication/Passkey.js index bff9bbf..3f16306 100644 --- a/frontend/src/Authentication/Passkey.js +++ b/frontend/src/Authentication/Passkey.js @@ -1,11 +1,11 @@ import axios from "axios"; import routes from "@/api/routes"; -async function fetchRegisterOptions(username, email) { +async function fetchRegisterOptions(username, name) { console.log("route", routes.authentication.register_options); return axios.post(routes.authentication.register_options, { "username": username, - "email": email, + "display_name": name, }); } @@ -34,8 +34,8 @@ function BufToBase64url(buf) { .replace(/=/g, ''); } -async function getRegisterCredentialOptions(username, email) { - const options_str = await fetchRegisterOptions(username, email); +async function getRegisterCredentialOptions(username, name) { + const options_str = await fetchRegisterOptions(username, name); const options = JSON.parse(options_str.data.options); const challenge_id = options_str.data.id; @@ -61,8 +61,8 @@ async function getLoginCredentialOptions(username_or_email) { } } -async function createPublicKeyCredential(username, email) { - const credentialOptions = await getRegisterCredentialOptions(username, email); +async function createPublicKeyCredential(username, name) { + const credentialOptions = await getRegisterCredentialOptions(username, name); const credentials_ = await navigator.credentials.create({publicKey: credentialOptions.options}); const credentials = { @@ -117,12 +117,12 @@ async function requestLogin(username_or_email, credentials, challenge_id) { return await axios.post(routes.authentication.login, data); } -async function requestRegister(username, email, credentials, challenge_id) { +async function requestRegister(username, name, credentials, challenge_id) { const data = { credentials: credentials, data: { username: username, - email: email, + display_name: name, challenge_id: challenge_id } } @@ -136,9 +136,9 @@ async function login(username_or_email) { return response; } -async function register(username, email){ - const credentials = await createPublicKeyCredential(username, email); - return await requestRegister(username, email, credentials.credentials, credentials.challenge_id); +async function register(username, name){ + const credentials = await createPublicKeyCredential(username, name); + return await requestRegister(username, name, credentials.credentials, credentials.challenge_id); } diff --git a/frontend/src/api/routes.js b/frontend/src/api/routes.js index 4a2a191..c5154a6 100644 --- a/frontend/src/api/routes.js +++ b/frontend/src/api/routes.js @@ -17,6 +17,9 @@ const routes = { downvote: (id) => `${BASE_URL}/posts/${id}/downvote/`, unvote: (id) => `${BASE_URL}/posts/${id}/unvote/`, }, + profile: { + show: `${BASE_URL}/profile/`, + } } export default routes; diff --git a/frontend/src/components/Navbar.vue b/frontend/src/components/Navbar.vue index a5afffe..98f4e9e 100644 --- a/frontend/src/components/Navbar.vue +++ b/frontend/src/components/Navbar.vue @@ -1,12 +1,16 @@