feat: add 13 changes

This commit is contained in:
roai_linux 2026-03-07 19:09:49 +03:30
parent bedecb82bd
commit a99d920f2c
22 changed files with 573 additions and 102 deletions

View File

@ -51,6 +51,14 @@ async def get_current_user(
detail="User is inactive", detail="User is inactive",
) )
# Check token version for remote logout
token_version = payload.get("token_version")
if token_version != user.token_version:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has been invalidated",
)
return user return user
@ -61,7 +69,7 @@ async def get_current_admin(
Ensure the authenticated user is an admin Ensure the authenticated user is an admin
""" """
if user.role != "admin": if not user.is_admin:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="Admin privileges required", detail="Admin privileges required",

View File

@ -6,6 +6,7 @@ from core.config import settings
def create_access_token( def create_access_token(
subject: str, subject: str,
token_version: int,
expires_delta: timedelta | None = None, expires_delta: timedelta | None = None,
) -> str: ) -> str:
@ -18,6 +19,7 @@ def create_access_token(
payload = { payload = {
"sub": subject, "sub": subject,
"token_version": token_version,
"exp": expire, "exp": expire,
} }

View File

@ -38,7 +38,8 @@ async def create_user(
user, secret = await admin_create_user( user, secret = await admin_create_user(
db, db,
payload.username, payload.username,
payload.role payload.phone_number,
payload.is_admin
) )
except ValueError as e: except ValueError as e:
@ -54,6 +55,22 @@ async def create_user(
} }
@router.post("/users/{user_id}/logout")
async def logout_user(
user_id: str,
db: AsyncSession = Depends(get_db),
admin=Depends(get_current_admin)
):
from domains.admin.service import admin_logout_user
user = await admin_logout_user(db, user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return {"message": "User logged out successfully"}
@router.post("/users/{user_id}/reset-secret", @router.post("/users/{user_id}/reset-secret",
response_model=AdminResetSecretResult) response_model=AdminResetSecretResult)
async def reset_secret( async def reset_secret(

View File

@ -1,15 +1,16 @@
import uuid import uuid
from pydantic import BaseModel from pydantic import BaseModel
from domains.users.models import UserRole
class AdminCreateUser(BaseModel): class AdminCreateUser(BaseModel):
username: str username: str
role: UserRole phone_number: str | None = None
is_admin: bool = False
class AdminUserResponse(BaseModel): class AdminUserResponse(BaseModel):
id: uuid.UUID id: uuid.UUID
username: str username: str
role: UserRole phone_number: str | None
is_admin: bool
is_active: bool is_active: bool
class Config: class Config:

View File

@ -20,7 +20,8 @@ def generate_user_secret():
async def admin_create_user( async def admin_create_user(
db: AsyncSession, db: AsyncSession,
username: str, username: str,
role: str phone_number: str | None = None,
is_admin: bool = False
): ):
existing = await get_user_by_username(db, username) existing = await get_user_by_username(db, username)
@ -32,7 +33,8 @@ async def admin_create_user(
user = User( user = User(
username=username, username=username,
role=role, phone_number=phone_number,
is_admin=is_admin,
secret_hash=hash_password(secret) secret_hash=hash_password(secret)
) )
@ -41,6 +43,19 @@ async def admin_create_user(
return user, secret return user, secret
async def admin_logout_user(
db: AsyncSession,
user_id: str
):
user = await get_user_by_id(db, user_id)
if not user:
return None
user.token_version += 1
await db.commit()
return user
async def admin_reset_user_secret( async def admin_reset_user_secret(
db: AsyncSession, db: AsyncSession,
user_id user_id

View File

@ -15,7 +15,7 @@ class TokenResponse(BaseModel):
class AuthUser(BaseModel): class AuthUser(BaseModel):
id: uuid.UUID id: uuid.UUID
username: str username: str
role: str is_admin: bool
class Config: class Config:
from_attributes = True from_attributes = True

View File

@ -44,7 +44,8 @@ async def login_user(
return None return None
token = create_access_token( token = create_access_token(
subject=str(user.id) subject=str(user.id),
token_version=user.token_version
) )
return { return {

View File

@ -2,17 +2,22 @@ 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 from core.deps import get_current_admin, get_current_user
from domains.groups.schemas import ( from domains.groups.schemas import (
GroupCreate, GroupCreate,
GroupResponse, GroupResponse,
AddMemberRequest AddMemberRequest,
GroupMemberResponse
) )
from domains.groups.service import ( from domains.groups.service import (
create_new_group, create_new_group,
add_member_to_group, list_user_groups,
list_all_groups_admin,
list_group_members_api,
invite_member_to_group,
remove_member_from_group
) )
@ -29,35 +34,100 @@ 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) user=Depends(get_current_user)
): ):
"""
Admin creates Public groups, Regular users create Private groups.
"""
group = await create_new_group( group = await create_new_group(
db, db,
payload.name payload.name,
user.id,
user.is_admin
) )
return group return group
@router.post("/{group_id}/members") @router.get("/my", response_model=list[GroupResponse])
async def add_member( async def my_groups(
group_id: str, db: AsyncSession = Depends(get_db),
payload: AddMemberRequest, user=Depends(get_current_user)
):
"""
List groups the user is a member of.
"""
return await list_user_groups(db, user.id)
@router.get("/admin/all", response_model=list[GroupResponse])
async def list_all_groups(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
admin=Depends(get_current_admin) admin=Depends(get_current_admin)
): ):
"""
Admin can see all groups.
"""
return await list_all_groups_admin(db)
@router.get("/{group_id}/members", response_model=list[GroupMemberResponse])
async def group_members(
group_id: str,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user)
):
"""
List group members with username and online status.
"""
return await list_group_members_api(db, group_id)
@router.post("/{group_id}/invite")
async def invite_member(
group_id: str,
payload: AddMemberRequest,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user)
):
"""
Invite a user by username. Sends a notification.
"""
try: try:
membership = await add_member_to_group( notification = await invite_member_to_group(
db, db,
group_id, group_id,
payload.user_id, user.id,
payload.username
) )
return {"message": "Invitation sent", "notification_id": notification.id}
except ValueError as e: except ValueError as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e) detail=str(e)
) )
return membership
@router.delete("/{group_id}/members/{user_id}")
async def remove_member(
group_id: str,
user_id: str,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user)
):
"""
Admin or Group Manager can remove a member.
"""
try:
await remove_member_from_group(
db,
group_id,
user_id,
user
)
return {"message": "Member removed successfully"}
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=str(e)
)

View File

@ -3,14 +3,20 @@ from enum import Enum
from sqlalchemy import String, Boolean, ForeignKey, Enum as SQLEnum from sqlalchemy import String, Boolean, ForeignKey, Enum as SQLEnum
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from domains.users.models import User
from db.base import Base from db.base import Base
class GroupType(str, Enum): class GroupType(str, Enum):
GROUP = "group" PUBLIC = "public"
DIRECT = "direct" PRIVATE = "private"
class GroupMemberRole(str, Enum):
MANAGER = "manager"
MEMBER = "member"
class Group(Base): class Group(Base):
__tablename__ = "groups" # type: ignore __tablename__ = "groups" # type: ignore
@ -23,7 +29,7 @@ class Group(Base):
type: Mapped[GroupType] = mapped_column( type: Mapped[GroupType] = mapped_column(
SQLEnum(GroupType, name="group_type"), SQLEnum(GroupType, name="group_type"),
default=GroupType.GROUP, default=GroupType.PUBLIC,
nullable=False nullable=False
) )
@ -33,6 +39,9 @@ class Group(Base):
index=True index=True
) )
# Relationship to members
members: Mapped[list["GroupMember"]] = relationship(back_populates="group", cascade="all, delete-orphan")
class GroupMember(Base): class GroupMember(Base):
__tablename__ = "group_members" # type: ignore __tablename__ = "group_members" # type: ignore
@ -45,4 +54,14 @@ class GroupMember(Base):
group_id: Mapped[uuid.UUID] = mapped_column( group_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("groups.id", ondelete="CASCADE"), ForeignKey("groups.id", ondelete="CASCADE"),
index=True index=True
) )
role: Mapped[GroupMemberRole] = mapped_column(
SQLEnum(GroupMemberRole, name="group_member_role"),
default=GroupMemberRole.MEMBER,
nullable=False
)
# Relationships
group: Mapped["Group"] = relationship(back_populates="members")
user: Mapped["User"] = relationship()

View File

@ -1,4 +1,5 @@
from sqlalchemy import select from sqlalchemy import select
import uuid
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from domains.groups.models import Group, GroupMember from domains.groups.models import Group, GroupMember
@ -31,6 +32,42 @@ async def get_user_groups(db: AsyncSession, user_id):
return result.scalars().all() return result.scalars().all()
async def get_groups_by_type(db: AsyncSession, group_type):
result = await db.execute(
select(Group).where(Group.type == group_type)
)
return result.scalars().all()
async def get_all_groups(db: AsyncSession): async def get_all_groups(db: AsyncSession):
result = await db.execute(select(Group)) result = await db.execute(select(Group))
return result.scalars().all() return result.scalars().all()
async def get_group_members_with_details(db: AsyncSession, group_id: uuid.UUID):
from domains.users.models import User
result = await db.execute(
select(GroupMember, User.username)
.join(User, GroupMember.user_id == User.id)
.where(GroupMember.group_id == group_id)
)
return result.all()
async def get_group_member(db: AsyncSession, group_id: uuid.UUID, user_id: uuid.UUID):
result = await db.execute(
select(GroupMember)
.where(GroupMember.group_id == group_id)
.where(GroupMember.user_id == user_id)
)
return result.scalar_one_or_none()
async def delete_group_member(db: AsyncSession, group_id: uuid.UUID, user_id: uuid.UUID):
from sqlalchemy import delete
await db.execute(
delete(GroupMember)
.where(GroupMember.group_id == group_id)
.where(GroupMember.user_id == user_id)
)
await db.commit()

View File

@ -1,7 +1,7 @@
import uuid import uuid
from pydantic import BaseModel from pydantic import BaseModel
from domains.groups.models import GroupType, GroupMemberRole
class GroupCreate(BaseModel): class GroupCreate(BaseModel):
name: str name: str
@ -9,17 +9,20 @@ class GroupCreate(BaseModel):
class GroupResponse(BaseModel): class GroupResponse(BaseModel):
id: uuid.UUID id: uuid.UUID
name: str name: str
type: GroupType
is_active: bool is_active: bool
class Config: class Config:
from_attributes = True from_attributes = True
class AddMemberRequest(BaseModel): class AddMemberRequest(BaseModel):
user_id: uuid.UUID username: str # Req 12 says user enters username
class GroupMemberResponse(BaseModel): class GroupMemberResponse(BaseModel):
user_id: uuid.UUID user_id: uuid.UUID
group_id: uuid.UUID username: str
role: GroupMemberRole
is_online: bool = False
class Config: class Config:
from_attributes = True from_attributes = True

View File

@ -1,46 +1,86 @@
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
import uuid
from domains.users.repo import get_user_by_id from domains.users.repo import get_user_by_id
from domains.groups.models import Group, GroupMember from domains.groups.models import Group, GroupMember, GroupType, GroupMemberRole
from domains.groups.repo import ( from domains.groups.repo import (
create_group, create_group,
get_group_by_id, get_group_by_id,
add_group_member, add_group_member,
get_user_groups get_user_groups,
get_group_members_with_details,
delete_group_member,
get_all_groups as repo_get_all_groups
) )
from domains.realtime.presence_service import list_online_users
async def create_new_group( async def create_new_group(
db: AsyncSession, db: AsyncSession,
name: str, name: str,
creator_id,
is_admin: bool
): ):
group_type = GroupType.PUBLIC if is_admin else GroupType.PRIVATE
group = Group( group = Group(
name=name, name=name,
type=group_type
) )
return await create_group(db, group) await create_group(db, group)
async def add_member_to_group( # Creator becomes Manager
membership = GroupMember(
group_id=group.id,
user_id=creator_id,
role=GroupMemberRole.MANAGER
)
await add_group_member(db, membership)
return group
async def invite_member_to_group(
db: AsyncSession, db: AsyncSession,
group_id, group_id,
user_id, sender_id,
target_username: str
): ):
from domains.users.repo import get_user_by_username
from domains.notifications.service import send_join_request
# 1. Check if group exists # 1. Check if group exists
group = await get_group_by_id(db, group_id) group = await get_group_by_id(db, group_id)
if not group: if not group:
raise ValueError("Group not found") raise ValueError("Group not found")
# 2. Check if user exists # 2. Check if target user exists
user = await get_user_by_id(db, user_id) target_user = await get_user_by_username(db, target_username)
if not user: if not target_user:
raise ValueError("User not found") raise ValueError("User not found")
# TODO: Check if already a member to avoid duplicate constraint if any (optional) # 3. Send notification (Req 12)
return await send_join_request(
db,
sender_id=str(sender_id),
receiver_id=str(target_user.id),
group_id=str(group.id),
title="Group Invitation",
description=f"You have been invited to join group {group.name}"
)
async def add_member_to_group(
db: AsyncSession,
group_id,
user_id,
role: GroupMemberRole = GroupMemberRole.MEMBER
):
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)
@ -49,4 +89,46 @@ async def list_user_groups(
db: AsyncSession, db: AsyncSession,
user_id user_id
): ):
return await get_user_groups(db, user_id) return await get_user_groups(db, user_id)
async def list_all_groups_admin(db: AsyncSession):
return await repo_get_all_groups(db)
async def list_group_members_api(db: AsyncSession, group_id: str):
group_id_uuid = uuid.UUID(group_id)
members_data = await get_group_members_with_details(db, group_id_uuid)
online_users = await list_online_users(str(group_id))
result = []
for member_info in members_data:
member, username = member_info
result.append({
"user_id": member.user_id,
"username": username,
"role": member.role,
"is_online": str(member.user_id) in online_users
})
return result
async def remove_member_from_group(
db: AsyncSession,
group_id,
target_user_id,
requesting_user
):
group_id_uuid = group_id if isinstance(group_id, uuid.UUID) else uuid.UUID(group_id)
target_user_id_uuid = target_user_id if isinstance(target_user_id, uuid.UUID) else uuid.UUID(target_user_id)
# Req 13: Admin or Group Manager can remove
if not requesting_user.is_admin:
# Check if requesting user is manager of this group
from domains.groups.repo import get_group_member
membership = await get_group_member(db, group_id_uuid, requesting_user.id)
if not membership or membership.role != GroupMemberRole.MANAGER:
raise ValueError("Permission denied. Only admin or group manager can remove members.")
# For now, let's assume we implement the check here or in repo.
await delete_group_member(db, group_id_uuid, target_user_id_uuid)

View File

View File

@ -0,0 +1,64 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from db.session import get_db
from core.deps import get_current_user, get_current_admin
from domains.notifications.schemas import (
NotificationResponse,
NotificationAction,
NotificationCreate
)
from domains.notifications.service import (
list_my_notifications,
respond_to_notification,
send_public_notification
)
from domains.users.repo import get_all_users
router = APIRouter(
prefix="/notifications",
tags=["notifications"]
)
@router.get("/", response_model=list[NotificationResponse])
async def my_notifications(
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user)
):
return await list_my_notifications(db, user.id)
@router.post("/{notification_id}/respond", response_model=NotificationResponse)
async def respond_notification(
notification_id: str,
payload: NotificationAction,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user)
):
try:
return await respond_to_notification(
db,
notification_id,
user.id,
payload.is_accepted
)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
@router.post("/public")
async def broadcast_notification(
title: str,
description: str,
db: AsyncSession = Depends(get_db),
admin=Depends(get_current_admin)
):
users = await get_all_users(db)
user_ids = [str(u.id) for u in users]
return await send_public_notification(
db,
title,
description,
admin.id,
user_ids
)

View File

@ -0,0 +1,42 @@
import uuid
from enum import Enum
from sqlalchemy import String, Boolean, ForeignKey, Enum as SQLEnum, Text
from sqlalchemy.orm import Mapped, mapped_column
from db.base import Base
class NotificationType(str, Enum):
PUBLIC = "public"
JOIN_REQUEST = "join_request"
class Notification(Base):
__tablename__ = "notifications" # type: ignore
title: Mapped[str] = mapped_column(String(200), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
type: Mapped[NotificationType] = mapped_column(
SQLEnum(NotificationType, name="notification_type"),
nullable=False
)
is_accepted: Mapped[bool | None] = mapped_column(
Boolean,
nullable=True,
default=None
)
receiver_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("users.id", ondelete="CASCADE"),
index=True,
nullable=False
)
sender_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey("users.id", ondelete="SET NULL"),
nullable=True
)
group_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey("groups.id", ondelete="CASCADE"),
nullable=True
)

View File

@ -0,0 +1,28 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from domains.notifications.models import Notification
async def create_notification(db: AsyncSession, notification: Notification):
db.add(notification)
await db.commit()
await db.refresh(notification)
return notification
async def get_notification_by_id(db: AsyncSession, notification_id):
result = await db.execute(
select(Notification).where(Notification.id == notification_id)
)
return result.scalar_one_or_none()
async def get_user_notifications(db: AsyncSession, user_id):
result = await db.execute(
select(Notification)
.where(Notification.receiver_id == user_id)
.order_by(Notification.created_at.desc())
)
return result.scalars().all()
async def update_notification(db: AsyncSession, notification: Notification):
await db.commit()
await db.refresh(notification)
return notification

View File

@ -0,0 +1,25 @@
import uuid
from pydantic import BaseModel
from domains.notifications.models import NotificationType
class NotificationBase(BaseModel):
title: str
description: str | None = None
type: NotificationType
group_id: uuid.UUID | None = None
class NotificationCreate(NotificationBase):
receiver_id: uuid.UUID
sender_id: uuid.UUID | None = None
class NotificationResponse(NotificationBase):
id: uuid.UUID
is_accepted: bool | None
receiver_id: uuid.UUID
sender_id: uuid.UUID | None
class Config:
from_attributes = True
class NotificationAction(BaseModel):
is_accepted: bool

View File

@ -0,0 +1,74 @@
from sqlalchemy.ext.asyncio import AsyncSession
from domains.notifications.models import Notification, NotificationType
from domains.notifications.repo import (
create_notification,
get_user_notifications,
get_notification_by_id,
update_notification
)
async def send_public_notification(
db: AsyncSession,
title: str,
description: str,
sender_id: str,
receiver_ids: list[str]
):
notifications = []
for receiver_id in receiver_ids:
notification = Notification(
title=title,
description=description,
type=NotificationType.PUBLIC,
sender_id=sender_id,
receiver_id=receiver_id
)
db.add(notification)
notifications.append(notification)
await db.commit()
return notifications
async def send_join_request(
db: AsyncSession,
sender_id: str,
receiver_id: str,
group_id: str,
title: str,
description: str | None = None
):
notification = Notification(
title=title,
description=description,
type=NotificationType.JOIN_REQUEST,
sender_id=sender_id,
receiver_id=receiver_id,
group_id=group_id
)
return await create_notification(db, notification)
async def list_my_notifications(db: AsyncSession, user_id):
return await get_user_notifications(db, user_id)
async def respond_to_notification(
db: AsyncSession,
notification_id: str,
user_id: str,
is_accepted: bool
):
notification = await get_notification_by_id(db, notification_id)
if not notification:
raise ValueError("Notification not found")
if str(notification.receiver_id) != str(user_id):
raise ValueError("Permission denied")
notification.is_accepted = is_accepted
await update_notification(db, notification)
# If it's a join request and accepted, add user to group
if notification.type == NotificationType.JOIN_REQUEST and is_accepted:
from domains.groups.service import add_member_to_group
await add_member_to_group(db, notification.group_id, user_id)
return notification

View File

@ -2,11 +2,10 @@ 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, get_current_user from core.deps import get_current_user
from domains.groups.schemas import GroupResponse
from domains.groups.service import list_user_groups
from domains.users.schemas import UserResponse
from domains.users.repo import get_all_users
router = APIRouter( router = APIRouter(
prefix="/users", prefix="/users",
@ -14,9 +13,12 @@ router = APIRouter(
) )
@router.get("/me/groups", response_model=list[GroupResponse]) @router.get("/", response_model=list[UserResponse])
async def my_groups( async def list_users(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
user=Depends(get_current_user) user=Depends(get_current_user)
): ):
return await list_user_groups(db, user.id) """
List all users. Regular users can use this to find people to invite to groups.
"""
return await get_all_users(db)

View File

@ -1,15 +1,9 @@
from enum import Enum from enum import Enum
from sqlalchemy import String, Boolean, Integer
from sqlalchemy import String, Boolean, Enum as SQLEnum
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from db.base import Base from db.base import Base
class UserRole(str, Enum):
ADMIN = "admin"
GROUP_MANAGER = "group_manager"
MEMBER = "member"
class User(Base): class User(Base):
username: Mapped[str] = mapped_column( username: Mapped[str] = mapped_column(
String(50), String(50),
@ -23,13 +17,26 @@ class User(Base):
nullable=False, nullable=False,
) )
role: Mapped[UserRole] = mapped_column( is_admin: Mapped[bool] = mapped_column(
SQLEnum(UserRole, name="user_role"), Boolean,
default=UserRole.MEMBER, default=False,
index=True, index=True,
nullable=False, nullable=False,
) )
phone_number: Mapped[str | None] = mapped_column(
String(11),
unique=True,
index=True,
nullable=True,
)
token_version: Mapped[int] = mapped_column(
Integer,
default=1,
nullable=False,
)
is_active: Mapped[bool] = mapped_column( is_active: Mapped[bool] = mapped_column(
Boolean, Boolean,
default=True, default=True,

View File

@ -1,15 +1,27 @@
import uuid import uuid
from pydantic import BaseModel from pydantic import BaseModel
from domains.users.models import UserRole from pydantic import BaseModel, Field, field_validator
import re
class UserCreate(BaseModel): class UserCreate(BaseModel):
username: str username: str
role: UserRole phone_number: str | None = Field(None, description="11 digit phone number")
is_admin: bool = False
@field_validator("phone_number")
@classmethod
def validate_phone_number(cls, v: str | None) -> str | None:
if v is None:
return v
if not re.match(r"^\d{11}$", v):
raise ValueError("Phone number must be exactly 11 digits")
return v
class UserResponse(BaseModel): class UserResponse(BaseModel):
id: uuid.UUID id: uuid.UUID
username: str username: str
role: str phone_number: str | None
is_admin: bool
is_active: bool is_active: bool
class Config: class Config:

View File

@ -1,44 +1,6 @@
import secrets
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from core.security import hash_password from domains.users.repo import get_user_by_id
from domains.users.models import User
from domains.users.repo import create_user
from core.config import settings
def generate_user_secret():
# return secrets.token_urlsafe(settings.SECRET_PASS_LENGTH)
return "1234"
async def create_user_by_admin(
db: AsyncSession,
username: str,
role: str
):
secret = generate_user_secret()
user = User(
username=username,
role=role,
secret_hash=hash_password(secret)
)
await create_user(db, user)
return user, secret
async def get_user(db: AsyncSession, user_id): async def get_user(db: AsyncSession, user_id):
from domains.users.repo import get_user_by_id return await get_user_by_id(db, user_id)
return await get_user_by_id(db, user_id)
async def reset_user_secret(db: AsyncSession, user: User):
new_secret = generate_user_secret()
user.secret_hash = hash_password(new_secret)
await db.commit()
return new_secret