mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-26 15:42:30 +01:00
Merge branch 'nostr' into diagon-alley
This commit is contained in:
commit
62f5376c11
15 changed files with 814 additions and 15 deletions
|
@ -5,11 +5,15 @@ QUART_DEBUG=true
|
||||||
HOST=127.0.0.1
|
HOST=127.0.0.1
|
||||||
PORT=5000
|
PORT=5000
|
||||||
|
|
||||||
|
|
||||||
LNBITS_ALLOWED_USERS=""
|
LNBITS_ALLOWED_USERS=""
|
||||||
LNBITS_ADMIN_USERS=""
|
LNBITS_ADMIN_USERS=""
|
||||||
|
# Extensions only admin can access
|
||||||
|
LNBITS_ADMIN_EXTENSIONS="nostradmin"
|
||||||
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
||||||
|
|
||||||
|
# Disable extensions for all users, use "all" to disable all extensions
|
||||||
|
LNBITS_DISABLED_EXTENSIONS="amilk"
|
||||||
|
|
||||||
# Database: to use SQLite, specify LNBITS_DATA_FOLDER
|
# Database: to use SQLite, specify LNBITS_DATA_FOLDER
|
||||||
# to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://...
|
# to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://...
|
||||||
# to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://...
|
# to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://...
|
||||||
|
@ -18,8 +22,6 @@ LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
||||||
LNBITS_DATA_FOLDER="./data"
|
LNBITS_DATA_FOLDER="./data"
|
||||||
# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename"
|
# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename"
|
||||||
|
|
||||||
# disable selected extensions, or use "all" to disable all extensions
|
|
||||||
LNBITS_DISABLED_EXTENSIONS="amilk,ngrok"
|
|
||||||
LNBITS_FORCE_HTTPS=true
|
LNBITS_FORCE_HTTPS=true
|
||||||
LNBITS_SERVICE_FEE="0.0"
|
LNBITS_SERVICE_FEE="0.0"
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ from urllib.parse import urlparse
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
from lnbits.db import Connection, POSTGRES, COCKROACH
|
from lnbits.db import Connection, POSTGRES, COCKROACH
|
||||||
from lnbits.settings import DEFAULT_WALLET_NAME
|
from lnbits.settings import DEFAULT_WALLET_NAME, LNBITS_ADMIN_USERS
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
from .models import User, Wallet, Payment, BalanceCheck
|
from .models import User, Wallet, Payment, BalanceCheck
|
||||||
|
@ -61,6 +61,7 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
|
||||||
email=user["email"],
|
email=user["email"],
|
||||||
extensions=[e[0] for e in extensions],
|
extensions=[e[0] for e in extensions],
|
||||||
wallets=[Wallet(**w) for w in wallets],
|
wallets=[Wallet(**w) for w in wallets],
|
||||||
|
admin=LNBITS_ADMIN_USERS and user["id"] in [x.strip() for x in LNBITS_ADMIN_USERS]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ from starlette.requests import Request
|
||||||
from lnbits.core.crud import get_user, get_wallet_for_key
|
from lnbits.core.crud import get_user, get_wallet_for_key
|
||||||
from lnbits.core.models import User, Wallet
|
from lnbits.core.models import User, Wallet
|
||||||
from lnbits.requestvars import g
|
from lnbits.requestvars import g
|
||||||
from lnbits.settings import LNBITS_ALLOWED_USERS, LNBITS_ADMIN_USERS
|
from lnbits.settings import LNBITS_ALLOWED_USERS, LNBITS_ADMIN_USERS, LNBITS_ADMIN_EXTENSIONS
|
||||||
|
|
||||||
|
|
||||||
class KeyChecker(SecurityBase):
|
class KeyChecker(SecurityBase):
|
||||||
|
@ -122,6 +122,7 @@ async def get_key_type(
|
||||||
# 0: admin
|
# 0: admin
|
||||||
# 1: invoice
|
# 1: invoice
|
||||||
# 2: invalid
|
# 2: invalid
|
||||||
|
pathname = r['path'].split('/')[1]
|
||||||
|
|
||||||
if not api_key_header and not api_key_query:
|
if not api_key_header and not api_key_query:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
|
||||||
|
@ -131,7 +132,10 @@ async def get_key_type(
|
||||||
try:
|
try:
|
||||||
checker = WalletAdminKeyChecker(api_key=token)
|
checker = WalletAdminKeyChecker(api_key=token)
|
||||||
await checker.__call__(r)
|
await checker.__call__(r)
|
||||||
return WalletTypeInfo(0, checker.wallet)
|
wallet = WalletTypeInfo(0, checker.wallet)
|
||||||
|
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS):
|
||||||
|
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized.")
|
||||||
|
return wallet
|
||||||
except HTTPException as e:
|
except HTTPException as e:
|
||||||
if e.status_code == HTTPStatus.BAD_REQUEST:
|
if e.status_code == HTTPStatus.BAD_REQUEST:
|
||||||
raise
|
raise
|
||||||
|
@ -143,7 +147,10 @@ async def get_key_type(
|
||||||
try:
|
try:
|
||||||
checker = WalletInvoiceKeyChecker(api_key=token)
|
checker = WalletInvoiceKeyChecker(api_key=token)
|
||||||
await checker.__call__(r)
|
await checker.__call__(r)
|
||||||
return WalletTypeInfo(1, checker.wallet)
|
wallet = WalletTypeInfo(0, checker.wallet)
|
||||||
|
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS):
|
||||||
|
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized.")
|
||||||
|
return wallet
|
||||||
except HTTPException as e:
|
except HTTPException as e:
|
||||||
if e.status_code == HTTPStatus.BAD_REQUEST:
|
if e.status_code == HTTPStatus.BAD_REQUEST:
|
||||||
raise
|
raise
|
||||||
|
|
3
lnbits/extensions/nostradmin/README.md
Normal file
3
lnbits/extensions/nostradmin/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Nostr
|
||||||
|
|
||||||
|
Opens a Nostr daemon
|
16
lnbits/extensions/nostradmin/__init__.py
Normal file
16
lnbits/extensions/nostradmin/__init__.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from lnbits.db import Database
|
||||||
|
from lnbits.helpers import template_renderer
|
||||||
|
|
||||||
|
db = Database("ext_nostradmin")
|
||||||
|
|
||||||
|
nostradmin_ext: APIRouter = APIRouter(prefix="/nostradmin", tags=["nostradmin"])
|
||||||
|
|
||||||
|
|
||||||
|
def nostr_renderer():
|
||||||
|
return template_renderer(["lnbits/extensions/nostradmin/templates"])
|
||||||
|
|
||||||
|
|
||||||
|
from .views import * # noqa
|
||||||
|
from .views_api import * # noqa
|
6
lnbits/extensions/nostradmin/config.json
Normal file
6
lnbits/extensions/nostradmin/config.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "NostrAdmin",
|
||||||
|
"short_description": "Admin daemon for Nostr",
|
||||||
|
"icon": "swap_horizontal_circle",
|
||||||
|
"contributors": ["arcbtc"]
|
||||||
|
}
|
146
lnbits/extensions/nostradmin/crud.py
Normal file
146
lnbits/extensions/nostradmin/crud.py
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
import shortuuid
|
||||||
|
from . import db
|
||||||
|
from .models import (
|
||||||
|
nostrKeys,
|
||||||
|
nostrNotes,
|
||||||
|
nostrCreateRelays,
|
||||||
|
nostrRelays,
|
||||||
|
nostrConnections,
|
||||||
|
nostrCreateConnections,
|
||||||
|
nostrRelayList,
|
||||||
|
)
|
||||||
|
from .models import nostrKeys, nostrCreateRelays, nostrRelaySetList
|
||||||
|
|
||||||
|
###############KEYS##################
|
||||||
|
|
||||||
|
|
||||||
|
async def create_nostrkeys(data: nostrKeys) -> nostrKeys:
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO nostradmin.keys (
|
||||||
|
pubkey,
|
||||||
|
privkey
|
||||||
|
)
|
||||||
|
VALUES (?, ?)
|
||||||
|
""",
|
||||||
|
(data.pubkey, data.privkey),
|
||||||
|
)
|
||||||
|
return await get_nostrkeys(nostrkey_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_nostrkeys(pubkey: str) -> nostrKeys:
|
||||||
|
row = await db.fetchone("SELECT * FROM nostradmin.keys WHERE pubkey = ?", (pubkey,))
|
||||||
|
return nostrKeys(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
###############NOTES##################
|
||||||
|
|
||||||
|
|
||||||
|
async def create_nostrnotes(data: nostrNotes) -> nostrNotes:
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO nostradmin.notes (
|
||||||
|
id,
|
||||||
|
pubkey,
|
||||||
|
created_at,
|
||||||
|
kind,
|
||||||
|
tags,
|
||||||
|
content,
|
||||||
|
sig
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
data.id,
|
||||||
|
data.pubkey,
|
||||||
|
data.created_at,
|
||||||
|
data.kind,
|
||||||
|
data.tags,
|
||||||
|
data.content,
|
||||||
|
data.sig,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return await get_nostrnotes(data.id)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_nostrnotes(nostrnote_id: str) -> nostrNotes:
|
||||||
|
row = await db.fetchone("SELECT * FROM nostradmin.notes WHERE id = ?", (nostrnote_id,))
|
||||||
|
return nostrNotes(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
###############RELAYS##################
|
||||||
|
|
||||||
|
|
||||||
|
async def create_nostrrelays(data: nostrCreateRelays) -> nostrCreateRelays:
|
||||||
|
nostrrelay_id = shortuuid.uuid(name=data.relay)
|
||||||
|
|
||||||
|
if await get_nostrrelay(nostrrelay_id):
|
||||||
|
return "error"
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO nostradmin.relays (
|
||||||
|
id,
|
||||||
|
relay
|
||||||
|
)
|
||||||
|
VALUES (?, ?)
|
||||||
|
""",
|
||||||
|
(nostrrelay_id, data.relay),
|
||||||
|
)
|
||||||
|
return await get_nostrrelay(nostrrelay_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_nostrrelays() -> nostrRelays:
|
||||||
|
rows = await db.fetchall("SELECT * FROM nostradmin.relays")
|
||||||
|
return [nostrRelays(**row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_nostrrelay(nostrrelay_id: str) -> nostrRelays:
|
||||||
|
row = await db.fetchone("SELECT * FROM nostradmin.relays WHERE id = ?", (nostrrelay_id,))
|
||||||
|
return nostrRelays(**row) if row else None
|
||||||
|
|
||||||
|
async def update_nostrrelaysetlist(data: nostrRelaySetList) -> nostrRelayList:
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
UPDATE nostradmin.relaylists SET
|
||||||
|
denylist = ?,
|
||||||
|
allowlist = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(data.denylist, data.allowlist, 1),
|
||||||
|
)
|
||||||
|
return await get_nostrrelaylist()
|
||||||
|
|
||||||
|
async def get_nostrrelaylist() -> nostrRelayList:
|
||||||
|
row = await db.fetchone("SELECT * FROM nostradmin.relaylists WHERE id = ?", (1,))
|
||||||
|
return nostrRelayList(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
###############CONNECTIONS##################
|
||||||
|
|
||||||
|
|
||||||
|
async def create_nostrconnections(
|
||||||
|
data: nostrCreateConnections
|
||||||
|
) -> nostrCreateConnections:
|
||||||
|
nostrrelay_id = shortuuid.uuid(name=data.relayid + data.pubkey)
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO nostradmin.connections (
|
||||||
|
id,
|
||||||
|
pubkey,
|
||||||
|
relayid
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
""",
|
||||||
|
(data.id, data.pubkey, data.relayid),
|
||||||
|
)
|
||||||
|
return await get_nostrconnections(data.id)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_nostrconnections(nostrconnections_id: str) -> nostrConnections:
|
||||||
|
row = await db.fetchone(
|
||||||
|
"SELECT * FROM nostradmin.connections WHERE id = ?", (nostrconnections_id,)
|
||||||
|
)
|
||||||
|
return nostrConnections(**row) if row else None
|
74
lnbits/extensions/nostradmin/migrations.py
Normal file
74
lnbits/extensions/nostradmin/migrations.py
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
from lnbits.db import Database
|
||||||
|
|
||||||
|
async def m001_initial(db):
|
||||||
|
"""
|
||||||
|
Initial nostradmin table.
|
||||||
|
"""
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
CREATE TABLE nostradmin.keys (
|
||||||
|
pubkey TEXT NOT NULL PRIMARY KEY,
|
||||||
|
privkey TEXT NOT NULL
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
CREATE TABLE nostradmin.notes (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
pubkey TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
kind INT NOT NULL,
|
||||||
|
tags TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
sig TEXT NOT NULL
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
CREATE TABLE nostradmin.relays (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
relay TEXT NOT NULL
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
CREATE TABLE nostradmin.relaylists (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY DEFAULT 1,
|
||||||
|
allowlist TEXT,
|
||||||
|
denylist TEXT
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO nostradmin.relaylists (
|
||||||
|
id,
|
||||||
|
denylist
|
||||||
|
)
|
||||||
|
VALUES (?, ?)
|
||||||
|
""",
|
||||||
|
("1", "wss://zucks-meta-relay.com\nwss://nostr.cia.gov",),
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
CREATE TABLE nostradmin.connections (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
publickey TEXT NOT NULL,
|
||||||
|
relayid TEXT NOT NULL
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
CREATE TABLE nostradmin.subscribed (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
userPubkey TEXT NOT NULL,
|
||||||
|
subscribedPubkey TEXT NOT NULL
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
52
lnbits/extensions/nostradmin/models.py
Normal file
52
lnbits/extensions/nostradmin/models.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import json
|
||||||
|
from sqlite3 import Row
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import Request
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from pydantic.main import BaseModel
|
||||||
|
from fastapi.param_functions import Query
|
||||||
|
|
||||||
|
class nostrKeys(BaseModel):
|
||||||
|
pubkey: str
|
||||||
|
privkey: str
|
||||||
|
|
||||||
|
class nostrNotes(BaseModel):
|
||||||
|
id: str
|
||||||
|
pubkey: str
|
||||||
|
created_at: str
|
||||||
|
kind: int
|
||||||
|
tags: str
|
||||||
|
content: str
|
||||||
|
sig: str
|
||||||
|
|
||||||
|
class nostrCreateRelays(BaseModel):
|
||||||
|
relay: str = Query(None)
|
||||||
|
|
||||||
|
class nostrCreateConnections(BaseModel):
|
||||||
|
pubkey: str = Query(None)
|
||||||
|
relayid: str = Query(None)
|
||||||
|
|
||||||
|
class nostrRelays(BaseModel):
|
||||||
|
id: Optional[str]
|
||||||
|
relay: Optional[str]
|
||||||
|
status: Optional[bool] = False
|
||||||
|
|
||||||
|
class nostrRelayList(BaseModel):
|
||||||
|
id: str
|
||||||
|
allowlist: Optional[str]
|
||||||
|
denylist: Optional[str]
|
||||||
|
|
||||||
|
class nostrRelaySetList(BaseModel):
|
||||||
|
allowlist: Optional[str]
|
||||||
|
denylist: Optional[str]
|
||||||
|
|
||||||
|
class nostrConnections(BaseModel):
|
||||||
|
id: str
|
||||||
|
pubkey: Optional[str]
|
||||||
|
relayid: Optional[str]
|
||||||
|
|
||||||
|
class nostrSubscriptions(BaseModel):
|
||||||
|
id: str
|
||||||
|
userPubkey: Optional[str]
|
||||||
|
subscribedPubkey: Optional[str]
|
303
lnbits/extensions/nostradmin/templates/nostradmin/index.html
Normal file
303
lnbits/extensions/nostradmin/templates/nostradmin/index.html
Normal file
|
@ -0,0 +1,303 @@
|
||||||
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
|
%} {% block page %}
|
||||||
|
<div class="row q-col-gutter-md">
|
||||||
|
<div class="col-12 col-md-7 q-gutter-y-md">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col">
|
||||||
|
<h5 class="text-subtitle1 q-my-none">NOSTR RELAYS ONLINE</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto">
|
||||||
|
<q-input
|
||||||
|
borderless
|
||||||
|
dense
|
||||||
|
debounce="300"
|
||||||
|
v-model="filter"
|
||||||
|
placeholder="Search"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon name="search"></q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
<q-btn flat color="grey" @click="exportlnurldeviceCSV"
|
||||||
|
>Export to CSV</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-table
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
:data="nostrrelayLinks"
|
||||||
|
row-key="id"
|
||||||
|
:columns="nostrTable.columns"
|
||||||
|
:pagination.sync="nostrTable.pagination"
|
||||||
|
:filter="filter"
|
||||||
|
>
|
||||||
|
{% raw %}
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
auto-width
|
||||||
|
>
|
||||||
|
<div v-if="col.name == 'id'"></div>
|
||||||
|
<div v-else>{{ col.label }}</div>
|
||||||
|
</q-th>
|
||||||
|
<!-- <q-th auto-width></q-th> -->
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
auto-width
|
||||||
|
>
|
||||||
|
<div v-if="col.name == 'id'"></div>
|
||||||
|
<div v-else>
|
||||||
|
<div v-if="col.value == true" style="background-color: green">
|
||||||
|
{{ col.value }}
|
||||||
|
</div>
|
||||||
|
<div v-else>{{ col.value }}</div>
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
{% endraw %}
|
||||||
|
</q-table>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
<q-card>
|
||||||
|
<q-tabs
|
||||||
|
v-model="listSelection"
|
||||||
|
dense
|
||||||
|
class="text-grey"
|
||||||
|
active-color="primary"
|
||||||
|
indicator-color="primary"
|
||||||
|
align="justify"
|
||||||
|
narrow-indicator
|
||||||
|
>
|
||||||
|
<q-tab name="denylist" label="Deny List"></q-tab>
|
||||||
|
<q-tab name="allowlist" label="Allow List"></q-tab>
|
||||||
|
</q-tabs>
|
||||||
|
|
||||||
|
<q-separator></q-separator>
|
||||||
|
|
||||||
|
<q-tab-panels v-model="listSelection" animated>
|
||||||
|
<q-tab-panel name="denylist">
|
||||||
|
<div class="text-h6">Relays in this list will NOT be used</div>
|
||||||
|
<q-form class="q-gutter-md q-y-md" @submit="setRelayList">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col q-mx-lg">
|
||||||
|
<q-input
|
||||||
|
v-model="setList.denylist"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
autogrow
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col q-mx-lg items-align flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<q-btn unelevated color="primary" type="submit">
|
||||||
|
Update Deny List
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<q-tab-panel name="allowlist">
|
||||||
|
<div class="text-h6">ONLY relays in this list will be used</div>
|
||||||
|
<q-form class="q-gutter-md q-y-md" @submit="setRelayList">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col q-mx-lg">
|
||||||
|
<q-input
|
||||||
|
v-model="setList.allowlist"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
autogrow
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col q-mx-lg items-align flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<q-btn unelevated color="primary" type="submit">
|
||||||
|
Update Allow List
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-tab-panel>
|
||||||
|
</q-tab-panels>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} Nostr Extension</h6>
|
||||||
|
<p>Only Admin users can manage this extension</p>
|
||||||
|
|
||||||
|
<q-card-section></q-card-section>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
Vue.component(VueQrcode.name, VueQrcode)
|
||||||
|
|
||||||
|
var maplrelays = obj => {
|
||||||
|
obj._data = _.clone(obj)
|
||||||
|
obj.theTime = obj.time * 60 - (Date.now() / 1000 - obj.timestamp)
|
||||||
|
obj.time = obj.time + 'mins'
|
||||||
|
|
||||||
|
if (obj.time_elapsed) {
|
||||||
|
obj.date = 'Time elapsed'
|
||||||
|
} else {
|
||||||
|
obj.date = Quasar.utils.date.formatDate(
|
||||||
|
new Date((obj.theTime - 3600) * 1000),
|
||||||
|
'HH:mm:ss'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [windowMixin],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
listSelection: 'denylist',
|
||||||
|
setList: {
|
||||||
|
allowlist: '',
|
||||||
|
denylist: ''
|
||||||
|
},
|
||||||
|
nostrrelayLinks: [],
|
||||||
|
filter: '',
|
||||||
|
nostrTable: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'wah',
|
||||||
|
align: 'left',
|
||||||
|
label: 'id',
|
||||||
|
field: 'id'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'relay',
|
||||||
|
align: 'left',
|
||||||
|
label: 'relay',
|
||||||
|
field: 'relay'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
align: 'left',
|
||||||
|
label: 'status',
|
||||||
|
field: 'status'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getRelays: function () {
|
||||||
|
var self = this
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
'/nostradmin/api/v1/relays',
|
||||||
|
self.g.user.wallets[0].adminkey
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
if (response.data) {
|
||||||
|
console.log(response.data)
|
||||||
|
response.data.map(maplrelays)
|
||||||
|
self.nostrrelayLinks = response.data
|
||||||
|
console.log(self.nostrrelayLinks)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setAllowList: function () {
|
||||||
|
var self = this
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'POST',
|
||||||
|
'/nostradmin/api/v1/allowlist',
|
||||||
|
self.g.user.wallets[0].adminkey,
|
||||||
|
self.allowList
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
if (response.data) {
|
||||||
|
self.allowList = response.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setRelayList: function () {
|
||||||
|
var self = this
|
||||||
|
console.log(self.setList)
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'POST',
|
||||||
|
'/nostradmin/api/v1/setlist',
|
||||||
|
self.g.user.wallets[0].adminkey,
|
||||||
|
self.setList
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
if (response.data) {
|
||||||
|
console.log(response.data)
|
||||||
|
// self.denyList = response.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getLists: function () {
|
||||||
|
var self = this
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
'/nostradmin/api/v1/relaylist',
|
||||||
|
self.g.user.wallets[0].adminkey
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
if (response.data) {
|
||||||
|
console.log(response.data)
|
||||||
|
self.setList.denylist = response.data.denylist
|
||||||
|
self.setList.allowlist = response.data.allowlist
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
exportlnurldeviceCSV: function () {
|
||||||
|
var self = this
|
||||||
|
LNbits.utils.exportCSV(self.nostrTable.columns, this.nostrLinks)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
var self = this
|
||||||
|
this.getRelays()
|
||||||
|
this.getLists()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
102
lnbits/extensions/nostradmin/views.py
Normal file
102
lnbits/extensions/nostradmin/views.py
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
import asyncio
|
||||||
|
from fastapi import Request
|
||||||
|
from fastapi.param_functions import Query
|
||||||
|
from fastapi.params import Depends
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
from starlette.responses import HTMLResponse
|
||||||
|
from . import nostradmin_ext, nostr_renderer
|
||||||
|
# FastAPI good for incoming
|
||||||
|
from fastapi import Request, WebSocket, WebSocketDisconnect
|
||||||
|
# Websockets needed for outgoing
|
||||||
|
import websockets
|
||||||
|
|
||||||
|
from lnbits.core.crud import update_payment_status
|
||||||
|
from lnbits.core.models import User
|
||||||
|
from lnbits.core.views.api import api_payment
|
||||||
|
from lnbits.decorators import check_user_exists
|
||||||
|
|
||||||
|
from .crud import get_nostrkeys, get_nostrrelay
|
||||||
|
|
||||||
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
||||||
|
@nostradmin_ext.get("/", response_class=HTMLResponse)
|
||||||
|
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||||
|
return nostr_renderer().TemplateResponse(
|
||||||
|
"nostradmin/index.html", {"request": request, "user": user.dict()}
|
||||||
|
)
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
#################### NOSTR WEBSOCKET THREAD #########################
|
||||||
|
##### THE QUEUE LOOP THREAD THING THAT LISTENS TO BUNCH OF ##########
|
||||||
|
### WEBSOCKET CONNECTIONS, STORING DATA IN DB/PUSHING TO FRONTEND ###
|
||||||
|
################### VIA updater() FUNCTION ##########################
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
websocket_queue = asyncio.Queue(1000)
|
||||||
|
|
||||||
|
# while True:
|
||||||
|
async def nostr_subscribe():
|
||||||
|
return
|
||||||
|
# for the relays:
|
||||||
|
# async with websockets.connect("ws://localhost:8765") as websocket:
|
||||||
|
# for the public keys:
|
||||||
|
# await websocket.send("subscribe to events")
|
||||||
|
# await websocket.recv()
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
################### LNBITS WEBSOCKET ROUTES #########################
|
||||||
|
#### HERE IS WHERE LNBITS FRONTEND CAN RECEIVE AND SEND MESSAGES ####
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
class ConnectionManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.active_connections: List[WebSocket] = []
|
||||||
|
|
||||||
|
async def connect(self, websocket: WebSocket, nostr_id: str):
|
||||||
|
await websocket.accept()
|
||||||
|
websocket.id = nostr_id
|
||||||
|
self.active_connections.append(websocket)
|
||||||
|
|
||||||
|
def disconnect(self, websocket: WebSocket):
|
||||||
|
self.active_connections.remove(websocket)
|
||||||
|
|
||||||
|
async def send_personal_message(self, message: str, nostr_id: str):
|
||||||
|
for connection in self.active_connections:
|
||||||
|
if connection.id == nostr_id:
|
||||||
|
await connection.send_text(message)
|
||||||
|
|
||||||
|
async def broadcast(self, message: str):
|
||||||
|
for connection in self.active_connections:
|
||||||
|
await connection.send_text(message)
|
||||||
|
|
||||||
|
|
||||||
|
manager = ConnectionManager()
|
||||||
|
|
||||||
|
|
||||||
|
@nostradmin_ext.websocket("/nostradmin/ws/relayevents/{nostr_id}", name="nostr_id.websocket_by_id")
|
||||||
|
async def websocket_endpoint(websocket: WebSocket, nostr_id: str):
|
||||||
|
await manager.connect(websocket, nostr_id)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = await websocket.receive_text()
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
manager.disconnect(websocket)
|
||||||
|
|
||||||
|
|
||||||
|
async def updater(nostr_id, message):
|
||||||
|
copilot = await get_copilot(nostr_id)
|
||||||
|
if not copilot:
|
||||||
|
return
|
||||||
|
await manager.send_personal_message(f"{message}", nostr_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def relay_check(relay: str):
|
||||||
|
async with websockets.connect(relay) as websocket:
|
||||||
|
if str(websocket.state) == "State.OPEN":
|
||||||
|
print(str(websocket.state))
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
63
lnbits/extensions/nostradmin/views_api.py
Normal file
63
lnbits/extensions/nostradmin/views_api.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
import asyncio
|
||||||
|
from fastapi import Request
|
||||||
|
from fastapi.param_functions import Query
|
||||||
|
from fastapi.params import Depends
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
|
from lnbits.core.crud import get_user
|
||||||
|
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||||
|
from lnbits.utils.exchange_rates import currencies
|
||||||
|
|
||||||
|
from lnbits.settings import LNBITS_ADMIN_USERS
|
||||||
|
from . import nostradmin_ext
|
||||||
|
from .crud import (
|
||||||
|
create_nostrkeys,
|
||||||
|
get_nostrkeys,
|
||||||
|
create_nostrnotes,
|
||||||
|
get_nostrnotes,
|
||||||
|
create_nostrrelays,
|
||||||
|
get_nostrrelays,
|
||||||
|
get_nostrrelaylist,
|
||||||
|
update_nostrrelaysetlist,
|
||||||
|
create_nostrconnections,
|
||||||
|
get_nostrconnections,
|
||||||
|
)
|
||||||
|
from .models import nostrKeys, nostrCreateRelays, nostrRelaySetList
|
||||||
|
from .views import relay_check
|
||||||
|
|
||||||
|
@nostradmin_ext.get("/api/v1/relays")
|
||||||
|
async def api_relays_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||||
|
relays = await get_nostrrelays()
|
||||||
|
if not relays:
|
||||||
|
await create_nostrrelays(nostrCreateRelays(relay="wss://relayer.fiatjaf.com"))
|
||||||
|
await create_nostrrelays(
|
||||||
|
nostrCreateRelays(relay="wss://nostr-pub.wellorder.net")
|
||||||
|
)
|
||||||
|
relays = await get_nostrrelays()
|
||||||
|
if not relays:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
for relay in relays:
|
||||||
|
relay.status = await relay_check(relay.relay)
|
||||||
|
return relays
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@nostradmin_ext.get("/api/v1/relaylist")
|
||||||
|
async def api_relaylist(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||||
|
if wallet.wallet.user not in LNBITS_ADMIN_USERS:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
|
||||||
|
)
|
||||||
|
return await get_nostrrelaylist()
|
||||||
|
|
||||||
|
@nostradmin_ext.post("/api/v1/setlist")
|
||||||
|
async def api_relayssetlist(data: nostrRelaySetList, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||||
|
if wallet.wallet.user not in LNBITS_ADMIN_USERS:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
|
||||||
|
)
|
||||||
|
return await update_nostrrelaysetlist(data)
|
|
@ -15,6 +15,7 @@ import lnbits.settings as settings
|
||||||
class Extension(NamedTuple):
|
class Extension(NamedTuple):
|
||||||
code: str
|
code: str
|
||||||
is_valid: bool
|
is_valid: bool
|
||||||
|
is_admin_only: bool
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
short_description: Optional[str] = None
|
short_description: Optional[str] = None
|
||||||
icon: Optional[str] = None
|
icon: Optional[str] = None
|
||||||
|
@ -25,6 +26,7 @@ class Extension(NamedTuple):
|
||||||
class ExtensionManager:
|
class ExtensionManager:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._disabled: List[str] = settings.LNBITS_DISABLED_EXTENSIONS
|
self._disabled: List[str] = settings.LNBITS_DISABLED_EXTENSIONS
|
||||||
|
self._admin_only: List[str] = [x.strip(' ') for x in settings.LNBITS_ADMIN_EXTENSIONS]
|
||||||
self._extension_folders: List[str] = [
|
self._extension_folders: List[str] = [
|
||||||
x[1] for x in os.walk(os.path.join(settings.LNBITS_PATH, "extensions"))
|
x[1] for x in os.walk(os.path.join(settings.LNBITS_PATH, "extensions"))
|
||||||
][0]
|
][0]
|
||||||
|
@ -47,14 +49,17 @@ class ExtensionManager:
|
||||||
) as json_file:
|
) as json_file:
|
||||||
config = json.load(json_file)
|
config = json.load(json_file)
|
||||||
is_valid = True
|
is_valid = True
|
||||||
|
is_admin_only = True if extension in self._admin_only else False
|
||||||
except Exception:
|
except Exception:
|
||||||
config = {}
|
config = {}
|
||||||
is_valid = False
|
is_valid = False
|
||||||
|
is_admin_only = False
|
||||||
|
|
||||||
output.append(
|
output.append(
|
||||||
Extension(
|
Extension(
|
||||||
extension,
|
extension,
|
||||||
is_valid,
|
is_valid,
|
||||||
|
is_admin_only,
|
||||||
config.get("name"),
|
config.get("name"),
|
||||||
config.get("short_description"),
|
config.get("short_description"),
|
||||||
config.get("icon"),
|
config.get("icon"),
|
||||||
|
|
|
@ -29,6 +29,7 @@ LNBITS_ALLOWED_USERS: List[str] = env.list(
|
||||||
"LNBITS_ALLOWED_USERS", default=[], subcast=str
|
"LNBITS_ALLOWED_USERS", default=[], subcast=str
|
||||||
)
|
)
|
||||||
LNBITS_ADMIN_USERS: List[str] = env.list("LNBITS_ADMIN_USERS", default=[], subcast=str)
|
LNBITS_ADMIN_USERS: List[str] = env.list("LNBITS_ADMIN_USERS", default=[], subcast=str)
|
||||||
|
LNBITS_ADMIN_EXTENSIONS: List[str] = env.list("LNBITS_ADMIN_EXTENSIONS", default=[], subcast=str)
|
||||||
LNBITS_DISABLED_EXTENSIONS: List[str] = env.list(
|
LNBITS_DISABLED_EXTENSIONS: List[str] = env.list(
|
||||||
"LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str
|
"LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str
|
||||||
)
|
)
|
||||||
|
|
|
@ -123,6 +123,7 @@ window.LNbits = {
|
||||||
[
|
[
|
||||||
'code',
|
'code',
|
||||||
'isValid',
|
'isValid',
|
||||||
|
'isAdminOnly',
|
||||||
'name',
|
'name',
|
||||||
'shortDescription',
|
'shortDescription',
|
||||||
'icon',
|
'icon',
|
||||||
|
@ -135,7 +136,12 @@ window.LNbits = {
|
||||||
return obj
|
return obj
|
||||||
},
|
},
|
||||||
user: function (data) {
|
user: function (data) {
|
||||||
var obj = {id: data.id, email: data.email, extensions: data.extensions, wallets: data.wallets}
|
var obj = {
|
||||||
|
id: data.id,
|
||||||
|
email: data.email,
|
||||||
|
extensions: data.extensions,
|
||||||
|
wallets: data.wallets
|
||||||
|
}
|
||||||
var mapWallet = this.wallet
|
var mapWallet = this.wallet
|
||||||
obj.wallets = obj.wallets
|
obj.wallets = obj.wallets
|
||||||
.map(function (obj) {
|
.map(function (obj) {
|
||||||
|
@ -153,16 +159,23 @@ window.LNbits = {
|
||||||
return obj
|
return obj
|
||||||
},
|
},
|
||||||
wallet: function (data) {
|
wallet: function (data) {
|
||||||
newWallet = {id: data.id, name: data.name, adminkey: data.adminkey, inkey: data.inkey}
|
newWallet = {
|
||||||
|
id: data.id,
|
||||||
|
name: data.name,
|
||||||
|
adminkey: data.adminkey,
|
||||||
|
inkey: data.inkey
|
||||||
|
}
|
||||||
newWallet.msat = data.balance_msat
|
newWallet.msat = data.balance_msat
|
||||||
newWallet.sat = Math.round(data.balance_msat / 1000)
|
newWallet.sat = Math.round(data.balance_msat / 1000)
|
||||||
newWallet.fsat = new Intl.NumberFormat(window.LOCALE).format(newWallet.sat)
|
newWallet.fsat = new Intl.NumberFormat(window.LOCALE).format(
|
||||||
|
newWallet.sat
|
||||||
|
)
|
||||||
newWallet.url = ['/wallet?usr=', data.user, '&wal=', data.id].join('')
|
newWallet.url = ['/wallet?usr=', data.user, '&wal=', data.id].join('')
|
||||||
return newWallet
|
return newWallet
|
||||||
},
|
},
|
||||||
payment: function (data) {
|
payment: function (data) {
|
||||||
obj = {
|
obj = {
|
||||||
checking_id:data.id,
|
checking_id: data.id,
|
||||||
pending: data.pending,
|
pending: data.pending,
|
||||||
amount: data.amount,
|
amount: data.amount,
|
||||||
fee: data.fee,
|
fee: data.fee,
|
||||||
|
@ -174,7 +187,7 @@ window.LNbits = {
|
||||||
extra: data.extra,
|
extra: data.extra,
|
||||||
wallet_id: data.wallet_id,
|
wallet_id: data.wallet_id,
|
||||||
webhook: data.webhook,
|
webhook: data.webhook,
|
||||||
webhook_status: data.webhook_status,
|
webhook_status: data.webhook_status
|
||||||
}
|
}
|
||||||
|
|
||||||
obj.date = Quasar.utils.date.formatDate(
|
obj.date = Quasar.utils.date.formatDate(
|
||||||
|
@ -225,7 +238,8 @@ window.LNbits = {
|
||||||
Quasar.plugins.Notify.create({
|
Quasar.plugins.Notify.create({
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
type: types[error.response.status] || 'warning',
|
type: types[error.response.status] || 'warning',
|
||||||
message: error.response.data.message || error.response.data.detail || null,
|
message:
|
||||||
|
error.response.data.message || error.response.data.detail || null,
|
||||||
caption:
|
caption:
|
||||||
[error.response.status, ' ', error.response.statusText]
|
[error.response.status, ' ', error.response.statusText]
|
||||||
.join('')
|
.join('')
|
||||||
|
@ -368,6 +382,10 @@ window.windowMixin = {
|
||||||
.filter(function (obj) {
|
.filter(function (obj) {
|
||||||
return !obj.hidden
|
return !obj.hidden
|
||||||
})
|
})
|
||||||
|
.filter(function (obj) {
|
||||||
|
if (window.user.admin) return obj
|
||||||
|
return !obj.isAdminOnly
|
||||||
|
})
|
||||||
.map(function (obj) {
|
.map(function (obj) {
|
||||||
if (user) {
|
if (user) {
|
||||||
obj.isEnabled = user.extensions.indexOf(obj.code) !== -1
|
obj.isEnabled = user.extensions.indexOf(obj.code) !== -1
|
||||||
|
|
Loading…
Add table
Reference in a new issue