fix merge conflict

This commit is contained in:
Денис Сарапулов 2023-05-17 03:02:17 +10:00
commit 31cd4f98a8
10 changed files with 300 additions and 88 deletions

View File

@ -0,0 +1,87 @@
from django.http import HttpResponseBadRequest, HttpResponse, JsonResponse, HttpRequest
def VVSUAuthProxy(req: HttpRequest):
proxy = 'https://vvsu.ru/connect' + req.path[len('/api/auth/vvsu'):];
preq = requests.request(req.method, proxy, headers={
'User-Agent': 'OIDC Client / Pairent',
'Origin': 'http://pairent.vvsu.ru',
'Referer': 'http://pairent.vvsu.ru'
});
resp = HttpResponse(preq.content);
resp.headers['Content-Type'] = preq.headers['Content-Type'];
return resp;
def register(oid, provider_id, name):
user = User(
favorites_apartments='',
comparison_apartments='',
name=name,
# date_of_birth=,
about_me='',
gender='?',
phone='+00000',
# email=,
# telegram=,
# discord=,
# city=,
role='s',
photo_provider='VVSU',
openid_addr=oid,
openid_id=provider_id,
);
user.save();
return user;
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();
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;

View File

@ -17,22 +17,31 @@ class Migration(migrations.Migration):
name='User',
fields=[
('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)")),
('name', models.CharField(max_length=256, help_text='ФИО Пользователя')),
('date_of_birth', models.DateField(help_text='Дата рождения пользователя')),
('about_me', models.CharField(max_length=1000, help_text='Поле "О Себе"')),
('gender', models.CharField(max_length=1, help_text='Пол пользователя (f,m,n,?)')),
('favorites_apartments', models.CharField(max_length=100, help_text="Избранные квартиры (CSV)", null=True)),
('comparison_apartments', models.CharField(max_length=100, help_text="Квартиры для сравнения (CSV)", null=True)),
('name', models.CharField(max_length=500, help_text='ФИО Пользователя', null=True)),
('date_of_birth', models.DateField(help_text='Дата рождения пользователя', null=True)),
('about_me', models.CharField(max_length=1000, help_text='Поле "О Себе"', null=True)),
('gender', models.CharField(max_length=1, help_text='Пол пользователя (f,m,n,?)', null=True)),
('phone', models.CharField(max_length=30, help_text='Телефон пользователя в международном формате (+00000000)', null=True)),
('email', models.CharField(max_length=1000, help_text='Почтовый ящик пользователя в формате user@example.com', null=True)),
('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)),
('photo_provider', models.CharField(max_length=100, verbose_name='Сервис, из которого загружается фотография пользователя (VVSU, GRAVATAR)')),
('photo_provider', models.CharField(max_length=100, verbose_name='Сервис, из которого загружается фотография пользователя (VVSU, GRAVATAR)', null=True)),
('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 Пользователя в системе провайдера авторизации (скорее всего ВВГУ)'))
('openid_id', models.CharField(max_length=5000, null=False, verbose_name='ID Пользователя в системе провайдера авторизации (скорее всего ВВГУ)'))
]
),
migrations.CreateModel(
name='AuthToken',
fields=[
('user', models.BigIntegerField(null=False, verbose_name='ID Пользователя, которому принадлежит токен')),
('key', models.TextField(verbose_name='Ключ API')),
('expires', models.BigIntegerField(verbose_name='Когда ключ истечет (Unix timestamp)')),
('ip', models.CharField(max_length=16, verbose_name='IP, с которого был создан ключ'))
]
)
]

View File

@ -130,3 +130,9 @@ class PsychTestAnswers(models.Model):
class Meta:
verbose_name = "Ответ на психологический тест"
verbose_name_plural = "Ответы на психологический тест"
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)');
ip = models.CharField(max_length=16, verbose_name='IP, с которого был создан ключ');

View File

@ -1,6 +1,10 @@
from rest_framework import serializers
<<<<<<< HEAD
from .models import Apartament, User, PsychTestAnswers
=======
from .models import Apartament, User, AuthToken
>>>>>>> 4f5f817d835f6a53448e7d60bb7591c24c825e96
class ApartamentListSerializer(serializers.ModelSerializer):
@ -37,3 +41,7 @@ class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = "__all__"
class TokenSerializer(serializers.ModelSerializer):
class Meta:
model = AuthToken
fields = '__all__'

View File

@ -10,15 +10,19 @@ 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,
PsychTestReultsSerializer,
UserSerializer)
UserSerializer,
TokenSerializer)
import json, math, random, re, requests, oidc_client, base64, hashlib
from .authlib import *
import json, math, random, re, requests, oidc_client, base64, uuid, time, ipware as iplib
ipware = iplib.IpWare();
class ApartamentViewSet(viewsets.ReadOnlyModelViewSet):
"""Вывод списка квартир или отдельной квартиры"""
@ -163,58 +167,15 @@ class CompatibleUsersView(viewsets.ViewSet):
return Response(users);
def VVSUAuthProxy(req: Request):
proxy = 'https://vvsu.ru/connect' + req.path[len('/api/auth/vvsu'):];
preq = requests.request(req.method, proxy, headers={
'User-Agent': 'OIDC Client / Pairent',
'Origin': 'http://pairent.vvsu.ru',
'Referer': 'http://pairent.vvsu.ru'
});
resp = HttpResponse(preq.content);
resp.headers['Content-Type'] = preq.headers['Content-Type'];
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):
# for debug purposes
# return HttpResponse("""{"user_data": {"id": 1, "name": "\u041d\u0438\u043a\u0438\u0442\u0430 \u041f\u0443\u0441\u0442\u043e\u0432\u0430\u043b\u043e\u0432", "date_of_birth": null, "about_me": "", "gender": "?", "phone": "+00000", "email": null, "telegram": null, "discord": null, "city": null, "role": "s", "photo_provider": "VVSU", "openid_addr": "blek__@vvsu.ru", "openid_id": "096C78CD-4943-4D57-BC6D-5CDE12F686E3"}, "new_user": false, "token": {"id": 2, "user": 1, "key": "e1c24581-523a-4f60-973f-02ba873b3edc", "expires": 1684423572, "ip": "127.0.0.1"}}""");
if (req.session.has_key('auth_data')):
# TODO: Return user object instead of error
return JsonResponse({'error': 'already authenticated'})
@ -231,42 +192,44 @@ 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 = 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'}
if ('error' in auth_data):
return JsonResponse(auth_data);
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;
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 = cb.status_code;
res.status_code = 500;
return res;
if ('error' in vvsu_data):
res = JsonResponse(vvsu_data);
res.status_code = 500;
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']);
user = register(vvsu_data['vvsu_login'], vvsu_data['id'], f"{vvsu_data['given_name']} {vvsu_data['family_name']}");
new_user = True;
return JsonResponse({
'user_data': user,
'new_user': new_user
'user_data': PublicUserSerializer(user).data,
'new_user': new_user,
'token': TokenSerializer(create_auth_token(user.id, ipware.get_client_ip(req.META)[0].exploded)).data
});
class UserGet(APIView):

View File

@ -4,4 +4,4 @@ djangorestframework
django-cors-headers
Pillow
requests
oidc-client
python-ipware

View File

@ -0,0 +1,57 @@
/**
* Basic API interaction & local caching interface
*/
class IAPIObject {
/**
* Local storage key used to save data.
* @type {string}
*/
static storage_key = undefined;
static _checkStorageSupport() {
if (this.storage_key === undefined) {
throw Error('This doesn\'t support local storage');
}
}
_getStorageKey() {
return Object.getPrototypeOf(this).constructor.storage_key;
}
_IcheckStorageSupport() {
if (this._getStorageKey() === undefined) {
throw Error('This doesn\'t support local storage');
}
}
/** @returns {ThisType} */
static restoreFromLocalStorage() {
_checkStorageSupport();
if (!window.sessionStorage.getItem(this.storage_key))
return false;
return new APIToken(window.sessionStorage.getItem(this.storage_key));
}
/** @returns {boolean} */
static isCached() {
this._checkStorageSupport();
if (window.sessionStorage.getItem(this.storage_key)) {
return true;
}
return false;
}
/**
* Save this object to local storage
* @throws {QuotaExceededError}
* @returns {void}
*/
saveToLocalStorage() {
this._IcheckStorageSupport();
window.sessionStorage.setItem(this._getStorageKey(), JSON.stringify(this));
}
}
export { IAPIObject };

View File

@ -1,6 +1,8 @@
import axios from 'axios';
import constants from '../constants';
import { IAPIObject } from './IAPIObject';
const { API_ROOT, api_path } = constants;
class UserLoginResponse {
@ -14,8 +16,43 @@ class UserLoginResponse {
id;
}
class User {
class APIToken extends IAPIObject {
static storage_key = 'pairent_api_key';
constructor(data) {
super();
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 {
isLoggedIn() {
return false;
}
static storage_key = 'pairent_user_data';
constructor(data) {
super();
for (const key in data) {
this[key] = data[key];
}
@ -44,7 +81,16 @@ class User {
}
const data = await axios.post(api_path('/api/auth/user/login'), response);
return new User(data.data);
if (data.status !== 200) {
return false;
}
if (!data.data.error) {
new APIToken(data.data.token).saveToLocalStorage();
new User(data.data.user_data).saveToLocalStorage();
}
return data.data;
}
}

View File

@ -32,11 +32,11 @@ export default class LoggedIn extends React.Component {
constructor(props) {
super(props);
this.response = new SigninResponse(new URL(window.location.href).searchParams);
}
async componentDidMount() {
if (this.response.error) return;
window.localStorage.removeItem('auth_fail');
let code_verifier = '?';
// get code verifier
@ -48,7 +48,21 @@ 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.error) {
// pass the data to LoginPage
window.localStorage.setItem('auth_fail', JSON.stringify(response));
window.location.href = '/login';
return;
}
if (response.new_user) {
// TODO: Make the page
window.location.href = '/register';
} else {
window.location.href = '/';
}
}
render() {

View File

@ -8,6 +8,8 @@ import FloatingBox from '../../components/UI/FloatingBox';
import * as OpenID from 'oidc-client-ts';
import constants from '../../constants';
import { User } from '../../API/User';
const { OIDCConfig } = constants;
const LoginButton = styled(BlueButton)`
@ -45,9 +47,11 @@ export default class LoginPage extends React.Component {
super(props);
this.state = {
loading: false
loading: false,
error: JSON.parse(window.localStorage.getItem('auth_fail'))
}
window.localStorage.removeItem('auth_fail');
this.openid = this.openid.bind(this);
}
@ -56,15 +60,18 @@ export default class LoginPage extends React.Component {
this.setState({loading: true});
OpenID.Log.setLogger(console);
OpenID.Log.setLevel(OpenID.Log.DEBUG);
OpenID.Log.setLevel(OpenID.Log.NONE);
let client = new OpenID.UserManager(OIDCConfig);
client.signinRedirect();
}
render() {
if (User.isCached())
window.location.href = '/';
return (
<div style={{height: '65vh'}}>
<FloatingBox>
@ -84,6 +91,21 @@ export default class LoginPage extends React.Component {
Вход осуществляется только через<br/>
Систему Единого Входа ВВГУ
</SmallText>
{
this.state.error ?
<SmallText style={{color: 'darkred', fontWeight: '600'}}>
Произошла ошибка: { this.state.error.error }
{
this.state.error.error_description ?
<>
<br/>
{this.state.error.error_description}
</>
: null
}
</SmallText>
: null
}
</FloatingBox>
</div>
);