diff --git a/backend/app/main.py b/backend/app/main.py index b36e999..5a69345 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,5 +1,6 @@ from dotenv import load_dotenv from fastapi import APIRouter, FastAPI +from fastapi.middleware.cors import CORSMiddleware from starlette.middleware.authentication import AuthenticationMiddleware load_dotenv() @@ -47,6 +48,19 @@ def create_app(): app.include_router(api_v1_router) app.mount(path="/v1/ocpp", app=create_ocpp_app()) + origins = [ + "http://localhost", + "http://localhost:5173", + ] + + app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + return app app = create_app() diff --git a/frontend/src/lib/axios.svelte.ts b/frontend/src/lib/axios.svelte.ts new file mode 100644 index 0000000..31ad377 --- /dev/null +++ b/frontend/src/lib/axios.svelte.ts @@ -0,0 +1,103 @@ +import axios from 'axios'; +import { dev } from '$app/environment'; +import { goto } from '$app/navigation'; +import { get } from 'svelte/store'; +import { persistentSettings, clearLoginState } from '$lib/persistent-store'; + +if (dev) { + axios.defaults.baseURL = "http://localhost:8000/api/v1" +} else { + axios.defaults.baseURL = "/api/v1" +} + +function createTokenRefreshInterceptor() { + const interceptor = axios.interceptors.response.use( + (response) => response, + (error) => { + // Reject promise if usual error + if (error.response.status !== 401) { + return Promise.reject(error); + } + + /* + * When response code is 401, try to refresh the token. + * Eject the interceptor so it doesn't loop in case + * token refresh causes the 401 response. + * + * Must be re-attached later on or the token refresh will only happen once + */ + axios.interceptors.response.eject(interceptor); + + return axios + .post("/auth/refresh", { + refresh_token: get(persistentSettings).refreshToken, + }) + .then((response) => { + // Save new refresh token + persistentSettings.update(settings => { + settings.refreshToken = response.data.refresh_token; + return settings; + }) + + // Update access token + const authHeader = "Bearer " + response.data.access_token; + axios.defaults.headers.common['Authorization'] = authHeader; + error.response.config.headers["Authorization"] = authHeader; + + // Retry initial request with new token + return axios(error.response.config); + }) + .catch((retryError) => { + // Retry failed, clean up and reject the promise + clearLoginState(); + axios.defaults.headers.common['Authorization'] = ""; + goto('/login?reauth') + return Promise.reject(retryError); + }) + .finally(createTokenRefreshInterceptor); // Re-attach interceptor for future requests + } + ); +} + +createTokenRefreshInterceptor(); + +export const login = async function(email: string, password: string) { + await axios + .post('/auth/login', { + email, + password, + }) + .then((response) => { + persistentSettings.update(settings => { + settings.loggedIn = true + settings.refreshToken = response.data.refresh_token + return settings; + }) + axios.defaults.headers.common['Authorization'] = 'Bearer ' + response.data.access_token + axios.get('/me').then((response) => { + persistentSettings.update(settings => { + settings.email = response.data.email + settings.friendlyName = response.data.friendly_name + settings.role = response.data.role + return settings; + }) + }) + goto('/') + return Promise.resolve(); + }) + .catch((error) => { + return Promise.reject(error); + }) +} + +export const logout = function() { + axios + .post('/auth/logout') + .then(() => { + clearLoginState(); + axios.defaults.headers.common['Authorization'] = ""; + goto('/login?logout') + }); +} + +export default axios; diff --git a/frontend/src/lib/persistent-store.ts b/frontend/src/lib/persistent-store.ts index 81b4fea..7e0c04c 100644 --- a/frontend/src/lib/persistent-store.ts +++ b/frontend/src/lib/persistent-store.ts @@ -20,4 +20,15 @@ const settingsDefault: PersistedSettings = { export const persistentSettings = writable(JSON.parse(localStorage.getItem('persistentSettings') || JSON.stringify(settingsDefault))) -persistentSettings.subscribe((value) => localStorage.persistentSettings = JSON.stringify(value)) \ No newline at end of file +persistentSettings.subscribe((value) => localStorage.persistentSettings = JSON.stringify(value)) + +export const clearLoginState = function() { + persistentSettings.update(settings => { + settings.refreshToken = ""; + settings.loggedIn = false; + settings.friendlyName = ""; + settings.email = ""; + settings.role = "member"; + return settings; + }) +} \ No newline at end of file diff --git a/frontend/src/routes/(navbar)/+layout.svelte b/frontend/src/routes/(navbar)/+layout.svelte index 191f2f6..b58d427 100644 --- a/frontend/src/routes/(navbar)/+layout.svelte +++ b/frontend/src/routes/(navbar)/+layout.svelte @@ -2,6 +2,8 @@ import { goto } from '$app/navigation' import { persistentSettings } from '$lib/persistent-store' import i18n from '$lib/i18n' + import { logout } from '$lib/axios.svelte' + let { children } = $props() if (!$persistentSettings.loggedIn) { @@ -86,7 +88,7 @@ class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow" >
  • {$i18n.t('common:navbar.link.profile')}
  • -
  • {$i18n.t('common:navbar.link.logout')}
  • +
  • diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte index 038f95f..a9e3b16 100644 --- a/frontend/src/routes/login/+page.svelte +++ b/frontend/src/routes/login/+page.svelte @@ -1,18 +1,37 @@
    @@ -30,28 +49,41 @@
    - +
    - {#if (isReauth || isLogout) && showToast} + {#if toastVisible}
    -
    - {#if isReauth} + {#if toastType === 'reauth'} +
    - {:else} + {$i18n.t('login:toast.reauth')} +
    + {:else if toastType === 'logout'} +
    - {/if} - {$i18n.t(isReauth ? 'login:toast.reauth' : 'login:toast.logoutSuccess')} -
    + {$i18n.t('login:toast.logoutSuccess')} +
    + {:else if toastType === 'error'} +
    + + {$i18n.t('login:toast.loginFailed')} +
    + {/if}
    {/if} diff --git a/frontend/static/locales/de/login.json b/frontend/static/locales/de/login.json index 69f7392..f744f34 100644 --- a/frontend/static/locales/de/login.json +++ b/frontend/static/locales/de/login.json @@ -5,6 +5,7 @@ "button": "Anmelden", "toast": { "reauth": "Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.", - "logoutSuccess": "Abmeldung erfolgreich." + "logoutSuccess": "Abmeldung erfolgreich.", + "loginFailed": "Anmeldung fehlgeschlagen." } } diff --git a/frontend/static/locales/en/login.json b/frontend/static/locales/en/login.json index 6d600d6..10be868 100644 --- a/frontend/static/locales/en/login.json +++ b/frontend/static/locales/en/login.json @@ -5,6 +5,7 @@ "button": "Login", "toast": { "reauth": "Your session expired. Please log in again.", - "logoutSuccess": "Successfully logged out." + "logoutSuccess": "Successfully logged out.", + "loginFailed": "Login failed." } }