-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5 from Ilhasoft/develop
Develop
- Loading branch information
Showing
32 changed files
with
814 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
from django.contrib.auth.password_validation import validate_password | ||
from django.utils.translation import ugettext_lazy as _ | ||
from rest_framework import serializers | ||
|
||
from weni.api.v1.fields import PasswordField | ||
from weni.authentication.models import User | ||
|
||
|
||
class UserSerializer(serializers.ModelSerializer): | ||
class Meta: | ||
model = User | ||
fields = ["username", "email", "first_name", "last_name", "photo"] | ||
ref_name = None | ||
|
||
username = serializers.CharField(label=_("Username"), read_only=True) | ||
email = serializers.EmailField(label=_("Email"), read_only=True) | ||
photo = serializers.ImageField(label=_("User photo"), read_only=True) | ||
|
||
|
||
class UserPhotoSerializer(serializers.Serializer): | ||
file = serializers.FileField(required=True) | ||
|
||
|
||
class ChangePasswordSerializer(serializers.Serializer): | ||
password = PasswordField( | ||
write_only=True, validators=[validate_password], label=_("Password") | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import filetype | ||
from django.conf import settings | ||
from django.utils.translation import ugettext_lazy as _ | ||
from keycloak import KeycloakGetError | ||
from rest_framework import mixins, permissions, parsers | ||
from rest_framework.decorators import action | ||
from rest_framework.exceptions import UnsupportedMediaType, ValidationError | ||
from rest_framework.response import Response | ||
from rest_framework.viewsets import GenericViewSet | ||
|
||
from weni.api.v1.account.serializers import ( | ||
UserSerializer, | ||
UserPhotoSerializer, | ||
ChangePasswordSerializer, | ||
) | ||
from weni.api.v1.keycloak import KeycloakControl | ||
from weni.authentication.models import User | ||
from weni.utils import upload_photo_rocket | ||
|
||
|
||
class MyUserProfileViewSet( | ||
mixins.RetrieveModelMixin, | ||
mixins.UpdateModelMixin, | ||
mixins.DestroyModelMixin, | ||
GenericViewSet, | ||
): | ||
""" | ||
Manager current user profile. | ||
retrieve: | ||
Get current user profile | ||
update: | ||
Update current user profile. | ||
partial_update: | ||
Update, partially, current user profile. | ||
""" | ||
|
||
serializer_class = UserSerializer | ||
queryset = User.objects | ||
lookup_field = None | ||
permission_classes = [permissions.IsAuthenticated] | ||
|
||
def get_object(self, *args, **kwargs): | ||
request = self.request | ||
user = request.user | ||
|
||
# May raise a permission denied | ||
self.check_object_permissions(self.request, user) | ||
|
||
return user | ||
|
||
@action( | ||
detail=True, | ||
methods=["POST"], | ||
url_name="profile-upload-photo", | ||
parser_classes=[parsers.MultiPartParser], | ||
serializer_class=UserPhotoSerializer, | ||
) | ||
def upload_photo(self, request, **kwargs): # pragma: no cover | ||
f = request.FILES.get("file") | ||
|
||
serializer = UserPhotoSerializer(data=request.data) | ||
serializer.is_valid(raise_exception=True) | ||
|
||
if filetype.is_image(f): | ||
self.request.user.photo = f | ||
self.request.user.save(update_fields=["photo"]) | ||
|
||
# Update avatar in all rocket chat registered | ||
for service in self.request.user.service_status.filter( | ||
service__rocket_chat=True | ||
): | ||
upload_photo_rocket( | ||
server_rocket=service.service.url, | ||
jwt_token=self.request.auth, | ||
avatar_url=self.request.user.photo.url, | ||
) | ||
|
||
return Response({"photo": self.request.user.photo.url}) | ||
raise UnsupportedMediaType( | ||
filetype.get_type(f.content_type).extension, | ||
detail=_("Unauthorized file type, upload an image file"), | ||
) | ||
|
||
@action( | ||
detail=True, | ||
methods=["DELETE"], | ||
url_name="profile-upload-photo", | ||
parser_classes=[parsers.MultiPartParser], | ||
) | ||
def delete_photo(self, request, **kwargs): # pragma: no cover | ||
self.request.user.photo.storage.delete(self.request.user.photo.name) | ||
self.request.user.photo = None | ||
self.request.user.save(update_fields=["photo"]) | ||
return Response({}) | ||
|
||
@action( | ||
detail=True, | ||
methods=["POST"], | ||
url_name="profile-change-password", | ||
serializer_class=ChangePasswordSerializer, | ||
) | ||
def change_password(self, request, **kwargs): # pragma: no cover | ||
serializer = ChangePasswordSerializer(data=request.data) | ||
serializer.is_valid(raise_exception=True) | ||
|
||
try: | ||
keycloak_instance = KeycloakControl() | ||
|
||
user_id = keycloak_instance.get_user_id_by_email( | ||
email=self.request.user.email | ||
) | ||
keycloak_instance.instance.set_user_password( | ||
user_id=user_id, password=request.data.get("password"), temporary=False | ||
) | ||
except KeycloakGetError: | ||
if not settings.DEBUG: | ||
raise ValidationError( | ||
_("System temporarily unavailable, please try again later.") | ||
) | ||
|
||
return Response() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from rest_framework import serializers | ||
|
||
|
||
class PasswordField(serializers.CharField): | ||
def __init__(self, *args, **kwargs): | ||
kwargs.pop("trim_whitespace", None) | ||
super().__init__(trim_whitespace=False, **kwargs) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
from django.conf import settings | ||
from keycloak import KeycloakAdmin | ||
|
||
|
||
class KeycloakControl: # pragma: no cover | ||
def __init__(self): | ||
self.instance = self.get_instance() | ||
|
||
def get_instance(self) -> KeycloakAdmin: | ||
return KeycloakAdmin( | ||
server_url=settings.OIDC_RP_SERVER_URL, | ||
realm_name=settings.OIDC_RP_REALM_NAME, | ||
client_id=settings.OIDC_RP_CLIENT_ID, | ||
client_secret_key=settings.OIDC_RP_CLIENT_SECRET, | ||
verify=True, | ||
auto_refresh_token=["get", "post", "put", "delete"], | ||
) | ||
|
||
def get_user_id_by_email(self, email: str) -> str: | ||
""" | ||
Get internal keycloak user id from email | ||
This is required for further actions against this user. | ||
UserRepresentation | ||
https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation | ||
:param email: id in UserRepresentation | ||
:return: user_id | ||
""" | ||
|
||
users = self.instance.get_users(query={"email": email}) | ||
return next((user["id"] for user in users if user["email"] == email), None) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import json | ||
|
||
from django.test import TestCase, RequestFactory | ||
from rest_framework import status | ||
|
||
from weni.api.v1.account.views import MyUserProfileViewSet | ||
from weni.api.v1.tests.utils import create_user_and_token | ||
|
||
|
||
class ListMyProfileTestCase(TestCase): | ||
def setUp(self): | ||
self.factory = RequestFactory() | ||
self.user, self.user_token = create_user_and_token() | ||
|
||
def request(self, token): | ||
authorization_header = {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} | ||
request = self.factory.get("/v2/account/my-profile/", **authorization_header) | ||
response = MyUserProfileViewSet.as_view({"get": "retrieve"})(request) | ||
response.render() | ||
content_data = json.loads(response.content) | ||
return (response, content_data) | ||
|
||
def test_okay(self): | ||
response, content_data = self.request(self.user_token) | ||
self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
self.assertEqual(content_data.get("username"), self.user.username) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,9 @@ | ||
from django.contrib.auth.models import User | ||
from rest_framework.authtoken.models import Token | ||
|
||
from weni.authentication.models import User | ||
|
||
|
||
def create_user_and_token(nickname="fake"): | ||
user = User.objects.create_user("{}@user.com".format(nickname), nickname) | ||
token, create = Token.objects.get_or_create(user=user) | ||
return (user, token) | ||
return user, token |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from django.contrib import admin | ||
|
||
from weni.authentication.models import User | ||
|
||
|
||
@admin.register(User) | ||
class UserAdmin(admin.ModelAdmin): | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class AuthenticationConfig(AppConfig): | ||
name = "weni.authentication" | ||
|
||
def ready(self): | ||
from .signals import update_user_keycloak # noqa: F401 |
Oops, something went wrong.