Skip to content
Snippets Groups Projects
Commit 4f96eba8 authored by Sivert Kronen Hatteberg's avatar Sivert Kronen Hatteberg
Browse files

Merge branch 'GREG-87-logout' into 'master'

Greg 87 invitation logout

See merge request !192
parents 285d8c84 ebbfb013
No related branches found
No related tags found
1 merge request!192Greg 87 invitation logout
Pipeline #102938 passed
......@@ -36,6 +36,7 @@
"libphonenumber-js": "^1.9.35",
"lodash": "^4.17.21",
"react": "^17.0.2",
"react-cookie": "^4.1.1",
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-hook-form": "^7.17.5",
......@@ -4222,6 +4223,11 @@
"@babel/types": "^7.3.0"
}
},
"node_modules/@types/cookie": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz",
"integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow=="
},
"node_modules/@types/eslint": {
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.28.2.tgz",
......@@ -4258,6 +4264,15 @@
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz",
"integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ=="
},
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"dependencies": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"node_modules/@types/html-minifier-terser": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz",
......@@ -18211,6 +18226,19 @@
"node": ">=10"
}
},
"node_modules/react-cookie": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.1.1.tgz",
"integrity": "sha512-ffn7Y7G4bXiFbnE+dKhHhbP+b8I34mH9jqnm8Llhj89zF4nPxPutxHT1suUqMeCEhLDBI7InYwf1tpaSoK5w8A==",
"dependencies": {
"@types/hoist-non-react-statics": "^3.0.1",
"hoist-non-react-statics": "^3.0.0",
"universal-cookie": "^4.0.0"
},
"peerDependencies": {
"react": ">= 16.3.0"
}
},
"node_modules/react-dev-utils": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.4.tgz",
......@@ -21857,6 +21885,15 @@
"node": ">=4"
}
},
"node_modules/universal-cookie": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz",
"integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==",
"dependencies": {
"@types/cookie": "^0.3.3",
"cookie": "^0.4.0"
}
},
"node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
......@@ -26973,6 +27010,11 @@
"@babel/types": "^7.3.0"
}
},
"@types/cookie": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz",
"integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow=="
},
"@types/eslint": {
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.28.2.tgz",
......@@ -27009,6 +27051,15 @@
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz",
"integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ=="
},
"@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"requires": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"@types/html-minifier-terser": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz",
......@@ -37821,6 +37872,16 @@
"whatwg-fetch": "^3.4.1"
}
},
"react-cookie": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.1.1.tgz",
"integrity": "sha512-ffn7Y7G4bXiFbnE+dKhHhbP+b8I34mH9jqnm8Llhj89zF4nPxPutxHT1suUqMeCEhLDBI7InYwf1tpaSoK5w8A==",
"requires": {
"@types/hoist-non-react-statics": "^3.0.1",
"hoist-non-react-statics": "^3.0.0",
"universal-cookie": "^4.0.0"
}
},
"react-dev-utils": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.4.tgz",
......@@ -40693,6 +40754,15 @@
"crypto-random-string": "^1.0.0"
}
},
"universal-cookie": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz",
"integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==",
"requires": {
"@types/cookie": "^0.3.3",
"cookie": "^0.4.0"
}
},
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
......@@ -31,6 +31,7 @@
"libphonenumber-js": "^1.9.35",
"lodash": "^4.17.21",
"react": "^17.0.2",
"react-cookie": "^4.1.1",
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-hook-form": "^7.17.5",
......
......@@ -12,6 +12,7 @@ function noop() {}
export const UserContext = createContext<IUserContext>({
user: {
auth: false,
auth_type: '',
fetched: false,
first_name: '',
last_name: '',
......
......@@ -64,6 +64,7 @@ export type FetchedRole = {
export interface User {
auth: boolean
auth_type: string
fetched: boolean
person_id: string
sponsor_id: string
......
......@@ -10,6 +10,7 @@ function UserProvider(props: UserProviderProps) {
const { children } = props
const [user, setUser] = useState({
auth: false,
auth_type: '',
fetched: false,
first_name: '',
last_name: '',
......@@ -31,6 +32,7 @@ function UserProvider(props: UserProviderProps) {
if (response.ok) {
setUser({
auth: true,
auth_type: data.auth_type,
fetched: true,
first_name: data.first_name,
last_name: data.last_name,
......@@ -46,6 +48,7 @@ function UserProvider(props: UserProviderProps) {
} else {
setUser({
auth: false,
auth_type: '',
fetched: true,
first_name: '',
last_name: '',
......@@ -62,6 +65,7 @@ function UserProvider(props: UserProviderProps) {
} catch (error) {
setUser({
auth: false,
auth_type: '',
fetched: true,
first_name: '',
last_name: '',
......@@ -92,6 +96,7 @@ function UserProvider(props: UserProviderProps) {
const clearUserInfo = () => {
setUser({
auth: false,
auth_type: '',
fetched: false,
first_name: '',
last_name: '',
......
......@@ -48,6 +48,9 @@ const Header = () => {
const { user } = useUserContext()
const { t } = useTranslation('common')
const logoutLink =
user.auth_type === 'oidc' ? '/oidc/logout' : '/invite/logout'
return (
<StyledHeader>
<MainContainer>
......@@ -61,7 +64,7 @@ const Header = () => {
sx={{
color: 'white',
}}
href="/oidc/logout"
href={logoutLink}
>
{t('button.logout')}
<LogoutIcon
......
......@@ -13,6 +13,7 @@ import Register from 'routes/sponsor/register'
import FrontPage from 'routes/frontpage'
import Invite from 'routes/invite'
import InviteLink from 'routes/invitelink'
import LogoutInviteSession from 'routes/invitelink/logout'
import Footer from 'routes/components/footer'
import Header from 'routes/components/header'
import NotFound from 'routes/components/notFound'
......@@ -77,6 +78,7 @@ export default function App() {
<Register />
</ProtectedRoute>
<Route path="/invitelink/" component={InviteLink} />
<Route path="/invite/logout" component={LogoutInviteSession} />
<Route path="/invite/" component={Invite} />
<Route path="/guestregister" component={GuestRegister} />
<Route>
......
import { useEffect, useState } from 'react'
import { Redirect } from 'react-router-dom'
import { useCookies } from 'react-cookie'
import { Box, CircularProgress } from '@mui/material'
import { useUserContext } from 'contexts'
export default function LogoutInviteSession() {
// Fetch backend endpoint to preserve invite_id in backend session then redirect
const [, , removeCookie] = useCookies(['sessionid'])
const [loggedOut, setLoggedOut] = useState(false)
const { fetchUserInfo } = useUserContext()
useEffect(() => {
fetch('/api/ui/v1/invitecheck/', { method: 'DELETE' })
.then(() => removeCookie('sessionid'))
.then(() => fetchUserInfo())
.then(() => setLoggedOut(true))
}, [])
if (loggedOut) {
return <Redirect to="/" />
}
return (
<Box sx={{ margin: 'auto' }}>
<CircularProgress />
</Box>
)
}
import logging
from enum import Enum
from typing import Optional, List
import structlog
from django.core import exceptions
from django.db import transaction
......@@ -24,7 +24,7 @@ from gregui.api.serializers.invitation import InviteGuestSerializer
from gregui.mailutils import send_invite_mail
from gregui.models import GregUserProfile
logger = logging.getLogger(__name__)
logger = structlog.getLogger(__name__)
class InvitationView(CreateAPIView, DestroyAPIView):
......@@ -91,9 +91,7 @@ class InvitationView(CreateAPIView, DestroyAPIView):
# not be verified, but including that check just in case here.
# If this is the case then there is an unexpected situation, the cancel option
# should only apply to guests that have not completed the registration
logger.warning(
f"Attempting to delete invitation for already registered guest with person ID {person_id}"
)
logger.warning("try_delete_registered_invite", person_id=person_id)
return Response(status=status.HTTP_400_BAD_REQUEST)
# Delete the person. The delete will cascade and all roles, identities and invitations will be removed.
......@@ -109,7 +107,7 @@ class CheckInvitationView(APIView):
permission_classes = [AllowAny]
throttle_classes = [AnonRateThrottle]
def post(self, request, *args, **kwargs):
def post(self, request, *args, **kwargs) -> Response:
"""
Endpoint for verifying and setting invite_id in session.
......@@ -132,6 +130,16 @@ class CheckInvitationView(APIView):
request.session["invite_id"] = invite_id
return Response(status=status.HTTP_200_OK)
def delete(self, request, *args, **kwargs) -> Response:
if "invite_id" in request.session:
logger.info(
"invitation_session_deleted", invite_id=request.session["invite_id"]
)
del request.session["invite_id"]
return Response(status.HTTP_200_OK)
return Response(status=status.HTTP_403_FORBIDDEN)
class SessionType(Enum):
INVITE = "invite"
......@@ -322,9 +330,7 @@ class ResendInvitationView(UpdateModelMixin, APIView):
if non_expired_links.count() > 0:
if non_expired_links.count() > 1:
# Do not expect this to happen
logger.warning(
f"Person with ID {person_id} has multiple invitation links"
)
logger.warning("found_multiple_invitation_links", person_id=person_id)
# Just resend all and do not create a new one
for link in non_expired_links:
......@@ -339,9 +345,7 @@ class ResendInvitationView(UpdateModelMixin, APIView):
# Do not expected that a person has several open invitations, it could happen
# if he has been invited by different sponsor at the same time, but that
# could be an indication that there has been a mixup
logger.warning(
f"Multiple invitations exist for person with ID {person_id}"
)
logger.warning("found_multiple_invitations", person_id=person_id)
for invitation in invitations_to_resend:
invitation_link = InvitationLink.objects.create(
......
......@@ -34,6 +34,10 @@ class UserInfoView(APIView):
user = request.user
invite_id = request.session.get("invite_id")
auth_type = "invite"
if "oidc_states" in request.session.keys():
auth_type = "oidc"
person = None
sponsor = None
content = {
......@@ -41,6 +45,7 @@ class UserInfoView(APIView):
"sponsor_id": None,
"person_id": None,
"roles": [],
"auth_type": auth_type,
}
# Fetch sponsor and/or person object from profile of authenticated user
......
......@@ -19,6 +19,7 @@ def test_userinfo_invited_get(client, invitation_link):
response = client.get(reverse("api-userinfo"))
assert response.status_code == status.HTTP_200_OK
assert response.json() == {
"auth_type": "invite",
"feide_id": None,
"sponsor_id": None,
"person_id": 1,
......@@ -59,6 +60,7 @@ def test_userinfo_sponsor_get(client, log_in, user_sponsor):
response = client.get(reverse("api-userinfo"))
assert response.status_code == status.HTTP_200_OK
assert response.json() == {
"auth_type": "oidc",
"feide_id": "",
"person_id": None,
"roles": [],
......@@ -73,6 +75,7 @@ def test_userinfo_guest_get(client, log_in, user_person):
response = client.get(reverse("api-userinfo"))
assert response.status_code == status.HTTP_200_OK
assert response.json() == {
"auth_type": "oidc",
"feide_id": "",
"sponsor_id": None,
"person_id": 1,
......
......@@ -471,6 +471,7 @@ def log_in(client) -> Callable[[UserModel], APIClient]:
# It seems like the session was not updated automatically this way
session = client.session
session["oidc_id_token_payload"] = {"iat": time.time()}
session["oidc_states"] = {}
session.save()
return client
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment