feat: add 13 changes
This commit is contained in:
parent
bedecb82bd
commit
a99d920f2c
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
@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)
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -46,3 +55,13 @@ class GroupMember(Base):
|
|||
ForeignKey("groups.id", ondelete="CASCADE"),
|
||||
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()
|
||||
|
|
@ -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()
|
||||
|
||||
|
||||
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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -50,3 +90,45 @@ async def list_user_groups(
|
|||
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)
|
||||
0
Back/domains/notifications/__init__.py
Normal file
0
Back/domains/notifications/__init__.py
Normal file
64
Back/domains/notifications/api.py
Normal file
64
Back/domains/notifications/api.py
Normal 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
|
||||
)
|
||||
42
Back/domains/notifications/models.py
Normal file
42
Back/domains/notifications/models.py
Normal 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
|
||||
)
|
||||
28
Back/domains/notifications/repo.py
Normal file
28
Back/domains/notifications/repo.py
Normal 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
|
||||
25
Back/domains/notifications/schemas.py
Normal file
25
Back/domains/notifications/schemas.py
Normal 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
|
||||
74
Back/domains/notifications/service.py
Normal file
74
Back/domains/notifications/service.py
Normal 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
|
||||
|
|
@ -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)
|
||||
"""
|
||||
List all users. Regular users can use this to find people to invite to groups.
|
||||
"""
|
||||
return await get_all_users(db)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
async def get_user(db: AsyncSession, 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
|
||||
Loading…
Reference in New Issue
Block a user