feat: Introduce data manager service with PocketBase authentication and professor dashboard.

This commit is contained in:
Elmar Kresse
2025-11-22 22:46:06 +01:00
parent 5f383f5638
commit f40558646c
7 changed files with 85 additions and 2 deletions

View File

@@ -19,12 +19,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, onMounted, onUnmounted } from "vue"; import { computed, ref, onMounted, onUnmounted } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useToast } from "primevue/usetoast";
import LocaleSwitcher from "./LocaleSwitcher.vue"; import LocaleSwitcher from "./LocaleSwitcher.vue";
import DarkModeSwitcher from "./DarkModeSwitcher.vue"; import DarkModeSwitcher from "./DarkModeSwitcher.vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { pb, login, logout } from "../service/pocketbase"; import { pb, login, logout } from "../service/pocketbase";
const { t } = useI18n({ useScope: "global" }); const { t } = useI18n({ useScope: "global" });
const toast = useToast();
const route = useRoute(); const route = useRoute();
@@ -98,6 +100,34 @@ const items = computed(() => {
return menuItems; return menuItems;
}); });
async function handleLogin() {
try {
await login();
toast.add({
severity: 'success',
summary: t('toast.loginSuccess'),
detail: t('toast.loginSuccessDetail'),
life: 3000
});
} catch (error: any) {
if (error?.message && error.message.includes('Login restricted')) {
toast.add({
severity: 'error',
summary: t('toast.loginError'),
detail: t('toast.loginRestricted'),
life: 5000
});
} else {
toast.add({
severity: 'error',
summary: t('toast.loginError'),
detail: error?.message || t('toast.loginErrorDetail'),
life: 5000
});
}
}
}
function handleDarkModeToggled(isDarkVar: boolean) { function handleDarkModeToggled(isDarkVar: boolean) {
// Do something with isDark value // Do something with isDark value
// For example, update the root isDark value // For example, update the root isDark value
@@ -173,7 +203,7 @@ function handleDarkModeToggled(isDarkVar: boolean) {
label="Login" label="Login"
icon="pi pi-sign-in" icon="pi pi-sign-in"
class="p-button-text" class="p-button-text"
@click="login" @click="handleLogin"
/> />
<Button <Button
v-else v-else

View File

@@ -9,6 +9,13 @@
"description": "Dein individueller Stundenplan mit Sportevents und Prüfungen. Finde kommende Veranstaltungen oder freie Räume zum Lernen und Arbeiten.", "description": "Dein individueller Stundenplan mit Sportevents und Prüfungen. Finde kommende Veranstaltungen oder freie Räume zum Lernen und Arbeiten.",
"english": "Englisch", "english": "Englisch",
"german": "Deutsch", "german": "Deutsch",
"toast": {
"loginError": "Login fehlgeschlagen",
"loginRestricted": "Login nur für Mitarbeitende der HTWK Leipzig.",
"loginSuccess": "Login erfolgreich",
"loginSuccessDetail": "Du kannst nun deinen Kalender erstellen.",
"loginErrorDetail": "Fehler beim Login. Bitte versuche es erneut."
},
"courseSelection": { "courseSelection": {
"headline": "Willkommen beim HTWKalender", "headline": "Willkommen beim HTWKalender",
"winterSemester": "Wintersemester", "winterSemester": "Wintersemester",

View File

@@ -9,6 +9,13 @@
"description": "Your individual timetable with sports events and exams. Find upcoming events or free rooms for studying and working.", "description": "Your individual timetable with sports events and exams. Find upcoming events or free rooms for studying and working.",
"english": "English", "english": "English",
"german": "German", "german": "German",
"toast": {
"loginError": "login failed",
"loginRestricted": "login restricted to htwk employees",
"loginSuccess": "login successful",
"loginSuccessDetail": "you can now create your calendar.",
"loginErrorDetail": "login failed. please try again."
},
"courseSelection": { "courseSelection": {
"headline": "welcome to HTWKalender", "headline": "welcome to HTWKalender",
"winterSemester": "winter semester", "winterSemester": "winter semester",

View File

@@ -19,7 +19,7 @@ export const pb = new PocketBase("/");
// Production: https://cal.htwk-leipzig.de/callback // Production: https://cal.htwk-leipzig.de/callback
export const login = async () => { export const login = async () => {
await pb.collection('users').authWithOAuth2({ provider: 'oidc' }); return await pb.collection('users').authWithOAuth2({ provider: 'oidc', scopes: ['openid', 'email', 'profile'] });
} }
export const logout = () => { export const logout = () => {

View File

@@ -68,6 +68,7 @@ onMounted(async () => {
return (b.confidenceScore || 0) - (a.confidenceScore || 0); // Descending order return (b.confidenceScore || 0) - (a.confidenceScore || 0); // Descending order
}); });
// Clear temporary entries and set actual modules
modules.value = sortedModules; modules.value = sortedModules;
// Pre-select modules with confidence score >= 0.7 (good match or better) // Pre-select modules with confidence score >= 0.7 (good match or better)
@@ -77,12 +78,15 @@ onMounted(async () => {
} catch (error: unknown) { } catch (error: unknown) {
console.error("Failed to fetch modules", error); console.error("Failed to fetch modules", error);
const err = error as { status?: number; message?: string }; const err = error as { status?: number; message?: string };
// Clear temporary entries on error
modules.value = [];
// If unauthorized, redirect to home // If unauthorized, redirect to home
if (err.status === 401 || err.message?.includes("401")) { if (err.status === 401 || err.message?.includes("401")) {
pb.authStore.clear(); pb.authStore.clear();
router.push("/"); router.push("/");
} }
} finally { } finally {
// Always disable loading animation
loading.value = false; loading.value = false;
} }
}); });

View File

@@ -57,6 +57,7 @@ func setupApp() *pocketbase.PocketBase {
}) })
service.AddRoutes(services) service.AddRoutes(services)
service.AddSchedules(services) service.AddSchedules(services)
service.AddHooks(app)
return app return app
} }

View File

@@ -0,0 +1,34 @@
package service
import (
"strings"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
)
func AddHooks(app *pocketbase.PocketBase) {
app.OnRecordAuthWithOAuth2Request("users").BindFunc(func(e *core.RecordAuthWithOAuth2RequestEvent) error {
email := e.OAuth2User.Email
// If email is not in the main field, try to extract it from RawUser
if email == "" {
if rawEmail, ok := e.OAuth2User.RawUser["email"].(string); ok {
email = rawEmail
// Explicitly set the email on the OAuth2User so PocketBase uses it
e.OAuth2User.Email = rawEmail
}
}
if email == "" {
return apis.NewBadRequestError("No email received from OAuth2 provider. Please ensure your account has an email address and the 'email' scope is granted.", nil)
}
// Restrict login to @htwk-leipzig.de employees only (not students)
if !strings.HasSuffix(email, "@htwk-leipzig.de") {
return apis.NewBadRequestError("Login restricted to @htwk-leipzig.de emails. Students (@stud.htwk-leipzig.de) are not allowed.", nil)
}
return e.Next()
})
}