Skip to content

Commit d006508

Browse files
committed
add user update endpoint
1 parent 7d054e0 commit d006508

File tree

7 files changed

+194
-1
lines changed

7 files changed

+194
-1
lines changed

backend/app/keycloak_auth.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33
import logging
44
from datetime import datetime
5+
from typing import Optional
56

67
from fastapi import Depends, HTTPException, Security
78
from fastapi.security import APIKeyCookie, APIKeyHeader, OAuth2AuthorizationCodeBearer
@@ -435,6 +436,43 @@ async def create_user(email: str, password: str, firstName: str, lastName: str):
435436
return user
436437

437438

439+
async def update_user(
440+
email: str,
441+
new_email: Optional[str],
442+
new_password: Optional[str],
443+
new_firstName: Optional[str],
444+
new_lastName: Optional[str],
445+
):
446+
"""Update existing user in Keycloak."""
447+
keycloak_admin = KeycloakAdmin(
448+
server_url=settings.auth_server_url,
449+
username=settings.keycloak_username,
450+
password=settings.keycloak_password,
451+
realm_name=settings.keycloak_realm_name,
452+
user_realm_name=settings.keycloak_user_realm_name,
453+
# client_secret_key=settings.auth_client_secret,
454+
# client_id=settings.keycloak_client_id,
455+
verify=True,
456+
)
457+
existing_user_id = keycloak_admin.get_user_id(email)
458+
existing_user = keycloak_admin.get_user(existing_user_id)
459+
# Update user and set password
460+
keycloak_admin.update_user(
461+
existing_user_id,
462+
{
463+
"email": new_email or existing_user["email"],
464+
"username": new_email or existing_user["email"],
465+
"firstName": new_firstName or existing_user["firstName"],
466+
"lastName": new_lastName or existing_user["lastName"],
467+
},
468+
)
469+
if new_password:
470+
keycloak_admin.set_user_password(existing_user_id, new_password, False)
471+
472+
updated_user = keycloak_admin.get_user(existing_user_id)
473+
return updated_user
474+
475+
438476
def delete_user(email: str):
439477
"""Create a user in Keycloak."""
440478
keycloak_admin = KeycloakAdmin(

backend/app/models/users.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ class UserIn(UserBase):
1818
password: str
1919

2020

21+
class UserUpdate(BaseModel):
22+
email: Optional[EmailStr]
23+
first_name: Optional[str]
24+
last_name: Optional[str]
25+
password: Optional[str]
26+
27+
2128
class UserLogin(BaseModel):
2229
email: EmailStr
2330
password: str

backend/app/routers/authentication.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@
55
enable_disable_user,
66
get_current_user,
77
keycloak_openid,
8+
update_user,
89
)
910
from app.models.datasets import DatasetDBViewList
10-
from app.models.users import UserDB, UserIn, UserLogin, UserOut
11+
from app.models.users import UserDB, UserIn, UserLogin, UserOut, UserUpdate
1112
from beanie import PydanticObjectId
1213
from fastapi import APIRouter, Depends, HTTPException
1314
from keycloak.exceptions import (
1415
KeycloakAuthenticationError,
1516
KeycloakGetError,
1617
KeycloakPostError,
18+
KeycloakPutError,
1719
)
1820
from passlib.hash import bcrypt
1921

@@ -65,6 +67,47 @@ async def save_user(userIn: UserIn):
6567
return user.dict()
6668

6769

70+
@router.patch("/users", response_model=UserOut)
71+
async def update_current_user(
72+
userUpdate: UserUpdate, current_user=Depends(get_current_user)
73+
):
74+
try:
75+
await update_user(
76+
current_user.email,
77+
userUpdate.email,
78+
userUpdate.password,
79+
userUpdate.first_name,
80+
userUpdate.last_name,
81+
)
82+
except KeycloakGetError as e:
83+
raise HTTPException(
84+
status_code=e.response_code,
85+
detail=json.loads(e.error_message),
86+
headers={"WWW-Authenticate": "Bearer"},
87+
)
88+
except KeycloakPutError as e:
89+
raise HTTPException(
90+
status_code=e.response_code,
91+
detail=json.loads(e.error_message),
92+
headers={"WWW-Authenticate": "Bearer"},
93+
)
94+
95+
# Update local user
96+
user = await UserDB.find_one(UserDB.email == current_user.email)
97+
98+
if userUpdate.email:
99+
user.email = userUpdate.email
100+
if userUpdate.first_name:
101+
user.first_name = userUpdate.first_name
102+
if userUpdate.last_name:
103+
user.last_name = userUpdate.last_name
104+
if userUpdate.password:
105+
user.hashed_password = bcrypt.hash(userUpdate.password)
106+
107+
await user.save()
108+
return user.dict()
109+
110+
68111
@router.post("/login")
69112
async def login(userIn: UserLogin):
70113
try:

frontend/src/openapi/v2/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export type { UserAPIKeyOut } from './models/UserAPIKeyOut';
7878
export type { UserIn } from './models/UserIn';
7979
export type { UserLogin } from './models/UserLogin';
8080
export type { UserOut } from './models/UserOut';
81+
export type { UserUpdate } from './models/UserUpdate';
8182
export type { ValidationError } from './models/ValidationError';
8283
export type { VisualizationConfigIn } from './models/VisualizationConfigIn';
8384
export type { VisualizationConfigOut } from './models/VisualizationConfigOut';
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/* istanbul ignore file */
2+
/* tslint:disable */
3+
/* eslint-disable */
4+
5+
export type UserUpdate = {
6+
email?: string;
7+
first_name?: string;
8+
last_name?: string;
9+
password?: string;
10+
}

frontend/src/openapi/v2/services/LoginService.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import type { UserIn } from '../models/UserIn';
55
import type { UserLogin } from '../models/UserLogin';
66
import type { UserOut } from '../models/UserOut';
7+
import type { UserUpdate } from '../models/UserUpdate';
78
import type { CancelablePromise } from '../core/CancelablePromise';
89
import { request as __request } from '../core/request';
910

@@ -29,6 +30,26 @@ export class LoginService {
2930
});
3031
}
3132

33+
/**
34+
* Update Current User
35+
* @param requestBody
36+
* @returns UserOut Successful Response
37+
* @throws ApiError
38+
*/
39+
public static updateCurrentUserApiV2UsersPatch(
40+
requestBody: UserUpdate,
41+
): CancelablePromise<UserOut> {
42+
return __request({
43+
method: 'PATCH',
44+
path: `/api/v2/users`,
45+
body: requestBody,
46+
mediaType: 'application/json',
47+
errors: {
48+
422: `Validation Error`,
49+
},
50+
});
51+
}
52+
3253
/**
3354
* Login
3455
* @param requestBody

openapi.json

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,56 @@
115115
}
116116
}
117117
}
118+
},
119+
"patch": {
120+
"tags": [
121+
"login"
122+
],
123+
"summary": "Update Current User",
124+
"operationId": "update_current_user_api_v2_users_patch",
125+
"requestBody": {
126+
"content": {
127+
"application/json": {
128+
"schema": {
129+
"$ref": "#/components/schemas/UserUpdate"
130+
}
131+
}
132+
},
133+
"required": true
134+
},
135+
"responses": {
136+
"200": {
137+
"description": "Successful Response",
138+
"content": {
139+
"application/json": {
140+
"schema": {
141+
"$ref": "#/components/schemas/UserOut"
142+
}
143+
}
144+
}
145+
},
146+
"422": {
147+
"description": "Validation Error",
148+
"content": {
149+
"application/json": {
150+
"schema": {
151+
"$ref": "#/components/schemas/HTTPValidationError"
152+
}
153+
}
154+
}
155+
}
156+
},
157+
"security": [
158+
{
159+
"OAuth2AuthorizationCodeBearer": []
160+
},
161+
{
162+
"APIKeyHeader": []
163+
},
164+
{
165+
"APIKeyCookie": []
166+
}
167+
]
118168
}
119169
},
120170
"/api/v2/login": {
@@ -14736,6 +14786,29 @@
1473614786
},
1473714787
"description": "Document Mapping class.\n\nFields:\n\n- `id` - MongoDB document ObjectID \"_id\" field.\nMapped to the PydanticObjectId class\n\nInherited from:\n\n- Pydantic BaseModel\n- [UpdateMethods](https://roman-right.github.io/beanie/api/interfaces/#aggregatemethods)"
1473814788
},
14789+
"UserUpdate": {
14790+
"title": "UserUpdate",
14791+
"type": "object",
14792+
"properties": {
14793+
"email": {
14794+
"title": "Email",
14795+
"type": "string",
14796+
"format": "email"
14797+
},
14798+
"first_name": {
14799+
"title": "First Name",
14800+
"type": "string"
14801+
},
14802+
"last_name": {
14803+
"title": "Last Name",
14804+
"type": "string"
14805+
},
14806+
"password": {
14807+
"title": "Password",
14808+
"type": "string"
14809+
}
14810+
}
14811+
},
1473914812
"ValidationError": {
1474014813
"title": "ValidationError",
1474114814
"required": [

0 commit comments

Comments
 (0)