Skip to content

Commit

Permalink
Event source invite
Browse files Browse the repository at this point in the history
This commit includes more updates to the event sourced invite flow.

The sign in code was updated to use form data rather than json.
  • Loading branch information
paulespinosa committed Nov 8, 2024
1 parent c08d095 commit 8144cd0
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 40 deletions.
2 changes: 2 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from app.modules.access.invite.contracts import (
SendInviteCommand, ProcessSentInviteCommand, FailedSentInviteCommand,
SendInviteRequestedDomainEvent, InviteSentDomainEvent,
UserCreatedDomainEvent,
InviteAcceptedDomainEvent, InviteSentFailedDomainEvent)
from app.modules.access.invite.application_service import InviteService
from app.modules.access.invite.processor import CognitoInviteUserProcessor
Expand Down Expand Up @@ -54,6 +55,7 @@ async def lifespan(app: FastAPI):
CognitoInviteUserProcessor(cognito_client, settings),
],
InviteSentDomainEvent: [dashboard_users_projection],
UserCreatedDomainEvent: [user_repo],
InviteAcceptedDomainEvent: [],
InviteSentFailedDomainEvent: [dashboard_users_projection],
}
Expand Down
12 changes: 6 additions & 6 deletions backend/app/modules/access/auth_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@

from app.modules.access.crud import create_user, delete_user, get_user
from app.modules.deps import (SettingsDep, DbSessionDep, CognitoIdpDep,
SecretHashFuncDep, CoordinatorDep,
requires_auth, allow_roles,
role_to_cognito_group_map)
SecretHashFuncDep, CoordinatorDep, requires_auth,
allow_roles, role_to_cognito_group_map)

from app.modules.access.models import EmailAddress
from app.modules.access.invite.contracts import SendInviteCommand, InviteAlreadySentException, NotInvitedException
Expand Down Expand Up @@ -188,7 +187,7 @@ def current_session(request: Request, settings: SettingsDep, db: DbSessionDep,
algorithms=["RS256"],
options={"verify_signature": False})

user = get_user(db, decoded_id_token['email'])
user = get_user(db, EmailAddress(decoded_id_token['email']))

try:
auth_response = cognito_client.initiate_auth(
Expand Down Expand Up @@ -322,7 +321,8 @@ def confirm_forgot_password(
status_code=202,
description="Invites a new user to join HUU.",
)
def invite(invite_request: InviteRequest, coordinator: CoordinatorDep) -> InviteResponse:
def invite(invite_request: InviteRequest,
coordinator: CoordinatorDep) -> InviteResponse:
"""Invite a new user to join HUU."""
inviter = coordinator

Expand Down Expand Up @@ -438,7 +438,7 @@ def new_password(body: NewPasswordRequest, response: Response,
options={"verify_signature": False})

try:
user = get_user(db, decoded_id_token['email'])
user = get_user(db, EmailAddress(decoded_id_token['email']))
if user is None:
raise HTTPException(status_code=404, detail="User not found")
except Exception as e:
Expand Down
4 changes: 4 additions & 0 deletions backend/app/modules/access/invite/invite_aggregate.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ def when_InviteSentDomainEvent(self, event: InviteSentDomainEvent):

self.invited = True

def when_UserCreatedDomainEvent(self, event: UserCreatedDomainEvent):
"""Update the state of an Invite."""
pass

def when_InviteAcceptedDomainEvent(self, event: InviteAcceptedDomainEvent):
"""Update state of an Invite."""
self.accepted_at = event.accepted_at
Expand Down
8 changes: 5 additions & 3 deletions backend/app/modules/access/schemas.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Optional

from pydantic import BaseModel, ConfigDict, EmailStr, Field

from .models import UserId, UserRoleEnum
Expand All @@ -6,8 +8,8 @@
class UserBase(BaseModel):
email: EmailStr
first_name: str
middle_name: str | None = None
last_name: str | None = None
middle_name: Optional[str] = None
last_name: str


class UserCreate(UserBase):
Expand Down Expand Up @@ -55,7 +57,7 @@ class ConfirmForgotPasswordResponse(BaseModel):
class InviteRequest(BaseModel):
email: EmailStr
firstName: str
middleName: str
middleName: Optional[str] = None
lastName: str
role: UserRoleEnum = UserRoleEnum.GUEST

Expand Down
43 changes: 33 additions & 10 deletions backend/app/modules/access/user_repo.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,45 @@
from app.modules.access.models import UserId, User, Role, UserRoleEnum
import logging

from app.core.interfaces import DomainEvent
from app.modules.access.models import EmailAddress, UserId, User, UserRoleEnum
from app.modules.access.invite.contracts import UserCreatedDomainEvent

log = logging.Logger(__name__)


class UserRepository:

def __init__(self, session_factory):
self.session_factory = session_factory

def mutate(self, domain_event: DomainEvent):
"""Update the projection based on the domain event."""
method = getattr(self, 'when_' + domain_event.__class__.__name__)
if method:
method(domain_event)
else:
log.warn(
f"when_{domain_event.__class__.__name__} not implemented.")

def when_UserCreatedDomainEvent(self, e: UserCreatedDomainEvent):
"""Update users."""
if not self.get_user_by_id(e.user_id):
self.add_user(e.user_id, e.email, e.role, e.first_name,
e.middle_name, e.last_name)

def add_user(self,
email: str,
user_id: UserId,
email: EmailAddress,
role: UserRoleEnum,
firstName: str,
middleName: str = None,
lastName: str = None) -> User:
new_user = User(email=email,
firstName=firstName,
middleName=middleName,
lastName=lastName,
role=role.value)
first_name: str,
middle_name: str = None,
last_name: str = None) -> User:
new_user = User(user_id=user_id,
email=email,
first_name=first_name,
middle_name=middle_name,
last_name=last_name,
role=role)
with self.session_factory.begin() as session:
session.add(new_user)

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/authentication/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ export const SignIn = () => {
password,
}).unwrap();

const {user, token} = response;
const {user, access_token: token} = response;

dispatch(setCredentials({user, token}));

navigate(redirectsByRole[user.role.type]);
navigate(redirectsByRole[user.role]);
} catch (err) {
if (isFetchBaseQueryError(err)) {
// you can access all properties of `FetchBaseQueryError` here
Expand Down
28 changes: 16 additions & 12 deletions frontend/src/pages/coordinator-dashboard/CoordinatorDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,23 @@ import {
} from '../../services/coordinator';
import {
GuestInviteButton,
LoadingComponent,
// LoadingComponent,
} from '../../features/coordinator-dashboard';

const columns: GridColDef[] = [
{
field: 'userName',
field: 'name',
headerName: 'Applicant',
flex: 1,
},
{field: 'userType', headerName: 'Type'},
{field: 'caseStatus', headerName: 'Status'},
{field: 'coordinatorName', headerName: 'Coordinator', flex: 1},
{field: 'lastUpdated', headerName: 'Updated', flex: 1},
{
field: 'role',
headerName: 'Type',
renderCell: params => params.value.toUpperCase(),
},
{field: 'status', headerName: 'Status', flex: 1},
{field: 'coordinator_name', headerName: 'Coordinator', flex: 1},
{field: 'updated', headerName: 'Updated', flex: 1},
{
field: 'notes',
headerName: 'Notes',
Expand Down Expand Up @@ -59,20 +63,20 @@ export const CoordinatorDashboard = () => {
if (value === 0) {
return row;
} else if (value === 1) {
return row.userType === 'GUEST';
return row.role === 'guest';
} else if (value === 2) {
return row.userType === 'HOST';
return row.role === 'host';
}
});

const totalAppUsers = dashboardDataItems.filter(
row => ['GUEST', 'HOST'].indexOf(row.userType) >= 0,
row => ['guest', 'host'].indexOf(row.role) >= 0,
).length;
const totalGuests = dashboardDataItems.filter(
row => row.userType === 'GUEST',
row => row.role === 'guest',
).length;
const totalHosts = dashboardDataItems.filter(
row => row.userType === 'HOST',
row => row.role === 'host',
).length;

const handleChange = (event: React.SyntheticEvent, newValue: number) => {
Expand Down Expand Up @@ -163,6 +167,7 @@ export const CoordinatorDashboard = () => {
disableRowSelectionOnClick
rows={dashboardData ? dashboardData : []}
columns={columns}
getRowId={row => row.user_id != -1 || row.email}
initialState={{
pagination: {
paginationModel: {
Expand All @@ -172,7 +177,6 @@ export const CoordinatorDashboard = () => {
}}
slots={{
pagination: CustomPagination,
noRowsOverlay: LoadingComponent,
}}
sx={{
height: '538.75px',
Expand Down
27 changes: 20 additions & 7 deletions frontend/src/services/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export interface SignUpRequest {

export interface SignInResponse {
user: User;
token: string;
access_token: string;
token_type: string;
}

export interface SignInRequest {
Expand Down Expand Up @@ -64,6 +65,12 @@ export interface ResendConfirmationCodeResponse {
}
// /auth/resend_confirmation_code

const encodeFormData = data => {
return Object.keys(data)
.map(key => encodeURIComponent(key) + '=' + encodeURIComponent(data[key]))
.join('&');
};

const authApi = api.injectEndpoints({
endpoints: build => ({
signUp: build.mutation<SignUpResponse, SignUpRequest>({
Expand All @@ -75,12 +82,18 @@ const authApi = api.injectEndpoints({
}),
}),
signIn: build.mutation<SignInResponse, SignInRequest>({
query: credentials => ({
url: 'auth/signin',
method: 'POST',
withCredentials: true,
body: credentials,
}),
query: credentials => {
const {email: username, password} = credentials;
return {
url: 'auth/signin',
method: 'POST',
withCredentials: true,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: encodeFormData({username, password, grant_type: 'password'}),
};
},
}),
googleSignUp: build.mutation<TokenResponse, TokenRequest>({
query: data => {
Expand Down

0 comments on commit 8144cd0

Please sign in to comment.