Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions usernator/internal/controllers/switch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package controllers

import (
"github.com/gofiber/fiber/v2"
"usernator/internal/models"
"usernator/internal/shared"
"usernator/internal/util"
)

// / Endpoint to switch to tutor
func SwitchToTutor(ctx *fiber.Ctx) error {
currentUser, ok := ctx.Locals("currentUser").(*models.User)
if !ok {
return fiber.NewError(fiber.StatusUnauthorized, "You need to be logged in")
}
if !util.ArrayContains(currentUser.Roles, "ROLE_STUDENT") {
return fiber.NewError(fiber.StatusForbidden, "You need to be a student in order to switch to tutor account")
}

shared.Database.Exec("UPDATE users SET roles = '{ROLE_TUTOR}' WHERE id = ?", currentUser.ID)
ctx.ClearCookie("session")
return ctx.SendStatus(fiber.StatusOK)
}
1 change: 1 addition & 0 deletions usernator/internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func CreateServer(configPath string) *fiber.App {
app.Post("/register", controllers.RegisterUser)
app.Post("/login", controllers.LoginUser)
app.Post("/create_tutor", controllers.CreateTutor)
app.Post("/switch_tutor", controllers.SwitchToTutor)

// These endpoints are currently disabled and therefore not accessed
//app.Post("/reset_password", controllers.ResetPassword)
Expand Down
11 changes: 11 additions & 0 deletions usernator/internal/util/array.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package util

// / Checks if an array contains a specific value
func ArrayContains(a []string, x string) bool {
for _, n := range a {
if x == n {
return true
}
}
return false
}
65 changes: 41 additions & 24 deletions web/app/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
"use client";

import {
Button,
Combobox,
Container,
Input,
InputBase,
MantineColorScheme,
Stack,
Title,
useCombobox,
useMantineColorScheme,
} from "@mantine/core";
import { useTranslation } from "react-i18next";
import {useTranslation} from "react-i18next";
import useCurrentUser from "@/hooks/useCurrentUser";
import {isGranted} from "@/service/auth";
import {UserRoles} from "@/service/types/usernator";
import {useState} from "react";
import SwitchToTutorModal from "@/components/SwitchToTutorModal";

const schemes = ["light", "dark", "auto"];

Expand All @@ -22,6 +29,8 @@ const SettingsPage = () => {
onDropdownClose: () => combobox.resetSelectedOption(),
});
const { t } = useTranslation("common");
const {user} = useCurrentUser();
const [switchModalOpen, setSwitchModalOpen] = useState<boolean>(false);

const options = schemes.map((item) => (
<Combobox.Option value={item} key={item}>
Expand All @@ -32,30 +41,38 @@ const SettingsPage = () => {
return (
<Container fluid>
<Title>{t("settings.settings")}</Title>
<Combobox
store={combobox}
withinPortal={false}
onOptionSubmit={(val) => {
setColorScheme(val as MantineColorScheme);
}}
>
<Combobox.Target>
<InputBase
component="button"
type="button"
pointer
rightSection={<Combobox.Chevron />}
onClick={() => combobox.toggleDropdown()}
rightSectionPointerEvents="none"
>
{colorScheme || <Input.Placeholder>Pick value</Input.Placeholder>}
</InputBase>
</Combobox.Target>
<Stack gap={25} mt={10}>
<Combobox
store={combobox}
withinPortal={false}
onOptionSubmit={(val) => {
setColorScheme(val as MantineColorScheme);
}}
>
<Combobox.Target>
<InputBase
component="button"
type="button"
pointer
rightSection={<Combobox.Chevron />}
onClick={() => combobox.toggleDropdown()}
rightSectionPointerEvents="none"
>
{colorScheme || <Input.Placeholder>Pick value</Input.Placeholder>}
</InputBase>
</Combobox.Target>

<Combobox.Dropdown>
<Combobox.Options>{options}</Combobox.Options>
</Combobox.Dropdown>
</Combobox>
<Combobox.Dropdown>
<Combobox.Options>{options}</Combobox.Options>
</Combobox.Dropdown>
</Combobox>
{isGranted(user, [UserRoles.Student]) && (
<Button variant="gradient" w="25%" onClick={() => setSwitchModalOpen(true)}>{t('common:titles.switch-to-tutor')}</Button>
)}
</Stack>
{switchModalOpen && (
<SwitchToTutorModal onClose={() => setSwitchModalOpen(false)} />
)}
</Container>
);
};
Expand Down
43 changes: 43 additions & 0 deletions web/components/SwitchToTutorModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {Button, Group, Modal, Text} from "@mantine/core";
import {useTranslation} from "react-i18next";
import useApiServiceClient from "@/hooks/useApiServiceClient";
import {showNotification} from "@mantine/notifications";

interface SwitchToTutorModalProps {
onClose: () => void;
}

const SwitchToTutorModal = ({onClose}: SwitchToTutorModalProps) => {

const {t} = useTranslation('common');
const api = useApiServiceClient();

const switchAccount = async () => {
try {
await api.switchToTutorAccount();
document.cookie = 'session=""';
window.location.reload();
} catch (e: any) {
showNotification({
title: t('common:messages.error'),
message: e?.message ?? "" ,
})
}
}

return (
<Modal opened onClose={onClose}>
<Text>
{t('common:messages.switch-to-tutor-text')}
</Text>
<Group mt={10}>
<Button onClick={switchAccount}>{t("actions.save")}</Button>
<Button onClick={onClose} color="gray">
{t("actions.cancel")}
</Button>
</Group>
</Modal>
);
}

export default SwitchToTutorModal;
6 changes: 4 additions & 2 deletions web/public/locales/de/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"log-out": "Abmelden",
"create-group": "Gruppe erstellen",
"update-group": "Gruppe aktualisieren",
"notifications": "Benachrichtigungen"
"notifications": "Benachrichtigungen",
"switch-to-tutor": "Zu einem Tutoren-Konto wechseln"
},
"cols": {
"id": "ID",
Expand Down Expand Up @@ -57,7 +58,8 @@
"created-report": "Bericht erstellt",
"no-files-selected": "Keine Dateien ausgewählt",
"copied-code": "Code kopiert!",
"rejected-files": "Abgelehnte Dateien"
"rejected-files": "Abgelehnte Dateien",
"switch-to-tutor-text": "Bist du sicher, dass du zu einem Tutor-Konto wechseln möchtest? Das bedeutet, dass du keine Aufgaben mehr bearbeiten kannst. Dsfür benötigst du einen weiteren Account. Also Tutor kannst du nur Aufgaben erstellen und verwalten innerhalb deiner eigenen Gruppen. Sei dir also deiner Entscheidung bewusst"
},
"errors": {
"title-empty": "Der Titel darf nicht leer sein",
Expand Down
6 changes: 4 additions & 2 deletions web/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"log-out": "Log out",
"create-group": "Create group",
"update-group": "Update group",
"notifications": "Notifications"
"notifications": "Notifications",
"switch-to-tutor": "Switch to tutor account"
},
"cols": {
"id": "ID",
Expand Down Expand Up @@ -57,7 +58,8 @@
"created-report": "Created report",
"no-files-selected": "No files selected",
"copied-code": "Copied code!",
"rejected-files": "Rejected files"
"rejected-files": "Rejected files",
"switch-to-tutor-text": "Are you sure you want to switch to a tutor account? This means you will no longer be able to work on tasks. For that, you would need a separate account. As a tutor, you can only create and manage tasks within your own groups. Please be sure of your decision."
},
"errors": {
"title-empty": "The title should not be empty",
Expand Down
4 changes: 4 additions & 0 deletions web/service/ApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,10 @@ class ApiService {
await this.post<any>(`/tasky/groups/${groupId}/enlist/${userId}`, {});
}

public async switchToTutorAccount(): Promise<void> {
await this.post<any>("/usernator/switch_tutor", {});
}

public async createOrUpdateCodeTests(
groupId: number,
assignmentId: number,
Expand Down