Skip to content
Open
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
60 changes: 60 additions & 0 deletions backend/app/alembic/versions/050_add_userproject_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Add userproject table

Revision ID: 050
Revises: 049
Create Date: 2026-04-01 12:17:42.165482

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "050"
down_revision = "049"
branch_labels = None
depends_on = None


def upgrade():
op.create_table(
"user_project",
sa.Column(
"user_id", sa.Integer(), nullable=False, comment="Reference to the user"
),
sa.Column(
"organization_id",
sa.Integer(),
nullable=False,
comment="Reference to the organization",
),
sa.Column(
"project_id",
sa.Integer(),
nullable=False,
comment="Reference to the project",
),
sa.Column(
"id",
sa.Integer(),
nullable=False,
comment="Unique identifier for the user-project mapping",
),
sa.Column(
"inserted_at",
sa.DateTime(),
nullable=False,
comment="Timestamp when the mapping was created",
),
sa.ForeignKeyConstraint(
["organization_id"], ["organization.id"], ondelete="CASCADE"
),
sa.ForeignKeyConstraint(["project_id"], ["project.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["user_id"], ["user.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("user_id", "project_id", name="uq_user_project"),
)


def downgrade():
op.drop_table("user_project")
46 changes: 42 additions & 4 deletions backend/app/api/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,22 @@
from pydantic import ValidationError
from sqlmodel import Session

from sqlmodel import and_, select

from app.core import security
from app.core.config import settings
from app.core.db import engine
from app.core.security import api_key_manager
from app.crud.organization import validate_organization
from app.crud.project import validate_project
from app.models import (
APIKey,
AuthContext,
Organization,
Project,
TokenPayload,
User,
UserProject,
)


Expand Down Expand Up @@ -64,10 +68,11 @@ def _authenticate_with_jwt(session: Session, token: str) -> AuthContext:
)

user = session.get(User, token_data.sub)
if not user:
raise HTTPException(status_code=404, detail="User not found")
if not user.is_active:
raise HTTPException(status_code=403, detail="Inactive user")
if not user or not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User access has been revoked",
)

organization: Organization | None = None
project: Project | None = None
Expand All @@ -77,6 +82,39 @@ def _authenticate_with_jwt(session: Session, token: str) -> AuthContext:
if token_data.project_id:
project = validate_project(session=session, project_id=token_data.project_id)

# Verify user still has access to this project
if project:
has_access = session.exec(
select(UserProject.id)
.where(
and_(
UserProject.user_id == user.id,
UserProject.project_id == project.id,
)
)
.limit(1)
).first()

if not has_access:
# Fallback: check APIKey table for backward compatibility
has_api_key = session.exec(
select(APIKey.id)
.where(
and_(
APIKey.user_id == user.id,
APIKey.project_id == project.id,
APIKey.is_deleted.is_(False),
)
)
.limit(1)
).first()

if not has_api_key:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User access to this project has been revoked",
)

return AuthContext(user=user, organization=organization, project=project)


Expand Down
17 changes: 17 additions & 0 deletions backend/app/api/docs/user_project/add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Add one or more users to a project by email. **Requires superuser access.**

**Request Body:**
- `organization_id` (required): The ID of the organization the project belongs to.
- `project_id` (required): The ID of the project to add users to.
- `users` (required): Array of user objects.
- `email` (required): User's email address.
- `full_name` (optional): User's full name.

**Examples:**
- **Single user**: `{"organization_id": 1, "project_id": 1, "users": [{"email": "user@gmail.com", "full_name": "User Name"}]}`
- **Multiple users**: `{"organization_id": 1, "project_id": 1, "users": [{"email": "a@gmail.com"}, {"email": "b@gmail.com"}]}`

**Behavior per email:**
- If the user does not exist, a new account is created with `is_active: false`. The user will be activated on their first Google login.
- If the user already exists and is already in this project, they are skipped.
- If the user exists but is not in this project, they are added.
9 changes: 9 additions & 0 deletions backend/app/api/docs/user_project/delete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Remove a user from a project. **Requires superuser access.**

**Path Parameters:**
- `user_id` (required): The ID of the user to remove.

**Query Parameters:**
- `project_id` (required): The ID of the project to remove the user from.

This only removes the user-project mapping — the user account itself is not deleted. You cannot remove yourself from a project.
6 changes: 6 additions & 0 deletions backend/app/api/docs/user_project/list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
List all users that belong to a project.

**Query Parameters:**
- `project_id` (required): The ID of the project to list users for.

Returns user details including their active status — users added via invitation will have `is_active: false` until they complete their first Google login.
6 changes: 4 additions & 2 deletions backend/app/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
config,
doc_transformation_job,
documents,
google_auth,
auth,
login,
languages,
llm,
Expand All @@ -18,6 +18,7 @@
responses,
private,
threads,
user_project,
users,
utils,
onboarding,
Expand All @@ -40,7 +41,7 @@
api_router.include_router(cron.router)
api_router.include_router(documents.router)
api_router.include_router(doc_transformation_job.router)
api_router.include_router(google_auth.router)
api_router.include_router(auth.router)
api_router.include_router(evaluations.router)
api_router.include_router(languages.router)
api_router.include_router(llm.router)
Expand All @@ -52,6 +53,7 @@
api_router.include_router(project.router)
api_router.include_router(responses.router)
api_router.include_router(threads.router)
api_router.include_router(user_project.router)
api_router.include_router(users.router)
api_router.include_router(utils.router)
api_router.include_router(fine_tuning.router)
Expand Down
Loading