Fakultas Ilmu Komputer UI

Commit 04c887a3 authored by Giovan Isa Musthofa's avatar Giovan Isa Musthofa
Browse files

Merge branch 'PBI-2/oauth' into 'PBI-2-register_and_login'

Pbi 2/oauth

See merge request !34
parents 5afa2c0e 01d3ee8c
Pipeline #38556 passed with stages
in 4 minutes and 8 seconds
......@@ -178,6 +178,15 @@ REST_FRAMEWORK = {
)
}
# Authlib settings
AUTHLIB_OAUTH_CLIENTS = {
'google': {
'client_id': os.getenv('GOOGLE_CLIENT_ID'),
'client_secret': os.getenv('GOOGLE_CLIENT_SECRET'),
}
}
# Email settings
ANYMAIL = {
......
......@@ -108,3 +108,34 @@ class RegistrationFullSerializer(RegistrationSerializer):
'required': True
},
}
class UserProfileSerializer(ModelSerializer):
profile = ProfilePartialSerializer()
def update(self, instance, validated_data):
profile_data = validated_data.pop('profile')
instance.first_name = validated_data.get('first_name', instance.first_name)
instance.save()
# Handle nested instance
profile = instance.profile
for field in ('body_weight', 'id_card_no', 'birthplace', 'birthdate', 'sex',
'profession', 'blood_type', 'married_status', 'address',
'city', 'district', 'village', 'phone_no', 'work_address',
'work_email', 'work_phone_no'):
value = profile_data.get(field, getattr(profile, field))
setattr(profile, field, value)
profile.save()
return instance
class Meta:
model = User
fields = ('email', 'first_name', 'profile')
extra_kwargs = {
'email': {
'read_only': True
},
}
from unittest.mock import patch
from django.conf import settings
from django.core import mail
from rest_framework import status
from rest_framework.test import APITestCase
from rest_framework.exceptions import ErrorDetail
from rest_framework_authlib.tokens import AccessToken
from urllib.parse import urljoin
from .factories import UserFactory
......@@ -96,6 +98,7 @@ class EmailVerificationView(APITestCase):
self.assertRedirects(response,
urljoin(settings.REST_CLIENT_SITE, '/email-verification/failed'),
fetch_redirect_response=False)
def test_register_without_name(self):
data = {
'email': 'donald@duckduckgo.org',
......@@ -160,3 +163,93 @@ class AccessTokenAPITestCase(APITestCase):
self.assertIn('access', response.data)
self.assertNotIn('password', response.data)
class UserProfileViewTestCase(APITestCase):
def setUp(self):
self.user = UserFactory()
self.user.save()
token = str(AccessToken.for_user(self.user))
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
def test_get_user_profile(self):
response = self.client.get('/user/profile/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['email'], self.user.email)
self.assertEqual(response.data['first_name'], self.user.first_name)
def test_put_new_user_profile(self):
response = self.client.get('/user/profile/')
data = response.data
data['first_name'] = 'Donald Duck'
data['profile']['city'] = 'Kendari'
response = self.client.put('/user/profile/', data=data, format='json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['first_name'], data['first_name'])
self.assertEqual(response.data['profile']['city'],
data['profile']['city'])
def test_put_new_user_profile_with_email(self):
response = self.client.get('/user/profile/')
data = response.data
data['email'] = 'donald@10minutesmail.xyz'
response = self.client.put('/user/profile/', data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertNotEqual(response.data['email'], data['email'])
class OAuthAccessTokenViewTestCase(APITestCase):
def setUp(self):
from authlib.jose import jwt
self.keys = ('somebody_else_key', 'totally_secret_key')
self.token = jwt.encode(
{
'alg': 'HS256',
},
{
'email': 'totallyfakedonald@gmail.com',
'name': 'Donald The Duck',
},
self.keys[1]).decode('utf-8')
self.bad_token = jwt.encode(
{
'alg': 'HS256',
},
{
'Email': 'totallyfakedonald@gmail.com',
'Name': 'Donald The Duck',
},
self.keys[1]).decode('utf-8')
def test_get_access_token(self):
with patch('main.views.OAuthAccessTokenView.get_keys') as fake_keys:
fake_keys.return_value = self.keys
response = self.client.post('/auth/access/oauth/',
data={'tokenId': self.token})
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_get_access_token_bad(self):
with patch('main.views.OAuthAccessTokenView.get_keys') as fake_keys:
fake_keys.return_value = self.keys
response = self.client.post('/auth/access/oauth/',
data={'tokenId': self.bad_token})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_get_access_token_no_token(self):
with patch('main.views.OAuthAccessTokenView.get_keys') as fake_keys:
fake_keys.return_value = self.keys
response = self.client.post('/auth/access/oauth/',
data={'token_id': self.token})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
......@@ -4,17 +4,26 @@ from rest_framework_authlib.views import AccessTokenView
from .views import (
EmailVerificationView,
HelloView,
OAuthAccessTokenView,
RegisterFullView,
RegisterView,
SecretView,
UserProfileView,
)
urlpatterns = [
path('hello/', HelloView.as_view()),
path('secret/', SecretView.as_view()),
# Auth related path
path('auth/register/', RegisterView.as_view()),
path('auth/register-full/', RegisterFullView.as_view()),
path('auth/access/', AccessTokenView.as_view()),
path('auth/access/oauth/', OAuthAccessTokenView.as_view()),
path('auth/email-verification/', EmailVerificationView.as_view(),
name='email-verification'),
# Misc PoC path
path('hello/', HelloView.as_view()),
path('secret/', SecretView.as_view()),
# Authenticated user related path
path('user/profile/', UserProfileView.as_view()),
]
......@@ -4,7 +4,7 @@ from django.http import HttpResponseRedirect
from django.shortcuts import reverse
from django.template.loader import render_to_string
from rest_framework import generics
from rest_framework import views
from rest_framework import status, views
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from urllib.parse import urljoin
......@@ -63,11 +63,7 @@ class RegisterView(generics.CreateAPIView):
def create(self, request, *args, **kwargs):
response = super().create(request, *args, **kwargs)
try:
user = models.User.objects.get(email=request.data['email'])
except models.User.DoesNotExists as err:
# I guess this will never be evaluated
raise serializers.ValidationError(err.args)
user = models.User.objects.get(email=request.data['email'])
self.send_mail(request, user)
......@@ -123,6 +119,7 @@ class EmailVerificationView(views.APIView):
return HttpResponseRedirect(urljoin(settings.REST_CLIENT_SITE, self.failed_path))
class RegisterFullView(generics.CreateAPIView):
"""
RegisterView handles email and password registration.
......@@ -130,3 +127,83 @@ class RegisterFullView(generics.CreateAPIView):
Request will be validated and the User object will be created.
"""
serializer_class = serializers.RegistrationFullSerializer
class OAuthAccessTokenView(views.APIView):
def post(self, request):
from .models import User
from rest_framework.exceptions import ValidationError
from rest_framework_authlib.tokens import AccessToken
try:
token = request.data['tokenId']
except KeyError:
raise ValidationError('No tokenId', code='no_token_id')
userinfo = self.verify_id_token(token)
try:
user = User.objects.get(email=userinfo['email'])
except KeyError:
raise ValidationError('Bad tokenId', code='bad_token_id')
except User.DoesNotExist:
user = User.objects.create_user(
userinfo['email'], '', first_name=userinfo['name'],
is_verified=True)
data = {
'access': str(AccessToken.for_user(user))
}
return Response(data)
def verify_id_token(self, token):
from authlib.jose import jwt
from authlib.jose.errors import JoseError
keys = self.get_keys()
userinfo = None
for key in keys:
try:
payload = jwt.decode(token, key)
payload.validate()
userinfo = payload
except JoseError:
continue
else:
break
return userinfo
def get_keys(self):
import requests
from authlib.jose import jwk
data = requests.get('https://www.googleapis.com/oauth2/v3/certs').json()
return [jwk.loads(key) for key in data['keys']]
class UserProfileView(generics.RetrieveUpdateAPIView):
serializer_class = serializers.UserProfileSerializer
permission_classes = [IsAuthenticated]
def get(self, request, *args, **kwargs):
serializer = self.serializer_class(instance=request.user)
return Response(serializer.data)
def put(self, request, *args, **kwargs):
serializer = self.serializer_class(instance=request.user,
data=request.data,
partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
else:
return Response(serializer.error_messages,
status=status.HTTP_400_BAD_REQUEST)
import axios from "axios"
import { BASE_API_URL } from "./config"
axios.defaults.baseURL = BASE_API_URL
export const getListJadwalDonor = date =>
axios.get(`${BASE_API_URL}/donor/jadwal/?date=${date}`)
axios.get(`/donor/jadwal/?date=${date}`)
export const postUserLogin = (email, password) =>
Promise.resolve({
data: {
access: "initokenyangsecure",
},
})
axios.post(
"/auth/access/",
{ email: email, password: password },
{ mode: "cors" }
)
export const postUserProfile = token =>
Promise.resolve({
data: {
email: "fairuzi@informatika.com",
nama: "Muhammad Fairuzi Teguh",
golongan_darah: "AB",
},
})
export const postUserLoginOAuth = tokenId =>
axios.post("auth/access/oauth/", { tokenId: tokenId }, { mode: "cors" })
export const getUserProfile = () =>
axios.get("/user/profile/", { mode: "cors", cache: "default" })
// Sebenernya sekarang tinggal buang token
// Tapi belom tau cara invalidasi token
export const postUserLogout = () =>
Promise.resolve({
data: {
......
......@@ -5,10 +5,11 @@ import { useForm } from "react-hook-form"
import { useAuth } from "../hooks/authenticate"
import { faCircleNotch } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { GATSBY_GOOGLE_CLIENT_ID } from "../config"
const ModalLogin = ({ show, handleClose }) => {
const { register, handleSubmit, errors } = useForm()
const { login } = useAuth()
const { login, loginOAuth } = useAuth()
const [isLoadingLogin, setIsLoadingLogin] = useState(false)
const [loginError, setLoginError] = useState(null)
const onSubmit = async data => {
......@@ -23,6 +24,18 @@ const ModalLogin = ({ show, handleClose }) => {
}
setIsLoadingLogin(false)
}
const googleResponse = async data => {
setLoginError(null)
setIsLoadingLogin(true)
try {
await loginOAuth(data.tokenId)
handleClose()
} catch (error) {
const message = error.response.data.detail
setLoginError(message)
}
}
return (
<Modal
aria-labelledby="contained-modal-title-vcenter"
......@@ -102,7 +115,11 @@ const ModalLogin = ({ show, handleClose }) => {
</p>
<div className="row px-5">
<div className="col-md-12 mt-1">
<GoogleLogin clientId={""} buttonText="Login">
<GoogleLogin
clientId={GATSBY_GOOGLE_CLIENT_ID}
onSuccess={googleResponse}
buttonText="Login"
>
<span> Login with Google</span>
</GoogleLogin>
</div>
......
......@@ -5,14 +5,14 @@ import {
screen,
} from "@testing-library/react"
import React from "react"
import { postUserLogin, postUserProfile, postUserLogout } from "../api"
import { postUserLogin, getUserProfile, postUserLogout } from "../api"
import { AuthProvider, useAuth } from "../hooks/authenticate"
import Navbar from "./navbar"
jest.mock("../api.js", () => ({
__esModule: true,
getUserProfile: jest.fn(),
postUserLogin: jest.fn(),
postUserProfile: jest.fn(),
postUserLogout: jest.fn(),
}))
jest.mock("../hooks/authenticate.js", () => ({
......@@ -41,7 +41,7 @@ const doSuccessfulLogin = async () => {
access: "initokenyangsecure",
},
})
postUserProfile.mockResolvedValueOnce({
getUserProfile.mockResolvedValueOnce({
data: {
email: "fairuzi@informatika.com",
nama: "Muhammad Fairuzi Teguh",
......
const ENV =
process.env.GATSBY_ACTIVE_ENV || process.env.NODE_ENV || "development"
export let BASE_API_URL
export let GATSBY_GOOGLE_CLIENT_ID
GATSBY_GOOGLE_CLIENT_ID = process.env.GATSBY_GOOGLE_CLIENT_ID
switch (ENV) {
case "production":
BASE_API_URL = "http://ssh.giovanis.me:8080"
......
import axios from "axios"
import React, { useEffect, useState } from "react"
import { postUserLogin, postUserProfile, postUserLogout } from "../api"
import {
postUserLogin,
getUserProfile,
postUserLogout,
postUserLoginOAuth,
} from "../api"
const LOCAL_STORAGE_TOKEN_KEY = "token"
let inMemoryToken
const AuthContext = React.createContext()
const AuthProvider = props => {
const [user, setUser] = useState(null)
const postAndSetUserProfile = async token => {
const getAndSetUserProfile = async () => {
try {
const { data: user } = await postUserProfile(token)
const { data: user } = await getUserProfile()
setUser(user)
} catch (error) {
// do nothing if the token is invalid
}
}
useEffect(() => {
const existentToken = window.localStorage.getItem(LOCAL_STORAGE_TOKEN_KEY)
if (existentToken) postAndSetUserProfile(existentToken)
const existentToken = inMemoryToken
if (existentToken) getAndSetUserProfile()
}, [])
const login = async (email, password) => {
// if login fail, it's ok to throw the error
const result = await postUserLogin(email, password)
const token = result.data.access
window.localStorage.setItem(LOCAL_STORAGE_TOKEN_KEY, token)
postAndSetUserProfile(token)
// TODO: axios set token
inMemoryToken = token
axios.defaults.headers.common = { Authorization: `Bearer ${inMemoryToken}` }
getAndSetUserProfile(token)
}
const loginOAuth = async tokenId => {
const result = await postUserLoginOAuth(tokenId)
const token = result.data.access
inMemoryToken = token
axios.defaults.headers.common = { Authorization: `Bearer ${inMemoryToken}` }
getAndSetUserProfile(token)
}
const register = () => {}
const logout = async () => {
// if not throwing error, than we are OK to proceed
await postUserLogout()
window.localStorage.removeItem(LOCAL_STORAGE_TOKEN_KEY)
inMemoryToken = null
axios.defaults.headers.common = {}
setUser(null)
// TODO: axios set token
}
return (
<AuthContext.Provider
value={{ user, login, register, logout }}
value={{ user, login, loginOAuth, register, logout }}
{...props}
/>
)
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment