From 1426ce41c0183f911febef2e742368440276f5b6 Mon Sep 17 00:00:00 2001 From: Nhat Minh Luu <54903938+nluu175@users.noreply.github.com> Date: Sun, 22 Oct 2023 17:14:59 -0600 Subject: [PATCH 01/11] Added api endpoints for login and signup --- .../migrations/0004_author_user.py | 25 +++++++ server/socialdistribution/models/author.py | 4 ++ server/socialdistribution/serializers.py | 8 +++ server/socialdistribution/urls.py | 2 + server/socialdistribution/views.py | 65 +++++++++++++++++++ 5 files changed, 104 insertions(+) create mode 100644 server/socialdistribution/migrations/0004_author_user.py diff --git a/server/socialdistribution/migrations/0004_author_user.py b/server/socialdistribution/migrations/0004_author_user.py new file mode 100644 index 00000000..e427a437 --- /dev/null +++ b/server/socialdistribution/migrations/0004_author_user.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.4 on 2023-10-22 23:14 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("socialdistribution", "0003_alter_post_contenttype"), + ] + + operations = [ + migrations.AddField( + model_name="author", + name="user", + field=models.OneToOneField( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/server/socialdistribution/models/author.py b/server/socialdistribution/models/author.py index 5dca6ee9..ff9d437c 100644 --- a/server/socialdistribution/models/author.py +++ b/server/socialdistribution/models/author.py @@ -2,6 +2,7 @@ from django.db import models from socialdistribution.utils.constants import STRING_MAXLEN, URL_MAXLEN +from django.contrib.auth.models import User class Author(models.Model): @@ -15,3 +16,6 @@ class Author(models.Model): host = models.URLField(max_length=URL_MAXLEN) # home host of the author profileImage = models.URLField(max_length=URL_MAXLEN) url = models.URLField(max_length=URL_MAXLEN) # url to the author's profile + + # TODO: Currently have user field as nullable + user = models.OneToOneField(User, on_delete=models.CASCADE, null=True) diff --git a/server/socialdistribution/serializers.py b/server/socialdistribution/serializers.py index 721bf178..bca8cd2e 100644 --- a/server/socialdistribution/serializers.py +++ b/server/socialdistribution/serializers.py @@ -1,6 +1,8 @@ from rest_framework import serializers from rest_framework.serializers import * from .models import Author, Post +from django.contrib.auth.models import User + class AuthorSerializer(serializers.ModelSerializer): @@ -51,3 +53,9 @@ def get_source_url(self, obj): else: uri = self.context["request"].build_absolute_uri("/") return f"{uri}author/{obj.author.id}/posts/{obj.id}" + + +class LoginSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ("username", "email", "password") diff --git a/server/socialdistribution/urls.py b/server/socialdistribution/urls.py index 5ac73f99..c5642c62 100644 --- a/server/socialdistribution/urls.py +++ b/server/socialdistribution/urls.py @@ -5,4 +5,6 @@ path("author//", AuthorView.as_view(), name="author"), path("author//posts/", PostsView.as_view(), name="posts"), path("author//posts//", PostView.as_view(), name="single_post"), + path("login/", LoginView.as_view(), name="login"), + path("signup/", SignUpView.as_view(), name="signup") ] \ No newline at end of file diff --git a/server/socialdistribution/views.py b/server/socialdistribution/views.py index e078aa86..3316fdd3 100644 --- a/server/socialdistribution/views.py +++ b/server/socialdistribution/views.py @@ -2,12 +2,15 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status +from django.contrib.auth.models import User from .serializers import * from .models import * from .utils import * + + # Create your views here. class AuthorView(APIView): queryset = Author.objects.all() @@ -93,3 +96,65 @@ def put(self, request, author_id, post_id): return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class LoginView(APIView): + http_method_names = ["post"] + + def post(self, request): + username = request.POST["username"] + password = request.POST["password"] + + try: + user = User.objects.get(username=username) + success = user.check_password(password) + + # on success login check + if success: + data = {"token": "123456"} + return Response(data, status=status.HTTP_201_CREATED) + # on wrong password + else: + data = {"message": "Wrong password"} + return Response(data, status=status.HTTP_401_UNAUTHORIZED) + + + except User.DoesNotExist: + data = {"message": "User not found"} + return Response(data, status=status.HTTP_404_NOT_FOUND) + + +class SignUpView(APIView): + http_method_names = ["post"] + + def post(self, request): + username = request.POST["username"] + email = request.POST["email"] + password = request.POST["password"] + + author_data = {"displayName": "placeholder", + "github": "https://placeholder.com", + "host": "https://placeholder.com", + "profileImage": "https://placeholder.com", + "url": "https://placeholder.com"} + + + post_object = Author.objects.create(displayName="placeholder", + github="https://placeholder.com", + host="https://placeholder.com", + profileImage="https://placeholder.com", + url="https://placeholder.com", + user=User.objects.create_user(username=username, + email=email, + password=password)) + + serializer = AuthorSerializer(instance=post_object, + data=author_data, + context={"request": request}) + + if serializer.is_valid(): + # save update and set updatedAt to current time + serializer.save(updatedAt=timezone.now()) + return Response(serializer.data) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) From 435e5687c290c4ab7f4f1ae1ca63f86949f96017 Mon Sep 17 00:00:00 2001 From: Nhat Minh Luu <54903938+nluu175@users.noreply.github.com> Date: Sun, 22 Oct 2023 17:28:43 -0600 Subject: [PATCH 02/11] Added auth token to login api --- server/server/settings.py | 3 ++- server/socialdistribution/views.py | 16 ++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/server/server/settings.py b/server/server/settings.py index 738f27fe..cd46f28b 100644 --- a/server/server/settings.py +++ b/server/server/settings.py @@ -39,7 +39,8 @@ "django.contrib.messages", "django.contrib.staticfiles", "rest_framework", - "corsheaders" + "corsheaders", + "rest_framework.authtoken" ] MIDDLEWARE = [ diff --git a/server/socialdistribution/views.py b/server/socialdistribution/views.py index 3316fdd3..b6eb7b90 100644 --- a/server/socialdistribution/views.py +++ b/server/socialdistribution/views.py @@ -2,8 +2,10 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status +from rest_framework.authtoken.models import Token from django.contrib.auth.models import User + from .serializers import * from .models import * from .utils import * @@ -111,7 +113,9 @@ def post(self, request): # on success login check if success: - data = {"token": "123456"} + # create token + token, created = Token.objects.get_or_create(user=user) + data = {"token": token.key} return Response(data, status=status.HTTP_201_CREATED) # on wrong password else: @@ -139,11 +143,11 @@ def post(self, request): "url": "https://placeholder.com"} - post_object = Author.objects.create(displayName="placeholder", - github="https://placeholder.com", - host="https://placeholder.com", - profileImage="https://placeholder.com", - url="https://placeholder.com", + post_object = Author.objects.create(displayName=author_data["displayName"], + github=author_data["github"], + host=author_data["host"], + profileImage=author_data["profileImage"], + url=author_data["url"], user=User.objects.create_user(username=username, email=email, password=password)) From 5d5cf9740cc1bc0924422423e669159cd3bb3814 Mon Sep 17 00:00:00 2001 From: Nhat Minh Luu <54903938+nluu175@users.noreply.github.com> Date: Sun, 22 Oct 2023 17:29:40 -0600 Subject: [PATCH 03/11] Added comment to the placeholder author data --- server/socialdistribution/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server/socialdistribution/views.py b/server/socialdistribution/views.py index b6eb7b90..3d1e07c5 100644 --- a/server/socialdistribution/views.py +++ b/server/socialdistribution/views.py @@ -136,6 +136,7 @@ def post(self, request): email = request.POST["email"] password = request.POST["password"] + # TODO: This one is currently a placeholder author_data = {"displayName": "placeholder", "github": "https://placeholder.com", "host": "https://placeholder.com", From c4c75db1f8ea06d74a16bd4b7fa9944ff7d19f70 Mon Sep 17 00:00:00 2001 From: Nhat Minh Luu <54903938+nluu175@users.noreply.github.com> Date: Sun, 22 Oct 2023 18:02:43 -0600 Subject: [PATCH 04/11] Added toastify for toast message --- client/package-lock.json | 63 +++++++++++++++++++++++++++++++++++----- client/package.json | 4 ++- client/src/App.tsx | 25 ++++++++++++---- client/src/index.tsx | 2 ++ 4 files changed, 80 insertions(+), 14 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index f7bb226f..ab169765 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -24,7 +24,9 @@ "react-dom": "^18.2.0", "react-router-dom": "^6.17.0", "react-scripts": "5.0.1", - "typescript": "^5.2.2", + "react-toastify": "^9.1.3", + "toastify-js": "^1.12.0", + "typescript": "^4.9.5", "web-vitals": "^2.1.4" } }, @@ -15338,6 +15340,26 @@ } } }, + "node_modules/react-toastify": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz", + "integrity": "sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==", + "dependencies": { + "clsx": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/react-toastify/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -17041,6 +17063,11 @@ "node": ">=8.0" } }, + "node_modules/toastify-js": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz", + "integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -17258,15 +17285,15 @@ } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=14.17" + "node": ">=4.2.0" } }, "node_modules/unbox-primitive": { @@ -29187,6 +29214,21 @@ "workbox-webpack-plugin": "^6.4.1" } }, + "react-toastify": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz", + "integrity": "sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==", + "requires": { + "clsx": "^1.1.1" + }, + "dependencies": { + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + } + } + }, "react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -30440,6 +30482,11 @@ "is-number": "^7.0.0" } }, + "toastify-js": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz", + "integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==" + }, "toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -30606,9 +30653,9 @@ } }, "typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==" + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==" }, "unbox-primitive": { "version": "1.0.2", diff --git a/client/package.json b/client/package.json index de12ca33..0dba8c8b 100644 --- a/client/package.json +++ b/client/package.json @@ -19,7 +19,9 @@ "react-dom": "^18.2.0", "react-router-dom": "^6.17.0", "react-scripts": "5.0.1", - "typescript": "^5.2.2", + "react-toastify": "^9.1.3", + "toastify-js": "^1.12.0", + "typescript": "^4.9.5", "web-vitals": "^2.1.4" }, "scripts": { diff --git a/client/src/App.tsx b/client/src/App.tsx index 06874267..46becfff 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -7,14 +7,29 @@ import Login from "./components/authentication/Login"; import SignUp from "./components/authentication/SignUp"; import "./App.css"; +import { ToastContainer } from "react-toastify"; const App = () => { return ( - - } /> - } /> - } /> - + <> + + + } /> + } /> + } /> + + ); }; diff --git a/client/src/index.tsx b/client/src/index.tsx index 18b48e0c..49361d64 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -8,6 +8,8 @@ import { ThemeProvider, createTheme } from "@mui/material/styles"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import "./index.css"; +import 'react-toastify/dist/ReactToastify.css'; + const theme = createTheme({ palette: { From edb636fe718845961d190959b2e24dd546a5cc06 Mon Sep 17 00:00:00 2001 From: Nhat Minh Luu <54903938+nluu175@users.noreply.github.com> Date: Sun, 22 Oct 2023 18:43:31 -0600 Subject: [PATCH 05/11] Added check for existing username on signup --- server/socialdistribution/views.py | 59 +++++++++++++++++------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/server/socialdistribution/views.py b/server/socialdistribution/views.py index 3d1e07c5..9dcf7289 100644 --- a/server/socialdistribution/views.py +++ b/server/socialdistribution/views.py @@ -136,30 +136,37 @@ def post(self, request): email = request.POST["email"] password = request.POST["password"] - # TODO: This one is currently a placeholder - author_data = {"displayName": "placeholder", - "github": "https://placeholder.com", - "host": "https://placeholder.com", - "profileImage": "https://placeholder.com", - "url": "https://placeholder.com"} - - - post_object = Author.objects.create(displayName=author_data["displayName"], - github=author_data["github"], - host=author_data["host"], - profileImage=author_data["profileImage"], - url=author_data["url"], - user=User.objects.create_user(username=username, - email=email, - password=password)) - - serializer = AuthorSerializer(instance=post_object, - data=author_data, - context={"request": request}) - if serializer.is_valid(): - # save update and set updatedAt to current time - serializer.save(updatedAt=timezone.now()) - return Response(serializer.data) - - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + try: + # check if the User with given username already exists + user = User.objects.get(username=username) + data = {"message": "Username already exists"} + return Response(data, status=status.HTTP_409_CONFLICT) + except: + # TODO: This one is currently a placeholder + author_data = {"displayName": "placeholder", + "github": "https://placeholder.com", + "host": "https://placeholder.com", + "profileImage": "https://placeholder.com", + "url": "https://placeholder.com"} + + + post_object = Author.objects.create(displayName=author_data["displayName"], + github=author_data["github"], + host=author_data["host"], + profileImage=author_data["profileImage"], + url=author_data["url"], + user=User.objects.create_user(username=username, + email=email, + password=password)) + + serializer = AuthorSerializer(instance=post_object, + data=author_data, + context={"request": request}) + + if serializer.is_valid(): + # save update and set updatedAt to current time + serializer.save(updatedAt=timezone.now()) + return Response(serializer.data) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) From e92b2593f7a547b23b0ac4879af4b2f2f19ddc85 Mon Sep 17 00:00:00 2001 From: Nhat Minh Luu <54903938+nluu175@users.noreply.github.com> Date: Sun, 22 Oct 2023 18:57:23 -0600 Subject: [PATCH 06/11] Finalized sign up --- .../src/components/authentication/SignUp.tsx | 67 +++++++++++++++++-- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/client/src/components/authentication/SignUp.tsx b/client/src/components/authentication/SignUp.tsx index 33bd5b42..70609629 100644 --- a/client/src/components/authentication/SignUp.tsx +++ b/client/src/components/authentication/SignUp.tsx @@ -1,4 +1,4 @@ -import React, { FormEvent } from "react"; +import React, { FormEvent, useState } from "react"; import Button from "@mui/material/Button"; import CssBaseline from "@mui/material/CssBaseline"; @@ -8,9 +8,46 @@ import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import Container from "@mui/material/Container"; +import axios from "axios"; + +import { toast } from "react-toastify"; +import { useNavigate } from "react-router-dom"; + const SignUp = () => { + const navigate = useNavigate(); + const [formData, setFormData] = useState({ + username: "", + email: "", + password: "", + repeatPassword: "", + }); + const handleSubmit = (e: FormEvent) => { - return; + e.preventDefault(); + + if (formData.password !== formData.repeatPassword) { + toast.error("Repeat password does not match!"); + return; + } + + const requestUrl = "http://127.0.0.1:8000/socialdistribution/signup/"; + + const form = new FormData(); + form.append("username", formData.username); + form.append("email", formData.email); + form.append("password", formData.password); + + axios + .post(requestUrl, form, { + headers: { "Content-Type": "multipart/form-data" }, + }) + .then((response: any) => { + toast.success("You have succesfully signed up"); + navigate("/login"); + }) + .catch((error) => { + toast.error(error.response.data.message); + }); }; return ( @@ -30,6 +67,18 @@ const SignUp = () => { Already have an account? Log In + { + setFormData({ ...formData, username: e.target.value }); + }} + /> { id="email" label="Enter your email" name="email" - autoFocus + onChange={(e) => { + setFormData({ ...formData, email: e.target.value }); + }} /> { label="Create password" type="password" id="password" + onChange={(e) => { + setFormData({ ...formData, password: e.target.value }); + }} /> { + setFormData({ ...formData, repeatPassword: e.target.value }); + }} />