2 Commits

Author SHA1 Message Date
e5086ec653 Add authentication and session management improvements
Some checks failed
Build and push image / deploy (push) Has been cancelled
Introduce navigation guards for authentication and admin access within routes. Replace localStorage usage with secure token storage via httpOnly cookies, and add token blacklisting upon logout. Enhance token refresh mechanism and session expiration handling to improve security and user experience.
2025-05-21 22:53:29 +01:00
dd5817419b Remove ThePdfReader.vue and migrate PDF handling to pymupdf
This commit removes the frontend component ThePdfReader.vue and replaces its functionality with a backend implementation based on pymupdf. Also includes package updates, refactors PDF archive handling, and adjusts security settings to support development on localhost.
2025-05-21 22:30:34 +01:00
15 changed files with 322 additions and 789 deletions

View File

@@ -46,6 +46,7 @@ INSTALLED_APPS = [
"corsheaders", "corsheaders",
'django_filters', 'django_filters',
'rest_framework', 'rest_framework',
'rest_framework_simplejwt.token_blacklist',
# 'silk' # 'silk'
] ]
@@ -197,8 +198,8 @@ CSP_STYLE_SRC = (
) )
CSP_IMG_SRC = ("'self'", "data:") CSP_IMG_SRC = ("'self'", "data:")
CSP_FONT_SRC = ("'self'",) CSP_FONT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", "'sha256-IYBrMxCTJ62EwagLTIRncEIpWwTmoXcXkqv3KZm/Wik='") CSP_SCRIPT_SRC = ("'self'", "'unsafe-eval'", "'unsafe-inline'", "localhost:8080")
CSP_CONNECT_SRC = ("'self'",) CSP_CONNECT_SRC = ("'self'", "ws://localhost:8080/ws")
CSP_INCLUDE_NONCE_IN = ['script-src'] CSP_INCLUDE_NONCE_IN = ['script-src']
CSP_SCRIPT_SRC_ATTR = ("'self'",) # "'unsafe-inline'") CSP_SCRIPT_SRC_ATTR = ("'self'",) # "'unsafe-inline'")
@@ -237,8 +238,13 @@ REST_FRAMEWORK = {
CORS_ALLOW_ALL_ORIGINS = True CORS_ALLOW_ALL_ORIGINS = True
SIMPLE_JWT = { SIMPLE_JWT = {
"ROTATE_REFRESH_TOKENS": True, "ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=10), 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=10),
'LEEWAY': timedelta(minutes=5), 'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'LEEWAY': timedelta(seconds=30),
'ALGORITHM': 'HS256',
'AUDIENCE': 'cbwebreader-users',
'ISSUER': 'cbwebreader',
} }
FRONTEND_DIR = os.path.join(BASE_DIR, 'frontend') FRONTEND_DIR = os.path.join(BASE_DIR, 'frontend')

View File

@@ -24,7 +24,7 @@ from drf_yasg.views import get_schema_view
from rest_framework import permissions from rest_framework import permissions
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
# from rest_framework_extensions.routers import ExtendedDefaultRouter # from rest_framework_extensions.routers import ExtendedDefaultRouter
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenBlacklistView
from comic import rest, feeds from comic import rest, feeds
@@ -62,6 +62,7 @@ urlpatterns = [
re_path(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), re_path(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('api/token/blacklist/', TokenBlacklistView.as_view(), name='token_blacklist'),
path('api/', include(router.urls)), path('api/', include(router.urls)),
path("", path("",
TemplateView.as_view(template_name="application.html"), TemplateView.as_view(template_name="application.html"),

View File

@@ -7,7 +7,7 @@ from pathlib import Path
from typing import Optional, List, Union, Tuple, Final, IO from typing import Optional, List, Union, Tuple, Final, IO
# noinspection PyPackageRequirements # noinspection PyPackageRequirements
import fitz import pymupdf
import rarfile import rarfile
from PIL import Image, UnidentifiedImageError from PIL import Image, UnidentifiedImageError
from PIL.Image import Image as Image_type from PIL.Image import Image as Image_type
@@ -52,7 +52,8 @@ class Directory(models.Model):
ordering = ['name'] ordering = ['name']
def __str__(self) -> str: def __str__(self) -> str:
return f"Directory: {self.name}; {self.parent}"
return f"Directory: {self.name}: {self.parent}"
@property @property
def title(self) -> str: def title(self) -> str:
@@ -141,21 +142,34 @@ class ComicBook(models.Model):
return Path(base_dir, self.file_name) return Path(base_dir, self.file_name)
def get_image(self, page: int) -> Union[Tuple[IO[bytes], str], Tuple[bool, bool]]: def get_image(self, page: int) -> Union[Tuple[IO[bytes], str], Tuple[bool, bool]]:
base_dir = settings.COMIC_BOOK_VOLUME if self.file_name.lower().endswith('.pdf'):
if self.directory: # noinspection PyUnresolvedReferences
archive_path = Path(base_dir, self.directory.path, self.file_name) doc = pymupdf.open(self.get_pdf())
page: pymupdf.Page = doc[page]
pix = page.get_pixmap()
mode: Final = "RGBA" if pix.alpha else "RGB"
# noinspection PyTypeChecker
pil_data = Image.frombytes(mode, (pix.width, pix.height), pix.samples)
img = io.BytesIO()
pil_data.save(img, format="PNG")
img.seek(0)
return img, "Image/PNG"
else: else:
archive_path = Path(base_dir, self.file_name) base_dir = settings.COMIC_BOOK_VOLUME
try: if self.directory:
archive = rarfile.RarFile(archive_path) archive_path = Path(base_dir, self.directory.path, self.file_name)
except rarfile.NotRarFile: else:
# pylint: disable=consider-using-with archive_path = Path(base_dir, self.file_name)
archive = zipfile.ZipFile(archive_path) try:
except zipfile.BadZipfile: archive = rarfile.RarFile(archive_path)
return False, False except rarfile.NotRarFile:
# pylint: disable=consider-using-with
archive = zipfile.ZipFile(archive_path)
except zipfile.BadZipfile:
return False, False
file_name, file_mime = self.get_archive_files(archive)[page] file_name, file_mime = self.get_archive_files(archive)[page]
return archive.open(file_name), file_mime return archive.open(file_name), file_mime
def generate_thumbnail_pdf(self, page_index: int = 0) -> Tuple[io.BytesIO, Image_type, str]: def generate_thumbnail_pdf(self, page_index: int = 0) -> Tuple[io.BytesIO, Image_type, str]:
img, pil_data = self._get_pdf_image(page_index if page_index else 0) img, pil_data = self._get_pdf_image(page_index if page_index else 0)
@@ -196,8 +210,7 @@ class ComicBook(models.Model):
self.save() self.save()
def _get_pdf_image(self, page_index: int) -> Tuple[io.BytesIO, Image_type]: def _get_pdf_image(self, page_index: int) -> Tuple[io.BytesIO, Image_type]:
# noinspection PyUnresolvedReferences doc = pymupdf.open(self.get_pdf())
doc = fitz.open(self.get_pdf())
page = doc[page_index] page = doc[page_index]
pix = page.get_pixmap() pix = page.get_pixmap()
mode: Final = "RGBA" if pix.alpha else "RGB" mode: Final = "RGBA" if pix.alpha else "RGB"
@@ -239,7 +252,7 @@ class ComicBook(models.Model):
return Path(settings.COMIC_BOOK_VOLUME, self.directory.get_path(), self.file_name) return Path(settings.COMIC_BOOK_VOLUME, self.directory.get_path(), self.file_name)
return Path(settings.COMIC_BOOK_VOLUME, self.file_name) return Path(settings.COMIC_BOOK_VOLUME, self.file_name)
def get_archive(self) -> Tuple[Union[rarfile.RarFile, zipfile.ZipFile, fitz.Document], str]: def get_archive(self) -> Tuple[Union[rarfile.RarFile, zipfile.ZipFile, pymupdf.Document], str]:
archive_path = self.get_archive_path archive_path = self.get_archive_path
try: try:
return rarfile.RarFile(archive_path), 'archive' return rarfile.RarFile(archive_path), 'archive'
@@ -252,7 +265,7 @@ class ComicBook(models.Model):
try: try:
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
return fitz.open(str(archive_path)), 'pdf' return pymupdf.open(str(archive_path)), 'pdf'
except RuntimeError: except RuntimeError:
pass pass
raise NotCompatibleArchive raise NotCompatibleArchive
@@ -291,8 +304,8 @@ class ComicStatus(models.Model):
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
f"<ComicStatus:{self.user.username}:{self.comic.file_name}:{self.last_read_page}:" f"<ComicStatus: {self.user.username}: {self.comic.file_name}: {self.last_read_page}: "
f"{self.unread}:{self.finished}" f"{self.unread}: {self.finished}"
) )

View File

@@ -1,3 +1,4 @@
from http.client import HTTPResponse
from pathlib import Path from pathlib import Path
from typing import Union, Optional, Dict, Iterable, List from typing import Union, Optional, Dict, Iterable, List
from uuid import UUID from uuid import UUID
@@ -247,7 +248,7 @@ class ReadViewSet(viewsets.GenericViewSet):
@swagger_auto_schema(responses={status.HTTP_200_OK: 'PDF Binary Data', @swagger_auto_schema(responses={status.HTTP_200_OK: 'PDF Binary Data',
status.HTTP_400_BAD_REQUEST: 'User below classification allowed'}) status.HTTP_400_BAD_REQUEST: 'User below classification allowed'})
@action(methods=['get'], detail=True) @action(methods=['get'], detail=True)
def pdf(self, request: Request, selector: UUID) -> Union[FileResponse, Response]: def pdf(self, request: Request, selector: UUID) -> Union[FileResponse, Response, HTTPResponse]:
book = models.ComicBook.objects.get(selector=selector) book = models.ComicBook.objects.get(selector=selector)
misc, _ = models.UserMisc.objects.get_or_create(user=request.user) misc, _ = models.UserMisc.objects.get_or_create(user=request.user)
try: try:

1
data Normal file

File diff suppressed because one or more lines are too long

View File

@@ -15,25 +15,22 @@
"bootstrap": "^5.2.0", "bootstrap": "^5.2.0",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"pdfvuer": "^2.0.1",
"reveal.js": "^5.2.1", "reveal.js": "^5.2.1",
"timeago.js": "^4.0.2", "timeago.js": "^4.0.2",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.0.3", "vue-router": "^4.0.3",
"vue-toast-notification": "3.0", "vue-toast-notification": "^3.0",
"vuejs-paginate-next": "^1.0.2", "vuejs-paginate-next": "^1.0.2",
"vuex": "^4.0.0", "vuex": "^4.0.0",
"webpack": "^5.98.0" "webpack": "^5.98.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.26.10", "@babel/core": "^7.26.10",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "^5.0.8", "@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-plugin-eslint": "^5.0.8",
"@vue/cli-plugin-router": "^5.0.0", "@vue/cli-plugin-router": "^5.0.0",
"@vue/cli-plugin-vuex": "^5.0.0", "@vue/cli-plugin-vuex": "^5.0.0",
"@vue/cli-service": "^5.0.8", "@vue/cli-service": "^5.0.8",
"eslint": "^9.23.0", "eslint": "^9.24.0",
"eslint-plugin-vue": "^10.0.0", "eslint-plugin-vue": "^10.0.0",
"jshint": "^2.13.5", "jshint": "^2.13.5",
"mini-css-extract-plugin": "^2.9.2", "mini-css-extract-plugin": "^2.9.2",
@@ -130,25 +127,6 @@
"url": "https://opencollective.com/babel" "url": "https://opencollective.com/babel"
} }
}, },
"node_modules/@babel/eslint-parser": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.27.0.tgz",
"integrity": "sha512-dtnzmSjXfgL/HDgMcmsLSzyGbEosi4DrGWoCNfuI+W4IkVJw6izpTe7LtOdwAXnkDqw5yweboYCTkM2rQizCng==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1",
"eslint-visitor-keys": "^2.1.0",
"semver": "^6.3.1"
},
"engines": {
"node": "^10.13.0 || ^12.13.0 || >=14.0.0"
},
"peerDependencies": {
"@babel/core": "^7.11.0",
"eslint": "^7.5.0 || ^8.0.0 || ^9.0.0"
}
},
"node_modules/@babel/generator": { "node_modules/@babel/generator": {
"version": "7.27.0", "version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz",
@@ -1741,9 +1719,9 @@
} }
}, },
"node_modules/@eslint/config-array": { "node_modules/@eslint/config-array": {
"version": "0.19.2", "version": "0.20.0",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz",
"integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@@ -1816,9 +1794,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.23.0", "version": "9.24.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.23.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz",
"integrity": "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==", "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -2149,16 +2127,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
"integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"eslint-scope": "5.1.1"
}
},
"node_modules/@node-ipc/js-queue": { "node_modules/@node-ipc/js-queue": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/@node-ipc/js-queue/-/js-queue-2.0.3.tgz", "resolved": "https://registry.npmjs.org/@node-ipc/js-queue/-/js-queue-2.0.3.tgz",
@@ -2827,148 +2795,6 @@
"@vue/cli-service": "^3.0.0 || ^4.0.0 || ^5.0.0-0" "@vue/cli-service": "^3.0.0 || ^4.0.0 || ^5.0.0-0"
} }
}, },
"node_modules/@vue/cli-plugin-eslint": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-eslint/-/cli-plugin-eslint-5.0.8.tgz",
"integrity": "sha512-d11+I5ONYaAPW1KyZj9GlrV/E6HZePq5L5eAF5GgoVdu6sxr6bDgEoxzhcS1Pk2eh8rn1MxG/FyyR+eCBj/CNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/cli-shared-utils": "^5.0.8",
"eslint-webpack-plugin": "^3.1.0",
"globby": "^11.0.2",
"webpack": "^5.54.0",
"yorkie": "^2.0.0"
},
"peerDependencies": {
"@vue/cli-service": "^3.0.0 || ^4.0.0 || ^5.0.0-0",
"eslint": ">=7.5.0"
}
},
"node_modules/@vue/cli-plugin-eslint/node_modules/@types/eslint": {
"version": "8.56.12",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
"integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*",
"@types/json-schema": "*"
}
},
"node_modules/@vue/cli-plugin-eslint/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@vue/cli-plugin-eslint/node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3"
},
"peerDependencies": {
"ajv": "^8.8.2"
}
},
"node_modules/@vue/cli-plugin-eslint/node_modules/eslint-webpack-plugin": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz",
"integrity": "sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint": "^7.29.0 || ^8.4.1",
"jest-worker": "^28.0.2",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
"schema-utils": "^4.0.0"
},
"engines": {
"node": ">= 12.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
"eslint": "^7.0.0 || ^8.0.0",
"webpack": "^5.0.0"
}
},
"node_modules/@vue/cli-plugin-eslint/node_modules/jest-worker": {
"version": "28.1.3",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz",
"integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"merge-stream": "^2.0.0",
"supports-color": "^8.0.0"
},
"engines": {
"node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
}
},
"node_modules/@vue/cli-plugin-eslint/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/@vue/cli-plugin-eslint/node_modules/schema-utils": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz",
"integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.9",
"ajv": "^8.9.0",
"ajv-formats": "^2.1.1",
"ajv-keywords": "^5.1.0"
},
"engines": {
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/@vue/cli-plugin-eslint/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/@vue/cli-plugin-router": { "node_modules/@vue/cli-plugin-router": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-router/-/cli-plugin-router-5.0.8.tgz", "resolved": "https://registry.npmjs.org/@vue/cli-plugin-router/-/cli-plugin-router-5.0.8.tgz",
@@ -4176,9 +4002,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/bootstrap": { "node_modules/bootstrap": {
"version": "5.3.4", "version": "5.3.5",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.4.tgz", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.5.tgz",
"integrity": "sha512-q2oK3ZPDTa5I44FTyY3H76+SDTJREvOBxtX1HNLHcxMni50jMvUtOh+dgFdgpsAHtJ9bfNAWr6d6VezJHJ/7tg==", "integrity": "sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -4385,9 +4211,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001709", "version": "1.0.30001712",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001709.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001712.tgz",
"integrity": "sha512-NgL3vUTnDrPCZ3zTahp4fsugQ4dc7EKTSzwQDPEel6DMoMnfH2jhry9n2Zm8onbSR+f/QtKHFOA+iAQu4kbtWA==", "integrity": "sha512-MBqPpGYYdQ7/hfKiet9SCI+nmN5/hp4ZzveOJubl5DTAMa5oggjAuoi0Z4onBpKPFI2ePGnQuQIzF3VxDjDJig==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -4475,13 +4301,6 @@
"node": ">=6.0" "node": ">=6.0"
} }
}, },
"node_modules/ci-info": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz",
"integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==",
"dev": true,
"license": "MIT"
},
"node_modules/clean-css": { "node_modules/clean-css": {
"version": "5.3.3", "version": "5.3.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
@@ -5775,9 +5594,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.131", "version": "1.5.134",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.131.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.134.tgz",
"integrity": "sha512-fJFRYXVEJgDCiqFOgRGJm8XR97hZ13tw7FXI9k2yC5hgY+nyzC2tMO8baq1cQR7Ur58iCkASx2zrkZPZUnfzPg==", "integrity": "sha512-zSwzrLg3jNP3bwsLqWHmS5z2nIOQ5ngMnfMZOWWtXnqqQkPVyOipxK98w+1beLw1TB+EImPNcG8wVP/cLVs2Og==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
@@ -5955,19 +5774,19 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.23.0", "version": "9.24.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.23.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz",
"integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==", "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.19.2", "@eslint/config-array": "^0.20.0",
"@eslint/config-helpers": "^0.2.0", "@eslint/config-helpers": "^0.2.0",
"@eslint/core": "^0.12.0", "@eslint/core": "^0.12.0",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.23.0", "@eslint/js": "9.24.0",
"@eslint/plugin-kit": "^0.2.7", "@eslint/plugin-kit": "^0.2.7",
"@humanfs/node": "^0.16.6", "@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
@@ -6051,26 +5870,33 @@
} }
}, },
"node_modules/eslint-scope": { "node_modules/eslint-scope": {
"version": "5.1.1", "version": "8.3.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==",
"dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"esrecurse": "^4.3.0", "esrecurse": "^4.3.0",
"estraverse": "^4.1.1" "estraverse": "^5.2.0"
}, },
"engines": { "engines": {
"node": ">=8.0.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/eslint-visitor-keys": { "node_modules/eslint-visitor-keys": {
"version": "2.1.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">=10" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/eslint/node_modules/chalk": { "node_modules/eslint/node_modules/chalk": {
@@ -6090,46 +5916,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/eslint/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/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/eslint/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/espree": { "node_modules/espree": {
"version": "10.3.0", "version": "10.3.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
@@ -6148,19 +5934,6 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/espree/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/esquery": { "node_modules/esquery": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
@@ -6174,16 +5947,6 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/esquery/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/esrecurse": { "node_modules/esrecurse": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
@@ -6196,7 +5959,7 @@
"node": ">=4.0" "node": ">=4.0"
} }
}, },
"node_modules/esrecurse/node_modules/estraverse": { "node_modules/estraverse": {
"version": "5.3.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
@@ -6205,15 +5968,6 @@
"node": ">=4.0" "node": ">=4.0"
} }
}, },
"node_modules/estraverse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
},
"node_modules/estree-walker": { "node_modules/estree-walker": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
@@ -7307,9 +7061,9 @@
} }
}, },
"node_modules/http-parser-js": { "node_modules/http-parser-js": {
"version": "0.5.9", "version": "0.5.10",
"resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz", "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz",
"integrity": "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==", "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -7532,19 +7286,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/is-ci": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz",
"integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"ci-info": "^1.5.0"
},
"bin": {
"is-ci": "bin.js"
}
},
"node_modules/is-core-module": { "node_modules/is-core-module": {
"version": "2.16.1", "version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
@@ -9433,30 +9174,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/pdfjs-dist": {
"version": "2.5.207",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.5.207.tgz",
"integrity": "sha512-xGDUhnCYPfHy+unMXCLCJtlpZaaZ17Ew3WIL0tnSgKFUZXHAPD49GO9xScyszSsQMoutNDgRb+rfBXIaX/lJbw==",
"license": "Apache-2.0"
},
"node_modules/pdfvuer": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pdfvuer/-/pdfvuer-2.0.1.tgz",
"integrity": "sha512-5aEjPoYuaD9uc0Bw2gVadXw9Ez0J1s78xXrryUS1SqKaVVtCcTpyZ1eBllZXz3kD+DUVNhHr9O88ygxfYkLhzQ==",
"license": "MIT",
"dependencies": {
"pdfjs-dist": "2.5.207",
"raw-loader": "^0.5.1",
"vue-resize-sensor": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"pdfjs-dist": "2.5.207",
"vue": "^3.1.0"
}
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -10444,11 +10161,6 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/raw-loader": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz",
"integrity": "sha512-sf7oGoLuaYAScB4VGr0tzetsYlS8EJH6qnTCfQ/WVEa89hALQ4RQfCKt5xCyPQKPDUbVUAIP1QsxAwfAjlDp7Q=="
},
"node_modules/read-pkg": { "node_modules/read-pkg": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
@@ -11626,16 +11338,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/strip-indent": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz",
"integrity": "sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/strip-json-comments": { "node_modules/strip-json-comments": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -12238,9 +11940,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vue-eslint-parser": { "node_modules/vue-eslint-parser": {
"version": "10.1.1", "version": "10.1.3",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.1.1.tgz", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.1.3.tgz",
"integrity": "sha512-bh2Z/Au5slro9QJ3neFYLanZtb1jH+W2bKqGHXAoYD4vZgNG3KeotL7JpPv5xzY4UXUXJl7TrIsnzECH63kd3Q==", "integrity": "sha512-dbCBnd2e02dYWsXoqX5yKUZlOt+ExIpq7hmHKPb5ZqKcjf++Eo0hMseFTZMLKThrUk61m+Uv6A2YSBve6ZvuDQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
@@ -12263,49 +11965,6 @@
"eslint": "^8.57.0 || ^9.0.0" "eslint": "^8.57.0 || ^9.0.0"
} }
}, },
"node_modules/vue-eslint-parser/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",
"peer": true,
"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/vue-eslint-parser/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",
"peer": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/vue-eslint-parser/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",
"peer": true,
"engines": {
"node": ">=4.0"
}
},
"node_modules/vue-eslint-parser/node_modules/semver": { "node_modules/vue-eslint-parser/node_modules/semver": {
"version": "7.7.1", "version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
@@ -12367,12 +12026,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/vue-resize-sensor": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/vue-resize-sensor/-/vue-resize-sensor-2.0.0.tgz",
"integrity": "sha512-W+y2EAI/BxS4Vlcca9scQv8ifeBFck56DRtSwWJ2H4Cw1GLNUYxiZxUHHkuzuI5JPW/cYtL1bPO5xPyEXx4LmQ==",
"license": "MIT"
},
"node_modules/vue-router": { "node_modules/vue-router": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.0.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.0.tgz",
@@ -12414,9 +12067,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vue-toast-notification": { "node_modules/vue-toast-notification": {
"version": "3.0.4", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/vue-toast-notification/-/vue-toast-notification-3.0.4.tgz", "resolved": "https://registry.npmjs.org/vue-toast-notification/-/vue-toast-notification-3.1.3.tgz",
"integrity": "sha512-rEhLtcKg8SVdBpdN7PrNst5nmY8dw0j3NkNImqurhlGurqR/QDKoou0t2PuCReEOCTKqHvfLCle2I3kwQWDWDQ==", "integrity": "sha512-XNyWqwLIGBFfX5G9sK+clq3N3IPlhDjzNdbZaXkEElcotPlWs0wWZailk1vqhdtLYT/93Y4FHAVuzyatLmPZRA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12.15.0" "node": ">=12.15.0"
@@ -12488,9 +12141,9 @@
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/webpack": { "node_modules/webpack": {
"version": "5.98.0", "version": "5.99.3",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.3.tgz",
"integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", "integrity": "sha512-Sb8csGqLL9kY7nqHyJq9Yw1sx+/mpBLXuqM6edfjFOpODiFjzkLUKF08s5WxDxWg9akMklrbTsVsoj7jBULhfw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/eslint-scope": "^3.7.7", "@types/eslint-scope": "^3.7.7",
@@ -12957,6 +12610,28 @@
"ajv": "^8.8.2" "ajv": "^8.8.2"
} }
}, },
"node_modules/webpack/node_modules/eslint-scope": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^4.1.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/webpack/node_modules/estraverse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
},
"node_modules/webpack/node_modules/json-schema-traverse": { "node_modules/webpack/node_modules/json-schema-traverse": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@@ -13200,128 +12875,6 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/yorkie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/yorkie/-/yorkie-2.0.0.tgz",
"integrity": "sha512-jcKpkthap6x63MB4TxwCyuIGkV0oYP/YRyuQU5UO0Yz/E/ZAu+653/uov+phdmO54n6BcvFRyyt0RRrWdN2mpw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"execa": "^0.8.0",
"is-ci": "^1.0.10",
"normalize-path": "^1.0.0",
"strip-indent": "^2.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/yorkie/node_modules/cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
"integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"lru-cache": "^4.0.1",
"shebang-command": "^1.2.0",
"which": "^1.2.9"
}
},
"node_modules/yorkie/node_modules/execa": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz",
"integrity": "sha512-zDWS+Rb1E8BlqqhALSt9kUhss8Qq4nN3iof3gsOdyINksElaPyNBtKUMTR62qhvgVWR0CqCX7sdnKe4MnUbFEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"cross-spawn": "^5.0.1",
"get-stream": "^3.0.0",
"is-stream": "^1.1.0",
"npm-run-path": "^2.0.0",
"p-finally": "^1.0.0",
"signal-exit": "^3.0.0",
"strip-eof": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/yorkie/node_modules/get-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/yorkie/node_modules/lru-cache": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
"integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
"dev": true,
"license": "ISC",
"dependencies": {
"pseudomap": "^1.0.2",
"yallist": "^2.1.2"
}
},
"node_modules/yorkie/node_modules/normalize-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-1.0.0.tgz",
"integrity": "sha512-7WyT0w8jhpDStXRq5836AMmihQwq2nrUVQrgjvUo/p/NZf9uy/MeJ246lBJVmWuYXMlJuG9BNZHF0hWjfTbQUA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/yorkie/node_modules/shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
"integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^1.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/yorkie/node_modules/shebang-regex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
"integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/yorkie/node_modules/which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"which": "bin/which"
}
},
"node_modules/yorkie/node_modules/yallist": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==",
"dev": true,
"license": "ISC"
} }
} }
} }

View File

@@ -15,25 +15,22 @@
"bootstrap": "^5.2.0", "bootstrap": "^5.2.0",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"pdfvuer": "^2.0.1",
"reveal.js": "^5.2.1", "reveal.js": "^5.2.1",
"timeago.js": "^4.0.2", "timeago.js": "^4.0.2",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.0.3", "vue-router": "^4.0.3",
"vue-toast-notification": "3.0", "vue-toast-notification": "^3.0",
"vuejs-paginate-next": "^1.0.2", "vuejs-paginate-next": "^1.0.2",
"vuex": "^4.0.0", "vuex": "^4.0.0",
"webpack": "^5.98.0" "webpack": "^5.98.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.26.10", "@babel/core": "^7.26.10",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "^5.0.8", "@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-plugin-eslint": "^5.0.8",
"@vue/cli-plugin-router": "^5.0.0", "@vue/cli-plugin-router": "^5.0.0",
"@vue/cli-plugin-vuex": "^5.0.0", "@vue/cli-plugin-vuex": "^5.0.0",
"@vue/cli-service": "^5.0.8", "@vue/cli-service": "^5.0.8",
"eslint": "^9.23.0", "eslint": "^9.24.0",
"eslint-plugin-vue": "^10.0.0", "eslint-plugin-vue": "^10.0.0",
"jshint": "^2.13.5", "jshint": "^2.13.5",
"mini-css-extract-plugin": "^2.9.2", "mini-css-extract-plugin": "^2.9.2",

View File

@@ -3,38 +3,77 @@ import router from "@/router";
import store from "@/store"; import store from "@/store";
import { jwtDecode } from "jwt-decode"; import { jwtDecode } from "jwt-decode";
/**
* Gets a valid access token or refreshes if needed
* Uses a consistent 5-minute threshold for token expiration
*/
async function get_access_token() { async function get_access_token() {
let access = jwtDecode(store.state.jwt.access) // If we don't have tokens in the store, return null
let refresh = jwtDecode(store.state.jwt.refresh) if (!store.state.jwt || !store.state.jwt.access) {
if (access.exp - Date.now()/1000 < 5) { return null;
if (refresh.exp - Date.now()/1000 < 5) { }
await router.push({name: 'login'})
return null try {
} else { const access = jwtDecode(store.state.jwt.access);
return store.dispatch('refreshToken').then(() => {return store.state.jwt.access}) const now = Date.now() / 1000;
const refreshThreshold = 300; // 5 minutes in seconds
// If token is about to expire, refresh it
if (access.exp - now < refreshThreshold) {
try {
// Wait for the token to refresh
await store.dispatch('refreshToken');
return store.state.jwt.access;
} catch (error) {
console.error('Failed to refresh token:', error);
return null;
}
} }
return store.state.jwt.access;
} catch (error) {
console.error('Error decoding token:', error);
return null;
} }
return store.state.jwt.access }
}
const axios_jwt = axios.create(); const axios_jwt = axios.create();
axios_jwt.interceptors.request.use(async function (config) { // Add CSRF token to all requests if using cookies for authentication
let access_token = await get_access_token().catch(() => { axios_jwt.interceptors.request.use(function(config) {
if (router.currentRoute.value.fullPath.includes('login')){ // Get CSRF token from cookie if it exists
router.push({name: 'login'}) const csrfToken = document.cookie
}else { .split('; ')
router.push({name: 'login', query: { next: router.currentRoute.value.fullPath }}) .find(row => row.startsWith('csrftoken='))
} ?.split('=')[1];
}) if (csrfToken) {
config.headers = { config.headers['X-CSRFToken'] = csrfToken;
Authorization: "Bearer " + access_token
} }
return config
}, function (error) { return config;
// Do something with request error });
// Add JWT token to all requests
axios_jwt.interceptors.request.use(async function (config) {
const access_token = await get_access_token();
if (access_token) {
config.headers.Authorization = "Bearer " + access_token;
} else if (!router.currentRoute.value.fullPath.includes('login')) {
// Only redirect if we're not already on the login page
router.push({
name: 'login',
query: {
next: router.currentRoute.value.fullPath,
error: 'Please log in to continue'
}
});
}
return config;
}, function (error) {
return Promise.reject(error); return Promise.reject(error);
}); });
export default axios_jwt export default axios_jwt

View File

@@ -6,7 +6,8 @@
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <!-- Show these links only when user is authenticated -->
<ul class="navbar-nav me-auto mb-2 mb-lg-0" v-if="isAuthenticated">
<li class="nav-item"> <li class="nav-item">
<router-link :to="{name: 'browse'}" class="nav-link" >Browse</router-link> <router-link :to="{name: 'browse'}" class="nav-link" >Browse</router-link>
</li> </li>
@@ -26,6 +27,12 @@
<a class="nav-link" @click="logout">Log Out</a> <a class="nav-link" @click="logout">Log Out</a>
</li> </li>
</ul> </ul>
<!-- Show login link when user is not authenticated -->
<ul class="navbar-nav me-auto mb-2 mb-lg-0" v-else>
<li class="nav-item">
<router-link :to="{name: 'login'}" class="nav-link">Log In</router-link>
</li>
</ul>
</div> </div>
</div> </div>
</nav> </nav>
@@ -42,6 +49,11 @@ export default {
visible: false visible: false
} }
}, },
computed: {
isAuthenticated() {
return !!this.$store.state.jwt;
}
},
methods: { methods: {
logout () { logout () {
store.commit('logOut') store.commit('logOut')

View File

@@ -1,170 +0,0 @@
<template>
<div class="container" ref="pdfContainer">
<div class="row w-100 pb-5 mb-5" v-if="loaded">
<pdf :src="pdfdata" :page="page" ref="pdfWindow" :resize="true">
<template v-slot:loading>
loading content here...
</template>
</pdf>
</div>
</div>
<div class="row navButtons pb-2">
<comic-paginate
v-model="page"
:page_count="numPages"
@setPage="setPage"
@prevComic="prevComic"
@nextComic="nextComic"
/>
</div>
</template>
<script>
import pdfvuer from 'pdfvuer'
import api from "@/api";
import * as Hammer from 'hammerjs'
import ComicPaginate from "@/components/ComicPaginate";
export default {
name: "ThePdfReader",
components: {
ComicPaginate,
pdf: pdfvuer
},
data () {
return {
page: 1,
numPages: 0,
pdfdata: undefined,
errors: [],
scale: 'page-width',
loaded: false,
key_timeout: null,
hammertime: null,
next_comic: {},
prev_comic: {}
}
},
props: {
selector: String
},
computed: {
},
mounted () {
this.getPdf()
window.addEventListener('keyup', this.keyPressDebounce)
},
beforeUnmount() {
window.removeEventListener('keyup', this.keyPressDebounce)
},
watch: {
},
methods: {
getPdf () {
let comic_data_url = '/api/read/' + this.selector + '/'
api.get(comic_data_url)
.then(response => {
let parameter = {
url: '/api/read/' + this.selector + '/pdf/',
httpHeaders: { Authorization: 'Bearer ' + this.$store.state.jwt.access },
withCredentials: true,
}
this.pdfdata = pdfvuer.createLoadingTask(parameter);
this.pdfdata.then(pdf => {
this.numPages = pdf.numPages;
this.loaded = true
this.page = response.data.last_read_page+1
this.setReadPage(this.page)
this.next_comic = response.data.next_comic
this.prev_comic = response.data.prev_comic
this.hammertime = new Hammer(this.$refs.pdfContainer, {})
this.hammertime.on('swipeleft', (_e, self=this) => {
self.nextPage()
})
this.hammertime.on('swiperight', (_e, self=this) => {
self.prevPage()
})
this.hammertime.on('tap', (_e, self=this) => {
self.nextPage()
})
}).catch(e => {console.log(e)});
})
},
prevComic(){
this.$router.push({
name: this.prev_comic.route,
params: {selector: this.prev_comic.selector}
})
},
nextComic(){
this.$router.push({
name: this.next_comic.route,
params: {selector: this.next_comic.selector}
})
},
nextPage () {
if (this.page < this.numPages){
this.page += 1
this.setReadPage(this.page)
} else {
this.nextComic()
}
},
prevPage() {
if (this.page > 1){
this.page -= 1
this.setReadPage(this.page)
} else {
this.prevComic()
}
},
setPage(num) {
this.page = num
this.setReadPage(this.page)
},
setReadPage(num){
this.$refs.pdfContainer.scrollIntoView()
let payload = {
page: num-1
}
api.put('/api/read/'+ this.selector +'/set_page/', payload)
},
keyPressDebounce(e){
clearTimeout(this.key_timeout)
this.key_timeout = setTimeout(() => {this.keyPress(e)}, 50)
},
keyPress(e) {
if (e.key === 'ArrowRight') {
this.nextPage()
} else if (e.key === 'ArrowLeft') {
this.prevPage()
} else if (e.key === 'ArrowUp') {
window.scrollTo({
top: window.scrollY-window.innerHeight*.7,
left: 0,
behavior: 'smooth'
});
} else if (e.key === 'ArrowDown') {
window.scrollTo({
top: window.scrollY+window.innerHeight*.7,
left: 0,
behavior: 'smooth'
});
}
}
}
}
</script>
<style scoped>
.navButtons {
position: fixed;
left: 50%;
transform: translateX(-50%);
bottom: 0;
z-index: 1030;
width: auto;
cursor: pointer;
}
</style>

View File

@@ -1,4 +1,5 @@
import { createRouter, createWebHashHistory } from 'vue-router' import { createRouter, createWebHashHistory } from 'vue-router'
import store from '@/store'
const ReadView = () => import('@/views/ReadView') const ReadView = () => import('@/views/ReadView')
const RecentView = () => import('@/views/RecentView') const RecentView = () => import('@/views/RecentView')
@@ -8,6 +9,30 @@ const UserView = () => import('@/views/UserView')
const LoginView = () => import('@/views/LoginView') const LoginView = () => import('@/views/LoginView')
const HistoryView = () => import('@/views/HistoryView') const HistoryView = () => import('@/views/HistoryView')
// Navigation guard to check if user is authenticated
function requireAuth(to, from, next) {
if (!store.state.jwt) {
next({
name: 'login',
query: { next: to.fullPath, error: 'Please log in to access this page' }
});
} else {
next();
}
}
// Navigation guard to check if user is admin
function requireAdmin(to, from, next) {
if (!store.state.jwt || !store.getters.is_superuser) {
next({
name: 'login',
query: { next: to.fullPath, error: 'Admin access required' }
});
} else {
next();
}
}
const routes = [ const routes = [
{ {
path: '/', path: '/',
@@ -20,13 +45,15 @@ const routes = [
path: '/browse/:selector?', path: '/browse/:selector?',
name: 'browse', name: 'browse',
component: BrowseView, component: BrowseView,
props: true props: true,
beforeEnter: requireAuth
}, },
{ {
path: '/read/:selector', path: '/read/:selector',
name: 'read', name: 'read',
component: ReadView, component: ReadView,
props: true props: true,
beforeEnter: requireAuth
}, },
{ {
path: '/login', path: '/login',
@@ -36,23 +63,27 @@ const routes = [
{ {
path: '/recent', path: '/recent',
name: 'recent', name: 'recent',
component: RecentView component: RecentView,
beforeEnter: requireAuth
}, },
{ {
path: '/history', path: '/history',
name: 'history', name: 'history',
component: HistoryView component: HistoryView,
beforeEnter: requireAuth
}, },
{ {
path: '/account', path: '/account',
name: 'account', name: 'account',
component: AccountView component: AccountView,
beforeEnter: requireAuth
}, },
{ {
path: '/user/:userid?', path: '/user/:userid?',
name: 'user', name: 'user',
component: UserView, component: UserView,
props: true props: true,
beforeEnter: requireAdmin
}, },
{ {
path: '/about', path: '/about',

View File

@@ -5,12 +5,11 @@ import {useToast} from "vue-toast-notification";
import router from "@/router"; import router from "@/router";
import api from "@/api"; import api from "@/api";
// We'll no longer use localStorage for tokens
// Instead, tokens will be stored in httpOnly cookies by the backend
// and automatically included in requests
function get_jwt_from_storage(){ function get_jwt_from_storage(){
try { return null; // Initial state will be null until login
return JSON.parse(localStorage.getItem('t'))
} catch {
return null
}
} }
function get_user_from_storage(){ function get_user_from_storage(){
try { try {
@@ -44,12 +43,18 @@ export default createStore({
}, },
mutations: { mutations: {
updateToken(state, newToken){ updateToken(state, newToken){
localStorage.setItem('t', JSON.stringify(newToken)); // No longer storing tokens in localStorage
// Tokens are stored in httpOnly cookies by the backend
state.jwt = newToken; state.jwt = newToken;
}, },
logOut(state){ logOut(state){
localStorage.removeItem('t'); // Clear user data from localStorage
localStorage.removeItem('u') localStorage.removeItem('u')
// Clear state
// Make a request to the backend to invalidate the token
axios.post('/api/token/blacklist/', { refresh: state.jwt?.refresh })
.catch(error => console.error('Error blacklisting token:', error));
state.jwt = null; state.jwt = null;
state.user = null state.user = null
}, },
@@ -92,31 +97,66 @@ export default createStore({
}) })
}, },
refreshToken(){ refreshToken(){
// Don't attempt to refresh if we don't have a token
if (!this.state.jwt || !this.state.jwt.refresh) {
return Promise.reject(new Error('No refresh token available'));
}
const payload = { const payload = {
refresh: this.state.jwt.refresh refresh: this.state.jwt.refresh
} }
return axios.post('/api/token/refresh/', payload) return axios.post('/api/token/refresh/', payload)
.then((response)=>{ .then((response) => {
this.commit('updateToken', response.data) this.commit('updateToken', response.data);
}) return response.data;
.catch((error)=>{ })
console.log(error) .catch((error) => {
// router.push({name: 'login', query: {area: 'store'}}) console.error('Token refresh failed:', error);
}) // If refresh fails, log the user out and redirect to login
this.commit('logOut');
router.push({
name: 'login',
query: {
next: router.currentRoute.value.fullPath,
error: 'Your session has expired. Please log in again.'
}
});
return Promise.reject(error);
});
}, },
inspectToken(){ inspectToken(){
const token = this.state.jwt; const token = this.state.jwt;
if(token){ if (!token) return;
const decoded = jwtDecode(token);
const exp = decoded.exp try {
const orig_iat = decoded.iat // For access token
if(exp - (Date.now()/1000) < 1800 && (Date.now()/1000) - orig_iat < 628200){ const decoded = jwtDecode(token.access);
this.dispatch('refreshToken') const exp = decoded.exp;
} else if (exp -(Date.now()/1000) < 1800){ const now = Date.now() / 1000;
// DO NOTHING, DO NOT REFRESH
} else { // Refresh when token is within 5 minutes of expiring
// PROMPT USER TO RE-LOGIN, THIS ELSE CLAUSE COVERS THE CONDITION WHERE A TOKEN IS EXPIRED AS WELL const refreshThreshold = 300; // 5 minutes in seconds
if (exp - now < refreshThreshold) {
// Token is about to expire, refresh it
this.dispatch('refreshToken');
} else if (exp < now) {
// Token is already expired, force logout
this.commit('logOut');
router.push({
name: 'login',
query: {
next: router.currentRoute.value.fullPath,
error: 'Your session has expired. Please log in again.'
}
});
} }
} catch (error) {
console.error('Error inspecting token:', error);
// If we can't decode the token, log the user out
this.commit('logOut');
router.push({name: 'login'});
} }
} }
}, },

View File

@@ -3,6 +3,11 @@
<div class="row" v-if="!initialSetupRequired"> <div class="row" v-if="!initialSetupRequired">
<div class="col col-lg-4" /> <div class="col col-lg-4" />
<div class="col col-lg-4" id="login-col"> <div class="col col-lg-4" id="login-col">
<!-- Display error message if present -->
<div class="alert alert-danger" v-if="errorMessage">
{{ errorMessage }}
</div>
<form @submit="login" v-on:submit.prevent="onSubmit"> <form @submit="login" v-on:submit.prevent="onSubmit">
<label class="form-label" for="username">Username</label> <label class="form-label" for="username">Username</label>
<input id="username" placeholder="username" aria-describedby="loginFormControlInputHelpInline" class="form-control" type="text" v-model="username" /> <input id="username" placeholder="username" aria-describedby="loginFormControlInputHelpInline" class="form-control" type="text" v-model="username" />
@@ -34,7 +39,8 @@ export default {
username: '', username: '',
password: '', password: '',
password_alert: false, password_alert: false,
initialSetupRequired: false initialSetupRequired: false,
errorMessage: ''
} }
}, },
methods: { methods: {
@@ -43,11 +49,23 @@ export default {
} }
}, },
mounted() { mounted() {
// Check for error message in route query params
if (this.$route.query.error) {
this.errorMessage = this.$route.query.error;
}
// Check if initial setup is required
axios.get('/api/initial_setup/required/').then(response => { axios.get('/api/initial_setup/required/').then(response => {
if (response.data.required){ if (response.data.required){
this.initialSetupRequired = true this.initialSetupRequired = true
} }
}) })
},
// Clear error message when route changes
watch: {
'$route'(to) {
this.errorMessage = to.query.error || '';
}
} }
} }
</script> </script>

View File

@@ -1,17 +1,15 @@
<template> <template>
<the-breadcrumbs :selector="selector" /> <the-breadcrumbs :selector="selector" />
<the-comic-reader :selector="selector" v-if="comic_loaded" :key="selector" /> <the-comic-reader :selector="selector" v-if="comic_loaded" :key="selector" />
<the-pdf-reader :selector="selector" v-if="pdf_loaded" :key="selector" />
</template> </template>
<script> <script>
import TheBreadcrumbs from "@/components/TheBreadcrumbs"; import TheBreadcrumbs from "@/components/TheBreadcrumbs";
import TheComicReader from "@/components/TheComicReader"; import TheComicReader from "@/components/TheComicReader";
import api from "@/api"; import api from "@/api";
import ThePdfReader from "@/components/ThePdfReader";
export default { export default {
name: "ReadView", name: "ReadView",
components: {ThePdfReader, TheComicReader, TheBreadcrumbs}, components: {TheComicReader, TheBreadcrumbs},
props: { props: {
selector: String selector: String
}, },
@@ -19,7 +17,6 @@ export default {
return { return {
comic_data: {}, comic_data: {},
comic_loaded: false, comic_loaded: false,
pdf_loaded: false
} }
}, },
methods: { methods: {
@@ -27,13 +24,7 @@ export default {
let comic_data_url = '/api/read/' + this.selector + '/type/' let comic_data_url = '/api/read/' + this.selector + '/type/'
api.get(comic_data_url) api.get(comic_data_url)
.then(response => { .then(response => {
if (response.data.type === 'pdf'){
this.pdf_loaded = true
this.comic_loaded = false
} else {
this.comic_loaded = true this.comic_loaded = true
this.pdf_loaded = false
}
}) })
.catch((error) => {console.log(error)}) .catch((error) => {console.log(error)})
} }

2
uv.lock generated
View File

@@ -65,7 +65,7 @@ wheels = [
[[package]] [[package]]
name = "cbwebreader" name = "cbwebreader"
version = "1.1.1" version = "1.1.3"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "dj-database-url" }, { name = "dj-database-url" },