feat(frontend): Add frontend #25

Merged
Bluemedia merged 16 commits from feature/frontend into main 2025-07-27 21:39:24 +02:00
9 changed files with 64 additions and 33 deletions
Showing only changes of commit f6117d04ee - Show all commits

View file

@ -1,6 +1,6 @@
# Simple OCCP Central System # LibreCharge
This is a simple implementation of a basic [OCPP](https://openchargealliance.org/protocols/open-charge-point-protocol/) 2.0.1 compliant central system (backend) for EV charging stations. LibreCharge is a simple implementation of a basic [OCPP](https://openchargealliance.org/protocols/open-charge-point-protocol/) 2.0.1 compliant central system (backend) for EV charging stations.
## Features ## Features
@ -12,5 +12,7 @@ This is a simple implementation of a basic [OCPP](https://openchargealliance.org
- ✅ RESTful API - ✅ RESTful API
## Tested charging stations ## Tested charging stations
This project has been successfully tested with the following charging stations: This project has been successfully tested with the following charging stations:
- Alfen Eve Single S-line - Alfen Eve Single S-line

3
backend/.gitignore vendored
View file

@ -1,3 +1,4 @@
**/__pycache__ **/__pycache__
simple-ocpp-cs.db simple-ocpp-cs.db
.env .env
static

View file

@ -1,6 +1,8 @@
import os
from dotenv import load_dotenv from dotenv import load_dotenv
from fastapi import APIRouter, FastAPI from fastapi import APIRouter, FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from starlette.middleware.authentication import AuthenticationMiddleware from starlette.middleware.authentication import AuthenticationMiddleware
load_dotenv() load_dotenv()
@ -17,25 +19,46 @@ from app.routers import (
) )
from app.security.websocket_auth_backend import BasicAuthBackend from app.security.websocket_auth_backend import BasicAuthBackend
def create_ocpp_app():
app_ocpp = FastAPI(
responses={404: {"description": "Not found"}},
)
app_ocpp.include_router(ocpp_v1.router)
app_ocpp.add_middleware(AuthenticationMiddleware, backend=BasicAuthBackend())
return app_ocpp
def create_app(): def create_app():
app = FastAPI( # Common app config
title="simple-ocpp-cs", title="LibreCharge"
version="0.2.0", version="0.2.0"
summary="Simple implementation of a basic OCPP 2.0.1 compliant central system (backend) for EV charging stations", summary="Simple implementation of a basic OCPP 2.0.1 compliant central system (backend) for EV charging stations"
responses={404: {"description": "Not found"}}, responses={404: {"description": "Not found"}}
# Root FastAPI app
root_app = FastAPI(
title=title,
version=version,
summary=summary,
responses=responses,
docs_url=None,
redoc_url=None,
)
# FastAPI app for OCPP handler
ocpp_app = FastAPI(
title=title,
version=version,
summary=summary,
responses=responses,
docs_url=None,
redoc_url=None,
)
ocpp_app.include_router(ocpp_v1.router)
ocpp_app.add_middleware(AuthenticationMiddleware, backend=BasicAuthBackend())
root_app.mount(path="/v1/ocpp", app=ocpp_app)
# FastAPI app for REST API
api_app = FastAPI(
title=title,
version=version,
summary=summary,
responses=responses,
) )
api_v1_router = APIRouter( api_v1_router = APIRouter(
prefix="/api/v1" prefix="/v1"
) )
api_v1_router.include_router(auth_v1.router) api_v1_router.include_router(auth_v1.router)
api_v1_router.include_router(chargepoint_v1.router) api_v1_router.include_router(chargepoint_v1.router)
@ -45,15 +68,20 @@ def create_app():
api_v1_router.include_router(meter_value_v1.router) api_v1_router.include_router(meter_value_v1.router)
api_v1_router.include_router(transaction_v1.router) api_v1_router.include_router(transaction_v1.router)
app.include_router(api_v1_router) api_app.include_router(api_v1_router)
app.mount(path="/v1/ocpp", app=create_ocpp_app()) root_app.mount(path="/api", app=api_app)
# Serve static files if existent
if os.path.isdir('static'):
static_files = StaticFiles(directory="static", html=True)
root_app.mount(path="/", app=static_files)
origins = [ origins = [
"http://localhost", "http://localhost",
"http://localhost:5173", "http://localhost:5173",
] ]
app.add_middleware( root_app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=origins, allow_origins=origins,
allow_credentials=True, allow_credentials=True,
@ -61,6 +89,6 @@ def create_app():
allow_headers=["*"], allow_headers=["*"],
) )
return app return root_app
app = create_app() app = create_app()

View file

@ -55,7 +55,7 @@ function createTokenRefreshInterceptor() {
// Retry failed, clean up and reject the promise // Retry failed, clean up and reject the promise
clearLoginState(); clearLoginState();
axios.defaults.headers.common['Authorization'] = ""; axios.defaults.headers.common['Authorization'] = "";
goto('/login?reauth') goto('#/login?reauth')
return Promise.reject(retryError); return Promise.reject(retryError);
}) })
.finally(createTokenRefreshInterceptor); // Re-attach interceptor for future requests .finally(createTokenRefreshInterceptor); // Re-attach interceptor for future requests
@ -101,7 +101,7 @@ export const logout = function() {
.then(() => { .then(() => {
clearLoginState(); clearLoginState();
axios.defaults.headers.common['Authorization'] = ""; axios.defaults.headers.common['Authorization'] = "";
goto('/login?logout') goto('#/login?logout')
}); });
} }

View file

@ -59,7 +59,7 @@
</div> </div>
</td> </td>
<td> <td>
<a href="/chargepoint/{transaction.chargepoint.id}" class="btn btn-sm btn-primary"> <a href="#/chargepoint/{transaction.chargepoint.id}" class="btn btn-sm btn-primary">
<i class="bi bi-plug-fill text-lg"></i> <i class="bi bi-plug-fill text-lg"></i>
{transaction.chargepoint.identity} {transaction.chargepoint.identity}
</a> </a>
@ -71,7 +71,7 @@
>{((transaction.meter_end - transaction.meter_start) * transaction.price).toFixed(2)}</td >{((transaction.meter_end - transaction.meter_start) * transaction.price).toFixed(2)}</td
> >
<th> <th>
<a href="/transaction/{transaction.id}" class="btn btn-sm btn-primary"> <a href="#/transaction/{transaction.id}" class="btn btn-sm btn-primary">
{$i18n.t('common:transactionTable.detailButton')} {$i18n.t('common:transactionTable.detailButton')}
<i class="bi bi-arrow-right"></i> <i class="bi bi-arrow-right"></i>
</a> </a>

View file

@ -7,7 +7,7 @@
let { children } = $props() let { children } = $props()
if (!$persistentSettings.loggedIn) { if (!$persistentSettings.loggedIn) {
goto('/login') goto('#/login')
} }
let drawerOpen = $state(false) let drawerOpen = $state(false)
@ -44,7 +44,7 @@
onclick={() => { onclick={() => {
drawerOpen = !drawerOpen drawerOpen = !drawerOpen
}} }}
href="/idtoken" href="#/idtoken"
> >
<i class="bi bi-credit-card-fill text-xl"></i> <i class="bi bi-credit-card-fill text-xl"></i>
<span>{$i18n.t('common:navbar.link.idtoken')}</span> <span>{$i18n.t('common:navbar.link.idtoken')}</span>
@ -55,7 +55,7 @@
onclick={() => { onclick={() => {
drawerOpen = !drawerOpen drawerOpen = !drawerOpen
}} }}
href="/transaction" href="#/transaction"
> >
<i class="bi bi-battery-charging text-xl"></i> <i class="bi bi-battery-charging text-xl"></i>
<span>{$i18n.t('common:navbar.link.transaction')}</span> <span>{$i18n.t('common:navbar.link.transaction')}</span>
@ -66,7 +66,7 @@
onclick={() => { onclick={() => {
drawerOpen = !drawerOpen drawerOpen = !drawerOpen
}} }}
href="/chargepoint" href="#/chargepoint"
> >
<i class="bi bi-plug-fill text-xl"></i> <i class="bi bi-plug-fill text-xl"></i>
<span>{$i18n.t('common:navbar.link.chargepoint')}</span> <span>{$i18n.t('common:navbar.link.chargepoint')}</span>
@ -87,7 +87,7 @@
tabindex="-1" tabindex="-1"
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow" 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="#/profile">{$i18n.t('common:navbar.link.profile')}</a></li>
<li><button onclick={logout}>{$i18n.t('common:navbar.link.logout')}</button></li> <li><button onclick={logout}>{$i18n.t('common:navbar.link.logout')}</button></li>
</ul> </ul>
</div> </div>

View file

@ -81,7 +81,7 @@
</td> </td>
<td>{chargepoint.price}</td> <td>{chargepoint.price}</td>
<th> <th>
<a href="/chargepoint/{chargepoint.id}" class="btn btn-sm btn-primary"> <a href="#/chargepoint/{chargepoint.id}" class="btn btn-sm btn-primary">
{$i18n.t('common:transactionTable.detailButton')} {$i18n.t('common:transactionTable.detailButton')}
<i class="bi bi-arrow-right"></i> <i class="bi bi-arrow-right"></i>
</a> </a>

View file

@ -1 +0,0 @@
export const ssr = false

View file

@ -11,6 +11,7 @@ const config = {
adapter: adapter({ adapter: adapter({
fallback: 'index.html', fallback: 'index.html',
}), }),
router: { type: 'hash' },
}, },
} }