From 0789752dc66b77dd6648a5a13ef2e5eba0421a97 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 17 Apr 2026 04:26:07 +0000 Subject: [PATCH] Auto commit: 2026-04-17T04:26:07.442Z --- config/__pycache__/settings.cpython-311.pyc | Bin 5707 -> 6111 bytes config/settings.py | 5 + core/__pycache__/forms.cpython-311.pyc | Bin 12104 -> 12489 bytes core/__pycache__/jwt_auth.cpython-311.pyc | Bin 0 -> 7198 bytes core/__pycache__/models.cpython-311.pyc | Bin 10951 -> 12454 bytes core/__pycache__/urls.cpython-311.pyc | Bin 2067 -> 2274 bytes core/__pycache__/views.cpython-311.pyc | Bin 30507 -> 36763 bytes core/forms.py | 13 +- core/jwt_auth.py | 112 +++++++++ ...02_businessprofile_mobile_token_version.py | 18 ++ ...ofile_mobile_token_version.cpython-311.pyc | Bin 0 -> 843 bytes core/models.py | 21 ++ core/urls.py | 3 + core/views.py | 230 +++++++++++++++--- requirements.txt | 1 + 15 files changed, 368 insertions(+), 35 deletions(-) create mode 100644 core/__pycache__/jwt_auth.cpython-311.pyc create mode 100644 core/jwt_auth.py create mode 100644 core/migrations/0002_businessprofile_mobile_token_version.py create mode 100644 core/migrations/__pycache__/0002_businessprofile_mobile_token_version.cpython-311.pyc diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index f942531228ecd43acd3e249cfe73c399d5b3fead..9a65d4dc191f34ea440b742ac233d0dd4fa3ad7a 100644 GIT binary patch delta 468 zcmX@Db6=ltIWI340}$L?_%Jh8a3Y@s(=67F8ZTJHm>5#kftWc9EDchU;-2C$xq(?$ z+!%|TCq&NQ1Wk`uiuW=m28Pu@3;|K5XexYCe9=^xrT9ger!WUIX!>uKVx7cQe@ntE zJS5)H+1WKXI6lPR+tn}L*V8XF#5MR9ld-`qF_3JKt6Pw3um@Dy#W6DY78gj|GdMWZ zHRu*sZhmflPHIYeYSAqouxX(#p00k*u2q5v8Qt9cq|BVuD%q5*#Ju!;y_6)q-29Z( z$$R((7%euR<#%HeDRKpRqsSdZcz_5mATe23NRPV&$Y2EG;#ii=K0@!AOQq#z7|*Dj zQ8cIWf~NCju9>M^R7Z47m%MHW&GAukhP8@H`MTxxgR< JK}CW<+W|xUf&l;k delta 94 zcmcbwe_DrcIWI340}wFIe3%(7Fp*D!=^e{PjTbCjDeh6mDa^qPnjV{vu}OK1bG7h$W&u%?3k*UKR3r$L0094`8HNA= diff --git a/config/settings.py b/config/settings.py index c03dda4..04d1f85 100644 --- a/config/settings.py +++ b/config/settings.py @@ -152,4 +152,9 @@ LOGIN_URL = 'login' LOGIN_REDIRECT_URL = 'home' LOGOUT_REDIRECT_URL = 'home' +JWT_ACCESS_TOKEN_MINUTES = int(os.getenv('JWT_ACCESS_TOKEN_MINUTES', '30')) +JWT_REFRESH_TOKEN_DAYS = int(os.getenv('JWT_REFRESH_TOKEN_DAYS', '30')) +JWT_ISSUER = os.getenv('JWT_ISSUER', 'momoledger') +JWT_AUDIENCE = os.getenv('JWT_AUDIENCE', 'momoledger-mobile') + DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index 9ff84cf382612afd02b5d4b98e6dba9f2127a7f4..1d0225ec6fdf0b24d173f3f7b444e05e3b3080d9 100644 GIT binary patch delta 1216 zcmah|TWk|o5WTbZZj2o}5WBY5yH0Fs)VRTILi&&rsQQtj3J_A1HY!3T+1d+oaO~vn zA}F;~Xay-MeU1qdMJOO15fAmTmHO9;KN|G|Dob_EPy17eKO?Brk5=N&20|sMI?_Fw zGjsOt(d_KC9X}eO3&EhDiTHN@_u@#`*P%NRc^@z~i}UTO)BVJYAcqT)lajM7x(Y_6 zrthsiRi)tEH;7`l^Z?y(~{45cJ5}r-Gp;W}vd>SQf2xjmlEup+F3f4{Bp>6m57U(#- z;d&s0jy5Ol1l+9m>B0cb%#DMa{>|n+?hC)ugLqjVOA4EpmaRTw^N7AT+;(0O8gZ6F z49#=o>^qP8l-SvT2>gkP*Xrdl{YN4$j?Q5-Ik)hj6j)d+K zw3r+W{n^e|YWPobZx^|5whx>6;*?R^STsuoPRqqGqwxz9GZX}AM$ zSBlTzmPl8}1SttNiDO-M!rfb8{HsEXdE}@62mBqf-eQk|)vIKGi~Tn^-=|Ik%sLaR z7Xd!O-b_)MAsZY|WD7XuZX9{hHB~S+aLZELYs=Tr=iLx+*I2BLcF}^cIY_ zty{Dc{6jp{lS!YTy2MXXg>wFpfLq;^-M@XYM+1(3^rTf+^o3|;W%oyG%63KE$7`6% zZUJ;Qn1~Y^3q}(EG77eVxl4%gM@@gI7oaHXiO_o*sbDYC6It< zL?sM9FvNfwHEL9{@#@Kgj~F5GU>Y&uNK8x(9z6Qbw%|dHll_7V^`1(-J zgY0ZG;|3E8;Xc<)&XP?y35*TnRF*?4%HIq|+azeM1-VcIaZ}p~CEOKKly;t?8$AVn zEc`6(oXR(oT_uq>&DFjI~9;0gfiVEdUD8ofsO7!S(~hh=qLsgnk6SB{Tc}&13yTs@vGDTXKo{% zW|UVlC2!doW4<8S9?g@uFJ7go5p;NJ-917~K^TV8K0~T zi`_It4>~xZ8X?b`3o(#zH-7h_Z$`cp;wF5j?Fd(pKb) zDEiiv+$7x~-y(&$e2K$i5wTSDeLg4j;89RWt6_a9 e@H_ewMnmI~@1+0u9jpg@8YsY*0TJ&6%6|cIpyqD? diff --git a/core/__pycache__/jwt_auth.cpython-311.pyc b/core/__pycache__/jwt_auth.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0e0dd5dcac968b092a00640687b3720b32879cdd GIT binary patch literal 7198 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 literal 0 HcmV?d00001 diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index fb3db03b115396ea7f7b4b5d27fb5bd370fda0ab..c0adb4c8669c5964a0c36df76eeed7a1d0802140 100644 GIT binary patch delta 3157 zcmai0Yj7J^72YdpWyz8)`61a_$+Gd${ip+a&m)#u&k8z$H0gi{ijtI@p08dd|aB%%h73vvx>u>acoF`iZ?Gz>Rp+CLH-pi|A39Q95a zdbE@Mu_VO)?Kn?5*tFO}YS<02jmX^kp?I>T3rJ?!sAec5Q>m1aj*n|}7cPY32s%2C z(GK>Eb0?A5Wv2`)-*NujyJhlRNhgW2@09d#wzEXz!2>129T(Gqcej6TX9 zbA>A&d%(5kdWtl%w_I⁣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 jy

ifhFJ~OTZPDfXyG+L>T#1h54BpxIwT;7Z_{+G-0OM delta 473 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 h3w+%oU$?9)Lv8@%t_KSKmM`+I+~B4h1LI.', code='auth_header_invalid') + + payload = decode_token(token.strip(), expected_type='access') + return get_user_from_payload(payload) + + +def authenticate_refresh_token(refresh_token: str): + if not refresh_token: + raise JWTAuthError('Refresh token is required.', code='refresh_required') + + payload = decode_token(refresh_token.strip(), expected_type='refresh') + return get_user_from_payload(payload) + + +def revoke_user_tokens(user: User): + profile, _ = BusinessProfile.objects.get_or_create(user=user) + profile.mobile_token_version += 1 + profile.save(update_fields=['mobile_token_version', 'updated_at']) + return profile diff --git a/core/migrations/0002_businessprofile_mobile_token_version.py b/core/migrations/0002_businessprofile_mobile_token_version.py new file mode 100644 index 0000000..636adc1 --- /dev/null +++ b/core/migrations/0002_businessprofile_mobile_token_version.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-04-17 02:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='businessprofile', + name='mobile_token_version', + field=models.PositiveIntegerField(default=1), + ), + ] diff --git a/core/migrations/__pycache__/0002_businessprofile_mobile_token_version.cpython-311.pyc b/core/migrations/__pycache__/0002_businessprofile_mobile_token_version.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1148ed54637618b9d34e60f39324649f0d880159 GIT binary patch literal 843 zcmZuuF>ljA6uz?^$FWjDtw;rx1qnetv<^%eDkK&V2r8sX3>MPK$z6j}+ZWDufh_|A zW46vLMW`M4DMiW<-G-PdF}O;nPP}vCA_#Z(yZ7$ByZ3$HeOXzt5sY7-e}rR<&`%Xq zqqcBn+u$4`iYSgyjC~wyzJ`&8o*=4yL{w*ZpjU^Ez&pN*e4QG*$gQ2Jw5v%w4*Md= zLY}13j5%dd+N(~gwY~^uJ~)SnAsjl zWeVMN%+WJ7#f_@Vj%&yU9Rx|AZ_#ckOex14$C@fDYj1*gX>HHdeJIu;RF9Ot0#oQ1 gW59;pQ?#-C6=oBg1zI*^dvg8Gxk_C8CtLRY4R)H{{r~^~ literal 0 HcmV?d00001 diff --git a/core/models.py b/core/models.py index e3297bd..c58636f 100644 --- a/core/models.py +++ b/core/models.py @@ -13,6 +13,7 @@ class BusinessProfile(models.Model): opening_physical_cash = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00')) current_ecash = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00')) current_physical_cash = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00')) + mobile_token_version = models.PositiveIntegerField(default=1) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -168,6 +169,11 @@ class Transaction(models.Model): ecash_after = cls._round(ecash_before + ecash_delta) physical_after = cls._round(physical_before + physical_delta) + if ecash_after < 0: + raise ValidationError('This change would make e-cash go below zero in your transaction history.') + if physical_after < 0: + raise ValidationError('This change would make physical cash go below zero in your transaction history.') + cls.objects.filter(pk=entry.pk).update( service_charge=fee, ecash_before=ecash_before, @@ -184,6 +190,21 @@ class Transaction(models.Model): business.save(update_fields=['current_ecash', 'current_physical_cash', 'updated_at']) return business + @transaction.atomic + def update_logged_transaction(self, *, client_name: str, amount: Decimal, transaction_type: str, notes: str = ''): + business = BusinessProfile.objects.select_for_update().get(pk=self.business_id) + self.client_name = client_name + self.amount = self.__class__._round(amount) + self.transaction_type = transaction_type + self.notes = notes + self.save(update_fields=['client_name', 'amount', 'transaction_type', 'notes']) + business = self.__class__.rebalance_business_ledger(business) + refreshed_entry = self.__class__.objects.select_related('created_by').get(pk=self.pk) + return { + 'transaction': refreshed_entry, + 'business': business, + } + @transaction.atomic def delete_logged_transaction(self): business = BusinessProfile.objects.select_for_update().get(pk=self.business_id) diff --git a/core/urls.py b/core/urls.py index 8a01ab5..29185be 100644 --- a/core/urls.py +++ b/core/urls.py @@ -5,6 +5,7 @@ from .views import ( api_login_view, api_logout_view, api_profile_view, + api_token_refresh_view, api_transaction_detail_view, api_transactions_view, home, @@ -23,6 +24,8 @@ urlpatterns = [ path('', home, name='home'), path('api/health/', api_health_view, name='api_health'), path('api/login/', api_login_view, name='api_login'), + path('api/token/', api_login_view, name='api_token_login'), + path('api/token/refresh/', api_token_refresh_view, name='api_token_refresh'), path('api/logout/', api_logout_view, name='api_logout'), path('api/profile/', api_profile_view, name='api_profile'), path('api/transactions/', api_transactions_view, name='api_transactions'), diff --git a/core/views.py b/core/views.py index 6c79b4c..e34b7a2 100644 --- a/core/views.py +++ b/core/views.py @@ -1,5 +1,6 @@ import json from datetime import datetime, time +from functools import wraps from io import BytesIO from django.contrib import messages @@ -14,15 +15,46 @@ from django.views.decorators.http import require_GET, require_POST, require_http from django.utils import timezone from .forms import BusinessProfileForm, LoginForm, ReportFilterForm, SignUpForm, TransactionForm +from .jwt_auth import ( + JWTAuthError, + authenticate_authorization_header, + authenticate_refresh_token, + issue_token_pair, + revoke_user_tokens, +) from .models import BusinessProfile, Transaction +def get_api_authenticated_user(request): + if request.user.is_authenticated: + return request.user + + authorization = (request.META.get('HTTP_AUTHORIZATION') or '').strip() + if not authorization: + return None + + user, _ = authenticate_authorization_header(authorization) + request.user = user + return user + + def api_login_required(view_func): + @wraps(view_func) def wrapped(request, *args, **kwargs): - if not request.user.is_authenticated: + try: + user = get_api_authenticated_user(request) + except JWTAuthError as exc: + return JsonResponse({ + 'ok': False, + 'error': exc.message, + 'code': exc.code, + }, status=exc.status_code) + + if user is None: return JsonResponse({ 'ok': False, 'error': 'Authentication required.', + 'code': 'auth_required', }, status=401) return view_func(request, *args, **kwargs) @@ -367,25 +399,62 @@ def api_health_view(request): 'app': 'MoMoLedger API', 'message': 'Android backend starter endpoints are available.', 'server_time': timezone.now().isoformat(), - 'authenticated': request.user.is_authenticated, + 'authenticated': bool(request.user.is_authenticated or (request.META.get('HTTP_AUTHORIZATION') or '').strip()), }) @csrf_exempt @require_POST def api_login_view(request): - if request.user.is_authenticated: - profile = get_profile(request.user) - return JsonResponse({ - 'ok': True, - 'message': 'Already logged in.', - 'user': { - 'username': request.user.username, - 'email': request.user.email, - 'business_name': profile.business_name, - }, - }) + user = request.user if request.user.is_authenticated else None + if user is None: + payload = parse_api_payload(request) + if payload is None: + return JsonResponse({ + 'ok': False, + 'error': 'Invalid JSON body.', + }, status=400) + + username = (payload.get('username') or '').strip() + password = payload.get('password') or '' + + if not username or not password: + return JsonResponse({ + 'ok': False, + 'error': 'Username and password are required.', + }, status=400) + + user = authenticate(request, username=username, password=password) + if user is None: + return JsonResponse({ + 'ok': False, + 'error': 'Invalid username or password.', + }, status=401) + + login(request, user) + message = 'Login successful.' + else: + message = 'Already logged in.' + + profile = get_profile(user) + tokens = issue_token_pair(user) + return JsonResponse({ + 'ok': True, + 'message': message, + 'user': { + 'id': user.id, + 'username': user.username, + 'email': user.email, + 'business_name': profile.business_name, + }, + 'tokens': tokens, + }) + + +@csrf_exempt +@require_POST +def api_token_refresh_view(request): payload = parse_api_payload(request) if payload is None: return JsonResponse({ @@ -393,49 +462,77 @@ def api_login_view(request): 'error': 'Invalid JSON body.', }, status=400) - username = (payload.get('username') or '').strip() - password = payload.get('password') or '' - - if not username or not password: + refresh_token = (payload.get('refresh_token') or payload.get('refresh') or '').strip() + try: + user, profile = authenticate_refresh_token(refresh_token) + except JWTAuthError as exc: return JsonResponse({ 'ok': False, - 'error': 'Username and password are required.', - }, status=400) + 'error': exc.message, + 'code': exc.code, + }, status=exc.status_code) - user = authenticate(request, username=username, password=password) - if user is None: - return JsonResponse({ - 'ok': False, - 'error': 'Invalid username or password.', - }, status=401) - - login(request, user) - profile = get_profile(user) return JsonResponse({ 'ok': True, - 'message': 'Login successful.', + 'message': 'Token refreshed successfully.', 'user': { 'id': user.id, 'username': user.username, 'email': user.email, 'business_name': profile.business_name, }, + 'tokens': issue_token_pair(user), }) @csrf_exempt @require_POST def api_logout_view(request): - if not request.user.is_authenticated: + payload = parse_api_payload(request) + if payload is None: + return JsonResponse({ + 'ok': False, + 'error': 'Invalid JSON body.', + }, status=400) + + user = request.user if request.user.is_authenticated else None + refresh_token = (payload.get('refresh_token') or payload.get('refresh') or '').strip() + + if user is None and refresh_token: + try: + user, _ = authenticate_refresh_token(refresh_token) + except JWTAuthError as exc: + return JsonResponse({ + 'ok': False, + 'error': exc.message, + 'code': exc.code, + }, status=exc.status_code) + + if user is None: + authorization = (request.META.get('HTTP_AUTHORIZATION') or '').strip() + if authorization: + try: + user, _ = authenticate_authorization_header(authorization) + except JWTAuthError as exc: + return JsonResponse({ + 'ok': False, + 'error': exc.message, + 'code': exc.code, + }, status=exc.status_code) + + if user is None: return JsonResponse({ 'ok': True, 'message': 'No active session.', }) - logout(request) + revoke_user_tokens(user) + if request.user.is_authenticated: + logout(request) + return JsonResponse({ 'ok': True, - 'message': 'Logout successful.', + 'message': 'Logout successful. Mobile tokens revoked.', }) @@ -522,12 +619,77 @@ def api_transactions_view(request): @csrf_exempt @api_login_required -@require_http_methods(['DELETE']) +@require_http_methods(['GET', 'PUT', 'PATCH', 'DELETE']) def api_transaction_detail_view(request, transaction_id): profile = get_profile(request.user) entry = get_object_or_404(profile.transactions.select_related('created_by'), id=transaction_id) + + if request.method == 'GET': + return JsonResponse({ + 'ok': True, + 'transaction': serialize_transaction(entry), + 'balances': { + 'current_ecash': str(profile.current_ecash), + 'current_physical_cash': str(profile.current_physical_cash), + 'total_cash': str(profile.total_cash), + }, + 'summary': build_transaction_summary(profile), + }) + + if request.method in {'PUT', 'PATCH'}: + payload = parse_api_payload(request) + if payload is None: + return JsonResponse({ + 'ok': False, + 'error': 'Invalid JSON body.', + }, status=400) + + merged_payload = { + 'client_name': entry.client_name, + 'amount': str(entry.amount), + 'transaction_type': entry.transaction_type, + 'notes': entry.notes, + } + merged_payload.update(payload) + form = TransactionForm(merged_payload, business=profile, instance=entry) + if not form.is_valid(): + return JsonResponse({ + 'ok': False, + 'error': 'Validation failed.', + 'errors': serialize_form_errors(form), + }, status=400) + + try: + update_result = form.save(request.user) + except ValidationError as exc: + return JsonResponse({ + 'ok': False, + 'error': exc.messages[0] if exc.messages else 'Could not update transaction.', + }, status=400) + + profile = update_result['business'] + entry = update_result['transaction'] + return JsonResponse({ + 'ok': True, + 'message': 'Transaction updated successfully.', + 'transaction': serialize_transaction(entry), + 'balances': { + 'current_ecash': str(profile.current_ecash), + 'current_physical_cash': str(profile.current_physical_cash), + 'total_cash': str(profile.total_cash), + }, + 'summary': build_transaction_summary(profile), + }) + deleted_transaction = serialize_transaction(entry) - delete_result = entry.delete_logged_transaction() + try: + delete_result = entry.delete_logged_transaction() + except ValidationError as exc: + return JsonResponse({ + 'ok': False, + 'error': exc.messages[0] if exc.messages else 'Could not delete transaction.', + }, status=400) + profile = delete_result['business'] return JsonResponse({ 'ok': True, diff --git a/requirements.txt b/requirements.txt index 6cf2a72..b2b934c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ Django==5.2.7 mysqlclient==2.2.7 python-dotenv==1.1.1 reportlab==4.4.1 +PyJWT==2.10.1