diff --git a/.env b/.env index 8f71f45d..be7405d3 100644 --- a/.env +++ b/.env @@ -4,7 +4,7 @@ ENVIRONMENT=dev DB_HOST=localhost DB_USER=postgres DB_PORT=5432 -DB_PASSWORD=quick +DB_PASSWORD=admin DB_NAME=QuickGpt SUPER_ADMIN_EMAIL=superadmin@email.com diff --git a/alembic/versions/62c1a29320fc_updated_audit_log.py b/alembic/versions/62c1a29320fc_updated_audit_log.py deleted file mode 100644 index 8bfaf661..00000000 --- a/alembic/versions/62c1a29320fc_updated_audit_log.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Updated audit log - -Revision ID: 62c1a29320fc -Revises: b526c0676007 -Create Date: 2024-02-24 17:27:33.407895 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision: str = '62c1a29320fc' -down_revision: Union[str, None] = 'b526c0676007' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('audit', 'timestamp', - existing_type=postgresql.TIMESTAMP(), - nullable=False) - op.alter_column('audit', 'model', - existing_type=sa.VARCHAR(), - nullable=False) - op.alter_column('audit', 'action', - existing_type=sa.VARCHAR(), - nullable=False) - op.drop_column('audit', 'url') - op.drop_column('audit', 'model_id') - # op.create_unique_constraint('unique_user_role', 'user_roles', ['user_id', 'role_id', 'company_id']) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - # op.drop_constraint('unique_user_role', 'user_roles', type_='unique') - op.add_column('audit', sa.Column('model_id', sa.INTEGER(), autoincrement=False, nullable=True)) - op.add_column('audit', sa.Column('url', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.alter_column('audit', 'action', - existing_type=sa.VARCHAR(), - nullable=True) - op.alter_column('audit', 'model', - existing_type=sa.VARCHAR(), - nullable=True) - op.alter_column('audit', 'timestamp', - existing_type=postgresql.TIMESTAMP(), - nullable=True) - # ### end Alembic commands ### diff --git a/alembic/versions/eb0a7f182c75_audit_log.py b/alembic/versions/9aa759c05b19_create_audit_model.py similarity index 60% rename from alembic/versions/eb0a7f182c75_audit_log.py rename to alembic/versions/9aa759c05b19_create_audit_model.py index c43a7de7..b48d89f6 100644 --- a/alembic/versions/eb0a7f182c75_audit_log.py +++ b/alembic/versions/9aa759c05b19_create_audit_model.py @@ -1,18 +1,18 @@ -"""Audit log +"""Create audit model -Revision ID: eb0a7f182c75 +Revision ID: 9aa759c05b19 Revises: -Create Date: 2024-02-24 11:37:43.713632 +Create Date: 2024-02-25 10:03:28.092131 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa - +from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. -revision: str = 'eb0a7f182c75' +revision: str = '9aa759c05b19' down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -22,19 +22,23 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table('audit', sa.Column('id', sa.Integer(), nullable=False), - sa.Column('url', sa.String(), nullable=True), - sa.Column('headers', sa.ARRAY(sa.String()), nullable=True), - sa.Column('method', sa.String(), nullable=True), - sa.Column('response', sa.String(), nullable=True), + sa.Column('timestamp', sa.DateTime(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('model', sa.String(), nullable=False), + sa.Column('action', sa.String(), nullable=False), + sa.Column('details', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), sa.PrimaryKeyConstraint('id') ) op.create_index(op.f('ix_audit_id'), 'audit', ['id'], unique=False) # op.create_unique_constraint('unique_user_role', 'user_roles', ['user_id', 'role_id', 'company_id']) + op.add_column('users', sa.Column('password_created', sa.DateTime(), nullable=True)) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('users', 'password_created') # op.drop_constraint('unique_user_role', 'user_roles', type_='unique') op.drop_index(op.f('ix_audit_id'), table_name='audit') op.drop_table('audit') diff --git a/alembic/versions/b526c0676007_audit_log_update.py b/alembic/versions/b526c0676007_audit_log_update.py deleted file mode 100644 index 8f5163d3..00000000 --- a/alembic/versions/b526c0676007_audit_log_update.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Audit log update - -Revision ID: b526c0676007 -Revises: eb0a7f182c75 -Create Date: 2024-02-24 12:00:11.632187 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision: str = 'b526c0676007' -down_revision: Union[str, None] = 'eb0a7f182c75' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('audit', sa.Column('timestamp', sa.DateTime(), nullable=True)) - op.add_column('audit', sa.Column('user_id', sa.Integer(), nullable=True)) - op.add_column('audit', sa.Column('model', sa.String(), nullable=True)) - op.add_column('audit', sa.Column('model_id', sa.Integer(), nullable=True)) - op.add_column('audit', sa.Column('action', sa.String(), nullable=True)) - op.add_column('audit', sa.Column('details', postgresql.JSONB(astext_type=sa.Text()), nullable=True)) - op.create_foreign_key(None, 'audit', 'users', ['user_id'], ['id']) - op.drop_column('audit', 'headers') - op.drop_column('audit', 'response') - op.drop_column('audit', 'method') - # op.create_unique_constraint('unique_user_role', 'user_roles', ['user_id', 'role_id', 'company_id']) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - # op.drop_constraint('unique_user_role', 'user_roles', type_='unique') - op.add_column('audit', sa.Column('method', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('audit', sa.Column('response', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('audit', sa.Column('headers', postgresql.ARRAY(sa.VARCHAR()), autoincrement=False, nullable=True)) - op.drop_constraint(None, 'audit', type_='foreignkey') - op.drop_column('audit', 'details') - op.drop_column('audit', 'action') - op.drop_column('audit', 'model_id') - op.drop_column('audit', 'model') - op.drop_column('audit', 'user_id') - op.drop_column('audit', 'timestamp') - # ### end Alembic commands ### diff --git a/alembic/versions/b7b23e5d5214_added_password_expiry.py b/alembic/versions/b7b23e5d5214_added_password_expiry.py deleted file mode 100644 index 1100122f..00000000 --- a/alembic/versions/b7b23e5d5214_added_password_expiry.py +++ /dev/null @@ -1,32 +0,0 @@ -"""added password expiry - -Revision ID: b7b23e5d5214 -Revises: 62c1a29320fc -Create Date: 2024-02-24 19:47:49.069873 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'b7b23e5d5214' -down_revision: Union[str, None] = '62c1a29320fc' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - # op.create_unique_constraint('unique_user_role', 'user_roles', ['user_id', 'role_id', 'company_id']) - op.add_column('users', sa.Column('password_created', sa.DateTime(), nullable=True)) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('users', 'password_created') - # op.drop_constraint('unique_user_role', 'user_roles', type_='unique') - # ### end Alembic commands ### diff --git a/private_gpt/server/chat/chat_router.py b/private_gpt/server/chat/chat_router.py index f6ddeee7..50232619 100644 --- a/private_gpt/server/chat/chat_router.py +++ b/private_gpt/server/chat/chat_router.py @@ -103,6 +103,7 @@ def chat_completion( use_context=body.use_context, context_filter=body.context_filter, ) + return to_openai_response( completion.response, completion.sources if body.include_sources else None ) diff --git a/private_gpt/server/completions/completions_router.py b/private_gpt/server/completions/completions_router.py index 17fa76e5..187619bb 100644 --- a/private_gpt/server/completions/completions_router.py +++ b/private_gpt/server/completions/completions_router.py @@ -106,6 +106,7 @@ def prompt_completion( db: Session = Depends(deps.get_db), current_user: models.User = Security( deps.get_current_user, + deps.get_active_subscription, ), ) -> OpenAICompletion | StreamingResponse: try: diff --git a/private_gpt/server/ingest/ingest_router.py b/private_gpt/server/ingest/ingest_router.py index 35bd3e98..b80ca1c6 100644 --- a/private_gpt/server/ingest/ingest_router.py +++ b/private_gpt/server/ingest/ingest_router.py @@ -160,10 +160,17 @@ def delete_file( print("Unable to delete file from the static directory") document = crud.documents.get_by_filename(db,file_name=filename) if document: - log_audit(model='Document', action='delete', - details={"status": "SUCCESS", "message": f"{filename}' successfully deleted."}, user_id=current_user.id) + log_audit( + model='Document', + action='delete', + details={ + "detail": f"{filename}' deleted successfully.", + 'user': current_user.fullname, + }, + user_id=current_user.id + ) crud.documents.remove(db=db, id=document.id) - return {"status": "SUCCESS", "message": f"{filename}' successfully deleted."} + return {"status": "SUCCESS", "message": f"{filename}' deleted successfully."} except Exception as e: print(traceback.print_exc()) logger.error( @@ -200,14 +207,14 @@ def ingest_file( detail="No file name provided", ) - try: - docs_in = schemas.DocumentCreate(filename=file.filename, uploaded_by=current_user.id, department_id=current_user.department_id) - crud.documents.create(db=db, obj_in=docs_in) - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Unable to upload file.", - ) + # try: + docs_in = schemas.DocumentCreate(filename=file.filename, uploaded_by=current_user.id, department_id=current_user.department_id) + crud.documents.create(db=db, obj_in=docs_in) + # except Exception as e: + # raise HTTPException( + # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + # detail="Unable to upload file.", + # ) upload_path = Path(f"{UPLOAD_DIR}/{file.filename}") with open(upload_path, "wb") as f: @@ -218,11 +225,13 @@ def ingest_file( logger.info(f"{file.filename} is uploaded by the {current_user.fullname}.") response = IngestResponse( object="list", model="private-gpt", data=ingested_documents) + log_audit(model='Document', action='create', details={ - 'filename': file.filename, + 'filename': f"{file.filename} uploaded successfully", 'user': current_user.fullname, }, user_id=current_user.id) + return response except HTTPException: print(traceback.print_exc()) @@ -274,8 +283,16 @@ async def common_ingest_logic( f.write(file.read()) file.seek(0) ingested_documents = service.ingest_bin_data(file_name, file) - log_audit(model='Document', action='create', - details={'status': "SUCCESS", 'message': f"{file_name} uploaded successfully."}, user_id=current_user.id) + + log_audit( + model='Document', + action='create', + details={ + 'detail': f"{file_name} uploaded successfully", + 'user': f"{current_user.fullname}" + }, + user_id=current_user.id + ) logger.info( f"{file_name} is uploaded by the {current_user.fullname}.") diff --git a/private_gpt/users/api/deps.py b/private_gpt/users/api/deps.py index c42525c6..c7e8fa98 100644 --- a/private_gpt/users/api/deps.py +++ b/private_gpt/users/api/deps.py @@ -117,17 +117,16 @@ def get_company_name(company_id: int, db: Session = Depends(get_db)) -> str: def get_active_subscription( - current_user: models.User = Depends(get_current_user), db: Session = Depends(get_db), ): - company_id = current_user.user_role.company_id + company_id = 1 if company_id: company = crud.company.get(db, company_id) if company and company.subscriptions: active_subscription = next((sub for sub in company.subscriptions if sub.is_active), None) if active_subscription: - return company - + print("Has active Subscription") + return active_subscription raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access Forbidden - No Active Subscription", diff --git a/private_gpt/users/api/v1/api.py b/private_gpt/users/api/v1/api.py index 53cb59c5..dd003c62 100644 --- a/private_gpt/users/api/v1/api.py +++ b/private_gpt/users/api/v1/api.py @@ -1,3 +1,4 @@ +from private_gpt.users.api import deps from private_gpt.users.api.v1.routers import auth, roles, user_roles, users, subscriptions, companies, departments, documents, audits from fastapi import APIRouter diff --git a/private_gpt/users/api/v1/routers/auth.py b/private_gpt/users/api/v1/routers/auth.py index e3b89a2e..d59f4261 100644 --- a/private_gpt/users/api/v1/routers/auth.py +++ b/private_gpt/users/api/v1/routers/auth.py @@ -111,6 +111,7 @@ def login_access_token( log_audit: models.Audit = Depends(deps.get_audit_logger), db: Session = Depends(deps.get_db), form_data: OAuth2PasswordRequestForm = Depends(), + active_subscription: models.Subscription = Depends(deps.get_active_subscription) ) -> Any: """ OAuth2 compatible token login, get an access token for future requests @@ -208,7 +209,7 @@ def register( # password: str = Body(...), company_id: int = Body(None, title="Company ID", description="Company ID for the user (if applicable)"), - department_id: str = Body(None, title="Department ID", + department_id: int = Body(None, title="Department ID", description="Department name for the user (if applicable)"), role_name: str = Body(None, title="Role Name", description="User role name (if applicable)"), diff --git a/private_gpt/users/api/v1/routers/departments.py b/private_gpt/users/api/v1/routers/departments.py index bab12f1d..f0c5709c 100644 --- a/private_gpt/users/api/v1/routers/departments.py +++ b/private_gpt/users/api/v1/routers/departments.py @@ -92,7 +92,7 @@ def create_department( department1 = jsonable_encoder(department) details = { - 'user_id': current_user.id, + 'detail': 'Department created successfully', 'department_id': department.id, 'department_name': department.name } @@ -131,16 +131,6 @@ def read_department( department = crud.department.get_by_id(db, id=department_id) if department is None: raise HTTPException(status_code=404, detail="Department not found") - - details = { - 'status': 200, - 'user_id': current_user.id, - 'department_id': department.id, - 'department_name': department.name - } - - log_audit_department(db, current_user, 'read', details) - return department except Exception as e: print(traceback.format_exc()) @@ -174,8 +164,7 @@ def update_department( updated_department = jsonable_encoder(updated_department) details = { - 'status': '200', - 'user_id': current_user.id, + 'detail': 'Department updated successfully!', 'department_id': department.id, 'old_department_name': old_name, 'new_department_name': department.name, @@ -218,18 +207,16 @@ def delete_department( raise HTTPException(status_code=404, detail="Department not found") details = { - 'status': 200, - 'user_id': current_user.id, + 'detail': "Department deleted successfully!", 'department_id': department.id, 'department_name': department.name } - log_audit_department(db, current_user, 'delete', details) deleted_department = crud.department.remove(db=db, id=department_id) + log_audit_department(db, current_user, 'delete', details) + deleted_department = jsonable_encoder(deleted_department) - - return JSONResponse( status_code=status.HTTP_200_OK, content={ diff --git a/private_gpt/users/api/v1/routers/users.py b/private_gpt/users/api/v1/routers/users.py index 62a76b54..ae4c7e89 100644 --- a/private_gpt/users/api/v1/routers/users.py +++ b/private_gpt/users/api/v1/routers/users.py @@ -127,6 +127,7 @@ def update_username( Update own username. """ user_in = schemas.UserUpdate(fullname=update_in.fullname, email=current_user.email, company_id=current_user.company_id) + old_fullname = current_user.fullname user = crud.user.update(db, db_obj=current_user, obj_in=user_in) user_data = schemas.UserBaseSchema( email=user.email, @@ -135,11 +136,8 @@ def update_username( department_id=user.department_id, ) details = { - 'user_id': user.id, - 'email': user.email, - 'fullname': user.fullname, - 'company_id': user.company_id, - 'department_id': user.department_id, + 'old_fullname': old_fullname, + 'new_fullname': user.fullname, } log_audit_user(db, current_user, 'update_username', details) return JSONResponse( @@ -200,11 +198,8 @@ def change_password( ) details = { + 'detail': 'Password changed successfully!', 'user_id': current_user.id, - 'email': current_user.email, - 'fullname': current_user.fullname, - 'company_id': current_user.company_id, - 'department_id': current_user.department_id, } log_audit_user(db, current_user, 'change_password', details) @@ -346,11 +341,9 @@ def delete_user( user = crud.user.get(db, id=user_id) details = { - 'admin_id': current_user.id, - 'deleted_user_id': user.id, + 'detail': "user deleted successfully!", 'email': user.email, 'fullname': user.fullname, - 'company_id': user.company_id, 'department_id': user.department_id, } @@ -378,53 +371,66 @@ def admin_update_user( """ Update the user by the Admin/Super_ADMIN """ - existing_user = crud.user.get(db, id=user_update.id) + try: - if existing_user is None: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"User not found with id: {user_update.id}", - ) - if existing_user.fullname == user_update.fullname: - pass - else: - fullname = crud.user.get_by_name(db, name=user_update.fullname) - if fullname: + existing_user = crud.user.get_by_id(db, id=user_update.id) + + if existing_user is None: raise HTTPException( - status_code=409, - detail="The user with this username already exists!", + status_code=status.HTTP_404_NOT_FOUND, + detail=f"User not found with id: {user_update.id}", + ) + old_detail = { + 'fullnam': existing_user.fullname, + 'role': existing_user.user_role.role.name, + 'department': existing_user.department_id + } + if existing_user.fullname == user_update.fullname: + pass + else: + fullname = crud.user.get_by_name(db, name=user_update.fullname) + if fullname: + raise HTTPException( + status_code=409, + detail="The user with this username already exists!", + ) + + role = crud.role.get_by_name(db, name=user_update.role) + if role.id == 1: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Cannot create SUPER ADMIN!", ) - role = crud.role.get_by_name(db, name=user_update.role) - if role.id == 1: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Cannot create SUPER ADMIN!", + user_role = crud.user_role.get_by_user_id(db, user_id=existing_user.id) + role_in = schemas.UserRoleUpdate( + user_id=existing_user.id, + role_id=role.id, ) + role = crud.user_role.update(db, db_obj=user_role, obj_in=role_in) + print(f"THe new user name : {user_update.fullname} Department: {user_update.department_id}") + user_update_in = schemas.UserAdmin(fullname=user_update.fullname, department_id=user_update.department_id) - user_role = crud.user_role.get_by_user_id(db, user_id=existing_user.id) - role_in = schemas.UserRoleUpdate( - user_id=existing_user.id, - role_id=role.id, - ) - role = crud.user_role.update(db, db_obj=user_role, obj_in=role_in) + new_detail = { + 'email': existing_user.email, + 'fullname': existing_user.fullname, + 'department_id': existing_user.department_id, + } + details = { + 'old detail': old_detail, + 'new detail': new_detail, + } + log_audit_user(db, current_user, 'admin_update', details) + user = crud.user.get_by_id(db, id=existing_user.id) + crud.user.update(db, db_obj=user, obj_in=user_update_in) - user_in = schemas.UserAdmin(fullname=user_update.fullname, department_id=user_update.department_id) - - details = { - 'admin_id': current_user.id, - 'user_id': existing_user.id, - 'email': existing_user.email, - 'fullname': existing_user.fullname, - 'company_id': existing_user.company_id, - 'department_id': existing_user.department_id, - } - log_audit_user(db, current_user, 'admin_update', details) - - crud.user.update(db, db_obj=existing_user, obj_in=user_in) - - return JSONResponse( - status_code=status.HTTP_200_OK, - content={"message": "User updated successfully", - } - ) + return JSONResponse( + status_code=status.HTTP_200_OK, + content={"message": "User updated successfully"} + ) + except Exception as e: + print(traceback.print_exc()) + return HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Unable to update user' + ) \ No newline at end of file diff --git a/private_gpt/users/core/config.py b/private_gpt/users/core/config.py index 34d668cc..4cc02b24 100644 --- a/private_gpt/users/core/config.py +++ b/private_gpt/users/core/config.py @@ -8,7 +8,7 @@ SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{username}:{password}@{host}:{p port='5432', db_name='QuickGpt', username='postgres', - password="quick", + password="admin", ) class Settings(BaseSettings): diff --git a/private_gpt/users/crud/department_crud.py b/private_gpt/users/crud/department_crud.py index 8c6c8376..7cc47382 100644 --- a/private_gpt/users/crud/department_crud.py +++ b/private_gpt/users/crud/department_crud.py @@ -17,11 +17,9 @@ class CRUDDepartments(CRUDBase[Department, DepartmentCreate, DepartmentUpdate]): ) -> List[Department]: return ( db.query(self.model) - .filter(Department.department_id == department_id) + .filter(Department.id == department_id) .offset(skip) .limit(limit) .all() ) - - department = CRUDDepartments(Department)