From 94a77f9baca7134ad12a41998306b7d59dd4e548 Mon Sep 17 00:00:00 2001 From: hashlash <muh.ashlah@gmail.com> Date: Thu, 17 Sep 2020 15:40:34 +0700 Subject: [PATCH 1/4] [RED] add graphql auth tests --- requirements.txt | 3 + sizakat/account/__init__.py | 0 sizakat/account/migrations/0001_initial.py | 44 +++++ sizakat/account/migrations/__init__.py | 0 sizakat/account/models.py | 5 + sizakat/account/tests/__init__.py | 0 sizakat/account/tests/test_auth.py | 186 +++++++++++++++++++++ sizakat/account/tests/test_password.py | 152 +++++++++++++++++ sizakat/account/tests/test_registration.py | 54 ++++++ 9 files changed, 444 insertions(+) create mode 100644 sizakat/account/__init__.py create mode 100644 sizakat/account/migrations/0001_initial.py create mode 100644 sizakat/account/migrations/__init__.py create mode 100644 sizakat/account/models.py create mode 100644 sizakat/account/tests/__init__.py create mode 100644 sizakat/account/tests/test_auth.py create mode 100644 sizakat/account/tests/test_password.py create mode 100644 sizakat/account/tests/test_registration.py diff --git a/requirements.txt b/requirements.txt index cb089ee..703b030 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,10 @@ asgiref==3.2.10 coverage==5.2.1 Django==3.0.7 django-cors-headers==3.4.0 +django-filter==2.3.0 +django-graphql-auth==0.3.11 django-graphql-jwt==0.3.1 +freezegun==1.0.0 graphene-django==2.10.1 gunicorn==20.0.4 psycopg2-binary==2.8.5 diff --git a/sizakat/account/__init__.py b/sizakat/account/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sizakat/account/migrations/0001_initial.py b/sizakat/account/migrations/0001_initial.py new file mode 100644 index 0000000..83ee5ba --- /dev/null +++ b/sizakat/account/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# Generated by Django 3.0.7 on 2020-09-01 10:04 + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0011_update_proxy_permissions'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.AutoField(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(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('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={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/sizakat/account/migrations/__init__.py b/sizakat/account/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sizakat/account/models.py b/sizakat/account/models.py new file mode 100644 index 0000000..3d30525 --- /dev/null +++ b/sizakat/account/models.py @@ -0,0 +1,5 @@ +from django.contrib.auth.models import AbstractUser + + +class User(AbstractUser): + pass diff --git a/sizakat/account/tests/__init__.py b/sizakat/account/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sizakat/account/tests/test_auth.py b/sizakat/account/tests/test_auth.py new file mode 100644 index 0000000..13163f6 --- /dev/null +++ b/sizakat/account/tests/test_auth.py @@ -0,0 +1,186 @@ +import json +from datetime import datetime, timedelta + +from django.contrib.auth import get_user_model +from freezegun import freeze_time +from graphene_django.utils.testing import GraphQLTestCase +from graphql_auth.constants import Messages + +from sizakat.schema import schema + +User = get_user_model() + + +class UserAuthenticationTestCase(GraphQLTestCase): + GRAPHQL_SCHEMA = schema + + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create_user( + 'testuser', + 'test@mail.com', + 'supersecretpassword', + ) + + def test_login_with_username(self): + token_resp = self.query( + ''' + mutation { + tokenAuth( + username: "testuser" + password: "supersecretpassword" + ) { + token + } + } + ''' + ) + + token = json.loads(token_resp.content)['data']['tokenAuth']['token'] + + me_resp = self.query( + ''' + { + me { + username + email + } + } + ''', + headers={'HTTP_AUTHORIZATION': 'JWT {}'.format(token)}, + ) + + me = json.loads(me_resp.content)['data']['me'] + self.assertEqual(me['username'], 'testuser') + self.assertEqual(me['email'], 'test@mail.com') + + def test_login_with_email(self): + token_resp = self.query( + ''' + mutation { + tokenAuth( + email: "test@mail.com" + password: "supersecretpassword" + ) { + token + } + } + ''' + ) + + token = json.loads(token_resp.content)['data']['tokenAuth']['token'] + + me_resp = self.query( + ''' + { + me { + username + email + } + } + ''', + headers={'HTTP_AUTHORIZATION': 'JWT {}'.format(token)}, + ) + + me = json.loads(me_resp.content)['data']['me'] + self.assertEqual(me['username'], 'testuser') + self.assertEqual(me['email'], 'test@mail.com') + + def test_refresh_token(self): + token_resp = self.query( + ''' + mutation { + tokenAuth( + username: "testuser" + password: "supersecretpassword" + ) { + token + refreshToken + } + } + ''' + ) + + token_auth = json.loads(token_resp.content)['data']['tokenAuth'] + token = token_auth['token'] + refresh_token = token_auth['refreshToken'] + + with freeze_time(datetime.now() + timedelta(minutes=5, seconds=1), tick=True): + me_resp = self.query( + ''' + { + me { + username + } + } + ''', + headers={'HTTP_AUTHORIZATION': 'JWT {}'.format(token)}, + ) + + self.assertIsNone(json.loads(me_resp.content)['data']['me']) + + new_token_resp = self.query( + ''' + mutation {{ + refreshToken(refreshToken: "{}") {{ + token + }} + }} + '''.format(refresh_token) + ) + + new_token = json.loads(new_token_resp.content)['data']['refreshToken']['token'] + + new_me_resp = self.query( + ''' + { + me { + username + } + } + ''', + headers={'HTTP_AUTHORIZATION': 'JWT {}'.format(new_token)}, + ) + + self.assertEqual(json.loads(new_me_resp.content)['data']['me']['username'], 'testuser') + + def test_revoke_refresh_token(self): + token_resp = self.query( + ''' + mutation { + tokenAuth( + username: "testuser" + password: "supersecretpassword" + ) { + refreshToken + } + } + ''' + ) + + refresh_token = json.loads(token_resp.content)['data']['tokenAuth']['refreshToken'] + + revoke_resp = self.query( + ''' + mutation {{ + revokeToken(refreshToken: "{}") {{ + success + }} + }} + '''.format(refresh_token) + ) + + self.assertTrue(json.loads(revoke_resp.content)['data']['revokeToken']['success']) + + refresh_resp = self.query( + ''' + mutation {{ + refreshToken(refreshToken: "{}") {{ + errors + }} + }} + '''.format(refresh_token) + ) + + errors = json.loads(refresh_resp.content)['data']['refreshToken']['errors'] + self.assertEqual(errors['nonFieldErrors'], Messages.INVALID_TOKEN) diff --git a/sizakat/account/tests/test_password.py b/sizakat/account/tests/test_password.py new file mode 100644 index 0000000..705ade8 --- /dev/null +++ b/sizakat/account/tests/test_password.py @@ -0,0 +1,152 @@ +import json +import re + +from django.contrib.auth import get_user_model +from django.core import mail +from graphene_django.utils.testing import GraphQLTestCase +from graphql_auth.constants import Messages + +from sizakat.schema import schema + +User = get_user_model() + + +class UserPasswordTestCase(GraphQLTestCase): + GRAPHQL_SCHEMA = schema + + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create_user( + 'testuser', + 'test@mail.com', + 'supersecretpassword', + ) + cls.user.status.verified = True + cls.user.status.save() + + def test_password_change(self): + token_resp = self.query( + ''' + mutation { + tokenAuth( + username: "testuser" + password: "supersecretpassword" + ) { + token + } + } + ''' + ) + + token = json.loads(token_resp.content)['data']['tokenAuth']['token'] + + pwchange_resp = self.query( + ''' + mutation { + passwordChange( + oldPassword: "supersecretpassword" + newPassword1: "abrandnewpassword" + newPassword2: "abrandnewpassword" + ) { + success + } + } + ''', + headers={'HTTP_AUTHORIZATION': 'JWT {}'.format(token)}, + ) + + self.assertTrue(json.loads(pwchange_resp.content)['data']['passwordChange']['success']) + + old_auth_resp = self.query( + ''' + mutation { + tokenAuth( + username: "testuser" + password: "supersecretpassword" + ) { + errors + } + } + ''' + ) + + errors = json.loads(old_auth_resp.content)['data']['tokenAuth']['errors'] + self.assertEqual(errors['nonFieldErrors'], Messages.INVALID_CREDENTIALS) + + new_auth_resp = self.query( + ''' + mutation { + tokenAuth( + username: "testuser" + password: "abrandnewpassword" + ) { + success + } + } + ''' + ) + + self.assertTrue(json.loads(new_auth_resp.content)['data']['tokenAuth']['success']) + + def test_reset_password(self): + sendreset_resp = self.query( + ''' + mutation { + sendPasswordResetEmail(email: "test@mail.com") { + success + } + } + ''' + ) + + self.assertTrue(json.loads(sendreset_resp.content)['data']['sendPasswordResetEmail']['success']) + + token_regex = '[A-z0-9-_]+:[A-z0-9-_]+:[A-z0-9-_]+' + token = re.search(token_regex, mail.outbox[0].body).group() + + pwreset_resp = self.query( + ''' + mutation {{ + passwordReset( + token: "{}" + newPassword1: "abrandnewpassword" + newPassword2: "abrandnewpassword" + ) {{ + success + }} + }} + '''.format(token) + ) + + self.assertTrue(json.loads(pwreset_resp.content)['data']['passwordReset']['success']) + + old_auth_resp = self.query( + ''' + mutation { + tokenAuth( + username: "testuser" + password: "supersecretpassword" + ) { + errors + } + } + ''' + ) + + errors = json.loads(old_auth_resp.content)['data']['tokenAuth']['errors'] + self.assertEqual(errors['nonFieldErrors'], Messages.INVALID_CREDENTIALS) + + new_auth_resp = self.query( + ''' + mutation { + tokenAuth( + username: "testuser" + password: "abrandnewpassword" + ) { + success + } + } + ''' + ) + + self.assertTrue(json.loads(new_auth_resp.content)['data']['tokenAuth']['success']) diff --git a/sizakat/account/tests/test_registration.py b/sizakat/account/tests/test_registration.py new file mode 100644 index 0000000..35929c9 --- /dev/null +++ b/sizakat/account/tests/test_registration.py @@ -0,0 +1,54 @@ +import json +import re + +from django.contrib.auth import get_user_model +from django.core import mail +from graphene_django.utils.testing import GraphQLTestCase + +from sizakat.schema import schema + +User = get_user_model() + + +class UserRegistrationTestCase(GraphQLTestCase): + GRAPHQL_SCHEMA = schema + + def test_register_with_verification(self): + register_resp = self.query( + ''' + mutation { + register( + username: "testuser" + email: "test@mail.com" + password1: "supersecretpassword" + password2: "supersecretpassword" + ) { + success + } + } + ''' + ) + + self.assertTrue(json.loads(register_resp.content)['data']['register']['success']) + + user = User.objects.get(username='testuser', email='test@mail.com') + self.assertFalse(user.status.verified) + + token_regex = '[A-z0-9-_]+:[A-z0-9-_]+:[A-z0-9-_]+' + token = re.search(token_regex, mail.outbox[0].body).group() + + verify_resp = self.query( + ''' + mutation {{ + verifyAccount(token: "{}") {{ + success + errors + }} + }} + '''.format(token) + ) + + self.assertTrue(json.loads(verify_resp.content)['data']['verifyAccount']['success']) + + user.status.refresh_from_db() + self.assertTrue(user.status.verified) -- GitLab From d0fb72c32d9be06e19b56a6b54106c8198d7fcc1 Mon Sep 17 00:00:00 2001 From: hashlash <muh.ashlah@gmail.com> Date: Thu, 17 Sep 2020 15:42:26 +0700 Subject: [PATCH 2/4] [GREEN] add graphql authentication --- sizakat/account/admin.py | 5 +++ sizakat/account/mutations.py | 24 ++++++++++++ sizakat/account/query.py | 5 +++ sizakat/schema.py | 6 ++- sizakat/settings.py | 71 +++++++++++++++++++++++++++++++++--- 5 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 sizakat/account/admin.py create mode 100644 sizakat/account/mutations.py create mode 100644 sizakat/account/query.py diff --git a/sizakat/account/admin.py b/sizakat/account/admin.py new file mode 100644 index 0000000..f91be8f --- /dev/null +++ b/sizakat/account/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from .models import User + +admin.site.register(User, UserAdmin) diff --git a/sizakat/account/mutations.py b/sizakat/account/mutations.py new file mode 100644 index 0000000..23bde20 --- /dev/null +++ b/sizakat/account/mutations.py @@ -0,0 +1,24 @@ +import graphene + +from graphql_auth import mutations + +class AccountMutation(graphene.ObjectType): + register = mutations.Register.Field() + verify_account = mutations.VerifyAccount.Field() + resend_activation_email = mutations.ResendActivationEmail.Field() + send_password_reset_email = mutations.SendPasswordResetEmail.Field() + password_reset = mutations.PasswordReset.Field() + password_change = mutations.PasswordChange.Field() + update_account = mutations.UpdateAccount.Field() + archive_account = mutations.ArchiveAccount.Field() + delete_account = mutations.DeleteAccount.Field() + send_secondary_email_activation = mutations.SendSecondaryEmailActivation.Field() + verify_secondary_email = mutations.VerifySecondaryEmail.Field() + swap_emails = mutations.SwapEmails.Field() + remove_secondary_email = mutations.RemoveSecondaryEmail.Field() + + # django-graphql-jwt inheritances + token_auth = mutations.ObtainJSONWebToken.Field() + verify_token = mutations.VerifyToken.Field() + refresh_token = mutations.RefreshToken.Field() + revoke_token = mutations.RevokeToken.Field() diff --git a/sizakat/account/query.py b/sizakat/account/query.py new file mode 100644 index 0000000..9735bba --- /dev/null +++ b/sizakat/account/query.py @@ -0,0 +1,5 @@ +import graphene +from graphql_auth.schema import UserQuery, MeQuery + +class AccountQuery(UserQuery, MeQuery, graphene.ObjectType): + pass diff --git a/sizakat/schema.py b/sizakat/schema.py index fabda0e..bdc74c7 100644 --- a/sizakat/schema.py +++ b/sizakat/schema.py @@ -2,6 +2,8 @@ import graphene from graphene_django import DjangoObjectType +from .account.mutations import AccountMutation +from .account.query import AccountQuery from .mustahik.mutations import ( MustahikMutation, DeleteMustahik, DataSourceMutation, DataSourceWargaMutation, DataSourceInstitusiMutation, @@ -18,14 +20,14 @@ ABOUT = ('Si Zakat merupakan sistem informasi untuk membantu masjid dalam ' 'yang dipimpin oleh Prof. Dr. Wisnu Jatmiko.') -class Query(MustahikQuery, TransactionQuery, graphene.ObjectType): +class Query(AccountQuery, MustahikQuery, TransactionQuery, graphene.ObjectType): about = graphene.String() def resolve_about(self, info): return ABOUT -class Mutation(graphene.ObjectType): +class Mutation(AccountMutation, graphene.ObjectType): mustahik_mutation = MustahikMutation.Field() delete_mustahik = DeleteMustahik.Field() data_source_mutation = DataSourceMutation.Field() diff --git a/sizakat/settings.py b/sizakat/settings.py index a3e25ad..cd1d75e 100644 --- a/sizakat/settings.py +++ b/sizakat/settings.py @@ -37,16 +37,16 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'graphene_django', 'corsheaders', + 'django_filters', + 'graphene_django', + 'graphql_auth', + 'graphql_jwt.refresh_token.apps.RefreshTokenConfig', + 'sizakat.account', 'sizakat.mustahik', 'sizakat.transaction', ] -GRAPHENE = { - 'SCHEMA': 'sizakat.schema.schema', -} - MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -66,7 +66,7 @@ ROOT_URLCONF = 'sizakat.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': [os.path.join(BASE_DIR, "templates")], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -103,6 +103,13 @@ if 'POSTGRES_DB' in os.environ: } } + +# User Model +# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-user-model + +AUTH_USER_MODEL = 'account.User' + + # Password validation # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators @@ -122,6 +129,15 @@ AUTH_PASSWORD_VALIDATORS = [ ] +# Authentication Backend +# https://docs.djangoproject.com/en/3.0/ref/settings/#authentication-backends + +AUTHENTICATION_BACKENDS = [ + 'graphql_auth.backends.GraphQLAuthBackend', + 'django.contrib.auth.backends.ModelBackend', +] + + # Internationalization # https://docs.djangoproject.com/en/3.0/topics/i18n/ @@ -143,3 +159,46 @@ STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') MEDIA_URL = '/img/' MEDIA_ROOT = os.path.join(BASE_DIR, 'images') + + +# Email COnfigurations +# https://docs.djangoproject.com/en/3.0/topics/email/ + +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = 'smtp.gmail.com' +EMAIL_PORT = 587 +EMAIL_USE_TLS = True +EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER') +EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD') + + +# Django Graphene +# https://docs.graphene-python.org/projects/django/en/latest/settings/ + +GRAPHENE = { + 'SCHEMA': 'sizakat.schema.schema', + 'MIDDLEWARE': [ + 'graphql_jwt.middleware.JSONWebTokenMiddleware', + ], +} + + +# Django GraphQL JWT +# https://django-graphql-jwt.domake.io/en/latest/settings.html + +GRAPHQL_JWT = { + "JWT_ALLOW_ANY_CLASSES": [ + "graphql_auth.mutations.Register", + "graphql_auth.mutations.VerifyAccount", + "graphql_auth.mutations.ResendActivationEmail", + "graphql_auth.mutations.SendPasswordResetEmail", + "graphql_auth.mutations.PasswordReset", + "graphql_auth.mutations.ObtainJSONWebToken", + "graphql_auth.mutations.VerifyToken", + "graphql_auth.mutations.RefreshToken", + "graphql_auth.mutations.RevokeToken", + "graphql_auth.mutations.VerifySecondaryEmail", + ], + 'JWT_VERIFY_EXPIRATION': True, + 'JWT_LONG_RUNNING_REFRESH_TOKEN': True, +} -- GitLab From 5c32d312dcca4ac83a5ca6559a0f539e1be5cb0f Mon Sep 17 00:00:00 2001 From: Muhammad Ashlah Shinfain <muhammad.ashlah@ui.ac.id> Date: Fri, 18 Sep 2020 09:57:40 +0700 Subject: [PATCH 3/4] [CHORES] fix code style --- sizakat/account/migrations/0001_initial.py | 42 +++++++++-- sizakat/account/mutations.py | 3 +- sizakat/account/query.py | 1 + sizakat/schema.py | 86 +++++++++++----------- 4 files changed, 81 insertions(+), 51 deletions(-) diff --git a/sizakat/account/migrations/0001_initial.py b/sizakat/account/migrations/0001_initial.py index 83ee5ba..d148a87 100644 --- a/sizakat/account/migrations/0001_initial.py +++ b/sizakat/account/migrations/0001_initial.py @@ -21,16 +21,46 @@ class Migration(migrations.Migration): ('id', models.AutoField(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(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('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( + error_messages={'unique': 'A user with that username already exists.'}, + help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', + max_length=150, unique=True, + validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], + verbose_name='username', + )), ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('is_staff', models.BooleanField( + default=False, + help_text='Designates whether the user can log into this admin site.', + verbose_name='staff status', + )), + ('is_active', models.BooleanField( + default=True, + help_text=('Designates whether this user should be treated as active. Unselect this instead of ' + 'deleting accounts.'), + verbose_name='active', + )), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('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')), + ('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={ 'verbose_name': 'user', diff --git a/sizakat/account/mutations.py b/sizakat/account/mutations.py index 23bde20..bc74cd1 100644 --- a/sizakat/account/mutations.py +++ b/sizakat/account/mutations.py @@ -2,6 +2,7 @@ import graphene from graphql_auth import mutations + class AccountMutation(graphene.ObjectType): register = mutations.Register.Field() verify_account = mutations.VerifyAccount.Field() @@ -12,7 +13,7 @@ class AccountMutation(graphene.ObjectType): update_account = mutations.UpdateAccount.Field() archive_account = mutations.ArchiveAccount.Field() delete_account = mutations.DeleteAccount.Field() - send_secondary_email_activation = mutations.SendSecondaryEmailActivation.Field() + send_secondary_email_activation = mutations.SendSecondaryEmailActivation.Field() verify_secondary_email = mutations.VerifySecondaryEmail.Field() swap_emails = mutations.SwapEmails.Field() remove_secondary_email = mutations.RemoveSecondaryEmail.Field() diff --git a/sizakat/account/query.py b/sizakat/account/query.py index 9735bba..f2a8cfd 100644 --- a/sizakat/account/query.py +++ b/sizakat/account/query.py @@ -1,5 +1,6 @@ import graphene from graphql_auth.schema import UserQuery, MeQuery + class AccountQuery(UserQuery, MeQuery, graphene.ObjectType): pass diff --git a/sizakat/schema.py b/sizakat/schema.py index bdc74c7..7205ad4 100644 --- a/sizakat/schema.py +++ b/sizakat/schema.py @@ -1,44 +1,42 @@ -import graphene - -from graphene_django import DjangoObjectType - -from .account.mutations import AccountMutation -from .account.query import AccountQuery -from .mustahik.mutations import ( - MustahikMutation, DeleteMustahik, DataSourceMutation, - DataSourceWargaMutation, DataSourceInstitusiMutation, - DataSourcePekerjaMutation, DeleteDataSource -) -from .mustahik.query import MustahikQuery -from .transaction.query import TransactionQuery -from .transaction.mutations import ( - MuzakkiMutation, TransactionMutation, ZakatTransactionMutation -) - -ABOUT = ('Si Zakat merupakan sistem informasi untuk membantu masjid dalam ' - 'mengelola transaksi zakat. Sistem ini dibuat oleh tim lab 1231, ' - 'yang dipimpin oleh Prof. Dr. Wisnu Jatmiko.') - - -class Query(AccountQuery, MustahikQuery, TransactionQuery, graphene.ObjectType): - about = graphene.String() - - def resolve_about(self, info): - return ABOUT - - -class Mutation(AccountMutation, graphene.ObjectType): - mustahik_mutation = MustahikMutation.Field() - delete_mustahik = DeleteMustahik.Field() - data_source_mutation = DataSourceMutation.Field() - data_source_warga_mutation = DataSourceWargaMutation.Field() - data_source_institusi_mutation = DataSourceInstitusiMutation.Field() - data_source_pekerja_mutation = DataSourcePekerjaMutation.Field() - delete_data_source = DeleteDataSource.Field() - - muzakki_mutation = MuzakkiMutation.Field() - transaction_mutation = TransactionMutation.Field() - zakat_transaction_mutation = ZakatTransactionMutation.Field() - - -schema = graphene.Schema(query=Query, mutation=Mutation) +import graphene + +from .account.mutations import AccountMutation +from .account.query import AccountQuery +from .mustahik.mutations import ( + MustahikMutation, DeleteMustahik, DataSourceMutation, + DataSourceWargaMutation, DataSourceInstitusiMutation, + DataSourcePekerjaMutation, DeleteDataSource +) +from .mustahik.query import MustahikQuery +from .transaction.query import TransactionQuery +from .transaction.mutations import ( + MuzakkiMutation, TransactionMutation, ZakatTransactionMutation +) + +ABOUT = ('Si Zakat merupakan sistem informasi untuk membantu masjid dalam ' + 'mengelola transaksi zakat. Sistem ini dibuat oleh tim lab 1231, ' + 'yang dipimpin oleh Prof. Dr. Wisnu Jatmiko.') + + +class Query(AccountQuery, MustahikQuery, TransactionQuery, graphene.ObjectType): + about = graphene.String() + + def resolve_about(self, info): + return ABOUT + + +class Mutation(AccountMutation, graphene.ObjectType): + mustahik_mutation = MustahikMutation.Field() + delete_mustahik = DeleteMustahik.Field() + data_source_mutation = DataSourceMutation.Field() + data_source_warga_mutation = DataSourceWargaMutation.Field() + data_source_institusi_mutation = DataSourceInstitusiMutation.Field() + data_source_pekerja_mutation = DataSourcePekerjaMutation.Field() + delete_data_source = DeleteDataSource.Field() + + muzakki_mutation = MuzakkiMutation.Field() + transaction_mutation = TransactionMutation.Field() + zakat_transaction_mutation = ZakatTransactionMutation.Field() + + +schema = graphene.Schema(query=Query, mutation=Mutation) -- GitLab From b5779a1642c3b27bccbe2b655249459fb72829ce Mon Sep 17 00:00:00 2001 From: hashlash <muh.ashlah@gmail.com> Date: Wed, 4 Nov 2020 15:39:19 +0700 Subject: [PATCH 4/4] add custom activation and password reset email template --- requirements.txt | 2 +- sizakat/settings.py | 18 ++++++++++++++---- sizakat/templates/email/activation_email.html | 7 +++++++ sizakat/templates/email/activation_subject.txt | 1 + .../templates/email/password_reset_email.html | 7 +++++++ .../templates/email/password_reset_subject.txt | 1 + 6 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 sizakat/templates/email/activation_email.html create mode 100644 sizakat/templates/email/activation_subject.txt create mode 100644 sizakat/templates/email/password_reset_email.html create mode 100644 sizakat/templates/email/password_reset_subject.txt diff --git a/requirements.txt b/requirements.txt index 703b030..1809097 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ coverage==5.2.1 Django==3.0.7 django-cors-headers==3.4.0 django-filter==2.3.0 -django-graphql-auth==0.3.11 +django-graphql-auth==0.3.14 django-graphql-jwt==0.3.1 freezegun==1.0.0 graphene-django==2.10.1 diff --git a/sizakat/settings.py b/sizakat/settings.py index cd1d75e..54484a9 100644 --- a/sizakat/settings.py +++ b/sizakat/settings.py @@ -15,7 +15,6 @@ import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ @@ -27,6 +26,8 @@ DEBUG = os.environ.get("DEBUG", False) ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS').split() +FRONTEND_BASE_URL = os.environ.get('FRONTEND_BASE_URL', 'http://localhost:3000') + # Application definition @@ -58,15 +59,14 @@ MIDDLEWARE = [ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] -CORS_ORIGIN_WHITELIST = os.environ.get( - 'CORS_ORIGIN_WHITELIST', 'http://localhost:3000').split() +CORS_ORIGIN_WHITELIST = os.environ.get('CORS_ORIGIN_WHITELIST', FRONTEND_BASE_URL).split() ROOT_URLCONF = 'sizakat.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, "templates")], + 'DIRS': [os.path.join(BASE_DIR, 'sizakat', 'templates')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -202,3 +202,13 @@ GRAPHQL_JWT = { 'JWT_VERIFY_EXPIRATION': True, 'JWT_LONG_RUNNING_REFRESH_TOKEN': True, } + + +# Django GraphQL Auth +# https://django-graphql-auth.readthedocs.io/en/latest/settings/ + +GRAPHQL_AUTH = { + 'EMAIL_TEMPLATE_VARIABLES': { + 'frontend_base_url': FRONTEND_BASE_URL, + } +} diff --git a/sizakat/templates/email/activation_email.html b/sizakat/templates/email/activation_email.html new file mode 100644 index 0000000..518f359 --- /dev/null +++ b/sizakat/templates/email/activation_email.html @@ -0,0 +1,7 @@ +<p>Assalamu'alaikum warrahmatullah wabarakatuh.</p> + +<p>Bapak/Ibu dapat mengaktifkan akun {{ user.username }} di Aplikasi SIZAKAT dengan klik link berikut:</p> + +<p>{{ frontend_base_url }}/{{ path }}/{{ token }}.</p> + +<p>Jazaakumullahu Khoiron.</p> diff --git a/sizakat/templates/email/activation_subject.txt b/sizakat/templates/email/activation_subject.txt new file mode 100644 index 0000000..56cb27f --- /dev/null +++ b/sizakat/templates/email/activation_subject.txt @@ -0,0 +1 @@ +[PENGAKTIFKAN AKUN SIZAKAT] diff --git a/sizakat/templates/email/password_reset_email.html b/sizakat/templates/email/password_reset_email.html new file mode 100644 index 0000000..900eca0 --- /dev/null +++ b/sizakat/templates/email/password_reset_email.html @@ -0,0 +1,7 @@ +<p>Assalamu'alaikum warrahmatullah wabarakatuh.</p> + +<p>Bapak/Ibu dapat membuat ulang password untuk akun {{ user.username }} di Aplikasi SIZAKAT dengan klik link berikut:</p> + +<p>{{ frontend_base_url }}/{{ path }}/{{ token }}</p> + +<p>Jazaakumullahu Khoiron.</p> diff --git a/sizakat/templates/email/password_reset_subject.txt b/sizakat/templates/email/password_reset_subject.txt new file mode 100644 index 0000000..f7e7ffd --- /dev/null +++ b/sizakat/templates/email/password_reset_subject.txt @@ -0,0 +1 @@ +[RESET PASSWORD SIZAKAT] -- GitLab