Added audit logs

This commit is contained in:
Saurab-Shrestha 2024-02-24 18:57:49 +05:45
parent 59d9413898
commit 493963908d
28 changed files with 596 additions and 329 deletions

2
.env
View File

@ -4,7 +4,7 @@ ENVIRONMENT=dev
DB_HOST=localhost DB_HOST=localhost
DB_USER=postgres DB_USER=postgres
DB_PORT=5432 DB_PORT=5432
DB_PASSWORD=admin DB_PASSWORD=quick
DB_NAME=QuickGpt DB_NAME=QuickGpt
SUPER_ADMIN_EMAIL=superadmin@email.com SUPER_ADMIN_EMAIL=superadmin@email.com

View File

@ -16,6 +16,7 @@ from private_gpt.users.models.subscription import Subscription
from private_gpt.users.models.company import Company from private_gpt.users.models.company import Company
from private_gpt.users.models.document import Document from private_gpt.users.models.document import Document
from private_gpt.users.models.department import Department from private_gpt.users.models.department import Department
from private_gpt.users.models.audit import Audit
# this is the Alembic Config object, which provides # this is the Alembic Config object, which provides
# access to the values within the .ini file in use. # access to the values within the .ini file in use.
config = context.config config = context.config

View File

@ -1,116 +0,0 @@
"""Create models
Revision ID: 0aeaf9df35a6
Revises:
Create Date: 2024-02-20 19:16:15.608391
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '0aeaf9df35a6'
down_revision: Union[str, None] = None
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_table('companies',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_companies_id'), 'companies', ['id'], unique=False)
op.create_index(op.f('ix_companies_name'), 'companies', ['name'], unique=True)
op.create_table('roles',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=100), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_roles_id'), 'roles', ['id'], unique=False)
op.create_index(op.f('ix_roles_name'), 'roles', ['name'], unique=False)
op.create_table('departments',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.Column('company_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['company_id'], ['companies.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_departments_id'), 'departments', ['id'], unique=False)
op.create_index(op.f('ix_departments_name'), 'departments', ['name'], unique=True)
op.create_table('subscriptions',
sa.Column('sub_id', sa.Integer(), nullable=False),
sa.Column('company_id', sa.Integer(), nullable=True),
sa.Column('start_date', sa.DateTime(), nullable=True),
sa.Column('end_date', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['company_id'], ['companies.id'], ),
sa.PrimaryKeyConstraint('sub_id')
)
op.create_index(op.f('ix_subscriptions_sub_id'), 'subscriptions', ['sub_id'], unique=False)
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(length=225), nullable=False),
sa.Column('hashed_password', sa.String(), nullable=False),
sa.Column('fullname', sa.String(length=225), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('last_login', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('company_id', sa.Integer(), nullable=True),
sa.Column('department_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['company_id'], ['companies.id'], ),
sa.ForeignKeyConstraint(['department_id'], ['departments.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email'),
sa.UniqueConstraint('fullname'),
sa.UniqueConstraint('fullname', name='unique_username_no_spacing')
)
op.create_table('document',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('filename', sa.String(length=225), nullable=False),
sa.Column('uploaded_by', sa.Integer(), nullable=False),
sa.Column('uploaded_at', sa.DateTime(), nullable=False),
sa.Column('department_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['department_id'], ['departments.id'], ),
sa.ForeignKeyConstraint(['uploaded_by'], ['users.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('filename')
)
op.create_index(op.f('ix_document_id'), 'document', ['id'], unique=False)
op.create_table('user_roles',
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('role_id', sa.Integer(), nullable=False),
sa.Column('company_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['company_id'], ['companies.id'], ),
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('user_id', 'role_id', 'company_id'),
sa.UniqueConstraint('user_id', 'role_id', 'company_id', name='unique_user_role')
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user_roles')
op.drop_index(op.f('ix_document_id'), table_name='document')
op.drop_table('document')
op.drop_table('users')
op.drop_index(op.f('ix_subscriptions_sub_id'), table_name='subscriptions')
op.drop_table('subscriptions')
op.drop_index(op.f('ix_departments_name'), table_name='departments')
op.drop_index(op.f('ix_departments_id'), table_name='departments')
op.drop_table('departments')
op.drop_index(op.f('ix_roles_name'), table_name='roles')
op.drop_index(op.f('ix_roles_id'), table_name='roles')
op.drop_table('roles')
op.drop_index(op.f('ix_companies_name'), table_name='companies')
op.drop_index(op.f('ix_companies_id'), table_name='companies')
op.drop_table('companies')
# ### end Alembic commands ###

View File

@ -0,0 +1,52 @@
"""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 ###

View File

@ -1,118 +0,0 @@
"""Create model
Revision ID: 8c4bd1aaf45a
Revises:
Create Date: 2024-02-22 13:19:29.947241
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '8c4bd1aaf45a'
down_revision: Union[str, None] = None
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_table('companies',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_companies_id'), 'companies', ['id'], unique=False)
op.create_index(op.f('ix_companies_name'), 'companies', ['name'], unique=True)
op.create_table('roles',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=100), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_roles_id'), 'roles', ['id'], unique=False)
op.create_index(op.f('ix_roles_name'), 'roles', ['name'], unique=False)
op.create_table('departments',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.Column('company_id', sa.Integer(), nullable=True),
sa.Column('total_users', sa.Integer(), nullable=True),
sa.Column('total_documents', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['company_id'], ['companies.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_departments_id'), 'departments', ['id'], unique=False)
op.create_index(op.f('ix_departments_name'), 'departments', ['name'], unique=True)
op.create_table('subscriptions',
sa.Column('sub_id', sa.Integer(), nullable=False),
sa.Column('company_id', sa.Integer(), nullable=True),
sa.Column('start_date', sa.DateTime(), nullable=True),
sa.Column('end_date', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['company_id'], ['companies.id'], ),
sa.PrimaryKeyConstraint('sub_id')
)
op.create_index(op.f('ix_subscriptions_sub_id'), 'subscriptions', ['sub_id'], unique=False)
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(length=225), nullable=False),
sa.Column('hashed_password', sa.String(), nullable=False),
sa.Column('fullname', sa.String(length=225), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('last_login', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('company_id', sa.Integer(), nullable=True),
sa.Column('department_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['company_id'], ['companies.id'], ),
sa.ForeignKeyConstraint(['department_id'], ['departments.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email'),
sa.UniqueConstraint('fullname'),
sa.UniqueConstraint('fullname', name='unique_username_no_spacing')
)
op.create_table('document',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('filename', sa.String(length=225), nullable=False),
sa.Column('uploaded_by', sa.Integer(), nullable=False),
sa.Column('uploaded_at', sa.DateTime(), nullable=False),
sa.Column('department_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['department_id'], ['departments.id'], ),
sa.ForeignKeyConstraint(['uploaded_by'], ['users.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('filename')
)
op.create_index(op.f('ix_document_id'), 'document', ['id'], unique=False)
op.create_table('user_roles',
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('role_id', sa.Integer(), nullable=False),
sa.Column('company_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['company_id'], ['companies.id'], ),
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('user_id', 'role_id', 'company_id'),
sa.UniqueConstraint('user_id', 'role_id', 'company_id', name='unique_user_role')
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user_roles')
op.drop_index(op.f('ix_document_id'), table_name='document')
op.drop_table('document')
op.drop_table('users')
op.drop_index(op.f('ix_subscriptions_sub_id'), table_name='subscriptions')
op.drop_table('subscriptions')
op.drop_index(op.f('ix_departments_name'), table_name='departments')
op.drop_index(op.f('ix_departments_id'), table_name='departments')
op.drop_table('departments')
op.drop_index(op.f('ix_roles_name'), table_name='roles')
op.drop_index(op.f('ix_roles_id'), table_name='roles')
op.drop_table('roles')
op.drop_index(op.f('ix_companies_name'), table_name='companies')
op.drop_index(op.f('ix_companies_id'), table_name='companies')
op.drop_table('companies')
# ### end Alembic commands ###

View File

@ -0,0 +1,50 @@
"""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 ###

View File

@ -1,8 +1,8 @@
"""update department model """Audit log
Revision ID: 36beb9b73c64 Revision ID: eb0a7f182c75
Revises: 0aeaf9df35a6 Revises:
Create Date: 2024-02-21 15:12:07.840057 Create Date: 2024-02-24 11:37:43.713632
""" """
from typing import Sequence, Union from typing import Sequence, Union
@ -12,16 +12,23 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = '36beb9b73c64' revision: str = 'eb0a7f182c75'
down_revision: Union[str, None] = '0aeaf9df35a6' down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None: def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.add_column('departments', sa.Column('total_users', sa.Integer(), nullable=True)) op.create_table('audit',
op.add_column('departments', sa.Column('total_documents', sa.Integer(), nullable=True)) 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.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.create_unique_constraint('unique_user_role', 'user_roles', ['user_id', 'role_id', 'company_id'])
# ### end Alembic commands ### # ### end Alembic commands ###
@ -29,6 +36,6 @@ def upgrade() -> None:
def downgrade() -> None: def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
# op.drop_constraint('unique_user_role', 'user_roles', type_='unique') # op.drop_constraint('unique_user_role', 'user_roles', type_='unique')
op.drop_column('departments', 'total_documents') op.drop_index(op.f('ix_audit_id'), table_name='audit')
op.drop_column('departments', 'total_users') op.drop_table('audit')
# ### end Alembic commands ### # ### end Alembic commands ###

View File

@ -1,5 +1,10 @@
# start a fastapi server with uvicorn # start a fastapi server with uvicorn
from datetime import datetime
from fastapi.middleware import Middleware
from private_gpt.users.db.session import SessionLocal
from private_gpt.users.models import Audit, User, Department, Document
from private_gpt.users.api.deps import get_audit_logger, get_db
import uvicorn import uvicorn
from private_gpt.main import app from private_gpt.main import app
@ -10,6 +15,7 @@ from private_gpt.constants import UPLOAD_DIR
# Set log_config=None to do not use the uvicorn logging configuration, and # Set log_config=None to do not use the uvicorn logging configuration, and
# use ours instead. For reference, see below: # use ours instead. For reference, see below:
# https://github.com/tiangolo/fastapi/discussions/7457#discussioncomment-5141108 # https://github.com/tiangolo/fastapi/discussions/7457#discussioncomment-5141108
app.mount("/static", StaticFiles(directory=UPLOAD_DIR), name="static") app.mount("/static", StaticFiles(directory=UPLOAD_DIR), name="static")
uvicorn.run(app, host="0.0.0.0", port=settings().server.port, log_config=None) uvicorn.run(app, host="0.0.0.0", port=settings().server.port, log_config=None)

View File

@ -131,6 +131,7 @@ def delete_ingested(request: Request, doc_id: str) -> None:
def delete_file( def delete_file(
request: Request, request: Request,
delete_input: DeleteFilename, delete_input: DeleteFilename,
log_audit: models.Audit = Depends(deps.get_audit_logger),
db: Session = Depends(deps.get_db), db: Session = Depends(deps.get_db),
current_user: models.User = Security( current_user: models.User = Security(
deps.get_current_user, deps.get_current_user,
@ -159,9 +160,12 @@ def delete_file(
print("Unable to delete file from the static directory") print("Unable to delete file from the static directory")
document = crud.documents.get_by_filename(db,file_name=filename) document = crud.documents.get_by_filename(db,file_name=filename)
if document: if document:
log_audit(model='Document', action='delete',
details={"status": "SUCCESS", "message": f"{filename}' successfully deleted."}, user_id=current_user.id)
crud.documents.remove(db=db, id=document.id) crud.documents.remove(db=db, id=document.id)
return {"status": "SUCCESS", "message": f"{filename}' successfully deleted."} return {"status": "SUCCESS", "message": f"{filename}' successfully deleted."}
except Exception as e: except Exception as e:
print(traceback.print_exc())
logger.error( logger.error(
f"Unexpected error deleting documents with filename '{filename}': {str(e)}") f"Unexpected error deleting documents with filename '{filename}': {str(e)}")
raise HTTPException( raise HTTPException(
@ -171,6 +175,8 @@ def delete_file(
@ingest_router.post("/ingest/file", response_model=IngestResponse, tags=["Ingestion"]) @ingest_router.post("/ingest/file", response_model=IngestResponse, tags=["Ingestion"])
def ingest_file( def ingest_file(
request: Request, request: Request,
log_audit: models.Audit = Depends(deps.get_audit_logger),
db: Session = Depends(deps.get_db), db: Session = Depends(deps.get_db),
file: UploadFile = File(...), file: UploadFile = File(...),
current_user: models.User = Security( current_user: models.User = Security(
@ -210,12 +216,23 @@ def ingest_file(
with open(upload_path, "rb") as f: with open(upload_path, "rb") as f:
ingested_documents = service.ingest_bin_data(file.filename, f) ingested_documents = service.ingest_bin_data(file.filename, f)
logger.info(f"{file.filename} is uploaded by the {current_user.fullname}.") logger.info(f"{file.filename} is uploaded by the {current_user.fullname}.")
response = IngestResponse(
return IngestResponse(object="list", model="private-gpt", data=ingested_documents) object="list", model="private-gpt", data=ingested_documents)
log_audit(model='Document', action='create',
details={
'status': '200',
'filename': file.filename,
'user': current_user.fullname,
}, user_id=current_user.id)
return response
except HTTPException: except HTTPException:
print(traceback.print_exc())
raise raise
except Exception as e: except Exception as e:
print(traceback.print_exc())
log_audit(model='Document', action='create',
details={"status": 500, "detail": "Internal Server Error: Unable to ingest file.", }, user_id=current_user.id)
logger.error(f"There was an error uploading the file(s): {str(e)}") logger.error(f"There was an error uploading the file(s): {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -225,11 +242,13 @@ def ingest_file(
async def common_ingest_logic( async def common_ingest_logic(
request: Request, request: Request,
db: Session, db: Session,
ocr_file, ocr_file,
current_user, current_user,
): ):
service = request.state.injector.get(IngestService) service = request.state.injector.get(IngestService)
log_audit: models.Audit = Depends(deps.get_audit_logger)
try: try:
with open(ocr_file, 'rb') as file: with open(ocr_file, 'rb') as file:
file_name = Path(ocr_file).name file_name = Path(ocr_file).name
@ -257,6 +276,8 @@ async def common_ingest_logic(
f.write(file.read()) f.write(file.read())
file.seek(0) # Move the file pointer back to the beginning file.seek(0) # Move the file pointer back to the beginning
ingested_documents = service.ingest_bin_data(file_name, file) ingested_documents = service.ingest_bin_data(file_name, file)
log_audit(model='Document', action='create',
details={'status': 200, 'message': "file uploaded successfully."}, user_id=current_user.id)
logger.info( logger.info(
f"{file_name} is uploaded by the {current_user.fullname}.") f"{file_name} is uploaded by the {current_user.fullname}.")
@ -264,10 +285,13 @@ async def common_ingest_logic(
return ingested_documents return ingested_documents
except HTTPException: except HTTPException:
print(traceback.print_exc())
raise raise
except Exception as e: except Exception as e:
logger.error(f"There was an error uploading the file(s): {str(e)}") print(traceback.print_exc())
log_audit(model='Document', action='create',
details={"status": 500, "detail": "Internal Server Error: Unable to ingest file.", }, user_id=current_user.id)
raise HTTPException( raise HTTPException(
status_code=500, status_code=500,
detail="Internal Server Error: Unable to ingest file.", detail="Internal Server Error: Unable to ingest file.",

View File

@ -1,3 +1,4 @@
from fastapi import Request, Depends, HTTPException
import logging import logging
from private_gpt.users.core.config import settings from private_gpt.users.core.config import settings
from private_gpt.users.constants.role import Role from private_gpt.users.constants.role import Role
@ -5,7 +6,7 @@ from typing import Union, Any, Generator
from datetime import datetime from datetime import datetime
from private_gpt.users import crud, models, schemas from private_gpt.users import crud, models, schemas
from private_gpt.users.db.session import SessionLocal from private_gpt.users.db.session import SessionLocal
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, Request, status
from fastapi.security import OAuth2PasswordBearer, SecurityScopes from fastapi.security import OAuth2PasswordBearer, SecurityScopes
from private_gpt.users.core.security import ( from private_gpt.users.core.security import (
ALGORITHM, ALGORITHM,
@ -13,6 +14,7 @@ from private_gpt.users.core.security import (
) )
from fastapi import Depends, HTTPException, Security, status from fastapi import Depends, HTTPException, Security, status
from jose import jwt from jose import jwt
from private_gpt.users.utils.audit import log_audit_entry
from pydantic import ValidationError from pydantic import ValidationError
from private_gpt.users.constants.role import Role from private_gpt.users.constants.role import Role
from private_gpt.users.schemas.token import TokenPayload from private_gpt.users.schemas.token import TokenPayload
@ -130,3 +132,10 @@ def get_active_subscription(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="Access Forbidden - No Active Subscription", detail="Access Forbidden - No Active Subscription",
) )
def get_audit_logger(request: Request, db: Session = Depends(get_db)):
try:
return lambda model, action, details, user_id=None: log_audit_entry(db, model, action, details, user_id)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error in get_audit_logger: {str(e)}")

View File

@ -1,4 +1,4 @@
from private_gpt.users.api.v1.routers import auth, roles, user_roles, users, subscriptions, companies, departments, documents from private_gpt.users.api.v1.routers import auth, roles, user_roles, users, subscriptions, companies, departments, documents, audits
from fastapi import APIRouter from fastapi import APIRouter
api_router = APIRouter(prefix="/v1") api_router = APIRouter(prefix="/v1")
@ -11,4 +11,5 @@ api_router.include_router(companies.router)
api_router.include_router(subscriptions.router) api_router.include_router(subscriptions.router)
api_router.include_router(departments.router) api_router.include_router(departments.router)
api_router.include_router(documents.router) api_router.include_router(documents.router)
api_router.include_router(audits.router)

View File

@ -0,0 +1,30 @@
from typing import Any, List
from sqlalchemy.orm import Session
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
from fastapi import APIRouter, Depends, HTTPException, status, Security
from private_gpt.users.api import deps
from private_gpt.users.constants.role import Role
from private_gpt.users import crud, models, schemas
router = APIRouter(prefix="/audit", tags=["Companies"])
@router.get("", response_model=List[schemas.Audit])
def list_companies(
db: Session = Depends(deps.get_db),
skip: int = 0,
limit: int = 100,
current_user: models.User = Security(
deps.get_current_user,
scopes=[Role.SUPER_ADMIN["name"]],
),
) -> List[schemas.Audit]:
"""
Retrieve a list of companies with pagination support.
"""
logs = crud.audit.get_multi(db, skip=skip, limit=limit)
return logs

View File

@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
LDAP_SERVER = settings.LDAP_SERVER LDAP_SERVER = settings.LDAP_SERVER
# LDAP_ENABLE = settings.LDAP_ENABLE # LDAP_ENABLE = settings.LDAP_ENABLE
LDAP_ENABLE = True LDAP_ENABLE = False
router = APIRouter(prefix="/auth", tags=["auth"]) router = APIRouter(prefix="/auth", tags=["auth"])
@ -108,6 +108,7 @@ def ad_user_register(
@router.post("/login", response_model=schemas.TokenSchema) @router.post("/login", response_model=schemas.TokenSchema)
def login_access_token( def login_access_token(
log_audit: models.Audit = Depends(deps.get_audit_logger),
db: Session = Depends(deps.get_db), db: Session = Depends(deps.get_db),
form_data: OAuth2PasswordRequestForm = Depends(), form_data: OAuth2PasswordRequestForm = Depends(),
) -> Any: ) -> Any:
@ -168,6 +169,8 @@ def login_access_token(
"user": token_payload, "user": token_payload,
"token_type": "bearer", "token_type": "bearer",
} }
log_audit(model='User', action='update',
details=token_payload, user_id=user.id)
return JSONResponse(content=response_dict) return JSONResponse(content=response_dict)
@ -197,6 +200,8 @@ def refresh_access_token(
@router.post("/register", response_model=schemas.TokenSchema) @router.post("/register", response_model=schemas.TokenSchema)
def register( def register(
*, *,
log_audit: models.Audit = Depends(deps.get_audit_logger),
db: Session = Depends(deps.get_db), db: Session = Depends(deps.get_db),
email: str = Body(...), email: str = Body(...),
fullname: str = Body(...), fullname: str = Body(...),
@ -218,6 +223,8 @@ def register(
existing_user = crud.user.get_by_email(db, email=email) existing_user = crud.user.get_by_email(db, email=email)
if existing_user: if existing_user:
log_audit(model='User', action='creation',
details={"status": '409', 'detail': "The user with this email already exists!", }, user_id=current_user.id)
raise HTTPException( raise HTTPException(
status_code=409, status_code=409,
detail="The user with this email already exists!", detail="The user with this email already exists!",
@ -245,6 +252,8 @@ def register(
) )
user_role_name = role_name or Role.GUEST["name"] user_role_name = role_name or Role.GUEST["name"]
user_role = create_user_role(db, user, user_role_name, company) user_role = create_user_role(db, user, user_role_name, company)
log_audit(model='user_roles', action='creation',
details={"status": '201', 'detail': "User role created successfully.", }, user_id=current_user.id)
except Exception as e: except Exception as e:
print(traceback.format_exc()) print(traceback.format_exc())
raise HTTPException( raise HTTPException(
@ -258,4 +267,7 @@ def register(
"token_type": "bearer", "token_type": "bearer",
"password": random_password, "password": random_password,
} }
log_audit(model='User', action='creation',
details={"status": '201', 'detail': "User created successfully.", }, user_id=current_user.id)
return JSONResponse(content=response_dict, status_code=status.HTTP_201_CREATED) return JSONResponse(content=response_dict, status_code=status.HTTP_201_CREATED)

View File

@ -1,4 +1,5 @@
from typing import Any, List import logging
import traceback
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
@ -9,28 +10,47 @@ from private_gpt.users.api import deps
from private_gpt.users.constants.role import Role from private_gpt.users.constants.role import Role
from private_gpt.users import crud, models, schemas from private_gpt.users import crud, models, schemas
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/departments", tags=["Departments"]) router = APIRouter(prefix="/departments", tags=["Departments"])
@router.get("", response_model=List[schemas.Department]) def log_audit_department(
def list_deparments( db: Session,
current_user: models.User,
action: str,
details: dict
):
try:
audit_entry = models.Audit(
user_id=current_user.id,
model='Department',
action=action,
details=details,
)
db.add(audit_entry)
db.commit()
except Exception as e:
print(traceback.format_exc())
@router.get("", response_model=list[schemas.Department])
def list_departments(
db: Session = Depends(deps.get_db), db: Session = Depends(deps.get_db),
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100,
current_user: models.User = Security( current_user: models.User = Security(
deps.get_current_user, deps.get_current_user,
), ),
) -> List[schemas.Department]: ) -> list[schemas.Department]:
""" """
Retrieve a list of department with pagination support. Retrieve a list of departments with pagination support.
""" """
deparments = crud.department.get_multi(db, skip=skip, limit=limit) departments = crud.department.get_multi(db, skip=skip, limit=limit)
return deparments return departments
@router.post("/create", response_model=schemas.Department) @router.post("/create", response_model=schemas.Department)
def create_deparment( def create_department(
department_in: schemas.DepartmentCreate, department_in: schemas.DepartmentCreate,
db: Session = Depends(deps.get_db), db: Session = Depends(deps.get_db),
current_user: models.User = Security( current_user: models.User = Security(
@ -41,18 +61,35 @@ def create_deparment(
""" """
Create a new department Create a new department
""" """
company_id = current_user.company_id try:
department_create_in = schemas.DepartmentAdminCreate(name=department_in.name, company_id=company_id) company_id = current_user.company_id
department = crud.department.create(db=db, obj_in=department_create_in) department_create_in = schemas.DepartmentAdminCreate(
department = jsonable_encoder(department) name=department_in.name, company_id=company_id)
department = crud.department.create(db=db, obj_in=department_create_in)
department1 = jsonable_encoder(department)
return JSONResponse( details = {
status_code=status.HTTP_201_CREATED, 'user_id': current_user.id,
content={ 'department_id': department.id,
"message": "Department created successfully", 'department_name': department.name
"department": department }
},
) log_audit_department(db, current_user, 'create', details)
return JSONResponse(
status_code=status.HTTP_201_CREATED,
content={
"message": "Department created successfully",
"department": department1
},
)
except Exception as e:
print(traceback.format_exc())
logger.error(f"Error creating department: {str(e)}")
raise HTTPException(
status_code=500,
detail="Internal Server Error",
)
@router.post("/read", response_model=schemas.Department) @router.post("/read", response_model=schemas.Department)
@ -67,10 +104,28 @@ def read_department(
""" """
Read a Department by ID Read a Department by ID
""" """
department = crud.department.get_by_id(db, id=department_id) try:
if department is None: department = crud.department.get_by_id(db, id=department_id)
raise HTTPException(status_code=404, detail="department not found") if department is None:
return department 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())
logger.error(f"Error reading department: {str(e)}")
raise HTTPException(
status_code=500,
detail="Internal Server Error",
)
@router.post("/update", response_model=schemas.Department) @router.post("/update", response_model=schemas.Department)
@ -85,20 +140,40 @@ def update_department(
""" """
Update a Department by ID Update a Department by ID
""" """
department = crud.department.get_by_id(db, id=department_in.id) try:
if department is None: department = crud.department.get_by_id(db, id=department_in.id)
raise HTTPException(status_code=404, detail="department not found") old_name = department.name
if department is None:
raise HTTPException(status_code=404, detail="Department not found")
updated_department = crud.department.update( updated_department = crud.department.update(
db=db, db_obj=department, obj_in=department_in) db=db, db_obj=department, obj_in=department_in)
updated_department = jsonable_encoder(updated_department) updated_department = jsonable_encoder(updated_department)
return JSONResponse(
status_code=status.HTTP_200_OK, details = {
content={ 'status': '200',
"message": f"{department_in} department updated successfully", 'user_id': current_user.id,
"department": updated_department 'department_id': department.id,
}, 'old_department_name': old_name,
) 'new_department_name': department.name,
}
log_audit_department(db, current_user, 'update', details)
return JSONResponse(
status_code=status.HTTP_200_OK,
content={
"message": f"Department updated successfully",
"department": updated_department
},
)
except Exception as e:
print(traceback.format_exc())
logger.error(f"Error updating department: {str(e)}")
raise HTTPException(
status_code=500,
detail="Internal Server Error",
)
@router.post("/delete", response_model=schemas.Department) @router.post("/delete", response_model=schemas.Department)
@ -113,19 +188,36 @@ def delete_department(
""" """
Delete a Department by ID Delete a Department by ID
""" """
department_id = department_in.id try:
department = crud.department.get(db, id=department_id) department_id = department_in.id
if department is None: department = crud.department.get(db, id=department_id)
raise HTTPException(status_code=404, detail="User not found") if department is None:
raise HTTPException(status_code=404, detail="Department not found")
department = crud.department.remove(db=db, id=department_id) details = {
if department is None: 'status': 200,
raise HTTPException(status_code=404, detail="department not found") 'user_id': current_user.id,
department = jsonable_encoder(department) 'department_id': department.id,
return JSONResponse( 'department_name': department.name
status_code=status.HTTP_200_OK, }
content={
"message": "Department deleted successfully", log_audit_department(db, current_user, 'delete', details)
"deparment": department,
}, deleted_department = crud.department.remove(db=db, id=department_id)
) deleted_department = jsonable_encoder(deleted_department)
return JSONResponse(
status_code=status.HTTP_200_OK,
content={
"message": "Department deleted successfully",
"department": deleted_department,
},
)
except Exception as e:
print(traceback.format_exc())
logger.error(f"Error deleting department: {str(e)}")
raise HTTPException(
status_code=500,
detail="Internal Server Error",
)

View File

@ -1,3 +1,4 @@
import traceback
from typing import Any, List, Optional from typing import Any, List, Optional
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@ -14,6 +15,26 @@ from private_gpt.users.core.security import verify_password, get_password_hash
router = APIRouter(prefix="/users", tags=["users"]) router = APIRouter(prefix="/users", tags=["users"])
def log_audit_user(
db: Session,
current_user: models.User,
action: str,
details: dict
):
try:
audit_entry = models.Audit(
user_id=current_user.id,
model='User',
action=action,
details=details,
)
db.add(audit_entry)
db.commit()
except Exception as e:
print(traceback.format_exc())
@router.get("", response_model=List[schemas.User]) @router.get("", response_model=List[schemas.User])
def read_users( def read_users(
skip: int = 0, skip: int = 0,
@ -78,6 +99,17 @@ def create_user(
detail="The user with this email already exists in the system.", detail="The user with this email already exists in the system.",
) )
user = crud.user.create(db, obj_in=user_in) user = crud.user.create(db, obj_in=user_in)
details = {
'admin_id': current_user.id,
'user_id': user.id,
'email': user.email,
'fullname': user.fullname,
'company_id': user.company_id,
'department_id': user.department_id,
}
log_audit_user(db, current_user, 'create', details)
return JSONResponse( return JSONResponse(
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
content={"message": "User created successfully", "user": jsonable_encoder(user)}, content={"message": "User created successfully", "user": jsonable_encoder(user)},
@ -102,6 +134,14 @@ def update_username(
company_id=user.company_id, company_id=user.company_id,
department_id=user.department_id, 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,
}
log_audit_user(db, current_user, 'update_username', details)
return JSONResponse( return JSONResponse(
status_code=status.HTTP_200_OK, status_code=status.HTTP_200_OK,
content={"message": "Username updated successfully", content={"message": "Username updated successfully",
@ -158,6 +198,15 @@ def change_password(
company_id= current_user.company_id, company_id= current_user.company_id,
department_id=current_user.department_id, department_id=current_user.department_id,
) )
details = {
'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)
return JSONResponse( return JSONResponse(
status_code=status.HTTP_200_OK, status_code=status.HTTP_200_OK,
@ -201,6 +250,7 @@ def update_user(
Update a user. Update a user.
""" """
user = crud.user.get(db, id=user_id) user = crud.user.get(db, id=user_id)
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@ -214,6 +264,14 @@ def update_user(
company_id=user.company_id, company_id=user.company_id,
department_id=user.department_id, 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,
}
log_audit_user(db, current_user, 'update user', details)
return JSONResponse( return JSONResponse(
status_code=status.HTTP_200_OK, status_code=status.HTTP_200_OK,
content={"message": "User updated successfully", "user": jsonable_encoder(user_data)}, content={"message": "User updated successfully", "user": jsonable_encoder(user_data)},
@ -286,6 +344,18 @@ def delete_user(
""" """
user_id = delete_user.id user_id = delete_user.id
user = crud.user.get(db, id=user_id) user = crud.user.get(db, id=user_id)
details = {
'admin_id': current_user.id,
'deleted_user_id': user.id,
'email': user.email,
'fullname': user.fullname,
'company_id': user.company_id,
'department_id': user.department_id,
}
log_audit_user(db, current_user, 'delete', details)
if user is None: if user is None:
raise HTTPException(status_code=404, detail="User not found") raise HTTPException(status_code=404, detail="User not found")
crud.user.remove(db, id=user_id) crud.user.remove(db, id=user_id)
@ -340,6 +410,17 @@ def admin_update_user(
role = crud.user_role.update(db, db_obj=user_role, obj_in=role_in) role = crud.user_role.update(db, db_obj=user_role, obj_in=role_in)
user_in = schemas.UserAdmin(fullname=user_update.fullname, department_id=user_update.department_id) 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) crud.user.update(db, db_obj=existing_user, obj_in=user_in)
return JSONResponse( return JSONResponse(

View File

@ -8,7 +8,7 @@ SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{username}:{password}@{host}:{p
port='5432', port='5432',
db_name='QuickGpt', db_name='QuickGpt',
username='postgres', username='postgres',
password="admin", password="quick",
) )
class Settings(BaseSettings): class Settings(BaseSettings):

View File

@ -61,3 +61,4 @@ def verify_refresh_token(token: str) -> Optional[Dict[str, Any]]:
return payload return payload
except JWTError: except JWTError:
return None return None

View File

@ -5,3 +5,4 @@ from .company_crud import company
from .subscription_crud import subscription from .subscription_crud import subscription
from .document_crud import documents from .document_crud import documents
from .department_crud import department from .department_crud import department
from .audit_crud import audit

View File

@ -0,0 +1,14 @@
from typing import Optional
from private_gpt.users.crud.base import CRUDBase
from private_gpt.users.models.audit import Audit
from private_gpt.users.schemas.audit import AuditCreate, AuditUpdate
from sqlalchemy.orm import Session
class CRUDAudit(CRUDBase[Audit, AuditCreate, AuditUpdate]):
def get_by_id(self, db: Session, *, id: str) -> Optional[Audit]:
return db.query(self.model).filter(Audit.id == id).first()
audit = CRUDAudit(Audit)

View File

@ -4,4 +4,5 @@ from .user_role import UserRole
from .role import Role from .role import Role
from .document import Document from .document import Document
from .subscription import Subscription from .subscription import Subscription
from .department import Department from .department import Department
from .audit import Audit

View File

@ -0,0 +1,18 @@
from datetime import datetime
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String
from private_gpt.users.db.base_class import Base
from sqlalchemy.dialects.postgresql import JSONB
class Audit(Base):
__tablename__ = "audit"
id = Column(Integer, primary_key=True, index=True)
timestamp = Column(DateTime, nullable=False, default=datetime.utcnow)
user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
model = Column(String, nullable=False)
action = Column(String, nullable=False)
details = Column(JSONB, nullable=True)
def __repr__(self):
return f"<Audit(id={self.id}, timestamp={self.timestamp}, user_id={self.user_id}, model={self.model}, action={self.action}, details={self.details})>"

View File

@ -3,8 +3,8 @@ from sqlalchemy.orm import relationship, Session
from sqlalchemy import Column, Integer, String from sqlalchemy import Column, Integer, String
from private_gpt.users.db.base_class import Base from private_gpt.users.db.base_class import Base
from private_gpt.users.models.document import Document # from private_gpt.users.models.document import Document
from private_gpt.users.models.user import User # from private_gpt.users.models.user import User
class Department(Base): class Department(Base):
@ -25,21 +25,25 @@ class Department(Base):
total_documents = Column(Integer, default=0) total_documents = Column(Integer, default=0)
# @event.listens_for(Department, 'after_insert')
def update_total_users(mapper, connection, target): # @event.listens_for(Department, 'after_update')
session = Session(bind=connection) # def update_total_users(mapper, connection, target):
target.total_users = session.query(User).filter_by( # print("--------------------------------------------------------------Calling Event User------------------------------------------------------------------------")
department_id=target.id).count() # connection.execute(
# Department.__table__.update().
# where(Department.id == target.id).
# values(total_users=Session.object_session(target).query(
# User).filter_by(department_id=target.id).count())
# )
def update_total_documents(mapper, connection, target): # @event.listens_for(Department, 'after_insert')
session = Session(bind=connection) # @event.listens_for(Department, 'after_update')
target.total_documents = session.query( # def update_total_documents(mapper, connection, target):
Document).filter_by(department_id=target.id).count() # print("--------------------------------------------------------------Calling Event Department------------------------------------------------------------------------")
# connection.execute(
# Department.__table__.update().
# Attach event listeners to Department model # where(Department.id == target.id).
event.listen(Department, 'after_insert', update_total_users) # values(total_documents=Session.object_session(target).query(
event.listen(Department, 'after_update', update_total_users) # Document).filter_by(department_id=target.id).count())
event.listen(Department, 'after_insert', update_total_documents) # )
event.listen(Department, 'after_update', update_total_documents)

View File

@ -1,7 +1,10 @@
from private_gpt.users.db.base_class import Base from private_gpt.users.db.base_class import Base
from datetime import datetime from datetime import datetime
from sqlalchemy import event, select, func, update
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime from sqlalchemy import Column, Integer, String, ForeignKey, DateTime
from private_gpt.users.models.department import Department
class Document(Base): class Document(Base):
@ -25,6 +28,27 @@ class Document(Base):
department_id = Column(Integer, ForeignKey( department_id = Column(Integer, ForeignKey(
"departments.id"), nullable=False) "departments.id"), nullable=False)
uploaded_by_user = relationship(
"User", back_populates="uploaded_documents") department = relationship("Department", back_populates="documents")
department = relationship("Department", back_populates="documents")
@event.listens_for(Document, 'after_insert')
@event.listens_for(Document, 'after_delete')
def update_total_documents(mapper, connection, target):
department_id = target.department_id
print(f"Department ID is: {department_id}")
# Use SQLAlchemy's ORM constructs for better readability and maintainability:
total_documents = connection.execute(
select([func.count()]).select_from(Document).where(
Document.department_id == department_id)
).scalar()
print(f"Total documents is: {total_documents}")
print("Updating total documents")
# Use the correct update construct for SQLAlchemy:
connection.execute(
update(Department).values(total_documents=total_documents).where(
Department.id == department_id)
)

View File

@ -9,9 +9,10 @@ from sqlalchemy import (
DateTime, DateTime,
ForeignKey ForeignKey
) )
from sqlalchemy import event, func, select, update
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from private_gpt.users.db.base_class import Base from private_gpt.users.db.base_class import Base
from private_gpt.users.models.department import Department
class User(Base): class User(Base):
"""Models a user table""" """Models a user table"""
__tablename__ = "users" __tablename__ = "users"
@ -53,3 +54,21 @@ class User(Base):
__table_args__ = ( __table_args__ = (
UniqueConstraint('fullname', name='unique_username_no_spacing'), UniqueConstraint('fullname', name='unique_username_no_spacing'),
) )
@event.listens_for(User, 'after_insert')
@event.listens_for(User, 'after_delete')
def update_total_users(mapper, connection, target):
department_id = target.department_id
print(f"Department ID is: {department_id}")
total_users = connection.execute(
select([func.count()]).select_from(User).where(
User.department_id == department_id)
).scalar()
print(f"Total users is: {total_users}")
connection.execute(
update(Department).values(total_users=total_users).where(
Department.id == department_id)
)

View File

@ -5,4 +5,5 @@ from .user_role import UserRole, UserRoleCreate, UserRoleInDB, UserRoleUpdate
from .subscription import Subscription, SubscriptionBase, SubscriptionCreate, SubscriptionUpdate from .subscription import Subscription, SubscriptionBase, SubscriptionCreate, SubscriptionUpdate
from .company import Company, CompanyBase, CompanyCreate, CompanyUpdate from .company import Company, CompanyBase, CompanyCreate, CompanyUpdate
from .documents import Document, DocumentCreate, DocumentsBase, DocumentUpdate, DocumentList from .documents import Document, DocumentCreate, DocumentsBase, DocumentUpdate, DocumentList
from .department import Department, DepartmentCreate, DepartmentUpdate, DepartmentAdminCreate, DepartmentDelete from .department import Department, DepartmentCreate, DepartmentUpdate, DepartmentAdminCreate, DepartmentDelete
from .audit import AuditBase, AuditCreate, AuditUpdate, Audit

View File

@ -0,0 +1,30 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel
class AuditBase(BaseModel):
id: int
model: str
user_id: int
action: str
details: dict
timestamp: Optional[datetime]
class AuditCreate(AuditBase):
pass
class AuditUpdate(AuditBase):
id: int
class AuditInDB(AuditBase):
id: int
class Config:
orm_mode = True
class Audit(AuditBase):
pass

View File

@ -7,7 +7,6 @@ class Ldap:
self.server = ldap3.Server(server_uri, get_info=ldap3.ALL) self.server = ldap3.Server(server_uri, get_info=ldap3.ALL)
print(f"Connected to ldap server: {self.server}") print(f"Connected to ldap server: {self.server}")
self.conn = ldap3.Connection(self.server, user=ldap_user, password=ldap_pass, auto_bind=True) self.conn = ldap3.Connection(self.server, user=ldap_user, password=ldap_pass, auto_bind=True)
print(self.conn)
def who_am_i(self): def who_am_i(self):
return self.conn.extend.standard.who_am_i() return self.conn.extend.standard.who_am_i()

View File

@ -0,0 +1,24 @@
from datetime import datetime
from sqlalchemy.orm import Session
from private_gpt.users.models.audit import Audit
def log_audit_entry(
session: Session,
model: str,
action: str,
details: dict,
user_id: int = None,
):
audit_entry = Audit(
timestamp=datetime.utcnow(),
user_id=user_id,
model=model,
action=action,
details=details,
)
session.add(audit_entry)
session.commit()