mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-03 20:13:02 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4159f36f64 | |||
| 72de120fe2 | |||
| 00027f686b | |||
| ab01f915c3 |
@@ -0,0 +1,31 @@
|
|||||||
|
.git
|
||||||
|
.gitmodules
|
||||||
|
.venv
|
||||||
|
.pytest_cache
|
||||||
|
.ruff_cache
|
||||||
|
__pycache__
|
||||||
|
**/__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
**/node_modules
|
||||||
|
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
**/build
|
||||||
|
**/dist
|
||||||
|
|
||||||
|
.dart_tool
|
||||||
|
**/.dart_tool
|
||||||
|
|
||||||
|
swingmusic_mobile/build
|
||||||
|
swingmusic_mobile/.flutter-plugins-dependencies
|
||||||
|
swingmusic_mobile/.idea
|
||||||
|
|
||||||
|
swingmusic-desktop/target
|
||||||
|
|
||||||
|
tests
|
||||||
|
reference
|
||||||
|
scripts
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# CasaOS Deployment Guide
|
||||||
|
|
||||||
|
## Why CasaOS Was Failing
|
||||||
|
|
||||||
|
The backend can fail in containerized environments when no static web client is bundled.
|
||||||
|
If `client.zip` is not available at runtime, the server may try to fetch web assets dynamically, which is fragile on appliance-like deployments.
|
||||||
|
|
||||||
|
## What Is Fixed In This Repo
|
||||||
|
|
||||||
|
- `Dockerfile` is now multi-stage and bundles webclient assets into `/app/client`.
|
||||||
|
- Runtime now uses `SWINGMUSIC_CLIENT_DIR=/app/client`.
|
||||||
|
- Compose has health checks for backend + Dragonfly.
|
||||||
|
- Added `docker-compose.casaos.yml` with CasaOS-friendly path/environment defaults.
|
||||||
|
|
||||||
|
## 1) Build Image On The CasaOS Host
|
||||||
|
|
||||||
|
Run on your CasaOS machine from this repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t swingmusic-local:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
If you prefer a registry image, set `SWINGMUSIC_IMAGE` accordingly in step 2.
|
||||||
|
|
||||||
|
## 2) Use CasaOS-Friendly Compose
|
||||||
|
|
||||||
|
Use [`docker-compose.casaos.yml`](/home/tdvorak/Desktop/PROG+HTML/SwingMusic/docker-compose.casaos.yml) as your custom app compose.
|
||||||
|
|
||||||
|
Important env values:
|
||||||
|
|
||||||
|
- `SWINGMUSIC_IMAGE` default: `swingmusic-local:latest`
|
||||||
|
- `SWINGMUSIC_PORT` default: `1970`
|
||||||
|
- `SWINGMUSIC_MUSIC_DIR` default: `/DATA/Media/Music`
|
||||||
|
- `SWINGMUSIC_CONFIG_DIR` default: `/DATA/AppData/swingmusic/config`
|
||||||
|
- `SWINGMUSIC_DRAGONFLY_DIR` default: `/DATA/AppData/swingmusic/dragonfly`
|
||||||
|
|
||||||
|
Pre-create and permission-check app data dirs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /DATA/AppData/swingmusic/config /DATA/AppData/swingmusic/dragonfly
|
||||||
|
chmod -R 775 /DATA/AppData/swingmusic
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3) Smoke Test
|
||||||
|
|
||||||
|
After app starts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS http://127.0.0.1:1970/healthz
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: JSON with `"ok": true`.
|
||||||
|
|
||||||
|
Then open:
|
||||||
|
|
||||||
|
- `http://<casaos-host-ip>:1970/`
|
||||||
|
- Complete setup (owner + music directory).
|
||||||
|
|
||||||
|
## 4) Verify Auth Flows (Web + Mobile + Desktop)
|
||||||
|
|
||||||
|
From web UI:
|
||||||
|
|
||||||
|
1. Login.
|
||||||
|
2. Generate pairing QR/code.
|
||||||
|
3. Mobile: scan QR or enter `server_url|pair_code` manually.
|
||||||
|
4. Desktop: use manual URL + pair code or URL + username/password.
|
||||||
|
|
||||||
|
Expected behavior:
|
||||||
|
|
||||||
|
- Unauthenticated clients are blocked from main app.
|
||||||
|
- `/auth/pair` code is single-use (first 200, second 400).
|
||||||
|
- Protected APIs return 401 without token.
|
||||||
|
|
||||||
|
## 5) If It Still Fails In CasaOS
|
||||||
|
|
||||||
|
Check container status:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker ps -a | grep -E 'swingmusic|dragonfly'
|
||||||
|
```
|
||||||
|
|
||||||
|
Or run the bundled diagnostics script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/casaos-diagnose.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Check logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs --tail 200 swingmusic
|
||||||
|
docker logs --tail 200 swingmusic-dragonfly
|
||||||
|
```
|
||||||
|
|
||||||
|
Check rendered compose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.casaos.yml config
|
||||||
|
```
|
||||||
|
|
||||||
|
Common issues:
|
||||||
|
|
||||||
|
1. Image missing:
|
||||||
|
- Build image first or set `SWINGMUSIC_IMAGE` to a valid registry tag.
|
||||||
|
2. Port conflict on `1970` or `6379`:
|
||||||
|
- Change `SWINGMUSIC_PORT` / `DRAGONFLY_PORT`.
|
||||||
|
3. Permission denied on `/DATA/AppData/...`:
|
||||||
|
- Fix host dir ownership/permissions.
|
||||||
|
4. DNS/pull failures:
|
||||||
|
- Confirm CasaOS host can pull `docker.dragonflydb.io/dragonflydb/dragonfly`.
|
||||||
|
|
||||||
|
## Notes About CasaOS Compose Compatibility
|
||||||
|
|
||||||
|
- CasaOS AppStore release notes mention Docker Compose engine upgrades to support newer compose formats:
|
||||||
|
https://github.com/IceWhaleTech/CasaOS-AppStore/releases
|
||||||
|
- Many CasaOS app templates map persistent data under `/DATA/AppData/$AppID/...`:
|
||||||
|
https://community.bigbeartechworld.com/t/no-update-for-immich/4089/4
|
||||||
+31
-9
@@ -1,26 +1,48 @@
|
|||||||
FROM python:3.11-slim
|
FROM node:20-bookworm-slim AS webclient-builder
|
||||||
|
WORKDIR /webclient
|
||||||
|
|
||||||
|
COPY swingmusic-webclient/package.json swingmusic-webclient/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY swingmusic-webclient/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
|
FROM python:3.11-slim AS runtime
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
LABEL "author"="swing music"
|
LABEL "author"="swing music"
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
SWINGMUSIC_CLIENT_DIR=/app/client
|
||||||
|
|
||||||
EXPOSE 1970/tcp
|
EXPOSE 1970/tcp
|
||||||
VOLUME /music
|
VOLUME /music
|
||||||
VOLUME /config
|
VOLUME /config
|
||||||
|
|
||||||
RUN apt-get update
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
RUN apt-get install -y gcc libev-dev
|
gcc \
|
||||||
RUN apt-get install -y ffmpeg libavcodec-extra
|
libev-dev \
|
||||||
RUN apt-get install -y redis-tools # For DragonflyDB/Redis connectivity
|
ffmpeg \
|
||||||
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
|
libavcodec-extra \
|
||||||
|
redis-tools \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy repo root files needed for installation
|
# Copy repo root files needed for installation
|
||||||
COPY pyproject.toml requirements.txt version.txt ./
|
COPY pyproject.toml requirements.txt version.txt ./
|
||||||
COPY src/ ./src/
|
COPY src/ ./src/
|
||||||
|
|
||||||
# Install the package and its dependencies
|
# Install the package and its dependencies
|
||||||
RUN pip install --no-cache-dir .
|
RUN pip install --no-cache-dir .
|
||||||
|
|
||||||
# Install Redis library for DragonflyDB support
|
# Install Redis library for DragonflyDB support
|
||||||
RUN pip install redis
|
RUN pip install --no-cache-dir redis
|
||||||
|
|
||||||
|
# Ship a deterministic web client with the backend image so startup does not
|
||||||
|
# depend on downloading release assets at runtime.
|
||||||
|
COPY --from=webclient-builder /webclient/dist /app/client
|
||||||
|
|
||||||
ENTRYPOINT ["python", "-m", "swingmusic", "--host", "0.0.0.0", "--config", "/config"]
|
ENTRYPOINT ["python", "-m", "swingmusic", "--host", "0.0.0.0", "--config", "/config"]
|
||||||
|
|||||||
@@ -182,6 +182,13 @@ docker run --name swingmusic -p 1970:1970 \
|
|||||||
ghcr.io/Dvorinka/swingmusic-extended:latest
|
ghcr.io/Dvorinka/swingmusic-extended:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### CasaOS
|
||||||
|
|
||||||
|
For CasaOS custom-app deployment, use:
|
||||||
|
|
||||||
|
- [`docker-compose.casaos.yml`](/home/tdvorak/Desktop/PROG+HTML/SwingMusic/docker-compose.casaos.yml)
|
||||||
|
- [`CASAOS_DEPLOYMENT.md`](/home/tdvorak/Desktop/PROG+HTML/SwingMusic/CASAOS_DEPLOYMENT.md)
|
||||||
|
|
||||||
## 🛠️ Development
|
## 🛠️ Development
|
||||||
|
|
||||||
### Backend Development
|
### Backend Development
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
name: swingmusic
|
||||||
|
|
||||||
|
services:
|
||||||
|
swingmusic:
|
||||||
|
image: ${SWINGMUSIC_IMAGE:-swingmusic-local:latest}
|
||||||
|
container_name: swingmusic
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${SWINGMUSIC_PORT:-1970}:1970"
|
||||||
|
environment:
|
||||||
|
DRAGONFLYDB_HOST: dragonfly
|
||||||
|
DRAGONFLYDB_PORT: "6379"
|
||||||
|
SWINGMUSIC_CLIENT_DIR: /app/client
|
||||||
|
volumes:
|
||||||
|
- ${SWINGMUSIC_MUSIC_DIR:-/DATA/Media/Music}:/music
|
||||||
|
- ${SWINGMUSIC_CONFIG_DIR:-/DATA/AppData/swingmusic/config}:/config
|
||||||
|
depends_on:
|
||||||
|
- dragonfly
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:1970/healthz', timeout=3)"]
|
||||||
|
interval: 20s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
dragonfly:
|
||||||
|
image: docker.dragonflydb.io/dragonflydb/dragonfly
|
||||||
|
container_name: swingmusic-dragonfly
|
||||||
|
restart: unless-stopped
|
||||||
|
command: --dir=/data --logtostdout
|
||||||
|
ports:
|
||||||
|
- "${DRAGONFLY_PORT:-6379}:6379"
|
||||||
|
volumes:
|
||||||
|
- ${SWINGMUSIC_DRAGONFLY_DIR:-/DATA/AppData/swingmusic/dragonfly}:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
+7
-1
@@ -1,8 +1,9 @@
|
|||||||
version: '3.8'
|
name: swingmusic
|
||||||
|
|
||||||
services:
|
services:
|
||||||
swingmusic:
|
swingmusic:
|
||||||
build: .
|
build: .
|
||||||
|
container_name: swingmusic
|
||||||
ports:
|
ports:
|
||||||
- "1970:1970"
|
- "1970:1970"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -13,6 +14,11 @@ services:
|
|||||||
- DRAGONFLYDB_PORT=6379
|
- DRAGONFLYDB_PORT=6379
|
||||||
depends_on:
|
depends_on:
|
||||||
- dragonfly
|
- dragonfly
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:1970/healthz', timeout=3)"]
|
||||||
|
interval: 20s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- swingmusic-network
|
- swingmusic-network
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ dependencies = [
|
|||||||
"bjoern >=3.2.2; sys_platform != 'win32'",
|
"bjoern >=3.2.2; sys_platform != 'win32'",
|
||||||
"aiohttp>=3.13.3",
|
"aiohttp>=3.13.3",
|
||||||
"aiofiles>=25.1.0",
|
"aiofiles>=25.1.0",
|
||||||
|
"redis>=5.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
[tool:pytest]
|
[pytest]
|
||||||
testpaths = tests
|
testpaths = tests
|
||||||
python_files = test_*.py
|
python_files = test_*.py
|
||||||
python_classes = Test*
|
python_classes = Test*
|
||||||
|
|||||||
+2
-1
@@ -26,4 +26,5 @@ rapidfuzz==3.11.0
|
|||||||
pendulum>=3.0.0
|
pendulum>=3.0.0
|
||||||
pystray>=0.19.5
|
pystray>=0.19.5
|
||||||
waitress==3.0.2; sys_platform == 'win32'
|
waitress==3.0.2; sys_platform == 'win32'
|
||||||
bjoern>=3.2.2; sys_platform != 'win32'
|
bjoern>=3.2.2; sys_platform != 'win32'
|
||||||
|
redis>=5.2.1
|
||||||
|
|||||||
Executable
+34
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "== CasaOS / Docker diagnostics =="
|
||||||
|
echo "timestamp: $(date -Is)"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "== Docker client/server =="
|
||||||
|
docker version || true
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "== Docker compose version =="
|
||||||
|
docker compose version || true
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "== Render compose =="
|
||||||
|
docker compose -f docker-compose.casaos.yml config || true
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "== Running containers =="
|
||||||
|
docker ps -a --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}' | grep -E 'swingmusic|dragonfly|NAMES' || true
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "== swingmusic logs (tail 200) =="
|
||||||
|
docker logs --tail 200 swingmusic 2>&1 || true
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "== dragonfly logs (tail 200) =="
|
||||||
|
docker logs --tail 200 swingmusic-dragonfly 2>&1 || true
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "== Health endpoint =="
|
||||||
|
curl -sS -m 5 http://127.0.0.1:1970/healthz || true
|
||||||
|
echo
|
||||||
@@ -39,9 +39,30 @@ api = APIBlueprint("auth", __name__, url_prefix="/auth", abp_tags=[bp_tag])
|
|||||||
|
|
||||||
def get_limiter():
|
def get_limiter():
|
||||||
"""Get the rate limiter from app context."""
|
"""Get the rate limiter from app context."""
|
||||||
from flask import current_app
|
# Prefer the global limiter initialized in app_builder.build().
|
||||||
|
# flask-limiter v4 may store a set in current_app.extensions["limiter"],
|
||||||
|
# so resolve defensively across versions.
|
||||||
|
try:
|
||||||
|
from swingmusic.app_builder import limiter as app_limiter
|
||||||
|
|
||||||
return current_app.extensions.get("limiter")
|
if app_limiter is not None and hasattr(app_limiter, "limit"):
|
||||||
|
return app_limiter
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ext = current_app.extensions.get("limiter")
|
||||||
|
if ext is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if hasattr(ext, "limit"):
|
||||||
|
return ext
|
||||||
|
|
||||||
|
if isinstance(ext, set):
|
||||||
|
for candidate in ext:
|
||||||
|
if hasattr(candidate, "limit"):
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def rate_limit(limit: str):
|
def rate_limit(limit: str):
|
||||||
@@ -208,10 +229,15 @@ def login(body: LoginBody):
|
|||||||
|
|
||||||
# Cache user session in DragonflyDB for fast lookups
|
# Cache user session in DragonflyDB for fast lookups
|
||||||
session_service = get_user_session_service()
|
session_service = get_user_session_service()
|
||||||
if session_service.session_cache.client.is_available():
|
if session_service.cache.client.is_available():
|
||||||
import contextlib
|
import contextlib
|
||||||
|
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
|
session_service.create_session(
|
||||||
|
token,
|
||||||
|
user.todict(),
|
||||||
|
ttl_hours=max(1, int(age // 3600)),
|
||||||
|
)
|
||||||
session_service.set_user_session(user.id, user.todict(), ttl_seconds=age)
|
session_service.set_user_session(user.id, user.todict(), ttl_seconds=age)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
@@ -334,13 +360,17 @@ def get_pair():
|
|||||||
server_url = request.headers.get("Origin", "").strip()
|
server_url = request.headers.get("Origin", "").strip()
|
||||||
if not server_url:
|
if not server_url:
|
||||||
server_url = request.host_url.rstrip("/")
|
server_url = request.host_url.rstrip("/")
|
||||||
|
else:
|
||||||
|
server_url = server_url.rstrip("/")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"code": code,
|
"code": code,
|
||||||
"expires_at": expires_at,
|
"expires_at": expires_at,
|
||||||
"ttl_seconds": pair_token_store.ttl_seconds,
|
"ttl_seconds": pair_token_store.ttl_seconds,
|
||||||
"server_url": server_url,
|
"server_url": server_url,
|
||||||
"qr_payload": f"{server_url} {code}",
|
# Keep payload contract explicit for mobile/desktop clients.
|
||||||
|
# Format: "<server_url>|<pair_code>"
|
||||||
|
"qr_payload": f"{server_url}|{code}",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -582,11 +612,11 @@ def logout():
|
|||||||
# Invalidate session in DragonflyDB
|
# Invalidate session in DragonflyDB
|
||||||
if current_user:
|
if current_user:
|
||||||
session_service = get_user_session_service()
|
session_service = get_user_session_service()
|
||||||
if session_service.session_cache.client.is_available():
|
if session_service.cache.client.is_available():
|
||||||
import contextlib
|
import contextlib
|
||||||
|
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
session_service.invalidate_session(current_user["id"])
|
session_service.invalidate_user_session(current_user["id"])
|
||||||
|
|
||||||
res = jsonify({"msg": "Logged out"})
|
res = jsonify({"msg": "Logged out"})
|
||||||
res.delete_cookie("access_token_cookie")
|
res.delete_cookie("access_token_cookie")
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ class UserSessionService:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.cache = DragonflyCache("sessions")
|
self.cache = DragonflyCache("sessions")
|
||||||
|
# Backward compatibility for older call sites.
|
||||||
|
self.session_cache = self.cache
|
||||||
|
|
||||||
def create_session(
|
def create_session(
|
||||||
self, session_token: str, user_data: dict[str, Any], ttl_hours: int = 24
|
self, session_token: str, user_data: dict[str, Any], ttl_hours: int = 24
|
||||||
@@ -119,6 +121,17 @@ class UserSessionService:
|
|||||||
"""Create user session"""
|
"""Create user session"""
|
||||||
return self.cache.set(f"session:{session_token}", user_data, ttl_hours)
|
return self.cache.set(f"session:{session_token}", user_data, ttl_hours)
|
||||||
|
|
||||||
|
def set_user_session(
|
||||||
|
self, userid: int, user_data: dict[str, Any], ttl_seconds: int = 24 * 3600
|
||||||
|
):
|
||||||
|
"""Store latest session payload by user id for quick lookups."""
|
||||||
|
ttl_hours = max(1, int(ttl_seconds // 3600))
|
||||||
|
return self.cache.set(f"user_session:{userid}", user_data, ttl_hours)
|
||||||
|
|
||||||
|
def get_user_session(self, userid: int) -> dict[str, Any] | None:
|
||||||
|
"""Get latest session payload for a user id."""
|
||||||
|
return self.cache.get(f"user_session:{userid}")
|
||||||
|
|
||||||
def get_session(self, session_token: str) -> dict[str, Any] | None:
|
def get_session(self, session_token: str) -> dict[str, Any] | None:
|
||||||
"""Get user session"""
|
"""Get user session"""
|
||||||
return self.cache.get(f"session:{session_token}")
|
return self.cache.get(f"session:{session_token}")
|
||||||
@@ -131,6 +144,10 @@ class UserSessionService:
|
|||||||
"""Invalidate user session"""
|
"""Invalidate user session"""
|
||||||
return self.cache.delete(f"session:{session_token}")
|
return self.cache.delete(f"session:{session_token}")
|
||||||
|
|
||||||
|
def invalidate_user_session(self, userid: int):
|
||||||
|
"""Invalidate latest session payload for a user id."""
|
||||||
|
return self.cache.delete(f"user_session:{userid}")
|
||||||
|
|
||||||
def get_user_sessions(self, userid: int) -> list[str]:
|
def get_user_sessions(self, userid: int) -> list[str]:
|
||||||
"""Get all active sessions for user"""
|
"""Get all active sessions for user"""
|
||||||
pattern = "session:*"
|
pattern = "session:*"
|
||||||
|
|||||||
+1
-1
Submodule swingmusic-desktop updated: 74be0b1d12...bf45d36f41
+1
-1
Submodule swingmusic-webclient updated: 19d402a940...cad049797e
+1
-1
Submodule swingmusic_mobile updated: fa027b588b...7bd87ac1f9
Reference in New Issue
Block a user