mirror of
				https://github.com/louislam/uptime-kuma.git
				synced 2025-11-04 13:46:13 +08:00 
			
		
		
		
	Merge branch 'louislam:master' into clear-monitor-data
This commit is contained in:
		@@ -77,6 +77,8 @@ module.exports = {
 | 
				
			|||||||
        "no-empty": ["error", {
 | 
					        "no-empty": ["error", {
 | 
				
			||||||
            "allowEmptyCatch": true
 | 
					            "allowEmptyCatch": true
 | 
				
			||||||
        }],
 | 
					        }],
 | 
				
			||||||
        "no-control-regex": "off"
 | 
					        "no-control-regex": "off",
 | 
				
			||||||
 | 
					        "one-var": ["error", "never"],
 | 
				
			||||||
 | 
					        "max-statements-per-line": ["error", { "max": 1 }]
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -73,6 +73,12 @@ For example, recently, because I am not a python expert, I spent a 2 hours to re
 | 
				
			|||||||
npm install --dev
 | 
					npm install --dev
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For npm@7, you need --legacy-peer-deps
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					npm install --legacy-peer-deps --dev
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Backend Dev
 | 
					# Backend Dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```bash
 | 
					```bash
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										27
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								README.md
									
									
									
									
									
								
							@@ -19,14 +19,6 @@ It is a self-hosted monitoring tool like "Uptime Robot".
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## 🔧 How to Install
 | 
					## 🔧 How to Install
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### 🚀 Installer via CLI
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Interactive CLI installer, supports Docker or without Docker. 
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```bash
 | 
					 | 
				
			||||||
curl -o kuma_install.sh http://git.kuma.pet/install.sh && sudo bash kuma_install.sh
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 🐳 Docker
 | 
					### 🐳 Docker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```bash
 | 
					```bash
 | 
				
			||||||
@@ -36,6 +28,25 @@ docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name upti
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Browse to http://localhost:3001 after started.
 | 
					Browse to http://localhost:3001 after started.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 💪🏻 Without Docker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Required Tools: Node.js >= 14, git and pm2.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					git clone https://github.com/louislam/uptime-kuma.git
 | 
				
			||||||
 | 
					cd uptime-kuma
 | 
				
			||||||
 | 
					npm run setup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Option 1. Try it
 | 
				
			||||||
 | 
					node server/server.js
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# (Recommended) Option 2. Run in background using PM2
 | 
				
			||||||
 | 
					# Install PM2 if you don't have: npm install pm2 -g
 | 
				
			||||||
 | 
					pm2 start server/server.js --name uptime-kuma
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Browse to http://localhost:3001 after started.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Advanced Installation
 | 
					### Advanced Installation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
If you need more options or need to browse via a reserve proxy, please read:
 | 
					If you need more options or need to browse via a reserve proxy, please read:
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										11
									
								
								dockerfile
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								dockerfile
									
									
									
									
									
								
							@@ -2,22 +2,13 @@
 | 
				
			|||||||
FROM node:14-alpine3.12 AS release
 | 
					FROM node:14-alpine3.12 AS release
 | 
				
			||||||
WORKDIR /app
 | 
					WORKDIR /app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# split the sqlite install here, so that it can caches the prebuilt
 | 
					 | 
				
			||||||
RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev && \
 | 
					 | 
				
			||||||
            ln -s /usr/bin/python3 /usr/bin/python && \
 | 
					 | 
				
			||||||
            npm install better-sqlite3@7.4.3 bcrypt@5.0.1 && \
 | 
					 | 
				
			||||||
            apk del .build-deps && \
 | 
					 | 
				
			||||||
            rm -f /usr/bin/python
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Touching above code may causes sqlite3 re-compile again, painful slow.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Install apprise
 | 
					# Install apprise
 | 
				
			||||||
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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY . .
 | 
					COPY . .
 | 
				
			||||||
RUN npm install && npm run build && npm prune
 | 
					RUN npm install --legacy-peer-deps && npm run build && npm prune
 | 
				
			||||||
 | 
					
 | 
				
			||||||
EXPOSE 3001
 | 
					EXPOSE 3001
 | 
				
			||||||
VOLUME ["/app/data"]
 | 
					VOLUME ["/app/data"]
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										3450
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3450
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										14
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								package.json
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    "name": "uptime-kuma",
 | 
					    "name": "uptime-kuma",
 | 
				
			||||||
    "version": "1.3.2",
 | 
					    "version": "1.5.0",
 | 
				
			||||||
    "license": "MIT",
 | 
					    "license": "MIT",
 | 
				
			||||||
    "repository": {
 | 
					    "repository": {
 | 
				
			||||||
        "type": "git",
 | 
					        "type": "git",
 | 
				
			||||||
@@ -20,10 +20,10 @@
 | 
				
			|||||||
        "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.3.2 --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.5.0 --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 --progress plain",
 | 
					        "build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
 | 
				
			||||||
        "setup": "git checkout 1.3.2 && npm install && npm run build",
 | 
					        "setup": "git checkout 1.5.0 && npm install --legacy-peer-deps && npm run build && npm prune",
 | 
				
			||||||
        "update-version": "node extra/update-version.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",
 | 
				
			||||||
        "reset-password": "node extra/reset-password.js",
 | 
					        "reset-password": "node extra/reset-password.js",
 | 
				
			||||||
@@ -40,13 +40,13 @@
 | 
				
			|||||||
        "@fortawesome/free-regular-svg-icons": "^5.15.4",
 | 
					        "@fortawesome/free-regular-svg-icons": "^5.15.4",
 | 
				
			||||||
        "@fortawesome/free-solid-svg-icons": "^5.15.4",
 | 
					        "@fortawesome/free-solid-svg-icons": "^5.15.4",
 | 
				
			||||||
        "@fortawesome/vue-fontawesome": "^3.0.0-4",
 | 
					        "@fortawesome/vue-fontawesome": "^3.0.0-4",
 | 
				
			||||||
 | 
					        "@louislam/better-sqlite3-with-prebuilds": "^7.4.3",
 | 
				
			||||||
        "@popperjs/core": "^2.9.3",
 | 
					        "@popperjs/core": "^2.9.3",
 | 
				
			||||||
        "args-parser": "^1.3.0",
 | 
					        "args-parser": "^1.3.0",
 | 
				
			||||||
        "axios": "^0.21.1",
 | 
					        "axios": "^0.21.1",
 | 
				
			||||||
        "bcrypt": "^5.0.1",
 | 
					        "bcryptjs": "^2.4.3",
 | 
				
			||||||
        "better-sqlite3": "^7.4.3",
 | 
					 | 
				
			||||||
        "bootstrap": "^5.1.0",
 | 
					        "bootstrap": "^5.1.0",
 | 
				
			||||||
        "chart.js": "^3.5.0",
 | 
					        "chart.js": "^3.5.1",
 | 
				
			||||||
        "chartjs-adapter-dayjs": "^1.0.0",
 | 
					        "chartjs-adapter-dayjs": "^1.0.0",
 | 
				
			||||||
        "command-exists": "^1.2.9",
 | 
					        "command-exists": "^1.2.9",
 | 
				
			||||||
        "compare-versions": "^3.6.0",
 | 
					        "compare-versions": "^3.6.0",
 | 
				
			||||||
@@ -60,7 +60,7 @@
 | 
				
			|||||||
        "password-hash": "^1.2.2",
 | 
					        "password-hash": "^1.2.2",
 | 
				
			||||||
        "prom-client": "^13.2.0",
 | 
					        "prom-client": "^13.2.0",
 | 
				
			||||||
        "prometheus-api-metrics": "^3.2.0",
 | 
					        "prometheus-api-metrics": "^3.2.0",
 | 
				
			||||||
        "redbean-node": "0.1.1",
 | 
					        "redbean-node": "0.1.2",
 | 
				
			||||||
        "socket.io": "^4.1.3",
 | 
					        "socket.io": "^4.1.3",
 | 
				
			||||||
        "socket.io-client": "^4.1.3",
 | 
					        "socket.io-client": "^4.1.3",
 | 
				
			||||||
        "tcp-ping": "^0.1.1",
 | 
					        "tcp-ping": "^0.1.1",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ dayjs.extend(timezone)
 | 
				
			|||||||
const axios = require("axios");
 | 
					const axios = require("axios");
 | 
				
			||||||
const { Prometheus } = require("../prometheus");
 | 
					const { Prometheus } = require("../prometheus");
 | 
				
			||||||
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
 | 
					const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
 | 
				
			||||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode } = require("../util-server");
 | 
					const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom } = require("../util-server");
 | 
				
			||||||
const { R } = require("redbean-node");
 | 
					const { R } = require("redbean-node");
 | 
				
			||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
 | 
					const { BeanModel } = require("redbean-node/dist/bean-model");
 | 
				
			||||||
const { Notification } = require("../notification")
 | 
					const { Notification } = require("../notification")
 | 
				
			||||||
@@ -353,10 +353,16 @@ class Monitor extends BeanModel {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    static async sendStats(io, monitorID, userID) {
 | 
					    static async sendStats(io, monitorID, userID) {
 | 
				
			||||||
        await Monitor.sendAvgPing(24, io, monitorID, userID);
 | 
					        const hasClients = getTotalClientInRoom(io, userID) > 0;
 | 
				
			||||||
        await Monitor.sendUptime(24, io, monitorID, userID);
 | 
					
 | 
				
			||||||
        await Monitor.sendUptime(24 * 30, io, monitorID, userID);
 | 
					        if (hasClients) {
 | 
				
			||||||
        await Monitor.sendCertInfo(io, monitorID, userID);
 | 
					            await Monitor.sendAvgPing(24, io, monitorID, userID);
 | 
				
			||||||
 | 
					            await Monitor.sendUptime(24, io, monitorID, userID);
 | 
				
			||||||
 | 
					            await Monitor.sendUptime(24 * 30, io, monitorID, userID);
 | 
				
			||||||
 | 
					            await Monitor.sendCertInfo(io, monitorID, userID);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            debug("No clients in the room, no need to send stats");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -96,9 +96,16 @@ class Notification {
 | 
				
			|||||||
                    return okMsg;
 | 
					                    return okMsg;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let url = monitorJSON["url"] === "https://" ? monitorJSON["hostname"] : monitorJSON["url"]
 | 
					                let url;
 | 
				
			||||||
                if (monitorJSON["port"]) {
 | 
					
 | 
				
			||||||
                    url += ":" + monitorJSON[port];
 | 
					                if (monitorJSON["type"] === "port") {
 | 
				
			||||||
 | 
					                    url = monitorJSON["hostname"];
 | 
				
			||||||
 | 
					                    if (monitorJSON["port"]) {
 | 
				
			||||||
 | 
					                        url += ":" + monitorJSON["port"];
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    url = monitorJSON["url"];
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // If heartbeatJSON is not null, we go into the normal alerting loop.
 | 
					                // If heartbeatJSON is not null, we go into the normal alerting loop.
 | 
				
			||||||
@@ -331,7 +338,7 @@ class Notification {
 | 
				
			|||||||
                    await axios.post(notification.mattermostWebhookUrl, mattermostTestData)
 | 
					                    await axios.post(notification.mattermostWebhookUrl, mattermostTestData)
 | 
				
			||||||
                    return okMsg;
 | 
					                    return okMsg;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                
 | 
					
 | 
				
			||||||
                const mattermostChannel = notification.mattermostchannel;
 | 
					                const mattermostChannel = notification.mattermostchannel;
 | 
				
			||||||
                const mattermostIconEmoji = notification.mattermosticonemo;
 | 
					                const mattermostIconEmoji = notification.mattermosticonemo;
 | 
				
			||||||
                const mattermostIconUrl = notification.mattermosticonurl;
 | 
					                const mattermostIconUrl = notification.mattermosticonurl;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
const passwordHashOld = require("password-hash");
 | 
					const passwordHashOld = require("password-hash");
 | 
				
			||||||
const bcrypt = require("bcrypt");
 | 
					const bcrypt = require("bcryptjs");
 | 
				
			||||||
const saltRounds = 10;
 | 
					const saltRounds = 10;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
exports.generate = function (password) {
 | 
					exports.generate = function (password) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,13 @@
 | 
				
			|||||||
// https://github.com/ben-bradley/ping-lite/blob/master/ping-lite.js
 | 
					// https://github.com/ben-bradley/ping-lite/blob/master/ping-lite.js
 | 
				
			||||||
// Fixed on Windows
 | 
					// Fixed on Windows
 | 
				
			||||||
const net = require("net");
 | 
					const net = require("net");
 | 
				
			||||||
const spawn = require("child_process").spawn,
 | 
					const spawn = require("child_process").spawn;
 | 
				
			||||||
    events = require("events"),
 | 
					const events = require("events");
 | 
				
			||||||
    fs = require("fs"),
 | 
					const fs = require("fs");
 | 
				
			||||||
    WIN = /^win/.test(process.platform),
 | 
					const WIN = /^win/.test(process.platform);
 | 
				
			||||||
    LIN = /^linux/.test(process.platform),
 | 
					const LIN = /^linux/.test(process.platform);
 | 
				
			||||||
    MAC = /^darwin/.test(process.platform);
 | 
					const MAC = /^darwin/.test(process.platform);
 | 
				
			||||||
    FBSD = /^freebsd/.test(process.platform);
 | 
					const FBSD = /^freebsd/.test(process.platform);
 | 
				
			||||||
const { debug } = require("../src/util");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = Ping;
 | 
					module.exports = Ping;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -22,15 +21,17 @@ function Ping(host, options) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    events.EventEmitter.call(this);
 | 
					    events.EventEmitter.call(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const timeout = 10;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (WIN) {
 | 
					    if (WIN) {
 | 
				
			||||||
        this._bin = "c:/windows/system32/ping.exe";
 | 
					        this._bin = "c:/windows/system32/ping.exe";
 | 
				
			||||||
        this._args = (options.args) ? options.args : [ "-n", "1", "-w", "5000", host ];
 | 
					        this._args = (options.args) ? options.args : [ "-n", "1", "-w", timeout * 1000, host ];
 | 
				
			||||||
        this._regmatch = /[><=]([0-9.]+?)ms/;
 | 
					        this._regmatch = /[><=]([0-9.]+?)ms/;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    } else if (LIN) {
 | 
					    } else if (LIN) {
 | 
				
			||||||
        this._bin = "/bin/ping";
 | 
					        this._bin = "/bin/ping";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const defaultArgs = [ "-n", "-w", "2", "-c", "1", host ];
 | 
					        const defaultArgs = [ "-n", "-w", timeout, "-c", "1", host ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (net.isIPv6(host) || options.ipv6) {
 | 
					        if (net.isIPv6(host) || options.ipv6) {
 | 
				
			||||||
            defaultArgs.unshift("-6");
 | 
					            defaultArgs.unshift("-6");
 | 
				
			||||||
@@ -47,13 +48,13 @@ function Ping(host, options) {
 | 
				
			|||||||
            this._bin = "/sbin/ping";
 | 
					            this._bin = "/sbin/ping";
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this._args = (options.args) ? options.args : [ "-n", "-t", "2", "-c", "1", host ];
 | 
					        this._args = (options.args) ? options.args : [ "-n", "-t", timeout, "-c", "1", host ];
 | 
				
			||||||
        this._regmatch = /=([0-9.]+?) ms/;
 | 
					        this._regmatch = /=([0-9.]+?) ms/;
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
    } else if (FBSD) {
 | 
					    } else if (FBSD) {
 | 
				
			||||||
        this._bin = "/sbin/ping";
 | 
					        this._bin = "/sbin/ping";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const defaultArgs = [ "-n", "-t", "2", "-c", "1", host ];
 | 
					        const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (net.isIPv6(host) || options.ipv6) {
 | 
					        if (net.isIPv6(host) || options.ipv6) {
 | 
				
			||||||
            defaultArgs.unshift("-6");
 | 
					            defaultArgs.unshift("-6");
 | 
				
			||||||
@@ -88,7 +89,9 @@ Ping.prototype.send = function (callback) {
 | 
				
			|||||||
        return self.emit("result", ms);
 | 
					        return self.emit("result", ms);
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let _ended, _exited, _errored;
 | 
					    let _ended;
 | 
				
			||||||
 | 
					    let _exited;
 | 
				
			||||||
 | 
					    let _errored;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this._ping = spawn(this._bin, this._args); // spawn the binary
 | 
					    this._ping = spawn(this._bin, this._args); // spawn the binary
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -120,9 +123,9 @@ Ping.prototype.send = function (callback) {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function onEnd() {
 | 
					    function onEnd() {
 | 
				
			||||||
        let stdout = this.stdout._stdout,
 | 
					        let stdout = this.stdout._stdout;
 | 
				
			||||||
            stderr = this.stderr._stderr,
 | 
					        let stderr = this.stderr._stderr;
 | 
				
			||||||
            ms;
 | 
					        let ms;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (stderr) {
 | 
					        if (stderr) {
 | 
				
			||||||
            return callback(new Error(stderr));
 | 
					            return callback(new Error(stderr));
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -248,3 +248,26 @@ exports.checkStatusCode = function (status, accepted_codes) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					exports.getTotalClientInRoom = (io, roomName) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const sockets = io.sockets;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (! sockets) {
 | 
				
			||||||
 | 
					        return 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const adapter = sockets.adapter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (! adapter) {
 | 
				
			||||||
 | 
					        return 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const room = adapter.rooms.get(roomName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (room) {
 | 
				
			||||||
 | 
					        return room.size;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        return 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -131,7 +131,7 @@ h2 {
 | 
				
			|||||||
    background-color: #090c10;
 | 
					    background-color: #090c10;
 | 
				
			||||||
    color: $dark-font-color;
 | 
					    color: $dark-font-color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &::-webkit-scrollbar-thumb {
 | 
					    &::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb {
 | 
				
			||||||
        background: $dark-border-color;
 | 
					        background: $dark-border-color;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <div class="shadow-box list mb-4">
 | 
					    <div class="shadow-box list mb-3" :class="{ scrollbar: scrollbar }">
 | 
				
			||||||
        <div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
 | 
					        <div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
 | 
				
			||||||
            {{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
 | 
					            {{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
@@ -34,6 +34,11 @@ export default {
 | 
				
			|||||||
        Uptime,
 | 
					        Uptime,
 | 
				
			||||||
        HeartbeatBar,
 | 
					        HeartbeatBar,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    props: {
 | 
				
			||||||
 | 
					        scrollbar: {
 | 
				
			||||||
 | 
					            type: Boolean,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    computed: {
 | 
					    computed: {
 | 
				
			||||||
        sortedMonitorList() {
 | 
					        sortedMonitorList() {
 | 
				
			||||||
            let result = Object.values(this.$root.monitorList);
 | 
					            let result = Object.values(this.$root.monitorList);
 | 
				
			||||||
@@ -83,8 +88,13 @@ export default {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.list {
 | 
					.list {
 | 
				
			||||||
    height: auto;
 | 
					    &.scrollbar {
 | 
				
			||||||
    min-height: calc(100vh - 240px);
 | 
					        min-height: calc(100vh - 240px);
 | 
				
			||||||
 | 
					        max-height: calc(100vh - 30px);
 | 
				
			||||||
 | 
					        overflow-y: auto;
 | 
				
			||||||
 | 
					        position: sticky;
 | 
				
			||||||
 | 
					        top: 10px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .item {
 | 
					    .item {
 | 
				
			||||||
        display: block;
 | 
					        display: block;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										106
									
								
								src/languages/fr.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/languages/fr.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,106 @@
 | 
				
			|||||||
 | 
					export default {
 | 
				
			||||||
 | 
					    languageName: "Français (France)",
 | 
				
			||||||
 | 
					    Settings: "Paramètres",
 | 
				
			||||||
 | 
					    Dashboard: "Dashboard",
 | 
				
			||||||
 | 
					    "New Update": "Mise à jour disponible",
 | 
				
			||||||
 | 
					    Language: "Langue",
 | 
				
			||||||
 | 
					    Appearance: "Apparence",
 | 
				
			||||||
 | 
					    Theme: "Thème",
 | 
				
			||||||
 | 
					    General: "Général",
 | 
				
			||||||
 | 
					    Version: "Version",
 | 
				
			||||||
 | 
					    "Check Update On GitHub": "Consulter les mises à jour sur Github",
 | 
				
			||||||
 | 
					    List: "Lister",
 | 
				
			||||||
 | 
					    Add: "Ajouter",
 | 
				
			||||||
 | 
					    "Add New Monitor": "Ajouter un nouveau check",
 | 
				
			||||||
 | 
					    "Quick Stats": "Résumé",
 | 
				
			||||||
 | 
					    Up: "En ligne",
 | 
				
			||||||
 | 
					    Down: "Hors ligne",
 | 
				
			||||||
 | 
					    Pending: "Dans la file d'attente",
 | 
				
			||||||
 | 
					    Unknown: "Inconnu",
 | 
				
			||||||
 | 
					    Pause: "En Pause",
 | 
				
			||||||
 | 
					    pauseDashboardHome: "Éléments mis en pause",
 | 
				
			||||||
 | 
					    Name: "Nom",
 | 
				
			||||||
 | 
					    Status: "État",
 | 
				
			||||||
 | 
					    DateTime: "Heure",
 | 
				
			||||||
 | 
					    Message: "Messages",
 | 
				
			||||||
 | 
					    "No important events": "Pas d'évènements important",
 | 
				
			||||||
 | 
					    Resume: "Reprendre",
 | 
				
			||||||
 | 
					    Edit: "Modifier",
 | 
				
			||||||
 | 
					    Delete: "Supprimer",
 | 
				
			||||||
 | 
					    Current: "Actuellement",
 | 
				
			||||||
 | 
					    Uptime: "Uptime",
 | 
				
			||||||
 | 
					    "Cert Exp.": "Cert Exp.",
 | 
				
			||||||
 | 
					    days: "Jours",
 | 
				
			||||||
 | 
					    day: "Jour",
 | 
				
			||||||
 | 
					    "-day": "Demi-Journée",
 | 
				
			||||||
 | 
					    hour: "Heure",
 | 
				
			||||||
 | 
					    "-hour": "Demi-Heure",
 | 
				
			||||||
 | 
					    checkEverySecond: "Vérifier toutes les {0} secondes",
 | 
				
			||||||
 | 
					    "Avg.": "Moy.",
 | 
				
			||||||
 | 
					    Response: "Réponse",
 | 
				
			||||||
 | 
					    Ping: "Ping",
 | 
				
			||||||
 | 
					    "Monitor Type": "Type de Monitoring",
 | 
				
			||||||
 | 
					    Keyword: "Mot-clé",
 | 
				
			||||||
 | 
					    "Friendly Name": "Nom d'affichage",
 | 
				
			||||||
 | 
					    URL: "URL",
 | 
				
			||||||
 | 
					    Hostname: "Nom d'hôte",
 | 
				
			||||||
 | 
					    Port: "Port",
 | 
				
			||||||
 | 
					    "Heartbeat Interval": "Intervale de vérifications",
 | 
				
			||||||
 | 
					    Retries: "Essais",
 | 
				
			||||||
 | 
					    retriesDescription: "Nombre d'essais avant que le service soit déclaré hors-ligne.",
 | 
				
			||||||
 | 
					    Advanced: "Avancé",
 | 
				
			||||||
 | 
					    ignoreTLSError: "Ignorer les erreurs liées au certificat SSL/TLS",
 | 
				
			||||||
 | 
					    "Upside Down Mode": "Mode inversé",
 | 
				
			||||||
 | 
					    upsideDownModeDescription: "Si le service est en ligne il sera alors noté hors-ligne et vice-versa.",
 | 
				
			||||||
 | 
					    "Max. Redirects": "Redirections",
 | 
				
			||||||
 | 
					    maxRedirectDescription: "Nombre maximal de redirections avant que le service soit noté hors-ligne.",
 | 
				
			||||||
 | 
					    "Accepted Status Codes": "Codes HTTP",
 | 
				
			||||||
 | 
					    acceptedStatusCodesDescription: "Si les codes HTTP reçus sont ceux séléctionnés, alors le serveur sera noté en ligne.",
 | 
				
			||||||
 | 
					    Save: "Sauvegarder",
 | 
				
			||||||
 | 
					    Notifications: "Notifications",
 | 
				
			||||||
 | 
					    "Not available, please setup.": "Créez des notifications depuis les paramètres.",
 | 
				
			||||||
 | 
					    "Setup Notification": "Créer une notification",
 | 
				
			||||||
 | 
					    Light: "Clair",
 | 
				
			||||||
 | 
					    Dark: "Sombre",
 | 
				
			||||||
 | 
					    Auto: "Automatique",
 | 
				
			||||||
 | 
					    "Theme - Heartbeat Bar": "Voir les services monitorés",
 | 
				
			||||||
 | 
					    Normal: "Général",
 | 
				
			||||||
 | 
					    Bottom: "Au dessus",
 | 
				
			||||||
 | 
					    None: "Neutre",
 | 
				
			||||||
 | 
					    Timezone: "Fuseau Horaire",
 | 
				
			||||||
 | 
					    "Search Engine Visibility": "SEO",
 | 
				
			||||||
 | 
					    "Allow indexing": "Autoriser l'indexation par des moteurs de recherche",
 | 
				
			||||||
 | 
					    "Discourage search engines from indexing site": "Empêche les moteurs de recherche d'indexer votre site",
 | 
				
			||||||
 | 
					    "Change Password": "Changer le mot de passe",
 | 
				
			||||||
 | 
					    "Current Password": "Mot de passe actuel",
 | 
				
			||||||
 | 
					    "New Password": "Nouveau mot de passe",
 | 
				
			||||||
 | 
					    "Repeat New Password": "Répéter votre nouveau mot de passe",
 | 
				
			||||||
 | 
					    passwordNotMatchMsg: "Les mots de passe ne correspondent pas",
 | 
				
			||||||
 | 
					    "Update Password": "Mettre à jour le mot de passe",
 | 
				
			||||||
 | 
					    "Disable Auth": "Désactiver l'authentification intégrée",
 | 
				
			||||||
 | 
					    "Enable Auth": "Activer l'authentification",
 | 
				
			||||||
 | 
					    Logout: "Se déconnecter",
 | 
				
			||||||
 | 
					    notificationDescription: "Une fois ajoutée, vous devez l'activer manuellement dans les paramètres de vos hosts.",
 | 
				
			||||||
 | 
					    Leave: "Quitter",
 | 
				
			||||||
 | 
					    "I understand, please disable": "Je comprends, je l'ai désactivé",
 | 
				
			||||||
 | 
					    Confirm: "Confirmer",
 | 
				
			||||||
 | 
					    Yes: "Oui",
 | 
				
			||||||
 | 
					    No: "Non",
 | 
				
			||||||
 | 
					    Username: "Nom d'utilisateur",
 | 
				
			||||||
 | 
					    Password: "Mot de passe",
 | 
				
			||||||
 | 
					    "Remember me": "Se souvenir de moi",
 | 
				
			||||||
 | 
					    Login: "Se connecter",
 | 
				
			||||||
 | 
					    "No Monitors, please": "Pas de monitor, veuillez ",
 | 
				
			||||||
 | 
					    "add one": "en ajouter un.",
 | 
				
			||||||
 | 
					    "Notification Type": "Type de notification",
 | 
				
			||||||
 | 
					    "Email": "Email",
 | 
				
			||||||
 | 
					    "Test": "Tester",
 | 
				
			||||||
 | 
					    keywordDescription: "Le mot clé sera cherché dans la réponse HTML/JSON reçue du site internet.",
 | 
				
			||||||
 | 
					    "Certificate Info": "Des informations sur le certificat SSL",
 | 
				
			||||||
 | 
					    deleteMonitorMsg: "Êtes-vous sûr de vouloir supprimer ce monitor ?",
 | 
				
			||||||
 | 
					    deleteNotificationMsg: "Êtes-vous sûr de vouloir supprimer ce type de notifications ? Une fois désactivée, les services qui l'utilisent ne pourront plus envoyer de notifications.",
 | 
				
			||||||
 | 
					    "Resolver Server": "Serveur DNS utilisé",
 | 
				
			||||||
 | 
					    "Resource Record Type": "Type d'enregistrement DNS recherché",
 | 
				
			||||||
 | 
					    resoverserverDescription: "Le DNS de cloudflare est utilisé par défaut, mais vous pouvez le changer si vous le souhaitez.",
 | 
				
			||||||
 | 
					    rrtypeDescription: "Veuillez séléctionner un type d'enregistrement DNS",
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -40,19 +40,10 @@
 | 
				
			|||||||
        </header>
 | 
					        </header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <main>
 | 
					        <main>
 | 
				
			||||||
            <!-- Add :key to disable vue router re-use the same component -->
 | 
					            <router-view v-if="$root.loggedIn" />
 | 
				
			||||||
            <router-view v-if="$root.loggedIn" :key="$route.fullPath" />
 | 
					 | 
				
			||||||
            <Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
 | 
					            <Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
 | 
				
			||||||
        </main>
 | 
					        </main>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <footer>
 | 
					 | 
				
			||||||
            <div class="container-fluid">
 | 
					 | 
				
			||||||
                Uptime Kuma -
 | 
					 | 
				
			||||||
                {{ $t("Version") }}: {{ $root.info.version }} -
 | 
					 | 
				
			||||||
                <a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </footer>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <!-- Mobile Only -->
 | 
					        <!-- Mobile Only -->
 | 
				
			||||||
        <div v-if="$root.isMobile" style="width: 100%; height: 60px;" />
 | 
					        <div v-if="$root.isMobile" style="width: 100%; height: 60px;" />
 | 
				
			||||||
        <nav v-if="$root.isMobile" class="bottom-nav">
 | 
					        <nav v-if="$root.isMobile" class="bottom-nav">
 | 
				
			||||||
@@ -190,15 +181,6 @@ main {
 | 
				
			|||||||
    color: white;
 | 
					    color: white;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
footer {
 | 
					 | 
				
			||||||
    color: #aaa;
 | 
					 | 
				
			||||||
    font-size: 13px;
 | 
					 | 
				
			||||||
    margin-top: 10px;
 | 
					 | 
				
			||||||
    padding-bottom: 30px;
 | 
					 | 
				
			||||||
    margin-left: 10px;
 | 
					 | 
				
			||||||
    text-align: center;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.dark {
 | 
					.dark {
 | 
				
			||||||
    header {
 | 
					    header {
 | 
				
			||||||
        background-color: #161b22;
 | 
					        background-color: #161b22;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,6 +26,7 @@ import { appName } from "./util.ts";
 | 
				
			|||||||
import en from "./languages/en";
 | 
					import en from "./languages/en";
 | 
				
			||||||
import zhHK from "./languages/zh-HK";
 | 
					import zhHK from "./languages/zh-HK";
 | 
				
			||||||
import deDE from "./languages/de-DE";
 | 
					import deDE from "./languages/de-DE";
 | 
				
			||||||
 | 
					import fr from "./languages/fr";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const routes = [
 | 
					const routes = [
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@@ -92,6 +93,7 @@ const languageList = {
 | 
				
			|||||||
    en,
 | 
					    en,
 | 
				
			||||||
    "zh-HK": zhHK,
 | 
					    "zh-HK": zhHK,
 | 
				
			||||||
    "de-DE": deDE,
 | 
					    "de-DE": deDE,
 | 
				
			||||||
 | 
					    "fr": fr,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const i18n = createI18n({
 | 
					const i18n = createI18n({
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,11 +5,12 @@
 | 
				
			|||||||
                <div>
 | 
					                <div>
 | 
				
			||||||
                    <router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("Add New Monitor") }}</router-link>
 | 
					                    <router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("Add New Monitor") }}</router-link>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <MonitorList />
 | 
					                <MonitorList scrollbar="true" />
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <div class="col-12 col-md-7 col-xl-8">
 | 
					            <div class="col-12 col-md-7 col-xl-8 mb-3">
 | 
				
			||||||
                <router-view />
 | 
					                <!-- Add :key to disable vue router re-use the same component -->
 | 
				
			||||||
 | 
					                <router-view :key="$route.fullPath" />
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
@@ -26,7 +27,6 @@ export default {
 | 
				
			|||||||
    data() {
 | 
					    data() {
 | 
				
			||||||
        return {}
 | 
					        return {}
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@
 | 
				
			|||||||
                {{ $t("Quick Stats") }}
 | 
					                {{ $t("Quick Stats") }}
 | 
				
			||||||
            </h1>
 | 
					            </h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <div class="shadow-box big-padding text-center">
 | 
					            <div class="shadow-box big-padding text-center mb-4">
 | 
				
			||||||
                <div class="row">
 | 
					                <div class="row">
 | 
				
			||||||
                    <div class="col">
 | 
					                    <div class="col">
 | 
				
			||||||
                        <h3>{{ $t("Up") }}</h3>
 | 
					                        <h3>{{ $t("Up") }}</h3>
 | 
				
			||||||
@@ -170,7 +170,6 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.shadow-box {
 | 
					.shadow-box {
 | 
				
			||||||
    padding: 20px;
 | 
					    padding: 20px;
 | 
				
			||||||
    margin-top: 25px;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
table {
 | 
					table {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -507,4 +507,5 @@ table {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -155,6 +155,14 @@
 | 
				
			|||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <footer>
 | 
				
			||||||
 | 
					                <div class="container-fluid">
 | 
				
			||||||
 | 
					                    Uptime Kuma -
 | 
				
			||||||
 | 
					                    {{ $t("Version") }}: {{ $root.info.version }} -
 | 
				
			||||||
 | 
					                    <a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </footer>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <NotificationDialog ref="notificationDialog" />
 | 
					            <NotificationDialog ref="notificationDialog" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth">
 | 
					            <Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth">
 | 
				
			||||||
@@ -314,4 +322,12 @@ export default {
 | 
				
			|||||||
        color: #000;
 | 
					        color: #000;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					footer {
 | 
				
			||||||
 | 
					    color: #aaa;
 | 
				
			||||||
 | 
					    font-size: 13px;
 | 
				
			||||||
 | 
					    margin-top: 20px;
 | 
				
			||||||
 | 
					    padding-bottom: 30px;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user