mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-04 23:13:12 -05:00
feat(frontend): ✨ add group permissions (#721)
* style(frontend): 💄 add darktheme custom * add dummy users in dev mode * feat(frontend): ✨ add group permissions editor UI * feat(backend): ✨ add group permissions setters * test(backend): ✅ tests for basic permission get/set (WIP) Needs more testing * remove old test * chore(backend): copy template.env on setup * feat(frontend): ✨ enable send invitation via email * feat(backend): ✨ enable send invitation via email * feat: ✨ add app config checker for site-settings * refactor(frontend): ♻️ consolidate bool checks Co-authored-by: Hayden <hay-kot@pm.me>
This commit is contained in:
@@ -55,7 +55,7 @@ class EmailService(BaseService):
|
||||
def send_invitation(self, address: str, invitation_url: str) -> bool:
|
||||
invitation = EmailTemplate(
|
||||
subject="Invitation to join Mealie",
|
||||
header_text="Invitation",
|
||||
header_text="Your Invited!",
|
||||
message_top="You have been invited to join Mealie.",
|
||||
message_bottom="Please click the button below to accept the invitation.",
|
||||
button_link=invitation_url,
|
||||
|
||||
@@ -419,7 +419,7 @@
|
||||
"
|
||||
>
|
||||
<div style="text-align: center">
|
||||
{{ data.bottom_message}}
|
||||
{{ data.message_bottom}}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -2,15 +2,17 @@ from __future__ import annotations
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import Depends
|
||||
from fastapi import Depends, HTTPException, status
|
||||
|
||||
from mealie.core.dependencies.grouped import UserDeps
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.schema.group.group_permissions import SetPermissions
|
||||
from mealie.schema.group.group_preferences import UpdateGroupPreferences
|
||||
from mealie.schema.group.invite_token import ReadInviteToken, SaveInviteToken
|
||||
from mealie.schema.group.invite_token import EmailInitationResponse, EmailInvitation, ReadInviteToken, SaveInviteToken
|
||||
from mealie.schema.recipe.recipe_category import CategoryBase
|
||||
from mealie.schema.user.user import GroupInDB
|
||||
from mealie.schema.user.user import GroupInDB, PrivateUser, UserOut
|
||||
from mealie.services._base_http_service.http_services import UserHttpService
|
||||
from mealie.services.email import EmailService
|
||||
from mealie.services.events import create_group_event
|
||||
|
||||
logger = get_logger(module=__name__)
|
||||
@@ -31,10 +33,38 @@ class GroupSelfService(UserHttpService[int, str]):
|
||||
"""Override parent method to remove `item_id` from arguments"""
|
||||
return super().write_existing(item_id=0, deps=deps)
|
||||
|
||||
@classmethod
|
||||
def manage_existing(cls, deps: UserDeps = Depends()):
|
||||
"""Override parent method to remove `item_id` from arguments"""
|
||||
if not deps.user.can_manage:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
return super().write_existing(item_id=0, deps=deps)
|
||||
|
||||
def populate_item(self, _: str = None) -> GroupInDB:
|
||||
self.item = self.db.groups.get(self.group_id)
|
||||
return self.item
|
||||
|
||||
# ====================================================================
|
||||
# Manage Menbers
|
||||
|
||||
def get_members(self) -> list[UserOut]:
|
||||
return self.db.users.multi_query(query_by={"group_id": self.item.id}, override_schema=UserOut)
|
||||
|
||||
def set_member_permissions(self, permissions: SetPermissions) -> PrivateUser:
|
||||
target_user = self.db.users.get(permissions.user_id)
|
||||
|
||||
if not target_user:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
|
||||
if target_user.group_id != self.group_id:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, detail="User is not a member of this group")
|
||||
|
||||
target_user.can_invite = permissions.can_invite
|
||||
target_user.can_manage = permissions.can_manage
|
||||
target_user.can_organize = permissions.can_organize
|
||||
|
||||
return self.db.users.update(permissions.user_id, target_user)
|
||||
|
||||
# ====================================================================
|
||||
# Meal Categories
|
||||
|
||||
@@ -53,11 +83,27 @@ class GroupSelfService(UserHttpService[int, str]):
|
||||
# Group Invites
|
||||
|
||||
def create_invite_token(self, uses: int = 1) -> None:
|
||||
if not self.user.can_invite:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, detail="User is not allowed to create invite tokens")
|
||||
|
||||
token = SaveInviteToken(uses_left=uses, group_id=self.group_id, token=uuid4().hex)
|
||||
return self.db.group_invite_tokens.create(token)
|
||||
|
||||
def get_invite_tokens(self) -> list[ReadInviteToken]:
|
||||
return self.db.group_invite_tokens.multi_query({"group_id": self.group_id})
|
||||
|
||||
def email_invitation(self, invite: EmailInvitation) -> EmailInitationResponse:
|
||||
email_service = EmailService()
|
||||
url = f"{self.settings.BASE_URL}/register?token={invite.token}"
|
||||
|
||||
success = False
|
||||
error = None
|
||||
try:
|
||||
success = email_service.send_invitation(address=invite.email, invitation_url=url)
|
||||
except Exception as e:
|
||||
error = str(e)
|
||||
|
||||
return EmailInitationResponse(success=success, error=error)
|
||||
|
||||
# ====================================================================
|
||||
# Export / Import Recipes
|
||||
|
||||
@@ -23,25 +23,21 @@ class RegistrationService(PublicHttpService[int, str]):
|
||||
|
||||
logger.info(f"Registering user {registration.username}")
|
||||
token_entry = None
|
||||
new_group = False
|
||||
|
||||
if registration.group:
|
||||
new_group = True
|
||||
group = self._register_new_group()
|
||||
|
||||
elif registration.group_token and registration.group_token != "":
|
||||
|
||||
token_entry = self.db.group_invite_tokens.get(registration.group_token)
|
||||
|
||||
print("Token Entry", token_entry)
|
||||
|
||||
if not token_entry:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, {"message": "Invalid group token"})
|
||||
|
||||
group = self.db.groups.get(token_entry.group_id)
|
||||
|
||||
else:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, {"message": "Missing group"})
|
||||
|
||||
user = self._create_new_user(group)
|
||||
user = self._create_new_user(group, new_group)
|
||||
|
||||
if token_entry and user:
|
||||
token_entry.uses_left = token_entry.uses_left - 1
|
||||
@@ -54,7 +50,7 @@ class RegistrationService(PublicHttpService[int, str]):
|
||||
|
||||
return user
|
||||
|
||||
def _create_new_user(self, group: GroupInDB) -> PrivateUser:
|
||||
def _create_new_user(self, group: GroupInDB, new_group=bool) -> PrivateUser:
|
||||
new_user = UserIn(
|
||||
email=self.registration.email,
|
||||
username=self.registration.username,
|
||||
@@ -62,6 +58,9 @@ class RegistrationService(PublicHttpService[int, str]):
|
||||
full_name=self.registration.username,
|
||||
advanced=self.registration.advanced,
|
||||
group=group.name,
|
||||
can_invite=new_group,
|
||||
can_manage=new_group,
|
||||
can_organize=new_group,
|
||||
)
|
||||
|
||||
return self.db.users.create(new_user)
|
||||
|
||||
Reference in New Issue
Block a user