Adds image library for admins (#3066)
This commit is contained in:
parent
35f7821183
commit
63adcb6780
7 changed files with 233 additions and 3 deletions
|
|
@ -167,6 +167,10 @@ def create_app() -> FastAPI:
|
|||
static = StaticFiles(directory=static_path)
|
||||
app.mount("/static", static, name="static")
|
||||
|
||||
images_path = os.path.abspath(os.path.join(settings.lnbits_data_folder, "images"))
|
||||
os.makedirs(images_path, exist_ok=True)
|
||||
app.mount("/library", StaticFiles(directory=images_path), name="library")
|
||||
|
||||
g().base_url = f"http://{settings.host}:{settings.port}"
|
||||
|
||||
app.add_middleware(
|
||||
|
|
|
|||
60
lnbits/core/templates/admin/_tab_library.html
Normal file
60
lnbits/core/templates/admin/_tab_library.html
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<q-tab-panel name="library">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h6 class="q-my-none q-mb-sm">
|
||||
<span v-text="$t('image_library')"></span>
|
||||
</h6>
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Add Image"
|
||||
@click="$refs.imageInput.click()"
|
||||
class="q-mb-md"
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
ref="imageInput"
|
||||
accept="image/png, image/jpeg, image/gif"
|
||||
style="display: none"
|
||||
@change="onImageInput"
|
||||
/>
|
||||
</q-card-section>
|
||||
<div class="row q-col-gutter-sm q-pa-sm">
|
||||
<div
|
||||
v-for="image in library_images"
|
||||
:key="image.filename"
|
||||
class="col-6 col-sm-4 col-md-3 col-lg-2"
|
||||
style="max-width: 200px"
|
||||
>
|
||||
<q-card class="q-mb-sm">
|
||||
<q-img :src="image.url" style="height: 150px" />
|
||||
|
||||
<q-card-section
|
||||
class="q-pt-md q-pb-md row items-center justify-between"
|
||||
>
|
||||
<small
|
||||
><div class="text-caption ellipsis" v-text="image.filename"></div
|
||||
></small>
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
size="sm"
|
||||
icon="content_copy"
|
||||
@click="copyText(image.url)"
|
||||
:title="$t('copy')"
|
||||
><q-tooltip>Copy image link</q-tooltip></q-btn
|
||||
>
|
||||
<q-btn
|
||||
color="negative"
|
||||
icon="delete"
|
||||
flat
|
||||
size="sm"
|
||||
@click="deleteImage(image.filename)"
|
||||
><q-tooltip>Delete image</q-tooltip></q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="library_images.length === 0" class="q-pa-xl">
|
||||
<div class="text-subtitle2 text-grey">No images uploaded yet.</div>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
|
|
@ -149,6 +149,14 @@ import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
|
|||
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||
><span v-text="$t('audit')"></span></q-tooltip
|
||||
></q-tab>
|
||||
<q-tab
|
||||
name="library"
|
||||
icon="image"
|
||||
:label="$q.screen.gt.sm ? $t('library') : null"
|
||||
@update="val => tab = val.name"
|
||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||
><span v-text="$t('library')"></span></q-tooltip
|
||||
></q-tab>
|
||||
<q-tab
|
||||
style="word-break: break-all"
|
||||
name="site_customisation"
|
||||
|
|
@ -177,7 +185,8 @@ import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
|
|||
"admin/_tab_extensions.html" %} {% include
|
||||
"admin/_tab_notifications.html" %} {% include
|
||||
"admin/_tab_security.html" %} {% include "admin/_tab_theme.html"
|
||||
%}{% include "admin/_tab_audit.html"%}
|
||||
%}{% include "admin/_tab_audit.html"%}{% include
|
||||
"admin/_tab_library.html"%}
|
||||
</q-tab-panels>
|
||||
</q-form>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
import glob
|
||||
import imghdr
|
||||
import os
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
from io import BytesIO
|
||||
from shutil import make_archive
|
||||
from subprocess import Popen
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
import shortuuid
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Path, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from lnbits.core.models import User
|
||||
|
|
@ -27,6 +31,7 @@ from .. import core_app_extra
|
|||
from ..crud import delete_admin_settings, get_admin_settings, update_admin_settings
|
||||
|
||||
admin_router = APIRouter(tags=["Admin UI"], prefix="/admin")
|
||||
file_upload = File(...)
|
||||
|
||||
|
||||
@admin_router.get(
|
||||
|
|
@ -159,3 +164,91 @@ async def api_download_backup() -> FileResponse:
|
|||
return FileResponse(
|
||||
path=f"{last_filename}.zip", filename=filename, media_type="application/zip"
|
||||
)
|
||||
|
||||
|
||||
@admin_router.post(
|
||||
"/api/v1/images",
|
||||
status_code=HTTPStatus.OK,
|
||||
dependencies=[Depends(check_admin)],
|
||||
)
|
||||
async def upload_image(file: UploadFile = file_upload):
|
||||
if not file or not file.filename:
|
||||
raise HTTPException(status_code=400, detail="No file provided")
|
||||
|
||||
ext = file.filename.split(".")[-1].lower()
|
||||
if ext not in {"png", "jpg", "jpeg", "gif"}:
|
||||
raise HTTPException(status_code=400, detail="Unsupported file type")
|
||||
|
||||
contents = BytesIO()
|
||||
total_size = 0
|
||||
max_size = 500000
|
||||
while chunk := await file.read(1024 * 1024):
|
||||
total_size += len(chunk)
|
||||
if total_size > max_size:
|
||||
raise HTTPException(
|
||||
status_code=413, detail=f"File too large ({max_size / 1000} KB max)"
|
||||
)
|
||||
contents.write(chunk)
|
||||
|
||||
contents.seek(0)
|
||||
|
||||
kind = imghdr.what(None, h=contents.read(512))
|
||||
if kind not in {"png", "jpeg", "gif"}:
|
||||
raise HTTPException(status_code=400, detail="Invalid image file")
|
||||
contents.seek(0)
|
||||
|
||||
filename = f"{shortuuid.uuid()[:5]}.{ext}"
|
||||
image_folder = os.path.join(settings.lnbits_data_folder, "images")
|
||||
file_path = os.path.join(image_folder, filename)
|
||||
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(contents.read())
|
||||
|
||||
return {"filename": filename, "url": f"{settings.lnbits_baseurl}library/{filename}"}
|
||||
|
||||
|
||||
@admin_router.get(
|
||||
"/api/v1/images",
|
||||
status_code=HTTPStatus.OK,
|
||||
dependencies=[Depends(check_admin)],
|
||||
)
|
||||
async def list_uploaded_images():
|
||||
image_folder = os.path.join(settings.lnbits_data_folder, "images")
|
||||
if not os.path.exists(image_folder):
|
||||
return []
|
||||
|
||||
files = glob.glob(os.path.join(image_folder, "*"))
|
||||
images = []
|
||||
|
||||
for file_path in files:
|
||||
if os.path.isfile(file_path):
|
||||
filename = os.path.basename(file_path)
|
||||
images.append(
|
||||
{
|
||||
"filename": filename,
|
||||
"url": f"{settings.lnbits_baseurl}library/{filename}",
|
||||
}
|
||||
)
|
||||
return images
|
||||
|
||||
|
||||
@admin_router.delete(
|
||||
"/api/v1/images/{filename}",
|
||||
status_code=HTTPStatus.OK,
|
||||
dependencies=[Depends(check_admin)],
|
||||
)
|
||||
async def delete_uploaded_image(
|
||||
filename: str = Path(..., description="Name of the image file to delete")
|
||||
):
|
||||
image_folder = os.path.join(settings.lnbits_data_folder, "images")
|
||||
file_path = os.path.join(image_folder, filename)
|
||||
|
||||
# Prevent dir traversal attack
|
||||
if not os.path.abspath(file_path).startswith(os.path.abspath(image_folder)):
|
||||
raise HTTPException(status_code=400, detail="Invalid filename")
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
raise HTTPException(status_code=404, detail="Image not found")
|
||||
|
||||
os.remove(file_path)
|
||||
return {"status": "success", "message": f"{filename} deleted"}
|
||||
|
|
|
|||
2
lnbits/static/bundle.min.js
vendored
2
lnbits/static/bundle.min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -27,6 +27,7 @@ window.localisation.en = {
|
|||
close_channel: 'Close Channel',
|
||||
close: 'Close',
|
||||
restart: 'Restart server',
|
||||
image_library: 'Image Library',
|
||||
save: 'Save',
|
||||
save_tooltip: 'Save your changes',
|
||||
credit_debit: 'Credit / Debit',
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ window.AdminPageLogic = {
|
|||
return {
|
||||
settings: {},
|
||||
logs: [],
|
||||
library_images: [],
|
||||
serverlogEnabled: false,
|
||||
lnbits_theme_options: [
|
||||
'classic',
|
||||
|
|
@ -140,6 +141,7 @@ window.AdminPageLogic = {
|
|||
async created() {
|
||||
await this.getSettings()
|
||||
await this.getAudit()
|
||||
await this.getUploadedImages()
|
||||
this.balance = +'{{ balance|safe }}'
|
||||
const hash = window.location.hash.replace('#', '')
|
||||
if (hash === 'exchange_providers') {
|
||||
|
|
@ -552,6 +554,67 @@ window.AdminPageLogic = {
|
|||
.catch(LNbits.utils.notifyApiError)
|
||||
})
|
||||
},
|
||||
onImageInput(e) {
|
||||
const file = e.target.files[0]
|
||||
if (file) {
|
||||
this.uploadImage(file)
|
||||
}
|
||||
},
|
||||
async uploadImage(file) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
try {
|
||||
const response = await LNbits.api.request(
|
||||
'POST',
|
||||
'/admin/api/v1/images',
|
||||
this.g.user.wallets[0].adminkey,
|
||||
formData,
|
||||
{headers: {'Content-Type': 'multipart/form-data'}}
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Image uploaded!',
|
||||
icon: null
|
||||
})
|
||||
await this.getUploadedImages()
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
async getUploadedImages() {
|
||||
try {
|
||||
const response = await LNbits.api.request(
|
||||
'GET',
|
||||
'/admin/api/v1/images',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
this.library_images = response.data
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
async deleteImage(filename) {
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this image?')
|
||||
.onOk(async () => {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'DELETE',
|
||||
`/admin/api/v1/images/${filename}`,
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Image deleted!',
|
||||
icon: null
|
||||
})
|
||||
await this.getUploadedImages()
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
})
|
||||
},
|
||||
downloadBackup() {
|
||||
window.open('/admin/api/v1/backup', '_blank')
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue