From ac8303378a7813a1f21e69bf491ecd4c9e9f4712 Mon Sep 17 00:00:00 2001
From: BluemediaDev <oliver@traber-info.de>
Date: Thu, 13 Mar 2025 16:34:41 +0000
Subject: [PATCH 01/10] Implement user authentication and permissions

---
 ...13_c7f72154c90b-add_user_authentication.py |  59 +++++
 app/cli.py                                    |  88 +++++++
 app/main.py                                   |  39 ++-
 app/models/__init__.py                        |   1 +
 app/models/session.py                         |  14 ++
 app/models/user.py                            |  10 +-
 app/routers/auth_v1.py                        |  72 ++++++
 app/routers/chargepoint_v1.py                 |  59 ++---
 app/routers/id_token_v1.py                    |  59 +++--
 app/routers/me_v1.py                          | 111 +++++++++
 app/routers/meter_value_v1.py                 |  13 +-
 app/routers/transaction_v1.py                 |  60 ++++-
 app/routers/user_v1.py                        | 127 ++++++----
 app/schemas/auth_token.py                     |  21 ++
 app/schemas/session.py                        |  11 +
 app/schemas/user.py                           |  23 +-
 app/security.py                               |  34 ---
 app/security/__init__.py                      |   0
 app/security/jwt_bearer.py                    |  47 ++++
 .../websocket_auth_backend.py                 |   0
 app/services/session_service.py               |  83 +++++++
 app/services/token_service.py                 |  69 ++++++
 app/services/user_service.py                  | 111 +++++++++
 app/util/errors.py                            |  14 ++
 poetry.lock                                   | 227 +++++++++++++++++-
 pyproject.toml                                |   2 +
 26 files changed, 1182 insertions(+), 172 deletions(-)
 create mode 100644 alembic/versions/20250313_c7f72154c90b-add_user_authentication.py
 create mode 100644 app/cli.py
 create mode 100644 app/models/session.py
 create mode 100644 app/routers/auth_v1.py
 create mode 100644 app/routers/me_v1.py
 create mode 100644 app/schemas/auth_token.py
 create mode 100644 app/schemas/session.py
 delete mode 100644 app/security.py
 create mode 100644 app/security/__init__.py
 create mode 100644 app/security/jwt_bearer.py
 rename app/{util => security}/websocket_auth_backend.py (100%)
 create mode 100644 app/services/session_service.py
 create mode 100644 app/services/token_service.py
 create mode 100644 app/services/user_service.py
 create mode 100644 app/util/errors.py

diff --git a/alembic/versions/20250313_c7f72154c90b-add_user_authentication.py b/alembic/versions/20250313_c7f72154c90b-add_user_authentication.py
new file mode 100644
index 0000000..7e2c00b
--- /dev/null
+++ b/alembic/versions/20250313_c7f72154c90b-add_user_authentication.py
@@ -0,0 +1,59 @@
+"""Add user authentication
+
+Revision ID: c7f72154c90b
+Revises: 097d427dfa07
+Create Date: 2025-03-13 14:57:05.805469+00:00
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision: str = 'c7f72154c90b'
+down_revision: Union[str, None] = '097d427dfa07'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('sessions',
+    sa.Column('id', sa.Uuid(), nullable=False),
+    sa.Column('name', sa.String(), nullable=True),
+    sa.Column('refresh_token', sa.String(), nullable=False),
+    sa.Column('last_used', sa.DateTime(timezone=True), nullable=True),
+    sa.Column('user_id', sa.Uuid(), nullable=False),
+    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.create_index(op.f('ix_sessions_refresh_token'), 'sessions', ['refresh_token'], unique=True)
+    op.create_index(op.f('ix_sessions_user_id'), 'sessions', ['user_id'], unique=False)
+    op.add_column('users', sa.Column('email', sa.String(), nullable=True))
+    op.add_column('users', sa.Column('password', sa.String(), nullable=True))
+    op.add_column('users', sa.Column('role', sa.Enum('MEMBER', 'ADMINISTRATOR', name='role'), nullable=True))
+    op.execute('UPDATE users SET email = id || \'@example.com\'')
+    op.execute('UPDATE users SET password = \'invalid\'')
+    op.execute('UPDATE users SET role = \'MEMBER\'')
+    with op.batch_alter_table('users', schema=None) as batch_op:
+        batch_op.alter_column('email', nullable=False)
+        batch_op.alter_column('password', nullable=False)
+        batch_op.alter_column('role', nullable=False)
+    op.drop_index('ix_users_friendly_name', table_name='users')
+    op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
+    # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_index(op.f('ix_users_email'), table_name='users')
+    op.create_index('ix_users_friendly_name', 'users', ['friendly_name'], unique=1)
+    op.drop_column('users', 'role')
+    op.drop_column('users', 'password')
+    op.drop_column('users', 'email')
+    op.drop_index(op.f('ix_sessions_user_id'), table_name='sessions')
+    op.drop_index(op.f('ix_sessions_refresh_token'), table_name='sessions')
+    op.drop_table('sessions')
+    # ### end Alembic commands ###
diff --git a/app/cli.py b/app/cli.py
new file mode 100644
index 0000000..7ce93b6
--- /dev/null
+++ b/app/cli.py
@@ -0,0 +1,88 @@
+import secrets
+import sys
+import os
+sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
+
+import argparse
+from dotenv import load_dotenv
+
+from sqlalchemy import select
+from sqlalchemy.orm import Session
+from argon2 import PasswordHasher
+
+from app.models import *
+
+load_dotenv()
+
+from app.database import SessionLocal
+
+def __get_user_by_email(db: Session, email: str):
+    stmt = select(user.User).where(user.User.email == email)
+    result = db.execute(stmt)
+    return result.scalars().first()
+
+def create_user(args):
+    hasher = PasswordHasher(memory_cost=102400)
+    with SessionLocal() as db:
+        db_user = __get_user_by_email(db, args.email)
+        if db_user is not None:
+            print(f'Error: A user with email \'{args.email}\' already exists.')
+            sys.exit(1)
+        hashed_password = hasher.hash(args.password)
+        db_user = user.User(
+            friendly_name=args.name, email=args.email, password=hashed_password
+        )
+        db.add(db_user)
+        db.commit()
+        print(f'Success: Created user \'{args.email}\'.')
+
+def reset_password(args):
+    hasher = PasswordHasher(memory_cost=102400)
+    with SessionLocal() as db:
+        db_user = __get_user_by_email(db, args.email)
+        if db_user is None:
+            print(f'Error: No user with email \'{args.email}\' found.')
+            sys.exit(1)
+        db_user.password = hasher.hash(args.password)
+        db.commit()
+        print(f'Success: Changed password for user \'{args.email}\'.')
+
+def set_role(args):
+    with SessionLocal() as db:
+        db_user = __get_user_by_email(db, args.email)
+        if db_user is None:
+            print(f'Error: No user with email \'{args.email}\' found.')
+            sys.exit(1)
+        db_user.role = user.Role(args.role)
+        db.commit()
+        print(f'Success: Role of user \'{args.email}\' changed to \'{db_user.role}\'.')
+
+def generate_secret(args):
+    print(f'Your secret: {secrets.token_urlsafe(64)}')
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    subparsers = parser.add_subparsers(help="subcommands", required=True)
+
+    p_create_user = subparsers.add_parser('user_create', help='Create a new user')
+    p_create_user.add_argument('--email', type=str, required=True, help='Email of the new user')
+    p_create_user.add_argument('--password', type=str, required=True, help='Password for the new user')
+    p_create_user.add_argument('--name', type=str, required=False, help='Display name of the new user')
+    p_create_user.add_argument('--role', type=str, choices=('member', 'administrator'), default='member', required=False, help='Role of the new user')
+    p_create_user.set_defaults(func=create_user)
+
+    p_reset_password = subparsers.add_parser('user_reset_password', help='Reset password for a user')
+    p_reset_password.add_argument('--email', type=str, required=True, help='Email of the user to modify')
+    p_reset_password.add_argument('--password', type=str, required=True, help='New password for the specified user')
+    p_reset_password.set_defaults(func=reset_password)
+
+    p_set_role = subparsers.add_parser('user_set_role', help='Update role of a user')
+    p_set_role.add_argument('--email', type=str, required=True, help='Email of the user to modify')
+    p_set_role.add_argument('--role', type=str, choices=('member', 'administrator'), required=True, help='New role for the specified user')
+    p_set_role.set_defaults(func=set_role)
+
+    p_set_role = subparsers.add_parser('generate_secret', help='Generate safe to use secret for token signing')
+    p_set_role.set_defaults(func=generate_secret)
+
+    args = parser.parse_args()
+    args.func(args)
diff --git a/app/main.py b/app/main.py
index c9d8efe..7e92b37 100644
--- a/app/main.py
+++ b/app/main.py
@@ -1,19 +1,20 @@
 from dotenv import load_dotenv
-from fastapi import FastAPI
-from fastapi.openapi.utils import get_openapi
+from fastapi import APIRouter, FastAPI
 from starlette.middleware.authentication import AuthenticationMiddleware
 
 load_dotenv()
 
 from app.routers import (
+    auth_v1,
     chargepoint_v1,
     id_token_v1,
+    me_v1,
     meter_value_v1,
     ocpp_v1,
     transaction_v1,
     user_v1
 )
-from app.util.websocket_auth_backend import BasicAuthBackend
+from app.security.websocket_auth_backend import BasicAuthBackend
 
 def create_ocpp_app():
     app_ocpp = FastAPI(
@@ -24,30 +25,26 @@ def create_ocpp_app():
     
     return app_ocpp
 
-def custom_openapi():
-    if app.openapi_schema:
-        return app.openapi_schema
-    openapi_schema = get_openapi(
-        title="simple-ocpp-cs",
-        version="0.1.0",
-        summary="Simple implementation of a basic OCPP 2.0.1 compliant central system (backend) for EV charging stations",
-        routes=app.routes,
-    )
-    app.openapi_schema = openapi_schema
-    return app.openapi_schema
-
 def create_app():
     app = FastAPI(
+        title="simple-ocpp-cs",
+        summary="Simple implementation of a basic OCPP 2.0.1 compliant central system (backend) for EV charging stations",
         responses={404: {"description": "Not found"}},
     )
     
-    app.include_router(chargepoint_v1.router, prefix="/v1")
-    app.include_router(id_token_v1.router, prefix="/v1")
-    app.include_router(user_v1.router, prefix="/v1")
-    app.include_router(meter_value_v1.router, prefix="/v1")
-    app.include_router(transaction_v1.router, prefix="/v1")
+    api_v1_router = APIRouter(
+        prefix="/api/v1"
+    )
+    api_v1_router.include_router(auth_v1.router)
+    api_v1_router.include_router(chargepoint_v1.router)
+    api_v1_router.include_router(id_token_v1.router)
+    api_v1_router.include_router(me_v1.router)
+    api_v1_router.include_router(user_v1.router)
+    api_v1_router.include_router(meter_value_v1.router)
+    api_v1_router.include_router(transaction_v1.router)
+
+    app.include_router(api_v1_router)
     app.mount(path="/v1/ocpp", app=create_ocpp_app())
-    app.openapi = custom_openapi
 
     return app
 
diff --git a/app/models/__init__.py b/app/models/__init__.py
index b6ddc91..26e5c03 100644
--- a/app/models/__init__.py
+++ b/app/models/__init__.py
@@ -4,6 +4,7 @@ __all__ = [
     "connector",
     "id_token",
     "meter_value",
+    "session",
     "transaction",
     "user"
 ]
\ No newline at end of file
diff --git a/app/models/session.py b/app/models/session.py
new file mode 100644
index 0000000..b1c2213
--- /dev/null
+++ b/app/models/session.py
@@ -0,0 +1,14 @@
+import uuid
+from sqlalchemy import Column, DateTime, ForeignKey, String, Uuid
+
+from app.database import Base
+
+class Session(Base):
+    __tablename__ = "sessions"
+
+    id = Column(Uuid, primary_key=True, default=uuid.uuid4)
+    name = Column(String)
+    refresh_token = Column(String, unique=True, index=True)
+    last_used = Column(DateTime(timezone=True))
+
+    user_id = Column(Uuid, ForeignKey("users.id"), index=True)
\ No newline at end of file
diff --git a/app/models/user.py b/app/models/user.py
index 80bff7d..75cf5da 100644
--- a/app/models/user.py
+++ b/app/models/user.py
@@ -1,15 +1,19 @@
 import uuid
-from sqlalchemy import Uuid, Boolean, Column, String
+from sqlalchemy import Enum, Uuid, Boolean, Column, String
 from sqlalchemy.orm import relationship
 
 from app.database import Base
+from app.schemas.user import Role
 
 class User(Base):
     __tablename__ = "users"
 
     id = Column(Uuid, primary_key=True, default=uuid.uuid4)
-    friendly_name = Column(String, unique=True, index=True)
-    is_active = Column(Boolean, default=True)
+    friendly_name = Column(String, nullable=True)
+    is_active = Column(Boolean, nullable=False, default=True)
+    email = Column(String, nullable=False, unique=True, index=True)
+    password = Column(String, nullable=False)
+    role = Column(Enum(Role), nullable=False, default=Role.MEMBER)
     
     id_tokens = relationship("IdToken", back_populates="owner", cascade="delete, delete-orphan")
     transactions = relationship("Transaction", cascade="delete, delete-orphan")
diff --git a/app/routers/auth_v1.py b/app/routers/auth_v1.py
new file mode 100644
index 0000000..3abc4de
--- /dev/null
+++ b/app/routers/auth_v1.py
@@ -0,0 +1,72 @@
+from uuid import UUID
+from fastapi import APIRouter, HTTPException
+from fastapi.params import Depends
+from sqlalchemy.orm import Session
+
+from app.database import get_db
+from app.schemas.auth_token import (
+    AccessToken,
+    TokenRefreshRequest,
+    TokenResponse,
+)
+from app.schemas.user import LoginRequest
+from app.security.jwt_bearer import JWTBearer
+from app.services import session_service, token_service, user_service
+from app.util.errors import NotFoundError
+
+router = APIRouter(prefix="/auth", tags=["Authentication (v1)"])
+
+@router.post(path="/login", response_model=TokenResponse)
+async def login(
+    login_request: LoginRequest, db: Session = Depends(get_db)
+):
+    """
+    Login to a existing account. Creates a new session and returns a access and refresh token.
+    """
+    user = await user_service.validate_login(
+        db=db, login=login_request
+    )
+    if not user:
+        raise HTTPException(status_code=403, detail="invalid_email_or_password")
+    session = await session_service.create_session(db=db, user=user, useragent="")
+    token, expire = await token_service.create_access_token(
+        user=user, session_id=session.id
+    )
+    return TokenResponse(
+        access_token=token, refresh_token=session.refresh_token, not_after=expire
+    )
+
+
+@router.post(path="/logout", response_model=list[None])
+async def logout(
+    db: Session = Depends(get_db), token: AccessToken = Depends(JWTBearer())
+):
+    """
+    Remove the current session based on the access token, effectively invalidating the current refresh token.
+    """
+    await session_service.remove_session(
+        db=db, id=UUID(token.session), initiator=f"user:{token.subject}"
+    )
+    return list()
+
+@router.post(path="/refresh", response_model=TokenResponse)
+async def refresh_access_token(
+    token_request: TokenRefreshRequest,
+    db: Session = Depends(get_db),
+):
+    """
+    Use an existing refresh token to generate a new access token and a new refresh token.
+    """
+    try:
+        session = await session_service.validate_and_rotate_refresh_token(
+            db=db, refresh_token=token_request.refresh_token
+        )
+        user = await user_service.get_user(db=db, id=session.user_id)
+        token, expire = await token_service.create_access_token(
+            user=user, session_id=session.id
+        )
+        return TokenResponse(
+            access_token=token, refresh_token=session.refresh_token, not_after=expire
+        )
+    except NotFoundError:
+        raise HTTPException(status_code=403, detail="invalid_refresh_token")
\ No newline at end of file
diff --git a/app/routers/chargepoint_v1.py b/app/routers/chargepoint_v1.py
index ab995de..ff22bfd 100644
--- a/app/routers/chargepoint_v1.py
+++ b/app/routers/chargepoint_v1.py
@@ -2,7 +2,7 @@ import random
 import string
 from datetime import datetime, timedelta, UTC
 from uuid import UUID
-from fastapi import APIRouter, HTTPException, Security
+from fastapi import APIRouter, HTTPException
 from fastapi.params import Depends
 from sqlalchemy.orm import Session
 
@@ -10,6 +10,7 @@ from ocpp.v201.call import Reset, SetVariables
 
 from app.database import get_db
 from app.ocpp_proto import chargepoint_manager
+from app.schemas.auth_token import AccessToken
 from app.schemas.chargepoint import (
     ChargePoint,
     ChargePointCreate,
@@ -30,7 +31,7 @@ from app.schemas.chargepoint_variable import (
 from app.models.chargepoint import ChargePoint as DbChargePoint
 from app.models.user import User as DbUser
 from app.models.chargepoint_variable import ChargepointVariable as DbChargepointVariable
-from app.security import get_api_key
+from app.security.jwt_bearer import JWTBearer
 
 router = APIRouter(
     prefix="/chargepoints",
@@ -41,16 +42,16 @@ router = APIRouter(
 async def get_chargepoints(
     skip: int = 0,
     limit: int = 20,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer()),
 ):
-    return db.query(DbChargePoint).offset(skip).limit(limit).all()
+    return db.query(DbChargePoint).order_by(DbChargePoint.identity).offset(skip).limit(limit).all()
 
 @router.get(path="/{chargepoint_id}", response_model=ChargePoint)
 async def get_chargepoint(
     chargepoint_id: UUID,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer()),
 ):
     chargepoint = db.get(DbChargePoint, chargepoint_id)
     if chargepoint is None:
@@ -60,8 +61,8 @@ async def get_chargepoint(
 @router.get(path="/{chargepoint_id}/password", response_model=ChargePointPassword)
 async def get_chargepoint_password(
     chargepoint_id: UUID,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
 ):
     chargepoint = db.get(DbChargePoint, chargepoint_id)
     if chargepoint is None:
@@ -71,8 +72,8 @@ async def get_chargepoint_password(
 @router.delete(path="/{chargepoint_id}/password", response_model=ChargePointPassword)
 async def reset_chargepoint_password(
     chargepoint_id: UUID,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
 ):
     chargepoint = db.get(DbChargePoint, chargepoint_id)
     if chargepoint is None:
@@ -84,8 +85,8 @@ async def reset_chargepoint_password(
 @router.post(path="", status_code=201, response_model=ChargePoint)
 async def create_chargepoint(
     chargepoint: ChargePointCreate,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
 ):
     chargepoint_db = DbChargePoint(
         identity=chargepoint.identity,
@@ -102,8 +103,8 @@ async def create_chargepoint(
 async def update_chargepoint(
     chargepoint_id: UUID,
     chargepoint_update: ChargePointUpdate,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
 ):
     chargepoint = db.get(DbChargePoint, chargepoint_id)
     if chargepoint is None:
@@ -116,8 +117,8 @@ async def update_chargepoint(
 @router.delete(path="/{chargepoint_id}", response_model=None)
 async def delete_chargepoint(
     chargepoint_id: UUID,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
 ):
     chargepoint = db.get(DbChargePoint, chargepoint_id)
     if chargepoint is None:
@@ -129,7 +130,7 @@ async def delete_chargepoint(
 @router.get(path="/{chargepoint_id}/status", response_model=ChargePointConnectionInfo)
 async def get_chargepoint_status(
     chargepoint_id: UUID,
-    api_key: str = Security(get_api_key)
+    token: AccessToken = Depends(JWTBearer()),
 ):
     return ChargePointConnectionInfo(
         connected=chargepoint_manager.is_connected(chargepoint_id)
@@ -139,7 +140,7 @@ async def get_chargepoint_status(
 async def reset_chargepoint(
     chargepoint_id: UUID,
     reset_request: ChargePointResetRequest,
-    api_key: str = Security(get_api_key)
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
 ):
     if chargepoint_manager.is_connected(chargepoint_id) == False:
         raise HTTPException(status_code=503, detail="Chargepoint not connected.")
@@ -156,8 +157,8 @@ async def reset_chargepoint(
 async def create_id_token_learn_request(
     chargepoint_id: UUID,
     learn_request: IdTokenLearnRequest,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
 ):
     chargepoint = db.get(DbChargePoint, chargepoint_id)
     if chargepoint is None:
@@ -188,8 +189,8 @@ async def create_id_token_learn_request(
 @router.get(path="/{chargepoint_id}/token-learning", response_model=IdTokenLearnResponse)
 async def get_id_token_learn_request(
     chargepoint_id: UUID,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
 ):
     chargepoint = db.get(DbChargePoint, chargepoint_id)
     if chargepoint is None:
@@ -206,8 +207,8 @@ async def get_id_token_learn_request(
 @router.delete(path="/{chargepoint_id}/token-learning", response_model=[])
 async def get_id_token_learn_request(
     chargepoint_id: UUID,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
 ):
     chargepoint = db.get(DbChargePoint, chargepoint_id)
     if chargepoint is None:
@@ -225,8 +226,8 @@ async def get_id_token_learn_request(
 @router.get(path="/{chargepoint_id}/variables", response_model=list[ChargepointVariable])
 async def get_chargepoint_variables(
     chargepoint_id: UUID,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
 ):
     chargepoint = db.get(DbChargePoint, chargepoint_id)
     if chargepoint is None:
@@ -239,8 +240,8 @@ async def update_chargepoint_variable(
     chargepoint_id: UUID,
     variable_id: UUID,
     variable_update: ChargepointVariableUpdate,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
 ):
     chargepoint = db.get(DbChargePoint, chargepoint_id)
     if chargepoint is None:
diff --git a/app/routers/id_token_v1.py b/app/routers/id_token_v1.py
index 017aaaa..2b89c73 100644
--- a/app/routers/id_token_v1.py
+++ b/app/routers/id_token_v1.py
@@ -1,14 +1,16 @@
 from uuid import UUID
-from fastapi import APIRouter, HTTPException, Security
-from fastapi.exceptions import RequestValidationError
+from fastapi import APIRouter, HTTPException
 from fastapi.params import Depends
+from sqlalchemy import select
 from sqlalchemy.orm import Session
 
 from app.database import get_db
+from app.schemas.auth_token import AccessToken
 from app.schemas.id_token import IdToken, IdTokenCreate, IdTokenUpdate
 from app.models.id_token import IdToken as DbIdToken
 from app.models.user import User as DbUser
-from app.security import get_api_key
+from app.schemas.user import Role
+from app.security.jwt_bearer import JWTBearer
 
 router = APIRouter(
     prefix="/id-tokens",
@@ -16,32 +18,43 @@ router = APIRouter(
 )
 
 @router.get(path="", response_model=list[IdToken])
-async def get_it_tokens(
+async def get_id_tokens(
     skip: int = 0,
     limit: int = 20,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer()),
 ):
-    return db.query(DbIdToken).offset(skip).limit(limit).all()
+    stmt = select(Session)
+    if token.role != Role.ADMINISTRATOR:
+        stmt = stmt.where(DbIdToken.owner_id == token.subject)
+    stmt = stmt.order_by(DbIdToken.id).offset(skip).limit(limit)
+    result = db.execute(stmt)
+    return result.scalars().all()
 
 @router.get(path="/{id_token_id}", response_model=IdToken)
 async def get_id_token(
     id_token_id: UUID,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer()),
 ):
-    id_token = db.get(DbIdToken, id_token_id)
+    stmt = select(DbIdToken).where(DbIdToken.id == id_token_id)
+    result = db.execute(stmt)
+    id_token = result.scalars().first()
     if id_token == None:
         raise HTTPException(status_code=404, detail="IdToken not found")
+    if token.role != Role.ADMINISTRATOR & id_token.owner_id != token.subject:
+        raise HTTPException(status_code=404, detail="IdToken not found")
     return id_token
 
 @router.post(path="", status_code=201, response_model=IdToken)
 async def create_id_token(
     create_id_token: IdTokenCreate,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
 ):
-    owner = db.get(DbUser, create_id_token.owner_id)
+    stmt = select(DbUser).where(DbUser.id == create_id_token.owner_id)
+    result = db.execute(stmt)
+    owner = result.scalars().first()
     if owner == None:
         raise HTTPException(status_code=422, detail=[{
             "loc": ["body", "owner_id"],
@@ -63,15 +76,19 @@ async def create_id_token(
 async def update_id_token(
     id_token_id: UUID,
     id_token_update: IdTokenUpdate,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
 ):
-    id_token = db.get(DbIdToken, id_token_id)
+    stmt = select(DbIdToken).where(DbIdToken.id == id_token_id)
+    result = db.execute(stmt)
+    id_token = result.scalars().first()
     if id_token is None:
         raise HTTPException(status_code=404, detail="IdToken not found")
     for key, value in id_token_update.model_dump(exclude_unset=True).items():
         if key == "owner_id":
-            owner = db.get(DbUser, value)
+            stmt = select(DbUser).where(DbUser.id == id_token_update.owner_id)
+            result = db.execute(stmt)
+            owner = result.scalars().first()
             if owner == None:
                 raise HTTPException(status_code=422, detail=[{
                     "loc": ["body", "owner_id"],
@@ -85,10 +102,12 @@ async def update_id_token(
 @router.delete(path="/{id_token_id}", response_model=None)
 async def delete_id_token(
     id_token_id: UUID,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
 ):
-    id_token = db.get(DbIdToken, id_token_id)
+    stmt = select(DbIdToken).where(DbIdToken.id == id_token_id)
+    result = db.execute(stmt)
+    id_token = result.scalars().first()
     if id_token == None:
         raise HTTPException(status_code=404, detail="IdToken not found")
     db.delete(id_token)
diff --git a/app/routers/me_v1.py b/app/routers/me_v1.py
new file mode 100644
index 0000000..635f9ed
--- /dev/null
+++ b/app/routers/me_v1.py
@@ -0,0 +1,111 @@
+from uuid import UUID
+from fastapi import APIRouter, HTTPException
+from fastapi.params import Depends
+from sqlalchemy.orm import Session as DbSession
+
+from app.database import get_db
+from app.schemas.session import Session
+from app.schemas.auth_token import AccessToken
+from app.schemas.user import PasswordUpdate, UserUpdate, User
+from app.security.jwt_bearer import JWTBearer
+from app.services import session_service, user_service
+from app.util.errors import InvalidStateError, NotFoundError
+
+router = APIRouter(prefix="/me", tags=["Me (v1)"])
+
+
+@router.get(path="", response_model=User)
+async def get_myself(
+    db: DbSession = Depends(get_db), token: AccessToken = Depends(JWTBearer())
+):
+    """
+    Get the currently authenticated user.
+    """
+    user = await user_service.get_user(db=db, id=UUID(token.subject))
+    if not user:
+        raise HTTPException(status_code=404, detail="user_not_found")
+    else:
+        return user
+
+
+@router.patch(path="", response_model=User)
+async def update_myself(
+    user_update: UserUpdate,
+    db: DbSession = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer()),
+):
+    """
+    Update the currently authenticated user. Changing the email address automatically marks it as not verified
+    and starts a new verification workflow.
+    """
+    try:
+        return await user_service.update_user(
+            db, UUID(token.subject), user_update
+        )
+    except NotFoundError:
+        raise HTTPException(status_code=404, detail="user_not_found")
+
+
+@router.post(path="/password", response_model=list[None])
+async def change_password(
+    update: PasswordUpdate,
+    db: DbSession = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer()),
+):
+    """
+    Change the password of the currently authenticated user.
+    """
+    try:
+        await user_service.change_user_password(
+            db=db, id=UUID(token.subject), update=update
+        )
+        return list()
+    except NotFoundError:
+        raise HTTPException(status_code=404, detail="user_not_found")
+    except InvalidStateError:
+        raise HTTPException(status_code=409, detail="incorrect_password")
+
+
+@router.get(path="/sessions", response_model=list[Session])
+async def get_user_sessions(
+    db: DbSession = Depends(get_db), token: AccessToken = Depends(JWTBearer())
+):
+    """
+    List the active sessions of the currently authenticated user.
+    """
+    return await session_service.get_sessions_by_user(
+        db=db, user_id=UUID(token.subject)
+    )
+
+
+@router.delete(path="/sessions", response_model=list[None])
+async def clear_user_sessions(
+    db: DbSession = Depends(get_db), token: AccessToken = Depends(JWTBearer())
+):
+    """
+    Clear all sessions of the currently authenticated user.
+    """
+    await session_service.remove_all_sessions_for_user(
+        db=db, user_id=UUID(token.subject),
+    )
+    return list()
+
+
+@router.delete(path="/sessions/{session_id}", response_model=list[None])
+async def delete_user_session(
+    session_id: UUID,
+    db: DbSession = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer()),
+):
+    """
+    Invalidate a specific session of the currently authenticated user.
+    """
+    try:
+        await session_service.remove_session_for_user(
+            db=db,
+            id=session_id,
+            user_id=UUID(token.subject),
+        )
+    except NotFoundError:
+        raise HTTPException(status_code=404, detail="session_not_found")
+    return list()
\ No newline at end of file
diff --git a/app/routers/meter_value_v1.py b/app/routers/meter_value_v1.py
index 3558600..1b91eaf 100644
--- a/app/routers/meter_value_v1.py
+++ b/app/routers/meter_value_v1.py
@@ -1,11 +1,12 @@
 from fastapi import APIRouter, Depends
-from fastapi.params import Security
+from sqlalchemy import select
 from sqlalchemy.orm import Session
 
-from app.security import get_api_key
+from app.schemas.auth_token import AccessToken
 from app.database import get_db
 from app.schemas.meter_value import MeterValue
 from app.models.meter_value import MeterValue as DbMeterValue
+from app.security.jwt_bearer import JWTBearer
 
 router = APIRouter(
     prefix="/meter-values",
@@ -16,7 +17,9 @@ router = APIRouter(
 async def get_meter_values(
     skip: int = 0,
     limit: int = 20,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
 ):
-    return db.query(DbMeterValue).offset(skip).limit(limit).all()
\ No newline at end of file
+    stmt = select(DbMeterValue).order_by(DbMeterValue.timestamp).offset(skip).limit(limit)
+    result = db.execute(stmt)
+    return result.scalars().all()
\ No newline at end of file
diff --git a/app/routers/transaction_v1.py b/app/routers/transaction_v1.py
index 60b09ee..4c6feb2 100644
--- a/app/routers/transaction_v1.py
+++ b/app/routers/transaction_v1.py
@@ -1,14 +1,18 @@
 from fastapi import APIRouter, Depends, HTTPException
-from fastapi.params import Security
+from sqlalchemy import select
 from sqlalchemy.orm import Session
 
 from ocpp.v201.call import RequestStopTransaction
 
 from app.ocpp_proto import chargepoint_manager
-from app.security import get_api_key
+from app.schemas.auth_token import AccessToken
 from app.database import get_db
+from app.schemas.meter_value import MeterValue
 from app.schemas.transaction import Transaction, RemoteTransactionStartStopResponse, TransactionStatus, RemoteTransactionStartStopStatus
 from app.models.transaction import Transaction as DbTransaction
+from app.models.meter_value import MeterValue as DbMeterValue
+from app.schemas.user import Role
+from app.security.jwt_bearer import JWTBearer
 
 router = APIRouter(
     prefix="/transactions",
@@ -19,31 +23,61 @@ router = APIRouter(
 async def get_transactions(
     skip: int = 0,
     limit: int = 20,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer()),
 ):
-    return db.query(DbTransaction).offset(skip).limit(limit).all()
+    stmt = select(DbTransaction)
+    if (token.role != Role.ADMINISTRATOR):
+        stmt = stmt.where(DbTransaction.user_id == token.subject)
+    stmt = stmt.order_by(DbTransaction.started_at).offset(skip).limit(limit)
+    result = db.execute(stmt)
+    return result.scalars().all()
 
 @router.get(path="/{transaction_id}", response_model=Transaction)
 async def get_transaction(
     transaction_id: str,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer()),
 ):
-    transaction = db.get(DbTransaction, transaction_id)
+    stmt = select(DbTransaction).where(DbTransaction.id == transaction_id)
+    result = db.execute(stmt)
+    transaction = result.scalars().first()
     if transaction == None:
         raise HTTPException(404, "Transaction not found")
+    if token.role != Role.ADMINISTRATOR & transaction.user_id != token.subject:
+        raise HTTPException(404, "Transaction not found")
     return transaction
 
+@router.get(path="/{transaction_id}/meter-values", response_model=list[MeterValue])
+async def get_transaction_meter_values(
+    transaction_id: str,
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer()),
+):
+    stmt = select(DbTransaction).where(DbTransaction.id == transaction_id)
+    result = db.execute(stmt)
+    transaction = result.scalars().first()
+    if transaction == None:
+        raise HTTPException(404, "Transaction not found")
+    if token.role != Role.ADMINISTRATOR & transaction.user_id != token.subject:
+        raise HTTPException(404, "Transaction not found")
+    stmt = select(DbMeterValue).where(DbMeterValue.transaction_id == transaction_id).order_by(DbMeterValue.timestamp)
+    result = db.execute(stmt)
+    return result.scalars().all()
+
 @router.post(path="/{transaction_id}/remote-stop", response_model=RemoteTransactionStartStopResponse)
 async def remote_stop_transaction(
     transaction_id: str,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: Session = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer()),
 ):
-    transaction = db.get(DbTransaction, transaction_id)
+    stmt = select(DbTransaction).where(DbTransaction.id == transaction_id)
+    result = db.execute(stmt)
+    transaction = result.scalars().first()
     if transaction == None:
         raise HTTPException(404, "Transaction not found")
+    if token.role != Role.ADMINISTRATOR & transaction.user_id != token.subject:
+        raise HTTPException(404, "Transaction not found")
     if transaction.status != TransactionStatus.ONGOING:
         raise HTTPException(status_code=422, detail=[{
             "loc": ["path", "transaction_id"],
@@ -51,7 +85,7 @@ async def remote_stop_transaction(
             "type": "invalid_transaction_state" 
         }])
     if chargepoint_manager.is_connected(transaction.chargepoint_id) == False:
-        raise HTTPException(status_code=503, detail="Chargepoint not connected.")
+        raise HTTPException(status_code=503, detail="chargepoint_offline")
     try:
         result = await chargepoint_manager.call(
             transaction.chargepoint_id,
@@ -63,4 +97,4 @@ async def remote_stop_transaction(
             raise HTTPException(status_code=500, detail=result.status)
         return RemoteTransactionStartStopResponse(status=result.status)
     except TimeoutError:
-        raise HTTPException(status_code=503, detail="Chargepoint didn't respond in time.")
+        raise HTTPException(status_code=503, detail="chargepoint_operation_timeout")
diff --git a/app/routers/user_v1.py b/app/routers/user_v1.py
index 5655dad..7239ece 100644
--- a/app/routers/user_v1.py
+++ b/app/routers/user_v1.py
@@ -1,12 +1,15 @@
 from uuid import UUID
-from fastapi import APIRouter, HTTPException, Security
+from fastapi import APIRouter, HTTPException
 from fastapi.params import Depends
-from sqlalchemy.orm import Session
+from sqlalchemy.orm import Session as DbSession
 
 from app.database import get_db
+from app.schemas.session import Session
+from app.schemas.auth_token import AccessToken
 from app.schemas.user import User, UserCreate, UserUpdate
-from app.models.user import User as DbUser
-from app.security import get_api_key
+from app.security.jwt_bearer import JWTBearer
+from app.services import session_service, user_service
+from app.util.errors import NotFoundError
 
 router = APIRouter(
     prefix="/users",
@@ -15,63 +18,99 @@ router = APIRouter(
 
 @router.get(path="", response_model=list[User])
 async def get_users(
+    email: str = None,
     skip: int = 0,
     limit: int = 20,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: DbSession = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
 ):
-    return db.query(DbUser).offset(skip).limit(limit).all()
-
-@router.get(path="/{user_id}", response_model=User)
-async def get_user(
-    user_id: UUID,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
-):
-    user = db.get(DbUser, user_id)
-    if user == None:
-        raise HTTPException(status_code=404, detail="User not found")
-    return user
+    return await user_service.get_users(db, skip, limit, email)
 
 @router.post(path="", status_code=201, response_model=User)
 async def create_user(
     create_user: UserCreate,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: DbSession = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
 ):
-    db_user = DbUser(
-        friendly_name=create_user.friendly_name,
-        is_active=create_user.is_active
+    user = await user_service.create_user(
+        db=db, user=create_user
     )
-    db.add(db_user)
-    db.commit()
-    db.refresh(db_user)
-    return db_user
+    return user
 
 @router.patch(path="/{user_id}", response_model=User)
 async def update_user(
     user_id: UUID,
     user_update: UserUpdate,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: DbSession = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
 ):
-    user = db.get(DbUser, user_id)
-    if user is None:
-        raise HTTPException(status_code=404, detail="User not found")
-    for key, value in user_update.model_dump(exclude_unset=True).items():
-        setattr(user, key, value)
-    db.commit()
-    return user
+    try:
+        return await user_service.update_user(
+            db, user_id, user_update
+        )
+    except NotFoundError:
+        raise HTTPException(status_code=404, detail="user_not_found")
 
 @router.delete(path="/{user_id}", response_model=None)
 async def delete_user(
     user_id: UUID,
-    api_key: str = Security(get_api_key),
-    db: Session = Depends(get_db)
+    db: DbSession = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
 ):
-    user = db.get(DbUser, user_id)
-    if user == None:
-        raise HTTPException(status_code=404, detail="User not found")
-    db.delete(user)
-    db.commit()
-    return []
+    try:
+        await user_service.remove_user(db, user_id)
+        return list()
+    except NotFoundError:
+        raise HTTPException(status_code=404, detail="user_not_found")
+
+@router.get(
+    path="/{user_id}/sessions", response_model=list[Session]
+)
+async def get_user_sessions(
+    user_id: UUID,
+    db: DbSession = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
+):
+    """
+    Query sessions of the specified user. Requires the "administrator" role.
+    """
+    return await session_service.get_sessions_by_user(db=db, user_id=user_id)
+
+
+@router.delete(
+    path="/{user_id}/sessions", response_model=list[None]
+)
+async def remove_all_user_session(
+    user_id: UUID,
+    db: DbSession = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
+):
+    """
+    Delete all sessions of the specified user. Requires the "administrator" role.
+    """
+    await session_service.remove_all_sessions_for_user(
+        db=db, user_id=user_id
+    )
+    return list()
+
+
+@router.delete(
+    path="/{user_id}/sessions/{session_id}",
+    response_model=list[None],
+)
+async def remove_user_session(
+    user_id: UUID,
+    session_id: UUID,
+    db: DbSession = Depends(get_db),
+    token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
+):
+    """
+    Delete the specified session of the specified user. Requires the "administrator" role.
+    """
+    try:
+        await session_service.remove_session_for_user(
+            db=db, id=session_id, user_id=user_id
+        )
+    except NotFoundError:
+        raise HTTPException(status_code=404, detail="session_not_found")
+    return list()
diff --git a/app/schemas/auth_token.py b/app/schemas/auth_token.py
new file mode 100644
index 0000000..bb9ee60
--- /dev/null
+++ b/app/schemas/auth_token.py
@@ -0,0 +1,21 @@
+from dataclasses import dataclass
+from datetime import datetime
+from pydantic import BaseModel
+
+from app.schemas.user import Role
+
+
+@dataclass
+class AccessToken:
+    subject: str
+    role: Role
+    session: str
+
+class TokenRefreshRequest(BaseModel):
+    refresh_token: str
+
+
+class TokenResponse(BaseModel):
+    access_token: str
+    refresh_token: str
+    not_after: datetime
diff --git a/app/schemas/session.py b/app/schemas/session.py
new file mode 100644
index 0000000..b6a320b
--- /dev/null
+++ b/app/schemas/session.py
@@ -0,0 +1,11 @@
+from datetime import datetime
+from uuid import UUID
+from pydantic import BaseModel
+
+
+class Session(BaseModel):
+    id: UUID
+    name: str
+    last_used: datetime
+
+    model_config = {"from_attributes": True}
\ No newline at end of file
diff --git a/app/schemas/user.py b/app/schemas/user.py
index d9c3349..8737aa4 100644
--- a/app/schemas/user.py
+++ b/app/schemas/user.py
@@ -1,20 +1,39 @@
+import enum
 from typing import Optional
 from uuid import UUID
-from pydantic import BaseModel
+from pydantic import BaseModel, EmailStr, Field
+
+class Role(enum.StrEnum):
+    MEMBER = "member"
+    ADMINISTRATOR = "administrator"
 
 class UserBase(BaseModel):
+    email: EmailStr = Field(max_length=60)
     friendly_name: str
     is_active: bool
 
 class UserUpdate(BaseModel):
+    email: Optional[str] = None
     friendly_name: Optional[str] = None
+
+class AdministrativeUserUpdate(UserUpdate):
     is_active: Optional[bool] = None
 
 class UserCreate(UserBase):
+    password: str = Field(max_length=100)
     pass
 
 class User(UserBase):
     id: UUID
+    role: Role
 
     class Config:
-        from_attributes = True
\ No newline at end of file
+        from_attributes = True
+
+class PasswordUpdate(BaseModel):
+    old_password: str = Field(max_length=100)
+    new_password: str = Field(max_length=100)
+
+class LoginRequest(BaseModel):
+    email: EmailStr = Field(max_length=60)
+    password: str = Field(max_length=100)
\ No newline at end of file
diff --git a/app/security.py b/app/security.py
deleted file mode 100644
index 8d8e1ff..0000000
--- a/app/security.py
+++ /dev/null
@@ -1,34 +0,0 @@
-import os
-from fastapi import HTTPException, Security, status
-from fastapi.security import APIKeyHeader, HTTPBasic
-
-basic_auth = HTTPBasic()
-
-api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
-
-def get_api_key(
-    api_key_header: str = Security(api_key_header),
-) -> str:
-    """Retrieve and validate an API key from the HTTP header.
-
-    Args:
-        api_key_header: The API key passed in the HTTP header.
-
-    Returns:
-        The validated API key.
-
-    Raises:
-        HTTPException: If the API key is invalid or missing.
-    """
-    api_key = os.getenv("CS_API_KEY", "default")
-    if api_key == "default":
-        raise HTTPException(
-            status_code=status.HTTP_401_UNAUTHORIZED,
-            detail="API key not set. Authentication not possible.",
-        )
-    if api_key_header == api_key:
-        return api_key_header
-    raise HTTPException(
-        status_code=status.HTTP_401_UNAUTHORIZED,
-        detail="Invalid or missing API Key",
-    )
\ No newline at end of file
diff --git a/app/security/__init__.py b/app/security/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/security/jwt_bearer.py b/app/security/jwt_bearer.py
new file mode 100644
index 0000000..c82a297
--- /dev/null
+++ b/app/security/jwt_bearer.py
@@ -0,0 +1,47 @@
+from typing import Optional
+from fastapi import Request, HTTPException
+from fastapi.params import Depends
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.database import get_db
+from app.services import token_service
+from app.util.errors import InsufficientPermissionsError, InvalidTokenAudienceError
+from app.schemas.auth_token import AccessToken
+
+
+class JWTBearer(HTTPBearer):
+    __required_roles: list[str] | None
+
+    def __init__(
+        self, required_roles: Optional[list[str]] = None, auto_error: bool = True
+    ):
+        self.__required_roles = required_roles
+        super(JWTBearer, self).__init__(auto_error=auto_error)
+
+    async def __call__(
+        self, request: Request, db: AsyncSession = Depends(get_db)
+    ) -> AccessToken:
+        credentials: HTTPAuthorizationCredentials | None = await super(
+            JWTBearer, self
+        ).__call__(request)
+        if credentials:
+            if not credentials.scheme == "Bearer":
+                raise HTTPException(
+                    status_code=403, detail="authentication_scheme_invalid"
+                )
+            try:
+                token = await token_service.verify_access_token(
+                    credentials.credentials, self.__required_roles
+                )
+                if not token:
+                    raise HTTPException(
+                        status_code=403, detail="token_invalid_or_expired"
+                    )
+                return token
+            except InsufficientPermissionsError:
+                raise HTTPException(status_code=403, detail="insufficient_permissions")
+            except InvalidTokenAudienceError:
+                raise HTTPException(status_code=403, detail="invalid_token_audience")
+        else:
+            raise HTTPException(status_code=403, detail="authorization_code_invalid")
\ No newline at end of file
diff --git a/app/util/websocket_auth_backend.py b/app/security/websocket_auth_backend.py
similarity index 100%
rename from app/util/websocket_auth_backend.py
rename to app/security/websocket_auth_backend.py
diff --git a/app/services/session_service.py b/app/services/session_service.py
new file mode 100644
index 0000000..f46ccd3
--- /dev/null
+++ b/app/services/session_service.py
@@ -0,0 +1,83 @@
+from datetime import datetime, UTC
+import secrets
+from uuid import UUID
+from sqlalchemy import select, delete
+from sqlalchemy.orm import Session as SqlaSession
+
+from app.models.session import Session
+from app.models.user import User
+from app.util.errors import NotFoundError
+
+
+async def get_sessions(
+    db: SqlaSession, skip: int = 0, limit: int = 20
+) -> tuple[Session]:
+    stmt = select(Session).offset(skip).limit(limit)
+    result = db.execute(stmt)
+    return result.scalars().all()
+
+
+async def get_sessions_by_user(db: SqlaSession, user_id: UUID) -> tuple[Session]:
+    stmt = select(Session).where(Session.user_id == user_id)
+    result = db.execute(stmt)
+    return result.scalars().all()
+
+
+async def create_session(db: SqlaSession, user: User, useragent: str) -> Session:
+    session = Session(
+        name=useragent,
+        refresh_token=secrets.token_urlsafe(64),
+        last_used=datetime.now(UTC),
+        user_id=user.id,
+    )
+    db.add(session)
+    db.commit()
+    db.refresh(session)
+    return session
+
+
+async def remove_session(db: SqlaSession, id: UUID):
+    session = db.get(Session, id)
+    if not session:
+        raise NotFoundError
+    db.delete(session)
+    db.commit()
+
+
+async def remove_session_for_user(
+    db: SqlaSession, id: UUID, user_id: UUID
+):
+    stmt = select(Session).where(Session.id == id and Session.user_id == user_id)
+    result = db.execute(stmt)
+    session = result.scalars().first()
+    if not session:
+        raise NotFoundError
+    db.delete(session)
+    db.commit()
+
+
+async def remove_all_sessions_for_user(db: SqlaSession, user_id: UUID):
+    stmt = delete(Session).where(Session.user_id == user_id)
+    db.execute(stmt)
+    db.commit()
+
+
+async def remove_all_sessions(db: SqlaSession):
+    stmt = delete(Session)
+    db.execute(stmt)
+    db.commit()
+
+
+async def validate_and_rotate_refresh_token(
+    db: SqlaSession, refresh_token: str
+) -> Session:
+    stmt = select(Session).where(Session.refresh_token == refresh_token)
+    result = db.execute(stmt)
+    session = result.scalars().first()
+    if not session:
+        raise NotFoundError
+    session.refresh_token = secrets.token_urlsafe(64)
+    session.last_used = datetime.now(UTC)
+
+    db.commit()
+    return session
\ No newline at end of file
diff --git a/app/services/token_service.py b/app/services/token_service.py
new file mode 100644
index 0000000..360adb0
--- /dev/null
+++ b/app/services/token_service.py
@@ -0,0 +1,69 @@
+import json
+import os
+import secrets
+from typing import Optional
+from uuid import UUID
+from jwcrypto import jwt, jwk
+from datetime import datetime, timedelta, UTC
+
+from app.models.user import User
+from app.schemas.auth_token import AccessToken
+from app.schemas.user import Role
+from app.util.errors import InsufficientPermissionsError, InvalidTokenAudienceError
+
+__signing_key = jwk.JWK.from_password(os.getenv("CS_TOKEN_SECRET", secrets.token_urlsafe(64)))
+
+async def __create_token(claims: dict) -> str:
+    default_claims = {
+        "iss": os.getenv("CS_TOKEN_ISSUER", "https://localhost:8000"),
+        "iat": datetime.now(UTC).timestamp(),
+    }
+    header = {"alg": "HS256", "typ": "JWT", "kid": "default"}
+    token = jwt.JWT(header=header, claims=(claims | default_claims))
+    token.make_signed_token(__signing_key)
+    return token.serialize()
+
+
+async def __verify_token(token: str, audience: str) -> dict | None:
+    try:
+        token = jwt.JWT(jwt=token, key=__signing_key)
+        claims = json.loads(token.claims)
+        if claims.get("aud") == audience:
+            return claims
+        else:
+            raise InvalidTokenAudienceError
+    except Exception:
+        return None
+
+
+async def create_access_token(
+    user: User, session_id: UUID
+) -> tuple[str, datetime]:
+    token_lifetime = float(os.getenv("CS_ACCESS_TOKEN_LIFETIME_SECONDS", "300"))
+    exp_time = datetime.now(UTC) + timedelta(seconds=token_lifetime)
+    claims = {
+        "aud": "access",
+        "sub": str(user.id),
+        "exp": exp_time.timestamp(),
+        "session": str(session_id),
+        "role": str(user.role),
+    }
+    return await __create_token(claims=claims), exp_time
+
+async def verify_access_token(
+    token: str, required_roles: Optional[list[str]] = None
+) -> AccessToken | None:
+    try:
+        claims = await __verify_token(token=token, audience="access")
+        if not claims:
+            return None
+        if not required_roles or claims.get("role") in required_roles:
+            return AccessToken(
+                subject=claims.get("sub"),
+                role=Role(claims.get("role")),
+                session=claims.get("session"),
+            )
+        else:
+            raise InsufficientPermissionsError
+    except InvalidTokenAudienceError:
+        pass
diff --git a/app/services/user_service.py b/app/services/user_service.py
new file mode 100644
index 0000000..b33eaa4
--- /dev/null
+++ b/app/services/user_service.py
@@ -0,0 +1,111 @@
+from uuid import UUID
+import uuid
+
+from sqlalchemy import select
+from sqlalchemy.orm import Session
+from argon2 import PasswordHasher
+from argon2.exceptions import VerifyMismatchError
+
+from app.models.user import User
+from app.schemas.user import (
+    UserCreate,
+    UserUpdate,
+    AdministrativeUserUpdate,
+    PasswordUpdate,
+    LoginRequest,
+)
+from app.util.errors import InvalidStateError, NotFoundError
+
+hasher = PasswordHasher(memory_cost=102400)
+
+async def get_user(db: Session, id: UUID):
+    return db.get(User, id)
+
+
+async def get_user_by_email(db: Session, email: str):
+    stmt = select(User).where(User.email == email)
+    result = db.execute(stmt)
+    return result.scalars().first()
+
+
+async def get_users(
+    db: Session, skip: int = 0, limit: int = 20, email: str = None
+):
+    stmt = select(User)
+    if email is not None:
+        stmt = stmt.where(User.email.like(email))
+    stmt = stmt.offset(skip).limit(limit)
+    result = db.execute(stmt)
+    return result.scalars().all()
+
+
+async def create_user(db: Session, user: UserCreate) -> User:
+    if await get_user_by_email(db=db, email=user.email):
+        raise InvalidStateError
+    hashed_password = hasher.hash(user.password)
+    db_user = User(
+        friendly_name=user.friendly_name, email=user.email, password=hashed_password
+    )
+    db.add(db_user)
+    db.commit()
+    db.refresh(db_user)
+    return db_user
+
+
+async def update_user(
+    db: Session,
+    id: UUID,
+    update: UserUpdate | AdministrativeUserUpdate,
+) -> User:
+    db_user = await get_user(db, id)
+    if db_user is None:
+        raise NotFoundError
+
+    changed_attributes = dict()
+    for key, value in update.model_dump(exclude_unset=True).items():
+        changed_attributes[key] = {"old": getattr(db_user, key), "new": value}
+        setattr(db_user, key, value)
+    db.commit()
+    return db_user
+
+
+async def change_user_password(db: Session, id: UUID, update: PasswordUpdate):
+    db_user = await get_user(db, id)
+    if db_user is None:
+        raise NotFoundError
+    try:
+        hasher.verify(hash=db_user.password, password=update.old_password)
+        db_user.password = hasher.hash(update.new_password)
+        db.commit()
+    except VerifyMismatchError:
+        raise InvalidStateError
+
+
+async def remove_user(db: Session, id: UUID):
+    db_user = await get_user(db, id)
+    if db_user is None:
+        raise NotFoundError
+    db.delete(db_user)
+    db.commit()
+
+
+async def validate_login(db: Session, login: LoginRequest) -> User | None:
+    stmt = select(User).where(User.email == login.email)
+    result = db.execute(stmt)
+    db_user = result.scalars().first()
+    if db_user is None:
+        db.commit()
+        return None
+    try:
+        hasher.verify(hash=db_user.password, password=login.password)
+        if hasher.check_needs_rehash(db_user.password):
+            db_user.password = hasher.hash(login.password)
+        if db_user.is_active:
+            db.commit()
+            return db_user
+        else:
+            db.commit()
+            return None
+    except VerifyMismatchError:
+        db.commit()
+        return None
diff --git a/app/util/errors.py b/app/util/errors.py
new file mode 100644
index 0000000..062bca0
--- /dev/null
+++ b/app/util/errors.py
@@ -0,0 +1,14 @@
+class NotFoundError(Exception):
+    pass
+
+
+class InvalidStateError(Exception):
+    pass
+
+
+class InsufficientPermissionsError(Exception):
+    pass
+
+
+class InvalidTokenAudienceError(Exception):
+    pass
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
index 268a0fe..084e2b7 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -54,6 +54,65 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)",
 test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""]
 trio = ["trio (>=0.26.1)"]
 
+[[package]]
+name = "argon2-cffi"
+version = "23.1.0"
+description = "Argon2 for Python"
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+    {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"},
+    {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"},
+]
+
+[package.dependencies]
+argon2-cffi-bindings = "*"
+
+[package.extras]
+dev = ["argon2-cffi[tests,typing]", "tox (>4)"]
+docs = ["furo", "myst-parser", "sphinx", "sphinx-copybutton", "sphinx-notfound-page"]
+tests = ["hypothesis", "pytest"]
+typing = ["mypy"]
+
+[[package]]
+name = "argon2-cffi-bindings"
+version = "21.2.0"
+description = "Low-level CFFI bindings for Argon2"
+optional = false
+python-versions = ">=3.6"
+groups = ["main"]
+files = [
+    {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"},
+    {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"},
+    {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"},
+    {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"},
+    {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"},
+    {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"},
+    {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"},
+    {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"},
+    {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"},
+    {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"},
+    {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"},
+    {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"},
+    {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"},
+    {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"},
+    {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"},
+    {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"},
+    {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"},
+    {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"},
+    {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"},
+    {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"},
+    {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"},
+]
+
+[package.dependencies]
+cffi = ">=1.0.1"
+
+[package.extras]
+dev = ["cogapp", "pre-commit", "pytest", "wheel"]
+tests = ["pytest"]
+
 [[package]]
 name = "attrs"
 version = "25.2.0"
@@ -86,6 +145,86 @@ files = [
     {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
 ]
 
+[[package]]
+name = "cffi"
+version = "1.17.1"
+description = "Foreign Function Interface for Python calling C code."
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+    {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
+    {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
+    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"},
+    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"},
+    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"},
+    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"},
+    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"},
+    {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"},
+    {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"},
+    {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"},
+    {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"},
+    {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"},
+    {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"},
+    {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"},
+    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"},
+    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"},
+    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"},
+    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"},
+    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"},
+    {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"},
+    {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"},
+    {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"},
+    {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"},
+    {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"},
+    {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"},
+    {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"},
+    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"},
+    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"},
+    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"},
+    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"},
+    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"},
+    {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"},
+    {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"},
+    {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"},
+    {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"},
+    {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"},
+    {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"},
+    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"},
+    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"},
+    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"},
+    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"},
+    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"},
+    {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"},
+    {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"},
+    {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"},
+    {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"},
+    {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"},
+    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"},
+    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"},
+    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"},
+    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"},
+    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"},
+    {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"},
+    {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"},
+    {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"},
+    {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"},
+    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"},
+    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"},
+    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"},
+    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"},
+    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"},
+    {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"},
+    {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"},
+    {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"},
+    {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"},
+    {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
+    {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
+]
+
+[package.dependencies]
+pycparser = "*"
+
 [[package]]
 name = "click"
 version = "8.1.8"
@@ -114,6 +253,64 @@ files = [
     {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
 ]
 
+[[package]]
+name = "cryptography"
+version = "44.0.2"
+description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
+optional = false
+python-versions = "!=3.9.0,!=3.9.1,>=3.7"
+groups = ["main"]
+files = [
+    {file = "cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7"},
+    {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1"},
+    {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb"},
+    {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843"},
+    {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5"},
+    {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c"},
+    {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a"},
+    {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308"},
+    {file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688"},
+    {file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7"},
+    {file = "cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79"},
+    {file = "cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa"},
+    {file = "cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3"},
+    {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639"},
+    {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd"},
+    {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181"},
+    {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea"},
+    {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699"},
+    {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9"},
+    {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23"},
+    {file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922"},
+    {file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4"},
+    {file = "cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5"},
+    {file = "cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6"},
+    {file = "cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb"},
+    {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41"},
+    {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562"},
+    {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5"},
+    {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa"},
+    {file = "cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d"},
+    {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d"},
+    {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471"},
+    {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615"},
+    {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390"},
+    {file = "cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0"},
+]
+
+[package.dependencies]
+cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
+
+[package.extras]
+docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""]
+docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
+nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""]
+pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
+sdist = ["build (>=1.0.0)"]
+ssh = ["bcrypt (>=3.1.5)"]
+test = ["certifi (>=2024)", "cryptography-vectors (==44.0.2)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
+test-randomorder = ["pytest-randomly"]
+
 [[package]]
 name = "dnspython"
 version = "2.7.0"
@@ -493,6 +690,22 @@ files = [
 [package.dependencies]
 referencing = ">=0.31.0"
 
+[[package]]
+name = "jwcrypto"
+version = "1.5.6"
+description = "Implementation of JOSE Web standards"
+optional = false
+python-versions = ">= 3.8"
+groups = ["main"]
+files = [
+    {file = "jwcrypto-1.5.6-py3-none-any.whl", hash = "sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789"},
+    {file = "jwcrypto-1.5.6.tar.gz", hash = "sha256:771a87762a0c081ae6166958a954f80848820b2ab066937dc8b8379d65b1b039"},
+]
+
+[package.dependencies]
+cryptography = ">=3.4"
+typing-extensions = ">=4.5.0"
+
 [[package]]
 name = "mako"
 version = "1.3.9"
@@ -726,6 +939,18 @@ files = [
     {file = "psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5"},
 ]
 
+[[package]]
+name = "pycparser"
+version = "2.22"
+description = "C parser in Python"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+    {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
+    {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
+]
+
 [[package]]
 name = "pydantic"
 version = "2.10.6"
@@ -1544,4 +1769,4 @@ files = [
 [metadata]
 lock-version = "2.1"
 python-versions = "^3.12"
-content-hash = "bb1221c903a3a45a36d44cd58bdf4d5794f7f8b653d3efb80ad2af8baffb4022"
+content-hash = "ae84368de3a8c75f6b38f405b9a356311c6282a9c23c2c2c1fa471d29d8d1ee2"
diff --git a/pyproject.toml b/pyproject.toml
index b65ed9d..c56e0c9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,6 +16,8 @@ sqlalchemy = "^2.0.28"
 alembic = "^1.15.1"
 gunicorn = "^22.0.0"
 psycopg2-binary = "^2.9.10"
+argon2-cffi = "^23.1.0"
+jwcrypto = "^1.5.6"
 
 [build-system]
 requires = ["poetry-core"]

From 14bcf93be3c98b9952c37f1a2e46c770920d6930 Mon Sep 17 00:00:00 2001
From: BluemediaDev <oliver@traber-info.de>
Date: Thu, 13 Mar 2025 16:38:21 +0000
Subject: [PATCH 02/10] Allow administrative user update

---
 app/routers/user_v1.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/routers/user_v1.py b/app/routers/user_v1.py
index 7239ece..d19ae13 100644
--- a/app/routers/user_v1.py
+++ b/app/routers/user_v1.py
@@ -6,7 +6,7 @@ from sqlalchemy.orm import Session as DbSession
 from app.database import get_db
 from app.schemas.session import Session
 from app.schemas.auth_token import AccessToken
-from app.schemas.user import User, UserCreate, UserUpdate
+from app.schemas.user import AdministrativeUserUpdate, User, UserCreate
 from app.security.jwt_bearer import JWTBearer
 from app.services import session_service, user_service
 from app.util.errors import NotFoundError
@@ -40,7 +40,7 @@ async def create_user(
 @router.patch(path="/{user_id}", response_model=User)
 async def update_user(
     user_id: UUID,
-    user_update: UserUpdate,
+    user_update: AdministrativeUserUpdate,
     db: DbSession = Depends(get_db),
     token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
 ):

From 49f84755f1a55048307f71c71f37372e946ab5dc Mon Sep 17 00:00:00 2001
From: BluemediaDev <oliver@traber-info.de>
Date: Thu, 13 Mar 2025 16:47:29 +0000
Subject: [PATCH 03/10] Configure alembic to use dotenv

---
 alembic/env.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/alembic/env.py b/alembic/env.py
index 8d40e79..4d69728 100644
--- a/alembic/env.py
+++ b/alembic/env.py
@@ -1,9 +1,12 @@
 from logging.config import fileConfig
 import os
 
+from dotenv import load_dotenv
 from sqlalchemy import create_engine
 from alembic import context
 
+load_dotenv()
+
 # Import models for autogenerate support
 from app.database import Base
 from app.models import *

From a1ddb43ed08ab9313d5cdeaf5b6c7fb3c7ecb23b Mon Sep 17 00:00:00 2001
From: BluemediaDev <oliver@traber-info.de>
Date: Thu, 13 Mar 2025 16:55:56 +0000
Subject: [PATCH 04/10] Handle abnormal closure of websocket

---
 app/ocpp_proto/chargepoint_manager.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/app/ocpp_proto/chargepoint_manager.py b/app/ocpp_proto/chargepoint_manager.py
index 56b2885..9739b2f 100644
--- a/app/ocpp_proto/chargepoint_manager.py
+++ b/app/ocpp_proto/chargepoint_manager.py
@@ -3,6 +3,7 @@ from typing import Any, Coroutine, Dict
 from uuid import UUID
 
 from websockets import ConnectionClosed
+from starlette.websockets import WebSocketDisconnect
 
 from app.ocpp_proto.chargepoint import ChargePoint
 
@@ -12,7 +13,7 @@ async def start(id: UUID, cp: ChargePoint):
     try:
         __active_connections[id] = cp
         await cp.start()
-    except ConnectionClosed:
+    except (ConnectionClosed, WebSocketDisconnect):
         logging.info("Charging station '%s' (%s) disconnected", cp.id, id)
         __active_connections.pop(id, None)
 

From 938582155d1708b1ec49a801f8a94c6d34be719c Mon Sep 17 00:00:00 2001
From: BluemediaDev <oliver@traber-info.de>
Date: Thu, 13 Mar 2025 22:11:20 +0100
Subject: [PATCH 05/10] Prepare monorepo

---
 Dockerfile                                                | 8 ++++----
 .../.devcontainer}/devcontainer.json                      | 0
 .../.devcontainer}/docker-compose.yml                     | 0
 .gitignore => backend/.gitignore                          | 0
 alembic.ini => backend/alembic.ini                        | 0
 {alembic => backend/alembic}/env.py                       | 0
 {alembic => backend/alembic}/script.py.mako               | 0
 .../versions/20240420_097d427dfa07-initial_migration.py   | 0
 .../20250313_c7f72154c90b-add_user_authentication.py      | 0
 {app => backend/app}/__init__.py                          | 0
 {app => backend/app}/cli.py                               | 0
 {app => backend/app}/database.py                          | 0
 {app => backend/app}/main.py                              | 0
 {app => backend/app}/models/__init__.py                   | 0
 {app => backend/app}/models/chargepoint.py                | 0
 {app => backend/app}/models/chargepoint_variable.py       | 0
 {app => backend/app}/models/connector.py                  | 0
 {app => backend/app}/models/id_token.py                   | 0
 {app => backend/app}/models/meter_value.py                | 0
 {app => backend/app}/models/session.py                    | 0
 {app => backend/app}/models/transaction.py                | 0
 {app => backend/app}/models/user.py                       | 0
 {app => backend/app}/ocpp_proto/__init__.py               | 0
 {app => backend/app}/ocpp_proto/chargepoint.py            | 0
 {app => backend/app}/ocpp_proto/chargepoint_manager.py    | 0
 {app => backend/app}/routers/__init__.py                  | 0
 {app => backend/app}/routers/auth_v1.py                   | 0
 {app => backend/app}/routers/chargepoint_v1.py            | 0
 {app => backend/app}/routers/id_token_v1.py               | 0
 {app => backend/app}/routers/me_v1.py                     | 0
 {app => backend/app}/routers/meter_value_v1.py            | 0
 {app => backend/app}/routers/ocpp_v1.py                   | 0
 {app => backend/app}/routers/transaction_v1.py            | 0
 {app => backend/app}/routers/user_v1.py                   | 0
 {app => backend/app}/schemas/__init__.py                  | 0
 {app => backend/app}/schemas/auth_token.py                | 0
 {app => backend/app}/schemas/chargepoint.py               | 0
 {app => backend/app}/schemas/chargepoint_variable.py      | 0
 {app => backend/app}/schemas/connector.py                 | 0
 {app => backend/app}/schemas/id_token.py                  | 0
 {app => backend/app}/schemas/meter_value.py               | 0
 {app => backend/app}/schemas/session.py                   | 0
 {app => backend/app}/schemas/transaction.py               | 0
 {app => backend/app}/schemas/user.py                      | 0
 {app => backend/app}/security/__init__.py                 | 0
 {app => backend/app}/security/jwt_bearer.py               | 0
 {app => backend/app}/security/websocket_auth_backend.py   | 0
 {app => backend/app}/services/__init__.py                 | 0
 {app => backend/app}/services/chargepoint_service.py      | 0
 {app => backend/app}/services/id_token_service.py         | 0
 {app => backend/app}/services/meter_value_service.py      | 0
 {app => backend/app}/services/session_service.py          | 0
 {app => backend/app}/services/token_service.py            | 0
 {app => backend/app}/services/transaction_service.py      | 0
 {app => backend/app}/services/user_service.py             | 0
 {app => backend/app}/services/variable_service.py         | 0
 {app => backend/app}/util/__init__.py                     | 0
 {app => backend/app}/util/errors.py                       | 0
 {app => backend/app}/util/websocket_wrapper.py            | 0
 poetry.lock => backend/poetry.lock                        | 0
 pyproject.toml => backend/pyproject.toml                  | 2 +-
 61 files changed, 5 insertions(+), 5 deletions(-)
 rename {.devcontainer => backend/.devcontainer}/devcontainer.json (100%)
 rename {.devcontainer => backend/.devcontainer}/docker-compose.yml (100%)
 rename .gitignore => backend/.gitignore (100%)
 rename alembic.ini => backend/alembic.ini (100%)
 rename {alembic => backend/alembic}/env.py (100%)
 rename {alembic => backend/alembic}/script.py.mako (100%)
 rename {alembic => backend/alembic}/versions/20240420_097d427dfa07-initial_migration.py (100%)
 rename {alembic => backend/alembic}/versions/20250313_c7f72154c90b-add_user_authentication.py (100%)
 rename {app => backend/app}/__init__.py (100%)
 rename {app => backend/app}/cli.py (100%)
 rename {app => backend/app}/database.py (100%)
 rename {app => backend/app}/main.py (100%)
 rename {app => backend/app}/models/__init__.py (100%)
 rename {app => backend/app}/models/chargepoint.py (100%)
 rename {app => backend/app}/models/chargepoint_variable.py (100%)
 rename {app => backend/app}/models/connector.py (100%)
 rename {app => backend/app}/models/id_token.py (100%)
 rename {app => backend/app}/models/meter_value.py (100%)
 rename {app => backend/app}/models/session.py (100%)
 rename {app => backend/app}/models/transaction.py (100%)
 rename {app => backend/app}/models/user.py (100%)
 rename {app => backend/app}/ocpp_proto/__init__.py (100%)
 rename {app => backend/app}/ocpp_proto/chargepoint.py (100%)
 rename {app => backend/app}/ocpp_proto/chargepoint_manager.py (100%)
 rename {app => backend/app}/routers/__init__.py (100%)
 rename {app => backend/app}/routers/auth_v1.py (100%)
 rename {app => backend/app}/routers/chargepoint_v1.py (100%)
 rename {app => backend/app}/routers/id_token_v1.py (100%)
 rename {app => backend/app}/routers/me_v1.py (100%)
 rename {app => backend/app}/routers/meter_value_v1.py (100%)
 rename {app => backend/app}/routers/ocpp_v1.py (100%)
 rename {app => backend/app}/routers/transaction_v1.py (100%)
 rename {app => backend/app}/routers/user_v1.py (100%)
 rename {app => backend/app}/schemas/__init__.py (100%)
 rename {app => backend/app}/schemas/auth_token.py (100%)
 rename {app => backend/app}/schemas/chargepoint.py (100%)
 rename {app => backend/app}/schemas/chargepoint_variable.py (100%)
 rename {app => backend/app}/schemas/connector.py (100%)
 rename {app => backend/app}/schemas/id_token.py (100%)
 rename {app => backend/app}/schemas/meter_value.py (100%)
 rename {app => backend/app}/schemas/session.py (100%)
 rename {app => backend/app}/schemas/transaction.py (100%)
 rename {app => backend/app}/schemas/user.py (100%)
 rename {app => backend/app}/security/__init__.py (100%)
 rename {app => backend/app}/security/jwt_bearer.py (100%)
 rename {app => backend/app}/security/websocket_auth_backend.py (100%)
 rename {app => backend/app}/services/__init__.py (100%)
 rename {app => backend/app}/services/chargepoint_service.py (100%)
 rename {app => backend/app}/services/id_token_service.py (100%)
 rename {app => backend/app}/services/meter_value_service.py (100%)
 rename {app => backend/app}/services/session_service.py (100%)
 rename {app => backend/app}/services/token_service.py (100%)
 rename {app => backend/app}/services/transaction_service.py (100%)
 rename {app => backend/app}/services/user_service.py (100%)
 rename {app => backend/app}/services/variable_service.py (100%)
 rename {app => backend/app}/util/__init__.py (100%)
 rename {app => backend/app}/util/errors.py (100%)
 rename {app => backend/app}/util/websocket_wrapper.py (100%)
 rename poetry.lock => backend/poetry.lock (100%)
 rename pyproject.toml => backend/pyproject.toml (97%)

diff --git a/Dockerfile b/Dockerfile
index a2460de..98246cf 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -45,7 +45,7 @@ RUN curl -sSL https://install.python-poetry.org | python3 -
 
 # copy project requirement files here to ensure they will be cached.
 WORKDIR $PYSETUP_PATH
-COPY poetry.lock pyproject.toml ./
+COPY backend/poetry.lock backend/pyproject.toml ./
 
 # install runtime deps - uses $POETRY_VIRTUALENVS_IN_PROJECT internally
 RUN poetry install
@@ -54,9 +54,9 @@ RUN poetry install
 FROM python-base AS production
 ENV FASTAPI_ENV=production
 COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH
-COPY alembic.ini /usr/src/
+COPY backend/alembic.ini /usr/src/
 COPY start.sh /usr/src/
-COPY ./alembic /usr/src/alembic
-COPY ./app /usr/src/app
+COPY backend/alembic /usr/src/alembic
+COPY backend/app /usr/src/app
 WORKDIR /usr/src
 CMD ["bash", "start.sh"]
\ No newline at end of file
diff --git a/.devcontainer/devcontainer.json b/backend/.devcontainer/devcontainer.json
similarity index 100%
rename from .devcontainer/devcontainer.json
rename to backend/.devcontainer/devcontainer.json
diff --git a/.devcontainer/docker-compose.yml b/backend/.devcontainer/docker-compose.yml
similarity index 100%
rename from .devcontainer/docker-compose.yml
rename to backend/.devcontainer/docker-compose.yml
diff --git a/.gitignore b/backend/.gitignore
similarity index 100%
rename from .gitignore
rename to backend/.gitignore
diff --git a/alembic.ini b/backend/alembic.ini
similarity index 100%
rename from alembic.ini
rename to backend/alembic.ini
diff --git a/alembic/env.py b/backend/alembic/env.py
similarity index 100%
rename from alembic/env.py
rename to backend/alembic/env.py
diff --git a/alembic/script.py.mako b/backend/alembic/script.py.mako
similarity index 100%
rename from alembic/script.py.mako
rename to backend/alembic/script.py.mako
diff --git a/alembic/versions/20240420_097d427dfa07-initial_migration.py b/backend/alembic/versions/20240420_097d427dfa07-initial_migration.py
similarity index 100%
rename from alembic/versions/20240420_097d427dfa07-initial_migration.py
rename to backend/alembic/versions/20240420_097d427dfa07-initial_migration.py
diff --git a/alembic/versions/20250313_c7f72154c90b-add_user_authentication.py b/backend/alembic/versions/20250313_c7f72154c90b-add_user_authentication.py
similarity index 100%
rename from alembic/versions/20250313_c7f72154c90b-add_user_authentication.py
rename to backend/alembic/versions/20250313_c7f72154c90b-add_user_authentication.py
diff --git a/app/__init__.py b/backend/app/__init__.py
similarity index 100%
rename from app/__init__.py
rename to backend/app/__init__.py
diff --git a/app/cli.py b/backend/app/cli.py
similarity index 100%
rename from app/cli.py
rename to backend/app/cli.py
diff --git a/app/database.py b/backend/app/database.py
similarity index 100%
rename from app/database.py
rename to backend/app/database.py
diff --git a/app/main.py b/backend/app/main.py
similarity index 100%
rename from app/main.py
rename to backend/app/main.py
diff --git a/app/models/__init__.py b/backend/app/models/__init__.py
similarity index 100%
rename from app/models/__init__.py
rename to backend/app/models/__init__.py
diff --git a/app/models/chargepoint.py b/backend/app/models/chargepoint.py
similarity index 100%
rename from app/models/chargepoint.py
rename to backend/app/models/chargepoint.py
diff --git a/app/models/chargepoint_variable.py b/backend/app/models/chargepoint_variable.py
similarity index 100%
rename from app/models/chargepoint_variable.py
rename to backend/app/models/chargepoint_variable.py
diff --git a/app/models/connector.py b/backend/app/models/connector.py
similarity index 100%
rename from app/models/connector.py
rename to backend/app/models/connector.py
diff --git a/app/models/id_token.py b/backend/app/models/id_token.py
similarity index 100%
rename from app/models/id_token.py
rename to backend/app/models/id_token.py
diff --git a/app/models/meter_value.py b/backend/app/models/meter_value.py
similarity index 100%
rename from app/models/meter_value.py
rename to backend/app/models/meter_value.py
diff --git a/app/models/session.py b/backend/app/models/session.py
similarity index 100%
rename from app/models/session.py
rename to backend/app/models/session.py
diff --git a/app/models/transaction.py b/backend/app/models/transaction.py
similarity index 100%
rename from app/models/transaction.py
rename to backend/app/models/transaction.py
diff --git a/app/models/user.py b/backend/app/models/user.py
similarity index 100%
rename from app/models/user.py
rename to backend/app/models/user.py
diff --git a/app/ocpp_proto/__init__.py b/backend/app/ocpp_proto/__init__.py
similarity index 100%
rename from app/ocpp_proto/__init__.py
rename to backend/app/ocpp_proto/__init__.py
diff --git a/app/ocpp_proto/chargepoint.py b/backend/app/ocpp_proto/chargepoint.py
similarity index 100%
rename from app/ocpp_proto/chargepoint.py
rename to backend/app/ocpp_proto/chargepoint.py
diff --git a/app/ocpp_proto/chargepoint_manager.py b/backend/app/ocpp_proto/chargepoint_manager.py
similarity index 100%
rename from app/ocpp_proto/chargepoint_manager.py
rename to backend/app/ocpp_proto/chargepoint_manager.py
diff --git a/app/routers/__init__.py b/backend/app/routers/__init__.py
similarity index 100%
rename from app/routers/__init__.py
rename to backend/app/routers/__init__.py
diff --git a/app/routers/auth_v1.py b/backend/app/routers/auth_v1.py
similarity index 100%
rename from app/routers/auth_v1.py
rename to backend/app/routers/auth_v1.py
diff --git a/app/routers/chargepoint_v1.py b/backend/app/routers/chargepoint_v1.py
similarity index 100%
rename from app/routers/chargepoint_v1.py
rename to backend/app/routers/chargepoint_v1.py
diff --git a/app/routers/id_token_v1.py b/backend/app/routers/id_token_v1.py
similarity index 100%
rename from app/routers/id_token_v1.py
rename to backend/app/routers/id_token_v1.py
diff --git a/app/routers/me_v1.py b/backend/app/routers/me_v1.py
similarity index 100%
rename from app/routers/me_v1.py
rename to backend/app/routers/me_v1.py
diff --git a/app/routers/meter_value_v1.py b/backend/app/routers/meter_value_v1.py
similarity index 100%
rename from app/routers/meter_value_v1.py
rename to backend/app/routers/meter_value_v1.py
diff --git a/app/routers/ocpp_v1.py b/backend/app/routers/ocpp_v1.py
similarity index 100%
rename from app/routers/ocpp_v1.py
rename to backend/app/routers/ocpp_v1.py
diff --git a/app/routers/transaction_v1.py b/backend/app/routers/transaction_v1.py
similarity index 100%
rename from app/routers/transaction_v1.py
rename to backend/app/routers/transaction_v1.py
diff --git a/app/routers/user_v1.py b/backend/app/routers/user_v1.py
similarity index 100%
rename from app/routers/user_v1.py
rename to backend/app/routers/user_v1.py
diff --git a/app/schemas/__init__.py b/backend/app/schemas/__init__.py
similarity index 100%
rename from app/schemas/__init__.py
rename to backend/app/schemas/__init__.py
diff --git a/app/schemas/auth_token.py b/backend/app/schemas/auth_token.py
similarity index 100%
rename from app/schemas/auth_token.py
rename to backend/app/schemas/auth_token.py
diff --git a/app/schemas/chargepoint.py b/backend/app/schemas/chargepoint.py
similarity index 100%
rename from app/schemas/chargepoint.py
rename to backend/app/schemas/chargepoint.py
diff --git a/app/schemas/chargepoint_variable.py b/backend/app/schemas/chargepoint_variable.py
similarity index 100%
rename from app/schemas/chargepoint_variable.py
rename to backend/app/schemas/chargepoint_variable.py
diff --git a/app/schemas/connector.py b/backend/app/schemas/connector.py
similarity index 100%
rename from app/schemas/connector.py
rename to backend/app/schemas/connector.py
diff --git a/app/schemas/id_token.py b/backend/app/schemas/id_token.py
similarity index 100%
rename from app/schemas/id_token.py
rename to backend/app/schemas/id_token.py
diff --git a/app/schemas/meter_value.py b/backend/app/schemas/meter_value.py
similarity index 100%
rename from app/schemas/meter_value.py
rename to backend/app/schemas/meter_value.py
diff --git a/app/schemas/session.py b/backend/app/schemas/session.py
similarity index 100%
rename from app/schemas/session.py
rename to backend/app/schemas/session.py
diff --git a/app/schemas/transaction.py b/backend/app/schemas/transaction.py
similarity index 100%
rename from app/schemas/transaction.py
rename to backend/app/schemas/transaction.py
diff --git a/app/schemas/user.py b/backend/app/schemas/user.py
similarity index 100%
rename from app/schemas/user.py
rename to backend/app/schemas/user.py
diff --git a/app/security/__init__.py b/backend/app/security/__init__.py
similarity index 100%
rename from app/security/__init__.py
rename to backend/app/security/__init__.py
diff --git a/app/security/jwt_bearer.py b/backend/app/security/jwt_bearer.py
similarity index 100%
rename from app/security/jwt_bearer.py
rename to backend/app/security/jwt_bearer.py
diff --git a/app/security/websocket_auth_backend.py b/backend/app/security/websocket_auth_backend.py
similarity index 100%
rename from app/security/websocket_auth_backend.py
rename to backend/app/security/websocket_auth_backend.py
diff --git a/app/services/__init__.py b/backend/app/services/__init__.py
similarity index 100%
rename from app/services/__init__.py
rename to backend/app/services/__init__.py
diff --git a/app/services/chargepoint_service.py b/backend/app/services/chargepoint_service.py
similarity index 100%
rename from app/services/chargepoint_service.py
rename to backend/app/services/chargepoint_service.py
diff --git a/app/services/id_token_service.py b/backend/app/services/id_token_service.py
similarity index 100%
rename from app/services/id_token_service.py
rename to backend/app/services/id_token_service.py
diff --git a/app/services/meter_value_service.py b/backend/app/services/meter_value_service.py
similarity index 100%
rename from app/services/meter_value_service.py
rename to backend/app/services/meter_value_service.py
diff --git a/app/services/session_service.py b/backend/app/services/session_service.py
similarity index 100%
rename from app/services/session_service.py
rename to backend/app/services/session_service.py
diff --git a/app/services/token_service.py b/backend/app/services/token_service.py
similarity index 100%
rename from app/services/token_service.py
rename to backend/app/services/token_service.py
diff --git a/app/services/transaction_service.py b/backend/app/services/transaction_service.py
similarity index 100%
rename from app/services/transaction_service.py
rename to backend/app/services/transaction_service.py
diff --git a/app/services/user_service.py b/backend/app/services/user_service.py
similarity index 100%
rename from app/services/user_service.py
rename to backend/app/services/user_service.py
diff --git a/app/services/variable_service.py b/backend/app/services/variable_service.py
similarity index 100%
rename from app/services/variable_service.py
rename to backend/app/services/variable_service.py
diff --git a/app/util/__init__.py b/backend/app/util/__init__.py
similarity index 100%
rename from app/util/__init__.py
rename to backend/app/util/__init__.py
diff --git a/app/util/errors.py b/backend/app/util/errors.py
similarity index 100%
rename from app/util/errors.py
rename to backend/app/util/errors.py
diff --git a/app/util/websocket_wrapper.py b/backend/app/util/websocket_wrapper.py
similarity index 100%
rename from app/util/websocket_wrapper.py
rename to backend/app/util/websocket_wrapper.py
diff --git a/poetry.lock b/backend/poetry.lock
similarity index 100%
rename from poetry.lock
rename to backend/poetry.lock
diff --git a/pyproject.toml b/backend/pyproject.toml
similarity index 97%
rename from pyproject.toml
rename to backend/pyproject.toml
index c56e0c9..fd456a4 100644
--- a/pyproject.toml
+++ b/backend/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "simple-ocpp-cs"
-version = "0.1.0"
+version = "0.2.0"
 description = "A simple OCPP 2.0.1 compliant central system"
 authors = ["Bluemedia <hi@bluemedia.dev>"]
 readme = "README.md"

From 60542935c832926b12d75cb43b7735bc72cb6082 Mon Sep 17 00:00:00 2001
From: BluemediaDev <oliver@traber-info.de>
Date: Thu, 13 Mar 2025 21:47:18 +0000
Subject: [PATCH 06/10] Update dev container configuration for monorepo

---
 .../backend}/devcontainer.json                |  2 +-
 .../backend}/docker-compose.yml               |  2 +-
 .devcontainer/frontend/devcontainer.json      | 27 +++++++++++++++++++
 .devcontainer/frontend/docker-compose.yml     |  6 +++++
 backend/simple-ocpp-cs.code-workspace         | 12 +++++++++
 frontend/simple-ocpp-cs.code-workspace        | 12 +++++++++
 6 files changed, 59 insertions(+), 2 deletions(-)
 rename {backend/.devcontainer => .devcontainer/backend}/devcontainer.json (93%)
 rename {backend/.devcontainer => .devcontainer/backend}/docker-compose.yml (91%)
 create mode 100644 .devcontainer/frontend/devcontainer.json
 create mode 100644 .devcontainer/frontend/docker-compose.yml
 create mode 100644 backend/simple-ocpp-cs.code-workspace
 create mode 100644 frontend/simple-ocpp-cs.code-workspace

diff --git a/backend/.devcontainer/devcontainer.json b/.devcontainer/backend/devcontainer.json
similarity index 93%
rename from backend/.devcontainer/devcontainer.json
rename to .devcontainer/backend/devcontainer.json
index e81c5b5..39c5ecc 100644
--- a/backend/.devcontainer/devcontainer.json
+++ b/.devcontainer/backend/devcontainer.json
@@ -5,7 +5,7 @@
 
 	"dockerComposeFile": "docker-compose.yml",
 	"service": "app",
-	"workspaceFolder": "/workspace",
+	"workspaceFolder": "/simple-ocpp-cs",
 	"shutdownAction": "stopCompose",
 
 	// Use 'forwardPorts' to make a list of ports inside the container available locally.
diff --git a/backend/.devcontainer/docker-compose.yml b/.devcontainer/backend/docker-compose.yml
similarity index 91%
rename from backend/.devcontainer/docker-compose.yml
rename to .devcontainer/backend/docker-compose.yml
index 7fd182f..8ef483e 100644
--- a/backend/.devcontainer/docker-compose.yml
+++ b/.devcontainer/backend/docker-compose.yml
@@ -2,7 +2,7 @@ services:
   app:
     image: mcr.microsoft.com/devcontainers/python:3.12-bookworm
     volumes:
-      - ..:/workspace:cached
+      - ../..:/simple-ocpp-cs:cached
     command: sleep infinity
 
   db:
diff --git a/.devcontainer/frontend/devcontainer.json b/.devcontainer/frontend/devcontainer.json
new file mode 100644
index 0000000..f5ace76
--- /dev/null
+++ b/.devcontainer/frontend/devcontainer.json
@@ -0,0 +1,27 @@
+// For format details, see https://aka.ms/devcontainer.json. For config options, see the
+// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node
+{
+	"name": "Node.js",
+
+	"dockerComposeFile": "docker-compose.yml",
+	"service": "app",
+	"workspaceFolder": "/simple-ocpp-cs",
+	"shutdownAction": "stopCompose",
+
+	// Use 'forwardPorts' to make a list of ports inside the container available locally.
+	"forwardPorts": [
+		5173
+	],
+
+	// Configure tool-specific properties.
+	"customizations": {
+		"vscode": {
+			"extensions": [
+				"Lokalise.i18n-ally",
+				"dbaeumer.vscode-eslint",
+				"esbenp.prettier-vscode",
+				"eamodio.gitlens"
+			]
+		}
+	}
+}
\ No newline at end of file
diff --git a/.devcontainer/frontend/docker-compose.yml b/.devcontainer/frontend/docker-compose.yml
new file mode 100644
index 0000000..edc0699
--- /dev/null
+++ b/.devcontainer/frontend/docker-compose.yml
@@ -0,0 +1,6 @@
+services:
+  app:
+    image: mcr.microsoft.com/devcontainers/javascript-node:1-22-bookworm
+    volumes:
+      - ../..:/simple-ocpp-cs:cached
+    command: sleep infinity
diff --git a/backend/simple-ocpp-cs.code-workspace b/backend/simple-ocpp-cs.code-workspace
new file mode 100644
index 0000000..0f6be18
--- /dev/null
+++ b/backend/simple-ocpp-cs.code-workspace
@@ -0,0 +1,12 @@
+{
+  "folders": [
+    {
+      "name": "Backend",
+      "path": "../backend"
+    },
+    {
+      "name": "Root",
+      "path": "../"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/frontend/simple-ocpp-cs.code-workspace b/frontend/simple-ocpp-cs.code-workspace
new file mode 100644
index 0000000..ba9fc2d
--- /dev/null
+++ b/frontend/simple-ocpp-cs.code-workspace
@@ -0,0 +1,12 @@
+{
+  "folders": [
+    {
+      "name": "Frontend",
+      "path": "../frontend"
+    },
+    {
+      "name": "Root",
+      "path": "../"
+    }
+  ]
+}
\ No newline at end of file

From 5b62099b3dfa34e92abbb0ff74c6a6ac8757f809 Mon Sep 17 00:00:00 2001
From: BluemediaDev <oliver@traber-info.de>
Date: Thu, 13 Mar 2025 22:47:06 +0000
Subject: [PATCH 07/10] Add skeleton frontend

---
 .devcontainer/frontend/devcontainer.json |    1 +
 frontend/.gitignore                      |   23 +
 frontend/.npmrc                          |    1 +
 frontend/.prettierignore                 |    4 +
 frontend/.prettierrc                     |   11 +
 frontend/README.md                       |   38 +
 frontend/eslint.config.js                |   39 +
 frontend/package-lock.json               | 3800 ++++++++++++++++++++++
 frontend/package.json                    |   40 +
 frontend/simple-ocpp-cs.code-workspace   |   15 +-
 frontend/src/app.d.ts                    |   13 +
 frontend/src/app.html                    |   12 +
 frontend/src/lib/index.ts                |    1 +
 frontend/src/routes/+layout.ts           |    1 +
 frontend/src/routes/+page.svelte         |    6 +
 frontend/src/style.css                   |    6 +
 frontend/static/favicon.png              |  Bin 0 -> 1571 bytes
 frontend/svelte.config.js                |   17 +
 frontend/tsconfig.json                   |   19 +
 frontend/vite.config.ts                  |   10 +
 20 files changed, 4052 insertions(+), 5 deletions(-)
 create mode 100644 frontend/.gitignore
 create mode 100644 frontend/.npmrc
 create mode 100644 frontend/.prettierignore
 create mode 100644 frontend/.prettierrc
 create mode 100644 frontend/README.md
 create mode 100644 frontend/eslint.config.js
 create mode 100644 frontend/package-lock.json
 create mode 100644 frontend/package.json
 create mode 100644 frontend/src/app.d.ts
 create mode 100644 frontend/src/app.html
 create mode 100644 frontend/src/lib/index.ts
 create mode 100644 frontend/src/routes/+layout.ts
 create mode 100644 frontend/src/routes/+page.svelte
 create mode 100644 frontend/src/style.css
 create mode 100644 frontend/static/favicon.png
 create mode 100644 frontend/svelte.config.js
 create mode 100644 frontend/tsconfig.json
 create mode 100644 frontend/vite.config.ts

diff --git a/.devcontainer/frontend/devcontainer.json b/.devcontainer/frontend/devcontainer.json
index f5ace76..9b8b456 100644
--- a/.devcontainer/frontend/devcontainer.json
+++ b/.devcontainer/frontend/devcontainer.json
@@ -17,6 +17,7 @@
 	"customizations": {
 		"vscode": {
 			"extensions": [
+				"svelte.svelte-vscode",
 				"Lokalise.i18n-ally",
 				"dbaeumer.vscode-eslint",
 				"esbenp.prettier-vscode",
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..3b462cb
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,23 @@
+node_modules
+
+# Output
+.output
+.vercel
+.netlify
+.wrangler
+/.svelte-kit
+/build
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Env
+.env
+.env.*
+!.env.example
+!.env.test
+
+# Vite
+vite.config.js.timestamp-*
+vite.config.ts.timestamp-*
diff --git a/frontend/.npmrc b/frontend/.npmrc
new file mode 100644
index 0000000..b6f27f1
--- /dev/null
+++ b/frontend/.npmrc
@@ -0,0 +1 @@
+engine-strict=true
diff --git a/frontend/.prettierignore b/frontend/.prettierignore
new file mode 100644
index 0000000..ab78a95
--- /dev/null
+++ b/frontend/.prettierignore
@@ -0,0 +1,4 @@
+# Package Managers
+package-lock.json
+pnpm-lock.yaml
+yarn.lock
diff --git a/frontend/.prettierrc b/frontend/.prettierrc
new file mode 100644
index 0000000..2b4eef9
--- /dev/null
+++ b/frontend/.prettierrc
@@ -0,0 +1,11 @@
+{
+  "trailingComma": "es5",
+  "semi": false,
+  "singleQuote": true,
+  "useTabs": false,
+  "tabWidth": 2,
+  "quoteProps": "consistent",
+  "bracketSpacing": true,
+  "arrowParens": "always",
+  "printWidth": 100
+}
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..b5b2950
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,38 @@
+# sv
+
+Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
+
+## Creating a project
+
+If you're seeing this, you've probably already done this step. Congrats!
+
+```bash
+# create a new project in the current directory
+npx sv create
+
+# create a new project in my-app
+npx sv create my-app
+```
+
+## Developing
+
+Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
+
+```bash
+npm run dev
+
+# or start the server and open the app in a new browser tab
+npm run dev -- --open
+```
+
+## Building
+
+To create a production version of your app:
+
+```bash
+npm run build
+```
+
+You can preview the production build with `npm run preview`.
+
+> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
new file mode 100644
index 0000000..081030e
--- /dev/null
+++ b/frontend/eslint.config.js
@@ -0,0 +1,39 @@
+import prettier from 'eslint-config-prettier'
+import js from '@eslint/js'
+import { includeIgnoreFile } from '@eslint/compat'
+import svelte from 'eslint-plugin-svelte'
+import globals from 'globals'
+import { fileURLToPath } from 'node:url'
+import ts from 'typescript-eslint'
+import svelteConfig from './svelte.config.js'
+const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url))
+
+export default ts.config(
+  includeIgnoreFile(gitignorePath),
+  js.configs.recommended,
+  ...ts.configs.recommended,
+  ...svelte.configs.recommended,
+  prettier,
+  ...svelte.configs.prettier,
+  {
+    languageOptions: {
+      globals: {
+        ...globals.browser,
+        ...globals.node,
+      },
+    },
+  },
+  {
+    files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
+    ignores: ['eslint.config.js', 'svelte.config.js'],
+
+    languageOptions: {
+      parserOptions: {
+        projectService: true,
+        extraFileExtensions: ['.svelte'],
+        parser: ts.parser,
+        svelteConfig,
+      },
+    },
+  }
+)
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..0fc5b86
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,3800 @@
+{
+	"name": "frontend",
+	"version": "0.0.1",
+	"lockfileVersion": 3,
+	"requires": true,
+	"packages": {
+		"": {
+			"name": "frontend",
+			"version": "0.0.1",
+			"dependencies": {
+				"@tailwindcss/vite": "^4.0.14",
+				"daisyui": "^5.0.3",
+				"tailwindcss": "^4.0.14"
+			},
+			"devDependencies": {
+				"@eslint/compat": "^1.2.5",
+				"@eslint/js": "^9.18.0",
+				"@sveltejs/adapter-auto": "^4.0.0",
+				"@sveltejs/adapter-static": "^3.0.8",
+				"@sveltejs/kit": "^2.16.0",
+				"@sveltejs/vite-plugin-svelte": "^5.0.0",
+				"eslint": "^9.18.0",
+				"eslint-config-prettier": "^10.0.1",
+				"eslint-plugin-svelte": "^3.0.0",
+				"globals": "^16.0.0",
+				"prettier": "^3.4.2",
+				"prettier-plugin-svelte": "^3.3.3",
+				"svelte": "^5.0.0",
+				"svelte-check": "^4.0.0",
+				"typescript": "^5.0.0",
+				"typescript-eslint": "^8.20.0",
+				"vite": "^6.0.0"
+			}
+		},
+		"node_modules/@ampproject/remapping": {
+			"version": "2.3.0",
+			"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+			"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"dependencies": {
+				"@jridgewell/gen-mapping": "^0.3.5",
+				"@jridgewell/trace-mapping": "^0.3.24"
+			},
+			"engines": {
+				"node": ">=6.0.0"
+			}
+		},
+		"node_modules/@esbuild/aix-ppc64": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz",
+			"integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==",
+			"cpu": [
+				"ppc64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"aix"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/android-arm": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz",
+			"integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==",
+			"cpu": [
+				"arm"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"android"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/android-arm64": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz",
+			"integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"android"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/android-x64": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz",
+			"integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"android"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/darwin-arm64": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz",
+			"integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/darwin-x64": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz",
+			"integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/freebsd-arm64": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz",
+			"integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"freebsd"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/freebsd-x64": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz",
+			"integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"freebsd"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/linux-arm": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz",
+			"integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==",
+			"cpu": [
+				"arm"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/linux-arm64": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz",
+			"integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/linux-ia32": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz",
+			"integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==",
+			"cpu": [
+				"ia32"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/linux-loong64": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz",
+			"integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==",
+			"cpu": [
+				"loong64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/linux-mips64el": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz",
+			"integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==",
+			"cpu": [
+				"mips64el"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/linux-ppc64": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz",
+			"integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==",
+			"cpu": [
+				"ppc64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/linux-riscv64": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz",
+			"integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==",
+			"cpu": [
+				"riscv64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/linux-s390x": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz",
+			"integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==",
+			"cpu": [
+				"s390x"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/linux-x64": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz",
+			"integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/netbsd-arm64": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz",
+			"integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"netbsd"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/netbsd-x64": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz",
+			"integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"netbsd"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/openbsd-arm64": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz",
+			"integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"openbsd"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/openbsd-x64": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz",
+			"integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"openbsd"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/sunos-x64": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz",
+			"integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"sunos"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/win32-arm64": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz",
+			"integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"win32"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/win32-ia32": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz",
+			"integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==",
+			"cpu": [
+				"ia32"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"win32"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@esbuild/win32-x64": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz",
+			"integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"win32"
+			],
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/@eslint-community/eslint-utils": {
+			"version": "4.5.1",
+			"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz",
+			"integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"eslint-visitor-keys": "^3.4.3"
+			},
+			"engines": {
+				"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+			},
+			"funding": {
+				"url": "https://opencollective.com/eslint"
+			},
+			"peerDependencies": {
+				"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+			}
+		},
+		"node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+			"version": "3.4.3",
+			"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+			"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"engines": {
+				"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+			},
+			"funding": {
+				"url": "https://opencollective.com/eslint"
+			}
+		},
+		"node_modules/@eslint-community/regexpp": {
+			"version": "4.12.1",
+			"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+			"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+			}
+		},
+		"node_modules/@eslint/compat": {
+			"version": "1.2.7",
+			"resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.7.tgz",
+			"integrity": "sha512-xvv7hJE32yhegJ8xNAnb62ggiAwTYHBpUCWhRxEj/ksvgDJuSXfoDkBcRYaYNFiJ+jH0IE3K16hd+xXzhBgNbg==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			},
+			"peerDependencies": {
+				"eslint": "^9.10.0"
+			},
+			"peerDependenciesMeta": {
+				"eslint": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@eslint/config-array": {
+			"version": "0.19.2",
+			"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz",
+			"integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"dependencies": {
+				"@eslint/object-schema": "^2.1.6",
+				"debug": "^4.3.1",
+				"minimatch": "^3.1.2"
+			},
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			}
+		},
+		"node_modules/@eslint/config-helpers": {
+			"version": "0.1.0",
+			"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.1.0.tgz",
+			"integrity": "sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			}
+		},
+		"node_modules/@eslint/core": {
+			"version": "0.12.0",
+			"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz",
+			"integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"dependencies": {
+				"@types/json-schema": "^7.0.15"
+			},
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			}
+		},
+		"node_modules/@eslint/eslintrc": {
+			"version": "3.3.0",
+			"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz",
+			"integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"ajv": "^6.12.4",
+				"debug": "^4.3.2",
+				"espree": "^10.0.1",
+				"globals": "^14.0.0",
+				"ignore": "^5.2.0",
+				"import-fresh": "^3.2.1",
+				"js-yaml": "^4.1.0",
+				"minimatch": "^3.1.2",
+				"strip-json-comments": "^3.1.1"
+			},
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			},
+			"funding": {
+				"url": "https://opencollective.com/eslint"
+			}
+		},
+		"node_modules/@eslint/eslintrc/node_modules/globals": {
+			"version": "14.0.0",
+			"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+			"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=18"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/sindresorhus"
+			}
+		},
+		"node_modules/@eslint/js": {
+			"version": "9.22.0",
+			"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.22.0.tgz",
+			"integrity": "sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			}
+		},
+		"node_modules/@eslint/object-schema": {
+			"version": "2.1.6",
+			"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
+			"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			}
+		},
+		"node_modules/@eslint/plugin-kit": {
+			"version": "0.2.7",
+			"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz",
+			"integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"dependencies": {
+				"@eslint/core": "^0.12.0",
+				"levn": "^0.4.1"
+			},
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			}
+		},
+		"node_modules/@humanfs/core": {
+			"version": "0.19.1",
+			"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+			"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"engines": {
+				"node": ">=18.18.0"
+			}
+		},
+		"node_modules/@humanfs/node": {
+			"version": "0.16.6",
+			"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
+			"integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"dependencies": {
+				"@humanfs/core": "^0.19.1",
+				"@humanwhocodes/retry": "^0.3.0"
+			},
+			"engines": {
+				"node": ">=18.18.0"
+			}
+		},
+		"node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
+			"version": "0.3.1",
+			"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
+			"integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"engines": {
+				"node": ">=18.18"
+			},
+			"funding": {
+				"type": "github",
+				"url": "https://github.com/sponsors/nzakas"
+			}
+		},
+		"node_modules/@humanwhocodes/module-importer": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+			"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"engines": {
+				"node": ">=12.22"
+			},
+			"funding": {
+				"type": "github",
+				"url": "https://github.com/sponsors/nzakas"
+			}
+		},
+		"node_modules/@humanwhocodes/retry": {
+			"version": "0.4.2",
+			"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz",
+			"integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"engines": {
+				"node": ">=18.18"
+			},
+			"funding": {
+				"type": "github",
+				"url": "https://github.com/sponsors/nzakas"
+			}
+		},
+		"node_modules/@jridgewell/gen-mapping": {
+			"version": "0.3.8",
+			"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
+			"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@jridgewell/set-array": "^1.2.1",
+				"@jridgewell/sourcemap-codec": "^1.4.10",
+				"@jridgewell/trace-mapping": "^0.3.24"
+			},
+			"engines": {
+				"node": ">=6.0.0"
+			}
+		},
+		"node_modules/@jridgewell/resolve-uri": {
+			"version": "3.1.2",
+			"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+			"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=6.0.0"
+			}
+		},
+		"node_modules/@jridgewell/set-array": {
+			"version": "1.2.1",
+			"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+			"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=6.0.0"
+			}
+		},
+		"node_modules/@jridgewell/sourcemap-codec": {
+			"version": "1.5.0",
+			"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+			"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/@jridgewell/trace-mapping": {
+			"version": "0.3.25",
+			"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+			"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@jridgewell/resolve-uri": "^3.1.0",
+				"@jridgewell/sourcemap-codec": "^1.4.14"
+			}
+		},
+		"node_modules/@nodelib/fs.scandir": {
+			"version": "2.1.5",
+			"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+			"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@nodelib/fs.stat": "2.0.5",
+				"run-parallel": "^1.1.9"
+			},
+			"engines": {
+				"node": ">= 8"
+			}
+		},
+		"node_modules/@nodelib/fs.stat": {
+			"version": "2.0.5",
+			"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+			"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">= 8"
+			}
+		},
+		"node_modules/@nodelib/fs.walk": {
+			"version": "1.2.8",
+			"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+			"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@nodelib/fs.scandir": "2.1.5",
+				"fastq": "^1.6.0"
+			},
+			"engines": {
+				"node": ">= 8"
+			}
+		},
+		"node_modules/@polka/url": {
+			"version": "1.0.0-next.28",
+			"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz",
+			"integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/@rollup/rollup-android-arm-eabi": {
+			"version": "4.35.0",
+			"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.35.0.tgz",
+			"integrity": "sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ==",
+			"cpu": [
+				"arm"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"android"
+			]
+		},
+		"node_modules/@rollup/rollup-android-arm64": {
+			"version": "4.35.0",
+			"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.35.0.tgz",
+			"integrity": "sha512-FtKddj9XZudurLhdJnBl9fl6BwCJ3ky8riCXjEw3/UIbjmIY58ppWwPEvU3fNu+W7FUsAsB1CdH+7EQE6CXAPA==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"android"
+			]
+		},
+		"node_modules/@rollup/rollup-darwin-arm64": {
+			"version": "4.35.0",
+			"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.35.0.tgz",
+			"integrity": "sha512-Uk+GjOJR6CY844/q6r5DR/6lkPFOw0hjfOIzVx22THJXMxktXG6CbejseJFznU8vHcEBLpiXKY3/6xc+cBm65Q==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"darwin"
+			]
+		},
+		"node_modules/@rollup/rollup-darwin-x64": {
+			"version": "4.35.0",
+			"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.35.0.tgz",
+			"integrity": "sha512-3IrHjfAS6Vkp+5bISNQnPogRAW5GAV1n+bNCrDwXmfMHbPl5EhTmWtfmwlJxFRUCBZ+tZ/OxDyU08aF6NI/N5Q==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"darwin"
+			]
+		},
+		"node_modules/@rollup/rollup-freebsd-arm64": {
+			"version": "4.35.0",
+			"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.35.0.tgz",
+			"integrity": "sha512-sxjoD/6F9cDLSELuLNnY0fOrM9WA0KrM0vWm57XhrIMf5FGiN8D0l7fn+bpUeBSU7dCgPV2oX4zHAsAXyHFGcQ==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"freebsd"
+			]
+		},
+		"node_modules/@rollup/rollup-freebsd-x64": {
+			"version": "4.35.0",
+			"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.35.0.tgz",
+			"integrity": "sha512-2mpHCeRuD1u/2kruUiHSsnjWtHjqVbzhBkNVQ1aVD63CcexKVcQGwJ2g5VphOd84GvxfSvnnlEyBtQCE5hxVVw==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"freebsd"
+			]
+		},
+		"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+			"version": "4.35.0",
+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.35.0.tgz",
+			"integrity": "sha512-mrA0v3QMy6ZSvEuLs0dMxcO2LnaCONs1Z73GUDBHWbY8tFFocM6yl7YyMu7rz4zS81NDSqhrUuolyZXGi8TEqg==",
+			"cpu": [
+				"arm"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			]
+		},
+		"node_modules/@rollup/rollup-linux-arm-musleabihf": {
+			"version": "4.35.0",
+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.35.0.tgz",
+			"integrity": "sha512-DnYhhzcvTAKNexIql8pFajr0PiDGrIsBYPRvCKlA5ixSS3uwo/CWNZxB09jhIapEIg945KOzcYEAGGSmTSpk7A==",
+			"cpu": [
+				"arm"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			]
+		},
+		"node_modules/@rollup/rollup-linux-arm64-gnu": {
+			"version": "4.35.0",
+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.35.0.tgz",
+			"integrity": "sha512-uagpnH2M2g2b5iLsCTZ35CL1FgyuzzJQ8L9VtlJ+FckBXroTwNOaD0z0/UF+k5K3aNQjbm8LIVpxykUOQt1m/A==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			]
+		},
+		"node_modules/@rollup/rollup-linux-arm64-musl": {
+			"version": "4.35.0",
+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.35.0.tgz",
+			"integrity": "sha512-XQxVOCd6VJeHQA/7YcqyV0/88N6ysSVzRjJ9I9UA/xXpEsjvAgDTgH3wQYz5bmr7SPtVK2TsP2fQ2N9L4ukoUg==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			]
+		},
+		"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+			"version": "4.35.0",
+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.35.0.tgz",
+			"integrity": "sha512-5pMT5PzfgwcXEwOaSrqVsz/LvjDZt+vQ8RT/70yhPU06PTuq8WaHhfT1LW+cdD7mW6i/J5/XIkX/1tCAkh1W6g==",
+			"cpu": [
+				"loong64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			]
+		},
+		"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+			"version": "4.35.0",
+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.35.0.tgz",
+			"integrity": "sha512-c+zkcvbhbXF98f4CtEIP1EBA/lCic5xB0lToneZYvMeKu5Kamq3O8gqrxiYYLzlZH6E3Aq+TSW86E4ay8iD8EA==",
+			"cpu": [
+				"ppc64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			]
+		},
+		"node_modules/@rollup/rollup-linux-riscv64-gnu": {
+			"version": "4.35.0",
+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.35.0.tgz",
+			"integrity": "sha512-s91fuAHdOwH/Tad2tzTtPX7UZyytHIRR6V4+2IGlV0Cej5rkG0R61SX4l4y9sh0JBibMiploZx3oHKPnQBKe4g==",
+			"cpu": [
+				"riscv64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			]
+		},
+		"node_modules/@rollup/rollup-linux-s390x-gnu": {
+			"version": "4.35.0",
+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.35.0.tgz",
+			"integrity": "sha512-hQRkPQPLYJZYGP+Hj4fR9dDBMIM7zrzJDWFEMPdTnTy95Ljnv0/4w/ixFw3pTBMEuuEuoqtBINYND4M7ujcuQw==",
+			"cpu": [
+				"s390x"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			]
+		},
+		"node_modules/@rollup/rollup-linux-x64-gnu": {
+			"version": "4.35.0",
+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.35.0.tgz",
+			"integrity": "sha512-Pim1T8rXOri+0HmV4CdKSGrqcBWX0d1HoPnQ0uw0bdp1aP5SdQVNBy8LjYncvnLgu3fnnCt17xjWGd4cqh8/hA==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			]
+		},
+		"node_modules/@rollup/rollup-linux-x64-musl": {
+			"version": "4.35.0",
+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.35.0.tgz",
+			"integrity": "sha512-QysqXzYiDvQWfUiTm8XmJNO2zm9yC9P/2Gkrwg2dH9cxotQzunBHYr6jk4SujCTqnfGxduOmQcI7c2ryuW8XVg==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			]
+		},
+		"node_modules/@rollup/rollup-win32-arm64-msvc": {
+			"version": "4.35.0",
+			"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.35.0.tgz",
+			"integrity": "sha512-OUOlGqPkVJCdJETKOCEf1mw848ZyJ5w50/rZ/3IBQVdLfR5jk/6Sr5m3iO2tdPgwo0x7VcncYuOvMhBWZq8ayg==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"win32"
+			]
+		},
+		"node_modules/@rollup/rollup-win32-ia32-msvc": {
+			"version": "4.35.0",
+			"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.35.0.tgz",
+			"integrity": "sha512-2/lsgejMrtwQe44glq7AFFHLfJBPafpsTa6JvP2NGef/ifOa4KBoglVf7AKN7EV9o32evBPRqfg96fEHzWo5kw==",
+			"cpu": [
+				"ia32"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"win32"
+			]
+		},
+		"node_modules/@rollup/rollup-win32-x64-msvc": {
+			"version": "4.35.0",
+			"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.35.0.tgz",
+			"integrity": "sha512-PIQeY5XDkrOysbQblSW7v3l1MDZzkTEzAfTPkj5VAu3FW8fS4ynyLg2sINp0fp3SjZ8xkRYpLqoKcYqAkhU1dw==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"win32"
+			]
+		},
+		"node_modules/@sveltejs/acorn-typescript": {
+			"version": "1.0.5",
+			"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz",
+			"integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==",
+			"dev": true,
+			"license": "MIT",
+			"peerDependencies": {
+				"acorn": "^8.9.0"
+			}
+		},
+		"node_modules/@sveltejs/adapter-auto": {
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-4.0.0.tgz",
+			"integrity": "sha512-kmuYSQdD2AwThymQF0haQhM8rE5rhutQXG4LNbnbShwhMO4qQGnKaaTy+88DuNSuoQDi58+thpq8XpHc1+oEKQ==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"import-meta-resolve": "^4.1.0"
+			},
+			"peerDependencies": {
+				"@sveltejs/kit": "^2.0.0"
+			}
+		},
+		"node_modules/@sveltejs/adapter-static": {
+			"version": "3.0.8",
+			"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.8.tgz",
+			"integrity": "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg==",
+			"dev": true,
+			"license": "MIT",
+			"peerDependencies": {
+				"@sveltejs/kit": "^2.0.0"
+			}
+		},
+		"node_modules/@sveltejs/kit": {
+			"version": "2.19.0",
+			"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.19.0.tgz",
+			"integrity": "sha512-UTx28Ad4sYsLU//gqkEo5aFOPFBRT2uXCmXTsURqhurDCvzkVwXruJgBcHDaMiK6RKKpYRteDUaXYqZyGPgCXQ==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@types/cookie": "^0.6.0",
+				"cookie": "^0.6.0",
+				"devalue": "^5.1.0",
+				"esm-env": "^1.2.2",
+				"import-meta-resolve": "^4.1.0",
+				"kleur": "^4.1.5",
+				"magic-string": "^0.30.5",
+				"mrmime": "^2.0.0",
+				"sade": "^1.8.1",
+				"set-cookie-parser": "^2.6.0",
+				"sirv": "^3.0.0"
+			},
+			"bin": {
+				"svelte-kit": "svelte-kit.js"
+			},
+			"engines": {
+				"node": ">=18.13"
+			},
+			"peerDependencies": {
+				"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0",
+				"svelte": "^4.0.0 || ^5.0.0-next.0",
+				"vite": "^5.0.3 || ^6.0.0"
+			}
+		},
+		"node_modules/@sveltejs/vite-plugin-svelte": {
+			"version": "5.0.3",
+			"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.0.3.tgz",
+			"integrity": "sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@sveltejs/vite-plugin-svelte-inspector": "^4.0.1",
+				"debug": "^4.4.0",
+				"deepmerge": "^4.3.1",
+				"kleur": "^4.1.5",
+				"magic-string": "^0.30.15",
+				"vitefu": "^1.0.4"
+			},
+			"engines": {
+				"node": "^18.0.0 || ^20.0.0 || >=22"
+			},
+			"peerDependencies": {
+				"svelte": "^5.0.0",
+				"vite": "^6.0.0"
+			}
+		},
+		"node_modules/@sveltejs/vite-plugin-svelte-inspector": {
+			"version": "4.0.1",
+			"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz",
+			"integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"debug": "^4.3.7"
+			},
+			"engines": {
+				"node": "^18.0.0 || ^20.0.0 || >=22"
+			},
+			"peerDependencies": {
+				"@sveltejs/vite-plugin-svelte": "^5.0.0",
+				"svelte": "^5.0.0",
+				"vite": "^6.0.0"
+			}
+		},
+		"node_modules/@tailwindcss/node": {
+			"version": "4.0.14",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.14.tgz",
+			"integrity": "sha512-Ux9NbFkKWYE4rfUFz6M5JFLs/GEYP6ysxT8uSyPn6aTbh2K3xDE1zz++eVK4Vwx799fzMF8CID9sdHn4j/Ab8w==",
+			"license": "MIT",
+			"dependencies": {
+				"enhanced-resolve": "^5.18.1",
+				"jiti": "^2.4.2",
+				"tailwindcss": "4.0.14"
+			}
+		},
+		"node_modules/@tailwindcss/oxide": {
+			"version": "4.0.14",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.14.tgz",
+			"integrity": "sha512-M8VCNyO/NBi5vJ2cRcI9u8w7Si+i76a7o1vveoGtbbjpEYJZYiyc7f2VGps/DqawO56l3tImIbq2OT/533jcrA==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 10"
+			},
+			"optionalDependencies": {
+				"@tailwindcss/oxide-android-arm64": "4.0.14",
+				"@tailwindcss/oxide-darwin-arm64": "4.0.14",
+				"@tailwindcss/oxide-darwin-x64": "4.0.14",
+				"@tailwindcss/oxide-freebsd-x64": "4.0.14",
+				"@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.14",
+				"@tailwindcss/oxide-linux-arm64-gnu": "4.0.14",
+				"@tailwindcss/oxide-linux-arm64-musl": "4.0.14",
+				"@tailwindcss/oxide-linux-x64-gnu": "4.0.14",
+				"@tailwindcss/oxide-linux-x64-musl": "4.0.14",
+				"@tailwindcss/oxide-win32-arm64-msvc": "4.0.14",
+				"@tailwindcss/oxide-win32-x64-msvc": "4.0.14"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-android-arm64": {
+			"version": "4.0.14",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.14.tgz",
+			"integrity": "sha512-VBFKC2rFyfJ5J8lRwjy6ub3rgpY186kAcYgiUr8ArR8BAZzMruyeKJ6mlsD22Zp5ZLcPW/FXMasJiJBx0WsdQg==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"android"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-darwin-arm64": {
+			"version": "4.0.14",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.14.tgz",
+			"integrity": "sha512-U3XOwLrefGr2YQZ9DXasDSNWGPZBCh8F62+AExBEDMLDfvLLgI/HDzY8Oq8p/JtqkAY38sWPOaNnRwEGKU5Zmg==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-darwin-x64": {
+			"version": "4.0.14",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.14.tgz",
+			"integrity": "sha512-V5AjFuc3ndWGnOi1d379UsODb0TzAS2DYIP/lwEbfvafUaD2aNZIcbwJtYu2DQqO2+s/XBvDVA+w4yUyaewRwg==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-freebsd-x64": {
+			"version": "4.0.14",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.14.tgz",
+			"integrity": "sha512-tXvtxbaZfcPfqBwW3f53lTcyH6EDT+1eT7yabwcfcxTs+8yTPqxsDUhrqe9MrnEzpNkd+R/QAjJapfd4tjWdLg==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"freebsd"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+			"version": "4.0.14",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.14.tgz",
+			"integrity": "sha512-cSeLNWWqIWeSTmBntQvyY2/2gcLX8rkPFfDDTQVF8qbRcRMVPLxBvFVJyfSAYRNch6ZyVH2GI6dtgALOBDpdNA==",
+			"cpu": [
+				"arm"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+			"version": "4.0.14",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.14.tgz",
+			"integrity": "sha512-bwDWLBalXFMDItcSXzFk6y7QKvj6oFlaY9vM+agTlwFL1n1OhDHYLZkSjaYsh6KCeG0VB0r7H8PUJVOM1LRZyg==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+			"version": "4.0.14",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.14.tgz",
+			"integrity": "sha512-gVkJdnR/L6iIcGYXx64HGJRmlme2FGr/aZH0W6u4A3RgPMAb+6ELRLi+UBiH83RXBm9vwCfkIC/q8T51h8vUJQ==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+			"version": "4.0.14",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.14.tgz",
+			"integrity": "sha512-EE+EQ+c6tTpzsg+LGO1uuusjXxYx0Q00JE5ubcIGfsogSKth8n8i2BcS2wYTQe4jXGs+BQs35l78BIPzgwLddw==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-linux-x64-musl": {
+			"version": "4.0.14",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.14.tgz",
+			"integrity": "sha512-KCCOzo+L6XPT0oUp2Jwh233ETRQ/F6cwUnMnR0FvMUCbkDAzHbcyOgpfuAtRa5HD0WbTbH4pVD+S0pn1EhNfbw==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+			"version": "4.0.14",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.14.tgz",
+			"integrity": "sha512-AHObFiFL9lNYcm3tZSPqa/cHGpM5wOrNmM2uOMoKppp+0Hom5uuyRh0QkOp7jftsHZdrZUpmoz0Mp6vhh2XtUg==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"win32"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+			"version": "4.0.14",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.14.tgz",
+			"integrity": "sha512-rNXXMDJfCJLw/ZaFTOLOHoGULxyXfh2iXTGiChFiYTSgKBKQHIGEpV0yn5N25WGzJJ+VBnRjHzlmDqRV+d//oQ==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"win32"
+			],
+			"engines": {
+				"node": ">= 10"
+			}
+		},
+		"node_modules/@tailwindcss/vite": {
+			"version": "4.0.14",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.14.tgz",
+			"integrity": "sha512-y69ztPTRFy+13EPS/7dEFVl7q2Goh1pQueVO8IfGeyqSpcx/joNJXFk0lLhMgUbF0VFJotwRSb9ZY7Xoq3r26Q==",
+			"license": "MIT",
+			"dependencies": {
+				"@tailwindcss/node": "4.0.14",
+				"@tailwindcss/oxide": "4.0.14",
+				"lightningcss": "1.29.2",
+				"tailwindcss": "4.0.14"
+			},
+			"peerDependencies": {
+				"vite": "^5.2.0 || ^6"
+			}
+		},
+		"node_modules/@types/cookie": {
+			"version": "0.6.0",
+			"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
+			"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/@types/estree": {
+			"version": "1.0.6",
+			"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+			"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+			"license": "MIT"
+		},
+		"node_modules/@types/json-schema": {
+			"version": "7.0.15",
+			"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+			"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/@typescript-eslint/eslint-plugin": {
+			"version": "8.26.1",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz",
+			"integrity": "sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@eslint-community/regexpp": "^4.10.0",
+				"@typescript-eslint/scope-manager": "8.26.1",
+				"@typescript-eslint/type-utils": "8.26.1",
+				"@typescript-eslint/utils": "8.26.1",
+				"@typescript-eslint/visitor-keys": "8.26.1",
+				"graphemer": "^1.4.0",
+				"ignore": "^5.3.1",
+				"natural-compare": "^1.4.0",
+				"ts-api-utils": "^2.0.1"
+			},
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/typescript-eslint"
+			},
+			"peerDependencies": {
+				"@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
+				"eslint": "^8.57.0 || ^9.0.0",
+				"typescript": ">=4.8.4 <5.9.0"
+			}
+		},
+		"node_modules/@typescript-eslint/parser": {
+			"version": "8.26.1",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.1.tgz",
+			"integrity": "sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@typescript-eslint/scope-manager": "8.26.1",
+				"@typescript-eslint/types": "8.26.1",
+				"@typescript-eslint/typescript-estree": "8.26.1",
+				"@typescript-eslint/visitor-keys": "8.26.1",
+				"debug": "^4.3.4"
+			},
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/typescript-eslint"
+			},
+			"peerDependencies": {
+				"eslint": "^8.57.0 || ^9.0.0",
+				"typescript": ">=4.8.4 <5.9.0"
+			}
+		},
+		"node_modules/@typescript-eslint/scope-manager": {
+			"version": "8.26.1",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.1.tgz",
+			"integrity": "sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@typescript-eslint/types": "8.26.1",
+				"@typescript-eslint/visitor-keys": "8.26.1"
+			},
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/typescript-eslint"
+			}
+		},
+		"node_modules/@typescript-eslint/type-utils": {
+			"version": "8.26.1",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.1.tgz",
+			"integrity": "sha512-Kcj/TagJLwoY/5w9JGEFV0dclQdyqw9+VMndxOJKtoFSjfZhLXhYjzsQEeyza03rwHx2vFEGvrJWJBXKleRvZg==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@typescript-eslint/typescript-estree": "8.26.1",
+				"@typescript-eslint/utils": "8.26.1",
+				"debug": "^4.3.4",
+				"ts-api-utils": "^2.0.1"
+			},
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/typescript-eslint"
+			},
+			"peerDependencies": {
+				"eslint": "^8.57.0 || ^9.0.0",
+				"typescript": ">=4.8.4 <5.9.0"
+			}
+		},
+		"node_modules/@typescript-eslint/types": {
+			"version": "8.26.1",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.1.tgz",
+			"integrity": "sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/typescript-eslint"
+			}
+		},
+		"node_modules/@typescript-eslint/typescript-estree": {
+			"version": "8.26.1",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.1.tgz",
+			"integrity": "sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@typescript-eslint/types": "8.26.1",
+				"@typescript-eslint/visitor-keys": "8.26.1",
+				"debug": "^4.3.4",
+				"fast-glob": "^3.3.2",
+				"is-glob": "^4.0.3",
+				"minimatch": "^9.0.4",
+				"semver": "^7.6.0",
+				"ts-api-utils": "^2.0.1"
+			},
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/typescript-eslint"
+			},
+			"peerDependencies": {
+				"typescript": ">=4.8.4 <5.9.0"
+			}
+		},
+		"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+			"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"balanced-match": "^1.0.0"
+			}
+		},
+		"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+			"version": "9.0.5",
+			"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+			"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+			"dev": true,
+			"license": "ISC",
+			"dependencies": {
+				"brace-expansion": "^2.0.1"
+			},
+			"engines": {
+				"node": ">=16 || 14 >=14.17"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/isaacs"
+			}
+		},
+		"node_modules/@typescript-eslint/utils": {
+			"version": "8.26.1",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.1.tgz",
+			"integrity": "sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@eslint-community/eslint-utils": "^4.4.0",
+				"@typescript-eslint/scope-manager": "8.26.1",
+				"@typescript-eslint/types": "8.26.1",
+				"@typescript-eslint/typescript-estree": "8.26.1"
+			},
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/typescript-eslint"
+			},
+			"peerDependencies": {
+				"eslint": "^8.57.0 || ^9.0.0",
+				"typescript": ">=4.8.4 <5.9.0"
+			}
+		},
+		"node_modules/@typescript-eslint/visitor-keys": {
+			"version": "8.26.1",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.1.tgz",
+			"integrity": "sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@typescript-eslint/types": "8.26.1",
+				"eslint-visitor-keys": "^4.2.0"
+			},
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/typescript-eslint"
+			}
+		},
+		"node_modules/acorn": {
+			"version": "8.14.1",
+			"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
+			"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
+			"dev": true,
+			"license": "MIT",
+			"bin": {
+				"acorn": "bin/acorn"
+			},
+			"engines": {
+				"node": ">=0.4.0"
+			}
+		},
+		"node_modules/acorn-jsx": {
+			"version": "5.3.2",
+			"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+			"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+			"dev": true,
+			"license": "MIT",
+			"peerDependencies": {
+				"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+			}
+		},
+		"node_modules/ajv": {
+			"version": "6.12.6",
+			"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+			"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"fast-deep-equal": "^3.1.1",
+				"fast-json-stable-stringify": "^2.0.0",
+				"json-schema-traverse": "^0.4.1",
+				"uri-js": "^4.2.2"
+			},
+			"funding": {
+				"type": "github",
+				"url": "https://github.com/sponsors/epoberezkin"
+			}
+		},
+		"node_modules/ansi-styles": {
+			"version": "4.3.0",
+			"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+			"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"color-convert": "^2.0.1"
+			},
+			"engines": {
+				"node": ">=8"
+			},
+			"funding": {
+				"url": "https://github.com/chalk/ansi-styles?sponsor=1"
+			}
+		},
+		"node_modules/argparse": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+			"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+			"dev": true,
+			"license": "Python-2.0"
+		},
+		"node_modules/aria-query": {
+			"version": "5.3.2",
+			"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
+			"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"engines": {
+				"node": ">= 0.4"
+			}
+		},
+		"node_modules/axobject-query": {
+			"version": "4.1.0",
+			"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
+			"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"engines": {
+				"node": ">= 0.4"
+			}
+		},
+		"node_modules/balanced-match": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+			"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/brace-expansion": {
+			"version": "1.1.11",
+			"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+			"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"balanced-match": "^1.0.0",
+				"concat-map": "0.0.1"
+			}
+		},
+		"node_modules/braces": {
+			"version": "3.0.3",
+			"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+			"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"fill-range": "^7.1.1"
+			},
+			"engines": {
+				"node": ">=8"
+			}
+		},
+		"node_modules/callsites": {
+			"version": "3.1.0",
+			"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+			"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=6"
+			}
+		},
+		"node_modules/chalk": {
+			"version": "4.1.2",
+			"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+			"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"ansi-styles": "^4.1.0",
+				"supports-color": "^7.1.0"
+			},
+			"engines": {
+				"node": ">=10"
+			},
+			"funding": {
+				"url": "https://github.com/chalk/chalk?sponsor=1"
+			}
+		},
+		"node_modules/chokidar": {
+			"version": "4.0.3",
+			"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+			"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"readdirp": "^4.0.1"
+			},
+			"engines": {
+				"node": ">= 14.16.0"
+			},
+			"funding": {
+				"url": "https://paulmillr.com/funding/"
+			}
+		},
+		"node_modules/clsx": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+			"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=6"
+			}
+		},
+		"node_modules/color-convert": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+			"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"color-name": "~1.1.4"
+			},
+			"engines": {
+				"node": ">=7.0.0"
+			}
+		},
+		"node_modules/color-name": {
+			"version": "1.1.4",
+			"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+			"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/concat-map": {
+			"version": "0.0.1",
+			"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+			"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/cookie": {
+			"version": "0.6.0",
+			"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+			"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.6"
+			}
+		},
+		"node_modules/cross-spawn": {
+			"version": "7.0.6",
+			"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+			"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"path-key": "^3.1.0",
+				"shebang-command": "^2.0.0",
+				"which": "^2.0.1"
+			},
+			"engines": {
+				"node": ">= 8"
+			}
+		},
+		"node_modules/cssesc": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+			"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+			"dev": true,
+			"license": "MIT",
+			"bin": {
+				"cssesc": "bin/cssesc"
+			},
+			"engines": {
+				"node": ">=4"
+			}
+		},
+		"node_modules/daisyui": {
+			"version": "5.0.3",
+			"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.3.tgz",
+			"integrity": "sha512-27dou80uFNgxVcmQNzS8QFBoE1/01u7N0smdGqA2pHbaRPTFoHdJfXNzilpvt/0jAVu8cMEhrVPoYxpdxLwumA==",
+			"license": "MIT",
+			"funding": {
+				"url": "https://github.com/saadeghi/daisyui?sponsor=1"
+			}
+		},
+		"node_modules/debug": {
+			"version": "4.4.0",
+			"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+			"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"ms": "^2.1.3"
+			},
+			"engines": {
+				"node": ">=6.0"
+			},
+			"peerDependenciesMeta": {
+				"supports-color": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/deep-is": {
+			"version": "0.1.4",
+			"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+			"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/deepmerge": {
+			"version": "4.3.1",
+			"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+			"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=0.10.0"
+			}
+		},
+		"node_modules/detect-libc": {
+			"version": "2.0.3",
+			"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
+			"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
+			"license": "Apache-2.0",
+			"engines": {
+				"node": ">=8"
+			}
+		},
+		"node_modules/devalue": {
+			"version": "5.1.1",
+			"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz",
+			"integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/enhanced-resolve": {
+			"version": "5.18.1",
+			"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
+			"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
+			"license": "MIT",
+			"dependencies": {
+				"graceful-fs": "^4.2.4",
+				"tapable": "^2.2.0"
+			},
+			"engines": {
+				"node": ">=10.13.0"
+			}
+		},
+		"node_modules/esbuild": {
+			"version": "0.25.1",
+			"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
+			"integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==",
+			"hasInstallScript": true,
+			"license": "MIT",
+			"bin": {
+				"esbuild": "bin/esbuild"
+			},
+			"engines": {
+				"node": ">=18"
+			},
+			"optionalDependencies": {
+				"@esbuild/aix-ppc64": "0.25.1",
+				"@esbuild/android-arm": "0.25.1",
+				"@esbuild/android-arm64": "0.25.1",
+				"@esbuild/android-x64": "0.25.1",
+				"@esbuild/darwin-arm64": "0.25.1",
+				"@esbuild/darwin-x64": "0.25.1",
+				"@esbuild/freebsd-arm64": "0.25.1",
+				"@esbuild/freebsd-x64": "0.25.1",
+				"@esbuild/linux-arm": "0.25.1",
+				"@esbuild/linux-arm64": "0.25.1",
+				"@esbuild/linux-ia32": "0.25.1",
+				"@esbuild/linux-loong64": "0.25.1",
+				"@esbuild/linux-mips64el": "0.25.1",
+				"@esbuild/linux-ppc64": "0.25.1",
+				"@esbuild/linux-riscv64": "0.25.1",
+				"@esbuild/linux-s390x": "0.25.1",
+				"@esbuild/linux-x64": "0.25.1",
+				"@esbuild/netbsd-arm64": "0.25.1",
+				"@esbuild/netbsd-x64": "0.25.1",
+				"@esbuild/openbsd-arm64": "0.25.1",
+				"@esbuild/openbsd-x64": "0.25.1",
+				"@esbuild/sunos-x64": "0.25.1",
+				"@esbuild/win32-arm64": "0.25.1",
+				"@esbuild/win32-ia32": "0.25.1",
+				"@esbuild/win32-x64": "0.25.1"
+			}
+		},
+		"node_modules/escape-string-regexp": {
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+			"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=10"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/sindresorhus"
+			}
+		},
+		"node_modules/eslint": {
+			"version": "9.22.0",
+			"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.22.0.tgz",
+			"integrity": "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@eslint-community/eslint-utils": "^4.2.0",
+				"@eslint-community/regexpp": "^4.12.1",
+				"@eslint/config-array": "^0.19.2",
+				"@eslint/config-helpers": "^0.1.0",
+				"@eslint/core": "^0.12.0",
+				"@eslint/eslintrc": "^3.3.0",
+				"@eslint/js": "9.22.0",
+				"@eslint/plugin-kit": "^0.2.7",
+				"@humanfs/node": "^0.16.6",
+				"@humanwhocodes/module-importer": "^1.0.1",
+				"@humanwhocodes/retry": "^0.4.2",
+				"@types/estree": "^1.0.6",
+				"@types/json-schema": "^7.0.15",
+				"ajv": "^6.12.4",
+				"chalk": "^4.0.0",
+				"cross-spawn": "^7.0.6",
+				"debug": "^4.3.2",
+				"escape-string-regexp": "^4.0.0",
+				"eslint-scope": "^8.3.0",
+				"eslint-visitor-keys": "^4.2.0",
+				"espree": "^10.3.0",
+				"esquery": "^1.5.0",
+				"esutils": "^2.0.2",
+				"fast-deep-equal": "^3.1.3",
+				"file-entry-cache": "^8.0.0",
+				"find-up": "^5.0.0",
+				"glob-parent": "^6.0.2",
+				"ignore": "^5.2.0",
+				"imurmurhash": "^0.1.4",
+				"is-glob": "^4.0.0",
+				"json-stable-stringify-without-jsonify": "^1.0.1",
+				"lodash.merge": "^4.6.2",
+				"minimatch": "^3.1.2",
+				"natural-compare": "^1.4.0",
+				"optionator": "^0.9.3"
+			},
+			"bin": {
+				"eslint": "bin/eslint.js"
+			},
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			},
+			"funding": {
+				"url": "https://eslint.org/donate"
+			},
+			"peerDependencies": {
+				"jiti": "*"
+			},
+			"peerDependenciesMeta": {
+				"jiti": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/eslint-compat-utils": {
+			"version": "0.6.4",
+			"resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.6.4.tgz",
+			"integrity": "sha512-/u+GQt8NMfXO8w17QendT4gvO5acfxQsAKirAt0LVxDnr2N8YLCVbregaNc/Yhp7NM128DwCaRvr8PLDfeNkQw==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"semver": "^7.5.4"
+			},
+			"engines": {
+				"node": ">=12"
+			},
+			"peerDependencies": {
+				"eslint": ">=6.0.0"
+			}
+		},
+		"node_modules/eslint-config-prettier": {
+			"version": "10.1.1",
+			"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz",
+			"integrity": "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==",
+			"dev": true,
+			"license": "MIT",
+			"bin": {
+				"eslint-config-prettier": "bin/cli.js"
+			},
+			"peerDependencies": {
+				"eslint": ">=7.0.0"
+			}
+		},
+		"node_modules/eslint-plugin-svelte": {
+			"version": "3.1.0",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.1.0.tgz",
+			"integrity": "sha512-hSQyLDkuuHPJby1ixZfUVrfLON42mT0Odf18MbwAgFUPuyIwJlhy3acUY1/bxt+Njucq/dQxR543zYDqkBNLmw==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@eslint-community/eslint-utils": "^4.4.1",
+				"@jridgewell/sourcemap-codec": "^1.5.0",
+				"eslint-compat-utils": "^0.6.4",
+				"esutils": "^2.0.3",
+				"known-css-properties": "^0.35.0",
+				"postcss": "^8.4.49",
+				"postcss-load-config": "^3.1.4",
+				"postcss-safe-parser": "^7.0.0",
+				"semver": "^7.6.3",
+				"svelte-eslint-parser": "^1.0.1"
+			},
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/ota-meshi"
+			},
+			"peerDependencies": {
+				"eslint": "^8.57.1 || ^9.0.0",
+				"svelte": "^3.37.0 || ^4.0.0 || ^5.0.0"
+			},
+			"peerDependenciesMeta": {
+				"svelte": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/eslint-scope": {
+			"version": "8.3.0",
+			"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
+			"integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==",
+			"dev": true,
+			"license": "BSD-2-Clause",
+			"dependencies": {
+				"esrecurse": "^4.3.0",
+				"estraverse": "^5.2.0"
+			},
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			},
+			"funding": {
+				"url": "https://opencollective.com/eslint"
+			}
+		},
+		"node_modules/eslint-visitor-keys": {
+			"version": "4.2.0",
+			"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+			"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			},
+			"funding": {
+				"url": "https://opencollective.com/eslint"
+			}
+		},
+		"node_modules/esm-env": {
+			"version": "1.2.2",
+			"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
+			"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/espree": {
+			"version": "10.3.0",
+			"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
+			"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
+			"dev": true,
+			"license": "BSD-2-Clause",
+			"dependencies": {
+				"acorn": "^8.14.0",
+				"acorn-jsx": "^5.3.2",
+				"eslint-visitor-keys": "^4.2.0"
+			},
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			},
+			"funding": {
+				"url": "https://opencollective.com/eslint"
+			}
+		},
+		"node_modules/esquery": {
+			"version": "1.6.0",
+			"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+			"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+			"dev": true,
+			"license": "BSD-3-Clause",
+			"dependencies": {
+				"estraverse": "^5.1.0"
+			},
+			"engines": {
+				"node": ">=0.10"
+			}
+		},
+		"node_modules/esrap": {
+			"version": "1.4.5",
+			"resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.5.tgz",
+			"integrity": "sha512-CjNMjkBWWZeHn+VX+gS8YvFwJ5+NDhg8aWZBSFJPR8qQduDNjbJodA2WcwCm7uQa5Rjqj+nZvVmceg1RbHFB9g==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@jridgewell/sourcemap-codec": "^1.4.15"
+			}
+		},
+		"node_modules/esrecurse": {
+			"version": "4.3.0",
+			"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+			"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+			"dev": true,
+			"license": "BSD-2-Clause",
+			"dependencies": {
+				"estraverse": "^5.2.0"
+			},
+			"engines": {
+				"node": ">=4.0"
+			}
+		},
+		"node_modules/estraverse": {
+			"version": "5.3.0",
+			"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+			"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+			"dev": true,
+			"license": "BSD-2-Clause",
+			"engines": {
+				"node": ">=4.0"
+			}
+		},
+		"node_modules/esutils": {
+			"version": "2.0.3",
+			"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+			"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+			"dev": true,
+			"license": "BSD-2-Clause",
+			"engines": {
+				"node": ">=0.10.0"
+			}
+		},
+		"node_modules/fast-deep-equal": {
+			"version": "3.1.3",
+			"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+			"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/fast-glob": {
+			"version": "3.3.3",
+			"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+			"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@nodelib/fs.stat": "^2.0.2",
+				"@nodelib/fs.walk": "^1.2.3",
+				"glob-parent": "^5.1.2",
+				"merge2": "^1.3.0",
+				"micromatch": "^4.0.8"
+			},
+			"engines": {
+				"node": ">=8.6.0"
+			}
+		},
+		"node_modules/fast-glob/node_modules/glob-parent": {
+			"version": "5.1.2",
+			"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+			"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+			"dev": true,
+			"license": "ISC",
+			"dependencies": {
+				"is-glob": "^4.0.1"
+			},
+			"engines": {
+				"node": ">= 6"
+			}
+		},
+		"node_modules/fast-json-stable-stringify": {
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+			"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/fast-levenshtein": {
+			"version": "2.0.6",
+			"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+			"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/fastq": {
+			"version": "1.19.1",
+			"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+			"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+			"dev": true,
+			"license": "ISC",
+			"dependencies": {
+				"reusify": "^1.0.4"
+			}
+		},
+		"node_modules/fdir": {
+			"version": "6.4.3",
+			"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz",
+			"integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==",
+			"dev": true,
+			"license": "MIT",
+			"peerDependencies": {
+				"picomatch": "^3 || ^4"
+			},
+			"peerDependenciesMeta": {
+				"picomatch": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/file-entry-cache": {
+			"version": "8.0.0",
+			"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+			"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"flat-cache": "^4.0.0"
+			},
+			"engines": {
+				"node": ">=16.0.0"
+			}
+		},
+		"node_modules/fill-range": {
+			"version": "7.1.1",
+			"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+			"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"to-regex-range": "^5.0.1"
+			},
+			"engines": {
+				"node": ">=8"
+			}
+		},
+		"node_modules/find-up": {
+			"version": "5.0.0",
+			"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+			"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"locate-path": "^6.0.0",
+				"path-exists": "^4.0.0"
+			},
+			"engines": {
+				"node": ">=10"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/sindresorhus"
+			}
+		},
+		"node_modules/flat-cache": {
+			"version": "4.0.1",
+			"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+			"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"flatted": "^3.2.9",
+				"keyv": "^4.5.4"
+			},
+			"engines": {
+				"node": ">=16"
+			}
+		},
+		"node_modules/flatted": {
+			"version": "3.3.3",
+			"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+			"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+			"dev": true,
+			"license": "ISC"
+		},
+		"node_modules/fsevents": {
+			"version": "2.3.3",
+			"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+			"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+			"hasInstallScript": true,
+			"license": "MIT",
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+			}
+		},
+		"node_modules/glob-parent": {
+			"version": "6.0.2",
+			"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+			"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+			"dev": true,
+			"license": "ISC",
+			"dependencies": {
+				"is-glob": "^4.0.3"
+			},
+			"engines": {
+				"node": ">=10.13.0"
+			}
+		},
+		"node_modules/globals": {
+			"version": "16.0.0",
+			"resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz",
+			"integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=18"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/sindresorhus"
+			}
+		},
+		"node_modules/graceful-fs": {
+			"version": "4.2.11",
+			"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+			"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+			"license": "ISC"
+		},
+		"node_modules/graphemer": {
+			"version": "1.4.0",
+			"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+			"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/has-flag": {
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+			"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=8"
+			}
+		},
+		"node_modules/ignore": {
+			"version": "5.3.2",
+			"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+			"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">= 4"
+			}
+		},
+		"node_modules/import-fresh": {
+			"version": "3.3.1",
+			"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+			"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"parent-module": "^1.0.0",
+				"resolve-from": "^4.0.0"
+			},
+			"engines": {
+				"node": ">=6"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/sindresorhus"
+			}
+		},
+		"node_modules/import-meta-resolve": {
+			"version": "4.1.0",
+			"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
+			"integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==",
+			"dev": true,
+			"license": "MIT",
+			"funding": {
+				"type": "github",
+				"url": "https://github.com/sponsors/wooorm"
+			}
+		},
+		"node_modules/imurmurhash": {
+			"version": "0.1.4",
+			"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+			"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=0.8.19"
+			}
+		},
+		"node_modules/is-extglob": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+			"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=0.10.0"
+			}
+		},
+		"node_modules/is-glob": {
+			"version": "4.0.3",
+			"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+			"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"is-extglob": "^2.1.1"
+			},
+			"engines": {
+				"node": ">=0.10.0"
+			}
+		},
+		"node_modules/is-number": {
+			"version": "7.0.0",
+			"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+			"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=0.12.0"
+			}
+		},
+		"node_modules/is-reference": {
+			"version": "3.0.3",
+			"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
+			"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@types/estree": "^1.0.6"
+			}
+		},
+		"node_modules/isexe": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+			"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+			"dev": true,
+			"license": "ISC"
+		},
+		"node_modules/jiti": {
+			"version": "2.4.2",
+			"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
+			"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
+			"license": "MIT",
+			"bin": {
+				"jiti": "lib/jiti-cli.mjs"
+			}
+		},
+		"node_modules/js-yaml": {
+			"version": "4.1.0",
+			"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+			"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"argparse": "^2.0.1"
+			},
+			"bin": {
+				"js-yaml": "bin/js-yaml.js"
+			}
+		},
+		"node_modules/json-buffer": {
+			"version": "3.0.1",
+			"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+			"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/json-schema-traverse": {
+			"version": "0.4.1",
+			"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+			"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/json-stable-stringify-without-jsonify": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+			"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/keyv": {
+			"version": "4.5.4",
+			"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+			"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"json-buffer": "3.0.1"
+			}
+		},
+		"node_modules/kleur": {
+			"version": "4.1.5",
+			"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+			"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=6"
+			}
+		},
+		"node_modules/known-css-properties": {
+			"version": "0.35.0",
+			"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz",
+			"integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/levn": {
+			"version": "0.4.1",
+			"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+			"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"prelude-ls": "^1.2.1",
+				"type-check": "~0.4.0"
+			},
+			"engines": {
+				"node": ">= 0.8.0"
+			}
+		},
+		"node_modules/lightningcss": {
+			"version": "1.29.2",
+			"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
+			"integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==",
+			"license": "MPL-2.0",
+			"dependencies": {
+				"detect-libc": "^2.0.3"
+			},
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			},
+			"optionalDependencies": {
+				"lightningcss-darwin-arm64": "1.29.2",
+				"lightningcss-darwin-x64": "1.29.2",
+				"lightningcss-freebsd-x64": "1.29.2",
+				"lightningcss-linux-arm-gnueabihf": "1.29.2",
+				"lightningcss-linux-arm64-gnu": "1.29.2",
+				"lightningcss-linux-arm64-musl": "1.29.2",
+				"lightningcss-linux-x64-gnu": "1.29.2",
+				"lightningcss-linux-x64-musl": "1.29.2",
+				"lightningcss-win32-arm64-msvc": "1.29.2",
+				"lightningcss-win32-x64-msvc": "1.29.2"
+			}
+		},
+		"node_modules/lightningcss-darwin-arm64": {
+			"version": "1.29.2",
+			"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz",
+			"integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MPL-2.0",
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			}
+		},
+		"node_modules/lightningcss-darwin-x64": {
+			"version": "1.29.2",
+			"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz",
+			"integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MPL-2.0",
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			}
+		},
+		"node_modules/lightningcss-freebsd-x64": {
+			"version": "1.29.2",
+			"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz",
+			"integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MPL-2.0",
+			"optional": true,
+			"os": [
+				"freebsd"
+			],
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			}
+		},
+		"node_modules/lightningcss-linux-arm-gnueabihf": {
+			"version": "1.29.2",
+			"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz",
+			"integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==",
+			"cpu": [
+				"arm"
+			],
+			"license": "MPL-2.0",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			}
+		},
+		"node_modules/lightningcss-linux-arm64-gnu": {
+			"version": "1.29.2",
+			"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz",
+			"integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MPL-2.0",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			}
+		},
+		"node_modules/lightningcss-linux-arm64-musl": {
+			"version": "1.29.2",
+			"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz",
+			"integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MPL-2.0",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			}
+		},
+		"node_modules/lightningcss-linux-x64-gnu": {
+			"version": "1.29.2",
+			"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz",
+			"integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MPL-2.0",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			}
+		},
+		"node_modules/lightningcss-linux-x64-musl": {
+			"version": "1.29.2",
+			"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz",
+			"integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MPL-2.0",
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			}
+		},
+		"node_modules/lightningcss-win32-arm64-msvc": {
+			"version": "1.29.2",
+			"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz",
+			"integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==",
+			"cpu": [
+				"arm64"
+			],
+			"license": "MPL-2.0",
+			"optional": true,
+			"os": [
+				"win32"
+			],
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			}
+		},
+		"node_modules/lightningcss-win32-x64-msvc": {
+			"version": "1.29.2",
+			"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz",
+			"integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==",
+			"cpu": [
+				"x64"
+			],
+			"license": "MPL-2.0",
+			"optional": true,
+			"os": [
+				"win32"
+			],
+			"engines": {
+				"node": ">= 12.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/parcel"
+			}
+		},
+		"node_modules/lilconfig": {
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
+			"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=10"
+			}
+		},
+		"node_modules/locate-character": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
+			"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/locate-path": {
+			"version": "6.0.0",
+			"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+			"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"p-locate": "^5.0.0"
+			},
+			"engines": {
+				"node": ">=10"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/sindresorhus"
+			}
+		},
+		"node_modules/lodash.merge": {
+			"version": "4.6.2",
+			"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+			"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/magic-string": {
+			"version": "0.30.17",
+			"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+			"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@jridgewell/sourcemap-codec": "^1.5.0"
+			}
+		},
+		"node_modules/merge2": {
+			"version": "1.4.1",
+			"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+			"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">= 8"
+			}
+		},
+		"node_modules/micromatch": {
+			"version": "4.0.8",
+			"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+			"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"braces": "^3.0.3",
+				"picomatch": "^2.3.1"
+			},
+			"engines": {
+				"node": ">=8.6"
+			}
+		},
+		"node_modules/micromatch/node_modules/picomatch": {
+			"version": "2.3.1",
+			"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+			"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=8.6"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/jonschlinkert"
+			}
+		},
+		"node_modules/minimatch": {
+			"version": "3.1.2",
+			"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+			"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+			"dev": true,
+			"license": "ISC",
+			"dependencies": {
+				"brace-expansion": "^1.1.7"
+			},
+			"engines": {
+				"node": "*"
+			}
+		},
+		"node_modules/mri": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
+			"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=4"
+			}
+		},
+		"node_modules/mrmime": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
+			"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=10"
+			}
+		},
+		"node_modules/ms": {
+			"version": "2.1.3",
+			"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+			"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/nanoid": {
+			"version": "3.3.9",
+			"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.9.tgz",
+			"integrity": "sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==",
+			"funding": [
+				{
+					"type": "github",
+					"url": "https://github.com/sponsors/ai"
+				}
+			],
+			"license": "MIT",
+			"bin": {
+				"nanoid": "bin/nanoid.cjs"
+			},
+			"engines": {
+				"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+			}
+		},
+		"node_modules/natural-compare": {
+			"version": "1.4.0",
+			"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+			"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/optionator": {
+			"version": "0.9.4",
+			"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+			"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"deep-is": "^0.1.3",
+				"fast-levenshtein": "^2.0.6",
+				"levn": "^0.4.1",
+				"prelude-ls": "^1.2.1",
+				"type-check": "^0.4.0",
+				"word-wrap": "^1.2.5"
+			},
+			"engines": {
+				"node": ">= 0.8.0"
+			}
+		},
+		"node_modules/p-limit": {
+			"version": "3.1.0",
+			"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+			"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"yocto-queue": "^0.1.0"
+			},
+			"engines": {
+				"node": ">=10"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/sindresorhus"
+			}
+		},
+		"node_modules/p-locate": {
+			"version": "5.0.0",
+			"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+			"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"p-limit": "^3.0.2"
+			},
+			"engines": {
+				"node": ">=10"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/sindresorhus"
+			}
+		},
+		"node_modules/parent-module": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+			"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"callsites": "^3.0.0"
+			},
+			"engines": {
+				"node": ">=6"
+			}
+		},
+		"node_modules/path-exists": {
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+			"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=8"
+			}
+		},
+		"node_modules/path-key": {
+			"version": "3.1.1",
+			"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+			"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=8"
+			}
+		},
+		"node_modules/picocolors": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+			"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+			"license": "ISC"
+		},
+		"node_modules/picomatch": {
+			"version": "4.0.2",
+			"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+			"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+			"dev": true,
+			"license": "MIT",
+			"optional": true,
+			"peer": true,
+			"engines": {
+				"node": ">=12"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/jonschlinkert"
+			}
+		},
+		"node_modules/postcss": {
+			"version": "8.5.3",
+			"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
+			"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
+			"funding": [
+				{
+					"type": "opencollective",
+					"url": "https://opencollective.com/postcss/"
+				},
+				{
+					"type": "tidelift",
+					"url": "https://tidelift.com/funding/github/npm/postcss"
+				},
+				{
+					"type": "github",
+					"url": "https://github.com/sponsors/ai"
+				}
+			],
+			"license": "MIT",
+			"dependencies": {
+				"nanoid": "^3.3.8",
+				"picocolors": "^1.1.1",
+				"source-map-js": "^1.2.1"
+			},
+			"engines": {
+				"node": "^10 || ^12 || >=14"
+			}
+		},
+		"node_modules/postcss-load-config": {
+			"version": "3.1.4",
+			"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz",
+			"integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"lilconfig": "^2.0.5",
+				"yaml": "^1.10.2"
+			},
+			"engines": {
+				"node": ">= 10"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/postcss/"
+			},
+			"peerDependencies": {
+				"postcss": ">=8.0.9",
+				"ts-node": ">=9.0.0"
+			},
+			"peerDependenciesMeta": {
+				"postcss": {
+					"optional": true
+				},
+				"ts-node": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/postcss-load-config/node_modules/yaml": {
+			"version": "1.10.2",
+			"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+			"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+			"dev": true,
+			"license": "ISC",
+			"engines": {
+				"node": ">= 6"
+			}
+		},
+		"node_modules/postcss-safe-parser": {
+			"version": "7.0.1",
+			"resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz",
+			"integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==",
+			"dev": true,
+			"funding": [
+				{
+					"type": "opencollective",
+					"url": "https://opencollective.com/postcss/"
+				},
+				{
+					"type": "tidelift",
+					"url": "https://tidelift.com/funding/github/npm/postcss-safe-parser"
+				},
+				{
+					"type": "github",
+					"url": "https://github.com/sponsors/ai"
+				}
+			],
+			"license": "MIT",
+			"engines": {
+				"node": ">=18.0"
+			},
+			"peerDependencies": {
+				"postcss": "^8.4.31"
+			}
+		},
+		"node_modules/postcss-scss": {
+			"version": "4.0.9",
+			"resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz",
+			"integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==",
+			"dev": true,
+			"funding": [
+				{
+					"type": "opencollective",
+					"url": "https://opencollective.com/postcss/"
+				},
+				{
+					"type": "tidelift",
+					"url": "https://tidelift.com/funding/github/npm/postcss-scss"
+				},
+				{
+					"type": "github",
+					"url": "https://github.com/sponsors/ai"
+				}
+			],
+			"license": "MIT",
+			"engines": {
+				"node": ">=12.0"
+			},
+			"peerDependencies": {
+				"postcss": "^8.4.29"
+			}
+		},
+		"node_modules/postcss-selector-parser": {
+			"version": "7.1.0",
+			"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
+			"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"cssesc": "^3.0.0",
+				"util-deprecate": "^1.0.2"
+			},
+			"engines": {
+				"node": ">=4"
+			}
+		},
+		"node_modules/prelude-ls": {
+			"version": "1.2.1",
+			"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+			"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.8.0"
+			}
+		},
+		"node_modules/prettier": {
+			"version": "3.5.3",
+			"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
+			"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
+			"dev": true,
+			"license": "MIT",
+			"bin": {
+				"prettier": "bin/prettier.cjs"
+			},
+			"engines": {
+				"node": ">=14"
+			},
+			"funding": {
+				"url": "https://github.com/prettier/prettier?sponsor=1"
+			}
+		},
+		"node_modules/prettier-plugin-svelte": {
+			"version": "3.3.3",
+			"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.3.3.tgz",
+			"integrity": "sha512-yViK9zqQ+H2qZD1w/bH7W8i+bVfKrD8GIFjkFe4Thl6kCT9SlAsXVNmt3jCvQOCsnOhcvYgsoVlRV/Eu6x5nNw==",
+			"dev": true,
+			"license": "MIT",
+			"peerDependencies": {
+				"prettier": "^3.0.0",
+				"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
+			}
+		},
+		"node_modules/punycode": {
+			"version": "2.3.1",
+			"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+			"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=6"
+			}
+		},
+		"node_modules/queue-microtask": {
+			"version": "1.2.3",
+			"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+			"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+			"dev": true,
+			"funding": [
+				{
+					"type": "github",
+					"url": "https://github.com/sponsors/feross"
+				},
+				{
+					"type": "patreon",
+					"url": "https://www.patreon.com/feross"
+				},
+				{
+					"type": "consulting",
+					"url": "https://feross.org/support"
+				}
+			],
+			"license": "MIT"
+		},
+		"node_modules/readdirp": {
+			"version": "4.1.2",
+			"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+			"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">= 14.18.0"
+			},
+			"funding": {
+				"type": "individual",
+				"url": "https://paulmillr.com/funding/"
+			}
+		},
+		"node_modules/resolve-from": {
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+			"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=4"
+			}
+		},
+		"node_modules/reusify": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+			"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"iojs": ">=1.0.0",
+				"node": ">=0.10.0"
+			}
+		},
+		"node_modules/rollup": {
+			"version": "4.35.0",
+			"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.35.0.tgz",
+			"integrity": "sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg==",
+			"license": "MIT",
+			"dependencies": {
+				"@types/estree": "1.0.6"
+			},
+			"bin": {
+				"rollup": "dist/bin/rollup"
+			},
+			"engines": {
+				"node": ">=18.0.0",
+				"npm": ">=8.0.0"
+			},
+			"optionalDependencies": {
+				"@rollup/rollup-android-arm-eabi": "4.35.0",
+				"@rollup/rollup-android-arm64": "4.35.0",
+				"@rollup/rollup-darwin-arm64": "4.35.0",
+				"@rollup/rollup-darwin-x64": "4.35.0",
+				"@rollup/rollup-freebsd-arm64": "4.35.0",
+				"@rollup/rollup-freebsd-x64": "4.35.0",
+				"@rollup/rollup-linux-arm-gnueabihf": "4.35.0",
+				"@rollup/rollup-linux-arm-musleabihf": "4.35.0",
+				"@rollup/rollup-linux-arm64-gnu": "4.35.0",
+				"@rollup/rollup-linux-arm64-musl": "4.35.0",
+				"@rollup/rollup-linux-loongarch64-gnu": "4.35.0",
+				"@rollup/rollup-linux-powerpc64le-gnu": "4.35.0",
+				"@rollup/rollup-linux-riscv64-gnu": "4.35.0",
+				"@rollup/rollup-linux-s390x-gnu": "4.35.0",
+				"@rollup/rollup-linux-x64-gnu": "4.35.0",
+				"@rollup/rollup-linux-x64-musl": "4.35.0",
+				"@rollup/rollup-win32-arm64-msvc": "4.35.0",
+				"@rollup/rollup-win32-ia32-msvc": "4.35.0",
+				"@rollup/rollup-win32-x64-msvc": "4.35.0",
+				"fsevents": "~2.3.2"
+			}
+		},
+		"node_modules/run-parallel": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+			"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+			"dev": true,
+			"funding": [
+				{
+					"type": "github",
+					"url": "https://github.com/sponsors/feross"
+				},
+				{
+					"type": "patreon",
+					"url": "https://www.patreon.com/feross"
+				},
+				{
+					"type": "consulting",
+					"url": "https://feross.org/support"
+				}
+			],
+			"license": "MIT",
+			"dependencies": {
+				"queue-microtask": "^1.2.2"
+			}
+		},
+		"node_modules/sade": {
+			"version": "1.8.1",
+			"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
+			"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"mri": "^1.1.0"
+			},
+			"engines": {
+				"node": ">=6"
+			}
+		},
+		"node_modules/semver": {
+			"version": "7.7.1",
+			"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
+			"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+			"dev": true,
+			"license": "ISC",
+			"bin": {
+				"semver": "bin/semver.js"
+			},
+			"engines": {
+				"node": ">=10"
+			}
+		},
+		"node_modules/set-cookie-parser": {
+			"version": "2.7.1",
+			"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
+			"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/shebang-command": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+			"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"shebang-regex": "^3.0.0"
+			},
+			"engines": {
+				"node": ">=8"
+			}
+		},
+		"node_modules/shebang-regex": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+			"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=8"
+			}
+		},
+		"node_modules/sirv": {
+			"version": "3.0.1",
+			"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz",
+			"integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@polka/url": "^1.0.0-next.24",
+				"mrmime": "^2.0.0",
+				"totalist": "^3.0.0"
+			},
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/source-map-js": {
+			"version": "1.2.1",
+			"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+			"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+			"license": "BSD-3-Clause",
+			"engines": {
+				"node": ">=0.10.0"
+			}
+		},
+		"node_modules/strip-json-comments": {
+			"version": "3.1.1",
+			"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+			"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=8"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/sindresorhus"
+			}
+		},
+		"node_modules/supports-color": {
+			"version": "7.2.0",
+			"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+			"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"has-flag": "^4.0.0"
+			},
+			"engines": {
+				"node": ">=8"
+			}
+		},
+		"node_modules/svelte": {
+			"version": "5.23.0",
+			"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.23.0.tgz",
+			"integrity": "sha512-v0lL3NuKontiCxholEiAXCB+BYbndlKbwlDMK0DS86WgGELMJSpyqCSbJeMEMBDwOglnS7Ar2Rq0wwa/z2L8Vg==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@ampproject/remapping": "^2.3.0",
+				"@jridgewell/sourcemap-codec": "^1.5.0",
+				"@sveltejs/acorn-typescript": "^1.0.5",
+				"@types/estree": "^1.0.5",
+				"acorn": "^8.12.1",
+				"aria-query": "^5.3.1",
+				"axobject-query": "^4.1.0",
+				"clsx": "^2.1.1",
+				"esm-env": "^1.2.1",
+				"esrap": "^1.4.3",
+				"is-reference": "^3.0.3",
+				"locate-character": "^3.0.0",
+				"magic-string": "^0.30.11",
+				"zimmerframe": "^1.1.2"
+			},
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/svelte-check": {
+			"version": "4.1.5",
+			"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.5.tgz",
+			"integrity": "sha512-Gb0T2IqBNe1tLB9EB1Qh+LOe+JB8wt2/rNBDGvkxQVvk8vNeAoG+vZgFB/3P5+zC7RWlyBlzm9dVjZFph/maIg==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@jridgewell/trace-mapping": "^0.3.25",
+				"chokidar": "^4.0.1",
+				"fdir": "^6.2.0",
+				"picocolors": "^1.0.0",
+				"sade": "^1.7.4"
+			},
+			"bin": {
+				"svelte-check": "bin/svelte-check"
+			},
+			"engines": {
+				"node": ">= 18.0.0"
+			},
+			"peerDependencies": {
+				"svelte": "^4.0.0 || ^5.0.0-next.0",
+				"typescript": ">=5.0.0"
+			}
+		},
+		"node_modules/svelte-eslint-parser": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.0.1.tgz",
+			"integrity": "sha512-JjdEMXOJqy+dxeaElxbN+meTOtVpHfLnq9VGpiTAOLgM0uHO+ogmUsA3IFgx0x3Wl15pqTZWycCikcD7cAQN/g==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"eslint-scope": "^8.2.0",
+				"eslint-visitor-keys": "^4.0.0",
+				"espree": "^10.0.0",
+				"postcss": "^8.4.49",
+				"postcss-scss": "^4.0.9",
+				"postcss-selector-parser": "^7.0.0"
+			},
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/ota-meshi"
+			},
+			"peerDependencies": {
+				"svelte": "^3.37.0 || ^4.0.0 || ^5.0.0"
+			},
+			"peerDependenciesMeta": {
+				"svelte": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/tailwindcss": {
+			"version": "4.0.14",
+			"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.14.tgz",
+			"integrity": "sha512-92YT2dpt671tFiHH/e1ok9D987N9fHD5VWoly1CdPD/Cd1HMglvZwP3nx2yTj2lbXDAHt8QssZkxTLCCTNL+xw==",
+			"license": "MIT"
+		},
+		"node_modules/tapable": {
+			"version": "2.2.1",
+			"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
+			"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+			"license": "MIT",
+			"engines": {
+				"node": ">=6"
+			}
+		},
+		"node_modules/to-regex-range": {
+			"version": "5.0.1",
+			"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+			"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"is-number": "^7.0.0"
+			},
+			"engines": {
+				"node": ">=8.0"
+			}
+		},
+		"node_modules/totalist": {
+			"version": "3.0.1",
+			"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
+			"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=6"
+			}
+		},
+		"node_modules/ts-api-utils": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz",
+			"integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=18.12"
+			},
+			"peerDependencies": {
+				"typescript": ">=4.8.4"
+			}
+		},
+		"node_modules/type-check": {
+			"version": "0.4.0",
+			"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+			"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"prelude-ls": "^1.2.1"
+			},
+			"engines": {
+				"node": ">= 0.8.0"
+			}
+		},
+		"node_modules/typescript": {
+			"version": "5.8.2",
+			"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
+			"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"bin": {
+				"tsc": "bin/tsc",
+				"tsserver": "bin/tsserver"
+			},
+			"engines": {
+				"node": ">=14.17"
+			}
+		},
+		"node_modules/typescript-eslint": {
+			"version": "8.26.1",
+			"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.26.1.tgz",
+			"integrity": "sha512-t/oIs9mYyrwZGRpDv3g+3K6nZ5uhKEMt2oNmAPwaY4/ye0+EH4nXIPYNtkYFS6QHm+1DFg34DbglYBz5P9Xysg==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@typescript-eslint/eslint-plugin": "8.26.1",
+				"@typescript-eslint/parser": "8.26.1",
+				"@typescript-eslint/utils": "8.26.1"
+			},
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/typescript-eslint"
+			},
+			"peerDependencies": {
+				"eslint": "^8.57.0 || ^9.0.0",
+				"typescript": ">=4.8.4 <5.9.0"
+			}
+		},
+		"node_modules/uri-js": {
+			"version": "4.4.1",
+			"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+			"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+			"dev": true,
+			"license": "BSD-2-Clause",
+			"dependencies": {
+				"punycode": "^2.1.0"
+			}
+		},
+		"node_modules/util-deprecate": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+			"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+			"dev": true,
+			"license": "MIT"
+		},
+		"node_modules/vite": {
+			"version": "6.2.1",
+			"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz",
+			"integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==",
+			"license": "MIT",
+			"dependencies": {
+				"esbuild": "^0.25.0",
+				"postcss": "^8.5.3",
+				"rollup": "^4.30.1"
+			},
+			"bin": {
+				"vite": "bin/vite.js"
+			},
+			"engines": {
+				"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+			},
+			"funding": {
+				"url": "https://github.com/vitejs/vite?sponsor=1"
+			},
+			"optionalDependencies": {
+				"fsevents": "~2.3.3"
+			},
+			"peerDependencies": {
+				"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+				"jiti": ">=1.21.0",
+				"less": "*",
+				"lightningcss": "^1.21.0",
+				"sass": "*",
+				"sass-embedded": "*",
+				"stylus": "*",
+				"sugarss": "*",
+				"terser": "^5.16.0",
+				"tsx": "^4.8.1",
+				"yaml": "^2.4.2"
+			},
+			"peerDependenciesMeta": {
+				"@types/node": {
+					"optional": true
+				},
+				"jiti": {
+					"optional": true
+				},
+				"less": {
+					"optional": true
+				},
+				"lightningcss": {
+					"optional": true
+				},
+				"sass": {
+					"optional": true
+				},
+				"sass-embedded": {
+					"optional": true
+				},
+				"stylus": {
+					"optional": true
+				},
+				"sugarss": {
+					"optional": true
+				},
+				"terser": {
+					"optional": true
+				},
+				"tsx": {
+					"optional": true
+				},
+				"yaml": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/vitefu": {
+			"version": "1.0.6",
+			"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz",
+			"integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==",
+			"dev": true,
+			"license": "MIT",
+			"workspaces": [
+				"tests/deps/*",
+				"tests/projects/*"
+			],
+			"peerDependencies": {
+				"vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
+			},
+			"peerDependenciesMeta": {
+				"vite": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/which": {
+			"version": "2.0.2",
+			"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+			"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+			"dev": true,
+			"license": "ISC",
+			"dependencies": {
+				"isexe": "^2.0.0"
+			},
+			"bin": {
+				"node-which": "bin/node-which"
+			},
+			"engines": {
+				"node": ">= 8"
+			}
+		},
+		"node_modules/word-wrap": {
+			"version": "1.2.5",
+			"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+			"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=0.10.0"
+			}
+		},
+		"node_modules/yaml": {
+			"version": "2.7.0",
+			"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
+			"integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
+			"license": "ISC",
+			"optional": true,
+			"peer": true,
+			"bin": {
+				"yaml": "bin.mjs"
+			},
+			"engines": {
+				"node": ">= 14"
+			}
+		},
+		"node_modules/yocto-queue": {
+			"version": "0.1.0",
+			"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+			"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+			"dev": true,
+			"license": "MIT",
+			"engines": {
+				"node": ">=10"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/sindresorhus"
+			}
+		},
+		"node_modules/zimmerframe": {
+			"version": "1.1.2",
+			"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
+			"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
+			"dev": true,
+			"license": "MIT"
+		}
+	}
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..5b8ea11
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,40 @@
+{
+  "name": "frontend",
+  "private": true,
+  "version": "0.0.1",
+  "type": "module",
+  "scripts": {
+    "dev": "vite dev",
+    "build": "vite build",
+    "preview": "vite preview",
+    "prepare": "svelte-kit sync || echo ''",
+    "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+    "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
+    "format": "prettier --write .",
+    "lint": "prettier --check . && eslint ."
+  },
+  "devDependencies": {
+    "@eslint/compat": "^1.2.5",
+    "@eslint/js": "^9.18.0",
+    "@sveltejs/adapter-auto": "^4.0.0",
+    "@sveltejs/adapter-static": "^3.0.8",
+    "@sveltejs/kit": "^2.16.0",
+    "@sveltejs/vite-plugin-svelte": "^5.0.0",
+    "eslint": "^9.18.0",
+    "eslint-config-prettier": "^10.0.1",
+    "eslint-plugin-svelte": "^3.0.0",
+    "globals": "^16.0.0",
+    "prettier": "^3.4.2",
+    "prettier-plugin-svelte": "^3.3.3",
+    "svelte": "^5.0.0",
+    "svelte-check": "^4.0.0",
+    "typescript": "^5.0.0",
+    "typescript-eslint": "^8.20.0",
+    "vite": "^6.0.0"
+  },
+  "dependencies": {
+    "@tailwindcss/vite": "^4.0.14",
+    "daisyui": "^5.0.3",
+    "tailwindcss": "^4.0.14"
+  }
+}
diff --git a/frontend/simple-ocpp-cs.code-workspace b/frontend/simple-ocpp-cs.code-workspace
index ba9fc2d..0ef33ba 100644
--- a/frontend/simple-ocpp-cs.code-workspace
+++ b/frontend/simple-ocpp-cs.code-workspace
@@ -2,11 +2,16 @@
   "folders": [
     {
       "name": "Frontend",
-      "path": "../frontend"
+      "path": "../frontend",
     },
     {
       "name": "Root",
-      "path": "../"
-    }
-  ]
-}
\ No newline at end of file
+      "path": "../",
+    },
+  ],
+  "settings": {
+    "editor.formatOnSave": true,
+    "editor.tabSize": 2,
+    "editor.detectIndentation": false,
+  },
+}
diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts
new file mode 100644
index 0000000..c0c0816
--- /dev/null
+++ b/frontend/src/app.d.ts
@@ -0,0 +1,13 @@
+// See https://svelte.dev/docs/kit/types#app.d.ts
+// for information about these interfaces
+declare global {
+  namespace App {
+    // interface Error {}
+    // interface Locals {}
+    // interface PageData {}
+    // interface PageState {}
+    // interface Platform {}
+  }
+}
+
+export {}
diff --git a/frontend/src/app.html b/frontend/src/app.html
new file mode 100644
index 0000000..84ffad1
--- /dev/null
+++ b/frontend/src/app.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <link rel="icon" href="%sveltekit.assets%/favicon.png" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    %sveltekit.head%
+  </head>
+  <body data-sveltekit-preload-data="hover">
+    <div style="display: contents">%sveltekit.body%</div>
+  </body>
+</html>
diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts
new file mode 100644
index 0000000..856f2b6
--- /dev/null
+++ b/frontend/src/lib/index.ts
@@ -0,0 +1 @@
+// place files you want to import through the `$lib` alias in this folder.
diff --git a/frontend/src/routes/+layout.ts b/frontend/src/routes/+layout.ts
new file mode 100644
index 0000000..62ad4e4
--- /dev/null
+++ b/frontend/src/routes/+layout.ts
@@ -0,0 +1 @@
+export const ssr = false
diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte
new file mode 100644
index 0000000..f18ab60
--- /dev/null
+++ b/frontend/src/routes/+page.svelte
@@ -0,0 +1,6 @@
+<script>
+  import '../style.css'
+</script>
+
+<h1>Welcome to SvelteKit</h1>
+<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
diff --git a/frontend/src/style.css b/frontend/src/style.css
new file mode 100644
index 0000000..8f7fe78
--- /dev/null
+++ b/frontend/src/style.css
@@ -0,0 +1,6 @@
+@import 'tailwindcss';
+@plugin "daisyui" {
+  themes:
+    light --default,
+    dark --prefersdark;
+}
diff --git a/frontend/static/favicon.png b/frontend/static/favicon.png
new file mode 100644
index 0000000000000000000000000000000000000000..825b9e65af7c104cfb07089bb28659393b4f2097
GIT binary patch
literal 1571
zcmV+;2Hg3HP)<h;3K|Lk000e1NJLTq004jh004jp1ONa4X*a1r00001b5ch_0Itp)
z=>Px)-AP12RCwC$UE6KzI1p6{F2N<Fgp}YCTtZ542`(WexCEDw|A=A)1A-t30wEX>
z1VK2vi|pOpn{~#djwYcWXTI_im_u^TJgMZ4JMOsSj!0ma>B?-(Hr@X&W@|R-$}W@Z
zg<YDbY-gN-C@$NXr>j#$x=!~7LGqHW?IO8+*oE1MyDp!G=L<gz=E*n%PA*mnuaD-%
zU>0#^lUx?;!fXv@l^6SvTnf^ac{5OurzC#ZMYc20lI%HhX816AYVs1T3heS1*WaWH
z%;x>)-J}YB5#CLzU@GBR6sXYrD>Vw(Fmt#|JP;+}<#6b63Ike{Fuo!?M{yEffez;|
zp!PfsuaC)>h>-AdbnwN13g*1LowNjT5?+lFVd#9$!8Z9HA|$*6dQ8EHLu}U|obW6f
z2%uGv?vr=KNq7YYa2Roj;|zooo<)lf=&2yxM@e`kM$CmCR#x>gI>I|*Ubr({5Y^rb
zghxQU22N}F51}^yfDSt7<V*0Nvi!iKPY@n`1~$se=LwHTOE7`*@+};uU_gT$1{Nib
z9&~5~nc4J(EWez%_f0Ty7T;wB{7s*oNO=9p8(o}N*_V>86oMTc!W&V;d?76)9KXX1
z+6Okem(d}YXmmOiZq$!IPk5t8nnS{%?+vDFz3BevmFN<oPlV?qzG@=jl`t@|E8zV8
zJ4}It4e%+dFPWw16u{sAX42jlFu{_e!AKZE75iV2&Q7>gpIod~R{>@#@5x9zJK<vg
zI0+@80NMa{gtNoRhsR-w%piFI3d-5xrN)R}H!WtK=Gp%dC5(a`Q0V4_vS-mj<((Z~
zbV*PucDY#zFVglY>EHLHv!gHeK~n)Ld!M8DB|Kfe%~123&Hz1Z(86nU7*G5chmyDe
ziV7$pB7pJ=96hpxHv9rCR29%bLOXlKU<_13_M8x)6;P8E1Kz6G<&P?$P^%c!M5`2`
zfY2zg;VK5~^>TJGQzc+33-n~gKt{{of8GzUkWmU110IgI0DLxRIM>0US|TsM=L|@F
z0Bun8U!cRB7-2apz=y-7*UxOxz@Z0)@QM)9wSGki1AZ38ceG7Q72z5`i;i=J`ILzL
z@iUO?SBBG-0cQuo+an4TsL<j(YStU%^TNkjEqg808>y-g-x;8P4UVwk|D8{W@U1Zi
z!M)+jqy@nQ$p?5tsHp-6J304Q={v-B>66$P0IDx&YT(`IcZ~bZfmn11#rXd7<5s}y
zBi9eim&zQc0Dk|2>$bs0PnLmDfMP5lcXRY&cvJ=zKxI^f0%-d$tD!`LBf9^jMSYUA
zI8U?CWdY@}cRq6{5~y+<zzg4N+!Mudx;=&$zSp?cjRmyF-+404b-P66DA17$<$H}=
z7$P4)QXt>)#h1!*-HcGW@+gZ4B};0OnC~`xQOyH19z*TA!!<qA9l$G~6DRxiCy7Vo
zOl^oK!S3XbayWz&p33NSw$lxR1fm)O*rCE0^ZNmk0wnax!!?%g599+O37!666~F(y
z5fq?5T*Fm{^%YR^PiM%z#%x`fAC+;C&`X5JXN_40SieIEXoaUU=*}=c0OC7@a*rFE
z3xr3yyFB~zRl(lNY&B@$Fia%81M!xeIuTYNz!Dz6eBKONjL<@dJdT$H?ShKl6$p=F
ze!fdgzelJI&n`Hk9f}>BJ%9s0V3F?CAJ{hTd#*tf+ur-W9MOURF-@B77_-OshsY}6
zOXRY=5%C^*26z?l)1=$bz30!so5tfABdSYzO+H=CpV~aaUefmjvfZ3Ttu9W&W3Iu6
zROlh0MFA5h;my}8lB0tAV-Rvc2Zs_CCSJnx@d`<ao$!3dCkTn3@aF0ny6VrToO6qA
z-~&2=w&2nT&o5u>**$idgy-iMob4dJWWw|21b4NB=LfsYp0Aeh{Ov)yztQi;eL4y5
zMi>8^SzKqk8~k?UiQK^^-5d8c%bV?$F8%X~czyiaKCI2=UH<v&YL5FIcieFm_zQ2O
VHuiSbmk|H}002ovPDHLkV1f%e^4kCa

literal 0
HcmV?d00001

diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js
new file mode 100644
index 0000000..dad5e99
--- /dev/null
+++ b/frontend/svelte.config.js
@@ -0,0 +1,17 @@
+import adapter from '@sveltejs/adapter-static'
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+  // Consult https://svelte.dev/docs/kit/integrations
+  // for more information about preprocessors
+  preprocess: vitePreprocess(),
+
+  kit: {
+    adapter: adapter({
+      fallback: 'index.html',
+    }),
+  },
+}
+
+export default config
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..f4d0a0e
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,19 @@
+{
+  "extends": "./.svelte-kit/tsconfig.json",
+  "compilerOptions": {
+    "allowJs": true,
+    "checkJs": true,
+    "esModuleInterop": true,
+    "forceConsistentCasingInFileNames": true,
+    "resolveJsonModule": true,
+    "skipLibCheck": true,
+    "sourceMap": true,
+    "strict": true,
+    "moduleResolution": "bundler"
+  }
+  // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
+  // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
+  //
+  // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
+  // from the referenced tsconfig.json - TypeScript does not merge them in
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..3766806
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,10 @@
+import tailwindcss from '@tailwindcss/vite'
+import { sveltekit } from '@sveltejs/kit/vite'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+  plugins: [tailwindcss(), sveltekit()],
+  server: {
+    host: '127.0.0.1',
+  },
+})

From 50972c209e7b7a919dacb9c555fdc1abf214bf26 Mon Sep 17 00:00:00 2001
From: BluemediaDev <oliver@traber-info.de>
Date: Thu, 13 Mar 2025 23:09:36 +0000
Subject: [PATCH 08/10] Check if database URL is set in env

---
 backend/app/database.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/backend/app/database.py b/backend/app/database.py
index 233eeb5..9b1bb1b 100644
--- a/backend/app/database.py
+++ b/backend/app/database.py
@@ -3,7 +3,10 @@ from sqlalchemy import create_engine
 from sqlalchemy.ext.declarative import declarative_base
 from sqlalchemy.orm import sessionmaker
 
-SQLALCHEMY_DATABASE_URL = os.getenv("CS_DATABASE_URL", "sqlite:///./simple-ocpp-cs.db")
+if os.getenv("CS_DATABASE_URL", "invalid") == "invalid":
+    raise SystemExit('ERROR: Invalid CS_DATABASE_URL environment variable')
+
+SQLALCHEMY_DATABASE_URL = os.getenv("CS_DATABASE_URL")
 
 if SQLALCHEMY_DATABASE_URL.startswith("sqlite"):
     engine = create_engine(

From 44e6f85da12d5904f9c0b942aaed74fa7884e7e0 Mon Sep 17 00:00:00 2001
From: BluemediaDev <oliver@traber-info.de>
Date: Thu, 13 Mar 2025 23:36:21 +0000
Subject: [PATCH 09/10] Fix migration for PostgreSQL

---
 .../20250313_c7f72154c90b-add_user_authentication.py   | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/backend/alembic/versions/20250313_c7f72154c90b-add_user_authentication.py b/backend/alembic/versions/20250313_c7f72154c90b-add_user_authentication.py
index 7e2c00b..6edc093 100644
--- a/backend/alembic/versions/20250313_c7f72154c90b-add_user_authentication.py
+++ b/backend/alembic/versions/20250313_c7f72154c90b-add_user_authentication.py
@@ -9,6 +9,7 @@ from typing import Sequence, Union
 
 from alembic import op
 import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
 
 
 # revision identifiers, used by Alembic.
@@ -31,16 +32,23 @@ def upgrade() -> None:
     )
     op.create_index(op.f('ix_sessions_refresh_token'), 'sessions', ['refresh_token'], unique=True)
     op.create_index(op.f('ix_sessions_user_id'), 'sessions', ['user_id'], unique=False)
+
     op.add_column('users', sa.Column('email', sa.String(), nullable=True))
     op.add_column('users', sa.Column('password', sa.String(), nullable=True))
-    op.add_column('users', sa.Column('role', sa.Enum('MEMBER', 'ADMINISTRATOR', name='role'), nullable=True))
+
+    role_enum = postgresql.ENUM('MEMBER', 'ADMINISTRATOR', name='role')
+    role_enum.create(op.get_bind(), checkfirst=False)
+    op.add_column('users', sa.Column('role', type_=role_enum, nullable=True))
+
     op.execute('UPDATE users SET email = id || \'@example.com\'')
     op.execute('UPDATE users SET password = \'invalid\'')
     op.execute('UPDATE users SET role = \'MEMBER\'')
+
     with op.batch_alter_table('users', schema=None) as batch_op:
         batch_op.alter_column('email', nullable=False)
         batch_op.alter_column('password', nullable=False)
         batch_op.alter_column('role', nullable=False)
+        
     op.drop_index('ix_users_friendly_name', table_name='users')
     op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
     # ### end Alembic commands ###

From e5c5e9498976290349b4d82672398f1f18773787 Mon Sep 17 00:00:00 2001
From: BluemediaDev <oliver@traber-info.de>
Date: Thu, 13 Mar 2025 23:36:57 +0000
Subject: [PATCH 10/10] Load dotenv in cli.py before model import

---
 backend/app/cli.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/backend/app/cli.py b/backend/app/cli.py
index 7ce93b6..f532d14 100644
--- a/backend/app/cli.py
+++ b/backend/app/cli.py
@@ -10,10 +10,9 @@ from sqlalchemy import select
 from sqlalchemy.orm import Session
 from argon2 import PasswordHasher
 
-from app.models import *
-
 load_dotenv()
 
+from app.models import *
 from app.database import SessionLocal
 
 def __get_user_by_email(db: Session, email: str):