Commit 2f300868 authored by Patrick Ade's avatar Patrick Ade
Browse files

Refactor authentication and registration processes to use Axios; update API...

Refactor authentication and registration processes to use Axios; update API endpoints and improve error handling
parent 07514b02
...@@ -28,7 +28,7 @@ export const useAuthStore = defineStore('auth', { ...@@ -28,7 +28,7 @@ export const useAuthStore = defineStore('auth', {
actions: { actions: {
async login(username: string, password: string, router: Router | null = null): Promise<void> { async login(username: string, password: string, router: Router | null = null): Promise<void> {
try { try {
const response = await api.post('/auth/login', { const response = await api.post('/login', {
username, username,
password, password,
}) })
...@@ -64,7 +64,7 @@ export const useAuthStore = defineStore('auth', { ...@@ -64,7 +64,7 @@ export const useAuthStore = defineStore('auth', {
} }
try { try {
const response = await api.get('/auth/me') const response = await api.get('/user')
this.user = response.data this.user = response.data
this.isAuthenticated = true this.isAuthenticated = true
} catch (error) { } catch (error) {
......
...@@ -2,32 +2,33 @@ ...@@ -2,32 +2,33 @@
import { ref } from 'vue' import { ref } from 'vue'
import BauCard from '../components/BauCard.vue' import BauCard from '../components/BauCard.vue'
import RoomCard from '../components/RoomCard.vue' import RoomCard from '../components/RoomCard.vue'
import api from '@/services/apiClient'
const buildings = [ const buildings = [
{ {
id: '1', id: '1',
title: 'Bau 1', title: 'Bau 1',
description: 'Zentralgebäude mit Laboren', description: 'Zentralgebäude mit Laboren',
image: new URL('@/assets/bau-1.png', import.meta.url).href, image: new URL('@/assets/bau-1.png', import.meta.url).href,
}, },
{ {
id: '2', id: '2',
title: 'Bau 2', title: 'Bau 2',
description: 'Informatik und IT-Räume', description: 'Informatik und IT-Räume',
image: new URL('@/assets/bau-2.png', import.meta.url).href, image: new URL('@/assets/bau-2.png', import.meta.url).href,
}, },
{ {
id: '3', id: '3',
title: 'Bau 3', title: 'Bau 3',
description: 'Informatik und IT-Räume', description: 'Informatik und IT-Räume',
image: new URL('@/assets/bau-3.png', import.meta.url).href, image: new URL('@/assets/bau-3.png', import.meta.url).href,
}, },
{ {
id: '8', id: '8',
title: 'Bau 8', title: 'Bau 8',
description: 'Informatik und IT-Räume', description: 'Informatik und IT-Räume',
image: new URL('@/assets/bau-8.png', import.meta.url).href, image: new URL('@/assets/bau-8.png', import.meta.url).href,
} },
] ]
const selectedBuildingId = ref<string | null>(null) const selectedBuildingId = ref<string | null>(null)
...@@ -36,74 +37,73 @@ const isLoading = ref(false) ...@@ -36,74 +37,73 @@ const isLoading = ref(false)
const error = ref<string | null>(null) const error = ref<string | null>(null)
async function loadRooms(buildingId: string) { async function loadRooms(buildingId: string) {
selectedBuildingId.value = buildingId selectedBuildingId.value = buildingId
rooms.value = [] rooms.value = []
isLoading.value = true isLoading.value = true
error.value = null error.value = null
try { try {
const response = await fetch(`http://localhost:8000/api/get_rooms_from_building?building=${buildingId}`) const response = await api.get('/get_rooms_from_building', {
const data = await response.json() params: { building: buildingId },
})
if (response.ok) { rooms.value = response.data.rooms
rooms.value = data.rooms } catch (err: any) {
} else { error.value = err.response?.data?.detail || err.message || 'Fehler beim Laden der Räume'
throw new Error(data.detail || 'Fehler beim Laden der Räume') } finally {
isLoading.value = false
} }
} catch (err: any) {
error.value = err.message
} finally {
isLoading.value = false
}
} }
</script> </script>
<template> <template>
<div class="layout"> <div class="layout">
<!-- Linke Spalte: Gebäude --> <!-- Linke Spalte: Gebäude -->
<div class="building-grid"> <div class="building-grid">
<BauCard <BauCard
v-for="bau in buildings" v-for="bau in buildings"
:key="bau.id" :key="bau.id"
:building-id="bau.id" :building-id="bau.id"
:title="bau.title" :title="bau.title"
:description="bau.description" :description="bau.description"
:image="bau.image" :image="bau.image"
@select="loadRooms" @select="loadRooms"
/> />
</div> </div>
<!-- Rechte Spalte: Räume --> <!-- Rechte Spalte: Räume -->
<div class="room-panel"> <div class="room-panel">
<h3 v-if="selectedBuildingId">Räume in Bau {{ selectedBuildingId }}</h3> <h3 v-if="selectedBuildingId">Räume in Bau {{ selectedBuildingId }}</h3>
<div v-if="isLoading">Lade Räume...</div> <div v-if="isLoading">Lade Räume...</div>
<div v-else-if="error">{{ error }}</div> <div v-else-if="error">{{ error }}</div>
<div v-else> <div v-else>
<RoomCard v-for="room in rooms" :key="room" :room="room" /> <RoomCard v-for="room in rooms" :key="room" :room="room" />
</div> </div>
</div>
</div> </div>
</div>
</template> </template>
<style scoped> <style scoped>
.layout { .layout {
display: flex; display: flex;
padding: 2rem; padding: 2rem;
gap: 2rem; gap: 2rem;
align-items: flex-start; /* wichtige Ergänzung */ align-items: flex-start;
/* wichtige Ergänzung */
} }
.building-grid { .building-grid {
display: flex; display: flex;
flex-direction: column; /* Gebäude untereinander */ flex-direction: column;
gap: 1.5rem; /* Gebäude untereinander */
flex-shrink: 0; gap: 1.5rem;
flex-shrink: 0;
} }
.room-panel { .room-panel {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
flex-grow: 1; flex-grow: 1;
} }
</style> </style>
<template>
<div class="chart-view">
<h2>Daten für Raum: {{ room }}</h2>
<div class="time-form">
<label>
Start:
<input class="datetime" type="datetime-local" v-model="start" />
</label>
<label>
Stop:
<input class="datetime" type="datetime-local" v-model="stop" />
</label>
<button @click="loadData">Zeitraum aktualisieren</button>
<button @click="loadDataWithDefaultWindow" class="default-btn">Zeige die letzte Woche</button>
</div>
<div class="chart-grid">
<ChartCard
title="CO₂ Verlauf"
:labels="displayLabels"
:data="co2Data"
borderColor="#ff6384"
/>
<ChartCard
title="Temperatur Verlauf"
:labels="displayLabels"
:data="temperatureData"
borderColor="#36a2eb"
/>
<ChartCard
title="Luftfeuchtigkeit Verlauf"
:labels="displayLabels"
:data="humidityData"
borderColor="#4bc0c0"
/>
</div>
</div>
</template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, onMounted, computed } from 'vue' import { defineComponent, ref, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import ChartCard from '../components/ChartCard.vue' import ChartCard from '../components/ChartCard.vue'
import api from '@/services/apiClient' // Axios instance
export default defineComponent({ export default defineComponent({
name: 'ChartView', name: 'ChartView',
components: { ChartCard }, components: { ChartCard },
setup() { setup() {
const route = useRoute() const route = useRoute()
const room = decodeURIComponent(route.params.room as string) const room = decodeURIComponent(route.params.room as string)
const labels = ref<string[]>([]) const labels = ref<string[]>([])
const co2Data = ref<number[]>([]) const co2Data = ref<number[]>([])
const temperatureData = ref<number[]>([]) const temperatureData = ref<number[]>([])
const humidityData = ref<number[]>([]) const humidityData = ref<number[]>([])
const start = ref('') const start = ref('')
const stop = ref('') const stop = ref('')
// Computed property for displaying max 6 labels const displayLabels = computed(() => {
const displayLabels = computed(() => { const total = labels.value.length
const total = labels.value.length if (total <= 6) return labels.value
if (total <= 6) return labels.value const result: string[] = Array(total).fill('')
const result: string[] = [] for (let i = 0; i < 6; i++) {
for (let i = 0; i < total; i++) { const idx = Math.round((i * (total - 1)) / 5)
result.push('') result[idx] = labels.value[idx]
} }
for (let i = 0; i < 6; i++) { return result
const idx = Math.round(i * (total - 1) / 5) })
result[idx] = labels.value[idx]
} function toISOStringSafe(value: string): string {
return result try {
}) return new Date(value).toISOString()
} catch {
function toISOStringSafe(value: string): string { return ''
try { }
return new Date(value).toISOString() }
} catch {
return '' function setDefaultTimeWindow() {
} const now = new Date()
} const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
function setDefaultTimeWindow() { const formatForInput = (date: Date) => {
const now = new Date() const year = date.getFullYear()
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000) const month = ('0' + (date.getMonth() + 1)).slice(-2)
const day = ('0' + date.getDate()).slice(-2)
// Format dates properly for datetime-local inputs const hours = ('0' + date.getHours()).slice(-2)
const formatForInput = (date: Date) => { const minutes = ('0' + date.getMinutes()).slice(-2)
const year = date.getFullYear() return `${year}-${month}-${day}T${hours}:${minutes}`
const month = ('0' + (date.getMonth() + 1)).slice(-2) }
const day = ('0' + date.getDate()).slice(-2)
const hours = ('0' + date.getHours()).slice(-2) start.value = formatForInput(weekAgo)
const minutes = ('0' + date.getMinutes()).slice(-2) stop.value = formatForInput(now)
return `${year}-${month}-${day}T${hours}:${minutes}` }
}
async function loadData() {
start.value = formatForInput(weekAgo) if (!start.value || !stop.value) return
stop.value = formatForInput(now)
} const startISO = toISOStringSafe(start.value)
const stopISO = toISOStringSafe(stop.value)
async function loadData() {
if (!start.value || !stop.value) return if (new Date(startISO) >= new Date(stopISO)) {
alert('❌ Der Startzeitpunkt muss vor dem Endzeitpunkt liegen.')
const startISO = toISOStringSafe(start.value) return
const stopISO = toISOStringSafe(stop.value) }
if (new Date(startISO) >= new Date(stopISO)) { try {
alert('❌ Der Startzeitpunkt muss vor dem Endzeitpunkt liegen.') const response = await api.get('/room_data_range', {
return params: {
} room,
start: startISO,
const url = `http://localhost:8000/api/room_data_range?room=${encodeURIComponent( stop: stopISO,
room },
)}&start=${startISO}&stop=${stopISO}` })
try { const entries = Object.entries(response.data.data).sort(
const response = await fetch(url) ([a], [b]) => new Date(a).getTime() - new Date(b).getTime()
if (!response.ok) throw new Error('Fehler beim Laden') )
const json = await response.json() labels.value = entries.map(([timestamp]) =>
const entries = Object.entries(json.data).sort( new Date(timestamp).toLocaleString([], {
([a], [b]) => new Date(a).getTime() - new Date(b).getTime() day: '2-digit',
) month: '2-digit',
hour: '2-digit',
labels.value = entries.map(([timestamp]) => minute: '2-digit',
new Date(timestamp).toLocaleString([], { })
day: '2-digit', )
month: '2-digit', co2Data.value = entries.map(([, values]: any) => Number(values.co2))
hour: '2-digit', temperatureData.value = entries.map(([, values]: any) => Number(values.temperature))
minute: '2-digit', humidityData.value = entries.map(([, values]: any) => Number(values.humidity))
}) } catch (err: any) {
) console.error('❌ Fehler beim Laden der Daten:', err)
co2Data.value = entries.map(([, values]: any) => Number(values.co2)) alert('Fehler beim Laden der Daten')
temperatureData.value = entries.map(([, values]: any) => }
Number(values.temperature) }
)
humidityData.value = entries.map(([, values]: any) => async function loadDataWithDefaultWindow() {
Number(values.humidity) setDefaultTimeWindow()
) await loadData()
} catch (err) { }
console.error('❌ Fehler beim Laden der Daten:', err)
} onMounted(async () => {
} await loadDataWithDefaultWindow()
})
// Function to load data with default last week window
async function loadDataWithDefaultWindow() { return {
setDefaultTimeWindow() room,
await loadData() start,
} stop,
labels,
onMounted(async () => { displayLabels,
await loadDataWithDefaultWindow() co2Data,
}) temperatureData,
humidityData,
return { loadData,
room, loadDataWithDefaultWindow,
start, }
stop, },
labels,
displayLabels,
co2Data,
temperatureData,
humidityData,
loadData,
loadDataWithDefaultWindow,
}
},
}) })
</script> </script>
<style scoped>
.chart-view {
padding: 2rem;
color: #1a1a1a;
}
.time-form {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
align-items: center;
flex-wrap: wrap;
}
.time-form input {
padding: 0.5rem;
font-size: 1rem;
border-radius: 5px;
}
.time-form button {
background: #007bff;
color: white;
padding: 0.6rem 1.2rem;
border: none;
border-radius: 4px;
font-weight: bold;
cursor: pointer;
transition: background-color 0.2s ease;
}
.time-form button:hover {
background-color: #0056b3;
}
.time-form .default-btn {
background: #28a745;
}
.time-form .default-btn:hover {
background-color: #218838;
}
.chart-grid {
display: flex;
flex-wrap: nowrap;
gap: 2rem;
justify-content: flex-start;
flex-direction: row;
}
</style>
<script lang="ts"> <script lang="ts">
import { getCSRFToken } from '../stores/auth' import api from '@/services/apiClient' // Your axios instance
export default { export default {
data() { data() {
...@@ -13,29 +13,23 @@ export default { ...@@ -13,29 +13,23 @@ export default {
methods: { methods: {
async register() { async register() {
try { try {
const response = await fetch('http://localhost:8000/api/register', { const response = await api.post('/register', {
method: 'POST', username: this.email,
headers: { password: this.password,
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken(),
},
body: JSON.stringify({
email: this.email,
password: this.password,
}),
credentials: 'include',
}) })
const data = await response.json()
if (response.ok) { if (response.status === 200) {
this.success = 'Registration successful! Please log in.' this.success = 'Registration successful! Please log in.'
setTimeout(() => { setTimeout(() => {
this.$router.push('/login') this.$router.push('/login')
}, 1000) }, 1000)
} else { } else {
this.error = data.error || 'Registration failed' this.error = response.data.error || 'Registration failed'
} }
} catch (err) { } catch (err: any) {
this.error = 'An error occurred during registration: ' + err this.error =
err.response?.data?.error ||
'An error occurred during registration: ' + err.message
} }
}, },
}, },
...@@ -43,127 +37,128 @@ export default { ...@@ -43,127 +37,128 @@ export default {
</script> </script>
<template> <template>
<div class="register-container"> <div class="register-container">
<div class="register-card"> <div class="register-card">
<h1>Registrieren</h1> <h1>Registrieren</h1>
<p class="intro">Erstelle ein Konto, um Zugriff auf Sensor- und Raumdaten zu erhalten.</p> <p class="intro">
Erstelle ein Konto, um Zugriff auf Sensor- und Raumdaten zu erhalten.
<form @submit.prevent="register"> </p>
<div class="form-group">
<label for="email">Email</label> <form @submit.prevent="register">
<input <div class="form-group">
v-model="email" <label for="email">Email</label>
id="email" <input
type="email" v-model="email"
required id="email"
placeholder="z.B. max@ht.de" type="email"
/> required
</div> placeholder="z.B. max@ht.de"
/>
<div class="form-group"> </div>
<label for="password">Passwort</label>
<input <div class="form-group">
v-model="password" <label for="password">Passwort</label>
id="password" <input
type="password" v-model="password"
required id="password"
placeholder="••••••••" type="password"
/> required
placeholder="••••••••"
/>
</div>
<button type="submit">Registrieren</button>
<p v-if="error" class="error">{{ error }}</p>
<p v-if="success" class="success">{{ success }}</p>
</form>
</div> </div>
<button type="submit">Registrieren</button>
<p v-if="error" class="error">{{ error }}</p>
<p v-if="success" class="success">{{ success }}</p>
</form>
</div> </div>
</div>
</template> </template>
<style scoped> <style scoped>
.register-container { .register-container {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
min-height: 80vh; min-height: 80vh;
} }
.register-card { .register-card {
background-color: #f9f9f9; background-color: #f9f9f9;
padding: 2.5rem; padding: 2.5rem;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 2px 14px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 14px rgba(0, 0, 0, 0.1);
width: 100%; width: 100%;
max-width: 420px; max-width: 420px;
font-family: 'Segoe UI', sans-serif; font-family: 'Segoe UI', sans-serif;
} }
h1 { h1 {
text-align: center; text-align: center;
color: #333; color: #333;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.intro { .intro {
text-align: center; text-align: center;
font-size: 0.95rem; font-size: 0.95rem;
color: #666; color: #666;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.form-group { .form-group {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
label { label {
display: block; display: block;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
font-weight: 600; font-weight: 600;
color: #444; color: #444;
} }
input { input {
width: 100%; width: 100%;
padding: 0.6rem 0.75rem; padding: 0.6rem 0.75rem;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 6px; border-radius: 6px;
font-size: 1rem; font-size: 1rem;
} }
input:focus { input:focus {
border-color: #007bff; border-color: #007bff;
outline: none; outline: none;
} }
button { button {
width: 100%; width: 100%;
padding: 0.75rem; padding: 0.75rem;
background-color: #007bff; background-color: #007bff;
color: white; color: white;
font-weight: bold; font-weight: bold;
border: none; border: none;
border-radius: 6px; border-radius: 6px;
font-size: 1rem; font-size: 1rem;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
} }
button:hover { button:hover {
background-color: #0056b3; background-color: #0056b3;
} }
.error { .error {
margin-top: 1rem; margin-top: 1rem;
color: #dc3545; color: #dc3545;
text-align: center; text-align: center;
font-weight: 500; font-weight: 500;
} }
.success { .success {
margin-top: 1rem; margin-top: 1rem;
color: #28a745; color: #28a745;
text-align: center; text-align: center;
font-weight: 500; font-weight: 500;
} }
</style> </style>
\ No newline at end of file
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { getCSRFToken } from '../stores/auth'
import RoomCard from '../components/RoomCard.vue' import RoomCard from '../components/RoomCard.vue'
import api from '@/services/apiClient' // Custom Axios instance
const rooms = ref<string[]>([]) const rooms = ref<string[]>([])
const error = ref<string | null>(null) const error = ref<string | null>(null)
const fetchRooms = async () => { const fetchRooms = async () => {
try { try {
const response = await fetch('http://localhost:8000/api/get_rooms', { const response = await api.get('/get_rooms')
headers: {
'Content-Type': 'application/json', if (response.data.rooms) {
'X-CSRFToken': getCSRFToken(), rooms.value = response.data.rooms
}, } else {
credentials: 'include', error.value = 'Fehler beim Laden der Räume.'
}) }
} catch (err: any) {
const data = await response.json() error.value = err.response?.data?.detail || 'Serverfehler: ' + err.message
if (response.ok && data.rooms) {
rooms.value = data.rooms
} else {
error.value = 'Fehler beim Laden der Räume.'
} }
} catch (err) {
error.value = 'Serverfehler: ' + err
}
} }
onMounted(fetchRooms) onMounted(fetchRooms)
</script> </script>
<template> <template>
<div class="room-container"> <div class="room-container">
<h2>Räume</h2> <h2>Räume</h2>
<p v-if="error" class="error">{{ error }}</p> <p v-if="error" class="error">{{ error }}</p>
<div class="card-grid"> <div class="card-grid">
<RoomCard v-for="room in rooms" :key="room" :room="room" /> <RoomCard v-for="room in rooms" :key="room" :room="room" />
</div>
</div> </div>
</div>
</template> </template>
<style scoped> <style scoped>
.room-container { .room-container {
padding: 2rem; padding: 2rem;
} }
.card-grid { .card-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem; gap: 1rem;
margin-top: 1rem; margin-top: 1rem;
} }
.error { .error {
color: red; color: red;
} }
</style> </style>
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment