diff --git a/pairent_backend/pairent_app/models.py b/pairent_backend/pairent_app/models.py index 017488f..b03e5d8 100644 --- a/pairent_backend/pairent_app/models.py +++ b/pairent_backend/pairent_app/models.py @@ -131,7 +131,7 @@ class PsychTestAnswers(models.Model): verbose_name = "Ответ на психологический тест" verbose_name_plural = "Ответы на психологический тест" -class AuthTokens(models.Model): +class AuthToken(models.Model): user = models.BigIntegerField(null=False, verbose_name='ID Пользователя, которому принадлежит токен'); key = models.TextField(verbose_name='Ключ API'); expires = models.BigIntegerField(verbose_name='Когда ключ истечет (Unix timestamp)'); diff --git a/pairent_backend/pairent_app/serializer.py b/pairent_backend/pairent_app/serializer.py index 29416a2..221d2b8 100644 --- a/pairent_backend/pairent_app/serializer.py +++ b/pairent_backend/pairent_app/serializer.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from .models import Apartament, User +from .models import Apartament, User, AuthToken class ApartamentListSerializer(serializers.ModelSerializer): @@ -26,4 +26,9 @@ class PsychTestAddResultSerializer(serializers.ModelSerializer): class PublicUserSerializer(serializers.ModelSerializer): class Meta: model = User - exclude = ('favorites_apartments', 'comparison_apartments') \ No newline at end of file + exclude = ('favorites_apartments', 'comparison_apartments') + +class TokenSerializer(serializers.ModelSerializer): + class Meta: + model = AuthToken + fields = '__all__' \ No newline at end of file diff --git a/pairent_backend/pairent_app/views.py b/pairent_backend/pairent_app/views.py index 7b4de4e..6bd3249 100644 --- a/pairent_backend/pairent_app/views.py +++ b/pairent_backend/pairent_app/views.py @@ -10,13 +10,15 @@ from django.db.models.query import QuerySet from django.core.validators import validate_email from django.core.exceptions import ValidationError -from .models import Apartament, User, PsychTestAnswers +from .models import Apartament, User, PsychTestAnswers, AuthToken from .serializer import (ApartamentListSerializer, ApartamentDetailSerializer, PsychTestAddResultSerializer, - PublicUserSerializer) + PublicUserSerializer, + TokenSerializer) -import json, math, random, re, requests, oidc_client, base64, hashlib +import json, math, random, re, requests, oidc_client, base64, uuid, time, ipware as iplib +ipware = iplib.IpWare(); class ApartamentViewSet(viewsets.ReadOnlyModelViewSet): """Вывод списка квартир или отдельной квартиры""" @@ -185,7 +187,7 @@ def register(oid, provider_id, name): # discord=, # city=, role='s', - # photo_provider=, + photo_provider='VVSU', openid_addr=oid, openid_id=provider_id, ); @@ -206,7 +208,45 @@ def get_oauth_data(remote, key): 'User-Agent': 'curl/8.1' }).json(); +def create_auth_token(userid, ip): + + try: + token = AuthToken.objects.get(user=userid, ip=ip); + if (verify_auth_token(token.key, token.ip)): + return token; + except AuthToken.DoesNotExist: + 0 # ignore + + token = AuthToken( + user=userid, + key=str(uuid.uuid4()), + # 2 days + # vvv + expires=time.time() + 60 * 60 * 24 * 2, + ip=ip + ); + token.save(); + return token; + +def verify_auth_token(key, ip): + + try: + token = AuthToken.objects.get(key=key); + except AuthToken.DoesNotExist: + return False; + + if (token.ip != ip): + token.delete(); + return False; + + if (token.expires > time.time()): + token.delete(); + return False; + + return True; + class UserLogin(APIView): + # TODO: Remove csrf exempt when index.html is loaded through django @csrf_exempt def post(self, req: HttpRequest): @@ -227,18 +267,14 @@ class UserLogin(APIView): res.status_code = 400; return res; - # auth_data = get_oauth_token('https://vvsu.ru/connect', { - # 'grant_type': 'authorization_code', - # 'redirect_uri': 'https://pairent.vvsu.ru/sign-in/', - # 'code': data['code'], - # 'code_verifier': data['code_verifier'], - # 'client_id': 'it-hub-client', - # 'client_secret': 'U8y@uPVee6Q^*729esHTo4Vd' - # }); - - auth_data = {'access_token': 'gcH96CSYQBeiq9te1lpJV4T9mBH4UabT4_m6fJQFQK4.K4GA7sXFtBEM26kDladZjZ8phsI3aRPmqu5oRts4Csg', 'expires_in': 3600, 'id_token': 'eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpoeWRyYS5vcGVuaWQuaWQtdG9rZW4iLCJ0eXAiOiJKV1QifQ.eyJhY3IiOiIwIiwiYXRfaGFzaCI6ImJIZS1pWmlvX2Npa3diOFc3bnBkbEEiLCJhdWQiOlsiaXQtaHViLWNsaWVudCJdLCJhdXRoX3RpbWUiOjE2ODQyNDM0NjUsImNhbGxiYWNrX3VybCI6IiIsImV4cCI6MTY4NDI0NzA3MywiZmFtaWx5X25hbWUiOiLQn9GD0YHRgtC-0LLQsNC70L7QsiIsImdpdmVuX25hbWUiOiLQndC40LrQuNGC0LAiLCJpYXQiOjE2ODQyNDM0NzMsImlkIjoiMDk2Qzc4Q0QtNDk0My00RDU3LUJDNkQtNUNERTEyRjY4NkUzIiwiaXNzIjoiaHR0cHM6Ly93d3cudnZzdS5ydS9jb25uZWN0LyIsImp0aSI6IjU5M2FiYTQzLTU4OTQtNGZmNy1iMmU1LTdmOWZkYTZjZjFhZSIsImxvZ2luIjoiaHR0cHM6Ly9vcGVuaWQudnZzdS5ydS9ibGVrX18iLCJvcGVuaWQiOiJodHRwczovL29wZW5pZC52dnN1LnJ1L2JsZWtfXyIsInBpY3R1cmUiOiJodHRwczovL3d3dy52dnN1LnJ1L29pc2twL3Bob3RvL3B0aC5hc3A_SUQ9MDk2Qzc4Q0QtNDk0My00RDU3LUJDNkQtNUNERTEyRjY4NkUzXHUwMDI2IiwicHJvZmlsZV91cmwiOm51bGwsInJhdCI6MTY4NDI0MzQ1NCwic2lkIjoiMzEwZjU5MWEtZmNjYy00NzY3LTkzMmItYjM3OTQyZmFmMTA1Iiwic3ViIjoiaHR0cHM6Ly9vcGVuaWQudnZzdS5ydS9ibGVrX18iLCJzdXJuYW1lIjoi0J_Rg9GB0YLQvtCy0LDQu9C-0LIiLCJ0aXRsZSI6ItCh0YLRg9C00LXQvdGCIiwidnZzdV9JZEVtcGwiOm51bGwsInZ2c3VfSWRTdHVkIjoiMTk3MDgwIiwidnZzdV9JZFVzZXIiOjE5MDQ4OSwidnZzdV9sb2dpbiI6ImJsZWtfXyJ9.mClShf1lzGoKarsshafM6H2_57wrINbLSUjDQrEOAICN0V6TMNmC2zevgjxBbMl3BTIWhGJ37SNViyGvdNjPeG_S32TBr0m_vJEddZbHLzO7U7J2vqYVkiFQl8hziZkvhZUboSCu71aWexvN6rtX5grxIPAZswgGP4Mszg7ueQlhybgDELVg-UG-2OVH01-ynsfoZbaPYN6_8x44FJDUiltFbdx57kD8OEh4CdqEPTl3rL2T1U04cfNY0Ij2ivo9esEyAmuuXQCmwn_YwHO3TQc0S2Bq6DeIWa4gauynxGjPl2tf4fcyz-XOVWGeMNIwXCHvIDB_aHsZromG3UV2gY3ji-RlkEq81mYzFjOwB-LArkJQ68zQZlu5cFKqtWvZOzKqCzDDRUvfiRTu3OexQse_g10EeMi7vSeocGnfETlq5utar05gFGY-DxSaFYNCKzxqqS8V78d5aRFrWcQNbE6CVpKZPbZBBEQ-ItX-wh1FEyL3Uw-MsDztwJu6p_ftwRZLF0lk3ECFlbFt4NzzutFYqwS1s5ZoSZa-ylLY8PsZdr9gj58jBYD8c1foXZ9I_KzC_bYDOyUQfjec5njxGWN3828TvySclHkXMUgQxCM16OmPq8MICk_tfhqOSezcs0JpXIEtHHn0h9HNavZuhMTIaTWErYRIIxEPgtBn8r8', 'scope': 'openid vvsu_IdUser vvsu_IdEmpl vvsu_IdStud vvsu_login given_name family_name', 'token_type': 'bearer'} - - # print(auth_data); + auth_data = get_oauth_token('https://vvsu.ru/connect', { + 'grant_type': 'authorization_code', + 'redirect_uri': 'https://pairent.vvsu.ru/sign-in/', + 'code': data['code'], + 'code_verifier': data['code_verifier'], + 'client_id': 'it-hub-client', + 'client_secret': 'U8y@uPVee6Q^*729esHTo4Vd' + }); if ('error' in auth_data): return JsonResponse(auth_data); @@ -246,16 +282,13 @@ class UserLogin(APIView): user = None; new_user = False; - # vvsu_data = get_oauth_data('https://vvsu.ru/connect', auth_data['access_token']); - vvsu_data = {'acr': '0', 'aud': ['it-hub-client'], 'auth_time': 1684243465, 'callback_url': '', 'family_name': 'Пустовалов', 'given_name': 'Никита', 'iat': 1684243466, 'id': '096C78CD-4943-4D57-BC6D-5CDE12F686E3', 'iss': 'https://www.vvsu.ru/connect/', 'login': 'https://openid.vvsu.ru/blek__', 'openid': 'https://openid.vvsu.ru/blek__', 'picture': 'https://www.vvsu.ru/oiskp/photo/pth.asp?ID=096C78CD-4943-4D57-BC6D-5CDE12F686E3&', 'profile_url': None, 'rat': 1684243454, 'sub': 'https://openid.vvsu.ru/blek__', 'surname': 'Пустовалов', 'title': 'Студент', 'vvsu_IdEmpl': None, 'vvsu_IdStud': '197080', 'vvsu_IdUser': 190489, 'vvsu_login': 'blek__'} - + vvsu_data = get_oauth_data('https://vvsu.ru/connect', auth_data['access_token']); + if ('error' in vvsu_data): res = JsonResponse(vvsu_data); res.status_code = 500; return res; - req.session['auth_data'] = vvsu_data; - if ('error' in vvsu_data): res = JsonResponse(vvsu_data); res.status_code = 500; @@ -270,7 +303,8 @@ class UserLogin(APIView): return JsonResponse({ 'user_data': PublicUserSerializer(user).data, - 'new_user': new_user + 'new_user': new_user, + 'token': TokenSerializer(create_auth_token(user.id, ipware.get_client_ip(req.META)[0].exploded)).data }); class UserGet(APIView): diff --git a/pairent_backend/requirements.txt b/pairent_backend/requirements.txt index d495564..c8a891d 100644 --- a/pairent_backend/requirements.txt +++ b/pairent_backend/requirements.txt @@ -4,4 +4,4 @@ djangorestframework django-cors-headers Pillow requests -oidc-client \ No newline at end of file +python-ipware \ No newline at end of file diff --git a/pairent_frontend_react/src/API/User.js b/pairent_frontend_react/src/API/User.js index 4be7f14..1bfcce7 100644 --- a/pairent_frontend_react/src/API/User.js +++ b/pairent_frontend_react/src/API/User.js @@ -14,7 +14,70 @@ class UserLoginResponse { id; } -class User { +class IAPIObject { + /** + * Local storage key used to save data. + * @type {string} + */ + static storage_key = undefined; + + /** @returns {ThisType} */ + static restoreFromLocalStorage() { + if (storage_key !== undefined) { + throw Error('This doesn\'t support local storage'); + } + if (!window.localStorage.getItem(storage_key)) + return false; + return new APIToken(window.localStorage.getItem(storage_key)); + } + + /** + * Save this object to local storage + * @throws {QuotaExceededError} + * @returns {void} + */ + saveToLocalStorage() { + + // static this + sthis = Object.getPrototypeOf(this); + + if (sthis.storage_key !== undefined) { + throw Error('This doesn\'t support local storage'); + } + window.localStorage.setItem(sthis.storage_key, this); + } +} + +class APIToken extends IAPIObject { + + static storage_key = 'pairent_api_key'; + + constructor(data) { + this.user = data.user; + this.key = data.key; + this.expires = data.expires; + this.ip = data.ip; + } + + /** @type {number} */ + user; + + /** @type {string} */ + key; + + /** A Unix timestamp (when the token will expire) + * @type {number} + */ + expires; + + /** @type {string} */ + ip; +} + +class User extends IAPIObject { + + static storage_key = 'pairent_user_data'; + constructor(data) { for (const key in data) { this[key] = data[key]; @@ -44,7 +107,11 @@ class User { } const data = await axios.post(api_path('/api/auth/user/login'), response); - return new User(data.data); + + window.localStorage.setItem(APIToken.storage_key, data.data.token); + window.localStorage.setItem(User.storage_key, data.data.user_data); + + return data.data; } } diff --git a/pairent_frontend_react/src/pages/LoggedIn/index.jsx b/pairent_frontend_react/src/pages/LoggedIn/index.jsx index 476983b..eda1db9 100644 --- a/pairent_frontend_react/src/pages/LoggedIn/index.jsx +++ b/pairent_frontend_react/src/pages/LoggedIn/index.jsx @@ -4,6 +4,7 @@ import { HashLoader } from "react-spinners"; import { SigninResponse, UserManager } from 'oidc-client-ts'; import { User } from "../../API/User"; import FloatingBox from "../../components/UI/FloatingBox"; +import { useNavigate } from "react-router-dom"; import constants from "../../constants"; @@ -48,7 +49,13 @@ export default class LoggedIn extends React.Component { } } - console.log(await User.login({...this.response, code_verifier})); + const response = await User.login({...this.response, code_verifier}); + if (response.new_user) { + // TODO: Make the page + useNavigate('/register'); + } else { + useNavigate('/'); + } } render() {