Implement authentication logic
This commit is contained in:
parent
34b12f6a44
commit
be736638bd
11 changed files with 196 additions and 24 deletions
|
@ -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()
|
||||
|
|
108
frontend/src/lib/axios.svelte.ts
Normal file
108
frontend/src/lib/axios.svelte.ts
Normal file
|
@ -0,0 +1,108 @@
|
|||
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"
|
||||
}
|
||||
|
||||
// Get access token from local storage
|
||||
axios.defaults.headers.common['Authorization'] = "Bearer " + get(persistentSettings).accessToken;
|
||||
|
||||
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 tokens
|
||||
persistentSettings.update(settings => {
|
||||
settings.accessToken = response.data.access_token
|
||||
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.accessToken = response.data.access_token
|
||||
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;
|
|
@ -3,7 +3,7 @@ import Backend from 'i18next-chained-backend'
|
|||
import Fetch from 'i18next-fetch-backend'
|
||||
import LocalStorageBackend from 'i18next-localstorage-backend'
|
||||
import I18nextBrowserLanguageDetector from 'i18next-browser-languagedetector'
|
||||
import { createI18nStore } from './i18n-store'
|
||||
import { createI18nStore } from './i18n_store'
|
||||
|
||||
i18next
|
||||
.use(Backend)
|
||||
|
|
|
@ -6,6 +6,7 @@ interface PersistedSettings {
|
|||
friendlyName: string
|
||||
email: string
|
||||
role: string
|
||||
accessToken: string,
|
||||
refreshToken: string
|
||||
}
|
||||
|
||||
|
@ -15,9 +16,22 @@ const settingsDefault: PersistedSettings = {
|
|||
friendlyName: "",
|
||||
email: "",
|
||||
role: "member",
|
||||
accessToken: "",
|
||||
refreshToken: ""
|
||||
}
|
||||
|
||||
export const persistentSettings = writable<PersistedSettings>(JSON.parse(localStorage.getItem('persistentSettings') || JSON.stringify(settingsDefault)))
|
||||
|
||||
persistentSettings.subscribe((value) => localStorage.persistentSettings = JSON.stringify(value))
|
||||
|
||||
export const clearLoginState = function() {
|
||||
persistentSettings.update(settings => {
|
||||
settings.accessToken = "";
|
||||
settings.refreshToken = "";
|
||||
settings.loggedIn = false;
|
||||
settings.friendlyName = "";
|
||||
settings.email = "";
|
||||
settings.role = "member";
|
||||
return settings;
|
||||
})
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import { persistentSettings } from '$lib/persistent-store'
|
||||
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"
|
||||
>
|
||||
<li><a href="/profile">{$i18n.t('common:navbar.link.profile')}</a></li>
|
||||
<li><a href="/login?logout">{$i18n.t('common:navbar.link.logout')}</a></li>
|
||||
<li><button onclick={logout}>{$i18n.t('common:navbar.link.logout')}</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { persistentSettings } from '$lib/persistent-store'
|
||||
import { persistentSettings } from '$lib/persistent_store'
|
||||
import { currentDaytime } from '$lib/util'
|
||||
import i18n from '$lib/i18n'
|
||||
import DashboardCard from '$lib/component/DashboardCard.svelte'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { persistentSettings } from '$lib/persistent-store'
|
||||
import { persistentSettings } from '$lib/persistent_store'
|
||||
let { children } = $props()
|
||||
import '../app.css'
|
||||
</script>
|
||||
|
|
|
@ -1,18 +1,37 @@
|
|||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition'
|
||||
import i18n from '$lib/i18n'
|
||||
import { login as performLogin } from '$lib/axios.svelte'
|
||||
$i18n.loadNamespaces('login')
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const isReauth = urlParams.has('reauth')
|
||||
const isLogout = urlParams.has('logout')
|
||||
type ToastType = 'reauth' | 'logout' | 'error'
|
||||
let toastType: ToastType = $state('reauth')
|
||||
let toastVisible = $state(false)
|
||||
|
||||
let showToast = $state(true)
|
||||
if (isReauth || isLogout) {
|
||||
function showToast(type: ToastType) {
|
||||
toastType = type
|
||||
toastVisible = true
|
||||
setTimeout(() => {
|
||||
showToast = false
|
||||
toastVisible = false
|
||||
}, 6000)
|
||||
}
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
if (urlParams.has('reauth')) {
|
||||
showToast('reauth')
|
||||
}
|
||||
if (urlParams.has('logout')) {
|
||||
showToast('logout')
|
||||
}
|
||||
|
||||
let email: string = $state('')
|
||||
let password: string = $state('')
|
||||
|
||||
async function login() {
|
||||
await performLogin(email, password).catch(() => {
|
||||
showToast('error')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full flex flex-col justify-center items-center bg-base-200">
|
||||
|
@ -30,28 +49,41 @@
|
|||
<fieldset class="fieldset">
|
||||
<label class="fieldset-label input text-base-content">
|
||||
<i class="bi bi-envelope-at"></i>
|
||||
<input type="email" placeholder="me@example.com" required />
|
||||
<input type="email" bind:value={email} placeholder="me@example.com" required />
|
||||
</label>
|
||||
<label class="fieldset-label input text-base-content">
|
||||
<i class="bi bi-key"></i>
|
||||
<input type="password" placeholder={$i18n.t('login:passwordPlaceholder')} required />
|
||||
<input
|
||||
type="password"
|
||||
bind:value={password}
|
||||
placeholder={$i18n.t('login:passwordPlaceholder')}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<button class="btn btn-primary mt-4">{$i18n.t('login:button')}</button>
|
||||
<button class="btn btn-primary mt-4" onclick={login}>{$i18n.t('login:button')}</button>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="basis-0 grow"></span>
|
||||
{#if (isReauth || isLogout) && showToast}
|
||||
{#if toastVisible}
|
||||
<div class="toast toast-top" out:fly={{ x: 200 }}>
|
||||
<div class="alert text-base {isReauth ? 'alert-warning' : 'alert-success'}">
|
||||
{#if isReauth}
|
||||
{#if toastType === 'reauth'}
|
||||
<div class="alert text-base alert-warning">
|
||||
<i class="bi bi-exclamation-diamond text-xl"></i>
|
||||
{:else}
|
||||
<span>{$i18n.t('login:toast.reauth')}</span>
|
||||
</div>
|
||||
{:else if toastType === 'logout'}
|
||||
<div class="alert text-base alert-success">
|
||||
<i class="bi bi-check-circle text-xl"></i>
|
||||
{/if}
|
||||
<span>{$i18n.t(isReauth ? 'login:toast.reauth' : 'login:toast.logoutSuccess')}</span>
|
||||
</div>
|
||||
<span>{$i18n.t('login:toast.logoutSuccess')}</span>
|
||||
</div>
|
||||
{:else if toastType === 'error'}
|
||||
<div class="alert text-base alert-error">
|
||||
<i class="bi bi-x-circle text-xl"></i>
|
||||
<span>{$i18n.t('login:toast.loginFailed')}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue