Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions backend/app/keycloak_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import json
import logging
from datetime import datetime
from typing import Optional

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


async def update_user(
email: str,
new_email: Optional[str],
new_password: Optional[str],
new_firstName: Optional[str],
new_lastName: Optional[str],
):
"""Update existing user in Keycloak."""
keycloak_admin = KeycloakAdmin(
server_url=settings.auth_server_url,
username=settings.keycloak_username,
password=settings.keycloak_password,
realm_name=settings.keycloak_realm_name,
user_realm_name=settings.keycloak_user_realm_name,
# client_secret_key=settings.auth_client_secret,
# client_id=settings.keycloak_client_id,
verify=True,
)
existing_user_id = keycloak_admin.get_user_id(email)
existing_user = keycloak_admin.get_user(existing_user_id)
# Update user and set password
keycloak_admin.update_user(
existing_user_id,
{
"email": new_email or existing_user["email"],
"username": new_email or existing_user["email"],
"firstName": new_firstName or existing_user["firstName"],
"lastName": new_lastName or existing_user["lastName"],
},
)
if new_password:
keycloak_admin.set_user_password(existing_user_id, new_password, False)

updated_user = keycloak_admin.get_user(existing_user_id)
return updated_user


def delete_user(email: str):
"""Create a user in Keycloak."""
keycloak_admin = KeycloakAdmin(
Expand Down
6 changes: 6 additions & 0 deletions backend/app/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ class UserIn(UserBase):
password: str


class UserUpdate(BaseModel):
first_name: Optional[str]
last_name: Optional[str]
password: Optional[str]


class UserLogin(BaseModel):
email: EmailStr
password: str
Expand Down
43 changes: 42 additions & 1 deletion backend/app/routers/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@
enable_disable_user,
get_current_user,
keycloak_openid,
update_user,
)
from app.models.datasets import DatasetDBViewList
from app.models.users import UserDB, UserIn, UserLogin, UserOut
from app.models.users import UserDB, UserIn, UserLogin, UserOut, UserUpdate
from app.routers.utils import save_refresh_token
from beanie import PydanticObjectId
from fastapi import APIRouter, Depends, HTTPException
from keycloak.exceptions import (
KeycloakAuthenticationError,
KeycloakGetError,
KeycloakPostError,
KeycloakPutError,
)
from passlib.hash import bcrypt

Expand Down Expand Up @@ -97,6 +99,45 @@ async def authenticate_user(email: str, password: str):
return user


@router.patch("/users/me", response_model=UserOut)
async def update_current_user(
userUpdate: UserUpdate, current_user=Depends(get_current_user)
):
try:
await update_user(
current_user.email,
None,
userUpdate.password,
userUpdate.first_name,
userUpdate.last_name,
)
except KeycloakGetError as e:
raise HTTPException(
status_code=e.response_code,
detail=json.loads(e.error_message),
headers={"WWW-Authenticate": "Bearer"},
)
except KeycloakPutError as e:
raise HTTPException(
status_code=e.response_code,
detail=json.loads(e.error_message),
headers={"WWW-Authenticate": "Bearer"},
)

# Update local user
user = await UserDB.find_one(UserDB.email == current_user.email)

if userUpdate.first_name:
user.first_name = userUpdate.first_name
if userUpdate.last_name:
user.last_name = userUpdate.last_name
if userUpdate.password:
user.hashed_password = bcrypt.hash(userUpdate.password)

await user.save()
return user.dict()


@router.get("/users/me/is_admin", response_model=bool)
async def get_admin(
dataset_id: str = None, current_username=Depends(get_current_user)
Expand Down
1 change: 1 addition & 0 deletions frontend/src/openapi/v2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export type { UserAPIKeyOut } from './models/UserAPIKeyOut';
export type { UserIn } from './models/UserIn';
export type { UserLogin } from './models/UserLogin';
export type { UserOut } from './models/UserOut';
export type { UserUpdate } from './models/UserUpdate';
export type { ValidationError } from './models/ValidationError';
export type { VisualizationConfigIn } from './models/VisualizationConfigIn';
export type { VisualizationConfigOut } from './models/VisualizationConfigOut';
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/openapi/v2/models/UserUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */

export type UserUpdate = {
first_name?: string;
last_name?: string;
password?: string;
}
21 changes: 21 additions & 0 deletions frontend/src/openapi/v2/services/LoginService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import type { UserIn } from '../models/UserIn';
import type { UserLogin } from '../models/UserLogin';
import type { UserOut } from '../models/UserOut';
import type { UserUpdate } from '../models/UserUpdate';
import type { CancelablePromise } from '../core/CancelablePromise';
import { request as __request } from '../core/request';

Expand Down Expand Up @@ -49,6 +50,26 @@ export class LoginService {
});
}

/**
* Update Current User
* @param requestBody
* @returns UserOut Successful Response
* @throws ApiError
*/
public static updateCurrentUserApiV2UsersMePatch(
requestBody: UserUpdate,
): CancelablePromise<UserOut> {
return __request({
method: 'PATCH',
path: `/api/v2/users/me`,
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}

/**
* Get Admin
* @param datasetId
Expand Down
70 changes: 70 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,58 @@
}
}
},
"/api/v2/users/me": {
"patch": {
"tags": [
"login"
],
"summary": "Update Current User",
"operationId": "update_current_user_api_v2_users_me_patch",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserUpdate"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserOut"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
},
"security": [
{
"OAuth2AuthorizationCodeBearer": []
},
{
"APIKeyHeader": []
},
{
"APIKeyCookie": []
}
]
}
},
"/api/v2/users/me/is_admin": {
"get": {
"tags": [
Expand Down Expand Up @@ -14736,6 +14788,24 @@
},
"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)"
},
"UserUpdate": {
"title": "UserUpdate",
"type": "object",
"properties": {
"first_name": {
"title": "First Name",
"type": "string"
},
"last_name": {
"title": "Last Name",
"type": "string"
},
"password": {
"title": "Password",
"type": "string"
}
}
},
"ValidationError": {
"title": "ValidationError",
"required": [
Expand Down
Loading