diff --git a/Back/core/deps.py b/Back/core/deps.py index 42ec796..9f3048d 100644 --- a/Back/core/deps.py +++ b/Back/core/deps.py @@ -51,6 +51,14 @@ async def get_current_user( 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 @@ -61,7 +69,7 @@ async def get_current_admin( Ensure the authenticated user is an admin """ - if user.role != "admin": + if not user.is_admin: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required", diff --git a/Back/core/jwt.py b/Back/core/jwt.py index c6df1b2..fed860e 100755 --- a/Back/core/jwt.py +++ b/Back/core/jwt.py @@ -6,6 +6,7 @@ from core.config import settings def create_access_token( subject: str, + token_version: int, expires_delta: timedelta | None = None, ) -> str: @@ -18,6 +19,7 @@ def create_access_token( payload = { "sub": subject, + "token_version": token_version, "exp": expire, } diff --git a/Back/domains/admin/api.py b/Back/domains/admin/api.py index 257d278..446a050 100644 --- a/Back/domains/admin/api.py +++ b/Back/domains/admin/api.py @@ -38,7 +38,8 @@ async def create_user( user, secret = await admin_create_user( db, payload.username, - payload.role + payload.phone_number, + payload.is_admin ) 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", response_model=AdminResetSecretResult) async def reset_secret( diff --git a/Back/domains/admin/schemas.py b/Back/domains/admin/schemas.py index c794317..a0bb7f7 100644 --- a/Back/domains/admin/schemas.py +++ b/Back/domains/admin/schemas.py @@ -1,15 +1,16 @@ import uuid from pydantic import BaseModel -from domains.users.models import UserRole class AdminCreateUser(BaseModel): username: str - role: UserRole + phone_number: str | None = None + is_admin: bool = False class AdminUserResponse(BaseModel): id: uuid.UUID username: str - role: UserRole + phone_number: str | None + is_admin: bool is_active: bool class Config: diff --git a/Back/domains/admin/service.py b/Back/domains/admin/service.py index eea57c6..1093459 100644 --- a/Back/domains/admin/service.py +++ b/Back/domains/admin/service.py @@ -20,7 +20,8 @@ def generate_user_secret(): async def admin_create_user( db: AsyncSession, username: str, - role: str + phone_number: str | None = None, + is_admin: bool = False ): existing = await get_user_by_username(db, username) @@ -32,7 +33,8 @@ async def admin_create_user( user = User( username=username, - role=role, + phone_number=phone_number, + is_admin=is_admin, secret_hash=hash_password(secret) ) @@ -41,6 +43,19 @@ async def admin_create_user( 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( db: AsyncSession, user_id diff --git a/Back/domains/auth/schemas.py b/Back/domains/auth/schemas.py index c171682..997fe74 100644 --- a/Back/domains/auth/schemas.py +++ b/Back/domains/auth/schemas.py @@ -15,7 +15,7 @@ class TokenResponse(BaseModel): class AuthUser(BaseModel): id: uuid.UUID username: str - role: str + is_admin: bool class Config: from_attributes = True \ No newline at end of file diff --git a/Back/domains/auth/service.py b/Back/domains/auth/service.py index 42d0f05..35de957 100644 --- a/Back/domains/auth/service.py +++ b/Back/domains/auth/service.py @@ -44,7 +44,8 @@ async def login_user( return None token = create_access_token( - subject=str(user.id) + subject=str(user.id), + token_version=user.token_version ) return { diff --git a/Back/domains/groups/api.py b/Back/domains/groups/api.py index 56c58f9..be8b8a2 100644 --- a/Back/domains/groups/api.py +++ b/Back/domains/groups/api.py @@ -2,17 +2,22 @@ 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_admin +from core.deps import get_current_admin, get_current_user from domains.groups.schemas import ( GroupCreate, GroupResponse, - AddMemberRequest + AddMemberRequest, + GroupMemberResponse ) from domains.groups.service import ( 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( payload: GroupCreate, 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( db, - payload.name + payload.name, + user.id, + user.is_admin ) return group -@router.post("/{group_id}/members") -async def add_member( - group_id: str, - payload: AddMemberRequest, +@router.get("/my", response_model=list[GroupResponse]) +async def my_groups( + db: AsyncSession = Depends(get_db), + 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), 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: - membership = await add_member_to_group( + notification = await invite_member_to_group( db, group_id, - payload.user_id, + user.id, + payload.username ) + return {"message": "Invitation sent", "notification_id": notification.id} except ValueError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) - return membership \ No newline at end of file + +@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) + ) \ No newline at end of file diff --git a/Back/domains/groups/models.py b/Back/domains/groups/models.py index 8ecd293..688e397 100644 --- a/Back/domains/groups/models.py +++ b/Back/domains/groups/models.py @@ -3,14 +3,20 @@ from enum import Enum from sqlalchemy import String, Boolean, ForeignKey, Enum as SQLEnum 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 class GroupType(str, Enum): - GROUP = "group" - DIRECT = "direct" - + PUBLIC = "public" + PRIVATE = "private" +class GroupMemberRole(str, Enum): + MANAGER = "manager" + MEMBER = "member" class Group(Base): __tablename__ = "groups" # type: ignore @@ -23,7 +29,7 @@ class Group(Base): type: Mapped[GroupType] = mapped_column( SQLEnum(GroupType, name="group_type"), - default=GroupType.GROUP, + default=GroupType.PUBLIC, nullable=False ) @@ -33,6 +39,9 @@ class Group(Base): index=True ) + # Relationship to members + members: Mapped[list["GroupMember"]] = relationship(back_populates="group", cascade="all, delete-orphan") + class GroupMember(Base): __tablename__ = "group_members" # type: ignore @@ -45,4 +54,14 @@ class GroupMember(Base): group_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("groups.id", ondelete="CASCADE"), index=True - ) \ No newline at end of file + ) + + 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() \ No newline at end of file diff --git a/Back/domains/groups/repo.py b/Back/domains/groups/repo.py index 177bd55..c159701 100644 --- a/Back/domains/groups/repo.py +++ b/Back/domains/groups/repo.py @@ -1,4 +1,5 @@ from sqlalchemy import select +import uuid from sqlalchemy.ext.asyncio import AsyncSession from domains.groups.models import Group, GroupMember @@ -31,6 +32,42 @@ async def get_user_groups(db: AsyncSession, user_id): 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): result = await db.execute(select(Group)) - return result.scalars().all() \ No newline at end of file + 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() \ No newline at end of file diff --git a/Back/domains/groups/schemas.py b/Back/domains/groups/schemas.py index 2ba8963..b68cdfe 100644 --- a/Back/domains/groups/schemas.py +++ b/Back/domains/groups/schemas.py @@ -1,7 +1,7 @@ import uuid from pydantic import BaseModel - +from domains.groups.models import GroupType, GroupMemberRole class GroupCreate(BaseModel): name: str @@ -9,17 +9,20 @@ class GroupCreate(BaseModel): class GroupResponse(BaseModel): id: uuid.UUID name: str + type: GroupType is_active: bool class Config: from_attributes = True class AddMemberRequest(BaseModel): - user_id: uuid.UUID + username: str # Req 12 says user enters username class GroupMemberResponse(BaseModel): user_id: uuid.UUID - group_id: uuid.UUID + username: str + role: GroupMemberRole + is_online: bool = False class Config: from_attributes = True \ No newline at end of file diff --git a/Back/domains/groups/service.py b/Back/domains/groups/service.py index ff03b84..f851a28 100644 --- a/Back/domains/groups/service.py +++ b/Back/domains/groups/service.py @@ -1,46 +1,86 @@ from sqlalchemy.ext.asyncio import AsyncSession +import uuid 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 ( create_group, get_group_by_id, 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( db: AsyncSession, name: str, + creator_id, + is_admin: bool ): - + group_type = GroupType.PUBLIC if is_admin else GroupType.PRIVATE + group = Group( 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, 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 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: + # 2. Check if target user exists + target_user = await get_user_by_username(db, target_username) + if not target_user: 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( group_id=group_id, user_id=user_id, + role=role ) return await add_group_member(db, membership) @@ -49,4 +89,46 @@ async def list_user_groups( db: AsyncSession, user_id ): - return await get_user_groups(db, user_id) \ No newline at end of file + 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) \ No newline at end of file diff --git a/Back/domains/notifications/__init__.py b/Back/domains/notifications/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Back/domains/notifications/api.py b/Back/domains/notifications/api.py new file mode 100644 index 0000000..aa8a09a --- /dev/null +++ b/Back/domains/notifications/api.py @@ -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 + ) diff --git a/Back/domains/notifications/models.py b/Back/domains/notifications/models.py new file mode 100644 index 0000000..0e38033 --- /dev/null +++ b/Back/domains/notifications/models.py @@ -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 + ) diff --git a/Back/domains/notifications/repo.py b/Back/domains/notifications/repo.py new file mode 100644 index 0000000..e50b464 --- /dev/null +++ b/Back/domains/notifications/repo.py @@ -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 diff --git a/Back/domains/notifications/schemas.py b/Back/domains/notifications/schemas.py new file mode 100644 index 0000000..1f81efc --- /dev/null +++ b/Back/domains/notifications/schemas.py @@ -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 diff --git a/Back/domains/notifications/service.py b/Back/domains/notifications/service.py new file mode 100644 index 0000000..bfbfeea --- /dev/null +++ b/Back/domains/notifications/service.py @@ -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 diff --git a/Back/domains/users/api.py b/Back/domains/users/api.py index b76c9d1..a244395 100644 --- a/Back/domains/users/api.py +++ b/Back/domains/users/api.py @@ -2,11 +2,10 @@ from fastapi import APIRouter, Depends from sqlalchemy.ext.asyncio import AsyncSession from db.session import get_db -from core.deps import get_current_admin, get_current_user - -from domains.groups.schemas import GroupResponse -from domains.groups.service import list_user_groups +from core.deps import get_current_user +from domains.users.schemas import UserResponse +from domains.users.repo import get_all_users router = APIRouter( prefix="/users", @@ -14,9 +13,12 @@ router = APIRouter( ) -@router.get("/me/groups", response_model=list[GroupResponse]) -async def my_groups( +@router.get("/", response_model=list[UserResponse]) +async def list_users( db: AsyncSession = Depends(get_db), user=Depends(get_current_user) ): - return await list_user_groups(db, user.id) \ No newline at end of file + """ + List all users. Regular users can use this to find people to invite to groups. + """ + return await get_all_users(db) diff --git a/Back/domains/users/models.py b/Back/domains/users/models.py index 6c929dd..0e180a7 100644 --- a/Back/domains/users/models.py +++ b/Back/domains/users/models.py @@ -1,15 +1,9 @@ from enum import Enum - -from sqlalchemy import String, Boolean, Enum as SQLEnum +from sqlalchemy import String, Boolean, Integer from sqlalchemy.orm import Mapped, mapped_column from db.base import Base -class UserRole(str, Enum): - ADMIN = "admin" - GROUP_MANAGER = "group_manager" - MEMBER = "member" - class User(Base): username: Mapped[str] = mapped_column( String(50), @@ -23,13 +17,26 @@ class User(Base): nullable=False, ) - role: Mapped[UserRole] = mapped_column( - SQLEnum(UserRole, name="user_role"), - default=UserRole.MEMBER, + is_admin: Mapped[bool] = mapped_column( + Boolean, + default=False, index=True, 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( Boolean, default=True, diff --git a/Back/domains/users/schemas.py b/Back/domains/users/schemas.py index 41e1fe7..ffdc732 100644 --- a/Back/domains/users/schemas.py +++ b/Back/domains/users/schemas.py @@ -1,15 +1,27 @@ import uuid from pydantic import BaseModel -from domains.users.models import UserRole +from pydantic import BaseModel, Field, field_validator +import re class UserCreate(BaseModel): 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): id: uuid.UUID username: str - role: str + phone_number: str | None + is_admin: bool is_active: bool class Config: diff --git a/Back/domains/users/service.py b/Back/domains/users/service.py index 042f2dc..4f5f00b 100644 --- a/Back/domains/users/service.py +++ b/Back/domains/users/service.py @@ -1,44 +1,6 @@ -import secrets from sqlalchemy.ext.asyncio import AsyncSession -from core.security import hash_password -from domains.users.models import User -from domains.users.repo import create_user -from core.config import settings +from domains.users.repo import get_user_by_id -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): - from domains.users.repo import get_user_by_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 \ No newline at end of file + return await get_user_by_id(db, user_id) \ No newline at end of file