diff --git a/dev/scripts/dummy_users.py b/dev/scripts/dummy_users.py new file mode 100644 index 000000000000..2fe17e3f6345 --- /dev/null +++ b/dev/scripts/dummy_users.py @@ -0,0 +1,26 @@ +import json +from pathlib import Path + +import requests + +CWD = Path(__file__).parent + + +def login(username="changeme@email.com", password="MyPassword"): + + payload = {"username": username, "password": password} + r = requests.post("http://localhost:9000/api/auth/token", payload) + + # Bearer + token = json.loads(r.text).get("access_token") + return {"Authorization": f"Bearer {token}"} + + +def main(): + print("Starting...") + + print("Finished...") + + +if __name__ == "__main__": + main() diff --git a/frontend/api/class-interfaces/admin-about.ts b/frontend/api/class-interfaces/admin-about.ts index 548fd76667f1..667925d7d551 100644 --- a/frontend/api/class-interfaces/admin-about.ts +++ b/frontend/api/class-interfaces/admin-about.ts @@ -5,6 +5,7 @@ const prefix = "/api"; const routes = { about: `${prefix}/admin/about`, aboutStatistics: `${prefix}/admin/about/statistics`, + check: `${prefix}/admin/about/check`, }; export interface AdminAboutInfo { @@ -26,6 +27,11 @@ export interface AdminStatistics { untaggedRecipes: number; } +export interface CheckAppConfig { + emailReady: boolean; + baseUrlSet: boolean; +} + export class AdminAboutAPI extends BaseAPI { async about() { return await this.requests.get(routes.about); @@ -34,4 +40,8 @@ export class AdminAboutAPI extends BaseAPI { async statistics() { return await this.requests.get(routes.aboutStatistics); } + + async checkApp() { + return await this.requests.get(routes.check); + } } diff --git a/frontend/api/class-interfaces/email.ts b/frontend/api/class-interfaces/email.ts index e137f0cc6875..667c21c7ed33 100644 --- a/frontend/api/class-interfaces/email.ts +++ b/frontend/api/class-interfaces/email.ts @@ -2,6 +2,8 @@ import { BaseAPI } from "./_base"; const routes = { base: "/api/admin/email", + + invitation: "/api/groups/invitations/email", }; export interface CheckEmailResponse { @@ -17,6 +19,16 @@ export interface TestEmailPayload { email: string; } +export interface InvitationEmail { + email: string; + token: string; +} + +export interface InvitationEmailResponse { + success: boolean; + error: string; +} + export class EmailAPI extends BaseAPI { check() { return this.requests.get(routes.base); @@ -25,4 +37,8 @@ export class EmailAPI extends BaseAPI { test(payload: TestEmailPayload) { return this.requests.post(routes.base, payload); } + + sendInvitation(payload: InvitationEmail) { + return this.requests.post(routes.invitation, payload); + } } diff --git a/frontend/api/class-interfaces/groups.ts b/frontend/api/class-interfaces/groups.ts index 9d7e3f932805..e0f16297ac3f 100644 --- a/frontend/api/class-interfaces/groups.ts +++ b/frontend/api/class-interfaces/groups.ts @@ -1,5 +1,5 @@ import { BaseCRUDAPI } from "./_base"; -import { GroupInDB } from "~/types/api-types/user"; +import { GroupInDB, UserOut } from "~/types/api-types/user"; const prefix = "/api"; @@ -7,6 +7,8 @@ const routes = { groups: `${prefix}/admin/groups`, groupsSelf: `${prefix}/groups/self`, categories: `${prefix}/groups/categories`, + members: `${prefix}/groups/members`, + permissions: `${prefix}/groups/permissions`, preferences: `${prefix}/groups/preferences`, @@ -56,6 +58,13 @@ export interface Invitation { uses_left: number; } +export interface SetPermissions { + userId: number; + canInvite: boolean; + canManage: boolean; + canOrganize: boolean; +} + export class GroupAPI extends BaseCRUDAPI { baseRoute = routes.groups; itemRoute = routes.groupsId; @@ -84,4 +93,12 @@ export class GroupAPI extends BaseCRUDAPI { async createInvitation(payload: CreateInvitation) { return await this.requests.post(routes.invitation, payload); } + + async fetchMembers() { + return await this.requests.get(routes.members); + } + + async setMemberPermissions(payload: SetPermissions) { + return await this.requests.put(routes.permissions, payload); + } } diff --git a/frontend/assets/main.css b/frontend/assets/main.css index 91c64313ec00..a0bab629afd6 100644 --- a/frontend/assets/main.css +++ b/frontend/assets/main.css @@ -10,3 +10,21 @@ .narrow-container { max-width: 700px !important; } + +.theme--dark.v-application { + background-color: var(--v-background-base, #121212) !important; +} + +.theme--dark.v-navigation-drawer { + background-color: var(--v-background-base, #121212) !important; +} + +/* 1E1E1E */ + +.theme--dark.v-card { + background-color: #2b2b2b !important; +} + +.theme--light.v-application { + background-color: var(--v-background-base, white) !important; +} diff --git a/frontend/components/Layout/AppHeader.vue b/frontend/components/Layout/AppHeader.vue index 6572756978cb..489a1b1a6874 100644 --- a/frontend/components/Layout/AppHeader.vue +++ b/frontend/components/Layout/AppHeader.vue @@ -35,7 +35,7 @@ {{ $globals.icons.logout }} {{ $t("user.logout") }} - + {{ $globals.icons.user }} {{ $t("user.login") }} diff --git a/frontend/components/Layout/AppSidebar.vue b/frontend/components/Layout/AppSidebar.vue index 0e545a6f5337..c16e0b6ac6a2 100644 --- a/frontend/components/Layout/AppSidebar.vue +++ b/frontend/components/Layout/AppSidebar.vue @@ -121,6 +121,7 @@ + diff --git a/frontend/components/global/AppButtonCopy.vue b/frontend/components/global/AppButtonCopy.vue index c2dda842fc7f..8e59e5751c1d 100644 --- a/frontend/components/global/AppButtonCopy.vue +++ b/frontend/components/global/AppButtonCopy.vue @@ -11,7 +11,7 @@ > @@ -43,6 +44,10 @@ export default { type: String, default: "primary", }, + icon: { + type: Boolean, + default: true, + }, }, data() { return { diff --git a/frontend/components/global/BaseCardSectionTitle.vue b/frontend/components/global/BaseCardSectionTitle.vue index 80b24782d990..e0979465cb1e 100644 --- a/frontend/components/global/BaseCardSectionTitle.vue +++ b/frontend/components/global/BaseCardSectionTitle.vue @@ -1,5 +1,5 @@ diff --git a/frontend/pages/user/sign-up.vue b/frontend/pages/user/sign-up.vue index 6446b96a3d83..0cdf7951bb0d 100644 --- a/frontend/pages/user/sign-up.vue +++ b/frontend/pages/user/sign-up.vue @@ -68,7 +68,7 @@ - Login + Login diff --git a/frontend/plugins/theme.ts b/frontend/plugins/theme.ts index 7a02ce729008..7b4e9ce531aa 100644 --- a/frontend/plugins/theme.ts +++ b/frontend/plugins/theme.ts @@ -1,3 +1,7 @@ export default ({ $vuetify, $config }: any) => { $vuetify.theme.themes = $config.themes; + + if ($config.useDark) { + $vuetify.theme.dark = true; + } }; diff --git a/frontend/static/svgs/manage-members.svg b/frontend/static/svgs/manage-members.svg new file mode 100644 index 000000000000..13dab51edae4 --- /dev/null +++ b/frontend/static/svgs/manage-members.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/types/api-types/user.ts b/frontend/types/api-types/user.ts index 3cc1689e65f3..19fbddfc1b92 100644 --- a/frontend/types/api-types/user.ts +++ b/frontend/types/api-types/user.ts @@ -34,6 +34,9 @@ export interface GroupInDB { shoppingLists?: ShoppingListOut[]; } export interface UserOut { + canOrganize: boolean; + canManage: boolean; + canInvite: boolean; username?: string; fullName?: string; email: string; diff --git a/makefile b/makefile index 2c4e46589ce0..2fe5063b5983 100644 --- a/makefile +++ b/makefile @@ -70,8 +70,10 @@ coverage: ## ☂️ Check code coverage quickly with the default Python $(BROWSER) htmlcov/index.html setup: ## 🏗 Setup Development Instance + cp template.env .env -n poetry install && \ cd frontend && \ + cp template.env .env -n yarn install && \ cd .. diff --git a/mealie/db/data_initialization/init_users.py b/mealie/db/data_initialization/init_users.py new file mode 100644 index 000000000000..979404d97657 --- /dev/null +++ b/mealie/db/data_initialization/init_users.py @@ -0,0 +1,61 @@ +from mealie.core import root_logger +from mealie.core.config import settings +from mealie.core.security import hash_password +from mealie.db.data_access_layer.access_model_factory import Database + +logger = root_logger.get_logger("init_users") + + +def dev_users() -> list[dict]: + return [ + { + "full_name": "Jason", + "username": "jason", + "email": "jason@email.com", + "password": hash_password(settings.DEFAULT_PASSWORD), + "group": settings.DEFAULT_GROUP, + "admin": False, + }, + { + "full_name": "Bob", + "username": "bob", + "email": "bob@email.com", + "password": hash_password(settings.DEFAULT_PASSWORD), + "group": settings.DEFAULT_GROUP, + "admin": False, + }, + { + "full_name": "Sarah", + "username": "sarah", + "email": "sarah@email.com", + "password": hash_password(settings.DEFAULT_PASSWORD), + "group": settings.DEFAULT_GROUP, + "admin": False, + }, + { + "full_name": "Sammy", + "username": "sammy", + "email": "sammy@email.com", + "password": hash_password(settings.DEFAULT_PASSWORD), + "group": settings.DEFAULT_GROUP, + "admin": False, + }, + ] + + +def default_user_init(db: Database): + default_user = { + "full_name": "Change Me", + "username": "admin", + "email": settings.DEFAULT_EMAIL, + "password": hash_password(settings.DEFAULT_PASSWORD), + "group": settings.DEFAULT_GROUP, + "admin": True, + } + + logger.info("Generating Default User") + db.users.create(default_user) + + if not settings.PRODUCTION: + for user in dev_users(): + db.users.create(user) diff --git a/mealie/db/init_db.py b/mealie/db/init_db.py index 1b62718b11a6..3435de9c8c22 100644 --- a/mealie/db/init_db.py +++ b/mealie/db/init_db.py @@ -1,8 +1,8 @@ from mealie.core import root_logger from mealie.core.config import settings -from mealie.core.security import hash_password from mealie.db.data_access_layer.access_model_factory import Database from mealie.db.data_initialization.init_units_foods import default_recipe_unit_init +from mealie.db.data_initialization.init_users import default_user_init from mealie.db.database import get_database from mealie.db.db_setup import create_session, engine from mealie.db.models._model_base import SqlAlchemyBase @@ -37,20 +37,6 @@ def default_group_init(db: Database): create_new_group(db, GroupBase(name=settings.DEFAULT_GROUP)) -def default_user_init(db: Database): - default_user = { - "full_name": "Change Me", - "username": "admin", - "email": settings.DEFAULT_EMAIL, - "password": hash_password(settings.DEFAULT_PASSWORD), - "group": settings.DEFAULT_GROUP, - "admin": True, - } - - logger.info("Generating Default User") - db.users.create(default_user) - - def main(): create_all_models() diff --git a/mealie/db/models/users/users.py b/mealie/db/models/users/users.py index 4ceaaa2b0e48..c9f937fe68bd 100644 --- a/mealie/db/models/users/users.py +++ b/mealie/db/models/users/users.py @@ -34,8 +34,12 @@ class User(SqlAlchemyBase, BaseMixins): group_id = Column(Integer, ForeignKey("groups.id")) group = orm.relationship("Group", back_populates="users") - # Recipes + # Group Permissions + can_manage = Column(Boolean, default=False) + can_invite = Column(Boolean, default=False) + can_organize = Column(Boolean, default=False) + # Recipes tokens: list[LongLiveToken] = orm.relationship( LongLiveToken, back_populates="user", cascade="all, delete, delete-orphan", single_parent=True ) @@ -59,6 +63,9 @@ class User(SqlAlchemyBase, BaseMixins): group: str = settings.DEFAULT_GROUP, admin=False, advanced=False, + can_manage=False, + can_invite=False, + can_organize=False, **_ ) -> None: @@ -71,6 +78,15 @@ class User(SqlAlchemyBase, BaseMixins): self.password = password self.advanced = advanced + if self.admin: + self.can_manage = True + self.can_invite = True + self.can_organize = True + else: + self.can_manage = can_manage + self.can_invite = can_invite + self.can_organize = can_organize + self.favorite_recipes = [] if self.username is None: @@ -87,6 +103,9 @@ class User(SqlAlchemyBase, BaseMixins): favorite_recipes=None, password=None, advanced=False, + can_manage=False, + can_invite=False, + can_organize=False, **_ ): favorite_recipes = favorite_recipes or [] @@ -103,6 +122,15 @@ class User(SqlAlchemyBase, BaseMixins): if password: self.password = password + if self.admin: + self.can_manage = True + self.can_invite = True + self.can_organize = True + else: + self.can_manage = can_manage + self.can_invite = can_invite + self.can_organize = can_organize + def update_password(self, password): self.password = password diff --git a/mealie/routes/admin/admin_about.py b/mealie/routes/admin/admin_about.py index 949b4c542aa5..cc285570eedd 100644 --- a/mealie/routes/admin/admin_about.py +++ b/mealie/routes/admin/admin_about.py @@ -4,7 +4,7 @@ from sqlalchemy.orm.session import Session from mealie.core.config import APP_VERSION, get_settings from mealie.db.database import get_database from mealie.db.db_setup import generate_session -from mealie.schema.admin.about import AdminAboutInfo, AppStatistics +from mealie.schema.admin.about import AdminAboutInfo, AppStatistics, CheckAppConfig router = APIRouter(prefix="/about") @@ -36,3 +36,15 @@ async def get_app_statistics(session: Session = Depends(generate_session)): total_users=db.users.count_all(), total_groups=db.groups.count_all(), ) + + +@router.get("/check", response_model=CheckAppConfig) +async def check_app_config(): + settings = get_settings() + + url_set = settings.BASE_URL != "http://localhost:8080" + + return CheckAppConfig( + email_ready=settings.SMTP_ENABLE, + base_url_set=url_set, + ) diff --git a/mealie/routes/groups/invitations.py b/mealie/routes/groups/invitations.py index 247f67524f84..c80e990e767b 100644 --- a/mealie/routes/groups/invitations.py +++ b/mealie/routes/groups/invitations.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, Depends, status -from mealie.schema.group.invite_token import CreateInviteToken, ReadInviteToken +from mealie.schema.group.invite_token import CreateInviteToken, EmailInitationResponse, EmailInvitation, ReadInviteToken from mealie.services.group_services.group_service import GroupSelfService router = APIRouter() @@ -14,3 +14,8 @@ def get_invite_tokens(g_service: GroupSelfService = Depends(GroupSelfService.pri @router.post("", response_model=ReadInviteToken, status_code=status.HTTP_201_CREATED) def create_invite_token(uses: CreateInviteToken, g_service: GroupSelfService = Depends(GroupSelfService.private)): return g_service.create_invite_token(uses.uses) + + +@router.post("/email", response_model=EmailInitationResponse) +def email_invitation(invite: EmailInvitation, g_service: GroupSelfService = Depends(GroupSelfService.private)): + return g_service.email_invitation(invite) diff --git a/mealie/routes/groups/self_service.py b/mealie/routes/groups/self_service.py index 9977d85db12f..37d3e1f45b6f 100644 --- a/mealie/routes/groups/self_service.py +++ b/mealie/routes/groups/self_service.py @@ -1,7 +1,8 @@ from fastapi import Depends from mealie.routes.routers import UserAPIRouter -from mealie.schema.user.user import GroupInDB +from mealie.schema.group.group_permissions import SetPermissions +from mealie.schema.user.user import GroupInDB, UserOut from mealie.services.group_services.group_service import GroupSelfService user_router = UserAPIRouter(prefix="/groups", tags=["Groups: Self Service"]) @@ -10,5 +11,17 @@ user_router = UserAPIRouter(prefix="/groups", tags=["Groups: Self Service"]) @user_router.get("/self", response_model=GroupInDB) async def get_logged_in_user_group(g_service: GroupSelfService = Depends(GroupSelfService.write_existing)): """ Returns the Group Data for the Current User """ - return g_service.item + + +@user_router.get("/members", response_model=list[UserOut]) +async def get_group_members(g_service: GroupSelfService = Depends(GroupSelfService.write_existing)): + """ Returns the Group of user lists """ + return g_service.get_members() + + +@user_router.put("/permissions", response_model=UserOut) +async def set_member_permissions( + payload: SetPermissions, g_service: GroupSelfService = Depends(GroupSelfService.manage_existing) +): + return g_service.set_member_permissions(payload) diff --git a/mealie/schema/admin/about.py b/mealie/schema/admin/about.py index d7a762a723b8..fe3d704b55fe 100644 --- a/mealie/schema/admin/about.py +++ b/mealie/schema/admin/about.py @@ -23,3 +23,8 @@ class AdminAboutInfo(AppInfo): db_type: str db_url: Path default_group: str + + +class CheckAppConfig(CamelModel): + email_ready: bool = False + base_url_set: bool = False diff --git a/mealie/schema/group/group_permissions.py b/mealie/schema/group/group_permissions.py new file mode 100644 index 000000000000..70a1520dab92 --- /dev/null +++ b/mealie/schema/group/group_permissions.py @@ -0,0 +1,8 @@ +from fastapi_camelcase import CamelModel + + +class SetPermissions(CamelModel): + user_id: int + can_manage: bool = False + can_invite: bool = False + can_organize: bool = False diff --git a/mealie/schema/group/invite_token.py b/mealie/schema/group/invite_token.py index 31bc53c4cb32..3662d3327abb 100644 --- a/mealie/schema/group/invite_token.py +++ b/mealie/schema/group/invite_token.py @@ -18,3 +18,13 @@ class ReadInviteToken(CamelModel): class Config: orm_mode = True + + +class EmailInvitation(CamelModel): + email: str + token: str + + +class EmailInitationResponse(CamelModel): + success: bool + error: str = None diff --git a/mealie/schema/user/user.py b/mealie/schema/user/user.py index 2e1873816d22..b99ba93a38c6 100644 --- a/mealie/schema/user/user.py +++ b/mealie/schema/user/user.py @@ -55,6 +55,10 @@ class UserBase(CamelModel): advanced: bool = False favorite_recipes: Optional[list[str]] = [] + can_invite: bool = False + can_manage: bool = False + can_organize: bool = False + class Config: orm_mode = True diff --git a/mealie/services/email/email_service.py b/mealie/services/email/email_service.py index 2baf79ffe992..6bf13e7d2c12 100644 --- a/mealie/services/email/email_service.py +++ b/mealie/services/email/email_service.py @@ -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, diff --git a/mealie/services/email/templates/default.html b/mealie/services/email/templates/default.html index 091b7ae1966d..757f0b3bc352 100644 --- a/mealie/services/email/templates/default.html +++ b/mealie/services/email/templates/default.html @@ -419,7 +419,7 @@ " >
- {{ data.bottom_message}} + {{ data.message_bottom}}
diff --git a/mealie/services/group_services/group_service.py b/mealie/services/group_services/group_service.py index 1f05f1343de6..745e64dd84e8 100644 --- a/mealie/services/group_services/group_service.py +++ b/mealie/services/group_services/group_service.py @@ -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 diff --git a/mealie/services/user_services/registration_service.py b/mealie/services/user_services/registration_service.py index 49312a57a5e1..f9ac2cd80636 100644 --- a/mealie/services/user_services/registration_service.py +++ b/mealie/services/user_services/registration_service.py @@ -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) diff --git a/tests/integration_tests/user_group_tests/test_group_registration.py b/tests/integration_tests/user_group_tests/test_group_registration.py index 8117efdf6259..425603cff95c 100644 --- a/tests/integration_tests/user_group_tests/test_group_registration.py +++ b/tests/integration_tests/user_group_tests/test_group_registration.py @@ -4,6 +4,7 @@ from tests.utils.factories import user_registration_factory class Routes: + self = "/api/users/self" base = "/api/users/register" auth_token = "/api/auth/token" @@ -22,3 +23,31 @@ def test_user_registration_new_group(api_client: TestClient): token = response.json().get("access_token") assert token is not None + + +def test_new_user_group_permissions(api_client: TestClient): + registration = user_registration_factory() + + response = api_client.post(Routes.base, json=registration.dict(by_alias=True)) + assert response.status_code == 201 + + # Login + form_data = {"username": registration.email, "password": registration.password} + + response = api_client.post(Routes.auth_token, form_data) + assert response.status_code == 200 + token = response.json().get("access_token") + + assert token is not None + + # Get User + + headers = {"Authorization": f"Bearer {token}"} + response = api_client.get(Routes.self, headers=headers) + + assert response.status_code == 200 + user = response.json() + + assert user.get("canInvite") is True + assert user.get("canManage") is True + assert user.get("canOrganize") is True