diff --git a/backend/assets/accounts/auth.css b/backend/assets/accounts/auth.css new file mode 100644 index 0000000..0fb80f7 --- /dev/null +++ b/backend/assets/accounts/auth.css @@ -0,0 +1,4 @@ +* { + outline: 1px red solid; + } + \ No newline at end of file diff --git a/backend/core/settings.py b/backend/core/settings.py index b9a05b1..0bdc71f 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -10,6 +10,7 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.2/ref/settings/ """ +import os from pathlib import Path from decouple import config, Csv @@ -31,7 +32,12 @@ ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='*', cast=Csv()) # Application definition -SITE_ID = 2 +SITE_ID = 3 + +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', + 'allauth.account.auth_backends.AuthenticationBackend', +) INSTALLED_APPS = [ 'django.contrib.admin', @@ -40,25 +46,52 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'users', + 'rest_framework', + 'corsheaders', + 'django.contrib.sites', 'allauth', 'allauth.account', 'allauth.socialaccount', 'allauth.socialaccount.providers.google', + + 'dj_rest_auth', + 'dj_rest_auth.registration', + 'rest_framework.authtoken', ] +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated', + + ], + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework_simplejwt.authentication.JWTAuthentication', + 'dj_rest_auth.jwt_auth.JWTCookieAuthentication', + ] +} + +REST_USE_JWT = True + SOCIALACCOUNT_PROVIDERS = { - 'google' : { - 'SCOPE' : [ - 'profile', - 'email', - ], - 'AUTH_PARAMS' : {'access_type' : 'online'} + 'google': { + 'APP': { + 'client_id': config('GOOGLE_CLIENT_ID'), + 'secret': config('GOOGLE_CLIENT_SECRET'), + 'key': '' + } } } +CORS_ALLOWED_ORIGINS = [ + "http://localhost:8000", + "http://127.0.0.1:8000" +] + MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', @@ -74,7 +107,9 @@ ROOT_URLCONF = 'core.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': [ + os.path.join(BASE_DIR, 'templates') + ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -146,10 +181,13 @@ STATIC_URL = 'static/' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', - 'allauth.account.auth_backends.AuthenticationBackend', -) + +SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [ + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', +] LOGIN_REDIRECT_URL = '/' -LOGOUT_REDIRECT_URL = '/' \ No newline at end of file +LOGOUT_REDIRECT_URL = '/' + +AUTH_USER_MODEL = "users.CustomUser" \ No newline at end of file diff --git a/backend/core/urls.py b/backend/core/urls.py index 1e308bb..5e07d78 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -19,6 +19,6 @@ from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), + path('api/', include('users.urls')), path('accounts/', include('allauth.urls')), - path('', include('users.urls')), -] +] \ No newline at end of file diff --git a/backend/sample.env b/backend/sample.env index 1b58118..15b50eb 100644 --- a/backend/sample.env +++ b/backend/sample.env @@ -5,4 +5,6 @@ DB_NAME=your_DB_NAME DB_USER=your_DB_USER DB_PASSWORD=your_DB_PASSWORD DB_HOST=your_DB_HOST -DB_PORT=your_DB_PORT \ No newline at end of file +DB_PORT=your_DB_PORT +GOOGLE_CLIENT_ID=your_GOOGLE_CLIENT_ID +GOOGLE_CLIENT_SECRET=your_GOOGLE_CLIENT_SECRET \ No newline at end of file diff --git a/backend/users/admin.py b/backend/users/admin.py index 8c38f3f..d2e3674 100644 --- a/backend/users/admin.py +++ b/backend/users/admin.py @@ -1,3 +1,32 @@ from django.contrib import admin +from users.models import CustomUser +from django.contrib.auth.admin import UserAdmin +from django.forms import Textarea +from django.db import models -# Register your models here. +class UserAdminConfig(UserAdmin): + model = CustomUser + + search_fields = ('email', 'username', 'first_name',) + list_filter = ('email', 'username', 'first_name', 'is_active', 'is_staff') + ordering = ('-start_date',) + list_display = ('email', 'username', 'first_name', 'is_active', 'is_staff') + fieldsets = ( + (None, {'fields': ('email', 'username', 'first_name',)}), + ('Permissions', {'fields': ('is_staff', 'is_active')}), + ('Personal', {'fields': ('about',)}), + ) + + # Formfield overrides for 'about' field + formfield_overrides = { + models.TextField: {'widget': Textarea(attrs={'rows': 20, 'cols': 60})}, + } + + add_fieldsets = ( + (None, { + 'classes': ('wide',), + 'fields': ('email', 'username', 'first_name', 'password1', 'password2', 'is_active', 'is_staff') + }), + ) + +admin.site.register(CustomUser, UserAdminConfig) diff --git a/backend/users/managers.py b/backend/users/managers.py new file mode 100644 index 0000000..566f3cf --- /dev/null +++ b/backend/users/managers.py @@ -0,0 +1,31 @@ +from django.utils.translation import gettext_lazy as _ +from django.contrib.auth.models import BaseUserManager + +class CustomAccountManager(BaseUserManager): + def create_superuser(self, email, username, first_name, password, **other_fields): + """ + Create a superuser with the given email, username, first_name, and password. + """ + other_fields.setdefault('is_staff', True) + other_fields.setdefault('is_superuser', True) + other_fields.setdefault('is_active', True) + + if other_fields.get('is_staff') is not True: + raise ValueError('Superuser must be assigned to is_staff=True.') + if other_fields.get('is_superuser') is not True: + raise ValueError('Superuser must be assigned to is_superuser=True.') + + return self.create_user(email, username, first_name, password, **other_fields) + + def create_user(self, email, username, first_name, password, **other_fields): + """ + Create a user with the given email, username, first_name, and password. + """ + if not email: + raise ValueError(_('You must provide an email address')) + + email = self.normalize_email(email) + user = self.model(email=email, username=username, first_name=first_name, **other_fields) + user.set_password(password) + user.save() + return user diff --git a/backend/users/migrations/0001_initial.py b/backend/users/migrations/0001_initial.py new file mode 100644 index 0000000..be4ffe7 --- /dev/null +++ b/backend/users/migrations/0001_initial.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.6 on 2023-10-27 13:09 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='CustomUser', + 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')), + ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')), + ('username', models.CharField(max_length=150, unique=True)), + ('first_name', models.CharField(blank=True, max_length=150)), + ('start_date', models.DateTimeField(default=django.utils.timezone.now)), + ('about', models.TextField(blank=True, max_length=500, verbose_name='about')), + ('is_staff', models.BooleanField(default=False)), + ('is_active', models.BooleanField(default=True)), + ('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/backend/users/models.py b/backend/users/models.py index 71a8362..03d6e12 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -1,3 +1,28 @@ from django.db import models +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ +from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin -# Create your models here. +from .managers import CustomAccountManager + + +class CustomUser(AbstractBaseUser, PermissionsMixin): + # User fields + email = models.EmailField(_('email address'), unique=True) + username = models.CharField(max_length=150, unique=True) + first_name = models.CharField(max_length=150, blank=True) + start_date = models.DateTimeField(default=timezone.now) + about = models.TextField(_('about'), max_length=500, blank=True) + is_staff = models.BooleanField(default=False) + is_active = models.BooleanField(default=True) + + # Custom manager + objects = CustomAccountManager() + + # Fields for authentication + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['username', 'first_name'] + + def __str__(self): + # String representation of the user + return self.username diff --git a/backend/users/serializers.py b/backend/users/serializers.py new file mode 100644 index 0000000..d6deaa5 --- /dev/null +++ b/backend/users/serializers.py @@ -0,0 +1,39 @@ +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer +from rest_framework import serializers +from .models import CustomUser + + +class MyTokenObtainPairSerializer(TokenObtainPairSerializer): + @classmethod + def get_token(cls, user): + """ + Get the token for the user and add custom claims, such as 'username'. + """ + token = super(MyTokenObtainPairSerializer, cls).get_token(user) + token['username'] = user.username + return token + + +class CustomUserSerializer(serializers.ModelSerializer): + """ + Serializer for CustomUser model. + """ + email = serializers.EmailField(required=True) + username = serializers.CharField(required=True) + password = serializers.CharField(min_length=8, write_only=True) + + class Meta: + model = CustomUser + fields = ('email', 'username', 'password') + extra_kwargs = {'password': {'write_only': True}} + + def create(self, validated_data): + """ + Create a CustomUser instance with validated data, including password hashing. + """ + password = validated_data.pop('password', None) + instance = self.Meta.model(**validated_data) + if password is not None: + instance.set_password(password) + instance.save() + return instance diff --git a/backend/users/templates/users/home.html b/backend/users/templates/users/home.html deleted file mode 100644 index f6a48ec..0000000 --- a/backend/users/templates/users/home.html +++ /dev/null @@ -1,14 +0,0 @@ - - -
- - - -