Commit e6e9197f authored by Patrick Ade's avatar Patrick Ade
Browse files

Implement authentication store using Pinia; create auth store with login,...

Implement authentication store using Pinia; create auth store with login, logout, and user fetching functionalities.

Update main entry point to reflect new store structure; change import path for auth store.

Add TypeScript support for Vue components; create shims-vue.d.ts for .vue file declarations.

Create Login and Register views with authentication logic; implement user login and registration forms with error handling.

Refactor HomeView to utilize authentication store; display user information and logout functionality based on authentication state.
parent 71123f58
Showing with 769 additions and 450 deletions
+769 -450
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
INFLUXDB_URL=http://influxdb2:8086 INFLUXDB_URL=http://influxdb2:8086
INFLUXDB_ORG=docs INFLUXDB_ORG=docs
INFLUXDB_BUCKET=co2-test INFLUXDB_BUCKET=co2-test
INFLUXDB_TOKEN=1xa5lLACRZDYsvinhABndZ8GGzBY7-gTQsAf309c0aTnPPtBxixPEEOPuXLmkTxUKy8golKae6fsrh1wD4SL0A== INFLUXDB_TOKEN=xHpy7Vdt2_3hNb9DOpc1DHO6UkH19R58_okbCORz5vhDvYaiYJN2e0tl2H4rQlym6w5dCbRweeMWL00DU2ymNw==
# MQTT config # MQTT config
MQTT_BROKER_URL=mosquitto-broker MQTT_BROKER_URL=mosquitto-broker
MQTT_TOPIC="co2/#" MQTT_TOPIC="co2/#"
\ No newline at end of file
...@@ -71,6 +71,7 @@ def register(request): ...@@ -71,6 +71,7 @@ def register(request):
errors = form.errors.as_json() errors = form.errors.as_json()
return JsonResponse({"error": errors}, status=400) return JsonResponse({"error": errors}, status=400)
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def room_data_range(request): def room_data_range(request):
try: try:
...@@ -79,7 +80,9 @@ def room_data_range(request): ...@@ -79,7 +80,9 @@ def room_data_range(request):
stop = request.GET.get("stop", "now()") stop = request.GET.get("stop", "now()")
if not room: if not room:
return JsonResponse({"error": "Missing 'room' parameter"}, status=400) return JsonResponse(
{"error": "Missing 'room' parameter"}, status=400
)
load_dotenv() load_dotenv()
client = InfluxDBHelper( client = InfluxDBHelper(
...@@ -93,21 +96,23 @@ def room_data_range(request): ...@@ -93,21 +96,23 @@ def room_data_range(request):
results = [] results = []
for table in tables: for table in tables:
for record in table.records: for record in table.records:
results.append({ results.append(
{
"time": str(record.get_time()), "time": str(record.get_time()),
"field": record.get_field(), "field": record.get_field(),
"value": record.get_value(), "value": record.get_value(),
}) }
)
return JsonResponse({"room": room, "data": results}, status = 200) return JsonResponse({"room": room, "data": results}, status=200)
except json.JSONDecodeError: except json.JSONDecodeError:
return JsonResponse( return JsonResponse(
{"success": False, "message": "Invalid JSON"}, status=400 {"success": False, "message": "Invalid JSON"}, status=400
) )
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def get_rooms(request): def get_rooms(request):
client = InfluxDBHelper( client = InfluxDBHelper(
url=os.getenv("INFLUXDB_URL"), url=os.getenv("INFLUXDB_URL"),
token=os.getenv("INFLUXDB_TOKEN"), token=os.getenv("INFLUXDB_TOKEN"),
...@@ -123,6 +128,7 @@ def get_rooms(request): ...@@ -123,6 +128,7 @@ def get_rooms(request):
return JsonResponse({"rooms": sorted(rooms)}) return JsonResponse({"rooms": sorted(rooms)})
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def room_data_csv_view(request): def room_data_csv_view(request):
room = request.GET.get("room") room = request.GET.get("room")
...@@ -148,14 +154,17 @@ def room_data_csv_view(request): ...@@ -148,14 +154,17 @@ def room_data_csv_view(request):
for row in reader: for row in reader:
# optional: nur Datenzeilen filtern # optional: nur Datenzeilen filtern
if row.get("_field") and row.get("_value"): if row.get("_field") and row.get("_value"):
results.append({ results.append(
{
"time": row["_time"], "time": row["_time"],
"field": row["_field"], "field": row["_field"],
"value": row["_value"], "value": row["_value"],
}) }
)
return JsonResponse({"room": room, "data": results}) return JsonResponse({"room": room, "data": results})
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def room_data_csv_download(request): def room_data_csv_download(request):
room = request.GET.get("room") room = request.GET.get("room")
...@@ -177,4 +186,3 @@ def room_data_csv_download(request): ...@@ -177,4 +186,3 @@ def room_data_csv_download(request):
response = HttpResponse(csv_data, content_type="text/csv") response = HttpResponse(csv_data, content_type="text/csv")
response["Content-Disposition"] = f'attachment; filename="{room}_data.csv"' response["Content-Disposition"] = f'attachment; filename="{room}_data.csv"'
return response return response
...@@ -43,9 +43,22 @@ INSTALLED_APPS = [ ...@@ -43,9 +43,22 @@ INSTALLED_APPS = [
"corsheaders", "corsheaders",
] ]
CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_CREDENTIALS = True
CORS_ALLOWED_ORIGINS = ["http://localhost:5173"] # TODO frontend URL here
CSRF_TRUSTED_ORIGINS = ["http://localhost:5173"] # TODO frontend URL here CORS_ALLOWED_ORIGINS = [
"http://localhost:5173",
"http://127.0.0.1:5173",
] # TODO frontend URL here
CSRF_TRUSTED_ORIGINS = [
"http://localhost:5173",
"http://127.0.0.1:5173",
]
CSRF_COOKIE_HTTPONLY = False # allow JS to read the cookie
CSRF_COOKIE_SAMESITE = "Lax" # or 'None' if using https in development
CSRF_COOKIE_SECURE = False # True in production with HTTPS
MIDDLEWARE = [ MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware", "corsheaders.middleware.CorsMiddleware",
......
This diff is collapsed.
...@@ -33,5 +33,11 @@ ...@@ -33,5 +33,11 @@
"vite": "^6.2.4", "vite": "^6.2.4",
"vite-plugin-vue-devtools": "^7.7.2", "vite-plugin-vue-devtools": "^7.7.2",
"vue-tsc": "^2.2.8" "vue-tsc": "^2.2.8"
},
"prettier": {
"trailingComma": "es5",
"tabWidth": 4,
"semi": false,
"singleQuote": true
} }
} }
\ No newline at end of file
...@@ -8,11 +8,13 @@ import HelloWorld from './components/HelloWorld.vue' ...@@ -8,11 +8,13 @@ import HelloWorld from './components/HelloWorld.vue'
<img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" /> <img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />
<div class="wrapper"> <div class="wrapper">
<HelloWorld msg="You did it!" /> <!-- <HelloWorld msg="You did it!" /> -->
<nav> <nav>
<RouterLink to="/">Home</RouterLink> <RouterLink to="/">Home</RouterLink>
<RouterLink to="/login">Login</RouterLink>
<RouterLink to="/about">About</RouterLink> <RouterLink to="/about">About</RouterLink>
<RouterLink to="/register">register</RouterLink>
</nav> </nav>
</div> </div>
</header> </header>
......
...@@ -57,10 +57,13 @@ const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md') ...@@ -57,10 +57,13 @@ const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
Get official tools and libraries for your project: Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>, <a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>, <a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and <a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>,
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If and
you need more resources, we suggest paying <a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a> >. If you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener"
>Awesome Vue</a
>
a visit. a visit.
</WelcomeItem> </WelcomeItem>
...@@ -88,8 +91,8 @@ const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md') ...@@ -88,8 +91,8 @@ const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
</template> </template>
<template #heading>Support Vue</template> <template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help As an independent project, Vue relies on community backing for its sustainability. You can
us by help us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>. <a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem> </WelcomeItem>
</template> </template>
...@@ -2,7 +2,7 @@ import './assets/main.css' ...@@ -2,7 +2,7 @@ import './assets/main.css'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import { useAuthStore } from './store/auth' import { useAuthStore } from './stores/auth'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
......
/* eslint-disable */
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
\ No newline at end of file
import { defineStore } from 'pinia'
import { type Router } from 'vue-router'
interface AuthState {
user: User | null
isAuthenticated: boolean
}
interface User {
id: number
email: string
[key: string]: any // Add more fields as needed
}
export const useAuthStore = defineStore('auth', {
state: (): AuthState => {
const storedState = localStorage.getItem('authState')
return storedState
? JSON.parse(storedState)
: {
user: null,
isAuthenticated: false,
}
},
actions: {
async setCsrfToken(): Promise<void> {
await fetch('http://localhost:8000/api/set-csrf-token', {
method: 'GET',
credentials: 'include',
})
},
async login(email: string, password: string, router: Router | null = null): Promise<void> {
const response = await fetch('http://localhost:8000/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken(),
},
body: JSON.stringify({
email,
password,
}),
credentials: 'include',
})
const data = await response.json()
if (data.success) {
this.isAuthenticated = true
this.saveState()
if (router) {
await router.push({
name: 'home',
})
}
} else {
this.user = null
this.isAuthenticated = false
this.saveState()
}
},
async logout(router: Router | null = null): Promise<void> {
try {
const response = await fetch('http://localhost:8000/api/logout', {
method: 'POST',
headers: {
'X-CSRFToken': getCSRFToken(),
},
credentials: 'include',
})
if (response.ok) {
this.user = null
this.isAuthenticated = false
this.saveState()
if (router) {
await router.push({
name: 'login',
})
}
}
} catch (error) {
console.error('Logout failed', error)
throw error
}
},
async fetchUser(): Promise<void> {
try {
const response = await fetch('http://localhost:8000/api/user', {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken(),
},
})
if (response.ok) {
const data: User = await response.json()
this.user = data
this.isAuthenticated = true
} else {
this.user = null
this.isAuthenticated = false
}
} catch (error) {
console.error('Failed to fetch user', error)
this.user = null
this.isAuthenticated = false
}
this.saveState()
},
saveState(): void {
/*
We save state to local storage to keep the
state when the user reloads the page.
This is a simple way to persist state. For a more robust solution,
use pinia-persistent-state.
*/
localStorage.setItem(
'authState',
JSON.stringify({
user: this.user,
isAuthenticated: this.isAuthenticated,
}),
)
},
},
})
export function getCSRFToken(): string {
/*
We get the CSRF token from the cookie to include in our requests.
This is necessary for CSRF protection in Django.
*/
const name = 'csrftoken'
let cookieValue: string | null = null
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';')
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim()
if (cookie.substring(0, name.length + 1) === name + '=') {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1))
break
}
}
}
if (cookieValue === null) {
throw new Error('Missing CSRF cookie.')
}
return cookieValue
}
\ No newline at end of file
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', {
state: () => {
const storedState = localStorage.getItem('authState')
return storedState
? JSON.parse(storedState)
: {
user: null,
isAuthenticated: false,
}
},
actions: {
async setCsrfToken() {
await fetch('http://localhost:8000/api/set-csrf-token', {
method: 'GET',
credentials: 'include',
})
},
async login(email, password, router = null) {
const response = await fetch('http://localhost:8000/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken(),
},
body: JSON.stringify({
email,
password,
}),
credentials: 'include',
})
const data = await response.json()
if (data.success) {
this.isAuthenticated = true
this.saveState()
if (router) {
await router.push({
name: 'home',
})
}
} else {
this.user = null
this.isAuthenticated = false
this.saveState()
}
},
async logout(router = null) {
try {
const response = await fetch('http://localhost:8000/api/logout', {
method: 'POST',
headers: {
'X-CSRFToken': getCSRFToken(),
},
credentials: 'include',
})
if (response.ok) {
this.user = null
this.isAuthenticated = false
this.saveState()
if (router) {
await router.push({
name: 'login',
})
}
}
} catch (error) {
console.error('Logout failed', error)
throw error
}
},
async fetchUser() {
try {
const response = await fetch('http://localhost:8000/api/user', {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken(),
},
})
if (response.ok) {
const data = await response.json()
this.user = data
this.isAuthenticated = true
} else {
this.user = null
this.isAuthenticated = false
}
} catch (error) {
console.error('Failed to fetch user', error)
this.user = null
this.isAuthenticated = false
}
this.saveState()
},
saveState() {
/*
We save state to local storage to keep the
state when the user reloads the page.
This is a simple way to persist state. For a more robust solution,
use pinia-persistent-state.
*/
localStorage.setItem(
'authState',
JSON.stringify({
user: this.user,
isAuthenticated: this.isAuthenticated,
}),
)
},
},
})
export function getCSRFToken() {
/*
We get the CSRF token from the cookie to include in our requests.
This is necessary for CSRF protection in Django.
*/
const name = 'csrftoken'
let cookieValue = null
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';')
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim()
if (cookie.substring(0, name.length + 1) === name + '=') {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1))
break
}
}
}
if (cookieValue === null) {
throw 'Missing CSRF cookie.'
}
return cookieValue
}
\ No newline at end of file
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