mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-20 10:39:33 +08:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d315e8306b | ||
|
8cd0e7a058 | ||
|
8fce62632d | ||
|
38c0c170e7 | ||
|
655536e457 | ||
|
807db8a2d8 | ||
|
d707eba046 | ||
|
e34a8e2e4a | ||
|
6bd9d85a9a | ||
|
f2de6299f6 | ||
|
a28d6eafae | ||
|
fce0edebc9 | ||
|
221aad55de | ||
|
377d475e05 | ||
|
0c3c59df4e | ||
|
eba996b0f2 |
@@ -18,6 +18,8 @@ README.md
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
yarn.lock
|
yarn.lock
|
||||||
app.json
|
app.json
|
||||||
|
CODE_OF_CONDUCT.md
|
||||||
|
CONTRIBUTING.md
|
||||||
|
|
||||||
### .gitignore content (commented rules are duplicated)
|
### .gitignore content (commented rules are duplicated)
|
||||||
|
|
||||||
|
71
.github/workflows/codeql-analysis.yml
vendored
71
.github/workflows/codeql-analysis.yml
vendored
@@ -1,71 +0,0 @@
|
|||||||
# For most projects, this workflow file will not need changing; you simply need
|
|
||||||
# to commit it to your repository.
|
|
||||||
#
|
|
||||||
# You may wish to alter this file to override the set of languages analyzed,
|
|
||||||
# or to provide custom queries or build logic.
|
|
||||||
#
|
|
||||||
# ******** NOTE ********
|
|
||||||
# We have attempted to detect the languages in your repository. Please check
|
|
||||||
# the `language` matrix defined below to confirm you have the correct set of
|
|
||||||
# supported CodeQL languages.
|
|
||||||
#
|
|
||||||
name: "CodeQL"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
pull_request:
|
|
||||||
# The branches below must be a subset of the branches above
|
|
||||||
branches: [ master ]
|
|
||||||
schedule:
|
|
||||||
- cron: '35 5 * * 2'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
language: [ 'javascript' ]
|
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
|
||||||
# Learn more:
|
|
||||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v1
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
|
||||||
# By default, queries listed here will override any specified in a config file.
|
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
|
||||||
- name: Autobuild
|
|
||||||
uses: github/codeql-action/autobuild@v1
|
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
|
||||||
# 📚 https://git.io/JvXDl
|
|
||||||
|
|
||||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
|
||||||
# and modify them (or add more) to build your code if your project
|
|
||||||
# uses a compiled language
|
|
||||||
|
|
||||||
#- run: |
|
|
||||||
# make bootstrap
|
|
||||||
# make release
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v1
|
|
@@ -76,7 +76,7 @@ https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy
|
|||||||
|
|
||||||
<!---
|
<!---
|
||||||
Abort. Heroku instance killed the server.js if idle, stupid.
|
Abort. Heroku instance killed the server.js if idle, stupid.
|
||||||
[](https://heroku.com/deploy?template=https://github.com/louislam/uptime-kuma/tree/1.0.8)
|
[](https://heroku.com/deploy?template=https://github.com/louislam/uptime-kuma/tree/1.0.10)
|
||||||
-->
|
-->
|
||||||
|
|
||||||
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/louislam/uptime-kuma/tree/master&refcode=e2c7eb658434)
|
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/louislam/uptime-kuma/tree/master&refcode=e2c7eb658434)
|
||||||
@@ -93,7 +93,7 @@ PS: For every new release, it takes some time to build the docker image, please
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
git fetch --all
|
git fetch --all
|
||||||
git checkout 1.0.8 --force
|
git checkout 1.0.10 --force
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
pm2 restart uptime-kuma
|
pm2 restart uptime-kuma
|
||||||
|
70
db/patch5.sql
Normal file
70
db/patch5.sql
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
PRAGMA foreign_keys = off;
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
create table monitor_dg_tmp (
|
||||||
|
id INTEGER not null primary key autoincrement,
|
||||||
|
name VARCHAR(150),
|
||||||
|
active BOOLEAN default 1 not null,
|
||||||
|
user_id INTEGER references user on update cascade on delete
|
||||||
|
set
|
||||||
|
null,
|
||||||
|
interval INTEGER default 20 not null,
|
||||||
|
url TEXT,
|
||||||
|
type VARCHAR(20),
|
||||||
|
weight INTEGER default 2000,
|
||||||
|
hostname VARCHAR(255),
|
||||||
|
port INTEGER,
|
||||||
|
created_date DATETIME default (DATETIME('now')) not null,
|
||||||
|
keyword VARCHAR(255),
|
||||||
|
maxretries INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ignore_tls BOOLEAN default 0 not null,
|
||||||
|
upside_down BOOLEAN default 0 not null
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into
|
||||||
|
monitor_dg_tmp(
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
active,
|
||||||
|
user_id,
|
||||||
|
interval,
|
||||||
|
url,
|
||||||
|
type,
|
||||||
|
weight,
|
||||||
|
hostname,
|
||||||
|
port,
|
||||||
|
keyword,
|
||||||
|
maxretries,
|
||||||
|
ignore_tls,
|
||||||
|
upside_down
|
||||||
|
)
|
||||||
|
select
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
active,
|
||||||
|
user_id,
|
||||||
|
interval,
|
||||||
|
url,
|
||||||
|
type,
|
||||||
|
weight,
|
||||||
|
hostname,
|
||||||
|
port,
|
||||||
|
keyword,
|
||||||
|
maxretries,
|
||||||
|
ignore_tls,
|
||||||
|
upside_down
|
||||||
|
from
|
||||||
|
monitor;
|
||||||
|
|
||||||
|
drop table monitor;
|
||||||
|
|
||||||
|
alter table
|
||||||
|
monitor_dg_tmp rename to monitor;
|
||||||
|
|
||||||
|
create index user_id on monitor (user_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
PRAGMA foreign_keys = on;
|
@@ -14,7 +14,6 @@ RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev && \
|
|||||||
RUN apk add --no-cache python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib
|
RUN apk add --no-cache python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib
|
||||||
RUN pip3 --no-cache-dir install apprise && \
|
RUN pip3 --no-cache-dir install apprise && \
|
||||||
rm -rf /root/.cache
|
rm -rf /root/.cache
|
||||||
RUN apprise --version
|
|
||||||
|
|
||||||
# New things add here
|
# New things add here
|
||||||
|
|
||||||
|
78
extra/update-version.js
Normal file
78
extra/update-version.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* String.prototype.replaceAll() polyfill
|
||||||
|
* https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
|
||||||
|
* @author Chris Ferdinandi
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
if (!String.prototype.replaceAll) {
|
||||||
|
String.prototype.replaceAll = function(str, newStr) {
|
||||||
|
|
||||||
|
// If a regex pattern
|
||||||
|
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") {
|
||||||
|
return this.replace(str, newStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a string
|
||||||
|
return this.replace(new RegExp(str, "g"), newStr);
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pkg = require("../package.json");
|
||||||
|
const fs = require("fs");
|
||||||
|
const child_process = require("child_process");
|
||||||
|
const oldVersion = pkg.version;
|
||||||
|
const newVersion = process.argv[2];
|
||||||
|
|
||||||
|
console.log("Old Version: " + oldVersion);
|
||||||
|
console.log("New Version: " + newVersion);
|
||||||
|
|
||||||
|
if (! newVersion) {
|
||||||
|
console.error("invalid version");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = tagExists(newVersion);
|
||||||
|
|
||||||
|
if (! exists) {
|
||||||
|
// Process package.json
|
||||||
|
pkg.version = newVersion;
|
||||||
|
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
|
||||||
|
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion);
|
||||||
|
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||||
|
|
||||||
|
// Process README.md
|
||||||
|
fs.writeFileSync("README.md", fs.readFileSync("README.md", "utf8").replaceAll(oldVersion, newVersion));
|
||||||
|
|
||||||
|
commit(newVersion);
|
||||||
|
tag(newVersion);
|
||||||
|
} else {
|
||||||
|
console.log("version exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
function commit(version) {
|
||||||
|
let msg = "update to " + version;
|
||||||
|
|
||||||
|
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
|
||||||
|
let stdout = res.stdout.toString().trim();
|
||||||
|
console.log(stdout)
|
||||||
|
|
||||||
|
if (stdout.includes("no changes added to commit")) {
|
||||||
|
throw new Error("commit error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tag(version) {
|
||||||
|
let res = child_process.spawnSync("git", ["tag", version]);
|
||||||
|
console.log(res.stdout.toString().trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
function tagExists(version) {
|
||||||
|
if (! version) {
|
||||||
|
throw new Error("invalid version");
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = child_process.spawnSync("git", ["tag", "-l", version]);
|
||||||
|
|
||||||
|
return res.stdout.toString().trim() === version;
|
||||||
|
}
|
@@ -1,39 +0,0 @@
|
|||||||
/**
|
|
||||||
* String.prototype.replaceAll() polyfill
|
|
||||||
* https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
|
|
||||||
* @author Chris Ferdinandi
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
if (!String.prototype.replaceAll) {
|
|
||||||
String.prototype.replaceAll = function(str, newStr){
|
|
||||||
|
|
||||||
// If a regex pattern
|
|
||||||
if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') {
|
|
||||||
return this.replace(str, newStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a string
|
|
||||||
return this.replace(new RegExp(str, 'g'), newStr);
|
|
||||||
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const pkg = require('../package.json');
|
|
||||||
const fs = require("fs");
|
|
||||||
const oldVersion = pkg.version
|
|
||||||
const newVersion = process.argv[2]
|
|
||||||
|
|
||||||
console.log("Old Version: " + oldVersion)
|
|
||||||
console.log("New Version: " + newVersion)
|
|
||||||
|
|
||||||
if (newVersion) {
|
|
||||||
// Process package.json
|
|
||||||
pkg.version = newVersion
|
|
||||||
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion)
|
|
||||||
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion)
|
|
||||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n")
|
|
||||||
|
|
||||||
// Process README.md
|
|
||||||
fs.writeFileSync("README.md", fs.readFileSync("README.md", 'utf8').replaceAll(oldVersion, newVersion))
|
|
||||||
}
|
|
||||||
|
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "1.0.8",
|
"version": "1.0.10",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -16,11 +16,11 @@
|
|||||||
"update": "",
|
"update": "",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"vite-preview-dist": "vite preview --host",
|
"vite-preview-dist": "vite preview --host",
|
||||||
"build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.0.8 --target release . --push",
|
"build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.0.10 --target release . --push",
|
||||||
"build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
"build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||||
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push",
|
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push",
|
||||||
"setup": "git checkout 1.0.8 && npm install && npm run build",
|
"setup": "git checkout 1.0.10 && npm install && npm run build",
|
||||||
"version-global-replace": "node extra/version-global-replace.js",
|
"update-version": "node extra/update-version.js",
|
||||||
"mark-as-nightly": "node extra/mark-as-nightly.js"
|
"mark-as-nightly": "node extra/mark-as-nightly.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@@ -9,7 +9,7 @@ class Database {
|
|||||||
|
|
||||||
static templatePath = "./db/kuma.db"
|
static templatePath = "./db/kuma.db"
|
||||||
static path = "./data/kuma.db";
|
static path = "./data/kuma.db";
|
||||||
static latestVersion = 4;
|
static latestVersion = 5;
|
||||||
static noReject = true;
|
static noReject = true;
|
||||||
|
|
||||||
static async patch() {
|
static async patch() {
|
||||||
|
@@ -137,6 +137,24 @@ class Notification {
|
|||||||
throwGeneralAxiosError(error)
|
throwGeneralAxiosError(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else if (notification.type === "pushy") {
|
||||||
|
try {
|
||||||
|
await axios.post(`https://api.pushy.me/push?api_key=${notification.pushyAPIKey}`, {
|
||||||
|
"to": notification.pushyToken,
|
||||||
|
"data": {
|
||||||
|
"message": "Uptime-Kuma"
|
||||||
|
},
|
||||||
|
"notification": {
|
||||||
|
"body": msg,
|
||||||
|
"badge": 1,
|
||||||
|
"sound": "ping.aiff"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
} else if (notification.type === "slack") {
|
} else if (notification.type === "slack") {
|
||||||
try {
|
try {
|
||||||
if (heartbeatJSON == null) {
|
if (heartbeatJSON == null) {
|
||||||
|
@@ -96,8 +96,8 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
|||||||
app.get("/metrics", basicAuth, prometheusAPIMetrics())
|
app.get("/metrics", basicAuth, prometheusAPIMetrics())
|
||||||
|
|
||||||
// Universal Route Handler, must be at the end
|
// Universal Route Handler, must be at the end
|
||||||
app.get("*", function(request, response, next) {
|
app.get("*", function(_request, response) {
|
||||||
response.end(indexHTML)
|
response.send(indexHTML);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Adding socket handler")
|
console.log("Adding socket handler")
|
||||||
@@ -114,11 +114,6 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
|||||||
socket.emit("setup")
|
socket.emit("setup")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await setting("disableAuth")) {
|
|
||||||
console.log("Disabled Auth: auto login to admin")
|
|
||||||
await afterLogin(socket, await R.findOne("user", " username = 'admin' "))
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
socket.on("disconnect", () => {
|
||||||
totalClient--;
|
totalClient--;
|
||||||
});
|
});
|
||||||
@@ -139,8 +134,12 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
|||||||
])
|
])
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
|
debug("afterLogin")
|
||||||
|
|
||||||
await afterLogin(socket, user)
|
await afterLogin(socket, user)
|
||||||
|
|
||||||
|
debug("afterLogin ok")
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
})
|
})
|
||||||
@@ -536,6 +535,22 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
|||||||
callback(false);
|
callback(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
debug("added all socket handlers")
|
||||||
|
|
||||||
|
// ***************************
|
||||||
|
// Better do anything after added all socket handlers here
|
||||||
|
// ***************************
|
||||||
|
|
||||||
|
debug("check auto login")
|
||||||
|
if (await setting("disableAuth")) {
|
||||||
|
console.log("Disabled Auth: auto login to admin")
|
||||||
|
await afterLogin(socket, await R.findOne("user"))
|
||||||
|
socket.emit("autoLogin")
|
||||||
|
} else {
|
||||||
|
debug("need auth")
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Init")
|
console.log("Init")
|
||||||
@@ -605,8 +620,6 @@ async function afterLogin(socket, user) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendNotificationList(socket)
|
sendNotificationList(socket)
|
||||||
|
|
||||||
socket.emit("autoLogin")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMonitorJSONList(userID) {
|
async function getMonitorJSONList(userID) {
|
||||||
|
@@ -21,6 +21,7 @@
|
|||||||
<option value="gotify">Gotify</option>
|
<option value="gotify">Gotify</option>
|
||||||
<option value="slack">Slack</option>
|
<option value="slack">Slack</option>
|
||||||
<option value="pushover">Pushover</option>
|
<option value="pushover">Pushover</option>
|
||||||
|
<option value="pushy">Pushy</option>
|
||||||
<option value="lunasea">LunaSea</option>
|
<option value="lunasea">LunaSea</option>
|
||||||
<option value="apprise">Apprise (Support 50+ Notification services)</option>
|
<option value="apprise">Apprise (Support 50+ Notification services)</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -229,6 +230,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-if="notification.type === 'pushy'">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="pushy-app-token" class="form-label">API_KEY</label>
|
||||||
|
<input type="text" class="form-control" id="pushy-app-token" required v-model="notification.pushyAPIKey">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="pushy-user-key" class="form-label">USER_TOKEN</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input type="text" class="form-control" id="pushy-user-key" required v-model="notification.pushyToken">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
More info on: <a href="https://pushy.me/docs/api/send-notifications" target="_blank">https://pushy.me/docs/api/send-notifications</a>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type === 'pushover'">
|
<template v-if="notification.type === 'pushover'">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="pushover-user" class="form-label">User Key<span style="color:red;"><sup>*</sup></span></label>
|
<label for="pushover-user" class="form-label">User Key<span style="color:red;"><sup>*</sup></span></label>
|
||||||
|
Reference in New Issue
Block a user