diff --git a/pairent_backend/.DS_Store b/pairent_backend/.DS_Store index 56d0058..b78297c 100644 Binary files a/pairent_backend/.DS_Store and b/pairent_backend/.DS_Store differ diff --git a/pairent_backend/pairent_app/factories.py b/pairent_backend/pairent_app/factories.py index 08479df..e6d046a 100644 --- a/pairent_backend/pairent_app/factories.py +++ b/pairent_backend/pairent_app/factories.py @@ -1,6 +1,6 @@ import factory import random -import time, datetime +import time, datetime, uuid from django.db import models @@ -8,6 +8,10 @@ from pairent_app.models import Apartament, User factory.Faker.override_default_locale('ru_RU'); +class UUID(factory.declarations.BaseDeclaration): + def evaluate(self, instance, step, extra): + return str(uuid.uuid4()).upper(); + class OpenID_Address(factory.declarations.BaseDeclaration): def evaluate(self, instance, step, extra): return ''.join(random.choices(list('abcdef12345678990'), k=6)) + "@vvsu.ru"; @@ -115,7 +119,6 @@ class UserFactory(factory.django.DjangoModelFactory): favorites_apartments = CSV(1, 100, 1, 16); comparison_apartments = CSV(1, 100, 1, 5); - openid_addr = OpenID_Address(); name = factory.faker.Faker('name'); date_of_birth = Date(1980, 2006); about_me = factory.faker.Faker('sentence'); @@ -127,4 +130,8 @@ class UserFactory(factory.django.DjangoModelFactory): discord = '@uwu' # они поменяли формат city = Random(0,0,0, ['Владивосток', 'Хабаровск', 'Урюпинск', 'Мухосранск', 'Нью-Йорк']) - role = 's' \ No newline at end of file + role = 's' + + openid_addr = OpenID_Address(); + openid_id = UUID(); + photo_provider = Random(0,0,0, ['VVSU', 'GRAVATAR']); \ No newline at end of file diff --git a/pairent_backend/pairent_app/migrations/0005_user.py b/pairent_backend/pairent_app/migrations/0005_user.py index fc86736..f3951ef 100644 --- a/pairent_backend/pairent_app/migrations/0005_user.py +++ b/pairent_backend/pairent_app/migrations/0005_user.py @@ -19,7 +19,6 @@ class Migration(migrations.Migration): ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('favorites_apartments', models.CharField(max_length=100, help_text="Избранные квартиры (CSV)")), ('comparison_apartments', models.CharField(max_length=100, help_text="Квартиры для сравнения (CSV)")), - ('openid_addr', models.CharField(max_length=1000, null=False, help_text='Адрес Open ID Connect (login@provider.com, для ВВГУ - login@vvsu.ru)')), ('name', models.CharField(max_length=256, help_text='ФИО Пользователя')), ('date_of_birth', models.DateField(help_text='Дата рождения пользователя')), ('about_me', models.CharField(max_length=1000, help_text='Поле "О Себе"')), @@ -29,7 +28,11 @@ class Migration(migrations.Migration): ('telegram', models.CharField(max_length=1000, help_text='Телеграм пользователя', null=True)), ('discord', models.CharField(max_length=1000, help_text='Дискорд ник пользователя', null=True)), ('city', models.CharField(max_length=1000, help_text='Город пользователя', null=True)), - ('role', models.CharField(max_length=1, help_text='Роль пользователя (s - student, a - admin, m - moderator)', null=False)) + ('role', models.CharField(max_length=1, help_text='Роль пользователя (s - student, a - admin, m - moderator)', null=False)), + ('photo_provider', models.CharField(max_length=100, verbose_name='Сервис, из которого загружается фотография пользователя (VVSU, GRAVATAR)')), + + ('openid_addr', models.CharField(max_length=1000, null=False, help_text='Адрес Open ID Connect (login@provider.com, для ВВГУ - login@vvsu.ru)')), + ('openid_id', models.CharField(max_length=5000, verbose_name='ID Пользователя в системе провайдера авторизации (скорее всего ВВГУ)')) ] ) ] diff --git a/pairent_backend/pairent_app/migrations/0006_alter_user_options_alter_user_about_me_and_more.py b/pairent_backend/pairent_app/migrations/0006_alter_user_options_alter_user_about_me_and_more.py deleted file mode 100644 index 982d074..0000000 --- a/pairent_backend/pairent_app/migrations/0006_alter_user_options_alter_user_about_me_and_more.py +++ /dev/null @@ -1,103 +0,0 @@ -# Generated by Django 4.2.1 on 2023-05-15 14:41 - -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('pairent_app', '0005_user'), - ] - - operations = [ - migrations.AlterModelOptions( - name='user', - options={'verbose_name': 'Пользователь', 'verbose_name_plural': 'Пользователи'}, - ), - migrations.AlterField( - model_name='user', - name='about_me', - field=models.CharField(max_length=1000, verbose_name='Поле "О Себе"'), - ), - migrations.AlterField( - model_name='user', - name='city', - field=models.CharField(max_length=1000, null=True, verbose_name='Город пользователя'), - ), - migrations.AlterField( - model_name='user', - name='comparison_apartments', - field=models.CharField(max_length=100, verbose_name='Квартиры для сравнения (CSV)'), - ), - migrations.AlterField( - model_name='user', - name='date_of_birth', - field=models.DateField(verbose_name='Дата рождения пользователя'), - ), - migrations.AlterField( - model_name='user', - name='discord', - field=models.CharField(max_length=1000, null=True, verbose_name='Дискорд ник пользователя'), - ), - migrations.AlterField( - model_name='user', - name='email', - field=models.CharField(max_length=1000, null=True, verbose_name='Почтовый ящик пользователя в формате user@example.com'), - ), - migrations.AlterField( - model_name='user', - name='favorites_apartments', - field=models.CharField(max_length=100, verbose_name='Избранные квартиры (CSV)'), - ), - migrations.AlterField( - model_name='user', - name='gender', - field=models.CharField(max_length=1, verbose_name='Пол пользователя (f,m,n,?)'), - ), - migrations.AlterField( - model_name='user', - name='name', - field=models.CharField(max_length=256, verbose_name='ФИО Пользователя'), - ), - migrations.AlterField( - model_name='user', - name='openid_addr', - field=models.CharField(max_length=1000, verbose_name='Адрес Open ID Connect (login@provider.com, для ВВГУ - login@vvsu.ru)'), - ), - migrations.AlterField( - model_name='user', - name='phone', - field=models.CharField(max_length=30, null=True, verbose_name='Телефон пользователя в международном формате (+00000000)'), - ), - migrations.AlterField( - model_name='user', - name='role', - field=models.CharField(max_length=1, verbose_name='Роль пользователя (s - student, a - admin, m - moderator)'), - ), - migrations.AlterField( - model_name='user', - name='telegram', - field=models.CharField(max_length=1000, null=True, verbose_name='Телеграм пользователя'), - ), - migrations.CreateModel( - name='PsychTestAnswers', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('first_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на первый вопрос')), - ('second_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на второй вопрос')), - ('third_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на третий вопрос')), - ('fourth_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на четвертый вопрос')), - ('fifth_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на пятый вопрос')), - ('sixth_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на шестой вопрос')), - ('seventh_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на седьмой вопрос')), - ('eighth_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на восьмой вопрос')), - ('nineth_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на девятый вопрос')), - ('tenth_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на десятый вопрос')), - ('eleventh_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на одиннадцатый вопрос')), - ('twelfth_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на двенадцатый вопрос')), - ('users', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pairent_app.user', verbose_name='Пользователь')), - ], - ), - ] diff --git a/pairent_backend/pairent_app/migrations/0006_psychtestanswers.py b/pairent_backend/pairent_app/migrations/0006_psychtestanswers.py new file mode 100644 index 0000000..533007c --- /dev/null +++ b/pairent_backend/pairent_app/migrations/0006_psychtestanswers.py @@ -0,0 +1,33 @@ +import django.core.validators +from django.core.validators import RegexValidator, MaxValueValidator +from django.db import migrations, models + +from pairent_app.models import User + +class Migration(migrations.Migration): + + dependencies = [ + ('pairent_app', '0005_user'), + ] + + operations = [ + migrations.CreateModel( + name='PsychTestAnswers', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('first_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на первый вопрос')), + ('second_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на второй вопрос')), + ('third_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на третий вопрос')), + ('fourth_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на четвертый вопрос')), + ('fifth_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на пятый вопрос')), + ('sixth_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на шестой вопрос')), + ('seventh_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на седьмой вопрос')), + ('eighth_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на восьмой вопрос')), + ('nineth_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на девятый вопрос')), + ('tenth_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на десятый вопрос')), + ('eleventh_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на одиннадцатый вопрос')), + ('twelfth_question', models.IntegerField(validators=[django.core.validators.MaxValueValidator(5)], verbose_name='Ответ на двенадцатый вопрос')), + ('users', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pairent_app.user', verbose_name='Пользователь')), + ], + ) + ] diff --git a/pairent_backend/pairent_app/migrations/0007_alter_psychtestanswers_options_and_more.py b/pairent_backend/pairent_app/migrations/0007_alter_psychtestanswers_options_and_more.py deleted file mode 100644 index 167cf7a..0000000 --- a/pairent_backend/pairent_app/migrations/0007_alter_psychtestanswers_options_and_more.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 4.2.1 on 2023-05-15 15:23 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('pairent_app', '0006_alter_user_options_alter_user_about_me_and_more'), - ] - - operations = [ - migrations.AlterModelOptions( - name='psychtestanswers', - options={'verbose_name': 'Ответ на психологический тест', 'verbose_name_plural': 'Ответы на психологический тест'}, - ), - migrations.RenameField( - model_name='psychtestanswers', - old_name='users', - new_name='user', - ), - ] diff --git a/pairent_backend/pairent_app/models.py b/pairent_backend/pairent_app/models.py index 6f9f031..68fff93 100644 --- a/pairent_backend/pairent_app/models.py +++ b/pairent_backend/pairent_app/models.py @@ -86,7 +86,6 @@ class User(models.Model): favorites_apartments = models.CharField(max_length=100, verbose_name="Избранные квартиры (CSV)") comparison_apartments = models.CharField(max_length=100, verbose_name="Квартиры для сравнения (CSV)") - openid_addr = models.CharField(max_length=1000, null=False, verbose_name='Адрес Open ID Connect (login@provider.com, для ВВГУ - login@vvsu.ru)') name = models.CharField(max_length=256, verbose_name='ФИО Пользователя') date_of_birth = models.DateField(verbose_name='Дата рождения пользователя') about_me = models.CharField(max_length=1000, verbose_name='Поле "О Себе"') @@ -102,6 +101,11 @@ class User(models.Model): role = models.CharField(max_length=1, verbose_name='Роль пользователя (s - student, a - admin, m - moderator)', null=False) + photo_provider = models.CharField(max_length=100, verbose_name='Сервис, из которого загружается фотография пользователя (VVSU, GRAVATAR)') + + openid_addr = models.CharField(max_length=1000, null=False, verbose_name='Адрес Open ID (login@provider.com, для ВВГУ - login@vvsu.ru)') + openid_id = models.CharField(max_length=10000, verbose_name='ID Пользователя в системе провайдера авторизации (скорее всего ВВГУ)') + class Meta: verbose_name = "Пользователь" verbose_name_plural = "Пользователи" diff --git a/pairent_backend/pairent_app/urls.py b/pairent_backend/pairent_app/urls.py index 61e21df..a834842 100644 --- a/pairent_backend/pairent_app/urls.py +++ b/pairent_backend/pairent_app/urls.py @@ -10,7 +10,14 @@ urlpatterns = format_suffix_patterns([ path("apartaments/comparison/", views.ApartamentGetManyViewSet.as_view({'get': 'retrieve'})), # пример: apartaments/comparison/?user_id=1 user_id - id пользователя path("apartaments/favorite/", views.ApartamentGetManyViewSet.as_view({'get': 'list'})), # пример: apartaments/favorite/?user_id=1 user_id - id пользователя path("apartaments/filters/", views.ApartmentFilter.as_view({'post': 'list'})), - path("psych_test/add_result/", views.PsychTestAddResultViewSet.as_view({'post': 'create'})), # пример: psych_test/add_result/1/?result=50 result - результат псих теста пользователя - path("users/get_compatible", views.CompatibleUsersView.as_view({'post': 'list'})), + + # user + path("user/get_compatible", views.CompatibleUsersView.as_view({'post': 'list'})), + # пример: psych_test/add_result/1/?result=50 result - результат псих теста пользователя + path("psych_test/add_result/", views.PsychTestAddResultViewSet.as_view({'post': 'create'})), + path('user/get', views.UserGet.as_view()), + + # auth re_path(r'^auth/vvsu/', views.VVSUAuthProxy), + path('auth/user/login', views.UserLogin.as_view()) ]) diff --git a/pairent_backend/pairent_app/views.py b/pairent_backend/pairent_app/views.py index eb24614..b474033 100644 --- a/pairent_backend/pairent_app/views.py +++ b/pairent_backend/pairent_app/views.py @@ -2,8 +2,9 @@ from rest_framework import viewsets from rest_framework.response import Response from rest_framework.views import APIView, View from rest_framework.request import Request +from django.views.decorators.csrf import csrf_exempt -from django.http import HttpResponseBadRequest, HttpResponse +from django.http import HttpResponseBadRequest, HttpResponse, JsonResponse, HttpRequest from django.db.models.query import QuerySet from django.core.validators import validate_email @@ -15,7 +16,7 @@ from .serializer import (ApartamentListSerializer, PsychTestAddResultSerializer, PublicUserSerializer) -import json, math, random, re, requests +import json, math, random, re, requests, oidc_client, base64, hashlib class ApartamentViewSet(viewsets.ReadOnlyModelViewSet): """Вывод списка квартир или отдельной квартиры""" @@ -168,4 +169,121 @@ def VVSUAuthProxy(req: Request): resp = HttpResponse(preq.content); resp.headers['Content-Type'] = preq.headers['Content-Type']; - return resp; \ No newline at end of file + return resp; + +def regiserUser(oid, provider_id, name, date_of_birth): + user = User( + favorites_apartments='', + comparison_apartments='', + name=name, + date_of_birth=date_of_birth, + about_me='', + gender='?', + phone='+00000', + # email=, + # telegram=, + # discord=, + # city=, + role='s', + # photo_provider=, + openid_addr=oid, + openid_id=provider_id, + ) + +def get_oauth_token(remote, data): + return requests.post(remote + '/oauth2/token', data, + headers={ + 'Origin': 'https://pairent.vvsu.ru', + 'Referer': 'https://pairent.vvsu.ru' + }).json(); + +def get_oauth_data(remote, key): + return requests.get(remote + '/userinfo', headers={ + 'Origin': 'https://pairent.vvsu.ru', + 'Authorization': 'Bearer ' + key, + 'User-Agent': 'curl/8.1' + }).json(); + +class UserLogin(APIView): + # TODO: Remove csrf exempt when index.html is loaded through django + @csrf_exempt + def post(self, req: HttpRequest): + + if (req.session.has_key('auth_data')): + # TODO: Return user object instead of error + return JsonResponse({'error': 'already authenticated'}) + + if (req.content_type != 'application/json'): + res = HttpResponse({'error': 'bad content type'}); + res.status_code = 400; + return res; + + data = json.loads(req.body.decode('utf8')); + + if not ('code' in data and 'code_verifier' in data): + res = JsonResponse({'error': 'no code'}); + 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': '5kHvrjy91LJgJLKitejBBG24c7JiX45tEstKVHRpfHc._WQDwQ2F13aytbGFjlGnjXJeUWcDD1V3om3cRW0IujM', 'expires_in': 3600, 'id_token': 'eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpoeWRyYS5vcGVuaWQuaWQtdG9rZW4iLCJ0eXAiOiJKV1QifQ.eyJhY3IiOiIwIiwiYXRfaGFzaCI6IjRMR1dRekxVaXFodUVTYjU0QWFIM0EiLCJhdWQiOlsiaXQtaHViLWNsaWVudCJdLCJhdXRoX3RpbWUiOjE2ODQyMzc4MDksImNhbGxiYWNrX3VybCI6IiIsImV4cCI6MTY4NDI0MTQ1NSwiZmFtaWx5X25hbWUiOiLQn9GD0YHRgtC-0LLQsNC70L7QsiIsImdpdmVuX25hbWUiOiLQndC40LrQuNGC0LAiLCJpYXQiOjE2ODQyMzc4NTUsImlkIjoiMDk2Qzc4Q0QtNDk0My00RDU3LUJDNkQtNUNERTEyRjY4NkUzIiwiaXNzIjoiaHR0cHM6Ly93d3cudnZzdS5ydS9jb25uZWN0LyIsImp0aSI6IjEzMTBhNzcwLWFhZWUtNGExYS1hMTc1LWM3MzY3ZWM0ZjVhNyIsImxvZ2luIjoiaHR0cHM6Ly9vcGVuaWQudnZzdS5ydS9ibGVrX18iLCJvcGVuaWQiOiJodHRwczovL29wZW5pZC52dnN1LnJ1L2JsZWtfXyIsInBpY3R1cmUiOiJodHRwczovL3d3dy52dnN1LnJ1L29pc2twL3Bob3RvL3B0aC5hc3A_SUQ9MDk2Qzc4Q0QtNDk0My00RDU3LUJDNkQtNUNERTEyRjY4NkUzXHUwMDI2IiwicHJvZmlsZV91cmwiOm51bGwsInJhdCI6MTY4NDIzNzc5Nywic2lkIjoiOTYyYzg0OGYtZThkNS00ZDJjLWEwZmEtYjI5YmU3YjBlODAxIiwic3ViIjoiaHR0cHM6Ly9vcGVuaWQudnZzdS5ydS9ibGVrX18iLCJzdXJuYW1lIjoi0J_Rg9GB0YLQvtCy0LDQu9C-0LIiLCJ0aXRsZSI6ItCh0YLRg9C00LXQvdGCIiwidnZzdV9JZEVtcGwiOm51bGwsInZ2c3VfSWRTdHVkIjoiMTk3MDgwIiwidnZzdV9JZFVzZXIiOjE5MDQ4OSwidnZzdV9sb2dpbiI6ImJsZWtfXyJ9.A4BiOxpOqnesSiTGRdcTsC-lGhSABswivpUovD9EOdYmqKW753VlLcXQxfBPcfmq8Fdf7RmVvXTXPXYqkX7AKxQT-yUUm7XtJHCb85g2YfL64cjTP2sFYD6wPIU9nzXbCrsgKqKubY3p16Dn9VyrBCXE9N6jdbuNOFbWMLPLPlp7U5fx2SzVGaBMUONlTf8KiLkcisQoN4c_rPGqdi38gzhLf7WGEiKLOldXH1q-s_kPeObFvcdbsFrrnDPnJtdqBx8SF02wqJsrZlBiB9Hl-d6sSJYLZZWumFhS-qscfwRlTEZKqC-hWF5c9R8CUYewk89JxRvCcKrHZvPMip9j9vJF1_OjkSrC5EkGaprl765FgVPEBJqXj9LjGRkTOYfYUFAAMia_HhjtinQFp6XJ-Rh3JrmIfLAQ7DEUSOldMQ1xUw9GeHo_0sIsnjaM6lVx6M_SiDTWihxNu58DiI8tmvkdw7in95OJRoJZ30EhR3SGYsK3b51qdYK1aieufJHX40bN_S1gc84pisTg58z-zC5kGsjsZNv6gRSTO4oOpZMK1FMjv7HyasSMWEu-J052X4Qxquj4pWglpiGQNt3-E0jZUUjqmZ0-7AYiyEC_3IItBqWrve-LTXRF5faIZB5v3F3urY6Qjgn93m_AoK1oujfNAPk8WOLTv419CuC2fAc', 'scope': 'openid vvsu_IdUser vvsu_IdEmpl vvsu_IdStud vvsu_login given_name family_name', 'token_type': 'bearer'} + + user = None; + new_user = False; + + print(auth_data); + + return JsonResponse(get_oauth_data('https://vvsu.ru/connect', auth_data['access_token'])); + + req.session['auth_data'] = vvsu_data; + + if ('error' in vvsu_data): + res = JsonResponse(vvsu_data); + res.status_code = cb.status_code; + return res + + vvsu_data['vvsu_login'] += '@vvsu.ru'; + try: + user = User.objects.get(openid_addr=vvsu_data['vvsu_login']); + except User.DoesNotExist: + registerUser(vvsu_data['vvsu_login'], cb.id, f'{cb.given_name} {cb.family_name}'); + user = User.objects.get(openid_addr=vvsu_data['vvsu_login']); + new_user = True; + + return JsonResponse({ + 'user_data': user, + 'new_user': new_user + }); + +class UserGet(APIView): + def get(self, req: HttpRequest): + if not ('id' in req.GET.keys() or 'login' in req.GET.keys()): + res = JsonResponse({'error': 'no id or login'}); + res.status_code = 400; + return res; + + id_type = 'id' if 'id' in req.GET.keys() else 'login'; + id = req.GET.get(id_type); + + if (id_type == 'login'): + if not id.endswith('@vvsu.ru'): + id += '@vvsu.ru'; + id_type = 'openid_addr'; + + user = None; + try: + user = User.objects.get(**{id_type: id}); + except User.DoesNotExist: + res = JsonResponse({'error': 'not found'}); + res.status_code = 404; + return res; + + return JsonResponse(PublicUserSerializer(user).data); \ No newline at end of file diff --git a/pairent_backend/pairent_backend/settings.py b/pairent_backend/pairent_backend/settings.py index 9a97b53..c9eef6b 100644 --- a/pairent_backend/pairent_backend/settings.py +++ b/pairent_backend/pairent_backend/settings.py @@ -65,7 +65,7 @@ REST_FRAMEWORK = { } # Настройка отвечающая, что все могут отправлять запрос на бекенд. УБРАТЬ ПРИ ПРОДАКШЕНЕ! -CORS_ORIGIN_ALLOW_ALL = True + ROOT_URLCONF = 'pairent_backend.urls' @@ -146,3 +146,17 @@ STATIC_URL = 'src/' # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# VVSU Auth +OIDC_CONF = { + 'client-id': 'it-hub-client', + 'client-secret': 'U8y@uPVee6Q^*729esHTo4Vd', + 'authority': "https://vvsu.ru/connect" +} + +# CSRF +CORS_ORIGIN_ALLOW_ALL = DEBUG +CSRF_COOKIE_SECURE = not DEBUG +CSRF_COOKIE_HTTPONLY = not DEBUG +CSRF_TRUSTED_ORIGINS = ['http://pairent.vvsu.ru', 'http://localhost'] +CORS_ORIGIN_WHITELIST = ('http://pairent.vvsu.ru', 'http://localhost') \ No newline at end of file diff --git a/pairent_backend/requirements.txt b/pairent_backend/requirements.txt index fb3ac95..d495564 100644 --- a/pairent_backend/requirements.txt +++ b/pairent_backend/requirements.txt @@ -3,4 +3,5 @@ django djangorestframework django-cors-headers Pillow -requests \ No newline at end of file +requests +oidc-client \ No newline at end of file diff --git a/pairent_frontend_react/public/images/avatar-test.jpg b/pairent_frontend_react/public/images/avatar-test.jpg new file mode 100644 index 0000000..88a0230 Binary files /dev/null and b/pairent_frontend_react/public/images/avatar-test.jpg differ diff --git a/pairent_frontend_react/public/images/icons/camera.svg b/pairent_frontend_react/public/images/icons/camera.svg new file mode 100644 index 0000000..e76a820 --- /dev/null +++ b/pairent_frontend_react/public/images/icons/camera.svg @@ -0,0 +1,3 @@ + + + diff --git a/pairent_frontend_react/src/API/User.js b/pairent_frontend_react/src/API/User.js index 0f029f8..4be7f14 100644 --- a/pairent_frontend_react/src/API/User.js +++ b/pairent_frontend_react/src/API/User.js @@ -1,17 +1,51 @@ import axios from 'axios'; import constants from '../constants'; -const { API_ROOT } = constants; +const { API_ROOT, api_path } = constants; + +class UserLoginResponse { + /** @type {string} */ + session_key; + + /** @type {string} */ + openid_login; + + /** @type {number} */ + id; +} class User { constructor(data) { - this = { ...data, ...this }; + for (const key in data) { + this[key] = data[key]; + } } + static restoreFromLocalStorage() { + + } + + /** + * @param {string} id + * @returns {User} + */ static async getById(id) { - const data = await axios.post(API_ROOT + '/users/get', { id }); + const data = await axios.get(API_ROOT + '/api/user/get', { params: { id } }); if (data.data['error']) throw new Error(data.data['error']); return new User(data.data); } -} \ No newline at end of file + + /** @param {import('oidc-client-ts').SigninResponse} response */ + static async login(response) { + if (response.error !== null) { + throw new Error(response.error + ': ' + response.error_description); + return; + } + + const data = await axios.post(api_path('/api/auth/user/login'), response); + return new User(data.data); + } +} + +export { User, UserLoginResponse } \ No newline at end of file diff --git a/pairent_frontend_react/src/components/Header/index.jsx b/pairent_frontend_react/src/components/Header/index.jsx index b4cceb3..ae882f4 100644 --- a/pairent_frontend_react/src/components/Header/index.jsx +++ b/pairent_frontend_react/src/components/Header/index.jsx @@ -6,6 +6,7 @@ import SVGIcon from '../UI/Icon/SVGIcon'; // import './styles/Header.css'; const HeaderElement = styled.header` + min-width: 950px; display: flex; justify-content: space-between; align-items: center; diff --git a/pairent_frontend_react/src/components/UI/FloatingBox/index.jsx b/pairent_frontend_react/src/components/UI/FloatingBox/index.jsx new file mode 100644 index 0000000..c70df60 --- /dev/null +++ b/pairent_frontend_react/src/components/UI/FloatingBox/index.jsx @@ -0,0 +1,27 @@ +import React from "react"; +import { styled } from "styled-components"; + +const FloatingBox = styled.div` + position: fixed; + top: 45%; + left: 50%; + transform: translate(-50%, -50%); + border: 1px solid #c2c4c2; + border-radius: 12px; + padding: 24px 36px; + background: white; + box-shadow: 0 2px 1px #00000010; + + text-align: center; + + & hr { + margin-bottom: 24px; + height: 0px; + border: 0; + border-bottom: 1px solid #c2c4c2; + box-shadow: 0 2px 1px #c2c4c280; + } + +`; + +export default FloatingBox; \ No newline at end of file diff --git a/pairent_frontend_react/src/constants.js b/pairent_frontend_react/src/constants.js index 7c5e213..7e07e9a 100644 --- a/pairent_frontend_react/src/constants.js +++ b/pairent_frontend_react/src/constants.js @@ -1,5 +1,8 @@ import { nanoid } from 'nanoid' +/** @returns {string} */ +const api_path = path => API_ROOT + path; + /** * Api root path * @type {string} @@ -7,32 +10,14 @@ import { nanoid } from 'nanoid' const API_ROOT = window.location.protocol + '//127.0.0.1:8000'; // ДЛЯ ПРОДА ПОСТАВИТЬ ЭТО: '//pairent.vvsu.ru' -if (window.localStorage.getItem('oidc_client_key') == undefined) { - window.localStorage.setItem('oidc_client_key', nanoid(32)); -} - -const OIDC_CLIENT_KEY = window.localStorage.getItem('oidc_client_key'); - - /** OpenID Connect Client Config * @type {import('oidc-client-ts').OidcClientSettings} */ const OIDCConfig = { - onSignIn: () => {}, - authority: API_ROOT + '/api/auth/vvsu/', + authority: api_path('/api/auth/vvsu/'), client_id: 'it-hub-client', redirect_uri: 'https://pairent.vvsu.ru/sign-in/', - scope: [ - 'openid', - 'vvsu_IdUser', - 'vvsu_IdEmpl', - 'vvsu_IdStud', - 'vvsu_login', - 'given_name', - 'family_name' - ], - client_secret: OIDC_CLIENT_KEY + scope: 'openid vvsu_IdUser vvsu_IdEmpl vvsu_IdStud vvsu_login given_name family_name' }; - -export default Object.freeze({ API_ROOT, OIDCConfig, OIDC_CLIENT_KEY }); \ No newline at end of file +export default Object.freeze({ API_ROOT, OIDCConfig, api_path }); \ No newline at end of file diff --git a/pairent_frontend_react/src/pages/LoggedIn/index.jsx b/pairent_frontend_react/src/pages/LoggedIn/index.jsx index 39780b8..476983b 100644 --- a/pairent_frontend_react/src/pages/LoggedIn/index.jsx +++ b/pairent_frontend_react/src/pages/LoggedIn/index.jsx @@ -1,7 +1,11 @@ import React from "react"; import { styled } from "styled-components"; import { HashLoader } from "react-spinners"; -import { SigninResponse, SigninState } from 'oidc-client-ts'; +import { SigninResponse, UserManager } from 'oidc-client-ts'; +import { User } from "../../API/User"; +import FloatingBox from "../../components/UI/FloatingBox"; + +import constants from "../../constants"; const CenterContainer = styled.div` position: relative; @@ -18,27 +22,59 @@ const CenterContainer = styled.div` } `; +const ErrorText = styled.p` + font-family: monospace; + margin: 10px 0; + width: 780px; +`; + export default class LoggedIn extends React.Component { constructor(props) { super(props); this.response = new SigninResponse(new URL(window.location.href).searchParams); - this.signin_state = SigninState.fromStorageString(window.localStorage.getItem('oidc_signin_state')); + } async componentDidMount() { - console.log(this.response) + if (this.response.error) return; + + let code_verifier = '?'; + // get code verifier + for (const key in localStorage) { + if (key.startsWith('oidc.')) { + code_verifier = JSON.parse(localStorage[key]).code_verifier; + localStorage.removeItem(key); + break; + } + } + + console.log(await User.login({...this.response, code_verifier})); } render() { return ( -
- -

Подождите пожалуйста

-
- + <> +
+ +

Подождите пожалуйста

+
+ +
+
+
+ + { + this.response.error ? +
+ +

Ошибка авторизации

+

{this.response.error}

+ {this.response.error_description} +
- -
+ : null + } + ) } } \ No newline at end of file diff --git a/pairent_frontend_react/src/pages/LoginPage/index.jsx b/pairent_frontend_react/src/pages/LoginPage/index.jsx index 09f6ce7..40b5ba4 100644 --- a/pairent_frontend_react/src/pages/LoginPage/index.jsx +++ b/pairent_frontend_react/src/pages/LoginPage/index.jsx @@ -1,45 +1,15 @@ import React from 'react'; import { styled } from 'styled-components'; -import BlueButton from '../../components/UI/BlueButton'; import { HashLoader } from 'react-spinners'; +import BlueButton from '../../components/UI/BlueButton'; +import FloatingBox from '../../components/UI/FloatingBox'; import * as OpenID from 'oidc-client-ts'; import constants from '../../constants'; const { OIDCConfig } = constants; -const LoginBox = styled.div` - position: fixed; - top: 45%; - left: 50%; - transform: translate(-50%, -50%); - border: 1px solid #c2c4c2; - border-radius: 12px; - padding: 24px 36px; - background: white; - box-shadow: 0 2px 1px #00000010; - - text-align: center; - - & h2 { - margin: 0px 0; - } - & hr { - margin: 16px 0; - margin-bottom: 24px; - height: 0px; - border: 0; - border-bottom: 1px solid #c2c4c2; - box-shadow: 0 2px 1px #c2c4c280; - } - & p { - font-size: 8pt; - color: gray; - margin-top: 16px; - } -`; - const LoginButton = styled(BlueButton)` font-size: 11pt; width: 250px; @@ -60,6 +30,12 @@ const LoginButton = styled(BlueButton)` } `; +const SmallText = styled.p` + font-size: 8pt; + color: gray; + margin-top: 16px; +`; + const VVSULogoSVG = () => { return ; } @@ -82,17 +58,16 @@ export default class LoginPage extends React.Component { OpenID.Log.setLogger(console); OpenID.Log.setLevel(OpenID.Log.DEBUG); - let client = new OpenID.OidcClient(OIDCConfig); + let client = new OpenID.UserManager(OIDCConfig); - const req = await client.createSigninRequest({}); - window.localStorage.setItem('oidc_signin_state', req.state.toStorageString()); - window.location.href = req.url; + client.signinRedirect(); + } render() { return (
- +

Вход


@@ -105,11 +80,11 @@ export default class LoginPage extends React.Component { } -

+ Вход осуществляется только через
Систему Единого Входа ВВГУ -

-
+ +
); } diff --git a/pairent_frontend_react/src/pages/PersonalCabinet/index.jsx b/pairent_frontend_react/src/pages/PersonalCabinet/index.jsx new file mode 100644 index 0000000..02a6c37 --- /dev/null +++ b/pairent_frontend_react/src/pages/PersonalCabinet/index.jsx @@ -0,0 +1,360 @@ +import React, { useEffect, useState }from 'react'; +import styled, { keyframes } from 'styled-components'; +import SVGIcon from "../../components/UI/Icon/SVGIcon"; +import { User } from '../../API/User'; + +import { useFetching } from '../../hooks/useFetching'; + +import { Row, Col, Stack } from 'react-bootstrap'; + +const BackButton = styled.button` + border: 1px solid #c2c4c2; + display: inline-block; + padding: 12px 16px; + border-radius: 14px; + + background: #ffffff; + color: gray; + box-shadow: 0 2px 1px #00000010; + + font-size: 12pt; + float: left; + + & ${SVGIcon} { + transform: translate(-4px, 2px) + } +`; + +const Title = styled.div` + height: 100px; + padding-top: 32px; + z-index: 1; + position: relative; + display: flex; + align-items: center; + top: 0; + + & h2 { + margin-left: 28px; + font-weight: 600; + padding: 0; + display: inline-block; + transform: translateY(4px); + } + + & span { + margin-left: 14px; + font-weight: 500; + font-size: 18px; + line-height: 19px; + } +`; + +const CabinetSection = styled.div` + width: 1270px; + height: 650px; + + background: #FFFFFF; + box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.25); + border-radius: 20px; + margin: 27px auto; +`; + +const CabinetContainer = styled(Row)` + padding: 30px 26px 30px 36px; +`; + +const Avatar = styled.img` + width: 256px; + height: 256px; + + margin: 25px 0px 0px 65px; + + border-radius: 50%; + box-shadow: 0px 0px 12px 2px rgba(0, 0, 0, 0.34); +`; + +const UploadPhoto = styled.button` + display: flex; + align-items: center; + justify-content: center; + + width: 192px; + height: 32px; + + margin: 16px auto 0px; + + font-weight: 700; + font-size: 16px; + line-height: 19px; + color: #FFFFFF; + + background: #007EFF; + border-radius: 12px; +`; + +const WelcomeText = styled.p` + font-weight: 700; + font-size: 20px; + line-height: 24px; +`; + +const MainText = styled.p` + font-weight: 500; + font-size: 14px; + line-height: 24px; +`; + +const InputBlock = styled.input` + border: 1px solid #CCCCCC; + border-radius: 12px; + + width: ${props => props.width}px; + height: ${props => props.height}px; + + padding-left: 10px; + + font-weight: 500; + font-size: 14px; + line-height: 18px; + color: #000000; + + &::placeholder { + font-weight: 500; + font-size: 12px; + line-height: 24px; + color: #CCCCCC; + } +`; + +const TextAreaBlock = styled.textarea` + border: 1px solid #CCCCCC; + border-radius: 12px; + + margin: 16px 0px 0px 55px; + resize: none; + + padding: 8px; + + font-weight: 500; + font-size: 14px; + line-height: 18px; + color: #000000; + + &::placeholder { + font-weight: 500; + font-size: 14px; + line-height: 24px; + color: #CCCCCC; + } +`; + +const CharacterTraitBlock = styled.div` + width: calc(100% + 15px); + height: 28px; + + padding: 0; + + font-size: 14px; + line-height: 28px; + display: inline-block; + align-items: center; + text-align: center; + color: #FFFFFF; + + background: ${props => props.background}; + border-radius: 20px; +`; + +const ButtonCircleChangeTrait = styled.button` + width: 28px; + height: 28px; + + padding: 0px; + + font-weight: 700; + font-size: 20px; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + border-radius: 70%; + + color: #FFFFFF; + background: #D9D9D9; +`; + +const ButtonChangeTrait = styled.button` + margin-top: 14px; + + width: auto; + height: 27px; + + padding: 0; + + font-weight: 500; + font-size: 14px; + line-height: 17px; + + text-align: left; + + color: #007EFF; + background: none; +`; + +const InformationBlock = (props) => { + return ( + <> + + + {props.title} + + + {props.text} + + + + ); +} + +const InformationBlockInput = (props) => { + return ( + <> + + + {props.title} + + + + + + + ); +} + +const TraitsBlock = (props) => { + const list = props.list; + + if (list.length == 0) return ( + <> +

Вы ничего не указали

+ + ); + + return ( + <> + + { + list.map((el, i) => { + return ( + + + {el.text} + + + ); + }) + } + {props.button + ? + <> + + + + + + + + : + <> + Изменить.. + + } + + + ); +} + + +const PersonalCabinet = function () { + + const [user, setUser] = useState() + const userID = 1; + + const [fetchUser, isUserLoading, userError] = useFetching(async (userID) => { + const response = await User.getById(userID); + setUser(response) + console.log(user.name) + }) + + useEffect(() => { + fetchUser(userID) + }, []) + + const TraitsListOne = [{text: 'Честность', color: '#3F51B5'}, {text: 'Аккуратность', color: '#03BCD6'}, {text: 'Музыкальность', color: '#E91D65'}, + {text: 'Общительность', color: '#03A9F4'}, {text: 'Дружелюбность', color: '#8CC34D'}] + + const TraitsListTwo = [{text: 'Честность', color: '#3F51B5'}, {text: 'Аккуратность', color: '#03BCD6'}, {text: 'Музыкальность', color: '#E91D65'}, + {text: 'Общительность', color: '#03A9F4'}, {text: 'Дружелюбность', color: '#8CC34D'}, {text: 'Мудрость', color: '#FF5923'}, + {text: 'Адекватность', color: '#2196F4'}, {text: 'Щедрость', color: '#FFC308'}, {text: 'Вежливость', color: '#9D28B2'}] + + const TraitsListThree = [{text: 'Курит', color: '#94740B'}, {text: 'Равнодушие', color: '#D9B8B0'}, {text: 'Эгоист', color: '#6E3F58'}, {text: 'Лень', color: '#9F6844'}, + {text: 'Лживость', color: '#AD9029'}, {text: 'Диструктивность', color: '#9A150C'}, {text: 'Токсичность', color: '#608426'}] + + return ( + <> + + <BackButton> + <SVGIcon src='/images/icons/left-arrow-light.svg' width={10} height={16}/> + Вернуться назад + </BackButton> + <h2>Личный кабинет<span>/ Мои данные</span></h2> + + + + + + Добрый день, Александр! + Ваш статус - Студент + + + + Загрузить фото + + + + + + + + + + + + + + + + + + + + + + + Ваши личностные характеристики + + Желаемые черты соседа + + Нежелаемые черты соседа + + + + + + + ); +}; + +export default PersonalCabinet; \ No newline at end of file diff --git a/pairent_frontend_react/src/router/index.jsx b/pairent_frontend_react/src/router/index.jsx index f974609..3101195 100644 --- a/pairent_frontend_react/src/router/index.jsx +++ b/pairent_frontend_react/src/router/index.jsx @@ -6,6 +6,7 @@ import PsychTest from "../pages/PsychTest"; import Tinder from "../pages/Tinder"; import LoginPage from "../pages/LoginPage"; import LoggedIn from "../pages/LoggedIn"; +import PersonalCabinet from "../pages/PersonalCabinet"; // НА ПРОДАШКЕНЕ СДЕЛАТЬ ПРИВАТНЫЕ МАРШРУТЫ // export const privateRoutes = [ @@ -23,8 +24,9 @@ export default Object.freeze({ { path: "/comparisons", component: , exact: true }, { path: "/tinder", component: , exact: true }, { path: "/login", component: , exact: true }, - { path: "/sign-in", component: , exact: true }, + { path: "/sign-in", component: , exact: true }, { path: "/psych_test", component: , exact: true }, + { path: "/personal_cabinet", component: , exact: true }, ], privateRoutes: [], });