From a4d3a78ddcfb514cfe42220c0ecddb790dcecae6 Mon Sep 17 00:00:00 2001 From: Abheek Tripathy <90976669+abheektripathy@users.noreply.github.com> Date: Mon, 26 Jun 2023 11:47:23 +0530 Subject: [PATCH] added up auth module. (#43) * added up auth module * init frontend * added middleware/protected routes --- .gitignore | 3 +- buffalogs/authentication/__init__.py | 0 buffalogs/authentication/admin.py | 3 + buffalogs/authentication/apps.py | 6 + .../authentication/migrations/0001_initial.py | 82 + .../authentication/migrations/__init__.py | 0 buffalogs/authentication/models.py | 54 + buffalogs/authentication/serializers.py | 96 + buffalogs/authentication/tests.py | 3 + buffalogs/authentication/urls.py | 11 + buffalogs/authentication/views.py | 80 + buffalogs/buffalogs/settings/settings.py | 40 +- buffalogs/buffalogs/urls.py | 4 +- buffalogs/requirements.txt | 5 +- frontend/.eslintrc.json | 3 + frontend/.gitignore | 35 + frontend/README.md | 1 + frontend/components/ui/Form.tsx | 176 + frontend/components/ui/button.tsx | 55 + frontend/components/ui/input.tsx | 25 + frontend/components/ui/label.tsx | 26 + frontend/lib/auth.ts | 49 + frontend/lib/constants.ts | 6 + frontend/lib/token.ts | 23 + frontend/lib/utils.ts | 6 + frontend/middleware.ts | 16 + frontend/next.config.js | 6 + frontend/package-lock.json | 6920 +++++++++++++++++ frontend/package.json | 41 + frontend/pages/_app.tsx | 18 + frontend/pages/_document.tsx | 13 + frontend/pages/api/hello.ts | 13 + frontend/pages/auth/index.tsx | 126 + frontend/pages/dashboard/index.tsx | 25 + frontend/pages/index.tsx | 12 + frontend/pnpm-lock.yaml | 2862 +++++++ frontend/postcss.config.js | 6 + frontend/public/favicon.ico | Bin 0 -> 25931 bytes frontend/styles/globals.css | 81 + frontend/tailwind.config.js | 75 + frontend/tsconfig.json | 23 + 41 files changed, 11024 insertions(+), 5 deletions(-) create mode 100644 buffalogs/authentication/__init__.py create mode 100644 buffalogs/authentication/admin.py create mode 100644 buffalogs/authentication/apps.py create mode 100644 buffalogs/authentication/migrations/0001_initial.py create mode 100644 buffalogs/authentication/migrations/__init__.py create mode 100644 buffalogs/authentication/models.py create mode 100644 buffalogs/authentication/serializers.py create mode 100644 buffalogs/authentication/tests.py create mode 100644 buffalogs/authentication/urls.py create mode 100644 buffalogs/authentication/views.py create mode 100644 frontend/.eslintrc.json create mode 100644 frontend/.gitignore create mode 100644 frontend/README.md create mode 100644 frontend/components/ui/Form.tsx create mode 100644 frontend/components/ui/button.tsx create mode 100644 frontend/components/ui/input.tsx create mode 100644 frontend/components/ui/label.tsx create mode 100644 frontend/lib/auth.ts create mode 100644 frontend/lib/constants.ts create mode 100644 frontend/lib/token.ts create mode 100644 frontend/lib/utils.ts create mode 100644 frontend/middleware.ts create mode 100644 frontend/next.config.js create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/pages/_app.tsx create mode 100644 frontend/pages/_document.tsx create mode 100644 frontend/pages/api/hello.ts create mode 100644 frontend/pages/auth/index.tsx create mode 100644 frontend/pages/dashboard/index.tsx create mode 100644 frontend/pages/index.tsx create mode 100644 frontend/pnpm-lock.yaml create mode 100644 frontend/postcss.config.js create mode 100644 frontend/public/favicon.ico create mode 100644 frontend/styles/globals.css create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.json diff --git a/.gitignore b/.gitignore index d4ccd32..36b232a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.log* *.pyc -.vscode \ No newline at end of file +.vscode +.venv \ No newline at end of file diff --git a/buffalogs/authentication/__init__.py b/buffalogs/authentication/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/buffalogs/authentication/admin.py b/buffalogs/authentication/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/buffalogs/authentication/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/buffalogs/authentication/apps.py b/buffalogs/authentication/apps.py new file mode 100644 index 0000000..c65f1d2 --- /dev/null +++ b/buffalogs/authentication/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AuthenticationConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "authentication" diff --git a/buffalogs/authentication/migrations/0001_initial.py b/buffalogs/authentication/migrations/0001_initial.py new file mode 100644 index 0000000..bc96508 --- /dev/null +++ b/buffalogs/authentication/migrations/0001_initial.py @@ -0,0 +1,82 @@ +# Generated by Django 4.1.4 on 2023-06-20 18:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField(db_index=True, max_length=255, unique=True), + ), + ( + "email", + models.EmailField(db_index=True, max_length=255, unique=True), + ), + ("is_staff", models.BooleanField(default=False)), + ("is_verified", models.BooleanField(default=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("avatar", models.CharField(default="", max_length=225)), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/buffalogs/authentication/migrations/__init__.py b/buffalogs/authentication/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/buffalogs/authentication/models.py b/buffalogs/authentication/models.py new file mode 100644 index 0000000..2cf02e1 --- /dev/null +++ b/buffalogs/authentication/models.py @@ -0,0 +1,54 @@ +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin +from django.db import models +from rest_framework_simplejwt.tokens import RefreshToken + + +class UserManager(BaseUserManager): + def create_user(self, username, email, password=None): + if username is None: + raise TypeError("Users should have a username") + if email is None: + raise TypeError("Users should have a Email") + + user = self.model( + username=username, + email=self.normalize_email(email), + ) + user.set_password(password) + user.save() + return user + + def create_superuser(self, username, email, password=None): + if password is None: + raise TypeError("Password should not be none") + + user = self.create_user(username, email, password) + user.is_superuser = True + user.is_staff = True + user.save() + return user + + +class User(AbstractBaseUser, PermissionsMixin): + username = models.CharField(max_length=255, unique=True, db_index=True) + email = models.EmailField(max_length=255, unique=True, db_index=True) + is_staff = models.BooleanField(default=False) + is_verified = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + avatar = models.CharField(default="", max_length=225) + + USERNAME_FIELD = "email" + REQUIRED_FIELDS = ["username"] + + objects = UserManager() + + def __str__(self): + return self.email + + def tokens(self): + refresh = RefreshToken.for_user(self) + return {"refresh": str(refresh), "access": str(refresh.access_token)} + + +# Create your models here. diff --git a/buffalogs/authentication/serializers.py b/buffalogs/authentication/serializers.py new file mode 100644 index 0000000..fc45036 --- /dev/null +++ b/buffalogs/authentication/serializers.py @@ -0,0 +1,96 @@ +from django.contrib import auth +from django.contrib.auth import logout +from rest_framework import serializers as rfs +from rest_framework.exceptions import AuthenticationFailed +from rest_framework_simplejwt.tokens import TokenError + +from .models import User + + +class RegisterSerializer(rfs.ModelSerializer): + password = rfs.CharField(max_length=68, min_length=6, write_only=True) + + default_error_messages = {"username": "The username should only contain alphanumeric characters"} + + class Meta: + model = User + fields = ["email", "username", "password"] + + def validate(self, attrs): + email = attrs.get("email", "") + username = attrs.get("username", "") + + user_filtered_by_email = User.objects.filter(email=email).first() + if user_filtered_by_email: + raise rfs.ValidationError("User with that email already exists") + + user_filtered_by_username = User.objects.filter(username=username).first() + if user_filtered_by_username: + raise rfs.ValidationError("User with that username already exists") + + if not username.isalnum(): + raise rfs.ValidationError(self.default_error_messages) + return attrs + + def create(self, validated_data): + return User.objects.create_user(**validated_data) + + +class LoginSerializer(rfs.ModelSerializer): + email = rfs.EmailField(max_length=255, min_length=3) + password = rfs.CharField(max_length=68, min_length=6, write_only=True) + + tokens = rfs.SerializerMethodField() + + def get_tokens(self, obj): + user = User.objects.get(email=obj["email"]) + + return {"refresh": user.tokens()["refresh"], "access": user.tokens()["access"]} + + class Meta: + model = User + fields = [ + "email", + "password", + "tokens", + ] + + def validate(self, attrs): + email = attrs.get("email", "") + password = attrs.get("password", "") + + user = auth.authenticate(email=email, password=password) + + if not user: + raise AuthenticationFailed("Invalid credentials, try again") + + return {"email": user.email, "username": user.username, "tokens": user.tokens} + + +class LogoutSerializer(rfs.Serializer): + default_error_message = {"bad_token": ("Token is expired or invalid")} + + def validate(self, attrs): + user = self.context["request"].user + self.tokens = user.tokens() + return attrs + + def save(self): + try: + logout(self.context["request"]) + except TokenError: + self.fail("bad_token") + + +class UserSerializer(rfs.ModelSerializer): + class Meta: + model = User + fields = ( + "id", + "email", + "username", + "created_at", + "updated_at", + "avatar", + "is_staff", + ) diff --git a/buffalogs/authentication/tests.py b/buffalogs/authentication/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/buffalogs/authentication/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/buffalogs/authentication/urls.py b/buffalogs/authentication/urls.py new file mode 100644 index 0000000..a35d148 --- /dev/null +++ b/buffalogs/authentication/urls.py @@ -0,0 +1,11 @@ +from django.urls import path +from rest_framework_simplejwt.views import TokenRefreshView + +from .views import LoginAPIView, LogoutAPIView, RegisterView + +urlpatterns = [ + path("login", LoginAPIView.as_view(), name="login"), + path("register", RegisterView.as_view(), name="register"), + path("logout", LogoutAPIView.as_view(), name="logout"), + path("token/refresh", TokenRefreshView.as_view(), name="token_refresh"), +] diff --git a/buffalogs/authentication/views.py b/buffalogs/authentication/views.py new file mode 100644 index 0000000..fed8ffb --- /dev/null +++ b/buffalogs/authentication/views.py @@ -0,0 +1,80 @@ +import logging +import os + +from django.contrib.auth import get_user_model +from django.http import HttpResponsePermanentRedirect +from django.shortcuts import render +from rest_framework import generics, permissions, status +from rest_framework.exceptions import AuthenticationFailed +from rest_framework.response import Response + +from .serializers import LoginSerializer, LogoutSerializer, RegisterSerializer, UserSerializer + +logger = logging.getLogger(__name__) + + +User = get_user_model() + + +class CustomRedirect(HttpResponsePermanentRedirect): + allowed_schemes = [os.environ.get("APP_SCHEME"), "http", "https"] + + +class RegisterView(generics.GenericAPIView): + + serializer_class = RegisterSerializer + authentication_classes = [] + permission_classes = [] + + def post(self, request): + user = request.data + serializer = self.serializer_class(data=user) + serializer.is_valid(raise_exception=True) + serializer.save() + user_data = serializer.data + user = User.objects.get(email=user_data["email"]) + + return Response( + { + "status": "successful", + }, + status=status.HTTP_201_CREATED, + ) + + +class LoginAPIView(generics.GenericAPIView): + serializer_class = LoginSerializer + authentication_classes = [] + permission_classes = [] + + def post(self, request): + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + data = serializer.data + data["username"] = serializer.validated_data["username"] + return Response(data, status=status.HTTP_200_OK) + + +class LogoutAPIView(generics.GenericAPIView): + serializer_class = LogoutSerializer + permission_classes = (permissions.IsAuthenticated,) + + def post(self, request): + + serializer = self.serializer_class(data=request.data, context={"request": request}) + serializer.is_valid(raise_exception=True) + serializer.save() + + return Response({"status": "successful"}, status=status.HTTP_200_OK) + + +class MeAPIView(generics.ListAPIView): + serializer_class = UserSerializer + permission_classes = (permissions.IsAuthenticated,) + + def get_queryset(self): + user = self.request.user + return User.objects.filter(id=user.id) + + +# Create your views here. diff --git a/buffalogs/buffalogs/settings/settings.py b/buffalogs/buffalogs/settings/settings.py index 1c2da32..9b3c3b3 100644 --- a/buffalogs/buffalogs/settings/settings.py +++ b/buffalogs/buffalogs/settings/settings.py @@ -10,6 +10,7 @@ https://docs.djangoproject.com/en/4.1/ref/settings/ """ import os +from datetime import timedelta from pathlib import Path from celery.schedules import crontab @@ -24,8 +25,6 @@ SECRET_KEY = CERTEGO_SECRET_KEY DEBUG = CERTEGO_DEBUG -ALLOWED_HOSTS = ["*"] - # Application definition @@ -37,11 +36,17 @@ "django.contrib.messages", "django.contrib.staticfiles", "impossible_travel", + "rest_framework", + "rest_framework.authtoken", + "rest_framework_simplejwt", + "authentication", + "corsheaders", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", + "corsheaders.middleware.CorsMiddleware", # CORS "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", @@ -173,6 +178,37 @@ STATIC_URL = "/static/" STATIC_ROOT = CERTEGO_STATIC_ROOT +SIMPLE_JWT = { + "ROTATE_REFRESH_TOKENS": False, + "BLACKLIST_AFTER_ROTATION": False, + "UPDATE_LAST_LOGIN": False, + "ACCESS_TOKEN_LIFETIME": timedelta(weeks=5), +} + +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": ( + # enables simple command line authentication + "rest_framework.authentication.BasicAuthentication", + "rest_framework.authentication.SessionAuthentication", + "rest_framework.authentication.TokenAuthentication", + "rest_framework_simplejwt.authentication.JWTAuthentication", + ), + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", + "PAGE_SIZE": 50, +} + +AUTH_USER_MODEL = "authentication.User" + +CORS_ALLOWED_ORIGINS = [ + "http://localhost:3000", +] + +ALLOWED_HOSTS = ["*"] +CORS_ORIGIN_ALLOW_ALL = True +CORS_ALLOW_HEADERS = ["*"] + + # Default primary key field type # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field diff --git a/buffalogs/buffalogs/urls.py b/buffalogs/buffalogs/urls.py index 51d6dbf..e3ca249 100644 --- a/buffalogs/buffalogs/urls.py +++ b/buffalogs/buffalogs/urls.py @@ -14,8 +14,9 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import include, path from impossible_travel import views +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView urlpatterns = [ path("", views.homepage, name="homepage"), @@ -30,4 +31,5 @@ path("users//all_logins", views.all_logins, name="all_logins"), path("users//alerts/get_alerts", views.get_alerts, name="get_alerts"), path("users//alerts", views.alerts, name="alerts"), + path("authentication/", include("authentication.urls")), ] diff --git a/buffalogs/requirements.txt b/buffalogs/requirements.txt index 2244b7a..ceee07f 100644 --- a/buffalogs/requirements.txt +++ b/buffalogs/requirements.txt @@ -11,6 +11,9 @@ click-plugins==1.1.1 click-repl==0.2.0 distlib==0.3.6 Django==4.1.4 +djangorestframework +djangorestframework-simplejwt +django-cors-headers django-environ==0.9.0 elasticsearch==7.17.7 elasticsearch-dsl==7.4.0 @@ -29,7 +32,7 @@ pathspec==0.10.3 platformdirs==2.6.0 pre-commit==2.21.0 prompt-toolkit==3.0.33 -psycopg2==2.9.5 +psycopg2==2.9.3 pycodestyle==2.9.1 pyflakes==2.5.0 pygal==3.0.0 diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/frontend/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..8f322f0 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..2a3946e --- /dev/null +++ b/frontend/README.md @@ -0,0 +1 @@ +frontend for buffalogs. \ No newline at end of file diff --git a/frontend/components/ui/Form.tsx b/frontend/components/ui/Form.tsx new file mode 100644 index 0000000..4603f8b --- /dev/null +++ b/frontend/components/ui/Form.tsx @@ -0,0 +1,176 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +