Skip to content

Commit

Permalink
48 profile editing (#57)
Browse files Browse the repository at this point in the history
* update login api to use username only

* move vuejs files

* create class that centralise message management

* create class that centralise api requests

* use new centralised classes

* update django docker image command

* use vuex to store application state

* throw error after being catched to show message

* add snackbar messages + add timeout to messages

* add event bus to communicate between components

* add frontend to edit username and displayname

* add backend to edit profile

* remove unused 'unique' props

* add 'about' attribute to profile serializer

* add frontend to edit 'about me'

* change icon to create new post + remove search option

* refactor : code cleanup

* update views

* add method to delete user in api

* add method to show user posts in api

* update user profile view

* change confirmation icon when deleting account
  • Loading branch information
ylked authored Apr 16, 2024
1 parent e7eae26 commit 624843e
Show file tree
Hide file tree
Showing 35 changed files with 976 additions and 278 deletions.
14 changes: 14 additions & 0 deletions api/neuronaApp/serializers/authentication_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ def validate_username(self, value):
def get_error_message(self):
return self.errors["username"][0]

class UsernameLoginSerializer(serializers.Serializer):
username = serializers.CharField(max_length=100)

def validate_username(self, value):
if not User.objects.filter(username=value).exists():
raise serializers.ValidationError("Username not found")

return value

def get_user(self):
return User.objects.get(username=self.validated_data["username"])

def get_error_message(self):
return self.errors["username"][0]

class EmailSerializer(serializers.Serializer):
email = serializers.EmailField(max_length=100)
Expand Down
17 changes: 16 additions & 1 deletion api/neuronaApp/serializers/users_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from neuronaApp.models import User

DEFAULT_AVATAR_BASE_URL = "https://avatar.iran.liara.run/username?username="
BIOGRAPHY_MAX_LENGTH = 200


class UserSerializer(serializers.ModelSerializer):
class Meta:
Expand All @@ -14,6 +16,7 @@ class Meta:
"username",
"display_name",
"image_url",
"about",
]

def to_representation(self, instance):
Expand All @@ -22,4 +25,16 @@ def to_representation(self, 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
return data


class BiographySerializer(serializers.Serializer):
about = serializers.CharField(max_length=200)

def validate_about(self, value):
if len(value) > BIOGRAPHY_MAX_LENGTH:
raise serializers.ValidationError(f"Biography must be at most {BIOGRAPHY_MAX_LENGTH} characters long")
return value

def get_error_message(self):
return self.errors["about"][0]
12 changes: 6 additions & 6 deletions api/neuronaApp/views/authentication_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from neuronaApp.models import Challenges, User, PublicKeys, ApiKeys
from neuronaApp.serializers.authentication_serializer import ChallengeSerializer, ChallengeIdSerializer, \
UsernameSerializer, \
EmailSerializer, UsernameOrEmailSerializer, ApiKeySerializer, DisplayNameSerializer
EmailSerializer, UsernameOrEmailSerializer, ApiKeySerializer, DisplayNameSerializer, UsernameLoginSerializer

import webauthn
import logging
Expand Down Expand Up @@ -60,12 +60,12 @@ def register(self, request, **kwargs):

@action(detail=False, methods=["post"])
def login(self, request, **kwargs):
username_or_email_serializer = UsernameOrEmailSerializer(data=request.data)
username_serializer = UsernameLoginSerializer(data=request.data)

if not username_or_email_serializer.is_valid():
if not username_serializer.is_valid():
return Response({
"error": username_or_email_serializer.errors,
"message": username_or_email_serializer.get_error_message()
"error": username_serializer.errors,
"message": username_serializer.get_error_message()
}, status=400)

options = webauthn.generate_authentication_options(
Expand Down Expand Up @@ -167,7 +167,7 @@ def post(self, request, **kwargs):
credentials = request.data.get("credentials", {})
data = request.data.get("data", {})

user_serializer = UsernameOrEmailSerializer(data=data)
user_serializer = UsernameLoginSerializer(data=data)
challenge_id_serializer = ChallengeIdSerializer(data=data)

if not user_serializer.is_valid():
Expand Down
9 changes: 8 additions & 1 deletion api/neuronaApp/views/posts_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from rest_framework.response import Response
from rest_framework.views import APIView

from neuronaApp.models import Posts, Comments
from neuronaApp.models import Posts, Comments, User
from neuronaApp.serializers import PostsSerializer, CommentsSerializer, UserSerializer, PostsComplexSerializer
from neuronaApp.token_authentication import TokenAuthentication
from neuronaApp.views.authentication_view import logger
Expand All @@ -20,6 +20,13 @@ def list(self, request, *args, **kwargs):

return Response(serializer.data)

@action(detail=False, methods=["get"], url_path='user/(?P<username>[^/.]+)')
def user(self, request, username=None):
user_id = User.objects.get(username__exact=username).id
queryset = Posts.objects.filter(user_id__exact=user_id)
serializer = PostsComplexSerializer(queryset, context=request.user, many=True)
return Response(serializer.data)

def create(self, request, *args, **kwargs):
user = request.user
data = request.data.copy()
Expand Down
50 changes: 49 additions & 1 deletion api/neuronaApp/views/user_profile_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
from rest_framework.decorators import action
from rest_framework.response import Response

from neuronaApp.serializers import UsernameSerializer, EmailSerializer, UserSerializer
from neuronaApp.serializers import UsernameSerializer, EmailSerializer, UserSerializer, DisplayNameSerializer
from neuronaApp.serializers.users_serializer import BiographySerializer
from neuronaApp.token_authentication import TokenAuthentication


from neuronaApp.views.authentication_view import logger

class Validity(viewsets.ViewSet):
@action(detail=False, methods=["GET"])
def username(self, request, *args, **kwargs):
Expand Down Expand Up @@ -33,3 +36,48 @@ class Profile(viewsets.ViewSet):
def list(self, request, *args, **kwargs):
user = request.user
return Response(UserSerializer(user).data)

@action(detail=False, methods=['delete'])
def delete(self, request, *args, **kwargs):
user = request.user
user.delete()
return Response(status=204)

@action(detail=False, methods=["PUT"])
def username(self, request, *args, **kwargs):
user = request.user
serializer = UsernameSerializer(data=request.data)

if not serializer.is_valid():
return Response({"message": serializer.get_error_message()}, status=400)

user.username = serializer.validated_data["username"]
user.save()

return Response(status=200)

@action(detail=False, methods=["PUT"])
def display_name(self, request, *args, **kwargs):
user = request.user
serializer = DisplayNameSerializer(data=request.data)

if not serializer.is_valid():
return Response({"message": serializer.get_error_message()}, status=400)

user.display_name = serializer.validated_data["display_name"]
user.save()

return Response(status=200)

@action(detail=False, methods=["PUT"])
def about(self, request, *args, **kwargs):
user = request.user
serializer = BiographySerializer(data=request.data)

if not serializer.is_valid():
return Response({"message": serializer.get_error_message()}, status=400)

user.about = serializer.validated_data["about"]
user.save()

return Response(status=200)
3 changes: 2 additions & 1 deletion docker-images/django/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ WORKDIR /data

CMD sh -c "pipenv install && \
pipenv run pip3 install tzdata && \
pipenv run python3 manage.py migrate && \
pipenv run python3 manage.py migrate neuronaApp && \
pipenv run python3 manage.py migrate neuronaLogs --database logs \
pipenv run python3 manage.py runserver 0.0.0.0:8000"

48 changes: 47 additions & 1 deletion frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@
"axios": "^1.6.7",
"date-fns": "^3.6.0",
"dotenv": "^16.4.5",
"mitt": "^3.0.1",
"roboto-fontface": "*",
"vue": "^3.3.0",
"vue-router": "^4.2.5",
"vuetify": "^3.0.0"
"vuetify": "^3.0.0",
"vuex": "^4.1.0",
"vuex-persistedstate": "^4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.0",
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<template>
<v-app>
<Navbar :logged-in="state.authenticated"/>
<Navbar />
<v-main>
<AlertBanner :messages="messages"/>
<SnackbarMessage :message="snack.message" :timeout="snack.timeout"/>
<router-view/>
</v-main>
</v-app>
Expand All @@ -10,6 +12,10 @@
<script setup>
//
import Navbar from "@/components/Navbar.vue";
import {state} from "@/Authentication/store";
import AlertBanner from "@/components/alerts/AlertBanner.vue";
import MessageManager from "@/tools/MessageManager";
import SnackbarMessage from "@/components/alerts/SnackbarMessage.vue";
const messages = MessageManager.getInstance().get();
const snack = MessageManager.getInstance().getSnackbar();
</script>
16 changes: 7 additions & 9 deletions frontend/src/Authentication/Passkey.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@ import axios from "axios";
import routes from "@/api/routes";

async function fetchRegisterOptions(username, name) {
console.log("route", routes.authentication.register_options);
return axios.post(routes.authentication.register_options, {
"username": username,
"display_name": name,
});
}

async function fetchLoginOptions(username_or_email) {
async function fetchLoginOptions(username) {
return axios.post(routes.authentication.login_options, {
"username_or_email": username_or_email,
"username": username,
});
}

Expand Down Expand Up @@ -105,12 +104,12 @@ async function getPublicKeyCredential(username_or_email) {
}
}

async function requestLogin(username_or_email, credentials, challenge_id) {
async function requestLogin(username, credentials, challenge_id) {
const data = {
credentials: credentials,
data: {
challenge_id: challenge_id,
username_or_email: username_or_email,
username: username,
}
}

Expand All @@ -130,10 +129,9 @@ async function requestRegister(username, name, credentials, challenge_id) {
return await axios.post(routes.authentication.register, data);
}

async function login(username_or_email) {
const credentials = await getPublicKeyCredential(username_or_email);
const response = await requestLogin(username_or_email, credentials.credentials, credentials.challenge_id);
return response;
async function login(username) {
const credentials = await getPublicKeyCredential(username);
return await requestLogin(username, credentials.credentials, credentials.challenge_id);
}

async function register(username, name){
Expand Down
Loading

0 comments on commit 624843e

Please sign in to comment.