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