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
66 changes: 61 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,10 +284,10 @@ wegent/
│ └── init_data/ # YAML initialization data
├── frontend/ # Next.js frontend
│ └── src/
│ ├── app/ # Pages: /, /login, /settings, /chat, /code, /tasks, /shared/task
│ ├── apis/ # API clients (client.ts + module-specific)
│ ├── app/ # Pages: /, /login, /settings, /chat, /code, /tasks, /shared/task, /admin
│ ├── apis/ # API clients (client.ts + module-specific, admin.ts)
│ ├── components/ # UI components (ui/ for shadcn, common/)
│ ├── features/ # Feature modules (common, layout, login, settings, tasks, theme, onboarding)
│ ├── features/ # Feature modules (common, layout, login, settings, tasks, theme, onboarding, admin)
│ ├── hooks/ # Custom hooks (useChatStream, useTranslation, useAttachment, useStreamingRecovery)
│ ├── i18n/ # Internationalization (en, zh-CN)
│ └── types/ # TypeScript types
Expand Down Expand Up @@ -521,7 +521,26 @@ spec:
| `/api/dify` | Dify app info, parameters |
| `/api/v1/namespaces/{ns}/{kinds}` | Kubernetes-style Kind API |
| `/api/v1/kinds/skills` | Skill upload/management |
| `/api/admin` | Admin operations |
| `/api/admin` | Admin operations (user management, public models, system stats) |

### Admin API Endpoints (`/api/admin`)

| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/users` | GET | List all users with pagination |
| `/users` | POST | Create new user |
| `/users/{user_id}` | GET | Get user details |
| `/users/{user_id}` | PUT | Update user info |
| `/users/{user_id}` | DELETE | Soft delete user (deactivate) |
| `/users/{user_id}/reset-password` | POST | Reset user password |
| `/users/{user_id}/toggle-status` | POST | Toggle user active status |
| `/users/{user_id}/role` | PUT | Update user role |
| `/public-models` | GET | List public models |
| `/public-models` | POST | Create public model |
| `/public-models/{model_id}` | GET | Get public model details |
| `/public-models/{model_id}` | PUT | Update public model |
| `/public-models/{model_id}` | DELETE | Delete public model |
| `/stats` | GET | Get system statistics |

### Executor Manager Routes

Expand All @@ -534,13 +553,50 @@ spec:

---

## 👥 User Role System

### Role Types

| Role | Description | Permissions |
|------|-------------|-------------|
| `admin` | System administrator | Full access to admin panel, user management, public model management |
| `user` | Regular user | Standard access to tasks, teams, bots, models, shells |

### Role-Based Access Control

- **Admin Panel** (`/admin`): Only accessible to users with `role='admin'`
- **User Menu**: Admin users see additional "Admin" menu item
- **API Protection**: Admin endpoints require `get_admin_user` dependency

### User Model Fields

| Field | Type | Description |
|-------|------|-------------|
| `id` | int | Primary key |
| `user_name` | string | Unique username |
| `email` | string | Optional email |
| `role` | enum | 'admin' or 'user' (default: 'user') |
| `auth_source` | enum | 'password', 'oidc', or 'unknown' |
| `is_active` | bool | Account status |
| `created_at` | datetime | Creation timestamp |
| `updated_at` | datetime | Last update timestamp |

### Database Migration

The `role` column was added via migration `b2c3d4e5f6a7_add_role_to_users.py`:
- Default value: 'user'
- Users with `user_name='admin'` are automatically set to `role='admin'`

---

## 🔒 Security

- Never commit credentials - use `.env` files
- Frontend: Only use `NEXT_PUBLIC_*` for client-safe values
- Backend encrypts Git tokens and API keys (AES-256-CBC)
- Change default passwords in production
- OIDC support for enterprise SSO
- Role-based access control for admin operations

---

Expand Down Expand Up @@ -601,5 +657,5 @@ cd backend && alembic revision --autogenerate -m "msg" && alembic upgrade head
---

**Last Updated**: 2025-12
**Wegent Version**: 1.0.19
**Wegent Version**: 1.0.20
**Maintained by**: WeCode-AI Team
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@
Create Date: 2025-12-08 10:49:03.869486+08:00

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = '00162199d565'
down_revision: Union[str, Sequence[str], None] = ('2b3c4d5e6f7g', 'add_storage_backend_columns')
revision: str = "00162199d565"
down_revision: Union[str, Sequence[str], None] = (
"2b3c4d5e6f7g",
"add_storage_backend_columns",
)
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None

Expand Down
29 changes: 14 additions & 15 deletions backend/alembic/versions/add_storage_backend_columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = 'add_storage_backend_columns'
down_revision: Union[str, None] = 'add_subtask_attachments'
revision: str = "add_storage_backend_columns"
down_revision: Union[str, None] = "add_subtask_attachments"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None

Expand All @@ -29,23 +29,22 @@ def upgrade() -> None:
"""Add storage backend columns to subtask_attachments table."""
# Add storage_key column for external storage reference
op.add_column(
'subtask_attachments',
sa.Column('storage_key', sa.String(500), nullable=True)
"subtask_attachments", sa.Column("storage_key", sa.String(500), nullable=True)
)

# Add storage_backend column to track which backend stores the data
op.add_column(
'subtask_attachments',
sa.Column('storage_backend', sa.String(50), nullable=True)
"subtask_attachments",
sa.Column("storage_backend", sa.String(50), nullable=True),
)

# Make binary_data nullable to support external storage backends
# When using external storage, binary_data can be NULL
op.alter_column(
'subtask_attachments',
'binary_data',
"subtask_attachments",
"binary_data",
existing_type=sa.LargeBinary(),
nullable=True
nullable=True,
)

# Update existing records to have 'mysql' as storage_backend
Expand All @@ -62,12 +61,12 @@ def downgrade() -> None:

# Make binary_data non-nullable again
op.alter_column(
'subtask_attachments',
'binary_data',
"subtask_attachments",
"binary_data",
existing_type=sa.LargeBinary(),
nullable=False
nullable=False,
)

# Drop the storage columns
op.drop_column('subtask_attachments', 'storage_backend')
op.drop_column('subtask_attachments', 'storage_key')
op.drop_column("subtask_attachments", "storage_backend")
op.drop_column("subtask_attachments", "storage_key")
62 changes: 62 additions & 0 deletions backend/alembic/versions/b2c3d4e5f6a7_add_role_to_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# SPDX-FileCopyrightText: 2025 Weibo, Inc.
#
# SPDX-License-Identifier: Apache-2.0

"""Add role column to users table

Revision ID: b2c3d4e5f6a7
Revises: 2b3c4d5e6f7g
Create Date: 2025-07-22 10:00:00.000000+08:00

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'b2c3d4e5f6a7'
down_revision: Union[str, Sequence[str], None] = '2b3c4d5e6f7g'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Add role column to users table.

Values: 'admin', 'user'
Default 'user' for existing users.
Users with user_name='admin' will be set to role='admin'.
"""
# Check if column already exists before adding
op.execute("""
SET @column_exists = (
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'users'
AND COLUMN_NAME = 'role'
);
""")

op.execute("""
SET @query = IF(@column_exists = 0,
'ALTER TABLE users ADD COLUMN role VARCHAR(20) NOT NULL DEFAULT ''user'' AFTER is_active',
'SELECT 1'
);
""")

op.execute("PREPARE stmt FROM @query;")
op.execute("EXECUTE stmt;")
op.execute("DEALLOCATE PREPARE stmt;")
Comment on lines +33 to +52
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Migration uses MySQL-specific syntax, breaking portability.

The migration relies on MySQL session variables (SET @variable), PREPARE/EXECUTE/DEALLOCATE, and INFORMATION_SCHEMA queries. The backend model's __table_args__ suggests SQLite support (sqlite_autoincrement), which means this migration will fail on SQLite.

Consider using Alembic's dialect-aware operations or the op.get_bind().dialect.name check:

def upgrade() -> None:
    """Add role column to users table."""
    conn = op.get_bind()
    inspector = sa.inspect(conn)
    columns = [col['name'] for col in inspector.get_columns('users')]
    
    if 'role' not in columns:
        op.add_column('users', sa.Column(
            'role', 
            sa.String(20), 
            nullable=False, 
            server_default='user'
        ))
    
    # Set admin role for existing admin users
    op.execute("UPDATE users SET role = 'admin' WHERE user_name = 'admin'")


# Set admin role for users with user_name='admin'
op.execute("""
UPDATE users SET role = 'admin' WHERE user_name = 'admin';
""")
Comment on lines +54 to +57
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hardcoded username for admin role assignment.

Setting role='admin' for user_name='admin' assumes a specific naming convention. If the initial admin was created with a different username, they won't receive admin privileges after migration.

Consider documenting this assumption or providing a separate mechanism (e.g., environment variable or config) to specify the initial admin username.

🤖 Prompt for AI Agents
In backend/alembic/versions/b2c3d4e5f6a7_add_role_to_users.py around lines
54–57, the migration hardcodes user_name='admin' when granting the admin role;
change it to read an initial-admin username from configuration (e.g., an
environment variable like INITIAL_ADMIN_USERNAME with a sensible default of
'admin') and use that variable in a parameterized op.execute call so the
migration grants role to the configured user rather than a fixed string; also
add a short comment documenting the assumption and, if desired, a fallback log
or warning when the configured username is not present so operators know to
verify admin assignment after migration.



def downgrade() -> None:
"""Remove role column from users table."""
op.drop_column('users', 'role')
Loading
Loading