Skip to content

Commit 7c3b6c4

Browse files
committed
feat: Added implementation to leave group as student
1 parent 457be17 commit 7c3b6c4

File tree

7 files changed

+117
-4
lines changed

7 files changed

+117
-4
lines changed

tasky/src/routes/group.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::models::group_join_request::GroupJoinRequestRepository;
99
use crate::response::group::{GroupResponse, GroupsResponse};
1010
use crate::response::shared::User;
1111
use crate::response::Enrich;
12+
use crate::security::StaticSecurity;
1213
use crate::security::{IsGranted, SecurityAction};
1314
use crate::AppState;
1415
use actix_web::delete;
@@ -280,3 +281,42 @@ pub async fn remove_user(
280281
GroupRepository::update_group(group, conn);
281282
Ok(HttpResponse::Ok().finish())
282283
}
284+
285+
/// Endpoint to leave a specific group as student
286+
#[post("/groups/{id}/leave")]
287+
pub async fn leave_group(
288+
data: web::Data<AppState>,
289+
user: web::ReqData<UserData>,
290+
path: web::Path<(i32,)>,
291+
) -> Result<HttpResponse, ApiError> {
292+
let conn = &mut data.db.db.get().unwrap();
293+
let path_data = path.into_inner();
294+
295+
let mut group = GroupRepository::get_by_id(path_data.0, conn).ok_or(ApiError::BadRequest {
296+
message: "No access to group".to_string(),
297+
})?;
298+
299+
if !group.is_granted(SecurityAction::Read, &user) {
300+
return Err(ApiError::Forbidden {
301+
message: "You are not allowed to leave this group".to_string(),
302+
});
303+
}
304+
305+
if !StaticSecurity::is_granted(crate::security::StaticSecurityAction::IsStudent, &user) {
306+
return Err(ApiError::Forbidden {
307+
message: "You are not a student and not able to leave this group".to_string(),
308+
});
309+
}
310+
311+
// TODO: When switching to more scalabe approach, consider adding membership verification here
312+
// This is not nessesary for application security but would be a little extra
313+
314+
group.members = group
315+
.members
316+
.iter()
317+
.filter(|m| m.is_some() && m.unwrap() != user.user_id)
318+
.copied()
319+
.collect();
320+
GroupRepository::update_group(group, conn);
321+
Ok(HttpResponse::Ok().finish())
322+
}

tasky/src/routes/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub fn init_services(cfg: &mut web::ServiceConfig) {
2424
.service(group::get_enlistable_users)
2525
.service(group::enlist_user)
2626
.service(group::remove_user)
27+
.service(group::leave_group)
2728
.service(group_join_request::create_join_request)
2829
.service(group_join_request::get_join_requests)
2930
.service(group_join_request::approve_join_request)

web/app/groups/[groupId]/page.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import UpdateGroupModal from "@/components/group/UpdateGroupModal";
1313
import useCurrentUser from "@/hooks/useCurrentUser";
1414
import {isGranted} from "@/service/auth";
1515
import {UserRoles} from "@/service/types/usernator";
16+
import LeaveGroupModal from "@/components/group/LeaveGroupModal";
1617

1718
const GroupDetailsPage = ({ params }: { params: { groupId: string } }) => {
1819
const id = parseInt(`${params.groupId}`, 10);
@@ -21,6 +22,7 @@ const GroupDetailsPage = ({ params }: { params: { groupId: string } }) => {
2122
const [group, refetch] = useClientQuery<GroupType>(() => api.getGroup(id));
2223
const { addGroup } = useSpotlightStage2();
2324
const [updateModalOpen, setUpdateModalOpen] = useState<boolean>(false);
25+
const [leaveModalOpen, setLeaveModalOpen] = useState<boolean>(false);
2426
const { t } = useTranslation("common");
2527

2628
useEffect(() => {
@@ -48,6 +50,9 @@ const GroupDetailsPage = ({ params }: { params: { groupId: string } }) => {
4850
{(isGranted(user, [UserRoles.Admin]) || group?.tutor.id === user?.id) && (
4951
<Button onClick={() => setUpdateModalOpen(true)}>{t('common:titles.update-group')}</Button>
5052
)}
53+
{isGranted(user, [UserRoles.Student]) && (
54+
<Button color="red" onClick={() => setLeaveModalOpen(true)}>{t('group:actions.leave')}</Button>
55+
)}
5156
</Group>
5257
{group === null ? (
5358
<CentralLoading />
@@ -61,6 +66,9 @@ const GroupDetailsPage = ({ params }: { params: { groupId: string } }) => {
6166
refetch={refetch}
6267
/>
6368
)}
69+
{leaveModalOpen && group && (
70+
<LeaveGroupModal groupId={group.id} onClose={() => setLeaveModalOpen(false)} />
71+
)}
6472
</Container>
6573
);
6674
};
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {Button, Group, Modal, Text} from "@mantine/core";
2+
import {useTranslation} from "react-i18next";
3+
import useApiServiceClient from "@/hooks/useApiServiceClient";
4+
import {useRouter} from "next/navigation";
5+
import {showNotification} from "@mantine/notifications";
6+
7+
8+
interface LeaveGroupModalProps {
9+
groupId: number;
10+
onClose: () => void;
11+
}
12+
13+
const LeaveGroupModal = ({groupId, onClose}: LeaveGroupModalProps) => {
14+
15+
const {t} = useTranslation(['group', 'common']);
16+
const api = useApiServiceClient();
17+
const router = useRouter();
18+
19+
const leave = async () => {
20+
try {
21+
await api.leaveGroup(groupId);
22+
showNotification({
23+
title: t('common:messages.success'),
24+
message: t('group:messages.left-group')
25+
});
26+
router.push('/my-groups');
27+
} catch (e: any) {
28+
showNotification({
29+
title: t('common:messages.error'),
30+
message: e?.message ?? "",
31+
})
32+
}
33+
}
34+
35+
return (
36+
<Modal opened onClose={onClose} title={t('group:actions.leave')}>
37+
<Text>
38+
{t('group:text.leave-group')}
39+
</Text>
40+
<Group mt={10}>
41+
<Button onClick={leave} color="red">{t("group:actions.leave")}</Button>
42+
<Button onClick={onClose} color="gray">
43+
{t("common:actions.cancel")}
44+
</Button>
45+
</Group>
46+
</Modal>
47+
);
48+
}
49+
50+
export default LeaveGroupModal;

web/public/locales/de/group.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,18 @@
2222
"request-join": "Beitritt anfragen",
2323
"create-group": "Gruppe erstellen",
2424
"enlist-user": "Nutzer einschreiben",
25-
"enlist": "Einschreiben"
25+
"enlist": "Einschreiben",
26+
"leave": "Verlassen"
2627
},
2728
"messages": {
2829
"join-request-created-title": "Beitrittsanfrage erstellt",
2930
"join-request-created-text": "Beitrittsanfrage für die Gruppe erstellt ",
3031
"unknown-user": "Unbekannter Nutzer",
3132
"enlisted-title": "Eingeschrieben",
32-
"enlisted-text": "Du wurdest erfolgreich eingeschrieben "
33+
"enlisted-text": "Du wurdest erfolgreich eingeschrieben ",
34+
"left-group": "Gruppe erfolgreich verlassen"
35+
},
36+
"text": {
37+
"leave-group": "Bist du sicher, dass du die Gruppe verlassen möchtest?"
3338
}
3439
}

web/public/locales/en/group.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,18 @@
2222
"request-join": "Request Join",
2323
"create-group": "Create group",
2424
"enlist-user": "Enlist user",
25-
"enlist": "Enlist"
25+
"enlist": "Enlist",
26+
"leave": "Leave"
2627
},
2728
"messages": {
2829
"join-request-created-title": "Join Request created",
2930
"join-request-created-text": "Created join request on group ",
3031
"unknown-user": "Unknown user",
3132
"enlisted-title": "Enlisted",
32-
"enlisted-text": "You successfully enlisted into group "
33+
"enlisted-text": "You successfully enlisted into group ",
34+
"left-group": "Left group successfully"
35+
},
36+
"text": {
37+
"leave-group": "Are you sure you want to leave the group?"
3338
}
3439
}

web/service/ApiService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,10 @@ class ApiService {
317317
await this.post<any>("/usernator/switch_tutor", {});
318318
}
319319

320+
public async leaveGroup(groupId: number): Promise<void> {
321+
await this.post<any>(`/tasky/groups/${groupId}/leave`, {});
322+
}
323+
320324
public async createOrUpdateCodeTests(
321325
groupId: number,
322326
assignmentId: number,

0 commit comments

Comments
 (0)