update project
This commit is contained in:
parent
1f6250eea0
commit
f215f458bc
|
|
@ -86,7 +86,6 @@ path_separator = os
|
||||||
# database URL. This is consumed by the user-maintained env.py script only.
|
# database URL. This is consumed by the user-maintained env.py script only.
|
||||||
# other means of configuring database URLs may be customized within the env.py
|
# other means of configuring database URLs may be customized within the env.py
|
||||||
# file.
|
# file.
|
||||||
sqlalchemy.url = driver://user:pass@localhost/dbname
|
|
||||||
|
|
||||||
|
|
||||||
[post_write_hooks]
|
[post_write_hooks]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,17 @@
|
||||||
|
import asyncio
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
|
|
||||||
from sqlalchemy import engine_from_config
|
|
||||||
from sqlalchemy import pool
|
from sqlalchemy import pool
|
||||||
|
from sqlalchemy.ext.asyncio import async_engine_from_config, AsyncConnection
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
|
||||||
|
from core.config import settings
|
||||||
|
from core.config import settings
|
||||||
|
from db.base import Base
|
||||||
|
|
||||||
|
import domains.users.models
|
||||||
|
import domains.groups.models
|
||||||
|
|
||||||
from alembic import context
|
from alembic import context
|
||||||
|
|
||||||
|
|
@ -14,11 +24,7 @@ config = context.config
|
||||||
if config.config_file_name is not None:
|
if config.config_file_name is not None:
|
||||||
fileConfig(config.config_file_name)
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
# add your model's MetaData object here
|
target_metadata = Base.metadata
|
||||||
# for 'autogenerate' support
|
|
||||||
# from myapp import mymodel
|
|
||||||
# target_metadata = mymodel.Base.metadata
|
|
||||||
target_metadata = None
|
|
||||||
|
|
||||||
# other values from the config, defined by the needs of env.py,
|
# other values from the config, defined by the needs of env.py,
|
||||||
# can be acquired:
|
# can be acquired:
|
||||||
|
|
@ -38,7 +44,7 @@ def run_migrations_offline() -> None:
|
||||||
script output.
|
script output.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
url = config.get_main_option("sqlalchemy.url")
|
url = settings.DATABASE_URL
|
||||||
context.configure(
|
context.configure(
|
||||||
url=url,
|
url=url,
|
||||||
target_metadata=target_metadata,
|
target_metadata=target_metadata,
|
||||||
|
|
@ -50,26 +56,39 @@ def run_migrations_offline() -> None:
|
||||||
context.run_migrations()
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_online() -> None:
|
from sqlalchemy import Connection
|
||||||
"""Run migrations in 'online' mode.
|
|
||||||
|
|
||||||
In this scenario we need to create an Engine
|
def do_run_migrations(connection: Connection) -> None:
|
||||||
|
context.configure(connection=connection, target_metadata=target_metadata)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
async def run_async_migrations() -> None:
|
||||||
|
"""In this scenario we need to create an Engine
|
||||||
and associate a connection with the context.
|
and associate a connection with the context.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
connectable = engine_from_config(
|
|
||||||
config.get_section(config.config_ini_section, {}),
|
configuration = config.get_section(config.config_ini_section, {})
|
||||||
|
configuration["sqlalchemy.url"] = settings.DATABASE_URL
|
||||||
|
|
||||||
|
connectable = async_engine_from_config(
|
||||||
|
configuration,
|
||||||
prefix="sqlalchemy.",
|
prefix="sqlalchemy.",
|
||||||
poolclass=pool.NullPool,
|
poolclass=pool.NullPool,
|
||||||
)
|
)
|
||||||
|
|
||||||
with connectable.connect() as connection:
|
async with connectable.connect() as connection:
|
||||||
context.configure(
|
await connection.run_sync(do_run_migrations)
|
||||||
connection=connection, target_metadata=target_metadata
|
|
||||||
)
|
|
||||||
|
|
||||||
with context.begin_transaction():
|
await connectable.dispose()
|
||||||
context.run_migrations()
|
|
||||||
|
|
||||||
|
def run_migrations_online() -> None:
|
||||||
|
"""Run migrations in 'online' mode."""
|
||||||
|
asyncio.run(run_async_migrations())
|
||||||
|
|
||||||
|
|
||||||
if context.is_offline_mode():
|
if context.is_offline_mode():
|
||||||
|
|
|
||||||
77
Back/alembic/versions/4413cbcf58c3_create_db.py
Normal file
77
Back/alembic/versions/4413cbcf58c3_create_db.py
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
"""create db
|
||||||
|
|
||||||
|
Revision ID: 4413cbcf58c3
|
||||||
|
Revises:
|
||||||
|
Create Date: 2026-03-06 12:26:21.920942
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '4413cbcf58c3'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = None
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('groups',
|
||||||
|
sa.Column('name', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('type', sa.Enum('GROUP', 'DIRECT', name='group_type'), nullable=False),
|
||||||
|
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_groups_is_active'), 'groups', ['is_active'], unique=False)
|
||||||
|
op.create_index(op.f('ix_groups_name'), 'groups', ['name'], unique=False)
|
||||||
|
op.create_table('users',
|
||||||
|
sa.Column('username', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('secret_hash', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('role', sa.Enum('ADMIN', 'GROUP_MANAGER', 'MEMBER', name='user_role'), nullable=False),
|
||||||
|
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_users_is_active'), 'users', ['is_active'], unique=False)
|
||||||
|
op.create_index(op.f('ix_users_role'), 'users', ['role'], unique=False)
|
||||||
|
op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
|
||||||
|
op.create_table('group_members',
|
||||||
|
sa.Column('user_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('group_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('role', sa.Enum('MANAGER', 'MEMBER', name='group_role'), nullable=False),
|
||||||
|
sa.Column('id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_group_members_group_id'), 'group_members', ['group_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_group_members_user_id'), 'group_members', ['user_id'], unique=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index(op.f('ix_group_members_user_id'), table_name='group_members')
|
||||||
|
op.drop_index(op.f('ix_group_members_group_id'), table_name='group_members')
|
||||||
|
op.drop_table('group_members')
|
||||||
|
op.drop_index(op.f('ix_users_username'), table_name='users')
|
||||||
|
op.drop_index(op.f('ix_users_role'), table_name='users')
|
||||||
|
op.drop_index(op.f('ix_users_is_active'), table_name='users')
|
||||||
|
op.drop_table('users')
|
||||||
|
op.drop_index(op.f('ix_groups_name'), table_name='groups')
|
||||||
|
op.drop_index(op.f('ix_groups_is_active'), table_name='groups')
|
||||||
|
op.drop_table('groups')
|
||||||
|
# ### end Alembic commands ###
|
||||||
32
Back/alembic/versions/b1f09d977759_change_groups_models.py
Normal file
32
Back/alembic/versions/b1f09d977759_change_groups_models.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
"""change groups models
|
||||||
|
|
||||||
|
Revision ID: b1f09d977759
|
||||||
|
Revises: 4413cbcf58c3
|
||||||
|
Create Date: 2026-03-06 16:21:31.915163
|
||||||
|
|
||||||
|
"""
|
||||||
|
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 = 'b1f09d977759'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = '4413cbcf58c3'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('group_members', 'role')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('group_members', sa.Column('role', postgresql.ENUM('MANAGER', 'MEMBER', name='group_role'), autoincrement=False, nullable=False))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
@ -9,7 +9,7 @@ class Settings(BaseSettings):
|
||||||
SECRET_KEY: str
|
SECRET_KEY: str
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||||||
ALGORITHM: str = "HS256"
|
ALGORITHM: str = "HS256"
|
||||||
SECRET_PASS_LENGTH: int = 16
|
SECRET_PASS_LENGTH: int
|
||||||
|
|
||||||
DATABASE_URL: str
|
DATABASE_URL: str
|
||||||
REDIS_URL: str
|
REDIS_URL: str
|
||||||
|
|
@ -21,6 +21,7 @@ class Settings(BaseSettings):
|
||||||
class Config:
|
class Config:
|
||||||
env_file = ".env"
|
env_file = ".env"
|
||||||
case_sensitive = True
|
case_sensitive = True
|
||||||
|
extra = "ignore"
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,25 @@
|
||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from db.session import get_db
|
from db.session import get_db
|
||||||
from core.jwt import decode_token
|
from core.jwt import decode_token
|
||||||
from domains.users.repo import get_user_by_id
|
from domains.users.repo import get_user_by_id
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
|
|
||||||
|
# Bearer authentication scheme
|
||||||
|
security = HTTPBearer()
|
||||||
|
|
||||||
|
|
||||||
async def get_current_user(
|
async def get_current_user(
|
||||||
token: str = Depends(oauth2_scheme),
|
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Validate JWT token and return the authenticated user
|
||||||
|
"""
|
||||||
|
|
||||||
|
token = credentials.credentials
|
||||||
|
|
||||||
payload = decode_token(token)
|
payload = decode_token(token)
|
||||||
|
|
||||||
|
|
@ -24,6 +31,12 @@ async def get_current_user(
|
||||||
|
|
||||||
user_id = payload.get("sub")
|
user_id = payload.get("sub")
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Invalid token payload",
|
||||||
|
)
|
||||||
|
|
||||||
user = await get_user_by_id(db, user_id)
|
user = await get_user_by_id(db, user_id)
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
|
|
@ -32,10 +45,21 @@ async def get_current_user(
|
||||||
detail="User not found",
|
detail="User not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not user.is_active:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="User is inactive",
|
||||||
|
)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
async def get_current_admin(user=Depends(get_current_user)):
|
async def get_current_admin(
|
||||||
|
user=Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Ensure the authenticated user is an admin
|
||||||
|
"""
|
||||||
|
|
||||||
if user.role != "admin":
|
if user.role != "admin":
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ services:
|
||||||
container_name: neda_api
|
container_name: neda_api
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- "./:/app"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
@ -28,7 +30,7 @@ services:
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}" ]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
@ -41,7 +43,7 @@ services:
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
test: [ "CMD", "redis-cli", "ping" ]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,18 @@ from core.deps import get_current_admin
|
||||||
from domains.admin.schemas import (
|
from domains.admin.schemas import (
|
||||||
AdminCreateUser,
|
AdminCreateUser,
|
||||||
AdminCreateUserResult,
|
AdminCreateUserResult,
|
||||||
AdminResetSecretResult
|
AdminResetSecretResult,
|
||||||
|
AdminUserResponse
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from domains.groups.schemas import GroupResponse
|
||||||
|
from domains.groups.repo import get_all_groups
|
||||||
|
|
||||||
from domains.admin.service import (
|
from domains.admin.service import (
|
||||||
admin_create_user,
|
admin_create_user,
|
||||||
admin_reset_user_secret
|
admin_reset_user_secret
|
||||||
)
|
)
|
||||||
|
from domains.users.repo import get_all_users
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
|
|
@ -69,3 +74,18 @@ async def reset_secret(
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"secret": new_secret}
|
return {"secret": new_secret}
|
||||||
|
|
||||||
|
@router.get("/users", response_model=list[AdminUserResponse])
|
||||||
|
async def list_users(
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
admin=Depends(get_current_admin)
|
||||||
|
):
|
||||||
|
return await get_all_users(db)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/groups", response_model=list[GroupResponse])
|
||||||
|
async def list_groups(
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
admin=Depends(get_current_admin)
|
||||||
|
):
|
||||||
|
return await get_all_groups(db)
|
||||||
|
|
@ -14,7 +14,8 @@ from core.config import settings
|
||||||
|
|
||||||
|
|
||||||
def generate_user_secret():
|
def generate_user_secret():
|
||||||
return secrets.token_urlsafe(settings.SECRET_PASS_LENGTH)
|
# return secrets.token_urlsafe(settings.SECRET_PASS_LENGTH)
|
||||||
|
return "1234"
|
||||||
|
|
||||||
async def admin_create_user(
|
async def admin_create_user(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from db.session import get_db
|
from db.session import get_db
|
||||||
from core.deps import get_current_admin, get_current_user
|
from core.deps import get_current_admin
|
||||||
|
|
||||||
from domains.groups.schemas import (
|
from domains.groups.schemas import (
|
||||||
GroupCreate,
|
GroupCreate,
|
||||||
|
|
@ -13,7 +13,6 @@ from domains.groups.schemas import (
|
||||||
from domains.groups.service import (
|
from domains.groups.service import (
|
||||||
create_new_group,
|
create_new_group,
|
||||||
add_member_to_group,
|
add_member_to_group,
|
||||||
list_user_groups
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -30,13 +29,12 @@ router = APIRouter(
|
||||||
async def create_group(
|
async def create_group(
|
||||||
payload: GroupCreate,
|
payload: GroupCreate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
admin = Depends(get_current_admin)
|
admin=Depends(get_current_admin)
|
||||||
):
|
):
|
||||||
|
|
||||||
group = await create_new_group(
|
group = await create_new_group(
|
||||||
db,
|
db,
|
||||||
payload.name,
|
payload.name
|
||||||
payload.description
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return group
|
return group
|
||||||
|
|
@ -47,26 +45,19 @@ async def add_member(
|
||||||
group_id: str,
|
group_id: str,
|
||||||
payload: AddMemberRequest,
|
payload: AddMemberRequest,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
admin = Depends(get_current_admin)
|
admin=Depends(get_current_admin)
|
||||||
):
|
):
|
||||||
|
|
||||||
|
try:
|
||||||
membership = await add_member_to_group(
|
membership = await add_member_to_group(
|
||||||
db,
|
db,
|
||||||
group_id,
|
group_id,
|
||||||
payload.user_id,
|
payload.user_id,
|
||||||
payload.role
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=str(e)
|
||||||
)
|
)
|
||||||
|
|
||||||
return membership
|
return membership
|
||||||
|
|
||||||
|
|
||||||
@router.get("/me", response_model=list[GroupResponse])
|
|
||||||
async def my_groups(
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
|
|
||||||
return await list_user_groups(
|
|
||||||
db,
|
|
||||||
user.id
|
|
||||||
)
|
|
||||||
|
|
@ -10,9 +10,6 @@ class GroupType(str, Enum):
|
||||||
GROUP = "group"
|
GROUP = "group"
|
||||||
DIRECT = "direct"
|
DIRECT = "direct"
|
||||||
|
|
||||||
class GroupRole(str, Enum):
|
|
||||||
MANAGER = "manager"
|
|
||||||
MEMBER = "member"
|
|
||||||
|
|
||||||
|
|
||||||
class Group(Base):
|
class Group(Base):
|
||||||
|
|
@ -49,9 +46,3 @@ class GroupMember(Base):
|
||||||
ForeignKey("groups.id", ondelete="CASCADE"),
|
ForeignKey("groups.id", ondelete="CASCADE"),
|
||||||
index=True
|
index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
role: Mapped[GroupRole] = mapped_column(
|
|
||||||
SQLEnum(GroupRole, name="group_role"),
|
|
||||||
default=GroupRole.MEMBER,
|
|
||||||
nullable=False
|
|
||||||
)
|
|
||||||
|
|
@ -29,3 +29,8 @@ async def get_user_groups(db: AsyncSession, user_id):
|
||||||
.where(GroupMember.user_id == user_id)
|
.where(GroupMember.user_id == user_id)
|
||||||
)
|
)
|
||||||
return result.scalars().all()
|
return result.scalars().all()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_all_groups(db: AsyncSession):
|
||||||
|
result = await db.execute(select(Group))
|
||||||
|
return result.scalars().all()
|
||||||
|
|
@ -1,17 +1,14 @@
|
||||||
import uuid
|
import uuid
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from domains.groups.models import GroupRole
|
|
||||||
|
|
||||||
|
|
||||||
class GroupCreate(BaseModel):
|
class GroupCreate(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
description: str | None = None
|
|
||||||
|
|
||||||
class GroupResponse(BaseModel):
|
class GroupResponse(BaseModel):
|
||||||
id: uuid.UUID
|
id: uuid.UUID
|
||||||
name: str
|
name: str
|
||||||
description: str | None
|
|
||||||
is_active: bool
|
is_active: bool
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
|
@ -19,12 +16,10 @@ class GroupResponse(BaseModel):
|
||||||
|
|
||||||
class AddMemberRequest(BaseModel):
|
class AddMemberRequest(BaseModel):
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
role: GroupRole = GroupRole.MEMBER
|
|
||||||
|
|
||||||
class GroupMemberResponse(BaseModel):
|
class GroupMemberResponse(BaseModel):
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
group_id: uuid.UUID
|
group_id: uuid.UUID
|
||||||
role: GroupRole
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from domains.users.repo import get_user_by_id
|
||||||
from domains.groups.models import Group, GroupMember
|
from domains.groups.models import Group, GroupMember
|
||||||
from domains.groups.repo import (
|
from domains.groups.repo import (
|
||||||
create_group,
|
create_group,
|
||||||
|
|
@ -12,12 +13,10 @@ from domains.groups.repo import (
|
||||||
async def create_new_group(
|
async def create_new_group(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
name: str,
|
name: str,
|
||||||
description: str | None
|
|
||||||
):
|
):
|
||||||
|
|
||||||
group = Group(
|
group = Group(
|
||||||
name=name,
|
name=name,
|
||||||
description=description
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return await create_group(db, group)
|
return await create_group(db, group)
|
||||||
|
|
@ -26,13 +25,22 @@ async def add_member_to_group(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
group_id,
|
group_id,
|
||||||
user_id,
|
user_id,
|
||||||
role
|
|
||||||
):
|
):
|
||||||
|
# 1. Check if group exists
|
||||||
|
group = await get_group_by_id(db, group_id)
|
||||||
|
if not group:
|
||||||
|
raise ValueError("Group not found")
|
||||||
|
|
||||||
|
# 2. Check if user exists
|
||||||
|
user = await get_user_by_id(db, user_id)
|
||||||
|
if not user:
|
||||||
|
raise ValueError("User not found")
|
||||||
|
|
||||||
|
# TODO: Check if already a member to avoid duplicate constraint if any (optional)
|
||||||
|
|
||||||
membership = GroupMember(
|
membership = GroupMember(
|
||||||
group_id=group_id,
|
group_id=group_id,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
role=role
|
|
||||||
)
|
)
|
||||||
return await add_group_member(db, membership)
|
return await add_group_member(db, membership)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,12 @@
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from db.session import get_db
|
from db.session import get_db
|
||||||
from core.deps import get_current_admin
|
from core.deps import get_current_admin, get_current_user
|
||||||
|
|
||||||
from domains.users.schemas import (
|
from domains.groups.schemas import GroupResponse
|
||||||
UserCreate,
|
from domains.groups.service import list_user_groups
|
||||||
UserCreateResult
|
|
||||||
)
|
|
||||||
|
|
||||||
from domains.users.service import (
|
|
||||||
create_user_by_admin,
|
|
||||||
reset_user_secret
|
|
||||||
)
|
|
||||||
from domains.users.repo import get_user_by_id
|
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
prefix="/users",
|
prefix="/users",
|
||||||
|
|
@ -21,38 +14,9 @@ router = APIRouter(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=UserCreateResult)
|
@router.get("/me/groups", response_model=list[GroupResponse])
|
||||||
async def create_user(
|
async def my_groups(
|
||||||
payload: UserCreate,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
admin = Depends(get_current_admin)
|
user=Depends(get_current_user)
|
||||||
):
|
):
|
||||||
|
return await list_user_groups(db, user.id)
|
||||||
user, secret = await create_user_by_admin(
|
|
||||||
db,
|
|
||||||
payload.username,
|
|
||||||
payload.role
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"user": user,
|
|
||||||
"secret": secret
|
|
||||||
}
|
|
||||||
|
|
||||||
@router.post("/{user_id}/reset-secret")
|
|
||||||
async def reset_secret(
|
|
||||||
user_id: str,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
admin = Depends(get_current_admin)
|
|
||||||
):
|
|
||||||
|
|
||||||
user = await get_user_by_id(db, user_id)
|
|
||||||
if not user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="User not found"
|
|
||||||
)
|
|
||||||
|
|
||||||
new_secret = await reset_user_secret(db, user)
|
|
||||||
|
|
||||||
return {"secret": new_secret}
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from domains.users.models import User
|
from domains.users.models import User
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -10,16 +9,18 @@ async def get_user_by_id(db: AsyncSession, user_id):
|
||||||
)
|
)
|
||||||
return result.scalar_one_or_none()
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
async def get_user_by_username(db: AsyncSession, username):
|
async def get_user_by_username(db: AsyncSession, username):
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(User).where(User.username == username)
|
select(User).where(User.username == username)
|
||||||
)
|
)
|
||||||
return result.scalar_one_or_none()
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
async def create_user(db: AsyncSession, user: User):
|
async def create_user(db: AsyncSession, user: User):
|
||||||
db.add(user)
|
db.add(user)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(user)
|
await db.refresh(user)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
async def get_all_users(db: AsyncSession):
|
||||||
|
result = await db.execute(select(User))
|
||||||
|
return result.scalars().all()
|
||||||
|
|
@ -7,7 +7,8 @@ from core.config import settings
|
||||||
|
|
||||||
|
|
||||||
def generate_user_secret():
|
def generate_user_secret():
|
||||||
return secrets.token_urlsafe(settings.SECRET_PASS_LENGTH)
|
# return secrets.token_urlsafe(settings.SECRET_PASS_LENGTH)
|
||||||
|
return "1234"
|
||||||
|
|
||||||
async def create_user_by_admin(
|
async def create_user_by_admin(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ redis
|
||||||
|
|
||||||
python-jose[cryptography]
|
python-jose[cryptography]
|
||||||
passlib[bcrypt]
|
passlib[bcrypt]
|
||||||
|
bcrypt==4.0.1
|
||||||
|
|
||||||
pydantic-settings
|
pydantic-settings
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
|
|
||||||
36
Back/scripts/create_admin.py
Normal file
36
Back/scripts/create_admin.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import asyncio
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from db.session import AsyncSessionLocal
|
||||||
|
from core.security import hash_password
|
||||||
|
|
||||||
|
from domains.users.models import User, UserRole
|
||||||
|
|
||||||
|
|
||||||
|
async def create_admin():
|
||||||
|
|
||||||
|
username = input("Admin username: ")
|
||||||
|
|
||||||
|
secret = "1234"
|
||||||
|
|
||||||
|
async with AsyncSessionLocal() as db:
|
||||||
|
|
||||||
|
user = User(
|
||||||
|
username=username,
|
||||||
|
role=UserRole.ADMIN,
|
||||||
|
secret_hash=hash_password(secret),
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add(user)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
print("\nAdmin created successfully\n")
|
||||||
|
print("Username:", username)
|
||||||
|
print("Secret:", secret)
|
||||||
|
print("\nSave this secret!\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(create_admin())
|
||||||
Loading…
Reference in New Issue
Block a user