6 Commits

Author SHA1 Message Date
  alfred 26ed9a498b Register actions 1 month ago
  alfred 3d5d98c42a Add authorization by username and password 1 month ago
  alfred 31f97eda16 Authorize with JWT 1 month ago
  alfred 96bf9491b0 Login with passwordless method 1 month ago
  alfred 0c0eaa3c54 Update passwordless uml 1 month ago
  alfred d63eddf009 Invite and confirm users 1 month ago

+ 37
- 2
apps/authentication/authenticators.py View File

@@ -1,27 +1,57 @@
1 1
 from apps.authentication import models, forms, mixins
2 2
 from apps.base.classes import DJaWThObject
3
+from django.utils import timezone
3 4
 
4 5
 
5 6
 class Authenticator(DJaWThObject):
6 7
     methods = []
7 8
     project_methods = {}
9
+    credentials_model = None
8 10
 
9 11
     class NotAllowed(Exception):
10 12
         pass
11 13
 
12
-    def dispatch(self, action, project, request):
14
+    class CantConfirmIdentity(Exception):
15
+        pass
16
+
17
+    def dispatch(self, project, action, request, **kwargs):
13 18
         if action not in self.methods:
14 19
             raise self.NotAllowed(action)
15 20
         f = getattr(self, action)
16
-        return f(self, project, request)
21
+        return f(project, request, **kwargs)
17 22
 
18 23
     def login(self, project, request):
19 24
         raise NotImplementedError
20 25
 
26
+    def check_confirmation(self, project, identity, token):
27
+        try:
28
+            assert project.identities.filter(pk=identity.pk).exists()
29
+            assert identity.data['confirm_token'] == token
30
+        except AssertionError:
31
+            raise self.CantConfirmIdentity
32
+
33
+    def confirm_identity(self, project, identity, token, request, **kwargs):
34
+        from django.http import HttpResponseRedirect, HttpResponse
35
+        self.check_confirmation(project, identity, token)
36
+        identity.confirmed = timezone.now()
37
+        del identity.data['confirm_token']
38
+        identity.save()
39
+        return HttpResponse()  # TODO: HttpResponseRedirect(redirect_to=project.identity_confirmed_url)
40
+
41
+    def valid_login(self, project, identity, request, **kwargs):
42
+        from apps.project.models import Record
43
+        Record.register(action='valid login', request=request, identity=identity)
44
+        return project.authorization.authorize(project, identity, request, **kwargs)
45
+
46
+    def invalid_login(self, project, identity, request, **kwargs):
47
+        from apps.project.models import Record
48
+        Record.register(action='invalid login', request=request, identity=identity)
49
+
21 50
 
22 51
 class PasswordlessAuthenticator(mixins.PasswordlessMixin, Authenticator):
23 52
     alias = 'Passwordless'
24 53
     model = models.PasswordlessModel
54
+    credentials_model = models.PasswordlessCredentialsModel
25 55
     template = 'authentication/passwordless.html'
26 56
     help = 'authentication/passwordless-help.html'
27 57
     form = forms.PasswordlessForm
@@ -30,4 +60,9 @@ class PasswordlessAuthenticator(mixins.PasswordlessMixin, Authenticator):
30 60
 
31 61
 class UserPasswordAuthenticator(mixins.UserPasswordMixin, Authenticator):
32 62
     alias = 'User - Password'
63
+    model = models.UserPasswordModel
64
+    credentials_model = models.UserPasswordCredentialsModel
65
+    template = None
66
+    help = None
67
+    methods = ['change_password']
33 68
     project_methods = {'create_user': 'Create user'}

+ 1
- 0
apps/authentication/forms/__init__.py View File

@@ -0,0 +1 @@
1
+from .passwordless import PasswordlessForm

apps/authentication/forms.py → apps/authentication/forms/passwordless.py View File

@@ -1,5 +1,5 @@
1 1
 from apps.base.forms import DataclassForm
2
-from .models import PasswordlessModel
2
+from apps.authentication.models import PasswordlessModel
3 3
 
4 4
 
5 5
 class PasswordlessForm(DataclassForm):

+ 47
- 1
apps/authentication/mixins/passwordless.py View File

@@ -1,3 +1,49 @@
1
+from django.http import HttpResponse
2
+from django.core.mail import send_mail
3
+from django.conf import settings
4
+from django.template.loader import render_to_string
5
+import secrets
6
+import json
7
+import urllib.parse as urlparse
8
+from urllib.parse import urlencode
9
+
10
+
1 11
 class PasswordlessMixin:
12
+    def get_authorization_url(self, identity):
13
+        url_parts = list(urlparse.urlparse(self.data.web_hook_base))
14
+        query = dict(urlparse.parse_qsl(url_parts[4]))
15
+        query.update({
16
+            self.data.access_parameter: identity.credentials.token,
17
+            'user': str(identity.uuid)
18
+        })
19
+        url_parts[4] = urlencode(query)
20
+        return urlparse.urlunparse(url_parts)
21
+
22
+    def send_token_email(self, project, identity):
23
+        mail = identity.email.address
24
+        message = render_to_string(
25
+            'authentication/mailing/passwordless_token.html',
26
+            context={
27
+                'project': project,
28
+                'authorization_url': self.get_authorization_url(identity)
29
+            }
30
+        )
31
+        send_mail(f'Access to {project.name}', message, settings.EMAILS_FROM, [mail])
32
+
2 33
     def login(self, project, request):
3
-        pass
34
+        data = json.loads(request.body)
35
+        identity = project.identities.get(email__address=data['email'])
36
+        token = secrets.token_urlsafe(20)
37
+        identity.credentials = self.credentials_model(token=token)
38
+        identity.save()
39
+        self.send_token_email(project, identity)
40
+        return HttpResponse()
41
+
42
+    def authorize(self, project, request, **kwargs):
43
+        try:
44
+            data = json.loads(request.body)
45
+            identity = project.identities.get(pk=data['user'])
46
+            assert identity.credentials.token == data['token']
47
+            return self.valid_login(project, identity, request, **kwargs)
48
+        except AssertionError:
49
+            return self.invalid_login(project, identity, request, **kwargs)

+ 33
- 0
apps/authentication/mixins/userpassword.py View File

@@ -1,3 +1,36 @@
1
+from django.http import HttpResponse
2
+from django.views.generic.base import TemplateResponse
3
+from django.contrib.auth.hashers import make_password, check_password
4
+import json
5
+
6
+
1 7
 class UserPasswordMixin:
2 8
     def login(self, project, request):
9
+        try:
10
+            data = json.loads(request.body)
11
+            identity = project.identities.get(email__address=data['username'])
12
+            assert check_password(data['password'], identity.credentials.password)
13
+            return self.valid_login(project, identity, request)
14
+        except AssertionError:
15
+            return self.invalid_login(project, identity, request)
16
+
17
+    def confirm_identity(self, project, identity, token, request, **kwargs):
18
+        from django.contrib.auth.forms import SetPasswordForm
19
+        if request.method == 'POST':
20
+            form = SetPasswordForm(user=identity, data=request.POST)
21
+            if form.is_valid():
22
+                password = make_password(form.cleaned_data['new_password1'])
23
+                identity.credentials = self.credentials_model(password=password)  # in confirm_identity it will be saved
24
+                return super().confirm_identity(project, identity, token, request, **kwargs)
25
+        elif request.method == 'GET':
26
+            form = SetPasswordForm(user=identity)
27
+        return TemplateResponse(
28
+            request=request,
29
+            template='authentication/userpassword/set_password.html',
30
+            context={
31
+                'form': form
32
+            }
33
+        )
34
+
35
+    def change_password(self, project, request, **kwargs):
3 36
         pass

+ 17
- 1
apps/authentication/models.py View File

@@ -5,4 +5,20 @@ from dataclasses import dataclass, field
5 5
 class PasswordlessModel:
6 6
     one_use_access: bool = field(default=True, metadata={'label': 'One use access'})
7 7
     web_hook_base: str = field(default='https://example.com/authenticate', metadata={'label': 'Webhook'})
8
-    access_parameter: str = field(default='access', metadata={'label': 'Access parameter name'})
8
+    access_parameter: str = field(default='token', metadata={'label': 'Access parameter name'})
9
+
10
+
11
+@dataclass
12
+class PasswordlessCredentialsModel:
13
+    token: str
14
+
15
+
16
+@dataclass
17
+class UserPasswordModel:
18
+    pass
19
+
20
+
21
+@dataclass
22
+class UserPasswordCredentialsModel:
23
+    password: str
24
+

+ 3
- 0
apps/authentication/templates/authentication/mailing/passwordless_token.html View File

@@ -0,0 +1,3 @@
1
+Hi!
2
+To access to your data into {{ project.name }}, please, click on the next link:
3
+{{ authorization_url | safe }}

+ 25
- 0
apps/authentication/templates/authentication/userpassword/set_password.html View File

@@ -0,0 +1,25 @@
1
+ <!DOCTYPE html>
2
+<!--[if lte IE 6]><html class="preIE7 preIE8 preIE9"><![endif]-->
3
+<!--[if IE 7]><html class="preIE8 preIE9"><![endif]-->
4
+<!--[if IE 8]><html class="preIE9"><![endif]-->
5
+<!--[if gte IE 9]><!--><html><!--<![endif]-->
6
+  <head>
7
+    <meta charset="UTF-8">
8
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
9
+  <meta name="viewport" content="width=device-width,initial-scale=1">
10
+    <title>title</title>
11
+  <meta name="author" content="name">
12
+  <meta name="description" content="description here">
13
+  <meta name="keywords" content="keywords,here">
14
+  <link rel="shortcut icon" href="favicon.ico" type="image/vnd.microsoft.icon">
15
+  <style type="text/css">
16
+
17
+  </style>
18
+  </head>
19
+  <body>
20
+  <form method="post">
21
+      {{ form }}
22
+      <button type="submit">Set password</button>
23
+  </form>
24
+  </body>
25
+</html>

+ 6
- 2
apps/authorization/authorizers.py View File

@@ -1,9 +1,13 @@
1 1
 from apps.base.classes import DJaWThObject
2
+from .mixins import JwtAuthorizerMixin
3
+from apps.authorization import models
2 4
 
3 5
 
4 6
 class Authorizer(DJaWThObject):
5
-    pass
7
+    def authorize(self, project, identity, request, **kwargs):
8
+        raise NotImplementedError
6 9
 
7 10
 
8
-class JwtAuthorizer(Authorizer):
11
+class JwtAuthorizer(JwtAuthorizerMixin, Authorizer):
9 12
     alias = 'JWT'
13
+    model = models.JwtModel

+ 1
- 0
apps/authorization/mixins/__init__.py View File

@@ -0,0 +1 @@
1
+from .jwt import JwtAuthorizerMixin

+ 14
- 0
apps/authorization/mixins/jwt.py View File

@@ -0,0 +1,14 @@
1
+import jwt
2
+from datetime import datetime
3
+from django.http import JsonResponse
4
+
5
+
6
+class JwtAuthorizerMixin:
7
+    def authorize(self, project, identity, request, **kwargs):
8
+        new_timestamp = datetime.utcnow()
9
+        token = jwt.encode({
10
+            'uid': str(identity.uuid),
11
+            'timestamp': str(new_timestamp),
12
+            **kwargs
13
+        }, project.secret, algorithm='HS256')
14
+        return JsonResponse({'token': token.decode('utf-8')})

+ 6
- 0
apps/authorization/models.py View File

@@ -0,0 +1,6 @@
1
+from dataclasses import dataclass
2
+
3
+
4
+@dataclass
5
+class JwtModel:
6
+    pass

+ 2
- 2
apps/base/static/img/uml/passwordless.svg
File diff suppressed because it is too large
View File


+ 4
- 0
apps/base/static/styles/scss/stylesheet.scss View File

@@ -102,6 +102,10 @@ select {
102 102
         color: $primary-background-color;
103 103
     }
104 104
 
105
+    input[type=submit] {
106
+        margin-top: 10px;
107
+    }
108
+
105 109
     .no-projects {
106 110
         display: flex;
107 111
         height: 200px;

+ 2
- 0
apps/base/static/styles/stylesheet.css View File

@@ -6523,6 +6523,8 @@ select {
6523 6523
   margin-top: 1.5rem; }
6524 6524
   .content table, .content thead, .content tbody, .content tr, .content th, .content td, .content select {
6525 6525
     color: #1F2439; }
6526
+  .content input[type=submit] {
6527
+    margin-top: 10px; }
6526 6528
   .content .no-projects {
6527 6529
     display: flex;
6528 6530
     height: 200px;

+ 7
- 0
apps/base/utils.py View File

@@ -0,0 +1,7 @@
1
+def get_ip(request):
2
+    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
3
+    if x_forwarded_for:
4
+        ip = x_forwarded_for.split(',')[0]
5
+    else:
6
+        ip = request.META.get('REMOTE_ADDR')
7
+    return ip

+ 5
- 0
apps/project/forms.py View File

@@ -18,3 +18,8 @@ class EditProjectForm(forms.ModelForm):
18 18
     class Meta:
19 19
         model = Project
20 20
         fields = ['name', 'secret']
21
+
22
+
23
+class InviteUserForm(forms.Form):
24
+    address = forms.EmailField(required=True)
25
+    alias = forms.CharField(required=False)

+ 82
- 16
apps/project/models.py View File

@@ -4,9 +4,14 @@ from apps.authentication import auth_methods as authentication_methods
4 4
 from apps.authorization import authorization_methods
5 5
 from django.utils.text import slugify
6 6
 from django.contrib.postgres.fields import JSONField
7
+import uuid
8
+from apps.project import utils
7 9
 
8 10
 
9 11
 class Project(models.Model):
12
+    class Meta:
13
+        db_table = 'platforms'
14
+
10 15
     name = models.CharField(max_length=255, blank=False, null=False)
11 16
     owner = models.ForeignKey(base_models.User, on_delete=models.CASCADE, related_name='projects')
12 17
     identifier = models.CharField(max_length=265, blank=False, null=False, unique=True)
@@ -23,6 +28,12 @@ class Project(models.Model):
23 28
     ])
24 29
     authorization_data = JSONField(default=dict)
25 30
 
31
+    class IdentityAlreadyExists(Exception):
32
+        pass
33
+
34
+    class IdentityAlreadyConfirmed(Exception):
35
+        pass
36
+
26 37
     @classmethod
27 38
     def get_identifier(cls, name, value=0):
28 39
         _name = name if value == 0 else f'{name} {value}'
@@ -37,24 +48,50 @@ class Project(models.Model):
37 48
     def authentication(self):
38 49
         return authentication_methods[self.auth_type](**self.auth_data)
39 50
 
40
-    def get_identity(self, **kwargs):
41
-        return self.users.get(**kwargs)
51
+    @property
52
+    def identity_confirmed_url(self):
53
+        from django.urls import reverse
54
+        return reverse('confirmed')
42 55
 
43 56
     @property
44 57
     def authorization(self):
45
-        return authentication_methods[self.authorization_type](**self.authorization_data)
58
+        return authorization_methods[self.authorization_type](**self.authorization_data)
59
+
60
+    def invite(self, request, address, alias=None, force_send_invitation=False):
61
+        import secrets
62
+
63
+        email, _ = Email.objects.get_or_create(address=address)
64
+        identity, identity_created = Identity.objects.get_or_create(email=email, project=self)
65
+
66
+        if identity.confirmed:
67
+            raise self.IdentityAlreadyConfirmed
68
+        elif not identity_created and not force_send_invitation:
69
+            raise self.IdentityAlreadyExists
70
+
71
+        if alias is not None:
72
+            identity.data['alias'] = alias
73
+        identity.data['confirm_token'] = secrets.token_urlsafe(20)
74
+        identity.save()
75
+
76
+        utils.send_project_invitation(
77
+            project=self,
78
+            identity=identity,
79
+            request=request
80
+        )
46 81
 
47 82
 
48 83
 class Email(models.Model):
49
-    uuid = models.UUIDField(unique=True)
50
-    address = models.EmailField()
84
+    class Meta:
85
+        db_table = 'emails'
86
+
87
+    uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)
88
+    address = models.EmailField(unique=True)
51 89
     created = models.DateTimeField(auto_now_add=True)
52
-    confirmed = models.DateTimeField(null=True)
53 90
     data = JSONField(default=dict)
54 91
 
55 92
     @property
56 93
     def is_confirmed(self):
57
-        return self.confirmed is not None
94
+        return any([identity.is_confirmed for identity in self.identities])
58 95
 
59 96
     def confirm(self):
60 97
         from django.utils import timezone
@@ -66,33 +103,62 @@ class Email(models.Model):
66 103
 
67 104
 class Identity(models.Model):
68 105
     class Meta:
106
+        db_table = 'identities'
69 107
         unique_together = ['email', 'project']
70
-    uuid = models.UUIDField(unique=True)
108
+
109
+    uuid = models.UUIDField(primary_key=True, unique=True, default=uuid.uuid4, editable=False)
71 110
     email = models.ForeignKey(Email, on_delete=models.CASCADE, related_name='identities')
72
-    project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='users')
73
-    confirmed = models.BooleanField(default=False)
111
+    project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='identities')
112
+    confirmed = models.DateTimeField(default=None, null=True, blank=True)
74 113
     created = models.DateTimeField(auto_now_add=True)
75
-    credentials = JSONField(default=dict)
114
+    credentials_data = JSONField(default=dict)
115
+    data = JSONField(default=dict)
76 116
 
77 117
     class NotConfirmedException(Exception):
78 118
         pass
79 119
 
80
-    @classmethod
81
-    def invite(cls, email, project):
82
-        pass
120
+    @property
121
+    def is_confirmed(self):
122
+        return self.confirmed is not None
83 123
 
124
+    @property
84 125
     def is_allowed(self):
85
-        if not self.confirmed:
126
+        if not self.is_confirmed():
86 127
             raise self.NotConfirmedException()
87 128
         return True
88 129
 
130
+    @property
131
+    def credentials(self):
132
+        return self.project.authentication.credentials_model(**self.credentials_data)
133
+
134
+    @credentials.setter
135
+    def credentials(self, value):
136
+        from dataclasses import asdict
137
+        assert type(value) == self.project.authentication.credentials_model
138
+        self.credentials_data = asdict(value)
139
+
89 140
     def has_logged_in(self):
90 141
         return self.history.objects.count() > 0
91 142
 
92 143
 
93 144
 class Record(models.Model):
94
-    identity = models.ForeignKey(Identity, on_delete=models.CASCADE, related_name='history')
145
+    class Meta:
146
+        db_table = 'registry'
147
+
148
+    uuid = models.UUIDField(primary_key=True, unique=True, default=uuid.uuid4, editable=False)
149
+    identity = models.ForeignKey(Identity, on_delete=models.CASCADE, related_name='history', null=True)
95 150
     ip = models.GenericIPAddressField()
96 151
     action = models.CharField(max_length=100, blank=True, null=True)
97 152
     date = models.DateTimeField(auto_now_add=True)
98 153
     data = JSONField(default=dict)
154
+
155
+    @classmethod
156
+    def register(cls, action, request, identity=None, **kwargs):
157
+        from apps.base import utils
158
+        record = Record(
159
+            identity=identity,
160
+            ip=utils.get_ip(request),
161
+            action=action,
162
+            data=kwargs
163
+        )
164
+        record.save()

+ 13
- 0
apps/project/templates/projects/invite_user.html View File

@@ -0,0 +1,13 @@
1
+{% extends "base.html" %}
2
+
3
+{% block title %}
4
+<h2>Invite user to {{ object.name }}</h2>
5
+{% endblock %}
6
+
7
+{% block content %}
8
+<form method="post" action="{% url 'invite_user' identifier=project.identifier %}">
9
+{% csrf_token %}
10
+{{ form  }}
11
+<input type="submit" class="btn" value="Send">
12
+</form>
13
+{% endblock %}

+ 4
- 0
apps/project/templates/projects/mailing/project_invitation.html View File

@@ -0,0 +1,4 @@
1
+Hi!
2
+You were invited to take part into {{ project.name }}. Please, confirm you want to be member of this wonderful project:
3
+{{ confirm_url | safe }}
4
+For any doubt about this mail, please, contact: {{ project.email }}

+ 2
- 2
apps/project/templates/projects/project.html View File

@@ -5,7 +5,7 @@
5 5
 <div class="section-menu">
6 6
     <ul>
7 7
         <li><a href="#!">Users list</a></li>
8
-        <li><a href="#!">Invite user</a></li>
8
+        <li><a href="{% url 'invite_user' identifier=object.identifier %}">Invite user</a></li>
9 9
         {% for action, text in object.authentication.project_methods.items %}
10 10
         <li><a href="!#">{{ text }}</a></li>
11 11
         {% endfor %}
@@ -14,7 +14,7 @@
14 14
 {% endblock %}
15 15
 
16 16
 {% block content %}
17
-<form method="post" action="{% url 'project' identifier=object.identifier %}">
17
+<form method="post" action="{% url 'project' identifier=project.identifier %}">
18 18
 {% csrf_token %}
19 19
 {{ form  }}
20 20
 <input type="submit" class="btn" value="Update">

+ 20
- 11
apps/project/urls.py View File

@@ -4,11 +4,12 @@
4 4
 from django.urls import path, reverse_lazy
5 5
 from django.views.generic import TemplateView, UpdateView
6 6
 from django.contrib.auth.decorators import login_required
7
-from .views import ConfirmProjectView, UpdateAuthenticationView, is_project_owner
7
+from django.views.decorators.csrf import csrf_exempt
8
+from apps.project import views
8 9
 from .models import Project
9 10
 from apps.authentication import auth_methods
10 11
 from apps.authorization import authorization_methods
11
-from .forms import EditProjectForm, ConfirmProjectForm
12
+from apps.project import forms
12 13
 
13 14
 
14 15
 urlpatterns = [
@@ -24,22 +25,22 @@ urlpatterns = [
24 25
         }
25 26
     )), name='new_project'),
26 27
 
27
-    path('confirm_project', login_required(ConfirmProjectView.as_view(
28
+    path('confirm_project', login_required(views.ConfirmProjectView.as_view(
28 29
         template_name='projects/confirm_project.html',
29 30
         success_url='home',
30
-        form_class=ConfirmProjectForm
31
+        form_class=forms.ConfirmProjectForm
31 32
     )), name='confirm_project'),
32 33
 
33
-    path('<identifier>', is_project_owner(UpdateView.as_view(
34
+    path('<identifier>', views.is_project_owner(UpdateView.as_view(
34 35
         model=Project,
35 36
         slug_field='identifier',
36 37
         slug_url_kwarg='identifier',
37
-        form_class=EditProjectForm,
38
+        form_class=forms.EditProjectForm,
38 39
         template_name='projects/project.html',
39 40
         success_url=reverse_lazy('home')
40 41
     )), name='project'),
41 42
 
42
-    path('<identifier>/authentication', is_project_owner(UpdateAuthenticationView.as_view(
43
+    path('<identifier>/authentication', views.is_project_owner(views.UpdateAuthenticationView.as_view(
43 44
         model=Project,
44 45
         slug_field='identifier',
45 46
         slug_url_kwarg='identifier',
@@ -47,10 +48,18 @@ urlpatterns = [
47 48
         success_url=reverse_lazy('home')
48 49
     )), name='edit_authentication'),
49 50
 
50
-    # TODO: path('<identifier>/login')
51
+    path('<identifier>/invite', views.is_project_owner(views.InviteUserView.as_view(
52
+        form_class=forms.InviteUserForm,
53
+        template_name='projects/invite_user.html'
54
+    )), name='invite_user'),
55
+
56
+    path('confirm/<identifier>/<uuid>/<token>', csrf_exempt(views.ConfirmIdentity.as_view()), name='confirm_identity'),
57
+
58
+    path('<identifier>/login', csrf_exempt(views.LoginProjectView.as_view()), name='project_login'),
59
+
60
+    path('<identifier>/auth/<action>', csrf_exempt(views.AuthenticationAction.as_view()), name='action')
61
+
51 62
     # TODO: path('<identifier>/users')
52 63
     # TODO: path('<identifier>/<uuid>/history')
53
-    # TODO: path('<identifier>/invite')
54
-    # TODO: path('confirm/identity/<identifier>/<uuid>')
55
-    # TODO: path('auth/<identifier>/<action>')
64
+    # TODO: path('<identifier>/auth/<action>')
56 65
 ]

+ 20
- 0
apps/project/utils.py View File

@@ -0,0 +1,20 @@
1
+from django.core.mail import send_mail
2
+from django.conf import settings
3
+from django.urls import reverse
4
+from django.template.loader import render_to_string
5
+
6
+
7
+def send_project_invitation(project, identity, request):
8
+    mail = identity.email.address
9
+    message = render_to_string(
10
+        'projects/mailing/project_invitation.html',
11
+        context={
12
+            'project': project,
13
+            'confirm_url': request.build_absolute_uri(reverse('confirm_identity', kwargs={
14
+                'identifier': project.identifier,
15
+                'uuid': str(identity.uuid),
16
+                'token': identity.data['confirm_token']
17
+            }))
18
+        }
19
+    )
20
+    send_mail(f'Invitation to { project.name }', message, settings.EMAILS_FROM, [mail])

+ 57
- 1
apps/project/views.py View File

@@ -1,4 +1,4 @@
1
-from django.views.generic import CreateView, UpdateView
1
+from django.views.generic import CreateView, UpdateView, FormView, View
2 2
 from django.http import QueryDict, HttpResponseRedirect
3 3
 from django.contrib.auth.decorators import user_passes_test
4 4
 
@@ -70,3 +70,59 @@ class UpdateAuthenticationView(UpdateView):
70 70
             **super().get_context_data(**kwargs),
71 71
             'auth': self.get_authentication()
72 72
         }
73
+
74
+
75
+class InviteUserView(FormView):
76
+    def get_project(self):
77
+        from .models import Project
78
+        identifier = self.kwargs.get('identifier')
79
+        return Project.objects.get(identifier=identifier)
80
+
81
+    def form_valid(self, form):
82
+        project = self.get_project()
83
+        project.invite(self.request, **form.cleaned_data)
84
+        # TODO: Add message
85
+        return super().form_valid(form)
86
+
87
+    def get_success_url(self):
88
+        from django.urls import reverse
89
+        return reverse('project', kwargs={'identifier': self.get_project().identifier})
90
+
91
+    def get_context_data(self, **kwargs):
92
+        return {
93
+            **super().get_context_data(**kwargs),
94
+            'project': self.get_project()
95
+        }
96
+
97
+
98
+class ConfirmIdentity(View):
99
+    def perform(self, request, *args, **kwargs):
100
+        from .models import Project, Identity
101
+        identifier = kwargs.get('identifier')
102
+        uuid = kwargs.get('uuid')
103
+        token = kwargs.get('token')
104
+        project = Project.objects.get(identifier=identifier)
105
+        identity = Identity.objects.get(pk=uuid)
106
+        return project.authentication.confirm_identity(project, identity, token, request)
107
+
108
+    def get(self, request, *args, **kwargs):
109
+        return self.perform(request, *args, **kwargs)
110
+
111
+    def post(self, request, *args, **kwargs):
112
+        return self.perform(request, *args, **kwargs)
113
+
114
+
115
+class LoginProjectView(View):
116
+    def post(self, request, *args, **kwargs):
117
+        from .models import Project
118
+        identifier = kwargs.get('identifier')
119
+        project = Project.objects.get(identifier=identifier)
120
+        return project.authentication.login(project, request)
121
+
122
+
123
+class AuthenticationAction(View):
124
+    def post(self, request, *args, **kwargs):
125
+        from .models import Project
126
+        identifier = kwargs.get('identifier')
127
+        project = Project.objects.get(identifier=identifier)
128
+        return project.authentication.dispatch(project, kwargs.get('action'), request)

+ 3
- 0
djawth/settings/dev.py View File

@@ -128,3 +128,6 @@ STATIC_URL = '/static/'
128 128
 
129 129
 AUTH_USER_MODEL = 'base.User'
130 130
 LOGIN_REDIRECT_URL = 'home'
131
+
132
+EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
133
+EMAILS_FROM = 'test@test.com'

+ 1
- 1
resources/plantuml/passwordless.uml View File

@@ -3,8 +3,8 @@
3 3
 title: Passwordless
4 4
 
5 5
 actor User as user
6
-boundary "Platform" as platform
7 6
 database "D-JaWTh" as auth
7
+boundary "Platform" as platform
8 8
 
9 9
 user -> auth: login
10 10
 auth --> user: ✉ access key url

Loading…
Cancel
Save