Web M&M
https://mam.matfyz.cz
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
185 lines
5.4 KiB
185 lines
5.4 KiB
"""
|
|
Registrace uživatelů k existujícím osobám
|
|
|
|
V tomto souboru bude asi všechno, co je relevantní (kromě template), protože to
|
|
je dostatečně malá a jednorázová věc.
|
|
|
|
Importovat prosím jen ty dva Views, ve výjimečných případech invite, nic dalšího.
|
|
"""
|
|
|
|
#TODO: Logování (tohle logovat chce skoro určitě)
|
|
#TODO: Omezení počtu pokusů (per token -- města / údaje se bruteforcit dají, na rozdíl od tokenů)
|
|
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from django import forms
|
|
from django.contrib.auth.models import User, Permission
|
|
from django.forms import Form
|
|
from django.views import View
|
|
from django.views.generic.base import TemplateResponseMixin
|
|
import hmac
|
|
from typing import Optional
|
|
|
|
from django.conf.settings import SECRET_KEY
|
|
from seminar.models import Osoba
|
|
|
|
# Složitější class-based views mi neumožňují vracet chyby.
|
|
class DodatecnaRegistraceUzivateleView(TemplateResponseMixin, View):
|
|
template_name = ...
|
|
form = RegistraceUzivateleForm
|
|
|
|
def get(self, request, url_token):
|
|
# Ověřit token
|
|
tok_data = verify_token(UseCase.email, url_token)
|
|
if tok_data is None:
|
|
return render_to_response(
|
|
context={
|
|
'error': 'Token není platný',
|
|
},
|
|
status_code=400,
|
|
)
|
|
|
|
osoba_id, tok_timestamp = tok_data # Token prostě vypadá takhle, je to zafixované. Pokud tak nevypadá, je něco moc špatně.
|
|
|
|
# Zkontrolovat, že to není moc staré poslání
|
|
now = datetime.now()
|
|
token_generation = datetime.fromisoformat(tok_timestamp)
|
|
delta = now - token_generation
|
|
if delta >= timedelta(weeks=10):
|
|
return render_to_response(
|
|
context={
|
|
'error': 'Vypršela časová platnost tokenu',
|
|
},
|
|
status_code=400,
|
|
)
|
|
|
|
|
|
# Najít osobu
|
|
osoba = m.Osoba.objects.get(id=osoba_id)
|
|
if osoba.user is not None:
|
|
return render_to_response(
|
|
context={
|
|
'error': 'Už máte uživatele',
|
|
},
|
|
status_code=400,
|
|
)
|
|
# Vrátit view s formulářem a formulářovým tokenem
|
|
form_token = gen_token(UseCase.form, osoba=osoba_id, timestamp=datetime.now())
|
|
return render_to_response(
|
|
context={
|
|
'form': self.form(initial={
|
|
'token': form_token,
|
|
}),
|
|
}
|
|
)
|
|
|
|
def post(self, request, url_token):
|
|
# Zkontrolovat formulář
|
|
form = self.form(self.request.POST)
|
|
if not form.is_valid():
|
|
return render_to_response(
|
|
context={
|
|
'error': 'Chyba ve formuláři', # TODO: Umíme dostat konkrétní detaily?
|
|
# TODO: Formulář pro zkusení znovu?
|
|
},
|
|
status_code=400,
|
|
)
|
|
|
|
form_data = form.cleaned_data
|
|
# Zkontrolovat token
|
|
token_data = verify_token(UseCase.form, form_data['token'])
|
|
if token_data is None:
|
|
return render_to_response(
|
|
context={
|
|
'error': 'Neplatný token',
|
|
},
|
|
status_code=400,
|
|
)
|
|
osoba_id = int(token_data['osoba'])
|
|
osoba = m.Osoba.objects.get(id=osoba_id)
|
|
# Zkontrolovat verifikační field
|
|
...
|
|
# Vyrobit uživatele
|
|
u = User.objects.create_user(
|
|
username=form_data['username'],
|
|
password=form_data['password'],
|
|
email=osoba.email,
|
|
#first_name=o.jmeno,
|
|
#last_name=o.prijmeni,
|
|
)
|
|
u.user_permissions.add(Permission.objects.get(codename__exact='resitel'))
|
|
# Přesměrovat na kontrolu údajů
|
|
return ...
|
|
|
|
class KontrolaUdajuASouhlasyView(FormView):
|
|
...
|
|
|
|
class RegistraceUzivateleForm(Form):
|
|
#model = User
|
|
# Zkopírováno z přihlášky :-)
|
|
username = forms.CharField(label='Přihlašovací jméno',
|
|
max_length=256,
|
|
required=True,
|
|
help_text='Tímto jménem se následně budeš přihlašovat pro odevzdání řešení a další činnosti v semináři')
|
|
password = forms.CharField(
|
|
label='Heslo',
|
|
max_length=256,
|
|
required=True,
|
|
widget=forms.PasswordInput())
|
|
password_check = forms.CharField(
|
|
label='Ověření hesla',
|
|
max_length=256,
|
|
required=True,
|
|
widget=forms.PasswordInput())
|
|
|
|
# Dodatečné fieldy + token…
|
|
token = forms.CharField(widget=forms.HiddenInput(), required=True)
|
|
verifikace_TODO = ... # TODO: Co verifikovat
|
|
|
|
# TODO: clean_username, verifikace …
|
|
|
|
def invite(osoba: Osoba):
|
|
"""
|
|
Pošle dané osobě e-mail s odkazem na registraci.
|
|
"""
|
|
...
|
|
|
|
# Pozor, tokeny existují dva: jeden do URL do mailu, druhý do hidden položky ve formuláři.
|
|
class UseCase(Enum):
|
|
email = 'email'
|
|
form = 'form'
|
|
|
|
# Token kóduje všechno v sobě
|
|
# Tokenové metody zároveň řeší konzistentní zakódování dat do tokenu, takže už to nemusí řešit nikdo jiný.
|
|
HMAC_FUNCTION='sha-256'
|
|
def gen_token(usecase: _UseCase, *, osoba_id: int, timestamp: datetime) -> str:
|
|
strmsg = '@'.join([usecase.value, str(osoba_id), timestamp.isoformat()])
|
|
msg = bytes(strmsg, 'utf-8')
|
|
key = bytes(SECRET_KEY, 'utf-8')
|
|
mac = hmac.mac(key, msg, HMAC_FUNCTION).hexdigest()
|
|
return mac+'@'+msg
|
|
|
|
def verify_token(usecase: _UseCase, token: str) -> Optional[Tuple[int, datetime]]:
|
|
"""
|
|
Vrací dvojici dat, pokud je token validní, jinak None.
|
|
|
|
Inspirováno OSMO, je díky tomu jednoduché zároveň předat dekódovaná data a
|
|
výsledek ověření.
|
|
"""
|
|
try:
|
|
tok_mac, uc, tok_osoba, tok_ts = token.split('@')
|
|
except ValueError:
|
|
# Nepodařilo se rozbít na právě čtyři části, takže je token špatně.
|
|
return None
|
|
strmsg = '@'.join([usecase.value, tok_osoba, tok_ts])
|
|
msg = bytes(strmsg, 'utf-8')
|
|
key = bytes(SECRET_KEY, 'utf-8')
|
|
valid_mac = hmac.mac(key, msg, HMAC_FUNCTION).hexdigest()
|
|
|
|
if hmac.compare_digest(tok_mac, valid_mac):
|
|
# HMAC sedí, takže data jsou v pořádku
|
|
osoba_id = int(tok_osoba)
|
|
ts = datetime.fromisoformat(tok_ts)
|
|
return (osoba_id, ts)
|
|
else:
|
|
return None
|
|
|