Custom version of the PATCH-user route from fastapi-users
.
In order to keep the fastapi-users logic in place (which is convenient to
update user attributes), we split the endpoint into two branches. We either
go through the fastapi-users-based attribute-update branch, or through the
branch where we establish new user/group relationships.
Note that we prevent making both changes at the same time, since it would
be more complex to guarantee that endpoint error would leave the database
in the same state as before the API call.
Source code in fractal_server/app/routes/auth/users.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172 | @router_users.patch("/users/{user_id}/", response_model=UserRead)
async def patch_user(
user_id: int,
user_update: UserUpdateWithNewGroupIds,
current_superuser: UserOAuth = Depends(current_active_superuser),
user_manager: UserManager = Depends(get_user_manager),
db: AsyncSession = Depends(get_async_db),
):
"""
Custom version of the PATCH-user route from `fastapi-users`.
In order to keep the fastapi-users logic in place (which is convenient to
update user attributes), we split the endpoint into two branches. We either
go through the fastapi-users-based attribute-update branch, or through the
branch where we establish new user/group relationships.
Note that we prevent making both changes at the same time, since it would
be more complex to guarantee that endpoint error would leave the database
in the same state as before the API call.
"""
# We prevent simultaneous editing of both user attributes and user/group
# associations
user_update_dict_without_groups = user_update.dict(
exclude_unset=True, exclude={"new_group_ids"}
)
edit_attributes = user_update_dict_without_groups != {}
edit_groups = user_update.new_group_ids is not None
if edit_attributes and edit_groups:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=(
"Cannot modify both user attributes and group membership. "
"Please make two independent PATCH calls"
),
)
# Check that user exists
user_to_patch = await _user_or_404(user_id, db)
if edit_groups:
# Establish new user/group relationships
# Check that all required groups exist
# Note: The reason for introducing `col` is as in
# https://sqlmodel.tiangolo.com/tutorial/where/#type-annotations-and-errors,
stm = select(func.count()).where(
col(UserGroup.id).in_(user_update.new_group_ids)
)
res = await db.execute(stm)
number_matching_groups = res.scalar()
if number_matching_groups != len(user_update.new_group_ids):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=(
"Not all requested groups (IDs: "
f"{user_update.new_group_ids}) exist."
),
)
for new_group_id in user_update.new_group_ids:
link = LinkUserGroup(user_id=user_id, group_id=new_group_id)
db.add(link)
try:
await db.commit()
except IntegrityError as e:
error_msg = (
f"Cannot link groups with IDs {user_update.new_group_ids} "
f"to user {user_id}. "
"Likely reason: one of these links already exists.\n"
f"Original error: {str(e)}"
)
logger.info(error_msg)
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=error_msg,
)
patched_user = user_to_patch
elif edit_attributes:
# Modify user attributes
try:
user_update_without_groups = UserUpdate(
**user_update_dict_without_groups
)
user = await user_manager.update(
user_update_without_groups,
user_to_patch,
safe=False,
request=None,
)
validated_user = schemas.model_validate(UserOAuth, user)
patched_user = await db.get(
UserOAuth, validated_user.id, populate_existing=True
)
except exceptions.InvalidPasswordException as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={
"code": ErrorCode.UPDATE_USER_INVALID_PASSWORD,
"reason": e.reason,
},
)
except exceptions.UserAlreadyExists:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=ErrorCode.UPDATE_USER_EMAIL_ALREADY_EXISTS,
)
else:
# Nothing to do, just continue
patched_user = user_to_patch
# Enrich user object with `group_ids_names` attribute
patched_user_with_groups = await _get_single_user_with_groups(
patched_user, db
)
return patched_user_with_groups
|