feat: Implement user and role management with repositories
- Added RoleRepository and UserRepository for managing roles and users. - Implemented methods for creating, retrieving, and assigning roles to users. - Introduced functions to ensure default roles and an admin user exist in the system. - Updated UnitOfWork to include user and role repositories. - Created new security module for password hashing and JWT token management. - Added tests for authentication flows, including registration, login, and password reset. - Enhanced HTML templates for user registration, login, and password management with error handling. - Added a logo image to the static assets.
This commit is contained in:
210
alembic/versions/20251109_02_add_auth_tables.py
Normal file
210
alembic/versions/20251109_02_add_auth_tables.py
Normal file
@@ -0,0 +1,210 @@
|
||||
"""Add authentication and RBAC tables"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from passlib.context import CryptContext
|
||||
from sqlalchemy.sql import column, table
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "20251109_02"
|
||||
down_revision = "20251109_01"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
password_context = CryptContext(schemes=["argon2"], deprecated="auto")
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"users",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("email", sa.String(length=255), nullable=False),
|
||||
sa.Column("username", sa.String(length=128), nullable=False),
|
||||
sa.Column("password_hash", sa.String(length=255), nullable=False),
|
||||
sa.Column(
|
||||
"is_active",
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default=sa.true(),
|
||||
),
|
||||
sa.Column(
|
||||
"is_superuser",
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default=sa.false(),
|
||||
),
|
||||
sa.Column("last_login_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.func.now(),
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.func.now(),
|
||||
),
|
||||
sa.UniqueConstraint("email", name="uq_users_email"),
|
||||
sa.UniqueConstraint("username", name="uq_users_username"),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_users_active_superuser",
|
||||
"users",
|
||||
["is_active", "is_superuser"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"roles",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("name", sa.String(length=64), nullable=False),
|
||||
sa.Column("display_name", sa.String(length=128), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.func.now(),
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.func.now(),
|
||||
),
|
||||
sa.UniqueConstraint("name", name="uq_roles_name"),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"user_roles",
|
||||
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||
sa.Column("role_id", sa.Integer(), nullable=False),
|
||||
sa.Column(
|
||||
"granted_at",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.func.now(),
|
||||
),
|
||||
sa.Column("granted_by", sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["users.id"],
|
||||
ondelete="CASCADE",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["role_id"],
|
||||
["roles.id"],
|
||||
ondelete="CASCADE",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["granted_by"],
|
||||
["users.id"],
|
||||
ondelete="SET NULL",
|
||||
),
|
||||
sa.PrimaryKeyConstraint("user_id", "role_id"),
|
||||
sa.UniqueConstraint("user_id", "role_id",
|
||||
name="uq_user_roles_user_role"),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_user_roles_role_id",
|
||||
"user_roles",
|
||||
["role_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# Seed default roles
|
||||
roles_table = table(
|
||||
"roles",
|
||||
column("id", sa.Integer()),
|
||||
column("name", sa.String()),
|
||||
column("display_name", sa.String()),
|
||||
column("description", sa.Text()),
|
||||
)
|
||||
|
||||
op.bulk_insert(
|
||||
roles_table,
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "admin",
|
||||
"display_name": "Administrator",
|
||||
"description": "Full platform access with user management rights.",
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "project_manager",
|
||||
"display_name": "Project Manager",
|
||||
"description": "Manage projects, scenarios, and associated data.",
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "analyst",
|
||||
"display_name": "Analyst",
|
||||
"description": "Review dashboards and scenario outputs.",
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "viewer",
|
||||
"display_name": "Viewer",
|
||||
"description": "Read-only access to assigned projects and reports.",
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
admin_password_hash = password_context.hash("ChangeMe123!")
|
||||
|
||||
users_table = table(
|
||||
"users",
|
||||
column("id", sa.Integer()),
|
||||
column("email", sa.String()),
|
||||
column("username", sa.String()),
|
||||
column("password_hash", sa.String()),
|
||||
column("is_active", sa.Boolean()),
|
||||
column("is_superuser", sa.Boolean()),
|
||||
)
|
||||
|
||||
op.bulk_insert(
|
||||
users_table,
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"email": "admin@calminer.local",
|
||||
"username": "admin",
|
||||
"password_hash": admin_password_hash,
|
||||
"is_active": True,
|
||||
"is_superuser": True,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
user_roles_table = table(
|
||||
"user_roles",
|
||||
column("user_id", sa.Integer()),
|
||||
column("role_id", sa.Integer()),
|
||||
column("granted_by", sa.Integer()),
|
||||
)
|
||||
|
||||
op.bulk_insert(
|
||||
user_roles_table,
|
||||
[
|
||||
{
|
||||
"user_id": 1,
|
||||
"role_id": 1,
|
||||
"granted_by": 1,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_user_roles_role_id", table_name="user_roles")
|
||||
op.drop_table("user_roles")
|
||||
|
||||
op.drop_table("roles")
|
||||
|
||||
op.drop_index("ix_users_active_superuser", table_name="users")
|
||||
op.drop_table("users")
|
||||
Reference in New Issue
Block a user